feat: add configurable removedCells option and comprehensive documentation

- Add removedCells to GameOptions for custom puzzle sizes
- removedCells overrides difficulty default when provided
- Maintain backward compatibility with all defaults
- Complete IMPLEMENTATION.md with Iteration 16 and summary
- Completely rewrite README.md with comprehensive documentation:
  - Complete API reference (GameOptions, GameManager, CursorManager, ScoreManager)
  - Game states and state machine flow
  - Full VIM controls reference (movement, editing, hints, game control)
  - Cell state rules and visual feedback
  - Detailed scoring formula with examples
  - Game-over conditions (time and hint limits)
  - Hint system priority and penalties
  - Configuration examples for different play styles
  - Platform hook integration guide
- Mark implementation as COMPLETE and PRODUCTION READY
This commit is contained in:
2025-10-22 21:58:59 +01:00
parent f68098657b
commit 4236f53ef8
7 changed files with 536 additions and 181 deletions
+155 -4
View File
@@ -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!
+370 -167
View File
@@ -4,11 +4,9 @@
[![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**.
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 (
<div>
<h1>VIMazing Sudoku</h1>
<button onClick={initGame}>New Puzzle</button>
<div className="relative w-fit">
<div ref={containerRef} />
</div>
{gameStatus === 'waiting' && (
<button onClick={startGame}>Start Game</button>
)}
{gameStatus === "game-won" && (
<p>🎉 Score: {scoreManager.finalScore?.toLocaleString()}</p>
<div ref={containerRef} />
{gameStatus === 'game-won' && (
<div>
<h2>You Won!</h2>
<p>Score: {scoreManager.finalScore} / 1000</p>
<p>Time: {Math.floor(scoreManager.timeValue / 1000)}s</p>
</div>
)}
</div>
);
}
```
> **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<HTMLDivElement | null>;
// 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 |
| `<count><motion>` | 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 |
| `<count>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<HTMLDivElement>,
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
+1
View File
@@ -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 };
+4 -4
View File
@@ -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);
+1 -1
View File
@@ -12,7 +12,7 @@ export type BoardManager = {
containerRef: RefObject<HTMLDivElement | null>;
boardRef: RefObject<string>;
boardRendered: number;
generateBoard: (difficulty: Difficulty) => void;
generateBoard: (difficulty: Difficulty, removedCells?: number) => void;
renderBoard: () => void;
setCell: (row: number, col: number, value: string) => void;
getHint: () => HintResult;
+2 -2
View File
@@ -13,12 +13,12 @@ export function useBoard() {
const initialBoardRef = useRef<string>('');
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();
+3 -3
View File
@@ -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,