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:
+196
-1
@@ -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...*
|
||||
|
||||
Reference in New Issue
Block a user