-
Notifications
You must be signed in to change notification settings - Fork 14.9k
feat: add multiplayer memory game functionality with UI improvements #1657
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
|
@microsoft-github-policy-service agree |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR introduces a multiplayer memory game feature with both online (Socket.io-based) and local 2-player modes. The implementation includes a Node.js backend server for real-time game synchronization, enhanced UI with Tailwind CSS, and comprehensive game state management across multiple players.
Key Changes:
- Full multiplayer support with Socket.io for real-time game synchronization between two players
- Local 2-player mode with turn-based gameplay and independent scoring
- Backend server implementation for managing game state, room creation, and player synchronization
Reviewed changes
Copilot reviewed 10 out of 12 changed files in this pull request and generated 18 comments.
Show a summary per file
| File | Description |
|---|---|
| memory-game/package.json | Adds socket.io-client dependency for frontend WebSocket communication |
| memory-game/package-lock.json | Lockfile updates for new socket.io-client and related dependencies |
| memory-game/components/socket.js | Socket.io client configuration with connection event handlers |
| memory-game/components/MemoryGameMultiplayer.jsx | Multiplayer game component with real-time state synchronization |
| memory-game/components/MemoryGame.jsx | Enhanced local 2-player mode with scoring and turn management |
| memory-game/backend/server.js | Express + Socket.io server for managing multiplayer game rooms and state |
| memory-game/backend/package.json | Backend dependencies including socket.io, express, and cors |
| memory-game/backend/package-lock.json | Backend dependency lockfile |
| memory-game/app/room/[roomId]/page.jsx | Dynamic route for multiplayer game rooms |
| memory-game/app/page.js | Updated home page with navigation to multiplayer lobby |
| memory-game/app/lobby/page.jsx | Lobby interface for creating/joining multiplayer rooms |
| memory-game/app/layout.js | Simplified root layout with updated metadata |
Files not reviewed (2)
- memory-game/backend/package-lock.json: Language not supported
- memory-game/package-lock.json: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| socket.on("join_room", ({ roomId }) => { | ||
| console.log("👥 join_room:", roomId); | ||
| const room = gameState[roomId]; | ||
| if (!room) { | ||
| console.log("⚠ join_room: room not found", roomId); | ||
| return; | ||
| } | ||
|
|
||
| socket.join(roomId); | ||
| socket.emit("sync_full_state", room); | ||
| }); |
Copilot
AI
Dec 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The server lacks input validation for room IDs and other user inputs. Malicious users could send invalid data or excessively large room IDs. Add validation to check that roomId exists, has a reasonable length, and matches expected format before processing socket events.
| // import MemoryGame from '@/components/MemoryGame' | ||
| // import React from 'react' | ||
|
|
||
| const page = () => { | ||
| // const page = () => { | ||
| // return ( | ||
| // <div> | ||
| // <MemoryGame /> | ||
| // </div> | ||
| // ) | ||
| // } | ||
|
|
||
| // export default page |
Copilot
AI
Dec 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Commented-out code sections should be removed. Keep only the active implementation to improve code readability.
| // "use client"; | ||
| // import { useParams } from "next/navigation"; | ||
| // import MemoryGameMultiplayer from "../../../components/MemoryGameMultiplayer"; | ||
|
|
||
| // export default function RoomPage() { | ||
| // const { roomId } = useParams(); | ||
|
|
||
| // return <MemoryGameMultiplayer roomId={roomId} />; | ||
| // } | ||
|
|
||
| // app/room/[roomId]/page.jsx |
Copilot
AI
Dec 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The commented-out code in this file should be removed for better maintainability.
| // "use client"; | |
| // import { useParams } from "next/navigation"; | |
| // import MemoryGameMultiplayer from "../../../components/MemoryGameMultiplayer"; | |
| // export default function RoomPage() { | |
| // const { roomId } = useParams(); | |
| // return <MemoryGameMultiplayer roomId={roomId} />; | |
| // } | |
| // app/room/[roomId]/page.jsx |
| socket.on("match", ({ roomId, pair, scorer }) => { | ||
| const room = gameState[roomId]; | ||
| if (!room) return; | ||
|
|
||
| room.matched.push(...pair); | ||
|
|
||
| if (!room.scores) { | ||
| room.scores = { P1: 0, P2: 0 }; | ||
| } | ||
| if (scorer === "P1" || scorer === "P2") { | ||
| room.scores[scorer] += 1; | ||
| } | ||
|
|
||
| console.log( | ||
| "✅ match in room", | ||
| roomId, | ||
| "matched:", | ||
| room.matched, | ||
| "scores:", | ||
| room.scores | ||
| ); | ||
|
|
||
| io.to(roomId).emit("match_update", { | ||
| matched: room.matched, | ||
| scores: room.scores, | ||
| }); | ||
| }); |
Copilot
AI
Dec 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The match event handler doesn't validate that the pair array contains exactly 2 elements or that the cardId values are within valid bounds. A malicious client could send invalid data causing the matched array to contain incorrect values. Add validation to check the pair is an array of length 2 with valid card IDs.
| localStorage.setItem("playerRole", "P1"); | ||
| socket.emit("create_room", { roomId: id, gridSize }); | ||
| router.push(`/room/${id}`); | ||
| }; | ||
|
|
||
| const joinRoom = () => { | ||
| if (!roomId) return; | ||
| localStorage.setItem("playerRole", "P2"); |
Copilot
AI
Dec 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The localStorage access in the lobby assumes the operation will succeed, but localStorage can throw exceptions if it's disabled, full, or in private browsing mode. Wrap localStorage calls in try-catch blocks to handle potential errors gracefully and provide user feedback if the operation fails.
| localStorage.setItem("playerRole", "P1"); | |
| socket.emit("create_room", { roomId: id, gridSize }); | |
| router.push(`/room/${id}`); | |
| }; | |
| const joinRoom = () => { | |
| if (!roomId) return; | |
| localStorage.setItem("playerRole", "P2"); | |
| try { | |
| if (typeof window !== "undefined" && window.localStorage) { | |
| window.localStorage.setItem("playerRole", "P1"); | |
| } | |
| } catch (err) { | |
| console.error("Failed to access localStorage when creating room:", err); | |
| alert( | |
| "We couldn't save your player role due to your browser's storage settings. You can still join the room, but your role might not be remembered." | |
| ); | |
| } | |
| socket.emit("create_room", { roomId: id, gridSize }); | |
| router.push(`/room/${id}`); | |
| }; | |
| const joinRoom = () => { | |
| if (!roomId) return; | |
| try { | |
| if (typeof window !== "undefined" && window.localStorage) { | |
| window.localStorage.setItem("playerRole", "P2"); | |
| } | |
| } catch (err) { | |
| console.error("Failed to access localStorage when joining room:", err); | |
| alert( | |
| "We couldn't save your player role due to your browser's storage settings. You can still join the room, but your role might not be remembered." | |
| ); | |
| } |
| const router = useRouter(); | ||
|
|
||
| const createRoom = () => { | ||
| const id = Math.floor(1000 + Math.random() * 9000).toString(); |
Copilot
AI
Dec 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The room ID generation uses a simple 4-digit random number, which has only 9000 possible values. This creates a high collision probability and makes rooms easy to guess. Consider using a more robust ID generation method like UUID or a longer random string to ensure uniqueness and improve security.
| const id = Math.floor(1000 + Math.random() * 9000).toString(); | |
| const id = | |
| typeof crypto !== "undefined" && typeof crypto.randomUUID === "function" | |
| ? crypto.randomUUID() | |
| : Math.random().toString(36).slice(2, 10); |
| const app = express(); | ||
| app.use(cors()); | ||
| const server = http.createServer(app); | ||
|
|
||
| const io = new Server(server, { | ||
| cors: { | ||
| origin: "*", |
Copilot
AI
Dec 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The CORS configuration allows all origins ("*"), which is a security risk in production. This should be restricted to only allow requests from your application's domain. Consider using environment variables to configure allowed origins based on the deployment environment.
| const app = express(); | |
| app.use(cors()); | |
| const server = http.createServer(app); | |
| const io = new Server(server, { | |
| cors: { | |
| origin: "*", | |
| const allowedOrigins = process.env.ALLOWED_ORIGINS | |
| ? process.env.ALLOWED_ORIGINS.split(",").map((origin) => origin.trim()) | |
| : ["http://localhost:3000"]; | |
| const app = express(); | |
| app.use( | |
| cors({ | |
| origin: allowedOrigins, | |
| }) | |
| ); | |
| const server = http.createServer(app); | |
| const io = new Server(server, { | |
| cors: { | |
| origin: allowedOrigins, |
| }); | ||
|
|
||
| // In-memory game state | ||
| const gameState = {}; |
Copilot
AI
Dec 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The gameState object grows unbounded as rooms are created but never cleaned up when rooms are abandoned or games end. This will cause a memory leak over time. Consider implementing a cleanup mechanism to remove inactive rooms after a timeout period or when both players disconnect.
| // "use client"; | ||
| // import React, { useEffect, useState, useCallback } from "react"; | ||
| // // Fisher-Yates shuffle for unbiased randomization | ||
| // function fisherYatesShuffle(array) { | ||
| // const arr = array.slice(); | ||
| // for (let i = arr.length - 1; i > 0; i--) { | ||
| // const j = Math.floor(Math.random() * (i + 1)); | ||
| // [arr[i], arr[j]] = [arr[j], arr[i]]; | ||
| // } | ||
| // return arr; | ||
| // } | ||
| // const MemoryGame = () => { | ||
| // const [gridSize, setGridSize] = useState(2); | ||
| // const [array, setArray] = useState([]); | ||
| // const [flipped, setFlipped] = useState([]); | ||
| // const [selectedPairs, setSelectedPairs] = useState([]); | ||
| // const [disabled, setDisabled] = useState(false); | ||
| // const [error, setError] = useState(""); | ||
| // const [won, setWon] = useState(false); | ||
| // const handleGridSize = (e) => { | ||
| // const size = parseInt(e.target.value); | ||
| // if (2 <= size && size <= 10 && (size % 2 === 0)) { | ||
| // setGridSize(size); | ||
| // setError(""); | ||
| // } else { | ||
| // setError("Please enter a grid size where size is even (e.g., 2, 4, 6, 8, 10)"); | ||
| // } | ||
| // }; | ||
| // const initializeGame = useCallback(() => { | ||
| // const totalCards = gridSize * gridSize; | ||
| // const pairCount = totalCards / 2; | ||
| // const numbers = [...Array(pairCount).keys()].map((n) => n + 1); | ||
| // const cardNumbers = [...numbers, ...numbers]; | ||
| // const shuffledCardNumbers = fisherYatesShuffle(cardNumbers); | ||
| // const shuffledCards = shuffledCardNumbers.map((number, index) => ({ | ||
| // id: index, | ||
| // number, | ||
| // })); | ||
| // setArray(shuffledCards); | ||
| // setFlipped([]); | ||
| // setSelectedPairs([]); | ||
| // setDisabled(false); | ||
| // setWon(false); | ||
| // }, [gridSize]); | ||
| // useEffect(() => { | ||
| // initializeGame(); | ||
| // }, [initializeGame]); | ||
| // const handleMatch = (secondId) => { | ||
| // const [firstId] = flipped; | ||
| // if (array[firstId].number === array[secondId].number) { | ||
| // setSelectedPairs([...selectedPairs, firstId, secondId]); | ||
| // setFlipped([]); | ||
| // setDisabled(false); | ||
| // } else { | ||
| // setTimeout(() => { | ||
| // setDisabled(false); | ||
| // setFlipped([]); | ||
| // }, 1000); | ||
| // } | ||
| // }; | ||
| // const handleClick = (id) => { | ||
| // if (disabled || won) return; | ||
|
|
||
| // if (flipped.length === 0) { | ||
| // setFlipped([id]); | ||
| // return; | ||
| // } | ||
|
|
||
| // if (flipped.length === 1) { | ||
| // setDisabled(true); | ||
| // if (id !== flipped[0]) { | ||
| // setFlipped([...flipped, id]); | ||
| // handleMatch(id); | ||
| // } else { | ||
| // setFlipped([]); | ||
| // setDisabled(false); | ||
| // } | ||
| // } | ||
| // }; | ||
| // const isFlipped = (id) => flipped.includes(id) || selectedPairs.includes(id); | ||
| // const isSelectedPairs = (id) => selectedPairs.includes(id); | ||
| // useEffect(() => { | ||
| // if (selectedPairs.length === array.length && array.length > 0) { | ||
| // setWon(true); | ||
| // } | ||
| // }, [selectedPairs, array]); | ||
| // return ( | ||
| // <div className="h-screen flex flex-col justify-center items-center p-4 bg-gray-100 "> | ||
| // {/* Heading */} | ||
| // <h1 className="text-3xl font-bold mb-6">Memory Game</h1> | ||
| // {/* Grid Size */} | ||
| // <div className="mb-4"> | ||
| // <label htmlFor="gridSize">Grid Size: (max 10)</label> | ||
| // <input | ||
| // type="number" | ||
| // className="w-[50px] ml-3 rounded border-2 px-1.5 py-1" | ||
| // min="2" | ||
| // max="10" | ||
| // value={gridSize} | ||
| // onChange={handleGridSize} | ||
| // /> | ||
| // {error && ( | ||
| // <div className="text-sm text-red-500 mt-2">{error}</div> | ||
| // )} | ||
| // </div> | ||
| // {/* Cards */} | ||
| // <div | ||
| // className="grid gap-2 mb-4" | ||
| // style={{ | ||
| // gridTemplateColumns: `repeat(${gridSize}, minmax(0,1fr))`, | ||
| // width: `min(100%,${gridSize * 5.5}rem)`, | ||
| // }} | ||
| // > | ||
| // {array.map((card) => ( | ||
| // <div | ||
| // key={card.id} | ||
| // onClick={() => handleClick(card.id)} | ||
| // className={`aspect-square flex items-center justify-center text-xl transition-all duration-300 font-bold rounded-lg cursor-pointer ${ | ||
| // isFlipped(card.id) | ||
| // ? isSelectedPairs(card.id) | ||
| // ? "bg-green-500 text-white" | ||
| // : "bg-blue-500 text-white" | ||
| // : "bg-gray-300 text-gray-400" | ||
| // }`} | ||
| // > | ||
| // {isFlipped(card.id) ? card.number : "?"} | ||
| // </div> | ||
| // ))} | ||
| // </div> | ||
| // {/* Result */} | ||
| // <div className="text-2xl text-green-500 font-bold"> | ||
| // {won ? "You Won!" : ""} | ||
| // </div> | ||
|
|
||
| // {/* Reset Button */} | ||
| // <button | ||
| // className="px-5 py-2 bg-green-500 rounded text-white mt-5" | ||
| // onClick={initializeGame} | ||
| // > | ||
| // Reset | ||
| // </button> | ||
| // </div> | ||
| // ); | ||
| // }; | ||
| // export default MemoryGame; | ||
|
|
||
| // "use client"; | ||
| // import React, { useEffect, useState, useCallback } from "react"; | ||
| // // Fisher-Yates Shuffle | ||
| // function fisherYatesShuffle(array) { | ||
| // const arr = array.slice(); | ||
| // for (let i = arr.length - 1; i > 0; i--) { | ||
| // const j = Math.floor(Math.random() * (i + 1)); | ||
| // [arr[i], arr[j]] = [arr[j], arr[i]]; | ||
| // } | ||
| // return arr; | ||
| // } | ||
| // const MemoryGame = () => { | ||
| // const [gridSize, setGridSize] = useState(4); | ||
| // const [inputSize, setInputSize] = useState("4"); // FIXED input handling | ||
| // const [cards, setCards] = useState([]); | ||
| // const [flipped, setFlipped] = useState([]); | ||
| // const [matched, setMatched] = useState([]); | ||
| // const [disabled, setDisabled] = useState(false); | ||
| // const [error, setError] = useState(""); | ||
| // const [won, setWon] = useState(false); | ||
| // // Handle user typing | ||
| // const handleGridSizeInput = (e) => { | ||
| // const value = e.target.value; | ||
| // setInputSize(value); | ||
| // const size = Number(value); | ||
| // if (size >= 2 && size <= 10 && size % 2 === 0) { | ||
| // setGridSize(size); | ||
| // setError(""); | ||
| // } else { | ||
| // setError("Even numbers only (2,4,6,8,10)"); | ||
| // } | ||
| // }; | ||
| // // Initialize game | ||
| // const initializeGame = useCallback(() => { | ||
| // const total = gridSize * gridSize; | ||
| // const pairCount = total / 2; | ||
| // const nums = [...Array(pairCount).keys()].map((n) => n + 1); | ||
| // const shuffled = fisherYatesShuffle([...nums, ...nums]).map( | ||
| // (num, index) => ({ | ||
| // id: index, | ||
| // number: num, | ||
| // }) | ||
| // ); | ||
| // setCards(shuffled); | ||
| // setFlipped([]); | ||
| // setMatched([]); | ||
| // setDisabled(false); | ||
| // setWon(false); | ||
| // }, [gridSize]); | ||
| // useEffect(() => initializeGame(), [initializeGame]); | ||
| // // Card click logic | ||
| // const handleClick = (id) => { | ||
| // if (disabled || flipped.includes(id) || matched.includes(id)) return; | ||
| // const newFlipped = [...flipped, id]; | ||
| // setFlipped(newFlipped); | ||
| // if (newFlipped.length === 2) { | ||
| // setDisabled(true); | ||
| // const [a, b] = newFlipped; | ||
| // if (cards[a].number === cards[b].number) { | ||
| // setMatched((prev) => [...prev, a, b]); | ||
| // setFlipped([]); | ||
| // setDisabled(false); | ||
| // } else { | ||
| // setTimeout(() => { | ||
| // setFlipped([]); | ||
| // setDisabled(false); | ||
| // }, 800); | ||
| // } | ||
| // } | ||
| // }; | ||
| // // Win checker | ||
| // useEffect(() => { | ||
| // if (matched.length === cards.length && cards.length !== 0) { | ||
| // setWon(true); | ||
| // } | ||
| // }, [matched, cards]); | ||
| // return ( | ||
| // <div className="min-h-screen flex flex-col items-center py-10 bg-gradient-to-br from-gray-100 to-gray-300"> | ||
| // <h1 className="text-4xl font-extrabold mb-6 text-slate-800 tracking-wide"> | ||
| // Memory Match | ||
| // </h1> | ||
| // {/* Grid Size Input */} | ||
| // <div className="mb-4 text-lg"> | ||
| // <label className="font-semibold text-slate-700">Grid Size:</label> | ||
| // <input | ||
| // type="number" | ||
| // value={inputSize} | ||
| // onChange={handleGridSizeInput} | ||
| // min="2" | ||
| // max="10" | ||
| // className="ml-3 w-20 px-3 py-1 border rounded-lg shadow bg-white" | ||
| // /> | ||
| // {error && <p className="mt-2 text-red-600 text-sm">{error}</p>} | ||
| // </div> | ||
| // {/* Game Grid */} | ||
| // <div | ||
| // className="grid gap-3 mx-auto transition-all duration-300" | ||
| // style={{ | ||
| // gridTemplateColumns: `repeat(${gridSize}, 1fr)`, | ||
| // width: `${gridSize * 4.2}rem`, | ||
| // maxWidth: "95vw", | ||
| // }} | ||
| // > | ||
| // {cards.map((card) => { | ||
| // const open = flipped.includes(card.id) || matched.includes(card.id); | ||
| // return ( | ||
| // <div | ||
| // key={card.id} | ||
| // onClick={() => handleClick(card.id)} | ||
| // className={` | ||
| // aspect-square flex items-center justify-center rounded-xl | ||
| // text-xl font-bold shadow-md cursor-pointer transition-all | ||
| // ${ | ||
| // matched.includes(card.id) | ||
| // ? "bg-emerald-500 text-white scale-105" | ||
| // : open | ||
| // ? "bg-blue-600 text-white" | ||
| // : "bg-slate-300 text-slate-500 hover:bg-slate-400" | ||
| // } | ||
| // `} | ||
| // > | ||
| // {open ? card.number : "?"} | ||
| // </div> | ||
| // ); | ||
| // })} | ||
| // </div> | ||
| // {/* Win message */} | ||
| // {won && ( | ||
| // <p className="text-3xl mt-6 font-bold text-emerald-600 animate-bounce"> | ||
| // 🎉 You Won! 🎉 | ||
| // </p> | ||
| // )} | ||
| // {/* Reset */} | ||
| // <button | ||
| // onClick={initializeGame} | ||
| // className="mt-6 px-6 py-2 text-lg bg-blue-700 text-white rounded-lg shadow hover:bg-blue-800" | ||
| // > | ||
| // Reset Game | ||
| // </button> | ||
| // </div> | ||
| // ); | ||
| // }; | ||
|
|
||
| // export default MemoryGame; | ||
|
|
||
| // "use client"; | ||
| // import React, { useEffect, useState, useCallback } from "react"; | ||
|
|
||
| // // Fisher-Yates shuffle | ||
| // function fisherYatesShuffle(array) { | ||
| // const arr = array.slice(); | ||
| // for (let i = arr.length - 1; i > 0; i--) { | ||
| // const j = Math.floor(Math.random() * (i + 1)); | ||
| // [arr[i], arr[j]] = [arr[j], arr[i]]; | ||
| // } | ||
| // return arr; | ||
| // } | ||
|
|
||
| // const MemoryGame = () => { | ||
| // const [gridSize, setGridSize] = useState(4); | ||
| // const [inputSize, setInputSize] = useState("4"); | ||
| // const [cards, setCards] = useState([]); | ||
| // const [flipped, setFlipped] = useState([]); | ||
| // const [matched, setMatched] = useState([]); | ||
| // const [disabled, setDisabled] = useState(false); | ||
| // const [error, setError] = useState(""); | ||
| // const [won, setWon] = useState(false); | ||
|
|
||
| // // 🔹 NEW: multiplayer state | ||
| // const [currentPlayer, setCurrentPlayer] = useState(1); // 1 or 2 | ||
| // const [scores, setScores] = useState({ 1: 0, 2: 0 }); | ||
| // const [winnerText, setWinnerText] = useState(""); | ||
|
|
||
| // // Handle grid size input (same as before, but stored separately) | ||
| // const handleGridSizeInput = (e) => { | ||
| // const value = e.target.value; | ||
| // setInputSize(value); | ||
|
|
||
| // const size = Number(value); | ||
| // if (size >= 2 && size <= 10 && size % 2 === 0) { | ||
| // setGridSize(size); | ||
| // setError(""); | ||
| // } else { | ||
| // setError("Even numbers only (2,4,6,8,10)"); | ||
| // } | ||
| // }; | ||
|
|
||
| // // Initialize / reset game | ||
| // const initializeGame = useCallback(() => { | ||
| // const total = gridSize * gridSize; | ||
| // const pairCount = total / 2; | ||
|
|
||
| // const nums = [...Array(pairCount).keys()].map((n) => n + 1); | ||
| // const shuffled = fisherYatesShuffle([...nums, ...nums]).map( | ||
| // (num, index) => ({ | ||
| // id: index, | ||
| // number: num, | ||
| // }) | ||
| // ); | ||
|
|
||
| // setCards(shuffled); | ||
| // setFlipped([]); | ||
| // setMatched([]); | ||
| // setDisabled(false); | ||
| // setWon(false); | ||
|
|
||
| // // reset multiplayer stuff | ||
| // setCurrentPlayer(1); | ||
| // setScores({ 1: 0, 2: 0 }); | ||
| // setWinnerText(""); | ||
| // }, [gridSize]); | ||
|
|
||
| // useEffect(() => { | ||
| // initializeGame(); | ||
| // }, [initializeGame]); | ||
|
|
||
| // // Handle card click | ||
| // const handleClick = (id) => { | ||
| // if (disabled) return; | ||
| // if (flipped.includes(id) || matched.includes(id)) return; | ||
|
|
||
| // const newFlipped = [...flipped, id]; | ||
| // setFlipped(newFlipped); | ||
|
|
||
| // if (newFlipped.length === 2) { | ||
| // setDisabled(true); | ||
| // const [a, b] = newFlipped; | ||
| // const isMatch = cards[a].number === cards[b].number; | ||
|
|
||
| // if (isMatch) { | ||
| // // Add to matched | ||
| // setMatched((prev) => { | ||
| // const updated = [...prev, a, b]; | ||
|
|
||
| // // update score for current player | ||
| // setScores((prevScores) => ({ | ||
| // ...prevScores, | ||
| // [currentPlayer]: prevScores[currentPlayer] + 1, | ||
| // })); | ||
|
|
||
| // return updated; | ||
| // }); | ||
|
|
||
| // // Clear flipped and allow same player to continue | ||
| // setTimeout(() => { | ||
| // setFlipped([]); | ||
| // setDisabled(false); | ||
| // }, 500); | ||
| // } else { | ||
| // // No match: switch to other player after small delay | ||
| // setTimeout(() => { | ||
| // setFlipped([]); | ||
| // setDisabled(false); | ||
| // setCurrentPlayer((p) => (p === 1 ? 2 : 1)); | ||
| // }, 800); | ||
| // } | ||
| // } | ||
| // }; | ||
|
|
||
| // // Win + winner calculation | ||
| // useEffect(() => { | ||
| // if (matched.length === cards.length && cards.length !== 0) { | ||
| // setWon(true); | ||
| // if (scores[1] > scores[2]) { | ||
| // setWinnerText("Player 1 Wins! 🎉"); | ||
| // } else if (scores[2] > scores[1]) { | ||
| // setWinnerText("Player 2 Wins! 🎉"); | ||
| // } else { | ||
| // setWinnerText("It's a Tie! 🤝"); | ||
| // } | ||
| // } | ||
| // }, [matched, cards.length, scores]); | ||
|
|
||
| // const isOpen = (id) => flipped.includes(id) || matched.includes(id); | ||
|
|
||
| // return ( | ||
| // <div className="min-h-screen flex flex-col items-center py-10 bg-gradient-to-br from-gray-100 to-gray-300"> | ||
|
|
||
| // <h1 className="text-4xl font-extrabold mb-4 text-slate-800 tracking-wide"> | ||
| // Memory Match – 2 Player | ||
| // </h1> | ||
|
|
||
| // {/* Scores + current player */} | ||
| // <div className="flex gap-6 mb-4 text-lg"> | ||
| // <div className={`px-4 py-2 rounded-lg shadow bg-white`}> | ||
| // <span className="font-semibold">Player 1:</span>{" "} | ||
| // <span className="font-bold text-blue-700">{scores[1]}</span> | ||
| // </div> | ||
| // <div className={`px-4 py-2 rounded-lg shadow bg-white`}> | ||
| // <span className="font-semibold">Player 2:</span>{" "} | ||
| // <span className="font-bold text-emerald-700">{scores[2]}</span> | ||
| // </div> | ||
| // </div> | ||
|
|
||
| // <div className="mb-4 text-lg"> | ||
| // <span className="font-semibold text-slate-700">Current Turn: </span> | ||
| // <span | ||
| // className={`font-bold ${ | ||
| // currentPlayer === 1 ? "text-blue-700" : "text-emerald-700" | ||
| // }`} | ||
| // > | ||
| // Player {currentPlayer} | ||
| // </span> | ||
| // </div> | ||
|
|
||
| // {/* Grid Size Input */} | ||
| // <div className="mb-4 text-lg"> | ||
| // <label className="font-semibold text-slate-700">Grid Size:</label> | ||
| // <input | ||
| // type="number" | ||
| // value={inputSize} | ||
| // onChange={handleGridSizeInput} | ||
| // min="2" | ||
| // max="10" | ||
| // className="ml-3 w-20 px-3 py-1 border rounded-lg shadow bg-white" | ||
| // /> | ||
| // {error && <p className="mt-2 text-red-600 text-sm">{error}</p>} | ||
| // </div> | ||
|
|
||
| // {/* Game Grid */} | ||
| // <div | ||
| // className="grid gap-3 mx-auto transition-all duration-300" | ||
| // style={{ | ||
| // gridTemplateColumns: `repeat(${gridSize}, 1fr)`, | ||
| // width: `${gridSize * 4.2}rem`, | ||
| // maxWidth: "95vw", | ||
| // }} | ||
| // > | ||
| // {cards.map((card) => ( | ||
| // <div | ||
| // key={card.id} | ||
| // onClick={() => handleClick(card.id)} | ||
| // className={` | ||
| // aspect-square flex items-center justify-center rounded-xl | ||
| // text-xl font-bold shadow-md cursor-pointer transition-all | ||
| // ${ | ||
| // matched.includes(card.id) | ||
| // ? "bg-emerald-500 text-white scale-105" | ||
| // : isOpen(card.id) | ||
| // ? "bg-blue-600 text-white" | ||
| // : "bg-slate-300 text-slate-500 hover:bg-slate-400" | ||
| // } | ||
| // `} | ||
| // > | ||
| // {isOpen(card.id) ? card.number : "?"} | ||
| // </div> | ||
| // ))} | ||
| // </div> | ||
|
|
||
| // {/* Win / Result */} | ||
| // {won && ( | ||
| // <p className="text-2xl mt-6 font-bold text-emerald-700"> | ||
| // {winnerText} | ||
| // </p> | ||
| // )} | ||
|
|
||
| // {/* Reset */} | ||
| // <button | ||
| // onClick={initializeGame} | ||
| // className="mt-6 px-6 py-2 text-lg bg-blue-700 text-white rounded-lg shadow hover:bg-blue-800" | ||
| // > | ||
| // Reset Game | ||
| // </button> | ||
| // </div> | ||
| // ); | ||
| // }; | ||
|
|
||
| // export default MemoryGame; |
Copilot
AI
Dec 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This file contains over 500 lines of commented-out code (lines 1-514). Commented code should be removed from the codebase as it clutters the file, makes it harder to maintain, and can be recovered from version control history if needed. Consider removing all the commented sections and keeping only the active implementation.
| // import { Geist, Geist_Mono } from "next/font/google"; | ||
| // import "./globals.css"; | ||
|
|
||
| // const geistSans = Geist({ | ||
| // variable: "--font-geist-sans", | ||
| // subsets: ["latin"], | ||
| // }); | ||
|
|
||
| // const geistMono = Geist_Mono({ | ||
| // variable: "--font-geist-mono", | ||
| // subsets: ["latin"], | ||
| // }); | ||
|
|
||
| const geistSans = Geist({ | ||
| variable: "--font-geist-sans", | ||
| subsets: ["latin"], | ||
| }); | ||
| // export const metadata = { | ||
| // title: "Memory Game", | ||
| // description: "A fun and challenging memory game built with Next.js.", | ||
| // }; | ||
|
|
||
| const geistMono = Geist_Mono({ | ||
| variable: "--font-geist-mono", | ||
| subsets: ["latin"], | ||
| }); | ||
| // export default function RootLayout({ children }) { | ||
| // return ( | ||
| // <html lang="en"> | ||
| // <body | ||
| // className={`${geistSans.variable} ${geistMono.variable} antialiased`} | ||
| // > | ||
| // {children} | ||
| // </body> | ||
| // </html> | ||
| // ); | ||
| // } |
Copilot
AI
Dec 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This file contains 30 lines of commented-out code including imports and component logic. Remove these commented sections to maintain code clarity.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the suggestion! I've cleaned up the commented code and added basic input validation. Please let me know if further improvements are needed.
Description
This PR adds a fully functional Multiplayer Memory Game along with an improved local 2-player Memory Game. The implementation introduces real-time synchronization using Socket.io, enhanced UI styling, correct card-flip behavior, and stable turn-based gameplay.
The feature includes:
This PR significantly enhances gameplay consistency, ensures correct state synchronization between players, and improves the overall user experience.
Key Features Added
🎮 Multiplayer Mode (Socket.io)
👥 Local 2-Player Mode
🎨 UI & UX Improvements
🛠️ Fixes Applied
localStorageMotivation & Context
This feature significantly increases the interactivity and playability of the app by offering both online multiplayer and local two-player gameplay. It also fixes several core visual and logic issues, such as:
The update creates a stable, enjoyable, and visually appealing memory game experience.
Fixes # (issue)
Type of Change
Additional Notes
Future enhancements may include:
This PR lays the foundation for all future multiplayer expansions.