pull/1656/head
kumar 2 weeks ago
parent f2f7c3c28c
commit 328142a0fa

File diff suppressed because it is too large Load Diff

@ -1,26 +1,247 @@
@import "tailwindcss";
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
--background: #ffffff;
--foreground: #171717;
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
--background: #F8FAFC; /* light pastel background */
--foreground: #071122; /* darker text for contrast */
--modal-bg: #ffffff;
--modal-text: #071122;
--panel: rgba(15,23,36,0.02);
--muted: #475569; /* stronger muted */
--card-front: #60A5FA; /* soft blue (primary) */
--card-front-text: #062030;
--card-matched: #34D399; /* mint (match) */
--card-matched-contrast: #059669; /* darker mint for contrast */
--card-back: #E6EEF6; /* very light back */
--accent: #60A5FA; /* primary accent */
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
--modal-bg: #0f1724;
--modal-text: #edf2f7;
}
}
body {
background: var(--background);
color: var(--foreground);
font-family: Arial, Helvetica, sans-serif;
font-family: Inter, system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial;
}
/* Game UI styles */
.game-panel {
width: 100%;
max-width: 64rem;
background: linear-gradient(180deg, rgba(255,255,255,0.02), rgba(255,255,255,0.01));
border-radius: 0.5rem;
padding: 0.6rem;
border: 1px solid rgba(255,255,255,0.03);
box-shadow: 0 8px 30px rgba(2,6,23,0.6);
}
.controls {
display: flex;
gap: 0.4rem;
align-items: center;
justify-content: space-between;
width: 100%;
margin-bottom: 0.5rem;
}
.controls-left {
display:flex;
gap:0.4rem;
align-items:center;
}
.controls-right {
display:flex;
gap:0.4rem;
align-items:center;
}
/* spacing for the players row (added class in component) */
.players-row {
margin-bottom: 1rem;
}
/* Ensure player labels, inputs and stats are readable in both themes */
.game-panel input,
.game-panel .player-label,
.game-panel .stat,
.game-panel .small-muted {
color: var(--foreground);
}
.game-panel input {
background: transparent;
border: none;
}
.card-wrap {
perspective: 1000px;
}
/* make cards square and a bit larger */
.card {
/* ensure card buttons are square */
aspect-ratio: 1 / 1;
display: block;
}
.card {
position: relative;
width: 100%;
height: 100%;
}
.card-inner {
position: relative;
width: 100%;
height: 100%;
transform-style: preserve-3d;
transition: transform 400ms cubic-bezier(.2,.8,.2,1);
}
.card.flipped .card-inner {
transform: rotateY(180deg);
}
.card-face {
position: absolute;
inset: 0;
display:flex;
align-items:center;
justify-content:center;
backface-visibility: hidden;
border-radius: 0.5rem;
color: var(--foreground);
}
/* Card hover / focus styles */
.card {
box-shadow: 0 4px 8px rgba(15,23,42,0.05);
transition: transform 160ms ease, box-shadow 160ms ease;
}
.card:hover:not(.flipped) {
transform: translateY(-2px) scale(1.01);
box-shadow: 0 8px 18px rgba(15,23,42,0.08);
}
.card:focus {
outline: 2px solid rgba(6,182,212,0.18);
}
/* Match pulse */
.card.matched .card-inner {
animation: match-pulse 520ms ease;
}
@keyframes match-pulse {
0% { transform: scale(1) rotateY(0deg); }
50% { transform: scale(1.05) rotateY(0deg); }
100% { transform: scale(1) rotateY(0deg); }
}
/* Winner modal */
.overlay {
position: fixed;
inset: 0;
background: rgba(2,6,23,0.55);
display:flex;
align-items:center;
justify-content:center;
z-index: 60;
}
.modal {
background: var(--modal-bg);
color: var(--modal-text);
padding: 1rem;
border-radius: 0.5rem;
box-shadow: 0 16px 40px rgba(2,6,23,0.28);
max-width: 26rem;
width: 100%;
text-align: center;
}
.modal h2 { font-size: 1.5rem; margin-bottom: 0.5rem; }
.modal p { color: var(--modal-text); margin-bottom: 1rem; }
.modal button { margin: 0 0.25rem; }
/* Ensure secondary button in modal is readable in both themes */
.modal .btn-secondary {
background: rgba(255,255,255,0.08);
color: var(--modal-text);
border: none;
padding: 0.35rem 0.8rem;
border-radius: 0.45rem;
opacity: 1;
}
.modal .btn-secondary:hover { filter: brightness(0.92); }
.winner-text { color: var(--card-matched-contrast); }
/* Color tokens for card faces */
.card-face.card-front {
background: var(--card-front);
color: white;
}
.card.matched .card-face.card-front {
background: var(--card-matched);
color: white;
}
.card-face.card-back {
background: var(--card-back);
color: var(--foreground);
}
/* Button classes used in JSX */
.btn-primary {
background: var(--accent);
color: #ffffff; /* ensure readable on accent */
padding: 0.35rem 0.8rem;
border-radius: 0.45rem;
border: none;
}
.btn-secondary {
background: var(--card-back);
color: var(--foreground);
padding: 0.3rem 0.6rem;
border-radius: 0.35rem;
border: 1px solid rgba(15,23,42,0.04);
}
.player-active {
box-shadow: 0 6px 18px rgba(96,165,250,0.10);
border-radius: 0.45rem;
background: rgba(96,165,250,0.06);
}
/* small responsive tweaks */
@media (max-width: 640px) {
.card { border-radius: 0.4rem; }
.card-face { border-radius: 0.4rem; }
}
.card-front {
transform: rotateY(180deg);
}
.card-back {
}
.small-muted {
font-size: 0.85rem;
color: var(--muted);
}
.stat {
background: rgba(15,23,42,0.03);
padding: 0.25rem 0.5rem;
border-radius: 0.375rem;
}
@media (max-width: 640px) {
.game-panel { padding: 0.75rem; }
}

@ -1,8 +1,7 @@
"use client";
import React, { useEffect, useState, useCallback } from "react";
import React, { useEffect, useState, useCallback, useRef } from "react";
// Fisher-Yates shuffle for unbiased randomization
function fisherYatesShuffle(array) {
const arr = array.slice();
for (let i = arr.length - 1; i > 0; i--) {
@ -22,18 +21,47 @@ const MemoryGame = () => {
const [error, setError] = useState("");
const [won, setWon] = useState(false);
const timerRef = useRef(null);
const intervalRef = useRef(null);
const startTimeRef = useRef(null);
const [moves, setMoves] = useState(0);
const [elapsed, setElapsed] = useState(0);
const [running, setRunning] = useState(false);
const [best, setBest] = useState(null);
const [players, setPlayers] = useState([
{ name: "Player 1", score: 0 },
{ name: "Player 2", score: 0 },
]);
const [currentPlayer, setCurrentPlayer] = useState(0);
const [winner, setWinner] = useState(null);
const [showModal, setShowModal] = 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)");
let size = parseInt(e.target.value);
if (Number.isNaN(size)) return;
if (size < 2) size = 2;
if (size > 10) size = 10;
if (size % 2 !== 0) {
if (size < 10) size = size + 1;
else size = size - 1;
}
setGridSize(size);
setError("");
};
const initializeGame = useCallback(() => {
if (timerRef.current) {
clearTimeout(timerRef.current);
timerRef.current = null;
}
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
startTimeRef.current = null;
setElapsed(0);
setMoves(0);
setRunning(false);
const totalCards = gridSize * gridSize;
const pairCount = totalCards / 2;
@ -50,29 +78,69 @@ const MemoryGame = () => {
setSelectedPairs([]);
setDisabled(false);
setWon(false);
setPlayers((prev) => {
if (!prev || prev.length === 0) {
return [
{ name: "Player 1", score: 0 },
{ name: "Player 2", score: 0 },
];
}
return prev.map((p) => ({ ...p, score: 0 }));
});
setCurrentPlayer(0);
setWinner(null);
setShowModal(false);
}, [gridSize]);
useEffect(() => {
initializeGame();
}, [initializeGame]);
const handleMatch = (secondId) => {
const [firstId] = flipped;
const handleMatch = (firstId, secondId) => {
if (array[firstId].number === array[secondId].number) {
setSelectedPairs([...selectedPairs, firstId, secondId]);
setSelectedPairs((prev) => [...prev, firstId, secondId]);
// award point to current player
setPlayers((prev) => {
const next = prev.map((p) => ({ ...p }));
next[currentPlayer].score += 1;
return next;
});
setFlipped([]);
setDisabled(false);
} else {
setTimeout(() => {
if (timerRef.current) clearTimeout(timerRef.current);
timerRef.current = setTimeout(() => {
setDisabled(false);
setFlipped([]);
// switch turn after showing the cards briefly
setCurrentPlayer((c) => (c === 0 ? 1 : 0));
timerRef.current = null;
}, 1000);
}
};
// Start/stop game timer
const startTimer = () => {
if (intervalRef.current) return;
startTimeRef.current = Date.now();
intervalRef.current = setInterval(() => {
setElapsed(Math.floor((Date.now() - startTimeRef.current) / 1000));
}, 200);
setRunning(true);
};
const stopTimer = () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
setRunning(false);
};
const handleClick = (id) => {
if (disabled || won) return;
// start timer on first meaningful click
if (!intervalRef.current) startTimer();
if (flipped.length === 0) {
setFlipped([id]);
@ -80,14 +148,17 @@ const MemoryGame = () => {
}
if (flipped.length === 1) {
setDisabled(true);
if (id !== flipped[0]) {
setFlipped([...flipped, id]);
handleMatch(id);
} else {
const first = flipped[0];
if (id === first) {
setFlipped([]);
setDisabled(false);
return;
}
// increment move when a second distinct card is selected
setMoves((m) => m + 1);
setDisabled(true);
setFlipped([first, id]);
handleMatch(first, id);
}
};
@ -97,64 +168,177 @@ const MemoryGame = () => {
useEffect(() => {
if (selectedPairs.length === array.length && array.length > 0) {
setWon(true);
// determine winner
const p0 = players[0]?.score ?? 0;
const p1 = players[1]?.score ?? 0;
if (p0 > p1) setWinner(players[0].name);
else if (p1 > p0) setWinner(players[1].name);
else setWinner("Tie");
setShowModal(true);
}
}, [selectedPairs, array]);
// When player wins, stop timer and persist bests
useEffect(() => {
if (!won) return;
stopTimer();
// persist single-player best only when there is no multiplayer (tie or win by one?)
// we'll persist best as before (time/moves) regardless optional
const key = `memory-game-best-${gridSize}`;
try {
const prev = JSON.parse(localStorage.getItem(key) || "null");
const record = { time: elapsed, moves };
let better = false;
if (!prev) better = true;
else if (record.time < prev.time) better = true;
else if (record.time === prev.time && record.moves < prev.moves) better = true;
if (better) localStorage.setItem(key, JSON.stringify(record));
setBest(() => {
try {
return JSON.parse(localStorage.getItem(key) || "null");
} catch (e) {
return null;
}
});
} catch (e) {
// ignore localStorage errors
}
}, [won]);
// Load best when gridSize changes
useEffect(() => {
const key = `memory-game-best-${gridSize}`;
try {
const prev = JSON.parse(localStorage.getItem(key) || "null");
setBest(prev);
} catch (e) {
setBest(null);
}
}, [gridSize]);
useEffect(() => {
return () => {
if (timerRef.current) {
clearTimeout(timerRef.current);
timerRef.current = null;
}
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
};
}, []);
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 className="min-h-screen flex items-center justify-center p-3">
<div className="game-panel">
<div className="flex items-center justify-between mb-4">
<div>
<h1 className="text-3xl font-extrabold">Memory Game</h1>
<p className="small-muted">Find all matching pairs click or press Enter/Space to flip</p>
<div className="players-row mt-3 flex gap-3 items-center">
{players.map((p, idx) => (
<div
key={p.name}
className={`px-2 py-1 rounded-md flex items-center gap-2 ${currentPlayer === idx ? 'player-active' : ''}`}
>
<input
value={p.name}
onChange={(e) => setPlayers((prev) => prev.map((pl, i) => i === idx ? {...pl, name: e.target.value} : pl))}
className="font-semibold bg-transparent border-b border-transparent focus:border-gray-300 focus:outline-none text-sm"
style={{width: '6.5rem'}}
/>
<div className="stat small-muted">Score: <span className="font-semibold">{p.score}</span></div>
</div>
))}
</div>
</div>
))}
</div>
{/* Result */}
<div className="text-2xl text-green-500 font-bold">
{won ? "You Won!" : ""}
</div>
<div className="controls-right">
<div className="controls-left">
<label htmlFor="gridSize" className="small-muted">Grid</label>
<input
id="gridSize"
type="number"
className="w-14 ml-2 rounded border px-2 py-1 text-sm"
min="2"
max="10"
step="2"
value={gridSize}
onChange={handleGridSize}
aria-label="Grid size (even numbers only)"
/>
</div>
<div className="flex items-center gap-3">
<div className="small-muted">Moves: <span className="font-semibold">{moves}</span></div>
<div className="small-muted">Time: <span className="font-semibold">{elapsed}s</span></div>
<button
type="button"
onClick={initializeGame}
className="ml-3 btn-primary"
>
Reset
</button>
</div>
</div>
</div>
<div
className="grid gap-3 mb-4"
style={{
gridTemplateColumns: `repeat(${gridSize}, minmax(0,1fr))`,
width: `min(100%,${gridSize * 5.5}rem)`,
}}
>
{array.map((card) => {
const flippedState = isFlipped(card.id);
const matched = isSelectedPairs(card.id);
return (
<div key={card.id} className="card-wrap">
<button
type="button"
onClick={() => handleClick(card.id)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
handleClick(card.id);
}
}}
aria-pressed={flippedState}
aria-label={`Card ${card.id + 1} ${flippedState ? "showing " + card.number : "hidden"}`}
className={`card ${flippedState ? "flipped" : ""} ${matched ? 'matched' : ''}`}
>
<div className="card-inner">
<div className={`card-face card-front`}>
<span className="text-2xl font-bold">{card.number}</span>
</div>
<div className={`card-face card-back`}>
<span className="text-2xl font-extrabold">?</span>
</div>
</div>
</button>
</div>
);
})}
</div>
{/* Reset Button */}
<button
className="px-5 py-2 bg-green-500 rounded text-white mt-5"
onClick={initializeGame}
>
Reset
</button>
{showModal && (
<div className="overlay" role="dialog" aria-modal="true">
<div className="modal">
<h2>{winner === 'Tie' ? "It's a tie!" : `${winner} wins!`}</h2>
<p>Final score {players.map((p) => `${p.name}: ${p.score}`).join(' • ')}</p>
<div>
<button onClick={() => { setShowModal(false); initializeGame(); }} className="btn-primary">Play again</button>
<button onClick={() => setShowModal(false)} className="btn-secondary">Close</button>
</div>
</div>
</div>
)}
<div aria-live="polite" className="text-2xl font-semibold winner-text">
{won ? (winner === "Tie" ? "It's a tie!" : `${winner} wins!`) : ""}
</div>
</div>
</div>
);
};

@ -6,7 +6,7 @@
"dev": "next dev --turbopack",
"build": "next build --turbopack",
"start": "next start",
"lint": "eslint"
"lint": "next lint"
},
"dependencies": {
"react": "19.1.0",

3070
package-lock.json generated

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

@ -0,0 +1,3 @@
ignoredBuiltDependencies:
- docsify
- puppeteer
Loading…
Cancel
Save