Skip to content

vimazing/vim-sudoku

Repository files navigation

@vimazing/vim-sudoku

npm version npm downloads license

VIM Sudoku Demo

Lightweight, typed React hooks for building interactive sudoku games with VIM-style modal editing.

Part of the VIMazing project.


Contents


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

npm install @vimazing/vim-sudoku

Or with bun:

bun add @vimazing/vim-sudoku

Quick Start

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 (
    <div>
      <h1>VIMazing Sudoku</h1>
      
      {gameStatus === 'waiting' && (
        <button onClick={startGame}>Start Game</button>
      )}
      
      <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 game.css for styling.


API Reference

useGame(options?, platformHook?)

Main orchestrator hook that composes all game functionality.

Options

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:

// 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

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

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

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
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:

useGame({ 
  difficulty: 'easy',
  removedCells: 15,
  timeLimit: 900  // 15 minutes
})

Standard Easy:

useGame({ difficulty: 'easy' })
// 25 cells, 10 minutes, 1.0x multiplier

Standard Medium:

useGame({ difficulty: 'medium' })
// 40 cells, 10 minutes, 1.5x multiplier

Standard Hard:

useGame({ difficulty: 'hard' })
// 50 cells, 10 minutes, 2.0x multiplier

Speed Challenge:

useGame({
  difficulty: 'medium',
  timeLimit: 300  // 5 minutes
})

Custom Difficulty:

useGame({
  difficulty: 'easy',      // Easy scoring (1.0x, 25pt hints)
  removedCells: 60,        // But very hard puzzle
  timeLimit: 1200          // Generous time (20 min)
})

Game Instructions Export

The package exports a structured gameInfo object containing complete game documentation:

import { gameInfo } from '@vimazing/vim-sudoku';

// Access structured instructions
console.log(gameInfo.name);           // "VIM Sudoku"
console.log(gameInfo.controls);       // Navigation, editing, deletion, hints, game
console.log(gameInfo.rules);          // Cell types, modes, visual feedback
console.log(gameInfo.scoring);        // Formula, penalties, multipliers, examples
console.log(gameInfo.gameOver);       // Time and hint limit conditions
console.log(gameInfo.hints);          // How the hint system works
console.log(gameInfo.objective);      // Win condition

Use cases:

  • Render in-game help screens
  • Generate tutorials
  • Display control reference
  • Show scoring breakdown
  • Explain game mechanics

All data is fully typed with the GameInfo type for type safety.


Example App

A demo application lives under example/ and consumes the package directly.

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:

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

About

VIMazing VIM Sudoku engine — a lightweight, typed React hook set for sudoku games.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published