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...*
|
*To be filled as implementation progresses...*
|
||||||
|
|||||||
+7
-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, finalScore } = scoreManager;
|
const { timeValue, totalKeystrokes, hintsUsed, finalScore } = scoreManager;
|
||||||
|
|
||||||
const formatTime = (ms: number) => {
|
const formatTime = (ms: number) => {
|
||||||
const seconds = Math.floor(ms / 1000);
|
const seconds = Math.floor(ms / 1000);
|
||||||
@@ -76,6 +76,11 @@ function App() {
|
|||||||
<div className="text-muted-foreground">Keystrokes</div>
|
<div className="text-muted-foreground">Keystrokes</div>
|
||||||
<div className="text-lg font-bold">{totalKeystrokes}</div>
|
<div className="text-lg font-bold">{totalKeystrokes}</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="w-px h-12 bg-muted-foreground/20" />
|
||||||
|
<div>
|
||||||
|
<div className="text-muted-foreground">Hints</div>
|
||||||
|
<div className="text-lg font-bold">{hintsUsed}</div>
|
||||||
|
</div>
|
||||||
{gameStatus === "game-won" && finalScore !== null && (
|
{gameStatus === "game-won" && finalScore !== null && (
|
||||||
<>
|
<>
|
||||||
<div className="w-px h-12 bg-muted-foreground/20" />
|
<div className="w-px h-12 bg-muted-foreground/20" />
|
||||||
@@ -93,7 +98,7 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
<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">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>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>
|
||||||
|
|||||||
@@ -273,3 +273,41 @@
|
|||||||
box-shadow: 0 0 0 rgba(239, 68, 68, 0);
|
box-shadow: 0 0 0 rgba(239, 68, 68, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Hint flash - incorrect cell */
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes hint-error-flash {
|
||||||
|
0% {
|
||||||
|
background-color: rgba(239, 68, 68, 0.7);
|
||||||
|
box-shadow: 0 0 30px rgba(239, 68, 68, 0.9);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-color: rgba(239, 68, 68, 0);
|
||||||
|
box-shadow: 0 0 0 rgba(239, 68, 68, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hint flash - correct digit shown */
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes hint-correct-flash {
|
||||||
|
0% {
|
||||||
|
background-color: rgba(34, 197, 94, 0.7);
|
||||||
|
box-shadow: 0 0 30px rgba(34, 197, 94, 0.9);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-color: rgba(34, 197, 94, 0);
|
||||||
|
box-shadow: 0 0 0 rgba(34, 197, 94, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
import type { RefObject } from 'react';
|
import type { RefObject } from 'react';
|
||||||
import type { Difficulty } from '../types';
|
import type { Difficulty } from '../types';
|
||||||
|
|
||||||
|
export type HintResult = {
|
||||||
|
row: number;
|
||||||
|
col: number;
|
||||||
|
type: 'error' | 'correct';
|
||||||
|
value?: string;
|
||||||
|
} | null;
|
||||||
|
|
||||||
export type BoardManager = {
|
export type BoardManager = {
|
||||||
containerRef: RefObject<HTMLDivElement | null>;
|
containerRef: RefObject<HTMLDivElement | null>;
|
||||||
boardRef: RefObject<string>;
|
boardRef: RefObject<string>;
|
||||||
@@ -8,4 +15,5 @@ export type BoardManager = {
|
|||||||
generateBoard: (difficulty: Difficulty) => void;
|
generateBoard: (difficulty: Difficulty) => void;
|
||||||
renderBoard: () => void;
|
renderBoard: () => void;
|
||||||
setCell: (row: number, col: number, value: string) => void;
|
setCell: (row: number, col: number, value: string) => void;
|
||||||
|
getHint: () => HintResult;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useRef, useState } from 'react';
|
|||||||
import { SudokuGenerator } from './SudokuGenerator';
|
import { SudokuGenerator } from './SudokuGenerator';
|
||||||
import { SudokuRenderer } from './SudokuRenderer';
|
import { SudokuRenderer } from './SudokuRenderer';
|
||||||
import type { Difficulty } from './SudokuGenerator';
|
import type { Difficulty } from './SudokuGenerator';
|
||||||
|
import type { HintResult } from './types';
|
||||||
|
|
||||||
export function useBoard() {
|
export function useBoard() {
|
||||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||||
@@ -54,6 +55,47 @@ export function useBoard() {
|
|||||||
renderBoard();
|
renderBoard();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getHint(): HintResult {
|
||||||
|
const board = boardRef.current;
|
||||||
|
const solution = solutionRef.current;
|
||||||
|
const initialBoard = initialBoardRef.current;
|
||||||
|
|
||||||
|
if (!board || !solution) return null;
|
||||||
|
|
||||||
|
const incorrectCells: Array<{ row: number; col: number }> = [];
|
||||||
|
const emptyCells: Array<{ row: number; col: number }> = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < 81; i++) {
|
||||||
|
const row = Math.floor(i / 9);
|
||||||
|
const col = i % 9;
|
||||||
|
const currentValue = board[i];
|
||||||
|
const correctValue = solution[i];
|
||||||
|
const initialValue = initialBoard[i];
|
||||||
|
|
||||||
|
if (currentValue !== '.' && initialValue === '.' && currentValue !== correctValue) {
|
||||||
|
incorrectCells.push({ row, col });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentValue === '.') {
|
||||||
|
emptyCells.push({ row, col });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (incorrectCells.length > 0) {
|
||||||
|
const randomCell = incorrectCells[Math.floor(Math.random() * incorrectCells.length)];
|
||||||
|
return { ...randomCell, type: 'error' };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (emptyCells.length > 0) {
|
||||||
|
const randomCell = emptyCells[Math.floor(Math.random() * emptyCells.length)];
|
||||||
|
const index = randomCell.row * 9 + randomCell.col;
|
||||||
|
const correctValue = solution[index];
|
||||||
|
return { ...randomCell, type: 'correct', value: correctValue };
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
containerRef,
|
containerRef,
|
||||||
boardRef,
|
boardRef,
|
||||||
@@ -61,5 +103,6 @@ export function useBoard() {
|
|||||||
generateBoard,
|
generateBoard,
|
||||||
renderBoard,
|
renderBoard,
|
||||||
setCell,
|
setCell,
|
||||||
|
getHint,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import type { GameStatusManager } from '../useGameStatus/types';
|
|||||||
import type { BoardManager } from '../useBoard/types';
|
import type { BoardManager } from '../useBoard/types';
|
||||||
import { useVimMotions } from './useVimMotions';
|
import { useVimMotions } from './useVimMotions';
|
||||||
|
|
||||||
export function useCursor(gameStatusManager: GameStatusManager, containerRef: React.RefObject<HTMLDivElement | null>, boardManager: BoardManager) {
|
export function useCursor(gameStatusManager: GameStatusManager, containerRef: React.RefObject<HTMLDivElement | null>, boardManager: BoardManager, onHintUsed?: () => void) {
|
||||||
const [position, setPosition] = useState<Coord>({ row: 0, col: 0 });
|
const [position, setPosition] = useState<Coord>({ row: 0, col: 0 });
|
||||||
const [mode, setMode] = useState<CursorMode>('normal');
|
const [mode, setMode] = useState<CursorMode>('normal');
|
||||||
const [singleEdit, setSingleEdit] = useState<boolean>(false);
|
const [singleEdit, setSingleEdit] = useState<boolean>(false);
|
||||||
@@ -159,6 +159,39 @@ export function useCursor(gameStatusManager: GameStatusManager, containerRef: Re
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (key === 'H') {
|
||||||
|
const hint = boardManager.getHint();
|
||||||
|
if (!hint) return;
|
||||||
|
|
||||||
|
if (onHintUsed) onHintUsed();
|
||||||
|
|
||||||
|
const container = containerRef.current;
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
const cells = container.querySelectorAll('.sudoku-cell');
|
||||||
|
const index = hint.row * 9 + hint.col;
|
||||||
|
const targetCell = cells[index] as HTMLElement;
|
||||||
|
|
||||||
|
if (!targetCell) return;
|
||||||
|
|
||||||
|
if (hint.type === 'error') {
|
||||||
|
targetCell.classList.add('hint-flash-error');
|
||||||
|
setTimeout(() => {
|
||||||
|
targetCell.classList.remove('hint-flash-error');
|
||||||
|
}, 1000);
|
||||||
|
} else if (hint.type === 'correct' && hint.value) {
|
||||||
|
const originalText = targetCell.textContent;
|
||||||
|
targetCell.textContent = hint.value;
|
||||||
|
targetCell.classList.add('hint-flash-correct');
|
||||||
|
setTimeout(() => {
|
||||||
|
targetCell.classList.remove('hint-flash-correct');
|
||||||
|
targetCell.textContent = originalText;
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (key === 'i' || key === 'r' || key === 'c') {
|
if (key === 'i' || key === 'r' || key === 'c') {
|
||||||
const container = containerRef.current;
|
const container = containerRef.current;
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
|
|||||||
+6
-2
@@ -16,11 +16,15 @@ export function useGame(options: GameOptions = {}, platformHook?: unknown): Game
|
|||||||
|
|
||||||
const [keyLog, setKeyLog] = useState<{ key: string; timestamp: number }[]>([]);
|
const [keyLog, setKeyLog] = useState<{ key: string; timestamp: number }[]>([]);
|
||||||
|
|
||||||
const cursor = useCursor(gameStatusManager, containerRef, board);
|
const [hintsUsedRef] = useState({ current: 0 });
|
||||||
|
|
||||||
|
const cursor = useCursor(gameStatusManager, containerRef, board, () => {
|
||||||
|
hintsUsedRef.current++;
|
||||||
|
});
|
||||||
const cursorPosition = cursor.position();
|
const cursorPosition = cursor.position();
|
||||||
const cursorMode = cursor.mode();
|
const cursorMode = cursor.mode();
|
||||||
|
|
||||||
const scoreManager = useScore({ board, gameStatusManager, keyLog, cursorMode, difficulty });
|
const scoreManager = useScore({ board, gameStatusManager, keyLog, cursorMode, difficulty, hintsUsed: hintsUsedRef.current });
|
||||||
|
|
||||||
useCursorRender(containerRef, cursorPosition, cursorMode, gameStatus, boardRendered);
|
useCursorRender(containerRef, cursorPosition, cursorMode, gameStatus, boardRendered);
|
||||||
|
|
||||||
|
|||||||
@@ -11,5 +11,6 @@ export type ScoreManager = {
|
|||||||
stopTimer: () => void;
|
stopTimer: () => void;
|
||||||
resetTimer: () => void;
|
resetTimer: () => void;
|
||||||
totalKeystrokes: number;
|
totalKeystrokes: number;
|
||||||
|
hintsUsed: number;
|
||||||
finalScore: number | null;
|
finalScore: number | null;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ type UseScoreParams = {
|
|||||||
keyLog: { key: string; timestamp: number }[];
|
keyLog: { key: string; timestamp: number }[];
|
||||||
cursorMode: 'normal' | 'edit';
|
cursorMode: 'normal' | 'edit';
|
||||||
difficulty: Difficulty;
|
difficulty: Difficulty;
|
||||||
|
hintsUsed: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
const DIFFICULTY_MULTIPLIER = {
|
const DIFFICULTY_MULTIPLIER = {
|
||||||
@@ -19,7 +20,13 @@ const DIFFICULTY_MULTIPLIER = {
|
|||||||
hard: 2.0,
|
hard: 2.0,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export function useScore({ board, gameStatusManager, keyLog, cursorMode, difficulty }: UseScoreParams): ScoreManager {
|
const HINT_PENALTY = {
|
||||||
|
easy: 25,
|
||||||
|
medium: 50,
|
||||||
|
hard: 100,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export function useScore({ board, gameStatusManager, keyLog, cursorMode, difficulty, hintsUsed: hintsUsedProp }: 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;
|
||||||
@@ -58,13 +65,14 @@ export function useScore({ board, gameStatusManager, keyLog, cursorMode, difficu
|
|||||||
const seconds = timeValue / 1000;
|
const seconds = timeValue / 1000;
|
||||||
const timePenalty = seconds / 10;
|
const timePenalty = seconds / 10;
|
||||||
const keystrokePenalty = totalKeystrokes / 2;
|
const keystrokePenalty = totalKeystrokes / 2;
|
||||||
|
const hintPenalty = hintsUsedProp * HINT_PENALTY[difficulty];
|
||||||
const multiplier = DIFFICULTY_MULTIPLIER[difficulty];
|
const multiplier = DIFFICULTY_MULTIPLIER[difficulty];
|
||||||
|
|
||||||
const baseScore = 1000 - timePenalty - keystrokePenalty;
|
const baseScore = 1000 - timePenalty - keystrokePenalty - hintPenalty;
|
||||||
const score = Math.min(1000, Math.max(0, Math.round(baseScore * multiplier)));
|
const score = Math.min(1000, Math.max(0, Math.round(baseScore * multiplier)));
|
||||||
|
|
||||||
setFinalScore(score);
|
setFinalScore(score);
|
||||||
}, [gameStatus, timeValue, totalKeystrokes, difficulty]);
|
}, [gameStatus, timeValue, totalKeystrokes, hintsUsedProp, difficulty]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
timeValue,
|
timeValue,
|
||||||
@@ -72,6 +80,7 @@ export function useScore({ board, gameStatusManager, keyLog, cursorMode, difficu
|
|||||||
stopTimer,
|
stopTimer,
|
||||||
resetTimer,
|
resetTimer,
|
||||||
totalKeystrokes,
|
totalKeystrokes,
|
||||||
|
hintsUsed: hintsUsedProp,
|
||||||
finalScore,
|
finalScore,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user