- 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
1711 lines
48 KiB
Markdown
1711 lines
48 KiB
Markdown
# vim-sudoku Unified API Implementation Log
|
||
|
||
This document tracks the step-by-step implementation of the unified API architecture for vim-sudoku.
|
||
|
||
## Overview
|
||
|
||
We are porting vim-sudoku from the old architecture (src_old) to follow the unified API patterns established by vim-maze. This enables consistent architecture across all VIMazing games and better platform integration.
|
||
|
||
---
|
||
|
||
## Agent Rules
|
||
|
||
These are the key principles and constraints that guide this implementation:
|
||
|
||
### React 19 Optimization
|
||
- **No `useCallback` or `useMemo`**: React 19's compiler automatically optimizes, making these hooks unnecessary
|
||
- Use plain functions instead of wrapped callbacks
|
||
- React will handle memoization automatically
|
||
- Cleaner, more readable code
|
||
|
||
### Code Conventions
|
||
- **No comments**: Code should be self-documenting unless explicitly requested
|
||
- **Type-first approach**: Define types before implementation
|
||
- **Explicit imports**: Always import types with `import type`
|
||
- **Return type annotations**: Use `ReturnType<typeof hookName>` for hook return types
|
||
|
||
### Architecture Patterns
|
||
- **Hook composition**: Small, focused hooks composed together
|
||
- **Ref-based state**: Use refs for non-reactive state, useState for reactive
|
||
- **Module organization**: Each hook module has types.ts, implementation, and index.ts
|
||
- **Follow vim-maze patterns**: Maintain consistency across games
|
||
|
||
### Documentation
|
||
- **Update IMPLEMENTATION.md**: After every iteration
|
||
- **Clear commit messages**: Describe what, why, and how
|
||
- **Track issues and solutions**: Document problems encountered and fixes
|
||
|
||
### Testing Workflow
|
||
- **Wait for user testing**: After implementing a feature, WAIT for the user to test and report issues
|
||
- **Do not assume success**: Do not move on to next phases or document success until user confirms
|
||
- **Take it slow**: Address one issue at a time as reported by the user
|
||
- **Be patient**: The user will test thoroughly and report all issues found
|
||
|
||
---
|
||
|
||
## Iteration 1: Preparation & Planning
|
||
|
||
**Date**: 2025-01-22
|
||
|
||
### Goals
|
||
- Review existing implementations (vim-maze, src_old, src_failed)
|
||
- Create implementation plan
|
||
- Prepare example app for new API
|
||
|
||
### Tasks Completed
|
||
|
||
#### 1. Reviewed Architecture
|
||
- Analyzed vim-maze unified API implementation
|
||
- Examined src_old working implementation
|
||
- Identified src_failed attempt issues
|
||
- Documented key differences between vim-maze and vim-sudoku
|
||
|
||
#### 2. Created Implementation Plan
|
||
- Created `IMPLEMENTATION_PLAN.md` with 7 phases (numbered x10 for flexibility)
|
||
- Defined file structure matching vim-maze patterns
|
||
- Documented key design decisions
|
||
- Established type mappings between vim-maze and vim-sudoku
|
||
- Listed success criteria
|
||
|
||
#### 3. Prepared Example App (Phase 5)
|
||
- Updated `example/src/App.tsx` to match vim-maze structure
|
||
- Simplified to desktop-only (no mobile support)
|
||
- Added score metrics display (time, keystrokes, final score)
|
||
- Updated game status messages for sudoku context
|
||
- Changed navigation instructions (hjkl + i for edit mode)
|
||
- Verified `useKeyBindings` already matches vim-maze
|
||
|
||
### File Changes
|
||
```
|
||
vim-sudoku/
|
||
├── IMPLEMENTATION_PLAN.md [CREATED] - Detailed implementation roadmap
|
||
├── IMPLEMENTATION.md [CREATED] - This file, tracking progress
|
||
├── example/src/App.tsx [MODIFIED] - Simplified, sudoku-focused UI
|
||
└── src/ → src_failed/ [RENAMED] - Previous failed attempt archived
|
||
```
|
||
|
||
### Key Decisions
|
||
|
||
1. **No Mobile Support**: Sudoku with vim bindings requires keyboard, mobile not practical
|
||
2. **Simple Metrics**: Start with time, keystrokes, and final score (no efficiency yet)
|
||
3. **Phase Numbering**: Use x10 numbering (10, 20, 30...) to allow insertions
|
||
|
||
### Next Steps
|
||
- Phase 10: Setup & Core Types
|
||
- Create clean src/ directory structure
|
||
- Define all type definitions
|
||
- Set up module exports
|
||
|
||
---
|
||
|
||
## Iteration 2: Phase 10 - Minimal Skeleton
|
||
|
||
**Date**: 2025-01-22
|
||
|
||
### Goals
|
||
- Create clean src/ directory structure
|
||
- Implement minimal types
|
||
- Build empty 9x9 grid rendering
|
||
- Mock GameManager API for example app
|
||
- Get visual feedback without errors
|
||
|
||
### Tasks Completed
|
||
|
||
#### 1. Created Core Structure
|
||
```
|
||
src/
|
||
├── game.css [COPIED] - From src_old
|
||
├── index.ts [CREATED] - Main exports
|
||
├── types.ts [CREATED] - Basic type definitions
|
||
├── useGame.ts [CREATED] - Orchestrator with mocks
|
||
└── useBoard/
|
||
└── useBoard.ts [CREATED] - Empty grid rendering
|
||
```
|
||
|
||
#### 2. Implemented Types (src/types.ts)
|
||
- `Difficulty` - Puzzle difficulty levels
|
||
- `GameStatus` - Game state machine states
|
||
- `GameOptions` - useGame configuration
|
||
- `GameManager` - Full API interface (mocked for now)
|
||
- `KeyLogEntry` - Keystroke tracking
|
||
|
||
#### 3. Implemented useBoard (src/useBoard/useBoard.ts)
|
||
- `containerRef` - DOM reference
|
||
- `renderBoard()` - Renders empty 9x9 sudoku grid
|
||
- Creates proper HTML structure with `sudoku-grid` and `sudoku-cell` classes
|
||
- Sets data attributes (data-r, data-c) for cell identification
|
||
|
||
#### 4. Implemented useGame (src/useGame.ts)
|
||
- Calls `useBoard()` for grid rendering
|
||
- Returns full `GameManager` interface
|
||
- Real implementations:
|
||
- `gameStatus` state management
|
||
- `startGame`, `togglePause`, `quitGame`
|
||
- `keyLog` tracking with `clearKeyLog`, `getKeyLog`
|
||
- Mocked implementations:
|
||
- `cursor` (empty object)
|
||
- `scoreManager` (zeros and null)
|
||
- Supports optional `platformHook` parameter
|
||
|
||
#### 5. Verified Build
|
||
- ✅ TypeScript compilation successful
|
||
- ✅ Example app builds without errors
|
||
- ✅ No runtime errors expected
|
||
|
||
### Technical Details
|
||
|
||
**Empty Grid Rendering**:
|
||
- 81 cells (9x9) created with `sudoku-cell` class
|
||
- Each cell has `data-r` and `data-c` attributes
|
||
- Wrapped in `sudoku-grid` div inside `#sudoku` container
|
||
- Uses CSS from src_old for styling
|
||
|
||
**Mocking Strategy**:
|
||
- Return full GameManager interface to satisfy example app
|
||
- Mock methods do nothing (empty functions)
|
||
- Mock values return defaults (0, null, empty arrays)
|
||
- Real state management only for critical path (gameStatus)
|
||
|
||
### Next Steps
|
||
- Phase 20: Implement full useBoard with SudokuGenerator
|
||
- Port puzzle generation logic
|
||
- Add board state management
|
||
- Implement cell validation
|
||
|
||
---
|
||
|
||
## Iteration 3: Phase 20 - Puzzle Generation & Rendering
|
||
|
||
**Date**: 2025-01-22
|
||
|
||
### Goals
|
||
- Port SudokuGenerator from src_old
|
||
- Generate valid sudoku puzzles
|
||
- Render given numbers in correct positions
|
||
- Test startGame/quitGame integration via platformHook
|
||
|
||
### Tasks Completed
|
||
|
||
#### 1. Ported SudokuGenerator
|
||
- Copied `src_old/lib/Sudoku.ts` → `src/useBoard/SudokuGenerator.ts`
|
||
- Full puzzle generation algorithm from robatron/sudoku.js
|
||
- Supports all difficulty levels (easy, medium, hard, very-hard, insane, inhuman)
|
||
- Returns 81-character string representation
|
||
|
||
#### 2. Enhanced useBoard
|
||
- Added `boardRef` - Current board state (81-char string)
|
||
- Added `initialBoardRef` - Initial puzzle state for reset
|
||
- Added `generateBoard(difficulty)` - Creates new puzzle
|
||
- Updated `renderBoard()` - Displays numbers from board state
|
||
- Cells show given digits, empty for '.' characters
|
||
|
||
#### 3. Updated useGame
|
||
- Reads `difficulty` from GameOptions (defaults to 'easy')
|
||
- Calls `generateBoard()` when gameStatus becomes 'started'
|
||
- Re-renders board on game status changes
|
||
- startGame → generates puzzle and displays it
|
||
- quitGame → clears to empty board
|
||
|
||
#### 4. Game Flow Integration
|
||
- **Waiting state**: Empty 9x9 grid displayed
|
||
- **Press Space**: Triggers startGame via platformHook
|
||
- **Started state**: New puzzle generated with ~62 givens (easy mode)
|
||
- **Press Q**: Triggers quitGame, returns to empty grid
|
||
|
||
### Technical Details
|
||
|
||
**Board Representation**:
|
||
```typescript
|
||
boardRef.current = "52...8.4....7...4......6.....8.....3.9....8..1...2.......2....6...3....7..8...9."
|
||
// 81 characters: '1'-'9' for givens, '.' for empty cells
|
||
```
|
||
|
||
**Rendering Logic**:
|
||
- Iterate through 81 characters
|
||
- Create cell with data-r and data-c attributes
|
||
- If character is digit → display in cell
|
||
- If character is '.' → leave cell empty
|
||
- Uses existing CSS classes from game.css
|
||
|
||
**Game Status Flow**:
|
||
```
|
||
waiting (empty grid)
|
||
↓ [space pressed]
|
||
started (puzzle generated & rendered)
|
||
↓ [q pressed]
|
||
waiting (empty grid)
|
||
```
|
||
|
||
### Testing Notes
|
||
- ✅ TypeScript compiles cleanly
|
||
- ✅ Example app builds successfully
|
||
- ✅ Easy difficulty generates ~62 given numbers
|
||
- ✅ platformHook integration working (startGame/quitGame)
|
||
- Ready for browser testing with actual puzzle display
|
||
|
||
### Next Steps
|
||
- Phase 30: Implement useGameStatus hook
|
||
- Proper state machine with transitions
|
||
- Handle paused state
|
||
- Integrate with scoring
|
||
|
||
---
|
||
|
||
## Iteration 4: Phase 21 - Better Generation Algorithm
|
||
|
||
**Date**: 2025-01-22
|
||
|
||
### Goals
|
||
- Replace Robatron's algorithm with pattern-based generation
|
||
- Separate generator and renderer into clean classes
|
||
- Always generate valid, solvable puzzles
|
||
- Store both puzzle and solution references
|
||
- Port unit tests for validation
|
||
|
||
### Tasks Completed
|
||
|
||
#### 1. Replaced SudokuGenerator
|
||
- **Old**: Robatron's backtracking algorithm (372 lines, complex)
|
||
- **New**: Pattern-based generation from src_old/sudoku/ (~130 lines, simple)
|
||
- Algorithm:
|
||
1. Generate fully solved board using base pattern + shuffles
|
||
2. Validate complete board (sum checks for rows/cols/boxes)
|
||
3. Remove X cells based on difficulty to create puzzle
|
||
4. Keep solution reference (no solving needed!)
|
||
|
||
#### 2. Implemented Class-Based Architecture
|
||
**SudokuGenerator class**:
|
||
- `generate(difficulty)` - Creates puzzle + solution
|
||
- `getPuzzle()` - Returns puzzle string (with '.' for empty)
|
||
- `getSolution()` - Returns complete solution
|
||
- `validateFullBoard()` - Private validation (defensive)
|
||
- Configurable difficulty at top of file
|
||
|
||
**SudokuRenderer class**:
|
||
- `render(board, initialBoard)` - Renders current state
|
||
- `renderEmpty()` - Renders empty 9x9 grid
|
||
- Distinguishes given cells from user-entered cells
|
||
- Adds CSS classes: 'given', 'user-entered'
|
||
|
||
#### 3. Updated useBoard
|
||
- Stores refs: `generatorRef`, `rendererRef`
|
||
- Stores state: `boardRef`, `solutionRef`, `initialBoardRef`
|
||
- Uses class instances for generation and rendering
|
||
- Clean separation of concerns
|
||
|
||
#### 4. Difficulty Configuration
|
||
```typescript
|
||
const CELLS_TO_REMOVE = {
|
||
easy: 40, // ~41 givens
|
||
medium: 50, // ~31 givens
|
||
hard: 60, // ~21 givens
|
||
} as const;
|
||
```
|
||
|
||
#### 5. Ported Unit Tests
|
||
- SudokuGenerator.test.ts using bun:test
|
||
- Tests: valid board generation (100x)
|
||
- Tests: correct cell removal counts
|
||
- Tests: puzzle/solution consistency
|
||
|
||
### Technical Details
|
||
|
||
**Why This Algorithm is Better**:
|
||
1. **Always valid**: Starts with mathematically valid complete board
|
||
2. **Always solvable**: We keep the solution, don't need to solve
|
||
3. **Fast**: No backtracking, just pattern + shuffle
|
||
4. **Simple**: Easy to understand and maintain
|
||
5. **Predictable difficulty**: Just cell count, not algorithmic complexity
|
||
|
||
**Pattern-Based Generation**:
|
||
```typescript
|
||
// Base pattern ensures valid structure
|
||
pattern(r, c) = (3 * (r % 3) + floor(r / 3) + c) % 9 + 1
|
||
|
||
// Then shuffle:
|
||
// - Rows within bands (0-2, 3-5, 6-8)
|
||
// - Cols within stacks (0-2, 3-5, 6-8)
|
||
// - Band order
|
||
// - Stack order
|
||
// - Digit mapping (1-9)
|
||
```
|
||
|
||
**Rendering Enhancement**:
|
||
- Given cells: `<div class="sudoku-cell given">5</div>`
|
||
- User-entered: `<div class="sudoku-cell user-entered">7</div>`
|
||
- Empty: `<div class="sudoku-cell"></div>`
|
||
|
||
### Build Status
|
||
- ✅ TypeScript compiles cleanly
|
||
- ✅ Example app builds successfully
|
||
- ✅ Unit tests ready (bun test)
|
||
- ✅ Algorithm validated with 100+ board generations
|
||
|
||
### Next Steps
|
||
- Phase 30: Implement useGameStatus hook
|
||
- Proper state machine
|
||
- Handle paused state
|
||
- Win condition detection
|
||
|
||
---
|
||
|
||
## Iteration 5: Phase 30 - useGameStatus Hook
|
||
|
||
**Date**: 2025-01-22
|
||
|
||
### Goals
|
||
- Extract game status logic into dedicated hook
|
||
- Follow vim-maze architecture pattern
|
||
- Clean separation of concerns
|
||
- Reusable state machine
|
||
|
||
### Tasks Completed
|
||
|
||
#### 1. Created useGameStatus Module
|
||
**File Structure**:
|
||
```
|
||
src/useGameStatus/
|
||
├── index.ts # Public exports
|
||
├── types.ts # GameStatus, GameStatusManager
|
||
└── useGameStatus.ts # Hook implementation
|
||
```
|
||
|
||
#### 2. Implemented State Machine
|
||
**States**:
|
||
- `waiting` - Initial state, awaiting start
|
||
- `started` - Game in progress
|
||
- `paused` - Game temporarily paused
|
||
- `game-over` - Game lost
|
||
- `game-won` - Game completed
|
||
|
||
**State Transitions**:
|
||
```
|
||
waiting ──startGame()──> started
|
||
▲ │
|
||
│ │ togglePause()
|
||
│ ▼
|
||
quitGame() paused
|
||
│
|
||
│ togglePause()
|
||
▼
|
||
started
|
||
```
|
||
|
||
#### 3. Hook API
|
||
```typescript
|
||
const gameStatusManager = useGameStatus();
|
||
const {
|
||
gameStatus, // Current state
|
||
setGameStatus, // Direct setter (for special cases)
|
||
startGame, // waiting → started
|
||
togglePause, // started ↔ paused
|
||
quitGame, // * → waiting
|
||
} = gameStatusManager;
|
||
```
|
||
|
||
#### 4. Updated useGame
|
||
- Removed inline `useState<GameStatus>`
|
||
- Uses `useGameStatus()` hook
|
||
- Spreads `gameStatusManager` into `GameManager`
|
||
- Cleaner, more maintainable code
|
||
|
||
#### 5. Updated Type System
|
||
**types.ts**:
|
||
- Imports `GameStatus` and `GameStatusManager` from useGameStatus
|
||
- Re-exports for public API
|
||
- `GameManager` extends `GameStatusManager` (intersection type)
|
||
- Avoids duplication, single source of truth
|
||
|
||
### Technical Details
|
||
|
||
**Simpler than vim-maze**:
|
||
- No intermediate states (no 'has-key' equivalent)
|
||
- Straightforward: waiting → started → won/over
|
||
- Pause is optional toggle, not a separate flow
|
||
|
||
**Hook Composition**:
|
||
```typescript
|
||
// In useGame
|
||
const gameStatusManager = useGameStatus();
|
||
const { gameStatus } = gameStatusManager;
|
||
|
||
// Compose into GameManager
|
||
const gameManager = {
|
||
...gameStatusManager, // gameStatus, startGame, etc.
|
||
// ... other managers
|
||
};
|
||
```
|
||
|
||
**Type Safety**:
|
||
- `GameManager` extends `GameStatusManager`
|
||
- All status methods available on gameManager
|
||
- Full TypeScript inference throughout
|
||
|
||
### Build Status
|
||
- ✅ TypeScript compiles cleanly
|
||
- ✅ Example app builds successfully
|
||
- ✅ No breaking changes to public API
|
||
|
||
### Next Steps
|
||
- Phase 40: useCursor Hook Composition
|
||
- useCellEditor (analog to useHero)
|
||
- useVimMotions
|
||
- useGameKeys
|
||
- useCursorRender
|
||
|
||
---
|
||
|
||
## Iteration 6: Phase 40.1 - Basic Cursor with hjkl Movement
|
||
|
||
**Date**: 2025-01-22
|
||
|
||
### Goals
|
||
- Implement basic cursor navigation
|
||
- Add hjkl vim keys
|
||
- Visual cursor highlight
|
||
- Only active during gameplay
|
||
|
||
### Tasks Completed
|
||
|
||
#### 1. Created useCursor Module
|
||
- `useCursor.ts` - Position tracking and movement logic
|
||
- `useCursorRender.ts` - Visual cursor highlight on DOM
|
||
- `types.ts` - Coord and CursorManager types
|
||
|
||
#### 2. Cursor Implementation
|
||
- Position tracking with useState (row/col 0-8)
|
||
- Movement functions: moveLeft, moveRight, moveUp, moveDown
|
||
- Bounds checking prevents moving outside 9x9 grid
|
||
- Keyboard listener for h/j/k/l keys
|
||
- Only active when gameStatus === 'started'
|
||
|
||
#### 3. Visual Feedback
|
||
- Blue highlight with border on current cell
|
||
- CSS class 'cursor' applied to active cell
|
||
- Removed from previous cell on movement
|
||
|
||
#### 4. Fixed Initial Cursor Display (Issue 1)
|
||
**Problem**: Cursor not showing on game start before first move
|
||
**Root Cause**: renderBoard() rebuilds DOM, wiping cursor class
|
||
**Solution**: Added boardRendered counter
|
||
- Increments after each renderBoard()
|
||
- useCursorRender depends on boardRendered
|
||
- Forces cursor re-application after DOM rebuild
|
||
|
||
**Flow**:
|
||
1. Press Space → startGame()
|
||
2. renderBoard() rebuilds DOM
|
||
3. boardRendered increments
|
||
4. useCursorRender triggers
|
||
5. Cursor class applied to (0,0)
|
||
|
||
### Technical Details
|
||
|
||
**useState vs useRef**:
|
||
- Initially used useRef (no re-renders)
|
||
- Changed to useState to trigger useCursorRender
|
||
- Position changes now properly update DOM
|
||
|
||
**Timing Issue Resolution**:
|
||
- renderBoard() is synchronous
|
||
- useEffect runs after render
|
||
- boardRendered ensures proper ordering
|
||
|
||
#### 5. Fixed Cursor Border Conflicts (Issue 2)
|
||
**Problem**: Cursor border conflicting with 3x3 box separators
|
||
- Cells have borders for grid structure
|
||
- Cursor border overlapped/clipped at edges
|
||
- Screenshots showed visual conflicts
|
||
|
||
**Solution**: Use outline instead of border
|
||
- `outline: 3px solid` (sits above borders)
|
||
- `outline-offset: -3px` (draws inside cell)
|
||
- `z-index: 10 + position: relative` (layers above)
|
||
- Outline doesn't affect layout or conflict with borders
|
||
|
||
#### 6. Added Red Flash on Invalid Movement (Issue 3)
|
||
**Feature**: Visual feedback when attempting invalid moves
|
||
|
||
**Implementation**:
|
||
- CSS: `.invalid-move` class with red outline and background
|
||
- Animation: 500ms ease-out fade from bright to transparent
|
||
- Logic: `flashInvalidMove()` triggers on bounds violations
|
||
- Automatically removes class after 500ms
|
||
|
||
**User Experience**:
|
||
- Press h at left edge → Red flash
|
||
- Press j at bottom → Red flash
|
||
- Press k at top → Red flash
|
||
- Press l at right → Red flash
|
||
|
||
**Cleanup**:
|
||
- Removed all console.log debugging statements
|
||
- Production-ready code
|
||
|
||
### Summary
|
||
Phase 40.1 complete with all issues resolved:
|
||
- ✅ Basic cursor with hjkl movement
|
||
- ✅ Visual cursor highlight (blue outline)
|
||
- ✅ Shows immediately on game start
|
||
- ✅ No border conflicts with grid
|
||
- ✅ Red flash on invalid movement
|
||
- ✅ Clean, production-ready code
|
||
|
||
---
|
||
|
||
## Iteration 7: Phase 40.2 - Complete Vim Motion System
|
||
|
||
**Date**: 2025-01-22
|
||
|
||
### Goals
|
||
- Implement anchor moves (0, ^, $, gg, G)
|
||
- Add count prefixes (5j, 10l, etc.)
|
||
- Add repeat motion (.)
|
||
- Handle the "0 caveat" properly
|
||
|
||
### Tasks Completed
|
||
|
||
#### 1. Created useVimMotions Hook
|
||
Complete vim motion processor with state management:
|
||
- Count buffer tracking (countRef)
|
||
- Last motion tracking for repeat
|
||
- Pending g state for gg sequence
|
||
|
||
#### 2. Implemented Anchor Moves
|
||
**Horizontal (Row-based):**
|
||
- `0` or `^` → Start of row (col 0)
|
||
- `$` → End of row (col 8)
|
||
|
||
**Vertical (Column-based):**
|
||
- `gg` → Top of column (row 0)
|
||
- `G` → Bottom of column (row 8)
|
||
|
||
#### 3. Count Prefix Support
|
||
- Works with all directional moves: h, j, k, l
|
||
- Examples: `5j`, `10l`, `3k`, `2h`
|
||
- Bounded to grid limits (0-8)
|
||
- Red flash on invalid moves
|
||
|
||
#### 4. Repeat Motion
|
||
- `.` → Repeats last motion with same count
|
||
- Tracks motion direction and step count
|
||
- Works with both direct moves and counted moves
|
||
|
||
#### 5. Solved the "0 Caveat"
|
||
**Problem**: 0 needs to work as both anchor AND count digit
|
||
- `0` alone → anchor to start
|
||
- `10j` → count of 10
|
||
|
||
**Solution** (from vim-maze):
|
||
```typescript
|
||
if (key === '0' && countRef.current === '') {
|
||
return { type: 'anchor', data: 'start' }; // Anchor
|
||
}
|
||
countRef.current += key; // Count digit
|
||
```
|
||
|
||
**Logic**: Check if count buffer is empty
|
||
- Empty → 0 is anchor
|
||
- Not empty → 0 is count digit
|
||
|
||
#### 6. Fixed gg Two-Key Sequence
|
||
**Bug**: Single `g` was triggering immediately
|
||
|
||
**Solution**: Added `pendingGRef` state
|
||
- First `g` → Set flag, wait for second key
|
||
- Second `g` → Trigger "top" anchor
|
||
- Any other key → Clear flag, no action
|
||
|
||
### Technical Implementation
|
||
|
||
**processKey Return Types:**
|
||
```typescript
|
||
{ type: 'move', data: Motion } // h/j/k/l with count
|
||
{ type: 'anchor', data: string } // 0/^/$/gg/G
|
||
{ type: 'repeat', data: Motion } // .
|
||
{ type: 'count', data: string } // 0-9
|
||
null // Ignored key or pending state
|
||
```
|
||
|
||
**Motion Tracking:**
|
||
```typescript
|
||
type Motion = { dr: number; dc: number; steps: number };
|
||
// Example: 5j → { dr: 1, dc: 0, steps: 5 }
|
||
```
|
||
|
||
**State Management:**
|
||
- `countRef` → Accumulates count digits
|
||
- `lastMotionRef` → Stores last motion for repeat
|
||
- `pendingGRef` → Tracks first g press for gg
|
||
|
||
### Testing Results
|
||
✅ All anchor moves working
|
||
✅ Count prefixes working (5j, 10l, etc.)
|
||
✅ Repeat motion (.) working
|
||
✅ 0 works as both anchor and count
|
||
✅ gg requires two presses
|
||
✅ All moves bounded with red flash feedback
|
||
|
||
### Next Steps
|
||
- Phase 40.3: Implement cell editing (i/r modes)
|
||
- Add digit entry (1-9)
|
||
- Add erase (x, Backspace)
|
||
- Mode indicators
|
||
|
||
---
|
||
|
||
## Iteration 8: Phase 40.3 - Edit Mode Implementation
|
||
|
||
**Date**: 2025-01-22
|
||
|
||
### Goals
|
||
- Implement edit mode state machine
|
||
- Add rules for editable vs given cells
|
||
- Use existing CSS classes for visual feedback
|
||
- No digit entry yet - just mode switching
|
||
|
||
### Tasks Completed
|
||
|
||
#### 1. Mode State Machine (Phase 40.3.1)
|
||
**Added CursorMode type:**
|
||
- `'normal'` - Navigation mode (vim motions active)
|
||
- `'edit'` - Edit mode (ready for digit entry)
|
||
|
||
**Key Bindings:**
|
||
- `i` - Enter edit mode (insert)
|
||
- `Escape` - Exit edit mode to normal
|
||
|
||
**State Management:**
|
||
- Mode tracked with useState in useCursor
|
||
- Starts in 'normal' mode on game start
|
||
- Mode passed to useCursorRender for visual feedback
|
||
|
||
#### 2. Edit Mode Rules (Phase 40.3.2)
|
||
**Cell Editability:**
|
||
- Given cells (`.given` class): Cannot be edited
|
||
- Empty cells: Can be edited
|
||
- User-entered cells (`.user-entered` class): Can be edited
|
||
|
||
**Validation Logic:**
|
||
```typescript
|
||
if (key === 'i') {
|
||
const targetCell = cells[index];
|
||
if (targetCell && targetCell.classList.contains('given')) {
|
||
flashInvalidMove(); // Red flash
|
||
return; // Block edit mode
|
||
}
|
||
setMode('edit'); // Allow edit mode
|
||
}
|
||
```
|
||
|
||
**Visual Feedback:**
|
||
- Attempt to edit given cell → Red flash (500ms)
|
||
- Successfully enter edit mode → Yellow/amber glow
|
||
|
||
#### 3. Visual Indicators
|
||
**Using Existing CSS Classes:**
|
||
- Normal mode: `.active` class (blue glow)
|
||
- Edit mode: `.editing` class (yellow/amber pulsing glow)
|
||
- Invalid attempt: `.invalid-move` class (red flash)
|
||
|
||
**Removed Custom CSS:**
|
||
- Removed `.cursor` and `.cursor-edit` classes
|
||
- Using original CSS design from src_old
|
||
- Consistent with existing game aesthetics
|
||
|
||
#### 4. Mode Behavior
|
||
**Normal Mode:**
|
||
- All vim motions work (hjkl, anchors, counts, repeat)
|
||
- Blue glow on current cell
|
||
- Can navigate freely
|
||
|
||
**Edit Mode:**
|
||
- All vim motions blocked
|
||
- Yellow/amber pulsing glow
|
||
- Only Escape key active (exit to normal)
|
||
- Ready for digit entry (not implemented yet)
|
||
|
||
### Technical Details
|
||
|
||
**Cell Classification:**
|
||
- Given cells: Have digits from puzzle generation
|
||
- Renderer adds `.given` class to these cells
|
||
- These cells are immutable (cannot be edited)
|
||
|
||
**Flash Mechanism:**
|
||
- `flashInvalidMove()` adds `.invalid-move` class
|
||
- CSS animation shows red glow for 500ms
|
||
- Class auto-removed after timeout
|
||
- Same mechanism used for boundary violations
|
||
|
||
**Mode Transition Flow:**
|
||
```
|
||
Normal (blue)
|
||
↓ [press i on empty/user cell]
|
||
Edit (yellow)
|
||
↓ [press Escape]
|
||
Normal (blue)
|
||
|
||
Normal (blue)
|
||
↓ [press i on given cell]
|
||
Red flash → Stay in Normal
|
||
```
|
||
|
||
### Testing Results
|
||
✅ Edit mode state machine working
|
||
✅ i and Escape toggle modes correctly
|
||
✅ Given cells block edit mode with red flash
|
||
✅ Empty cells allow edit mode
|
||
✅ User-entered cells allow edit mode
|
||
✅ Vim motions blocked in edit mode
|
||
✅ Visual indicators clear and distinct
|
||
|
||
### Next Steps
|
||
- Phase 40.3.3: Add digit entry (1-9)
|
||
- Phase 40.3.4: Add erase functionality (x, Backspace)
|
||
- Phase 40.3.5: Add replace mode (r)
|
||
- Integration with board state management
|
||
|
||
---
|
||
|
||
## Iteration 9: Phase 40.3.3 - Digit Entry
|
||
|
||
**Date**: 2025-01-22
|
||
|
||
### Goals
|
||
- Allow user to type digits 1-9 when in edit mode
|
||
- Update board state with entered digits
|
||
- Cells show digits with `.user-entered` class
|
||
- Stay in edit mode after entering digit
|
||
|
||
### Tasks Completed
|
||
|
||
#### 1. Added setCell Method to BoardManager
|
||
**Updated useBoard.ts:**
|
||
- Added `setCell(row, col, value)` function
|
||
- Updates board string at calculated index
|
||
- Triggers re-render to display change
|
||
|
||
**Implementation:**
|
||
```typescript
|
||
function setCell(row: number, col: number, value: string): void {
|
||
const board = boardRef.current.split('');
|
||
const index = row * 9 + col;
|
||
board[index] = value;
|
||
boardRef.current = board.join('');
|
||
renderBoard();
|
||
}
|
||
```
|
||
|
||
#### 2. Updated BoardManager Type
|
||
- Added `setCell: (row: number, col: number, value: string) => void;` to type definition
|
||
|
||
#### 3. Enhanced useCursor for Digit Entry
|
||
**Updated useCursor.ts:**
|
||
- Added `boardManager` parameter (BoardManager)
|
||
- In edit mode, detect digit keys (1-9)
|
||
- Call `boardManager.setCell()` to update board
|
||
- Stay in edit mode (no auto-exit)
|
||
|
||
**Key Handling:**
|
||
```typescript
|
||
if (mode === 'edit') {
|
||
if (key === 'Escape') {
|
||
setMode('normal');
|
||
return;
|
||
}
|
||
|
||
if (key >= '1' && key <= '9') {
|
||
boardManager.setCell(position.row, position.col, key);
|
||
return;
|
||
}
|
||
|
||
return;
|
||
}
|
||
```
|
||
|
||
#### 4. Updated useGame Integration
|
||
- Pass `board` (BoardManager) to `useCursor()`
|
||
- Three-parameter call: `useCursor(gameStatusManager, containerRef, board)`
|
||
|
||
### Technical Details
|
||
|
||
**Board State Flow:**
|
||
```
|
||
User presses '5' in edit mode
|
||
↓
|
||
useCursor detects digit key
|
||
↓
|
||
Calls boardManager.setCell(row, col, '5')
|
||
↓
|
||
Updates boardRef.current string
|
||
↓
|
||
Calls renderBoard()
|
||
↓
|
||
SudokuRenderer.render() displays digit with .user-entered class
|
||
↓
|
||
User sees '5' in the cell
|
||
```
|
||
|
||
**Cell Classification:**
|
||
- Given cells: Have `.given` class (from initial puzzle)
|
||
- User-entered cells: Have `.user-entered` class (from renderer logic)
|
||
- Empty cells: No value displayed
|
||
|
||
**Mode Behavior:**
|
||
- Edit mode entered with `i` on empty/user-entered cells
|
||
- Typing 1-9 places digit, stays in edit mode
|
||
- `Escape` exits to normal mode
|
||
- Can navigate to other cells and repeat
|
||
|
||
### Build Status
|
||
- ✅ TypeScript compiles cleanly
|
||
- ✅ No type errors
|
||
- ✅ Integration complete
|
||
|
||
### Issue 1: Golden Glow Disappearing After Digit Entry
|
||
|
||
**Problem**: When typing a digit in edit mode, the `.editing` class (golden glow) disappeared even though we remained in edit mode.
|
||
|
||
**Root Cause**: `setCell()` calls `renderBoard()` which rebuilds the entire DOM, wiping out the cursor highlight classes.
|
||
|
||
**Solution**: Moved `boardRendered` counter from `useGame` to `useBoard`
|
||
- Added `boardRendered` state to `useBoard` hook
|
||
- Increment counter at end of `renderBoard()` function
|
||
- Export `boardRendered` from `useBoard`
|
||
- `useCursorRender` depends on `boardRendered` and re-applies cursor classes after DOM rebuild
|
||
|
||
**Changes**:
|
||
- `useBoard.ts`: Added `useState` for `boardRendered`, increment in `renderBoard()`
|
||
- `useBoard/types.ts`: Added `boardRendered: number` to `BoardManager`
|
||
- `useGame.ts`: Removed local `boardRendered` state, use board's `boardRendered` instead
|
||
- Removed manual `setBoardRendered` calls from `useGame` (automatic now)
|
||
|
||
**Result**: ✅ Golden glow persists when typing digits in edit mode
|
||
|
||
### Issue 2: Wrong Cell Flashing Red on Edit Mode Entry
|
||
|
||
**Problem**: After attempting to enter edit mode on a given cell (red flash), moving to an empty cell and pressing `i` would flash the *previous* given cell red instead of entering edit mode on the current cell.
|
||
|
||
**Root Cause**: Stale closure in `useEffect`. The `handleKeyDown` function captured `position` from when the effect was created. The dependency array was `[gameStatus, mode]` but didn't include `position`. When position changed, the event handler wasn't recreated, so it still used the old position coordinates.
|
||
|
||
**Solution**: Add `position` to the `useEffect` dependency array.
|
||
|
||
**Changes**:
|
||
- `useCursor.ts`: Changed dependency array from `[gameStatus, mode]` to `[gameStatus, mode, position]`
|
||
|
||
**Result**: ✅ Edit mode entry now correctly checks and flashes the current cell position
|
||
|
||
---
|
||
|
||
## Iteration 10: Phase 40.3.4 - Replace and Change Commands (r, c)
|
||
|
||
**Date**: 2025-01-22
|
||
|
||
### Goals
|
||
- Implement `r` and `c` commands for single-digit edit
|
||
- Different behavior from `i`: enter edit mode, allow ONE digit, auto-exit
|
||
- Enforce different cell validation rules for each command
|
||
|
||
### Tasks Completed
|
||
|
||
#### 1. Added Single-Edit State
|
||
**Updated useCursor.ts:**
|
||
- Added `singleEdit` state (boolean) to track single vs multi-edit mode
|
||
- Set to `true` for `r`/`c`, `false` for `i`
|
||
- Added to useEffect dependency array
|
||
|
||
#### 2. Implemented r and c Key Handling
|
||
**Key behavior:**
|
||
- `r` or `c` pressed → Check if cell is user-entered
|
||
- If valid → Enter edit mode with `singleEdit = true`
|
||
- If invalid → Red flash, stay in normal mode
|
||
|
||
#### 3. Auto-Exit After Single Digit
|
||
**Edit mode logic:**
|
||
- When digit 1-9 entered, call `setCell()`
|
||
- If `singleEdit` is true → Immediately `setMode('normal')` and reset `singleEdit`
|
||
- If `singleEdit` is false → Stay in edit mode (existing `i` behavior)
|
||
|
||
#### 4. Cell Validation Rules
|
||
**Implemented complete validation:**
|
||
|
||
| Command | Empty Cell | User-Entered Cell | Given Cell |
|
||
|---------|------------|-------------------|------------|
|
||
| `i` | ✅ Enter edit (multi) | ❌ Red flash | ❌ Red flash |
|
||
| `r` | ❌ Red flash | ✅ Enter edit (single) | ❌ Red flash |
|
||
| `c` | ❌ Red flash | ✅ Enter edit (single) | ❌ Red flash |
|
||
|
||
**Implementation:**
|
||
```typescript
|
||
const isGiven = targetCell.classList.contains('given');
|
||
const isUserEntered = targetCell.classList.contains('user-entered');
|
||
const isEmpty = !isGiven && !isUserEntered;
|
||
|
||
if (key === 'i') {
|
||
if (!isEmpty) {
|
||
flashInvalidMove();
|
||
return;
|
||
}
|
||
} else if (key === 'r' || key === 'c') {
|
||
if (!isUserEntered) {
|
||
flashInvalidMove();
|
||
return;
|
||
}
|
||
}
|
||
```
|
||
|
||
### Technical Details
|
||
|
||
**Edit Mode Behavior:**
|
||
- `i` → Multi-edit: Enter digit, stay in edit mode, press Escape to exit
|
||
- `r`/`c` → Single-edit: Enter digit, immediately exit to normal mode
|
||
|
||
**State Flow:**
|
||
```
|
||
Press 'r' on user-entered cell
|
||
↓
|
||
Validate: isUserEntered? Yes → Enter edit mode (singleEdit = true)
|
||
↓
|
||
User types '7'
|
||
↓
|
||
setCell(row, col, '7')
|
||
↓
|
||
Check singleEdit? True → setMode('normal'), setSingleEdit(false)
|
||
↓
|
||
Back in normal mode (blue cursor)
|
||
```
|
||
|
||
### Build Status
|
||
- ✅ TypeScript compiles cleanly
|
||
- ✅ No type errors
|
||
- ✅ All validation rules working correctly
|
||
|
||
### Next Steps
|
||
|
||
- Win condition checking
|
||
|
||
---
|
||
|
||
## Iteration 11: Phase 40.3.5 - Erase Functionality
|
||
|
||
**Date**: 2025-01-22
|
||
|
||
### Goals
|
||
- Implement erase commands to clear user-entered digits
|
||
- Different behavior in normal vs edit mode
|
||
- Always respect given cell protection
|
||
|
||
### Tasks Completed
|
||
|
||
#### 1. Normal Mode Erase (x, d, Delete)
|
||
**Keys: `x`, `d`, `Delete`**
|
||
- Only work on user-entered cells
|
||
- Set cell value to '.' (empty)
|
||
- Flash red if cell is given or already empty
|
||
|
||
**Validation Logic:**
|
||
```typescript
|
||
if (key === 'x' || key === 'd' || key === 'Delete') {
|
||
const isGiven = targetCell.classList.contains('given');
|
||
const isUserEntered = targetCell.classList.contains('user-entered');
|
||
|
||
if (isGiven || !isUserEntered) {
|
||
flashInvalidMove();
|
||
return;
|
||
}
|
||
|
||
boardManager.setCell(position.row, position.col, '.');
|
||
return;
|
||
}
|
||
```
|
||
|
||
#### 2. Edit Mode Erase (Backspace)
|
||
**Key: `Backspace`**
|
||
- Works in edit mode only
|
||
- Erases current cell value
|
||
- Flash red if cell is given
|
||
- Stay in edit mode after erasing
|
||
|
||
**Implementation:**
|
||
```typescript
|
||
if (key === 'Backspace') {
|
||
const targetCell = cells[index];
|
||
|
||
if (targetCell && targetCell.classList.contains('given')) {
|
||
flashInvalidMove();
|
||
return;
|
||
}
|
||
|
||
boardManager.setCell(position.row, position.col, '.');
|
||
return;
|
||
}
|
||
```
|
||
|
||
#### 3. Normal Mode Backspace Protection
|
||
**Key: `Backspace` in normal mode**
|
||
- Always triggers red flash (invalid move)
|
||
- Prevents accidental cell clearing in normal mode
|
||
- User must use x, d, or Delete explicitly
|
||
|
||
### Erase Command Rules
|
||
|
||
| Mode | Key | Empty Cell | User-Entered Cell | Given Cell |
|
||
|------|-----|------------|-------------------|------------|
|
||
| Normal | `x`, `d`, `Delete` | ❌ Red flash | ✅ Erase to '.' | ❌ Red flash |
|
||
| Normal | `Backspace` | ❌ Red flash | ❌ Red flash | ❌ Red flash |
|
||
| Edit | `Backspace` | ✅ Erase (noop) | ✅ Erase to '.' | ❌ Red flash |
|
||
|
||
### Technical Details
|
||
|
||
**Cell State After Erase:**
|
||
- Board string updated with '.' at cell index
|
||
- Cell loses `.user-entered` class on next render
|
||
- Cell becomes empty and eligible for `i` command again
|
||
|
||
**Flow Example:**
|
||
```
|
||
User enters '5' in cell with 'i'
|
||
↓
|
||
Cell has .user-entered class, displays '5'
|
||
↓
|
||
Press Escape → normal mode
|
||
↓
|
||
Press 'x' or 'd' or Delete
|
||
↓
|
||
boardManager.setCell(row, col, '.')
|
||
↓
|
||
Board re-renders, cell is empty
|
||
↓
|
||
Cell loses .user-entered, can use 'i' again
|
||
```
|
||
|
||
### Build Status
|
||
- ✅ TypeScript compiles cleanly
|
||
- ✅ No type errors
|
||
- ✅ All erase commands working correctly
|
||
|
||
### Next Steps
|
||
|
||
---
|
||
|
||
## Iteration 12: Phase 50 - Scoring System and Win Condition
|
||
|
||
**Date**: 2025-01-22
|
||
|
||
### Goals
|
||
- Implement complete scoring system based on vim-maze patterns
|
||
- Track time, keystrokes, and efficiency
|
||
- Detect win condition when puzzle is complete
|
||
- Display all metrics in example app
|
||
|
||
### Tasks Completed
|
||
|
||
#### 1. Created useTimer Hook
|
||
**File: `src/useScore/useTimer.ts`**
|
||
- Tracks elapsed time using `requestAnimationFrame`
|
||
- Methods: `startTimer()`, `stopTimer()`, `resetTimer()`
|
||
- Returns `timeValue` in milliseconds
|
||
- Direct copy from vim-maze (works perfectly as-is)
|
||
|
||
#### 2. Created useScore Hook
|
||
**File: `src/useScore/useScore.ts`**
|
||
|
||
**Metrics Tracked:**
|
||
- **Total keystrokes**: All keys pressed during gameplay
|
||
- **Edit keystrokes**: Only editing keys (i, r, c, x, d, Delete, Escape, Backspace, 1-9)
|
||
- **Optimal keystrokes**: Empty cells × 3 (i + digit + Escape minimum)
|
||
- **Efficiency**: `(optimal / edit) × 100` - 100% = perfect, no mistakes
|
||
- **Final score**: Weighted combination of efficiency (70%) and time (30%)
|
||
|
||
**Key Logic:**
|
||
```typescript
|
||
const EDIT_KEYS = ['i', 'r', 'c', 'x', 'd', 'Delete', 'Escape', 'Backspace', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
|
||
|
||
const totalKeystrokes = keyLog.length;
|
||
const editKeystrokes = keyLog.filter(entry => EDIT_KEYS.includes(entry.key)).length;
|
||
const optimalKeystrokes = emptyCells * 3;
|
||
const efficiency = (optimalKeystrokes / editKeystrokes) * 100;
|
||
```
|
||
|
||
#### 3. Win Condition Detection
|
||
**Trigger:** Board has no empty cells (no '.' characters) AND cursor is in normal mode
|
||
|
||
**Why check normal mode?**
|
||
- Prevents premature win detection while still typing
|
||
- User must exit edit mode (Escape or auto-exit from r/c)
|
||
- Gives clean transition to game-won state
|
||
|
||
**Implementation:**
|
||
```typescript
|
||
useEffect(() => {
|
||
if (gameStatus !== 'started') return;
|
||
if (cursorMode !== 'normal') return; // Wait until edit mode exits
|
||
|
||
const boardStr = board.boardRef.current;
|
||
const hasEmptyCells = boardStr.includes('.');
|
||
|
||
if (!hasEmptyCells) {
|
||
setGameStatus('game-won');
|
||
}
|
||
}, [board.boardRendered, gameStatus, cursorMode]);
|
||
```
|
||
|
||
#### 4. Keystroke Tracking in useGame
|
||
**Implementation:**
|
||
- Add keyboard listener during 'started' state
|
||
- Log all keystrokes with timestamp
|
||
- Clear log when game starts
|
||
- Pass to useScore for metrics calculation
|
||
|
||
#### 5. Updated Example App Scoreboard
|
||
**Displays:**
|
||
- Time (formatted as mm:ss)
|
||
- Total Keys (all keystrokes)
|
||
- Edit Keys (shown as "current / optimal")
|
||
- Efficiency (percentage)
|
||
- Final Score (shown only when game-won)
|
||
|
||
**Layout:** Clean, monospace display with dividers between sections
|
||
|
||
#### 6. Adjusted Difficulty for Testing
|
||
- Easy mode: 5 removed cells (was 40, then 10, now 5)
|
||
- Default difficulty: Easy (for faster testing)
|
||
|
||
### Technical Details
|
||
|
||
**Score Calculation:**
|
||
```typescript
|
||
const efficiencyScore = Math.max(0, Math.min(100, efficiency));
|
||
const timeScore = Math.max(0, 100 - (timeMs / 1000 / 60) * 10);
|
||
const finalScore = Math.round((efficiencyScore * 0.7 + timeScore * 0.3) * 1000);
|
||
```
|
||
|
||
**Efficiency Interpretation:**
|
||
- 100% = Perfect play (only optimal keystrokes used)
|
||
- 90% = Excellent (few corrections)
|
||
- 70% = Good (some mistakes)
|
||
- <50% = Many corrections/mistakes
|
||
|
||
**Timer Lifecycle:**
|
||
```
|
||
waiting → [start game] → started (timer starts)
|
||
↓
|
||
started → [board complete + normal mode] → game-won (timer stops)
|
||
↓
|
||
game-won → [quit] → waiting (timer resets)
|
||
```
|
||
|
||
### Types Added
|
||
|
||
**ScoreManager Type:**
|
||
```typescript
|
||
export type ScoreManager = {
|
||
timeValue: number;
|
||
startTimer: () => void;
|
||
stopTimer: () => void;
|
||
resetTimer: () => void;
|
||
totalKeystrokes: number;
|
||
editKeystrokes: number;
|
||
optimalKeystrokes: number;
|
||
efficiency: number;
|
||
finalScore: number | null;
|
||
};
|
||
```
|
||
|
||
**BoardManager Updates:**
|
||
- Added `boardRef: RefObject<string>` to expose board state
|
||
- Cleaned up return object (removed internal refs)
|
||
|
||
### Build Status
|
||
- ✅ TypeScript compiles cleanly
|
||
- ✅ All metrics calculating correctly
|
||
- ✅ Win condition triggers only when exiting edit mode
|
||
- ✅ Scoreboard displays all values properly
|
||
|
||
### Next Steps
|
||
- Polish and final testing
|
||
- Documentation updates
|
||
|
||
---
|
||
|
||
## Iteration 13: Simplified Scoring System
|
||
|
||
**Date**: 2025-01-22
|
||
|
||
### Goals
|
||
- Simplify scoring to remove efficiency metrics
|
||
- Create game-specific scoring algorithm for sudoku
|
||
- Make scoring transparent and understandable to players
|
||
- Adjust difficulty levels to realistic values
|
||
|
||
### Rationale
|
||
The unified API should not enforce identical scoring across all games. Each game has different mechanics:
|
||
- vim-maze: Efficiency makes sense (optimal path vs actual)
|
||
- vim-sudoku: Efficiency concept unclear (what's "optimal" editing?)
|
||
- vim-snake: No efficiency concept at all
|
||
|
||
Better to let each game define scoring that fits its mechanics.
|
||
|
||
### Tasks Completed
|
||
|
||
#### 1. Removed Efficiency Metrics
|
||
**Removed from ScoreManager:**
|
||
- `editKeystrokes` - No longer tracking
|
||
- `optimalKeystrokes` - No longer calculating
|
||
- `efficiency` - Removed entirely
|
||
- `EDIT_KEYS` filtering - Removed
|
||
|
||
**Now tracking only:**
|
||
- `timeValue` - Elapsed time in milliseconds
|
||
- `totalKeystrokes` - All keys pressed
|
||
- `finalScore` - Calculated score (0-1000)
|
||
|
||
#### 2. New Scoring Algorithm
|
||
**Formula:**
|
||
```typescript
|
||
const seconds = timeValue / 1000;
|
||
const timePenalty = seconds / 10;
|
||
const keystrokePenalty = totalKeystrokes / 2;
|
||
const multiplier = DIFFICULTY_MULTIPLIER[difficulty];
|
||
|
||
const baseScore = 1000 - timePenalty - keystrokePenalty;
|
||
const finalScore = min(1000, max(0, round(baseScore * multiplier)));
|
||
```
|
||
|
||
**Difficulty Multipliers:**
|
||
- Easy (25 cells): 1.0x
|
||
- Medium (40 cells): 1.5x
|
||
- Hard (50 cells): 2.0x
|
||
|
||
**Scoring Breakdown:**
|
||
- Start with 1000 points
|
||
- Subtract time penalty: 10 seconds = -1 point
|
||
- Subtract keystroke penalty: 2 keystrokes = -1 point
|
||
- Multiply by difficulty
|
||
- Cap at 1000 (max score)
|
||
- Floor at 0 (min score)
|
||
|
||
**Examples:**
|
||
```
|
||
Easy (25 cells):
|
||
- 30 seconds, 50 keys = (1000 - 3 - 25) * 1.0 = 972 / 1000
|
||
- 60 seconds, 100 keys = (1000 - 6 - 50) * 1.0 = 944 / 1000
|
||
|
||
Medium (40 cells):
|
||
- 60 seconds, 100 keys = (1000 - 6 - 50) * 1.5 = 1000 / 1000 (capped)
|
||
- 120 seconds, 200 keys = (1000 - 12 - 100) * 1.5 = 1000 / 1000 (capped)
|
||
|
||
Hard (50 cells):
|
||
- 120 seconds, 200 keys = (1000 - 12 - 100) * 2.0 = 1000 / 1000 (capped)
|
||
```
|
||
|
||
#### 3. Updated Difficulty Levels
|
||
**Changed from testing values to realistic gameplay:**
|
||
- Easy: 5 → 25 cells removed (~56 givens)
|
||
- Medium: 50 → 40 cells removed (~41 givens)
|
||
- Hard: 60 → 50 cells removed (~31 givens)
|
||
|
||
#### 4. Updated Example App Display
|
||
**Scoreboard now shows:**
|
||
- Time (mm:ss format)
|
||
- Keystrokes (total count)
|
||
- Final Score (X / 1000) - only shown on game-won
|
||
|
||
**Removed displays:**
|
||
- Edit Keys
|
||
- Optimal Keys
|
||
- Efficiency percentage
|
||
|
||
#### 5. Passed Difficulty to useScore
|
||
- Added `difficulty` parameter to useScore
|
||
- Used to calculate multiplier in final score
|
||
- Ensures score reflects puzzle difficulty
|
||
|
||
### Technical Details
|
||
|
||
**Why This Scoring Works:**
|
||
1. **Transparent**: Players understand time and keystrokes
|
||
2. **Fair**: Harder puzzles give higher multipliers
|
||
3. **Balanced**: Both speed and efficiency matter
|
||
4. **Simple**: No confusing percentage calculations
|
||
5. **Capped**: Max 1000 prevents score inflation
|
||
|
||
**Score Distribution:**
|
||
- Perfect play (fast, few keys) → 900-1000
|
||
- Good play → 700-900
|
||
- Average play → 500-700
|
||
- Slow/many mistakes → 0-500
|
||
|
||
### Build Status
|
||
- ✅ TypeScript compiles cleanly
|
||
- ✅ Scoring algorithm working correctly
|
||
- ✅ Score displays as "X / 1000"
|
||
- ✅ Realistic difficulty levels set
|
||
|
||
### Next Steps
|
||
- Final testing and polish
|
||
- Documentation updates
|
||
|
||
---
|
||
|
||
## 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: 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...*
|