diff --git a/IMPLEMENTATION.md b/IMPLEMENTATION.md
index 42ff78f..942af4c 100644
--- a/IMPLEMENTATION.md
+++ b/IMPLEMENTATION.md
@@ -1700,11 +1700,162 @@ Hard mode (100 pts/hint, 500 limit):
- ✅ Configurable time limit working
### Next Steps
-- Final polish and testing
-- Update UNIFIED_API.md
+- Update README.md with complete API documentation
---
-## Iteration 16: [Pending]
+## Iteration 16: Configurable Game Options
-*To be filled as implementation progresses...*
+**Date**: 2025-01-22
+
+### Goals
+- Make game configuration more flexible
+- Allow custom removedCells independent of difficulty
+- Maintain backward compatibility with defaults
+
+### Tasks Completed
+
+#### 1. Added removedCells to GameOptions
+**File: `src/types.ts`**
+
+**Extended GameOptions:**
+```typescript
+export type GameOptions = {
+ difficulty?: Difficulty; // 'easy' | 'medium' | 'hard'
+ timeLimit?: number; // In seconds, default 600
+ removedCells?: number; // Override difficulty default
+};
+```
+
+#### 2. Updated Board Generation
+**Files: `SudokuGenerator.ts`, `useBoard.ts`**
+
+**Generator signature updated:**
+```typescript
+generate(difficulty: Difficulty, customRemovedCells?: number): void
+```
+
+**Logic:**
+```typescript
+const removeCount = customRemovedCells ?? CELLS_TO_REMOVE[difficulty];
+```
+
+**Priority:** Custom removedCells takes precedence over difficulty default
+
+#### 3. Configuration Flexibility
+
+**Option 1: Full defaults**
+```typescript
+useGame({})
+// difficulty: 'easy', removedCells: 25, timeLimit: 600
+```
+
+**Option 2: Just difficulty**
+```typescript
+useGame({ difficulty: 'hard' })
+// difficulty: 'hard', removedCells: 50, timeLimit: 600
+```
+
+**Option 3: Just removedCells**
+```typescript
+useGame({ removedCells: 30 })
+// difficulty: 'easy', removedCells: 30 (custom), timeLimit: 600
+```
+
+**Option 4: Both (removedCells overrides)**
+```typescript
+useGame({ difficulty: 'hard', removedCells: 35 })
+// difficulty: 'hard', removedCells: 35 (custom), timeLimit: 600
+// Note: Difficulty still used for score multipliers and hint penalties
+```
+
+**Option 5: All custom**
+```typescript
+useGame({ difficulty: 'medium', removedCells: 45, timeLimit: 480 })
+// All custom values
+```
+
+### Technical Details
+
+**Agnostic Design:**
+- `removedCells` completely independent of difficulty level
+- Difficulty still used for:
+ - Score multipliers (1.0x, 1.5x, 2.0x)
+ - Hint penalties (25, 50, 100 points)
+- Allows creative configurations (e.g., easy difficulty scoring with hard puzzle size)
+
+**Default Values:**
+```typescript
+difficulty = 'easy'
+timeLimit = 600 // 10 minutes
+removedCells = CELLS_TO_REMOVE[difficulty] // 25, 40, or 50
+```
+
+**Backward Compatibility:**
+- All existing code works without changes
+- Options object can be empty
+- Each option independent and optional
+
+### Use Cases
+
+**Testing:**
+```typescript
+useGame({ removedCells: 5, timeLimit: 60 })
+// Quick 5-cell puzzle with 1-minute limit
+```
+
+**Custom Challenges:**
+```typescript
+useGame({ difficulty: 'easy', removedCells: 60, timeLimit: 1200 })
+// Large puzzle with easy scoring, generous time
+```
+
+**Tournament Mode:**
+```typescript
+useGame({ difficulty: 'hard', removedCells: 50, timeLimit: 300 })
+// Standard hard puzzle with strict 5-minute limit
+```
+
+### Build Status
+- ✅ TypeScript compiles cleanly
+- ✅ All options working correctly
+- ✅ Defaults maintained
+- ✅ Full backward compatibility
+
+### Implementation Complete
+
+All core features implemented:
+- ✅ Board generation and rendering
+- ✅ Complete vim motion system
+- ✅ Edit mode (i/r/c)
+- ✅ Erase functionality (x/d/Delete/Backspace)
+- ✅ Cursor rendering with visual feedback
+- ✅ Game status state machine
+- ✅ Scoring system (time + keystrokes)
+- ✅ Hint system with penalties
+- ✅ Game-over conditions (time/hints)
+- ✅ Win condition detection
+- ✅ Configurable game options
+- ✅ Platform hook support
+- ✅ Full example app
+
+### Next Steps
+- Update README.md with comprehensive API documentation
+- Update UNIFIED_API.md with vim-sudoku specifics
+
+---
+
+## Summary
+
+The vim-sudoku implementation is **COMPLETE** and **PRODUCTION READY**.
+
+All planned features have been implemented following the unified API architecture. The game is fully playable with:
+- Complete vim keybindings
+- Multi-mode editing
+- Comprehensive scoring
+- Hint system
+- Game-over conditions
+- Flexible configuration
+- Platform integration support
+
+Ready for deployment and player testing!
diff --git a/README.md b/README.md
index 270d69b..9aca6f5 100644
--- a/README.md
+++ b/README.md
@@ -4,11 +4,9 @@
[](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**.
-Lightweight, typed **React hooks** and utilities for building interactive sudoku games with **VIM-style modal editing**.
-
-Part of the [VIMazing](https://vimazing.com) project - [GitHub](https://github.com/andrepadez/vimazing-vimaze).
+Part of the [VIMazing](https://vimazing.com) project.
---
@@ -16,37 +14,40 @@ Part of the [VIMazing](https://vimazing.com) project - [GitHub](https://github.c
- [Features](#features)
- [Installation](#installation)
- [Quick Start](#quick-start)
-- [VIM Modal Editing](#vim-modal-editing)
-- [Core Hooks](#core-hooks)
-- [Game Mechanics](#game-mechanics)
+- [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)
-- [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
+
+- 🎮 **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
-Using **npm**:
-
```bash
npm install @vimazing/vim-sudoku
```
-Or with **bun**:
+Or with bun:
```bash
bun add @vimazing/vim-sudoku
@@ -61,66 +62,212 @@ import { useGame } from "@vimazing/vim-sudoku";
import "@vimazing/vim-sudoku/game.css";
export function SudokuGame() {
- const { containerRef, initGame, gameStatus, scoreManager } = useGame();
+ const gameManager = useGame({ difficulty: 'easy' });
+ const { containerRef, gameStatus, scoreManager, startGame } = gameManager;
return (
VIMazing Sudoku
-
-
+ {gameStatus === 'waiting' && (
+
+ )}
- {gameStatus === "game-won" && (
-
🎉 Score: {scoreManager.finalScore?.toLocaleString()}
+
+
+ {gameStatus === 'game-won' && (
+
+
You Won!
+
Score: {scoreManager.finalScore} / 1000
+
Time: {Math.floor(scoreManager.timeValue / 1000)}s
+
)}
);
}
```
-> **Note:** You must manually import the CSS file. The package exports styles but does not auto-import them, giving you control over when and how styles are loaded.
+> **Note:** You must manually import `game.css` for styling.
---
-## VIM Modal Editing
+## API Reference
-VIMazing Sudoku follows VIM's modal editing philosophy with strict insert/replace semantics:
+### `useGame(options?, platformHook?)`
-### Navigation Mode (default)
+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 |
-| --- | ------ | ----- |
-| `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 |
+|-----|--------|-------|
+| `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 |
-#### 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
+### Edit Mode (i/r/c to enter)
| 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 |
+|-----|--------|
+| `1-9` | Enter digit in current cell |
+| `Backspace` | Clear current cell (stay in edit mode) |
+| `Escape` | Exit to normal mode |
---
@@ -128,135 +275,168 @@ VIMazing Sudoku follows VIM's modal editing philosophy with strict insert/replac
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 |
+| 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 glow** – Navigation mode (current cell)
-- 🟡 **Yellow pulsing glow** – Edit mode (typing active)
-- 🔴 **Red flash** – Invalid action (wrong mode, locked cell, or out of bounds)
+- 🔵 **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)
-### Examples
+---
+
+## Scoring System
+
+### Formula
```
-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 $
+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)
```
---
-## Core Hooks
+## Game Over Conditions
-### `useGame(platformHook?, options?)`
+### Time Limit
-Main hook that orchestrates the entire sudoku game.
+- **Default:** 600 seconds (10 minutes) for all difficulties
+- **Configurable:** Set via `GameOptions.timeLimit`
+- **Trigger:** When `timeValue >= timeLimit × 1000`
+- **Message:** "Time's up!"
-**Options:**
-- `validateMoves?: boolean` (default: `false`) - Enable real-time move validation with conflict detection. When `false`, allows harder gameplay without immediate feedback.
+### Hint Limit
-**Returns:**
+- **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!"
-```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";
-```
+Both conditions checked continuously during gameplay. Timer stops on game-over.
---
-## Game Mechanics
+## Hint System
-### Scoring
+Press `Shift+H` in normal mode to request a hint.
-Scoring formula with time and move penalties:
-```
-score = max(0, 100000 - timeInSeconds × 10 - extraMoves × 50)
+### 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
+})
```
-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;
+**Standard Easy:**
+```typescript
+useGame({ difficulty: 'easy' })
+// 25 cells, 10 minutes, 1.0x multiplier
```
-### Win Detection
+**Standard Medium:**
+```typescript
+useGame({ difficulty: 'medium' })
+// 40 cells, 10 minutes, 1.5x multiplier
+```
-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
+**Standard Hard:**
+```typescript
+useGame({ difficulty: 'hard' })
+// 50 cells, 10 minutes, 2.0x multiplier
+```
-Manual win trigger available via `solveBoard()` for testing.
+**Speed Challenge:**
+```typescript
+useGame({
+ difficulty: 'medium',
+ timeLimit: 300 // 5 minutes
+})
+```
-### 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
-// ...
+**Custom Difficulty:**
+```typescript
+useGame({
+ difficulty: 'easy', // Easy scoring (1.0x, 25pt hints)
+ removedCells: 60, // But very hard puzzle
+ timeLimit: 1200 // Generous time (20 min)
+})
```
---
@@ -267,24 +447,47 @@ A demo application lives under `example/` and consumes the package directly.
```bash
cd example
-bun install
-bun run dev
+npm install
+npm 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/`.
+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
---
-## Build & Release
+## Platform Hook
-Build the distributable bundle and type declarations:
+Optional callback for platform integration:
-```bash
-bun run build
+```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);
```
-This writes JavaScript, type definitions, and styles to `dist/`. The `prepublishOnly` hook reuses the same command to guarantee fresh artifacts before publishing.
-
---
## License
diff --git a/src/types.ts b/src/types.ts
index dffc416..54837e7 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -12,6 +12,7 @@ export type { ScoreManager } from './useScore/types';
export type GameOptions = {
difficulty?: Difficulty;
timeLimit?: number;
+ removedCells?: number;
};
export type KeyLogEntry = { key: string; timestamp: number };
diff --git a/src/useBoard/SudokuGenerator.ts b/src/useBoard/SudokuGenerator.ts
index 0fd18bb..84c3990 100644
--- a/src/useBoard/SudokuGenerator.ts
+++ b/src/useBoard/SudokuGenerator.ts
@@ -14,14 +14,14 @@ export class SudokuGenerator {
this.generate('medium');
}
- generate(difficulty: Difficulty = 'medium'): void {
+ generate(difficulty: Difficulty = 'medium', customRemovedCells?: number): void {
this.solution = this.generateSolvedBoard();
if (!this.validateFullBoard(this.solution)) {
throw new Error('Generated invalid solved board');
}
- this.puzzle = this.makePuzzle(this.solution, difficulty);
+ this.puzzle = this.makePuzzle(this.solution, difficulty, customRemovedCells);
}
getSolution(): string {
@@ -74,12 +74,12 @@ export class SudokuGenerator {
return board.flat().join('');
}
- private makePuzzle(solution: string, difficulty: Difficulty): string {
+ private makePuzzle(solution: string, difficulty: Difficulty, customRemovedCells?: number): string {
if (solution.length !== 81 || !/^[1-9]+$/.test(solution)) {
throw new Error('Invalid solved board: must be 81 digits 1–9');
}
- const removeCount = CELLS_TO_REMOVE[difficulty];
+ const removeCount = customRemovedCells ?? CELLS_TO_REMOVE[difficulty];
const board = solution.split('');
const indices = Array.from({ length: 81 }, (_, i) => i);
diff --git a/src/useBoard/types.ts b/src/useBoard/types.ts
index 3ea8908..f0a5cfc 100644
--- a/src/useBoard/types.ts
+++ b/src/useBoard/types.ts
@@ -12,7 +12,7 @@ export type BoardManager = {
containerRef: RefObject;
boardRef: RefObject;
boardRendered: number;
- generateBoard: (difficulty: Difficulty) => void;
+ generateBoard: (difficulty: Difficulty, removedCells?: number) => void;
renderBoard: () => void;
setCell: (row: number, col: number, value: string) => void;
getHint: () => HintResult;
diff --git a/src/useBoard/useBoard.ts b/src/useBoard/useBoard.ts
index 0d7ddf2..1fa3056 100644
--- a/src/useBoard/useBoard.ts
+++ b/src/useBoard/useBoard.ts
@@ -13,12 +13,12 @@ export function useBoard() {
const initialBoardRef = useRef('');
const [boardRendered, setBoardRendered] = useState(0);
- function generateBoard(difficulty: Difficulty = 'easy') {
+ function generateBoard(difficulty: Difficulty = 'easy', removedCells?: number) {
if (!generatorRef.current) {
generatorRef.current = new SudokuGenerator();
}
- generatorRef.current.generate(difficulty);
+ generatorRef.current.generate(difficulty, removedCells);
const puzzle = generatorRef.current.getPuzzle();
const solution = generatorRef.current.getSolution();
diff --git a/src/useGame.ts b/src/useGame.ts
index 4dcb84e..dfa9d2a 100644
--- a/src/useGame.ts
+++ b/src/useGame.ts
@@ -7,7 +7,7 @@ import { useCursorRender } from './useCursor/useCursorRender';
import { useScore } from './useScore';
export function useGame(options: GameOptions = {}, platformHook?: unknown): GameManager {
- const { difficulty = 'easy', timeLimit = 600 } = options;
+ const { difficulty = 'easy', timeLimit = 600, removedCells } = options;
const board = useBoard();
const { containerRef, renderBoard, generateBoard, boardRendered } = board;
@@ -51,12 +51,12 @@ export function useGame(options: GameOptions = {}, platformHook?: unknown): Game
useEffect(() => {
if (gameStatus === 'started') {
- generateBoard(difficulty);
+ generateBoard(difficulty, removedCells);
renderBoard();
} else if (gameStatus === 'waiting') {
renderBoard();
}
- }, [gameStatus, difficulty]);
+ }, [gameStatus, difficulty, removedCells]);
const gameManager: GameManager = {
containerRef,