488 lines
14 KiB
Markdown
488 lines
14 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.
|
|
|
|
---
|
|
|
|
## 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
|
|
|
|
### Next Steps
|
|
- Issue 2: Fix cursor border conflicts with grid edges
|
|
- Issue 3: Add red flash on invalid movement attempts
|
|
|
|
---
|
|
|
|
## Iteration 7: [Pending]
|
|
|
|
*To be filled as implementation progresses...*
|