Documented: - Issue 3: Red flash on invalid movement (500ms animation) - Cleanup: Removed all debug console.logs - Summary: All 3 issues resolved, production-ready Phase 40.1 complete: ✅ Basic cursor with hjkl movement ✅ Visual cursor highlight (blue outline) ✅ Shows immediately on game start ✅ No border conflicts with grid edges ✅ Red flash feedback on invalid moves ✅ Clean, production-ready code Ready for next phase or sidequest.
16 KiB
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.
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.mdwith 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.tsxto 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
useKeyBindingsalready 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
- No Mobile Support: Sudoku with vim bindings requires keyboard, mobile not practical
- Simple Metrics: Start with time, keystrokes, and final score (no efficiency yet)
- 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 levelsGameStatus- Game state machine statesGameOptions- useGame configurationGameManager- Full API interface (mocked for now)KeyLogEntry- Keystroke tracking
3. Implemented useBoard (src/useBoard/useBoard.ts)
containerRef- DOM referencerenderBoard()- Renders empty 9x9 sudoku grid- Creates proper HTML structure with
sudoku-gridandsudoku-cellclasses - Sets data attributes (data-r, data-c) for cell identification
4. Implemented useGame (src/useGame.ts)
- Calls
useBoard()for grid rendering - Returns full
GameManagerinterface - Real implementations:
gameStatusstate managementstartGame,togglePause,quitGamekeyLogtracking withclearKeyLog,getKeyLog
- Mocked implementations:
cursor(empty object)scoreManager(zeros and null)
- Supports optional
platformHookparameter
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-cellclass - Each cell has
data-randdata-cattributes - Wrapped in
sudoku-griddiv inside#sudokucontainer - 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
difficultyfrom 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:
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:
- Generate fully solved board using base pattern + shuffles
- Validate complete board (sum checks for rows/cols/boxes)
- Remove X cells based on difficulty to create puzzle
- Keep solution reference (no solving needed!)
2. Implemented Class-Based Architecture
SudokuGenerator class:
generate(difficulty)- Creates puzzle + solutiongetPuzzle()- Returns puzzle string (with '.' for empty)getSolution()- Returns complete solutionvalidateFullBoard()- Private validation (defensive)- Configurable difficulty at top of file
SudokuRenderer class:
render(board, initialBoard)- Renders current staterenderEmpty()- 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
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:
- Always valid: Starts with mathematically valid complete board
- Always solvable: We keep the solution, don't need to solve
- Fast: No backtracking, just pattern + shuffle
- Simple: Easy to understand and maintain
- Predictable difficulty: Just cell count, not algorithmic complexity
Pattern-Based Generation:
// 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 startstarted- Game in progresspaused- Game temporarily pausedgame-over- Game lostgame-won- Game completed
State Transitions:
waiting ──startGame()──> started
▲ │
│ │ togglePause()
│ ▼
quitGame() paused
│
│ togglePause()
▼
started
3. Hook API
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
gameStatusManagerintoGameManager - Cleaner, more maintainable code
5. Updated Type System
types.ts:
- Imports
GameStatusandGameStatusManagerfrom useGameStatus - Re-exports for public API
GameManagerextendsGameStatusManager(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:
// In useGame
const gameStatusManager = useGameStatus();
const { gameStatus } = gameStatusManager;
// Compose into GameManager
const gameManager = {
...gameStatusManager, // gameStatus, startGame, etc.
// ... other managers
};
Type Safety:
GameManagerextendsGameStatusManager- 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 logicuseCursorRender.ts- Visual cursor highlight on DOMtypes.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:
- Press Space → startGame()
- renderBoard() rebuilds DOM
- boardRendered increments
- useCursorRender triggers
- 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-moveclass 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
Next Steps
- Continue Phase 40: Add vim motions (counts, anchors, repeat)
- Or sidequest before next phase
Iteration 7: [Pending]
To be filled as implementation progresses...