# @vimazing/vim-sudoku [![npm version](https://img.shields.io/npm/v/@vimazing/vim-sudoku.svg)](https://www.npmjs.com/package/@vimazing/vim-sudoku) [![npm downloads](https://img.shields.io/npm/dm/@vimazing/vim-sudoku.svg)](https://www.npmjs.com/package/@vimazing/vim-sudoku) [![license](https://img.shields.io/npm/l/@vimazing/vim-sudoku.svg)](https://github.com/vimazing/vim-sudoku/blob/main/LICENSE) ![VIM Sudoku Demo](./vim-sudoku.gif) Lightweight, typed **React hooks** for building interactive sudoku games with **VIM-style modal editing**. Part of the [VIMazing](https://vimazing.com) project. --- ## Contents - [Features](#features) - [Installation](#installation) - [Quick Start](#quick-start) - [API Reference](#api-reference) - [Game States](#game-states) - [VIM Controls](#vim-controls) - [Scoring System](#scoring-system) - [Game Over Conditions](#game-over-conditions) - [Hint System](#hint-system) - [Configuration](#configuration) - [Example App](#example-app) - [License](#license) --- ## Features - ๐ŸŽฎ **VIM modal editing** โ€“ Navigate in normal mode, edit cells with i/r/c commands - โŒจ๏ธ **Full VIM motions** โ€“ hjkl, counts (5j), anchors (^/$), gg/G, repeat (.) - ๐ŸŽฏ **Smart cell protection** โ€“ Pre-filled clues locked with visual feedback - ๐Ÿ’ก **Hint system** โ€“ Get hints for incorrect cells or empty cells (Shift+H) - โฑ๏ธ **Configurable limits** โ€“ Time limits and hint penalties trigger game-over - ๐Ÿ“Š **Comprehensive scoring** โ€“ Time + keystrokes + hints with difficulty multipliers - ๐ŸŽจ **Tokyo Night theme** โ€“ Beautiful dark theme with proper 3x3 box separators - ๐Ÿ“ฆ **Full TypeScript** โ€“ Complete type safety with generated declarations - ๐Ÿช **Composable architecture** โ€“ Clean separation: board, cursor, score, game status - ๐ŸŒ **Platform hooks** โ€“ Optional integration for analytics and custom bindings --- ## Installation ```bash npm install @vimazing/vim-sudoku ``` Or with bun: ```bash bun add @vimazing/vim-sudoku ``` --- ## Quick Start ```tsx import { useGame } from "@vimazing/vim-sudoku"; import "@vimazing/vim-sudoku/game.css"; export function SudokuGame() { const gameManager = useGame({ difficulty: 'easy' }); const { containerRef, gameStatus, scoreManager, startGame } = gameManager; return (

VIMazing Sudoku

{gameStatus === 'waiting' && ( )}
{gameStatus === 'game-won' && (

You Won!

Score: {scoreManager.finalScore} / 1000

Time: {Math.floor(scoreManager.timeValue / 1000)}s

)}
); } ``` > **Note:** You must manually import `game.css` for styling. --- ## API Reference ### `useGame(options?, platformHook?)` Main orchestrator hook that composes all game functionality. #### Options ```typescript type GameOptions = { difficulty?: 'easy' | 'medium' | 'hard'; // Default: 'easy' timeLimit?: number; // In seconds, default: 600 (10 min) removedCells?: number; // Override difficulty default }; ``` **Difficulty Defaults:** - `easy`: 25 cells removed (~56 givens) - `medium`: 40 cells removed (~41 givens) - `hard`: 50 cells removed (~31 givens) **Examples:** ```typescript // Default configuration useGame() // Hard difficulty with defaults useGame({ difficulty: 'hard' }) // Custom puzzle size useGame({ removedCells: 30 }) // Custom everything useGame({ difficulty: 'hard', removedCells: 45, timeLimit: 480 // 8 minutes }) ``` #### Returns: GameManager ```typescript type GameManager = { // DOM Reference containerRef: RefObject; // Rendering renderBoard: () => void; // Game Status gameStatus: GameStatus; setGameStatus: (status: GameStatus) => void; startGame: () => void; togglePause: (pause?: boolean) => void; quitGame: () => void; // Cursor cursor: CursorManager; // Scoring scoreManager: ScoreManager; // Input Tracking keyLog: KeyLogEntry[]; clearKeyLog: () => void; getKeyLog: () => KeyLogEntry[]; }; ``` #### CursorManager ```typescript type CursorManager = { position: () => Coord; // Current { row, col } mode: () => CursorMode; // 'normal' | 'edit' moveLeft: (count?: number) => void; moveRight: (count?: number) => void; moveUp: (count?: number) => void; moveDown: (count?: number) => void; moveToStart: () => void; // ^ or 0 moveToEnd: () => void; // $ moveToTop: () => void; // gg moveToBottom: () => void; // G repeatLastMotion: () => void; // . }; ``` #### ScoreManager ```typescript type ScoreManager = { timeValue: number; // Milliseconds elapsed startTimer: () => void; stopTimer: () => void; resetTimer: () => void; totalKeystrokes: number; // All keys pressed hintsUsed: number; // Number of hints requested finalScore: number | null; // 0-1000, null until game-won gameOverReason: string | null; // "Time's up!" or "Too many hints!" }; ``` --- ## Game States The game follows a strict state machine: ``` waiting โ†’ started โ†’ game-won โ†“ game-over All states โ†’ [quit] โ†’ waiting started โ†” paused ``` ### State Descriptions | State | Description | Triggers | |-------|-------------|----------| | `waiting` | Initial state, awaiting start | Default on load | | `started` | Game in progress | Press Space or call `startGame()` | | `paused` | Game temporarily paused | Press P or call `togglePause()` | | `game-won` | Puzzle completed successfully | All cells filled correctly | | `game-over` | Failed to complete in time/hints | Time limit or hint limit exceeded | --- ## VIM Controls ### Normal Mode (Default) #### Movement | Key | Action | Example | |-----|--------|---------| | `h` | Move left | `h` moves 1 left | | `j` | Move down | `j` moves 1 down | | `k` | Move up | `k` moves 1 up | | `l` | Move right | `l` moves 1 right | | `` | Move with count | `5j` moves 5 down, `3l` moves 3 right | | `0` or `^` | Jump to row start | Move to column 0 | | `$` | Jump to row end | Move to column 8 | | `gg` | Jump to board top | Move to row 0 | | `G` | Jump to board bottom | Move to row 8 | | `.` | Repeat last motion | Repeats with same count | #### Edit Commands | Key | Action | Valid On | Mode | |-----|--------|----------|------| | `i` | Insert digit | โœ… Empty cells only | Multi-edit: type digits until Esc | | `r` | Replace digit | โœ… User-entered cells only | Single-edit: auto-exit after 1 digit | | `c` | Change digit | โœ… User-entered cells only | Single-edit: auto-exit after 1 digit | | `x` | Delete digit | โœ… User-entered cells only | Instant | | `d` | Delete digit | โœ… User-entered cells only | Instant | | `Delete` | Delete digit | โœ… User-entered cells only | Instant | | `Backspace` | Invalid move | โŒ All cells | Red flash (use in edit mode) | #### Hints & Game Control | Key | Action | Notes | |-----|--------|-------| | `Shift+H` | Request hint | Penalty: 25/50/100 pts based on difficulty | | `q` | Quit game | Return to waiting state | | `p` | Pause/unpause | Toggle pause state | | `Space` | Start new game | Only in waiting/game-over state | ### Edit Mode (i/r/c to enter) | Key | Action | |-----|--------| | `1-9` | Enter digit in current cell | | `Backspace` | Clear current cell (stay in edit mode) | | `Escape` | Exit to normal mode | --- ## Cell State Rules Understanding cell states is crucial for VIM-style editing: | Cell State | `i` Insert | `r`/`c` Replace | `x`/`d`/`Delete` | Visual Class | |------------|------------|-----------------|------------------|--------------| | **Empty** | โœ… Enter multi-edit | ๐Ÿ”ด Flash (use i) | ๐Ÿ”ด Flash (nothing to delete) | None | | **User-entered** | ๐Ÿ”ด Flash (use r/c) | โœ… Enter single-edit | โœ… Delete instantly | `.user-entered` | | **Given** | ๐Ÿ”ด Flash (locked) | ๐Ÿ”ด Flash (locked) | ๐Ÿ”ด Flash (locked) | `.given` | ### Visual Feedback - ๐Ÿ”ต **Blue outline** โ€“ Normal mode cursor (`.active`) - ๐ŸŸก **Yellow pulsing glow** โ€“ Edit mode cursor (`.editing`) - ๐Ÿ”ด **Red flash** โ€“ Invalid move (`.invalid-move` - 500ms) - ๐Ÿ”ด **Red flash + background** โ€“ Hint: incorrect cell (`.hint-flash-error` - 1000ms) - ๐ŸŸข **Green flash + background** โ€“ Hint: correct digit shown (`.hint-flash-correct` - 1000ms) --- ## Scoring System ### Formula ``` Base Score = 1000 - (time penalty) - (keystroke penalty) - (hint penalty) Final Score = min(1000, max(0, round(Base Score ร— difficulty multiplier))) ``` ### Penalties **Time Penalty:** `seconds / 10` - 10 seconds = -1 point - 60 seconds = -6 points - 300 seconds = -30 points **Keystroke Penalty:** `totalKeystrokes / 2` - 2 keystrokes = -1 point - 50 keystrokes = -25 points - 200 keystrokes = -100 points **Hint Penalty:** `hintsUsed ร— penalty` - Easy: 25 points per hint - Medium: 50 points per hint - Hard: 100 points per hint ### Difficulty Multipliers - Easy: **1.0x** (no bonus) - Medium: **1.5x** (can exceed 1000 base, capped at 1000) - Hard: **2.0x** (can exceed 1000 base, capped at 1000) ### Example Scores **Easy Mode (25 cells, 1.0x):** ``` 30 seconds, 50 keys, 0 hints: = 1000 - 3 - 25 - 0 = 972 ร— 1.0 = 972 / 1000 ``` **Hard Mode (50 cells, 2.0x):** ``` 120 seconds, 150 keys, 2 hints: = 1000 - 12 - 75 - 200 = 713 ร— 2.0 = 1000 / 1000 (capped) ``` --- ## Game Over Conditions ### Time Limit - **Default:** 600 seconds (10 minutes) for all difficulties - **Configurable:** Set via `GameOptions.timeLimit` - **Trigger:** When `timeValue >= timeLimit ร— 1000` - **Message:** "Time's up!" ### Hint Limit - **Threshold:** 500 points total hint penalty - **Easy:** 20 hints max (20 ร— 25 = 500) - **Medium:** 10 hints max (10 ร— 50 = 500) - **Hard:** 5 hints max (5 ร— 100 = 500) - **Trigger:** When `hintsUsed ร— HINT_PENALTY[difficulty] >= 500` - **Message:** "Too many hints!" Both conditions checked continuously during gameplay. Timer stops on game-over. --- ## Hint System Press `Shift+H` in normal mode to request a hint. ### Hint Priority **Priority 1: Show Incorrect Cell (Red Flash)** - Finds all user-entered cells with wrong values - Selects one randomly - Flashes red with background for 1 second - Does NOT reveal correct digit (player must figure it out) **Priority 2: Show Correct Digit (Green Flash)** - If no incorrect cells exist - Finds all empty cells - Selects one randomly - Shows correct digit with green flash for 1 second - Digit disappears after flash (player must remember and enter it) ### Hint Penalties Hints subtract directly from base score before multiplier: - Easy: -25 points per hint - Medium: -50 points per hint - Hard: -100 points per hint After 500 points of penalties, game-over triggers. --- ## Configuration ### Recommended Configurations **Beginner Practice:** ```typescript useGame({ difficulty: 'easy', removedCells: 15, timeLimit: 900 // 15 minutes }) ``` **Standard Easy:** ```typescript useGame({ difficulty: 'easy' }) // 25 cells, 10 minutes, 1.0x multiplier ``` **Standard Medium:** ```typescript useGame({ difficulty: 'medium' }) // 40 cells, 10 minutes, 1.5x multiplier ``` **Standard Hard:** ```typescript useGame({ difficulty: 'hard' }) // 50 cells, 10 minutes, 2.0x multiplier ``` **Speed Challenge:** ```typescript useGame({ difficulty: 'medium', timeLimit: 300 // 5 minutes }) ``` **Custom Difficulty:** ```typescript useGame({ difficulty: 'easy', // Easy scoring (1.0x, 25pt hints) removedCells: 60, // But very hard puzzle timeLimit: 1200 // Generous time (20 min) }) ``` --- ## Example App A demo application lives under `example/` and consumes the package directly. ```bash cd example npm install npm run dev ``` The example shows: - Difficulty selection (Easy/Medium/Hard) - Live scoreboard (Time, Keystrokes, Hints) - Game status messages - Final score display on win - All vim controls working --- ## Platform Hook Optional callback for platform integration: ```typescript function myPlatformHook(gameManager: GameManager) { // Track analytics console.log('Game initialized'); // Add custom key handlers window.addEventListener('keydown', (e) => { if (e.key === 'F1') { console.log('Help requested'); } }); // Monitor game events const interval = setInterval(() => { if (gameManager.gameStatus === 'game-won') { console.log('Victory!', gameManager.scoreManager.finalScore); clearInterval(interval); } }, 100); } const gameManager = useGame({ difficulty: 'easy' }, myPlatformHook); ``` --- ## License MIT ยฉ [Andrรฉ Padez](https://github.com/andrepadez)