diff --git a/IMPLEMENTATION.md b/IMPLEMENTATION.md index c813594..42ff78f 100644 --- a/IMPLEMENTATION.md +++ b/IMPLEMENTATION.md @@ -1544,6 +1544,167 @@ Hard mode (100 points/hint): --- -## Iteration 15: [Pending] +## Iteration 15: Game Over Logic + +**Date**: 2025-01-22 + +### Goals +- Implement time limit for gameplay +- Implement hint limit based on penalty points +- Provide clear feedback on why game ended +- Make time limit configurable via game options + +### Tasks Completed + +#### 1. Added Time Limit Configuration +**File: `src/types.ts`** + +**GameOptions Extended:** +```typescript +export type GameOptions = { + difficulty?: Difficulty; + timeLimit?: number; // In seconds, default 600 (10 minutes) +}; +``` + +**Default:** 10 minutes (600 seconds) for all difficulty levels +**Rationale:** Harder puzzles naturally take longer, creating appropriate challenge + +#### 2. Implemented Game Over Triggers +**File: `src/useScore/useScore.ts`** + +**Two conditions trigger game-over:** + +**Condition 1: Time Limit Exceeded** +```typescript +const timeLimitMs = timeLimit * 1000; +if (timeValue >= timeLimitMs) { + setGameOverReason("Time's up!"); + setGameStatus('game-over'); + return; +} +``` + +**Condition 2: Hint Penalty Limit** +```typescript +const hintPenalty = hintsUsed * HINT_PENALTY[difficulty]; +if (hintPenalty >= 500) { + setGameOverReason('Too many hints!'); + setGameStatus('game-over'); + return; +} +``` + +**Hint Limits by Difficulty:** +- Easy (25 pts/hint): 20 hints max → 500 points +- Medium (50 pts/hint): 10 hints max → 500 points +- Hard (100 pts/hint): 5 hints max → 500 points + +#### 3. Added Game Over Reason Tracking +**ScoreManager Extended:** +```typescript +export type ScoreManager = { + // ... existing fields + gameOverReason: string | null; // "Time's up!" or "Too many hints!" +}; +``` + +**Reason Management:** +- Set when game-over triggered +- Reset on new game start +- Displayed to player in UI + +#### 4. Updated Game Lifecycle +**Status Flow:** +``` +waiting → started → [time/hints exceeded] → game-over + → [puzzle complete] → game-won + +game-over → [press space] → waiting → started (new game) +``` + +**Timer Behavior:** +- Stops on game-over +- Stops on game-won +- Resets on waiting + +#### 5. Updated Example App Display +**Game Over Message:** +```tsx +{gameStatus === "game-over" && ( +

{gameOverReason || "Game Over!"} Press space to restart

+)} +``` + +Shows specific reason: +- "Time's up! Press space to restart" +- "Too many hints! Press space to restart" + +### Technical Details + +**Check Frequency:** +- Both checks run on every render during 'started' state +- useEffect dependencies: `[gameStatus, timeValue, hintsUsed, difficulty, timeLimit]` +- Checks happen before win condition check + +**Priority:** +1. Game-over checks (time, hints) +2. Win condition check (board complete) + +**Time Limit Examples:** +``` +Default (10 minutes): +- Player has 600 seconds to complete puzzle +- At 600s: game-over triggered + +Custom (5 minutes): +- GameOptions: { timeLimit: 300 } +- Player has 300 seconds to complete +``` + +**Hint Limit Examples:** +``` +Easy mode (25 pts/hint, 500 limit): +- Hint 1-19: OK +- Hint 20: 20 × 25 = 500 → game-over + +Hard mode (100 pts/hint, 500 limit): +- Hint 1-4: OK +- Hint 5: 5 × 100 = 500 → game-over +``` + +### Design Rationale + +**Why Same Time Limit for All Difficulties?** +- Harder puzzles naturally take longer to solve +- Creates appropriate pressure at each level +- Simple and consistent rule + +**Why 500 Point Hint Limit?** +- Half of max score (1000) +- Prevents hint abuse +- Scales appropriately with difficulty: + - Easy: More hints allowed (puzzle easier) + - Hard: Fewer hints allowed (puzzle harder) + +**Why Two Separate Conditions?** +- Time limit: Encourages speed +- Hint limit: Encourages skill +- Both contribute to challenge + +### Build Status +- ✅ TypeScript compiles cleanly +- ✅ Both game-over conditions working +- ✅ Reasons displayed correctly +- ✅ Timer stops on game-over +- ✅ Configurable time limit working + +### Next Steps +- Final polish and testing +- Update UNIFIED_API.md + +--- + +## Iteration 16: [Pending] *To be filled as implementation progresses...* diff --git a/example/src/App.tsx b/example/src/App.tsx index 37b3ed5..84c2b51 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -8,7 +8,7 @@ function App() { const [difficulty, setDifficulty] = useState('easy'); const gameManager = useGame({ difficulty }, useKeyBindings); const { containerRef, gameStatus, scoreManager } = gameManager; - const { timeValue, totalKeystrokes, hintsUsed, finalScore } = scoreManager; + const { timeValue, totalKeystrokes, hintsUsed, finalScore, gameOverReason } = scoreManager; const formatTime = (ms: number) => { const seconds = Math.floor(ms / 1000); @@ -99,7 +99,7 @@ function App() {
{gameStatus === "waiting" &&

Press space to start

} {gameStatus === "started" &&

Use hjkl to navigate • Press i to edit • Press Shift+H for hint • Press q to quit

} - {gameStatus === "game-over" &&

Game Over! Press space to restart

} + {gameStatus === "game-over" &&

{gameOverReason || "Game Over!"} Press space to restart

} {gameStatus === "game-won" &&

You Won! Press space to play again

}
diff --git a/src/types.ts b/src/types.ts index 0bc0d57..dffc416 100644 --- a/src/types.ts +++ b/src/types.ts @@ -11,6 +11,7 @@ export type { ScoreManager } from './useScore/types'; export type GameOptions = { difficulty?: Difficulty; + timeLimit?: number; }; export type KeyLogEntry = { key: string; timestamp: number }; diff --git a/src/useGame.ts b/src/useGame.ts index 1cc2cce..4dcb84e 100644 --- a/src/useGame.ts +++ b/src/useGame.ts @@ -7,7 +7,7 @@ import { useCursorRender } from './useCursor/useCursorRender'; import { useScore } from './useScore'; export function useGame(options: GameOptions = {}, platformHook?: unknown): GameManager { - const { difficulty = 'easy' } = options; + const { difficulty = 'easy', timeLimit = 600 } = options; const board = useBoard(); const { containerRef, renderBoard, generateBoard, boardRendered } = board; @@ -24,7 +24,7 @@ export function useGame(options: GameOptions = {}, platformHook?: unknown): Game const cursorPosition = cursor.position(); const cursorMode = cursor.mode(); - const scoreManager = useScore({ board, gameStatusManager, keyLog, cursorMode, difficulty, hintsUsed: hintsUsedRef.current }); + const scoreManager = useScore({ board, gameStatusManager, keyLog, cursorMode, difficulty, hintsUsed: hintsUsedRef.current, timeLimit }); useCursorRender(containerRef, cursorPosition, cursorMode, gameStatus, boardRendered); diff --git a/src/useScore/types.ts b/src/useScore/types.ts index f5abec4..8ef1218 100644 --- a/src/useScore/types.ts +++ b/src/useScore/types.ts @@ -13,4 +13,5 @@ export type ScoreManager = { totalKeystrokes: number; hintsUsed: number; finalScore: number | null; + gameOverReason: string | null; }; diff --git a/src/useScore/useScore.ts b/src/useScore/useScore.ts index 54c0f61..4b84088 100644 --- a/src/useScore/useScore.ts +++ b/src/useScore/useScore.ts @@ -12,6 +12,7 @@ type UseScoreParams = { cursorMode: 'normal' | 'edit'; difficulty: Difficulty; hintsUsed: number; + timeLimit: number; }; const DIFFICULTY_MULTIPLIER = { @@ -26,26 +27,49 @@ const HINT_PENALTY = { hard: 100, } as const; -export function useScore({ board, gameStatusManager, keyLog, cursorMode, difficulty, hintsUsed: hintsUsedProp }: UseScoreParams): ScoreManager { +export function useScore({ board, gameStatusManager, keyLog, cursorMode, difficulty, hintsUsed: hintsUsedProp, timeLimit }: UseScoreParams): ScoreManager { const timer = useTimer(); const { gameStatus, setGameStatus } = gameStatusManager; const { timeValue, startTimer, stopTimer, resetTimer } = timer; const [finalScore, setFinalScore] = useState(null); + const [gameOverReason, setGameOverReason] = useState(null); useEffect(() => { if (gameStatus === 'started') { startTimer(); + setGameOverReason(null); } else if (gameStatus === 'waiting') { resetTimer(); setFinalScore(null); + setGameOverReason(null); } else if (gameStatus === 'game-won') { stopTimer(); + } else if (gameStatus === 'game-over') { + stopTimer(); } }, [gameStatus]); const totalKeystrokes = keyLog.length; + useEffect(() => { + if (gameStatus !== 'started') return; + + const timeLimitMs = timeLimit * 1000; + if (timeValue >= timeLimitMs) { + setGameOverReason("Time's up!"); + setGameStatus('game-over'); + return; + } + + const hintPenalty = hintsUsedProp * HINT_PENALTY[difficulty]; + if (hintPenalty >= 500) { + setGameOverReason('Too many hints!'); + setGameStatus('game-over'); + return; + } + }, [gameStatus, timeValue, hintsUsedProp, difficulty, timeLimit, setGameStatus]); + useEffect(() => { if (gameStatus !== 'started') return; if (cursorMode !== 'normal') return; @@ -82,5 +106,6 @@ export function useScore({ board, gameStatusManager, keyLog, cursorMode, difficu totalKeystrokes, hintsUsed: hintsUsedProp, finalScore, + gameOverReason, }; }