feat: implement hint system (Shift+H) with scoring penalties

- Add getHint() method to BoardManager with two-tier priority:
  1. Find incorrect user-entered cells → flash red
  2. Find empty cells → show correct digit green temporarily
- Add Shift+H key handler in useCursor (normal mode only)
- Visual feedback:
  - Red flash (hint-flash-error): incorrect cells, 1s duration, red bg + border
  - Green flash (hint-flash-correct): correct digit shown, 1s duration, green bg + border
  - Distinct from invalid-move (no background, 500ms)
- Score penalties based on difficulty:
  - Easy: 25 points per hint
  - Medium: 50 points per hint
  - Hard: 100 points per hint
- Track hints used in scoreboard display
- Correct digit appears temporarily (1s) then disappears (player still does the work)
- Update example app to show hints counter and Shift+H instruction
This commit is contained in:
2025-10-22 21:24:06 +01:00
parent cfdf8100fb
commit 97c31ab6cc
9 changed files with 345 additions and 9 deletions
+196 -1
View File
@@ -1349,6 +1349,201 @@ Hard (50 cells):
---
## Iteration 14: [Pending]
## Iteration 14: Hint System
**Date**: 2025-01-22
### Goals
- Implement hint system to help stuck players
- Two-tier hint priority (errors first, then correct digits)
- Apply scoring penalties for hint usage
- Visual feedback with distinct flash animations
### Tasks Completed
#### 1. Added getHint Method to BoardManager
**File: `src/useBoard/useBoard.ts`**
**Logic:**
1. Check all user-entered cells against solution
2. Find incorrect cells (user value ≠ solution value)
3. If incorrect cells exist → return random incorrect cell
4. If no incorrect cells → return random empty cell with correct digit
**Return Type:**
```typescript
type HintResult = {
row: number;
col: number;
type: 'error' | 'correct';
value?: string; // Correct digit (for 'correct' type)
} | null;
```
**Implementation:**
```typescript
function getHint(): HintResult {
const incorrectCells = []; // User-entered cells with wrong values
const emptyCells = []; // Empty cells to fill
// Priority 1: Find incorrect user entries
if (incorrectCells.length > 0) {
return { row, col, type: 'error' };
}
// Priority 2: Show correct digit for empty cell
if (emptyCells.length > 0) {
return { row, col, type: 'correct', value: correctDigit };
}
return null; // Puzzle complete
}
```
#### 2. Added Shift+H Key Handler
**File: `src/useCursor/useCursor.ts`**
**Key binding:** `Shift+H` (capital H)
**Mode:** Only works in normal mode
**Behavior:**
```typescript
if (key === 'H') {
const hint = boardManager.getHint();
if (!hint) return; // No hints available
if (onHintUsed) onHintUsed(); // Increment hint counter
if (hint.type === 'error') {
// Flash red for 1 second
targetCell.classList.add('hint-flash-error');
setTimeout(() => targetCell.classList.remove('hint-flash-error'), 1000);
} else if (hint.type === 'correct') {
// Show digit temporarily for 1 second
targetCell.textContent = hint.value;
targetCell.classList.add('hint-flash-correct');
setTimeout(() => {
targetCell.classList.remove('hint-flash-correct');
targetCell.textContent = ''; // Remove digit
}, 1000);
}
}
```
#### 3. Added Hint Tracking to Score
**File: `src/useScore/useScore.ts`**
**Penalty Structure:**
```typescript
const HINT_PENALTY = {
easy: 25, // 25 points per hint
medium: 50, // 50 points per hint
hard: 100, // 100 points per hint
};
```
**Score Calculation:**
```typescript
const timePenalty = seconds / 10;
const keystrokePenalty = totalKeystrokes / 2;
const hintPenalty = hintsUsed * HINT_PENALTY[difficulty];
const baseScore = 1000 - timePenalty - keystrokePenalty - hintPenalty;
const finalScore = min(1000, max(0, round(baseScore * multiplier)));
```
**Tracking:**
- Hints counter passed from useGame to useScore
- Displayed in scoreboard during gameplay
- Applied to final score calculation
#### 4. Added CSS Animations
**File: `src/game.css`**
**Error Hint Flash (Red):**
```css
.sudoku-cell.hint-flash-error {
background-color: rgba(239, 68, 68, 0.5);
outline: 3px solid rgb(239, 68, 68);
outline-offset: -3px;
animation: hint-error-flash 1s ease-out;
}
```
- Red border AND background (more obnoxious than invalid-move)
- 1 second animation duration
- Fades from bright to transparent
**Correct Hint Flash (Green):**
```css
.sudoku-cell.hint-flash-correct {
background-color: rgba(34, 197, 94, 0.5);
outline: 3px solid rgb(34, 197, 94);
outline-offset: -3px;
animation: hint-correct-flash 1s ease-out;
}
```
- Green border AND background
- 1 second animation duration
- Shows correct digit temporarily
**Comparison:**
- `.invalid-move` - Red border only, 500ms (boundary violations)
- `.hint-flash-error` - Red border + background, 1000ms (incorrect cells)
- `.hint-flash-correct` - Green border + background, 1000ms (correct hints)
#### 5. Updated Example App
**Scoreboard Display:**
- Added "Hints: X" counter
- Shows hint usage during gameplay
- Included in score calculation
**Instructions Updated:**
- Added "Press Shift+H for hint" to gameplay instructions
### Technical Details
**Hint Priority Logic:**
```
Press Shift+H
↓
Check for incorrect user-entered cells
↓
Found? → Flash random incorrect cell RED (1s)
↓
Not found? → Check for empty cells
↓
Found? → Flash random empty cell GREEN with correct digit (1s)
↓
Not found? → Do nothing (puzzle complete)
```
**Score Impact Examples:**
```
Easy mode (25 points/hint):
- 3 hints used = -75 points from base score
- 10 hints used = -250 points from base score
Hard mode (100 points/hint):
- 3 hints used = -300 points from base score
- 5 hints used = -500 points from base score
```
**Architecture Decision:**
- Hints counter managed in useGame (not useScore)
- Passed to useScore as prop to avoid circular dependency
- onHintUsed callback passed to useCursor for increment
### Build Status
- ✅ TypeScript compiles cleanly
- ✅ Hint system working perfectly
- ✅ Visual feedback distinct and clear
- ✅ Score penalties applied correctly
### Next Steps
- Final polish and testing
- Documentation updates
---
## Iteration 15: [Pending]
*To be filled as implementation progresses...*