feat: implement game-over logic with time and hint limits
- Add configurable timeLimit to GameOptions (default 600 seconds / 10 minutes) - Time limit applies to all difficulty levels (harder puzzles = more time pressure) - Implement game-over triggers: 1. Time limit exceeded: timeValue >= timeLimit → 'Time's up!' 2. Hint penalty limit: hintsUsed * penalty >= 500 → 'Too many hints!' - Hint limits by difficulty: - Easy: 20 hints max (20 × 25 = 500 points) - Medium: 10 hints max (10 × 50 = 500 points) - Hard: 5 hints max (5 × 100 = 500 points) - Add gameOverReason to ScoreManager to display specific failure reason - Timer stops on game-over - Update example app to show game-over reason - Both conditions checked during gameplay before win condition
This commit is contained in:
+162
-1
@@ -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" && (
|
||||||
|
<p>{gameOverReason || "Game Over!"} Press <kbd>space</kbd> to restart</p>
|
||||||
|
)}
|
||||||
|
```
|
||||||
|
|
||||||
|
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...*
|
*To be filled as implementation progresses...*
|
||||||
|
|||||||
+2
-2
@@ -8,7 +8,7 @@ function App() {
|
|||||||
const [difficulty, setDifficulty] = useState<Difficulty>('easy');
|
const [difficulty, setDifficulty] = useState<Difficulty>('easy');
|
||||||
const gameManager = useGame({ difficulty }, useKeyBindings);
|
const gameManager = useGame({ difficulty }, useKeyBindings);
|
||||||
const { containerRef, gameStatus, scoreManager } = gameManager;
|
const { containerRef, gameStatus, scoreManager } = gameManager;
|
||||||
const { timeValue, totalKeystrokes, hintsUsed, finalScore } = scoreManager;
|
const { timeValue, totalKeystrokes, hintsUsed, finalScore, gameOverReason } = scoreManager;
|
||||||
|
|
||||||
const formatTime = (ms: number) => {
|
const formatTime = (ms: number) => {
|
||||||
const seconds = Math.floor(ms / 1000);
|
const seconds = Math.floor(ms / 1000);
|
||||||
@@ -99,7 +99,7 @@ function App() {
|
|||||||
<div className="text-center text-2xl text-muted-foreground">
|
<div className="text-center text-2xl text-muted-foreground">
|
||||||
{gameStatus === "waiting" && <p>Press <kbd className="px-2 py-1 bg-muted rounded">space</kbd> to start</p>}
|
{gameStatus === "waiting" && <p>Press <kbd className="px-2 py-1 bg-muted rounded">space</kbd> to start</p>}
|
||||||
{gameStatus === "started" && <p>Use <kbd className="px-2 py-1 bg-muted rounded">hjkl</kbd> to navigate • Press <kbd className="px-2 py-1 bg-muted rounded">i</kbd> to edit • Press <kbd className="px-2 py-1 bg-muted rounded">Shift+H</kbd> for hint • Press <kbd className="px-2 py-1 bg-muted rounded">q</kbd> to quit</p>}
|
{gameStatus === "started" && <p>Use <kbd className="px-2 py-1 bg-muted rounded">hjkl</kbd> to navigate • Press <kbd className="px-2 py-1 bg-muted rounded">i</kbd> to edit • Press <kbd className="px-2 py-1 bg-muted rounded">Shift+H</kbd> for hint • Press <kbd className="px-2 py-1 bg-muted rounded">q</kbd> to quit</p>}
|
||||||
{gameStatus === "game-over" && <p>Game Over! Press <kbd className="px-2 py-1 bg-muted rounded">space</kbd> to restart</p>}
|
{gameStatus === "game-over" && <p>{gameOverReason || "Game Over!"} Press <kbd className="px-2 py-1 bg-muted rounded">space</kbd> to restart</p>}
|
||||||
{gameStatus === "game-won" && <p>You Won! Press <kbd className="px-2 py-1 bg-muted rounded">space</kbd> to play again</p>}
|
{gameStatus === "game-won" && <p>You Won! Press <kbd className="px-2 py-1 bg-muted rounded">space</kbd> to play again</p>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export type { ScoreManager } from './useScore/types';
|
|||||||
|
|
||||||
export type GameOptions = {
|
export type GameOptions = {
|
||||||
difficulty?: Difficulty;
|
difficulty?: Difficulty;
|
||||||
|
timeLimit?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type KeyLogEntry = { key: string; timestamp: number };
|
export type KeyLogEntry = { key: string; timestamp: number };
|
||||||
|
|||||||
+2
-2
@@ -7,7 +7,7 @@ import { useCursorRender } from './useCursor/useCursorRender';
|
|||||||
import { useScore } from './useScore';
|
import { useScore } from './useScore';
|
||||||
|
|
||||||
export function useGame(options: GameOptions = {}, platformHook?: unknown): GameManager {
|
export function useGame(options: GameOptions = {}, platformHook?: unknown): GameManager {
|
||||||
const { difficulty = 'easy' } = options;
|
const { difficulty = 'easy', timeLimit = 600 } = options;
|
||||||
const board = useBoard();
|
const board = useBoard();
|
||||||
const { containerRef, renderBoard, generateBoard, boardRendered } = board;
|
const { containerRef, renderBoard, generateBoard, boardRendered } = board;
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@ export function useGame(options: GameOptions = {}, platformHook?: unknown): Game
|
|||||||
const cursorPosition = cursor.position();
|
const cursorPosition = cursor.position();
|
||||||
const cursorMode = cursor.mode();
|
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);
|
useCursorRender(containerRef, cursorPosition, cursorMode, gameStatus, boardRendered);
|
||||||
|
|
||||||
|
|||||||
@@ -13,4 +13,5 @@ export type ScoreManager = {
|
|||||||
totalKeystrokes: number;
|
totalKeystrokes: number;
|
||||||
hintsUsed: number;
|
hintsUsed: number;
|
||||||
finalScore: number | null;
|
finalScore: number | null;
|
||||||
|
gameOverReason: string | null;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ type UseScoreParams = {
|
|||||||
cursorMode: 'normal' | 'edit';
|
cursorMode: 'normal' | 'edit';
|
||||||
difficulty: Difficulty;
|
difficulty: Difficulty;
|
||||||
hintsUsed: number;
|
hintsUsed: number;
|
||||||
|
timeLimit: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
const DIFFICULTY_MULTIPLIER = {
|
const DIFFICULTY_MULTIPLIER = {
|
||||||
@@ -26,26 +27,49 @@ const HINT_PENALTY = {
|
|||||||
hard: 100,
|
hard: 100,
|
||||||
} as const;
|
} 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 timer = useTimer();
|
||||||
const { gameStatus, setGameStatus } = gameStatusManager;
|
const { gameStatus, setGameStatus } = gameStatusManager;
|
||||||
const { timeValue, startTimer, stopTimer, resetTimer } = timer;
|
const { timeValue, startTimer, stopTimer, resetTimer } = timer;
|
||||||
|
|
||||||
const [finalScore, setFinalScore] = useState<number | null>(null);
|
const [finalScore, setFinalScore] = useState<number | null>(null);
|
||||||
|
const [gameOverReason, setGameOverReason] = useState<string | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (gameStatus === 'started') {
|
if (gameStatus === 'started') {
|
||||||
startTimer();
|
startTimer();
|
||||||
|
setGameOverReason(null);
|
||||||
} else if (gameStatus === 'waiting') {
|
} else if (gameStatus === 'waiting') {
|
||||||
resetTimer();
|
resetTimer();
|
||||||
setFinalScore(null);
|
setFinalScore(null);
|
||||||
|
setGameOverReason(null);
|
||||||
} else if (gameStatus === 'game-won') {
|
} else if (gameStatus === 'game-won') {
|
||||||
stopTimer();
|
stopTimer();
|
||||||
|
} else if (gameStatus === 'game-over') {
|
||||||
|
stopTimer();
|
||||||
}
|
}
|
||||||
}, [gameStatus]);
|
}, [gameStatus]);
|
||||||
|
|
||||||
const totalKeystrokes = keyLog.length;
|
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(() => {
|
useEffect(() => {
|
||||||
if (gameStatus !== 'started') return;
|
if (gameStatus !== 'started') return;
|
||||||
if (cursorMode !== 'normal') return;
|
if (cursorMode !== 'normal') return;
|
||||||
@@ -82,5 +106,6 @@ export function useScore({ board, gameStatusManager, keyLog, cursorMode, difficu
|
|||||||
totalKeystrokes,
|
totalKeystrokes,
|
||||||
hintsUsed: hintsUsedProp,
|
hintsUsed: hintsUsedProp,
|
||||||
finalScore,
|
finalScore,
|
||||||
|
gameOverReason,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user