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...*
|
||||
|
||||
+2
-2
@@ -8,7 +8,7 @@ function App() {
|
||||
const [difficulty, setDifficulty] = useState<Difficulty>('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() {
|
||||
<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 === "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>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -11,6 +11,7 @@ export type { ScoreManager } from './useScore/types';
|
||||
|
||||
export type GameOptions = {
|
||||
difficulty?: Difficulty;
|
||||
timeLimit?: number;
|
||||
};
|
||||
|
||||
export type KeyLogEntry = { key: string; timestamp: number };
|
||||
|
||||
+2
-2
@@ -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);
|
||||
|
||||
|
||||
@@ -13,4 +13,5 @@ export type ScoreManager = {
|
||||
totalKeystrokes: number;
|
||||
hintsUsed: number;
|
||||
finalScore: number | null;
|
||||
gameOverReason: string | null;
|
||||
};
|
||||
|
||||
@@ -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<number | null>(null);
|
||||
const [gameOverReason, setGameOverReason] = useState<string | null>(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,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user