# @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** 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)