Files
vim-sudoku/IMPLEMENTATION.md
T
pastilhas 8db4455dc5 feat: implement scoring system and win condition (Phase 50)
- Add useTimer hook for elapsed time tracking
- Add useScore hook with comprehensive metrics:
  - Total keystrokes (all keys)
  - Edit keystrokes (only editing keys)
  - Optimal keystrokes (empty cells × 3)
  - Efficiency percentage (optimal/edit × 100)
  - Final score (70% efficiency + 30% time-based)
- Win condition detects board completion only in normal mode
- Track all keystrokes with timestamps in useGame
- Update example app with full scoreboard display
- Expose boardRef in BoardManager for state checking
- Adjust easy mode to 5 removed cells for testing
2025-10-22 20:42:50 +01:00

1235 lines
36 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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: [Pending]
*To be filled as implementation progresses...*