Skip to content

vimazing/vim-maze

Repository files navigation

@vimazing/vim-maze

npm version npm downloads license

VIMazing maze demo

Lightweight, typed React hooks for building interactive maze games with VIM-style navigation.

Part of the VIMazing project.


Contents


Features

  • 🎮 VIM navigation – Full hjkl movement with counts, anchors, and repeat
  • 🗺️ Procedural mazes – Algorithm-generated mazes with guaranteed solvability
  • ⌨️ Complete VIM motions – hjkl, counts (5j), anchors (^/$), gg/G, repeat (.)
  • 🎯 Maze navigation – Find key, reach exit, with wall collision detection
  • ⏱️ Time-based gameplay – Configurable time limits with game-over on timeout
  • 📊 Simple scoring – Time + keystrokes with maze size multiplier
  • 🎨 Tokyo Night theme – Beautiful dark theme with clear visual feedback
  • 📦 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-maze

Or with bun:

bun add @vimazing/vim-maze

Quick Start

import { useGame } from "@vimazing/vim-maze";
import "@vimazing/vim-maze/game.css";

export function MazeGame() {
  const gameManager = useGame({ 
    rows: 24, 
    cols: 32,
    timeLimit: 600  // 10 minutes
  });
  
  const { containerRef, gameStatus, scoreManager, startGame } = gameManager;

  return (
    <div>
      <h1>VIMazing Maze</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 = {
  rows: number;         // Maze height in cells
  cols: number;         // Maze width in cells  
  timeLimit?: number;   // In seconds, default: 600 (10 min)
};

Examples:

// Small maze
useGame({ rows: 16, cols: 24 })

// Large maze with time pressure
useGame({ 
  rows: 32, 
  cols: 48,
  timeLimit: 300  // 5 minutes
})

// Custom configuration
useGame({ 
  rows: 24,
  cols: 32,
  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 (Hero)
  cursor: CursorManager;
  hero: HeroManager;
  renderer: HeroRenderManager;
  
  // Scoring
  scoreManager: ScoreManager;
  
  // Input Tracking
  keyLog: KeyLogEntry[];
  clearKeyLog: () => void;
  getKeyLog: () => KeyLogEntry[];
};

CursorManager

type CursorManager = {
  position: () => Coord;              // Current { row, col }
  mode: () => CursorMode;             // 'normal' | 'insert'
  move: (dCols: number, dRows: number, count: number) => void;
  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;       // .
  resetCount: () => void;
  getCount: () => string;
  hasCount: () => boolean;
  hero?: HeroManager;
};

HeroManager

type HeroManager = {
  heroPos: Coord | null;              // Hero location
  canMoveTo: (coord: Coord) => boolean;
  moveTo: (coord: Coord) => void;
  pickupKey: () => void;
  reachExit: () => void;
  reset: () => void;
};

ScoreManager

type ScoreManager = {
  timeValue: number;                  // Milliseconds elapsed
  startTimer: () => void;
  stopTimer: () => void;
  resetTimer: () => void;
  distToKey: number;                  // Distance to key
  distToExit: number;                 // Distance to exit
  keystrokes: number;                 // Total keys pressed
  finalScore: number | null;          // 0-1000, null until game-won
};

Game States

The game follows a strict state machine:

waiting → started → has-key → game-won
              ↓
           game-over
              
All states → [quit] → waiting
started/has-key ↔ paused

State Descriptions

State Description Triggers
waiting Initial state, awaiting start Default on load
started Game in progress, searching for key Press Space or call startGame()
has-key Key obtained, heading to exit Hero reaches key cell
paused Game temporarily paused Press P or call togglePause()
game-over Failed to complete in time Time limit exceeded
game-won Successfully reached exit with key Hero reaches exit after getting key

VIM Controls

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, 10l moves 10 right
0 or ^ Jump to row start Move to leftmost walkable cell
$ Jump to row end Move to rightmost walkable cell
gg Jump to maze top Move to topmost walkable row
G Jump to maze bottom Move to bottommost walkable row
. Repeat last motion Repeats with same count

Game Control

Key Action Notes
q Quit game Return to waiting state
p Pause/unpause Toggle pause state
Space Start new game Only in waiting/game-over state

Movement Rules

  • Wall collision: Movement stops at walls, no wrapping
  • Counted moves: Multi-step movements (e.g., 5j) stop at first wall
  • Key pickup: Automatic when hero reaches key cell
  • Exit: Can only enter after obtaining key

Scoring System

Formula

Base Score = 1000 - (time penalty) - (keystroke penalty)
Size Multiplier = max(1.0, (rows × cols) / 500)
Final Score = min(1000, max(0, round(Base Score × Size Multiplier)))

Penalties

Time Penalty: seconds / 10

  • 10 seconds = -1 point
  • 60 seconds = -6 points
  • 300 seconds = -30 points

Keystroke Penalty: keystrokes / 2

  • 2 keystrokes = -1 point
  • 50 keystrokes = -25 points
  • 200 keystrokes = -100 points

Size Multiplier

Rewards larger, more complex mazes:

  • Small maze (16×24 = 384 cells): 1.0x multiplier
  • Medium maze (24×32 = 768 cells): 1.54x multiplier
  • Large maze (32×48 = 1536 cells): 3.07x multiplier

Example Scores

Small Maze (16×24):

60 seconds, 80 keys:
= 1000 - 6 - 40 = 954 × 1.0 = 954 / 1000

Medium Maze (24×32):

120 seconds, 150 keys:
= 1000 - 12 - 75 = 913 × 1.54 = 1000 / 1000 (capped)

Large Maze (32×48):

180 seconds, 200 keys:
= 1000 - 18 - 100 = 882 × 3.07 = 1000 / 1000 (capped)

Game Over Conditions

Time Limit

  • Default: 600 seconds (10 minutes)
  • Configurable: Set via GameOptions.timeLimit
  • Trigger: When timeValue >= timeLimit × 1000
  • States: Checked during started and has-key states

No Other Limits

Unlike vim-sudoku, vim-maze has no hint system or additional penalties. The only way to lose is running out of time.


Configuration

Recommended Configurations

Beginner:

useGame({ 
  rows: 12,
  cols: 16,
  timeLimit: 900  // 15 minutes
})

Standard Small:

useGame({ 
  rows: 16,
  cols: 24,
  timeLimit: 600  // 10 minutes
})

Standard Medium:

useGame({ 
  rows: 24,
  cols: 32,
  timeLimit: 600  // 10 minutes
})

Standard Large:

useGame({ 
  rows: 32,
  cols: 48,
  timeLimit: 600  // 10 minutes
})

Speed Challenge:

useGame({
  rows: 24,
  cols: 32,
  timeLimit: 300  // 5 minutes
})

Marathon:

useGame({
  rows: 48,
  cols: 64,
  timeLimit: 1200  // 20 minutes
})

Game Instructions Export

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

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

// Access structured instructions
console.log(gameInfo.name);           // "VIM Maze"
console.log(gameInfo.controls);       // Navigation, game controls
console.log(gameInfo.rules);          // Movement, game flow, maze elements
console.log(gameInfo.scoring);        // Formula, penalties, size multiplier, examples
console.log(gameInfo.gameOver);       // Time limit condition
console.log(gameInfo.mazeGeneration); // Algorithm, guarantees, placement
console.log(gameInfo.metrics);        // Tracked metrics during gameplay
console.log(gameInfo.objective);      // Win condition

Use cases:

  • Render in-game help screens
  • Generate tutorials
  • Display control reference
  • Show scoring breakdown
  • Explain maze 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:

  • Maze size configuration
  • Live scoreboard (Time, Keystrokes, Distances)
  • 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('Maze initialized:', gameManager.hero.heroPos);
  
  // 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 === 'has-key') {
      console.log('Key obtained!');
    }
    if (gameManager.gameStatus === 'game-won') {
      console.log('Victory!', gameManager.scoreManager.finalScore);
      clearInterval(interval);
    }
  }, 100);
}

const gameManager = useGame(
  { rows: 24, cols: 32 }, 
  myPlatformHook
);

Maze Generation

Mazes are procedurally generated using a depth-first search algorithm with guaranteed solvability:

  • Entrance: Always top-left area
  • Key: Placed in maze requiring navigation
  • Exit: Always bottom-right area
  • Paths: Guaranteed path from entrance → key → exit
  • Walls: Procedurally generated with no isolated areas

License

MIT © André Padez


Acknowledgements

Inspired by The Art of Web – Random Maze Generator and extended for the VIMazing platform.

About

VIMazing VIM Maze engine — a lightweight, typed React hook set for maze-based games.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published