Skip to content

Commit

Permalink
Merge pull request #260 from gabito1451/Multiplayer-mode
Browse files Browse the repository at this point in the history
Added: Multiplayer.jsx
  • Loading branch information
BigBen-7 authored Feb 28, 2025
2 parents 8ee4854 + dfc2625 commit 9abc493
Show file tree
Hide file tree
Showing 2,412 changed files with 109,624 additions and 156 deletions.
347 changes: 244 additions & 103 deletions backend/package-lock.json

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@
"handlebars": "^4.7.8",
"ioredis": "^5.5.0",
"jsonwebtoken": "^9.0.2",
"nodemailer": "^6.10.0",
"nodemailer-express-handlebars": "^7.0.0",
"passport": "^0.7.0",
"passport-jwt": "^4.0.1",
"passport-local": "^1.0.0",
Expand Down Expand Up @@ -180,4 +182,4 @@
"coverageDirectory": "../coverage",
"testEnvironment": "node"
}
}
}
22 changes: 22 additions & 0 deletions backend/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,28 @@ import { RetentionMetricsModule } from './retention-metrics/retention-metrics.mo
load: [envConfiguration],
validate,
}),
MailerModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: async (configService: ConfigService) => ({
transport: {
host: configService.get<string>('SMTP_HOST'),
port: configService.get<number>('SMTP_PORT'),
auth: {
user: configService.get<string>('SMTP_USER'),
pass: configService.get<string>('SMTP_PASS'),
},
},
defaults: {
from: `"No Reply" <${configService.get<string>('SMTP_FROM')}>`,
},
template: {
dir: join(__dirname, '../templates'), // Email templates location
adapter: new HandlebarsAdapter(),
options: { strict: true },
},
}),
}),
TypeOrmModule.forRoot({
type: 'postgres',
host: process.env.DB_HOST,
Expand Down
12 changes: 12 additions & 0 deletions frontend/app/(root)/multiplayer/page.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
"use client";
import React from "react";

import DewordleMultiplayer from "@/components/Multiplayer";

export default function page() {
return (
<div>
<DewordleMultiplayer />
</div>
);
}
299 changes: 299 additions & 0 deletions frontend/components/Multiplayer.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,299 @@
"use client";

import React from "react";

import { useState, useEffect } from "react";
import { Share2, RefreshCw } from "lucide-react";

export default function DewordleMultiplayer() {
const [gameState, setGameState] = useState("create");
const [player1, setPlayer1] = useState({
name: "Player 1",
guesses: [],
score: 0,
});
const [player2, setPlayer2] = useState({
name: "Player 2",
guesses: [],
score: 0,
});
const [currentPlayer, setCurrentPlayer] = useState(1);
const [word, setWord] = useState("REACT");
const [guess, setGuess] = useState("");
const [winner, setWinner] = useState(null);
const [showEmoji, setShowEmoji] = useState(false);
const [emoji, setEmoji] = useState("");

useEffect(() => {
if (gameState === "play") {
const timer = setInterval(() => {
simulateOpponentGuess();
}, 5000);
return () => clearInterval(timer);
}
}, [gameState]); // Removed unnecessary dependency: currentPlayer

const startGame = () => {
setGameState("play");
setPlayer1({ ...player1, guesses: [] });
setPlayer2({ ...player2, guesses: [] });
setCurrentPlayer(1);
setWinner(null);
};

const handleGuess = (e) => {
e.preventDefault();
if (guess.length !== 5) return;

const newGuess = guess.toUpperCase();
const currentPlayerObj = currentPlayer === 1 ? player1 : player2;
const updatedGuesses = [...currentPlayerObj.guesses, newGuess];
const score = calculateScore(newGuess);

if (currentPlayer === 1) {
setPlayer1({
...player1,
guesses: updatedGuesses,
score: player1.score + score,
});
} else {
setPlayer2({
...player2,
guesses: updatedGuesses,
score: player2.score + score,
});
}

setGuess("");
checkGameEnd(newGuess, updatedGuesses.length);
switchPlayer();
};

const calculateScore = (guess) => {
let score = 0;
for (let i = 0; i < 5; i++) {
if (guess[i] === word[i]) {
score += 2;
} else if (word.includes(guess[i])) {
score += 1;
}
}
return score;
};

const checkGameEnd = (guess, guessCount) => {
if (guess === word || guessCount === 6) {
const winner =
player1.score > player2.score ? player1.name : player2.name;
setWinner(winner);
setGameState("results");
}
};

const switchPlayer = () => {
setCurrentPlayer(currentPlayer === 1 ? 2 : 1);
};

const simulateOpponentGuess = () => {
if (currentPlayer === 2 && gameState === "play") {
const opponentGuess = generateRandomGuess();
const updatedGuesses = [...player2.guesses, opponentGuess];
const score = calculateScore(opponentGuess);
setPlayer2({
...player2,
guesses: updatedGuesses,
score: player2.score + score,
});
checkGameEnd(opponentGuess, updatedGuesses.length);
switchPlayer();
showReactionEmoji();
}
};

const generateRandomGuess = () => {
const letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
return Array(5)
.fill()
.map(() => letters[Math.floor(Math.random() * 26)])
.join("");
};

const showReactionEmoji = () => {
const emojis = ["🔥", "😂", "😱", "🤔", "👍", "🎉"];
setEmoji(emojis[Math.floor(Math.random() * emojis.length)]);
setShowEmoji(true);
setTimeout(() => setShowEmoji(false), 2000);
};

const shareResults = () => {
const text = `I just ${
winner === player1.name ? "won" : "played"
} a Dewordle multiplayer game!\n${player1.name}: ${player1.score} pts\n${
player2.name
}: ${player2.score} pts\nCan you beat me? Play at: example.com/dewordle`;
if (navigator.share) {
navigator.share({ title: "Dewordle Multiplayer Results", text: text });
} else {
navigator.clipboard.writeText(text);
alert("Results copied to clipboard!");
}
};

const renderGameBoard = (playerObj) => (
<div className="mb-4">
<h3 className="text-lg font-semibold mb-2">{playerObj.name}</h3>
<div className="grid grid-cols-5 gap-1">
{Array(6)
.fill()
.map((_, rowIndex) => (
<React.Fragment key={rowIndex}>
{Array(5)
.fill()
.map((_, colIndex) => {
const letter = playerObj.guesses[rowIndex]?.[colIndex] || "";
let bgColor = "bg-gray-200 dark:bg-gray-700";
if (letter) {
if (letter === word[colIndex]) {
bgColor = "bg-green-500";
} else if (word.includes(letter)) {
bgColor = "bg-yellow-500";
} else {
bgColor = "bg-gray-400 dark:bg-gray-600";
}
}
return (
<div
key={colIndex}
className={`w-10 h-10 ${bgColor} flex items-center justify-center text-white font-bold rounded`}
>
{letter}
</div>
);
})}
</React.Fragment>
))}
</div>
</div>
);

return (
<div className="min-h-screen bg-gray-100 dark:bg-gray-900 flex flex-col items-center justify-center p-4">
<div className="w-full max-w-2xl bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6">
<h1 className="text-2xl font-bold text-center text-gray-900 dark:text-white mb-6">
Dewordle Multiplayer
</h1>

{gameState === "create" && (
<div className="text-center">
<h2 className="text-xl font-semibold mb-4">Challenge a Friend</h2>
<p className="mb-4 text-gray-600 dark:text-gray-400">
Start a new game and challenge your friend to solve the word!
</p>
<button
onClick={startGame}
className="px-6 py-3 bg-indigo-600 hover:bg-indigo-700 text-white font-medium rounded-lg transition-colors"
>
Start New Game
</button>
</div>
)}

{gameState === "play" && (
<div>
<div className="flex justify-between items-center mb-4">
<div className="text-lg font-semibold">
Current Turn:{" "}
{currentPlayer === 1 ? player1.name : player2.name}
</div>
<div className="flex items-center space-x-2">
<span className="font-medium">
{player1.name}: {player1.score}
</span>
<span className="text-gray-500">vs</span>
<span className="font-medium">
{player2.name}: {player2.score}
</span>
</div>
</div>

<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
{renderGameBoard(player1)}
{renderGameBoard(player2)}
</div>

{currentPlayer === 1 && (
<form onSubmit={handleGuess} className="mb-4">
<div className="flex space-x-2">
<input
type="text"
value={guess}
onChange={(e) => setGuess(e.target.value.toUpperCase())}
maxLength={5}
className="flex-grow px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500"
placeholder="Enter your guess"
/>
<button
type="submit"
className="px-4 py-2 bg-indigo-600 hover:bg-indigo-700 text-white font-medium rounded-lg transition-colors"
>
Guess
</button>
</div>
</form>
)}

{currentPlayer === 2 && (
<div className="text-center text-gray-600 dark:text-gray-400 mb-4">
Waiting for {player2.name} to make a guess...
</div>
)}

{showEmoji && (
<div className="fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 text-6xl animate-bounce">
{emoji}
</div>
)}
</div>
)}

{gameState === "results" && (
<div className="text-center">
<h2 className="text-2xl font-bold mb-4">
{winner === player1.name ? "You Won!" : "Game Over!"}
</h2>
<div className="flex justify-center items-center space-x-4 mb-6">
<div className="text-center">
<p className="font-semibold">{player1.name}</p>
<p className="text-2xl font-bold">{player1.score}</p>
</div>
<div className="text-4xl font-bold text-gray-400">vs</div>
<div className="text-center">
<p className="font-semibold">{player2.name}</p>
<p className="text-2xl font-bold">{player2.score}</p>
</div>
</div>
<p className="mb-6 text-gray-600 dark:text-gray-400">
The word was: <span className="font-bold">{word}</span>
</p>
<div className="flex justify-center space-x-4">
<button
onClick={startGame}
className="flex items-center px-4 py-2 bg-indigo-600 hover:bg-indigo-700 text-white font-medium rounded-lg transition-colors"
>
<RefreshCw className="w-5 h-5 mr-2" />
Rematch
</button>
<button
onClick={shareResults}
className="flex items-center px-4 py-2 bg-green-600 hover:bg-green-700 text-white font-medium rounded-lg transition-colors"
>
<Share2 className="w-5 h-5 mr-2" />
Share Results
</button>
</div>
</div>
)}
</div>
</div>
);
}
Loading

0 comments on commit 9abc493

Please sign in to comment.