diff --git a/public/SimonSays.png b/public/SimonSays.png new file mode 100644 index 0000000..ed98224 Binary files /dev/null and b/public/SimonSays.png differ diff --git a/src/app/game/page.tsx b/src/app/game/page.tsx index dea73e5..93328fd 100644 --- a/src/app/game/page.tsx +++ b/src/app/game/page.tsx @@ -6,6 +6,7 @@ import SudokuImage from '/public/SudokuLogo.webp' import SchulteImage from '/public/SchulteImage.webp' import MemoryFlip from '/public/MemoryFlip.png' import BulletMath from '/public/BulletMath.png' +import SimonSaysImage from '/public/SimonSays.png' const SelectGame = () => { return ( @@ -46,7 +47,20 @@ const SelectGame = () => { backgroundColor={'#760172'} textColor={'white'} /> - + + ) diff --git a/src/app/game/simon-says/page.module.css b/src/app/game/simon-says/page.module.css new file mode 100644 index 0000000..d367197 --- /dev/null +++ b/src/app/game/simon-says/page.module.css @@ -0,0 +1,841 @@ +/* Simon Says Game Styles */ +.container { + max-width: 1000px; + margin: 0 auto; + padding: 20px; + text-align: center; + background: white; + min-height: 100vh; + color: #333; +} + +.header { + margin-bottom: 30px; +} + +.title { + font-size: 2.8rem; + font-weight: bold; + margin-bottom: 10px; + background: linear-gradient(135deg, #9333EA, #7C3AED); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + text-shadow: none; +} + +.subtitle { + font-size: 1.3rem; + color: #666; + margin-bottom: 20px; +} + +/* Game Setup */ +.gameSetup { + max-width: 700px; + margin: 0 auto 3rem auto; + padding: 2rem; + background: #f8f9fa; + border-radius: 16px; + border: 2px solid #e2e8f0; +} + +.setupTitle { + font-size: 1.8rem; + margin-bottom: 2rem; + color: #333; + text-align: center; +} + +.setupSection { + margin-bottom: 2rem; + text-align: center; +} + +.setupSection h3 { + font-size: 1.2rem; + margin-bottom: 1rem; + color: #333; +} + +.buttonGroup { + display: flex; + justify-content: center; + gap: 15px; + flex-wrap: wrap; +} + +.setupButton { + padding: 15px 25px; + border: 2px solid #e2e8f0; + border-radius: 12px; + background: white; + color: #333; + cursor: pointer; + font-weight: 500; + transition: all 0.3s ease; + font-size: 1rem; + display: flex; + flex-direction: column; + align-items: center; + gap: 5px; + min-width: 120px; +} + +.setupButton:hover { + border-color: #9333EA; + transform: translateY(-2px); +} + +.setupButton.active { + background: linear-gradient(135deg, #9333EA, #7C3AED); + color: white; + border-color: #9333EA; +} + +.modeDescription { + font-size: 0.8rem; + opacity: 0.8; +} + +.playButton { + background: linear-gradient(135deg, #22C55E, #16A34A); + color: white; + border: none; + padding: 18px 40px; + font-size: 1.3rem; + font-weight: bold; + border-radius: 12px; + cursor: pointer; + transition: all 0.3s ease; + box-shadow: 0 4px 15px rgba(34, 197, 94, 0.3); + margin-top: 1.5rem; +} + +.playButton:hover { + background: linear-gradient(135deg, #16A34A, #15803D); + transform: translateY(-2px); + box-shadow: 0 6px 20px rgba(34, 197, 94, 0.4); +} + +.soundControl { + text-align: center; +} + +.soundToggle { + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + cursor: pointer; + font-size: 1rem; +} + +.soundToggle input[type="checkbox"] { + transform: scale(1.2); + cursor: pointer; +} + +/* Game Stats */ +.gameStats { + display: flex; + justify-content: center; + gap: 30px; + margin-bottom: 20px; + flex-wrap: wrap; +} + +.stat { + background: #f8f9fa; + padding: 15px 25px; + border-radius: 12px; + border: 2px solid #e2e8f0; + color: #2d3748; + min-width: 100px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); +} + +.statLabel { + display: block; + font-size: 0.9rem; + color: #666; + margin-bottom: 5px; +} + +.statValue { + display: block; + font-size: 1.5rem; + font-weight: bold; + color: #9333EA; +} + +/* Status Message */ +.statusMessage { + margin-bottom: 30px; + padding: 15px; + background: linear-gradient(135deg, #f3f4f6, #e5e7eb); + border-radius: 12px; + border-left: 4px solid #9333EA; +} + +.statusMessage p { + font-size: 1.1rem; + font-weight: 500; + margin: 0; + color: #374151; +} + +/* Game Board */ +.gameBoard { + display: grid; + gap: 20px; + margin: 30px auto; + max-width: 600px; + perspective: 1000px; + padding: 20px; +} + +.grid4 { + grid-template-columns: repeat(2, 1fr); + max-width: 400px; +} + +.grid7 { + grid-template-columns: repeat(3, 1fr); + max-width: 500px; +} + +.grid10 { + grid-template-columns: repeat(4, 1fr); + max-width: 600px; +} + +.gameButton { + aspect-ratio: 1; + border: none; + border-radius: 20px; + cursor: pointer; + font-size: 1rem; + font-weight: bold; + color: white; + text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); + transition: all 0.3s ease; + position: relative; + overflow: hidden; + min-height: 100px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 5px; + transform-style: preserve-3d; +} + +.gameButton:hover:not(:disabled) { + transform: translateY(-5px) scale(1.05); + box-shadow: 0 10px 25px rgba(0, 0, 0, 0.3) !important; +} + +.gameButton:active:not(:disabled) { + transform: translateY(-2px) scale(0.98); +} + +.gameButton:disabled { + cursor: not-allowed; + opacity: 0.8; +} + +.gameButton.active { + transform: translateY(-5px) scale(1.1); + filter: brightness(1.3); + animation: pulse 0.6s ease; +} + +@keyframes pulse { + 0%, 100% { transform: translateY(-5px) scale(1.1); } + 50% { transform: translateY(-8px) scale(1.15); } +} + +.buttonLabel { + font-size: 1rem; + font-weight: bold; +} + + + +/* Action Buttons */ +.actionButtons { + margin: 30px 0; +} + +.startButton { + background: linear-gradient(135deg, #22C55E, #16A34A); + color: white; + border: none; + padding: 15px 40px; + font-size: 1.2rem; + font-weight: bold; + border-radius: 12px; + cursor: pointer; + transition: all 0.3s ease; + box-shadow: 0 4px 15px rgba(34, 197, 94, 0.3); +} + +.startButton:hover:not(:disabled) { + background: linear-gradient(135deg, #16A34A, #15803D); + transform: translateY(-2px); + box-shadow: 0 6px 20px rgba(34, 197, 94, 0.4); +} + +.startButton:disabled { + opacity: 0.6; + cursor: not-allowed; + transform: none; +} + +/* Keyboard Info */ +.keyboardInfo { + margin: 20px 0; + padding: 15px; + background: #f8f9fa; + border-radius: 10px; + border: 1px solid #e2e8f0; +} + +.keyboardInfo h4 { + margin: 0 0 8px 0; + color: #333; + font-size: 1rem; +} + +.keyboardInfo p { + margin: 0; + color: #666; + font-size: 0.9rem; +} + +/* Rules and Benefits */ +.rulesContainer { + max-width: 800px; + margin: 3rem auto; + padding: 0 1rem; + font-family: system-ui, -apple-system, sans-serif; + text-align: left; +} + +.sectionTitle { + font-size: 1.6rem; + margin: 2rem 0 1.2rem 0; + color: #111; + font-weight: 600; + letter-spacing: -0.01em; + text-align: center; +} + +.rulesGrid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 1.5rem; + margin-bottom: 2rem; +} + +.ruleCard { + background: white; + padding: 1.5rem; + border-radius: 12px; + border: 2px solid #e2e8f0; + text-align: center; + transition: all 0.3s ease; +} + +.ruleCard:hover { + border-color: #9333EA; + transform: translateY(-2px); +} + +.ruleNumber { + width: 40px; + height: 40px; + background: linear-gradient(135deg, #9333EA, #7C3AED); + color: white; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-weight: bold; + font-size: 1.2rem; + margin: 0 auto 1rem auto; +} + +.ruleCard h4 { + margin: 0 0 0.5rem 0; + color: #333; + font-size: 1.1rem; +} + +.ruleCard p { + margin: 0; + color: #666; + line-height: 1.5; +} + + + +.algorithmText { + line-height: 1.7; + color: #333; + background: #f8f9fa; + padding: 1.5rem; + border-radius: 10px; + border-left: 4px solid #9333EA; +} + +.rulesGrid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 1.5rem; + margin-bottom: 2rem; +} + +.ruleCard { + background: white; + padding: 1.5rem; + border-radius: 12px; + border: 2px solid #e2e8f0; + text-align: center; + transition: all 0.3s ease; +} + +.ruleCard:hover { + border-color: #9333EA; + transform: translateY(-2px); +} + +.ruleNumber { + width: 40px; + height: 40px; + background: linear-gradient(135deg, #9333EA, #7C3AED); + color: white; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-weight: bold; + font-size: 1.2rem; + margin: 0 auto 1rem auto; +} + +.ruleCard h4 { + margin: 0 0 0.5rem 0; + color: #333; + font-size: 1.1rem; +} + +.ruleCard p { + margin: 0; + color: #666; + line-height: 1.5; +} + +.controlsInfo { + margin: 2rem 0; + background: white; + padding: 1.5rem; + border-radius: 12px; + border: 2px solid #e2e8f0; +} + +.controlsInfo h3 { + margin: 0 0 1rem 0; + color: #333; + text-align: center; +} + +.controlsList { + display: flex; + flex-direction: column; + gap: 0.8rem; +} + +.controlItem { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.8rem; + background: #f8f9fa; + border-radius: 8px; +} + +.controlKey { + font-weight: bold; + color: #9333EA; + background: #f3f0ff; + padding: 4px 8px; + border-radius: 4px; + font-size: 0.9rem; +} + +.rulesGrid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 1rem; + margin: 1rem auto; + align-items: stretch; + justify-items: center; + width: 100%; + max-width: 1000px; + padding: 0; +} + +.ruleCard { + background: linear-gradient(145deg, #f8fafc, #e2e8f0); + border: 1px solid #cbd5e1; + border-radius: 12px; + padding: 1rem; + transition: all 0.3s ease; + display: flex; + align-items: flex-start; + gap: 0.75rem; + height: 100%; +} + +.ruleCard:hover { + transform: translateY(-2px); + box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1); + border-color: #9333EA; +} + +.ruleNumber { + background: linear-gradient(135deg, #9333EA, #7c3aed); + color: white; + width: 24px; + height: 24px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-weight: bold; + font-size: 0.875rem; + flex-shrink: 0; + margin-top: 2px; +} + +.controlsInfo { + display: flex; + flex-wrap: wrap; + gap: 1rem; + margin: 1rem 0; + padding: 1rem; + background: linear-gradient(145deg, #f1f5f9, #e2e8f0); + border-radius: 12px; + border: 1px solid #cbd5e1; +} + +.controlKey { + background: linear-gradient(145deg, #ffffff, #f1f5f9); + border: 1px solid #cbd5e1; + border-radius: 6px; + padding: 0.5rem 0.75rem; + font-family: 'Courier New', monospace; + font-weight: bold; + color: #475569; + font-size: 0.875rem; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + transition: all 0.2s ease; +} + +.controlKey:hover { + background: linear-gradient(145deg, #f1f5f9, #e2e8f0); + transform: translateY(-1px); +} + +/* Responsive Design */ +@media (max-width: 768px) { + .container { + padding: 15px; + } + + .title { + font-size: 2.2rem; + } + + .subtitle { + font-size: 1.1rem; + } + + .gameControls { + gap: 15px; + } + + .buttonGroup { + gap: 8px; + } + + .gameSetup { + padding: 1.5rem; + } + + .setupTitle { + font-size: 1.5rem; + } + + .setupButton { + padding: 12px 20px; + font-size: 0.9rem; + min-width: 100px; + } + + .playButton { + padding: 15px 30px; + font-size: 1.1rem; + } + + .rulesContainer { + max-width: 500px; + margin: 2rem auto; + padding: 0 1rem; + text-align: center; + } + + .rulesGrid { + display: grid; + grid-template-columns: 1fr; + gap: 1rem; + justify-items: center; + align-items: stretch; + width: 100%; + } + + .ruleCard { + width: 100%; + max-width: 100%; + text-align: left; + padding: 1rem; + margin: 0; + box-sizing: border-box; + text-align: left; + } + + .controlsInfo { + display: flex; + flex-direction: column; + gap: 0.75rem; + align-items: center; + width: 100%; + max-width: 100%; + margin: 1rem 0; + padding: 1rem; + box-sizing: border-box; + } + + .ruleCard { + padding: 1.2rem; + } + + .controlsInfo, + .tipsSection { + padding: 1.2rem; + } + + .gameStats { + gap: 15px; + } + + .stat { + padding: 12px 20px; + min-width: 80px; + } + + .gameBoard { + gap: 15px; + padding: 15px; + max-width: 350px; + } + + .grid4 { + max-width: 280px; + } + + .grid7 { + max-width: 320px; + } + + .grid10 { + max-width: 350px; + } + + .gameButton { + min-height: 80px; + font-size: 0.9rem; + } + + .buttonLabel { + font-size: 0.9rem; + } + + + + .startButton { + padding: 12px 30px; + font-size: 1.1rem; + } + + .sectionTitle { + font-size: 1.4rem; + } + + +} + +@media (max-width: 480px) { + .gameBoard { + gap: 10px; + max-width: 300px; + } + + .grid4 { + max-width: 240px; + } + + .grid7 { + max-width: 280px; + } + + .grid10 { + max-width: 300px; + } + + .gameButton { + min-height: 70px; + border-radius: 15px; + } + + .buttonLabel { + font-size: 0.8rem; + } + + + + .container { + padding: 10px; + } + + .rulesContainer { + padding: 0 0.25rem; + text-align: center; + } + + .sectionTitle { + font-size: 1.1rem; + margin-bottom: 0.75rem; + } + + .rulesGrid { + grid-template-columns: 1fr; + gap: 0.75rem; + justify-items: center; + max-width: 100%; + margin: 0 auto; + } + + .ruleCard { + padding: 0.75rem; + gap: 0.5rem; + width: 100%; + max-width: 320px; + margin: 0 auto; + text-align: left; + } + + .ruleNumber { + width: 20px; + height: 20px; + font-size: 0.75rem; + margin-top: 0; + flex-shrink: 0; + } + + .controlsInfo { + gap: 0.5rem; + padding: 0.75rem; + flex-direction: column; + align-items: center; + max-width: 320px; + margin: 1rem auto; + } + + .controlKey { + padding: 0.4rem 0.6rem; + font-size: 0.75rem; + } + + .gameSetup { + padding: 1rem 0.75rem; + } + + .setupSection { + margin-bottom: 1rem; + } + +} + + /* ==== Alignment Overrides (Single Source of Truth) ==== */ + .rulesGrid { + display: flex; + flex-direction: column; + gap: 1rem; + width: 100%; + max-width: 760px; + margin: 0 auto 2rem auto; + padding: 0 0.5rem; + } + + .ruleCard { + display: flex; + align-items: center; + gap: 1.25rem; + width: 100%; + max-width: 100%; + box-sizing: border-box; + } + + .ruleNumber { + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + } + + .ruleText { + display: flex; + flex-direction: column; + gap: 0.25rem; + text-align: left; + flex: 1; + min-width: 0; + } + + .ruleText h4 { + margin: 0; + font-size: 1.15rem; + } + + .ruleText p { + margin: 0; + font-size: 0.95rem; + line-height: 1.3; + } + + @media (max-width: 480px) { + .rulesGrid { + gap: 0.85rem; + padding: 0 0.25rem; + } + .ruleCard { + padding: 0.85rem 0.9rem; + border-radius: 14px; + } + .ruleText h4 { font-size: 1.05rem; } + .ruleText p { font-size: 0.85rem; } + .ruleNumber { width: 32px; height: 32px; font-size: 0.85rem; } + } \ No newline at end of file diff --git a/src/app/game/simon-says/page.tsx b/src/app/game/simon-says/page.tsx new file mode 100644 index 0000000..24c3f15 --- /dev/null +++ b/src/app/game/simon-says/page.tsx @@ -0,0 +1,137 @@ +'use client'; + +import { useState } from 'react'; +import { useRouter } from 'next/navigation'; +import styles from './page.module.css'; + +type Difficulty = 'normal' | 'strict'; +type ButtonCount = 4 | 7 | 10; + +const SimonSaysLanding = () => { + const [difficulty, setDifficulty] = useState('normal'); + const [buttonCount, setButtonCount] = useState(4); + const router = useRouter(); + + const startGame = () => { + // Navigate to the game page with selected settings + router.push(`/game/simon-says/play?difficulty=${difficulty}&buttons=${buttonCount}`); + }; + + return ( +
+
+

Simon Says

+

Memory sequence game.

+
+ + {/* Game Setup */} +
+

Game Settings

+ +
+

Game Mode

+
+ + +
+
+ +
+

Difficulty Level

+
+ + + +
+
+ + +
+ + {/* Game Rules */} +
+

How to Play

+
+
+
1
+
+

Watch

+

Computer shows button sequence.

+
+
+
+
2
+
+

Repeat

+

Click buttons in same order.

+
+
+
+
3
+
+

Level Up

+

Sequence gets longer each round.

+
+
+
+
4
+
+

Game Modes

+

Normal: retry on mistake. Strict: game over.

+
+
+
+ +
+

Controls

+
+
+ Mouse + Click buttons +
+
+ Keyboard + Number keys or WASD +
+
+
+ + +
+
+ ); +}; + +export default SimonSaysLanding; \ No newline at end of file diff --git a/src/app/game/simon-says/play/page.module.css b/src/app/game/simon-says/play/page.module.css new file mode 100644 index 0000000..286ccfb --- /dev/null +++ b/src/app/game/simon-says/play/page.module.css @@ -0,0 +1,368 @@ +/* Simon Says Game Play Page Styles */ +.container { + max-width: 1000px; + margin: 0 auto; + padding: 20px; + text-align: center; + background: white; + min-height: 100vh; + color: #333; +} + +.header { + margin-bottom: 30px; + display: flex; + flex-direction: column; + align-items: center; + gap: 15px; +} + +.backButton { + background: linear-gradient(135deg, #6B7280, #4B5563); + color: white; + border: none; + padding: 10px 20px; + font-size: 0.9rem; + font-weight: 500; + border-radius: 8px; + cursor: pointer; + transition: all 0.3s ease; + align-self: flex-start; +} + +.backButton:hover { + background: linear-gradient(135deg, #4B5563, #374151); + transform: translateY(-2px); +} + +.title { + font-size: 2.5rem; + font-weight: bold; + margin: 0; + background: linear-gradient(135deg, #9333EA, #7C3AED); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.gameMode { + font-size: 1.1rem; + color: #666; + font-weight: 500; +} + +/* Game Stats */ +.gameStats { + display: flex; + justify-content: center; + gap: 30px; + margin-bottom: 20px; + flex-wrap: wrap; +} + +.stat { + background: #f8f9fa; + padding: 15px 25px; + border-radius: 12px; + border: 2px solid #e2e8f0; + color: #2d3748; + min-width: 100px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); +} + +.statLabel { + display: block; + font-size: 0.9rem; + color: #666; + margin-bottom: 5px; +} + +.statValue { + display: block; + font-size: 1.5rem; + font-weight: bold; + color: #9333EA; +} + +/* Sound Control */ +.soundControl { + text-align: center; + margin-bottom: 20px; +} + +.soundToggle { + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + cursor: pointer; + font-size: 1rem; +} + +.soundToggle input[type="checkbox"] { + transform: scale(1.2); + cursor: pointer; +} + +/* Status Message */ +.statusMessage { + margin-bottom: 30px; + padding: 15px; + background: linear-gradient(135deg, #f3f4f6, #e5e7eb); + border-radius: 12px; + border-left: 4px solid #9333EA; +} + +.statusMessage p { + font-size: 1.1rem; + font-weight: 500; + margin: 0; + color: #374151; +} + +/* Game Board */ +.gameBoard { + display: grid; + gap: 25px; + margin: 30px auto; + max-width: 700px; + perspective: 1000px; + padding: 20px; +} + +.grid4 { + grid-template-columns: repeat(2, 1fr); + max-width: 500px; +} + +.grid7 { + grid-template-columns: repeat(3, 1fr); + max-width: 600px; +} + +.grid10 { + grid-template-columns: repeat(4, 1fr); + max-width: 700px; +} + +.gameButton { + aspect-ratio: 1; + border: none; + border-radius: 20px; + cursor: pointer; + font-size: 1rem; + font-weight: bold; + color: white; + text-shadow: 0 2px 8px rgba(0, 0, 0, 0.8), 0 1px 3px rgba(0, 0, 0, 0.9); + transition: all 0.3s ease; + position: relative; + overflow: hidden; + min-height: 120px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 5px; + transform-style: preserve-3d; +} + +.gameButton:hover:not(:disabled) { + transform: translateY(-5px) scale(1.05); + box-shadow: 0 10px 25px rgba(0, 0, 0, 0.3) !important; +} + +.gameButton:active:not(:disabled) { + transform: translateY(-2px) scale(0.98); +} + +.gameButton:disabled { + cursor: not-allowed; + opacity: 0.8; +} + +.gameButton.active { + transform: translateY(-5px) scale(1.1); + filter: brightness(1.3); + animation: pulse 0.4s ease; +} + +@keyframes pulse { + 0%, 100% { transform: translateY(-5px) scale(1.1); } + 50% { transform: translateY(-8px) scale(1.15); } +} + +.buttonLabel { + font-size: 1rem; + font-weight: bold; +} + +/* Action Buttons */ +.actionButtons { + margin: 30px 0; +} + +.startButton { + background: linear-gradient(135deg, #22C55E, #16A34A); + color: white; + border: none; + padding: 15px 40px; + font-size: 1.2rem; + font-weight: bold; + border-radius: 12px; + cursor: pointer; + transition: all 0.3s ease; + box-shadow: 0 4px 15px rgba(34, 197, 94, 0.3); +} + +.startButton:hover:not(:disabled) { + background: linear-gradient(135deg, #16A34A, #15803D); + transform: translateY(-2px); + box-shadow: 0 6px 20px rgba(34, 197, 94, 0.4); +} + +.startButton:disabled { + opacity: 0.6; + cursor: not-allowed; + transform: none; +} + +/* Keyboard Info */ +.keyboardInfo { + margin: 20px 0; + padding: 15px; + background: #f8f9fa; + border-radius: 10px; + border: 1px solid #e2e8f0; +} + +.keyboardInfo h4 { + margin: 0 0 8px 0; + color: #333; + font-size: 1rem; +} + +.keyboardInfo p { + margin: 0; + color: #666; + font-size: 0.9rem; +} + +/* Responsive Design */ +@media (max-width: 768px) { + .container { + padding: 15px; + } + + .header { + gap: 10px; + } + + .backButton { + padding: 8px 16px; + font-size: 0.8rem; + } + + .title { + font-size: 2rem; + } + + .gameMode { + font-size: 1rem; + } + + .gameStats { + gap: 15px; + } + + .stat { + padding: 12px 20px; + min-width: 80px; + } + + .gameBoard { + gap: 18px; + padding: 15px; + max-width: 400px; + } + + .grid4 { + max-width: 320px; + } + + .grid7 { + max-width: 380px; + } + + .grid10 { + max-width: 400px; + } + + .gameButton { + min-height: 90px; + font-size: 0.9rem; + } + + .buttonLabel { + font-size: 0.9rem; + } + + .startButton { + padding: 12px 30px; + font-size: 1.1rem; + } +} + +@media (max-width: 480px) { + .container { + padding: 10px; + } + + .gameBoard { + gap: 8px; + max-width: 320px; + padding: 10px; + } + + .grid4 { + max-width: 260px; + } + + .grid7 { + max-width: 300px; + } + + .grid10 { + max-width: 320px; + } + + .gameButton { + min-height: 70px; + border-radius: 15px; + font-size: 0.85rem; + } + + .buttonLabel { + font-size: 0.75rem; + font-weight: 900; + text-shadow: 0 2px 6px rgba(0, 0, 0, 0.9), 0 1px 2px rgba(0, 0, 0, 1); + } + + .header { + margin-bottom: 15px; + gap: 8px; + } + + .title { + font-size: 1.6rem; + } + + .gameStats { + gap: 10px; + margin-bottom: 15px; + } + + .stat { + padding: 8px 12px; + min-width: 70px; + } +} \ No newline at end of file diff --git a/src/app/game/simon-says/play/page.tsx b/src/app/game/simon-says/play/page.tsx new file mode 100644 index 0000000..cea5f16 --- /dev/null +++ b/src/app/game/simon-says/play/page.tsx @@ -0,0 +1,541 @@ +'use client'; + +import { useState, useEffect, useCallback, useRef, Suspense } from 'react'; +import { useRouter, useSearchParams } from 'next/navigation'; +import dynamic from 'next/dynamic'; +import styles from './page.module.css'; + +interface GameState { + sequence: number[]; + playerSequence: number[]; + currentRound: number; + isShowingSequence: boolean; + isPlayerTurn: boolean; + gameStatus: 'idle' | 'showing' | 'waiting' | 'success' | 'failure'; + score: number; +} + +type Difficulty = 'normal' | 'strict'; +type ButtonCount = 4 | 7 | 10; + +interface Button { + id: number; + color: string; + tone: number; + label: string; + keyBinding?: string; +} + +const BUTTON_CONFIGS: Record = { + 4: [ + { id: 0, color: '#DC2626', tone: 261.63, label: 'Red', keyBinding: '1' }, + { id: 1, color: '#2563EB', tone: 329.63, label: 'Blue', keyBinding: '2' }, + { id: 2, color: '#16A34A', tone: 392.00, label: 'Green', keyBinding: '3' }, + { id: 3, color: '#CA8A04', tone: 523.25, label: 'Yellow', keyBinding: '4' } + ], + 7: [ + { id: 0, color: '#DC2626', tone: 261.63, label: 'Red', keyBinding: '1' }, + { id: 1, color: '#2563EB', tone: 329.63, label: 'Blue', keyBinding: '2' }, + { id: 2, color: '#16A34A', tone: 392.00, label: 'Green', keyBinding: '3' }, + { id: 3, color: '#CA8A04', tone: 523.25, label: 'Yellow', keyBinding: '4' }, + { id: 4, color: '#EA580C', tone: 349.23, label: 'Orange', keyBinding: '5' }, + { id: 5, color: '#7C3AED', tone: 440.00, label: 'Purple', keyBinding: '6' }, + { id: 6, color: '#0891B2', tone: 493.88, label: 'Cyan', keyBinding: '7' } + ], + 10: [ + { id: 0, color: '#DC2626', tone: 261.63, label: 'Red', keyBinding: '1' }, + { id: 1, color: '#2563EB', tone: 329.63, label: 'Blue', keyBinding: '2' }, + { id: 2, color: '#16A34A', tone: 392.00, label: 'Green', keyBinding: '3' }, + { id: 3, color: '#CA8A04', tone: 523.25, label: 'Yellow', keyBinding: '4' }, + { id: 4, color: '#EA580C', tone: 349.23, label: 'Orange', keyBinding: '5' }, + { id: 5, color: '#7C3AED', tone: 440.00, label: 'Purple', keyBinding: '6' }, + { id: 6, color: '#0891B2', tone: 493.88, label: 'Cyan', keyBinding: '7' }, + { id: 7, color: '#BE185D', tone: 587.33, label: 'Magenta', keyBinding: '8' }, + { id: 8, color: '#4B5563', tone: 659.25, label: 'Gray', keyBinding: '9' }, + { id: 9, color: '#92400E', tone: 698.46, label: 'Brown', keyBinding: '0' } + ] +}; + +const SimonSaysGame = () => { + + const searchParams = useSearchParams(); + const router = useRouter(); + + const [gameState, setGameState] = useState({ + sequence: [], + playerSequence: [], + currentRound: 0, + isShowingSequence: false, + isPlayerTurn: false, + gameStatus: 'idle', + score: 0 + }); + + const [difficulty, setDifficulty] = useState('normal'); + const [buttonCount, setButtonCount] = useState(4); + const [bestScore, setBestScore] = useState(null); + const [activeButton, setActiveButton] = useState(null); + const [soundEnabled, setSoundEnabled] = useState(true); + const [mounted, setMounted] = useState(false); + + const audioContextRef = useRef(null); + // Track ALL scheduled timeouts so we can clear them on unmount / restart + const timeoutsRef = useRef>>([]); + + const clearScheduledTimeouts = useCallback(() => { + if (timeoutsRef.current.length) { + timeoutsRef.current.forEach(id => clearTimeout(id)); + timeoutsRef.current = []; + } + }, []); + + const buttons = BUTTON_CONFIGS[buttonCount]; + + // Handle client-side mounting + useEffect(() => { + setMounted(true); + }, []); + + // Get URL parameters on component mount + useEffect(() => { + if (!mounted) return; + + try { + const difficultyParam = searchParams.get('difficulty') as Difficulty; + const buttonsParam = searchParams.get('buttons'); + + if (difficultyParam && ['normal', 'strict'].includes(difficultyParam)) { + setDifficulty(difficultyParam); + } + + if (buttonsParam && ['4', '7', '10'].includes(buttonsParam)) { + setButtonCount(parseInt(buttonsParam) as ButtonCount); + } + } catch (error) { + console.log('Error reading search params:', error); + } + }, [searchParams, mounted]); + + // Initialize audio context + const initAudioContext = useCallback(() => { + if (!audioContextRef.current && soundEnabled && typeof window !== 'undefined') { + const AudioContextClass = window.AudioContext || (window as unknown as { webkitAudioContext: typeof AudioContext }).webkitAudioContext; + audioContextRef.current = new AudioContextClass(); + } + }, [soundEnabled]); + + // Play tone for button + const playTone = useCallback((frequency: number, duration: number = 500) => { + if (!soundEnabled || !audioContextRef.current) return; + + try { + const oscillator = audioContextRef.current.createOscillator(); + const gainNode = audioContextRef.current.createGain(); + + oscillator.frequency.setValueAtTime(frequency, audioContextRef.current.currentTime); + oscillator.type = 'sine'; + + gainNode.gain.setValueAtTime(0.3, audioContextRef.current.currentTime); + gainNode.gain.exponentialRampToValueAtTime(0.01, audioContextRef.current.currentTime + duration / 1000); + + oscillator.connect(gainNode); + gainNode.connect(audioContextRef.current.destination); + + oscillator.start(); + oscillator.stop(audioContextRef.current.currentTime + duration / 1000); + } catch (error) { + console.warn('Audio playback failed:', error); + } + }, [soundEnabled]); + + // Load best score from localStorage + useEffect(() => { + if (!mounted || typeof window === 'undefined') return; + + try { + const savedScore = localStorage.getItem(`simonSays-${buttonCount}-${difficulty}`); + if (savedScore) { + setBestScore(parseInt(savedScore)); + } else { + setBestScore(null); + } + } catch (error) { + console.warn('Unable to load best score:', error); + } + }, [buttonCount, difficulty, mounted]); + + // Save best score to localStorage + const saveBestScore = useCallback((score: number) => { + if (!mounted || typeof window === 'undefined') return; + + try { + if (!bestScore || score > bestScore) { + setBestScore(score); + localStorage.setItem(`simonSays-${buttonCount}-${difficulty}`, score.toString()); + } + } catch (error) { + console.warn('Unable to save best score:', error); + } + }, [bestScore, buttonCount, difficulty, mounted]); + + // Generate next sequence step + const generateNextStep = useCallback(() => { + const nextStep = Math.floor(Math.random() * buttonCount); + return nextStep; + }, [buttonCount]); + + // Start new game + const startNewGame = useCallback(() => { + clearScheduledTimeouts(); + const firstStep = generateNextStep(); + setGameState({ + sequence: [firstStep], + playerSequence: [], + currentRound: 1, + isShowingSequence: true, + isPlayerTurn: false, + gameStatus: 'showing', + score: 0 + }); + initAudioContext(); + }, [generateNextStep, initAudioContext, clearScheduledTimeouts]); + + // Show sequence to player + const showSequence = useCallback(async () => { + if (gameState.gameStatus !== 'showing') return; + + // Clear any previously scheduled flashes before starting a new batch + clearScheduledTimeouts(); + setActiveButton(null); + + for (let i = 0; i < gameState.sequence.length; i++) { + // Wrap each step in a promise so we can await sequentially + await new Promise(resolve => { + const delay = i * 500 + 800; + const highlightTimeout = setTimeout(() => { + const buttonId = gameState.sequence[i]; + const button = buttons[buttonId]; + + setActiveButton(buttonId); + playTone(button.tone, 400); + + const resetTimeout = setTimeout(() => { + setActiveButton(null); + resolve(); + }, 400); + timeoutsRef.current.push(resetTimeout); + }, delay); + timeoutsRef.current.push(highlightTimeout); + }); + } + + const playerTurnTimeout = setTimeout(() => { + setGameState((prev: GameState) => ({ + ...prev, + isShowingSequence: false, + isPlayerTurn: true, + gameStatus: 'waiting', + playerSequence: [] + })); + }, 500); + timeoutsRef.current.push(playerTurnTimeout); + }, [gameState.sequence, gameState.gameStatus, buttons, playTone, clearScheduledTimeouts]); + + // Handle player button click + const handleButtonClick = useCallback((buttonId: number) => { + if (!gameState.isPlayerTurn || gameState.gameStatus !== 'waiting') return; + + const button = buttons[buttonId]; + playTone(button.tone, 300); + setActiveButton(buttonId); + const releaseTimeout = setTimeout(() => { + setActiveButton(null); + }, 300); + timeoutsRef.current.push(releaseTimeout); + + const newPlayerSequence = [...gameState.playerSequence, buttonId]; + const currentIndex = gameState.playerSequence.length; + const expectedButton = gameState.sequence[currentIndex]; + + if (buttonId !== expectedButton) { + // Wrong button pressed + if (difficulty === 'strict') { + // Game over in strict mode + setGameState((prev: GameState) => ({ + ...prev, + gameStatus: 'failure', + isPlayerTurn: false + })); + saveBestScore(gameState.currentRound - 1); + } else { + // Retry current round in normal mode + setGameState((prev: GameState) => ({ + ...prev, + playerSequence: [], + isShowingSequence: true, + isPlayerTurn: false, + gameStatus: 'showing' + })); + } + return; + } + + // Correct button pressed + if (newPlayerSequence.length === gameState.sequence.length) { + // Round completed successfully + const newRound = gameState.currentRound + 1; + const newScore = gameState.currentRound; // Score equals rounds completed + const nextStep = generateNextStep(); + + setGameState((prev: GameState) => ({ + ...prev, + sequence: [...prev.sequence, nextStep], + playerSequence: [], + currentRound: newRound, + score: newScore, + isShowingSequence: true, + isPlayerTurn: false, + gameStatus: 'showing' + })); + // Persist potential new best score immediately + saveBestScore(newScore); + } else { + // Continue with current sequence + setGameState((prev: GameState) => ({ + ...prev, + playerSequence: newPlayerSequence + })); + } + }, [gameState, buttons, playTone, difficulty, generateNextStep, saveBestScore]); + + // Keyboard controls + useEffect(() => { + if (!mounted || typeof window === 'undefined') return; + + const handleKeyPress = (event: KeyboardEvent) => { + if (!gameState.isPlayerTurn) return; + + const key = event.key.toLowerCase(); + let buttonId = -1; + + // Number key bindings + if (key >= '1' && key <= '9') { + buttonId = parseInt(key) - 1; + } else if (key === '0') { + buttonId = 9; + } + // Arrow key and WASD bindings for 4-button mode + else if (buttonCount === 4) { + switch (key) { + case 'arrowup': + case 'w': + buttonId = 0; + break; + case 'arrowright': + case 'd': + buttonId = 1; + break; + case 'arrowdown': + case 's': + buttonId = 2; + break; + case 'arrowleft': + case 'a': + buttonId = 3; + break; + } + } + + if (buttonId >= 0 && buttonId < buttonCount) { + handleButtonClick(buttonId); + } + }; + + window.addEventListener('keydown', handleKeyPress); + return () => window.removeEventListener('keydown', handleKeyPress); + }, [gameState.isPlayerTurn, buttonCount, handleButtonClick, mounted]); + + // Show sequence when status changes to 'showing' + useEffect(() => { + if (gameState.gameStatus === 'showing') { + showSequence(); + } + }, [gameState.gameStatus, showSequence]); + + // Cleanup all scheduled timeouts on unmount + useEffect(() => { + return () => { + clearScheduledTimeouts(); + }; + }, [clearScheduledTimeouts]); + + const getStatusMessage = () => { + switch (gameState.gameStatus) { + case 'idle': + return 'Press Start to begin.'; + case 'showing': + return `Round ${gameState.currentRound} - Watch`; + case 'waiting': + return `Your turn (${gameState.playerSequence.length + 1}/${gameState.sequence.length})`; + case 'failure': + return difficulty === 'strict' + ? `Game Over. Score: ${gameState.currentRound - 1}` + : 'Wrong. Watch again.'; + default: + return ''; + } + }; + + const goBack = () => { + router.push('/game/simon-says'); + }; + + // Show loading state until component is mounted + if (!mounted) { + return ( +
+
+

Simon Says

+
+
+

Loading game...

+
+
+ ); + } + + return ( +
+ {/* Header with Back Button */} +
+ +

Simon Says

+
+ {difficulty === 'strict' ? 'Strict Mode' : 'Normal Mode'} • {buttonCount} Buttons +
+
+ + {/* Game Stats */} +
+
+ Round: + {gameState.currentRound} +
+
+ Score: + {gameState.score} +
+ {bestScore !== null && ( +
+ Best: + {bestScore} +
+ )} +
+ + {/* Sound Control */} +
+ +
+ + {/* Status Message */} +
+

{getStatusMessage()}

+
+ + {/* Game Board */} +
+ {buttons.map((button) => ( + + ))} +
+ + {/* Control Buttons */} +
+ +
+ + {/* Keyboard Controls Info */} +
+

Controls

+

+ {buttonCount === 4 + ? 'Arrow Keys, WASD, or 1-4' + : `Number Keys 1-${buttonCount}` + } +

+
+
+ ); +}; + +// Create a dynamic component to prevent SSR issues +const DynamicSimonSaysGame = dynamic(() => Promise.resolve(SimonSaysGame), { + ssr: false, + loading: () => ( +
+

Simon Says

+

Loading game...

+
+ ) +}); + +const SimonSaysGamePage = () => { + return ( + +

Simon Says

+

Loading game...

+ + }> + +
+ ); +}; + +export default SimonSaysGamePage; \ No newline at end of file