Files
vim-sudoku/IMPLEMENTATION.md
T
pastilhas 1409dcf012 docs: document Issue 3 fix and Phase 40.1 completion
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.
2025-10-22 19:04:10 +01:00

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.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:

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

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:

// 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

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:

// 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

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...