# @vimazing/vim-sudoku
[](https://www.npmjs.com/package/@vimazing/vim-sudoku)
[](https://www.npmjs.com/package/@vimazing/vim-sudoku)
[](https://github.com/vimazing/vim-sudoku/blob/main/LICENSE)

Lightweight, typed **React hooks** and utilities for building interactive sudoku games with **VIM-style modal editing**.
Part of the [VIMazing](https://github.com/andrepadez/vimazing-vimaze) project.
---
## Contents
- [Features](#features)
- [Installation](#installation)
- [Quick Start](#quick-start)
- [VIM Modal Editing](#vim-modal-editing)
- [Core Hooks](#core-hooks)
- [Game Mechanics](#game-mechanics)
- [Example App](#example-app)
- [Build & Release](#build--release)
- [License](#license)
---
## Features
- ๐ฎ **VIM modal editing** โ Navigate in normal mode, edit cells in insert mode (R/C to enter)
- โจ๏ธ **Full VIM motions** โ `h`/`j`/`k`/`l`, counted moves, `^`/`$`/`0` for row navigation
- ๐ฏ **Smart cell locking** โ Pre-filled clues are protected with visual feedback
- โฑ๏ธ **Time & keystroke limits** โ Configurable game-over conditions
- ๐ **Built-in scoring** โ Time-based scoring with keystroke tracking
- ๐จ **Tokyo Night theme** โ Beautiful dark theme with 3x3 box separators
- ๐ฆ **Typed API** โ Full TypeScript support with generated declarations
- ๐ช **Composable hooks** โ Separate concerns for game state, rendering, input, and scoring
- ๐ฒ **Tree-shakeable exports** โ Import only what you need
---
## Installation
Using **npm**:
```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/sudoku.css";
export function SudokuGame() {
const { containerRef, initGame, gameStatus, scoreManager } = useGame();
return (
VIMazing Sudoku
{gameStatus === "game-won" && (
๐ Score: {scoreManager.finalScore?.toLocaleString()}
)}
);
}
```
---
## VIM Modal Editing
VIMazing Sudoku follows VIM's modal editing philosophy with strict insert/replace semantics:
### Navigation Mode (default)
#### Movement
| Key | Action | Notes |
| --- | ------ | ----- |
| `h` / `j` / `k` / `l` | Move cursor left/down/up/right | Core VIM navigation |
| `h/j/k/l` | Move with count | e.g., `3j` = down 3, `5l` = right 5 |
| `0` / `^` | Jump to start of row | Move to column 0 |
| `$` | Jump to end of row | Move to column 8 |
| `.` | Repeat last motion | Repeat with optional new count |
#### Editing
| Key | Action | Valid On |
| --- | ------ | -------- |
| `I` | Enter insert mode | โ
Empty cells only |
| `R` | Enter replace mode | โ
User-filled cells only |
| `D` / `X` | Delete cell value | โ
User-filled cells only |
#### Game Control
| Key | Action |
| --- | ------ |
| `Q` | Quit game |
### Edit Mode (I/R to enter)
| Key | Action |
| --- | ------ |
| `1-9` | Fill cell with number |
| `x` / `Backspace` | Clear cell |
| `Esc` / `Enter` | Exit edit mode back to navigation |
---
## Cell State Rules
Understanding cell states is crucial for VIM-style editing:
| Cell State | `I` (Insert) | `R` (Replace) | `D`/`X` (Delete) | Visual |
| ---------- | ------------ | ------------- | ---------------- | ------ |
| **Empty** (`.`) | โ
Enter edit | ๐ด Flash (use I) | โ No effect | Normal |
| **User-filled** | ๐ด Flash (use R) | โ
Enter edit | โ
Delete | Normal |
| **Pre-filled clue** | ๐ด Flash (locked) | ๐ด Flash (locked) | ๐ด Flash (locked) | Darker text |
### Visual Feedback
- ๐ต **Blue glow** โ Navigation mode (current cell)
- ๐ก **Yellow pulsing glow** โ Edit mode (typing active)
- ๐ด **Red flash** โ Invalid action (wrong mode, locked cell, or out of bounds)
### Examples
```
Empty cell: Press I โ type 5 โ Esc
Replace value: Press R โ type 7 โ Esc
Delete value: Press D (instant)
Move 3 down: Press 3j
Jump to end: Press $
```
---
## Core Hooks
### `useGame(platformHook?, options?)`
Main hook that orchestrates the entire sudoku game.
**Options:**
- `validateMoves?: boolean` (default: `false`) - Enable real-time move validation with conflict detection. When `false`, allows harder gameplay without immediate feedback.
**Returns:**
```ts
{
// Core
containerRef: RefObject,
gameStatus: GameStatus, // "waiting" | "started" | "game-won" | "game-over"
startGame: () => void,
stopGame: () => void,
initGame: () => void, // Reset board + start
// Cursor
cursor: { r: number; c: number },
moveCursor: (dr, dc, steps?) => void,
// Board
board: string, // 81-character string
initialBoard: string, // Original clues
setDigit: (digit) => void,
erase: () => void,
reset: () => void,
solveBoard: () => void,
boardToString: (human?) => string, // Format board for display
// Scoring
scoreManager: {
timeValue: number, // Time in milliseconds
finalScore: number | null,
timeLimit: number, // 60000ms (60 seconds)
keystrokeLimit: number, // 100 keystrokes
},
// Key logging
keyLog: KeyLogEntry[],
editMode: "navigation" | "edit",
}
```
### Individual Hooks
Export individual hooks for granular control:
```ts
import { useScore } from "@vimazing/vim-sudoku/useScore";
import { useBoardState } from "@vimazing/vim-sudoku/useGame/useBoardState";
```
---
## Game Mechanics
### Scoring
Scoring formula with time and move penalties:
```
score = max(0, 100000 - timeInSeconds ร 10 - extraMoves ร 50)
```
Where:
- `baseScore = 100000`
- `timePenalty = timeInSeconds ร 10`
- `extraMoves = actualKeystrokes - minMovesRequired`
- `movePenalty = extraMoves ร 50`
### Game Over Conditions
- โฑ๏ธ **Time limit**: 60 seconds
- โจ๏ธ **Keystroke limit**: 100 keystrokes
Customize in `src/useScore/useScore.ts`:
```ts
const TIME_LIMIT_MS = 60000;
const KEYSTROKE_LIMIT = 100;
```
### Win Detection
Automatic win detection is fully implemented using an efficient sum-to-45 algorithm:
- Validates when board is complete (no empty cells)
- Checks all rows sum to 45 (1+2+...+9)
- Checks all columns sum to 45
- Checks all 3x3 boxes sum to 45
Manual win trigger available via `solveBoard()` for testing.
### Board Format
```ts
// Computer format (compact)
boardToString() // "628.1...535928671..."
// Human format (readable)
boardToString(true)
// 6 2 8 | 1 . . | . 5 .
// 5 3 9 | 2 4 8 | 6 7 1
// ...
```
---
## Example App
A demo application lives under `example/` and consumes the package directly.
```bash
cd example
bun install
bun run dev
```
During local development the Vite config aliases `@vimazing/vim-sudoku` to the source folder so you can iterate without rebuilding. When publishing, run the build first (see below) so editors consume the generated declarations in `dist/`.
---
## Build & Release
Build the distributable bundle and type declarations:
```bash
bun run build
```
This writes JavaScript, type definitions, and styles to `dist/`. The `prepublishOnly` hook reuses the same command to guarantee fresh artifacts before publishing.
---
## License
MIT ยฉ [Andrรฉ Padez](https://github.com/andrepadez)