# 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` 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 --- ## 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: `
5
` - User-entered: `
7
` - Empty: `
` ### 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` - 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 ### Next Steps - Phase 40.3.4: Add erase functionality (x, Backspace, Delete) - Phase 40.3.5: Add replace mode (r) - Validation and conflict detection - Win condition checking --- ## Iteration 10: [Pending] *To be filled as implementation progresses...*