22 KiB
Unified API Documentation
Complete reference for the vim-maze game engine architecture. This document describes the current implementation in src/ and serves as a blueprint for porting games and creating new games.
Table of Contents
- Architecture Overview
- File Structure
- Type System
- Core Hooks
- Supporting Hooks
- Implementation Examples
- Integration Points
Architecture Overview
The vim-maze engine follows a composable hook architecture with a single entry point: useGame(options?, platformHook?).
Design Principles
- Single Entry Point: All games use
useGame()to initialize - Composable Hooks: Three core hooks compose to create the complete system
- Type-Safe: Full TypeScript support throughout
- Platform Agnostic: Works across any platform without modification
- Platform Optional: Optional platform hook for analytics and custom bindings
Core Composition
// Inside useGame:
const boardManager = useBoard(cols, rows);
const { cursor, keyManager } = useCursor(boardManager, gameStatusManager);
const scoreManager = useScore({ board, hero, gameStatusManager, keyManager });
const heroRenderer = useHeroRender({ gameStatus, board, hero });
// Returns unified GameManager interface
return {
containerRef,
renderBoard,
cursor,
hero,
renderer,
scoreManager,
gameStatus,
startGame,
togglePause,
quitGame,
keyLog,
...
};
File Structure
src/
├── index.ts # Main package exports
├── types.ts # All shared TypeScript types
├── useGame.ts # Main orchestrator hook
├── useGameStatus/
│ └── useGameStatus.ts # Game state management
├── useBoard/
│ ├── index.ts # Public exports
│ ├── useBoard.ts # Board/maze management hook
│ ├── MazeGenerator.ts # Maze generation algorithm
│ ├── MazeRenderer.ts # DOM rendering for maze
│ └── useMazeNavigation.ts # Pathfinding and validation
├── useCursor/
│ ├── index.ts # Public exports
│ ├── useCursor.ts # Main cursor/input hook
│ ├── useHero.ts # Player position and movement
│ ├── useHeroRender.ts # Hero rendering on DOM
│ ├── useVimMotions.ts # Vim keybinding system
│ ├── useGameKeys.ts # Game-specific key handling
│ └── useAnimation.ts # Multi-step movement animation
└── useScore/
├── index.ts # Public exports
├── useScore.ts # Main scoring hook
├── useScorePaths.ts # Distance calculations
├── useScoreTime.ts # Timer integration
└── useTimer.ts # Timer state management
Type System
All types are centralized in src/types.ts for consistency and maintainability.
Main Types
GameManager
The primary interface returned by useGame().
type GameManager = {
// DOM reference for rendering
containerRef: RefObject<HTMLDivElement | null>;
// Rendering
renderBoard: () => void;
// Core managers
cursor: CursorManager;
hero: HeroManager;
renderer: HeroRenderer;
scoreManager: ScoreManager;
// Game status
gameStatus: GameStatus;
setGameStatus: (status: GameStatus) => void;
startGame: () => void;
togglePause: (pause?: boolean) => void;
quitGame: () => void;
// Input tracking
keyLog: KeyLogEntry[];
clearKeyLog: () => void;
getKeyLog: () => KeyLogEntry[];
};
GameStatus
State machine for game lifecycle.
type GameStatus =
| 'waiting' // Initial state, awaiting start
| 'started' // Game in progress, before key pickup
| 'has-key' // Key obtained, heading to exit
| 'paused' // Game paused
| 'game-over' // Game lost (efficiency > 150%)
| 'game-won'; // Game completed
BoardManager
Manages maze data and rendering.
type BoardManager = {
containerRef: RefObject<HTMLDivElement | null>;
mazeRef: RefObject<MazeCell[][]>;
mazeInstanceRef: RefObject<MazeGenerator | null>;
rendererRef: RefObject<MazeRenderer | null>;
renderBoard: () => void;
};
CursorManager
Handles player input and vim motion system.
type CursorManager = {
position: () => Coord;
mode: () => CursorMode;
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; // Move to row start (^)
moveToEnd: () => void; // Move to row end ($)
moveToTop: () => void; // Move to maze top (g)
moveToBottom: () => void; // Move to maze bottom (G)
repeatLastMotion: () => void; // Repeat last motion (.)
resetCount: () => void;
getCount: () => string;
hasCount: () => boolean;
setCount: (digit: string) => void;
setLastKey: (key: string) => void;
getLastKey: () => string;
hero?: UseHeroType;
};
HeroManager
Player position and interaction.
type HeroManager = {
position: Coord | null;
canMoveTo: (coord: Coord) => boolean;
moveTo: (coord: Coord) => void;
pickupKey: () => void;
reachExit: () => void;
reset: () => void;
};
HeroRenderer
DOM rendering for hero.
type HeroRenderer = {
render: () => void;
updatePosition: (coord: Coord) => void;
showCoordinates: (show: boolean) => void;
};
ScoreManager
Scoring system return type.
type ScoreManager = {
timeValue: number;
startTimer: () => void;
stopTimer: () => void;
resetTimer: () => void;
distToKey: number; // Distance from hero to key
distToExit: number; // Distance from hero to exit
keystrokes: number; // Total keystrokes made
optimalSteps: number; // Optimal path length
efficiency: number; // Percentage (keystrokes / optimal * 100)
finalScore: number | null; // Only set when game-won
};
Coordinate & Maze Types
Basic data structures.
type Coord = { row: number; col: number };
type CellTag = 'wall' | 'door' | 'entrance' | 'exit' | 'key' | 'hero';
type MazeCell = CellTag[]; // Array of tags for a single cell
type MazeData = {
maze: MazeCell[][];
width: number;
height: number;
cols: number;
rows: number;
totalSteps: number;
};
type GameOptions = {
rows: number;
cols: number;
};
Core Hooks
useGame()
Main orchestrator hook. Single entry point for all games.
Purpose: Composes all sub-hooks into a unified interface and optionally integrates platform-specific functionality.
Parameters:
useGame(options: GameOptions, platformHook?: unknown): GameManager
// GameOptions
{
rows: number; // Maze height in cells
cols: number; // Maze width in cells
}
// platformHook (optional)
// A function that receives the GameManager for platform integration
// function(gameManager: GameManager): void
Return Type: GameManager - Complete game interface
Dependencies:
useBoard()- Maze generation and renderinguseCursor()- Input and movementuseGameStatus()- Game lifecycleuseScore()- Scoring systemuseHeroRender()- Visual rendering
Example:
const gameManager = useGame(
{ cols: 32, rows: 24 },
platformHook // Optional platform integration
);
const { containerRef, gameStatus, startGame } = gameManager;
useBoard()
Board and maze management.
Purpose: Generates maze, manages its data structure, and handles DOM rendering.
Parameters:
useBoard(cols: number, rows: number): BoardManager
Return Type: BoardManager
{
containerRef: RefObject<HTMLDivElement | null>;
mazeRef: RefObject<MazeCell[][]>;
mazeInstanceRef: RefObject<MazeGenerator | null>;
rendererRef: RefObject<MazeRenderer | null>;
renderBoard: () => void;
}
Dependencies:
MazeGenerator- Generates random mazeMazeRenderer- Renders maze to DOMuseMazeNavigation- Pathfinding utilities
Key Features:
- Generates randomized maze on mount
- Places key randomly in maze
- Manages maze reference for navigation
- Handles board re-rendering
Example:
const board = useBoard(32, 24);
// Render maze to DOM
useEffect(() => {
if (board.containerRef.current) {
board.renderBoard();
}
}, [board]);
// Access maze data
const maze = board.mazeRef.current; // MazeCell[][]
useCursor()
Input handling and movement system with vim bindings.
Purpose: Manages player cursor movement, vim key processing, and coordinate tracking.
Parameters:
useCursor(
board: BoardManager,
gameStatusManager: GameStatusManager
): CursorManager & { keyManager: GameKeyManager }
Return Type:
CursorManager & {
keyManager: {
keyLog: KeyLogEntry[];
clearKeyLog: () => void;
getKeyLog: () => KeyLogEntry[];
}
}
Vim Motion Support:
h,j,k,l- Move left, down, up, right^- Move to row start$- Move to row endg- Move to maze topG- Move to maze bottom.- Repeat last motion- Number prefixes for counts (e.g.,
5j= move down 5 cells)
Dependencies:
useHero()- Hero position managementuseMazeNavigation()- Path validationuseVimMotions()- Vim key processinguseGameKeys()- Game-specific key handling
Example:
const { cursor, keyManager } = useCursor(board, gameStatusManager);
// Direct movement
cursor.moveLeft(5);
cursor.moveDown();
// Query state
const pos = cursor.position(); // { row, col }
const count = cursor.getCount();
// Vim motions are processed by useGameKeys
keyManager.keyLog; // Track all keystrokes
useGameStatus()
Game lifecycle state management.
Purpose: Manages game status transitions (waiting → started → has-key → game-won).
Parameters:
useGameStatus(): GameStatusManager
Return Type:
{
gameStatus: GameStatus;
setGameStatus: (status: GameStatus) => void;
startGame: () => void;
togglePause: (pause?: boolean) => void;
quitGame: () => void;
}
State Transitions:
waiting ──start──> started ──pickup key──> has-key
▲ ▲ ▲
│ │ (pause) │
│ └─────────────────────────┘
│ paused
│
└──quit─────────────────────────────────────┘
started/has-key ──efficiency > 150%──> game-over
started/has-key ──reach exit + has-key──> game-won
Example:
const gameStatus = useGameStatus();
gameStatus.startGame();
gameStatus.togglePause();
gameStatus.quitGame();
Supporting Hooks
useScore()
Scoring and performance metrics.
Purpose: Calculates efficiency, tracks time and keystrokes, computes final score.
Parameters:
useScore({
board: BoardManager;
hero: UseHeroType;
gameStatusManager: GameStatusManager;
keyManager?: { keyLog: any[] };
}): ScoreManager
Return Type: ScoreManager
Scoring Logic:
- Optimal Steps: Calculated as distance(entrance → key) + distance(key → exit)
- Efficiency:
Math.round((keystrokes / optimalSteps) * 100) - Game Over Trigger:
efficiency > 150% - Final Score:
Math.round((optimalMs / logicMs) * 100000)when game-won - Max Score: 100,000 points
Tracking:
- Real-time keystroke count
- Elapsed time
- Distance to key and exit
- Player movement steps
Example:
const score = useScore({ board, hero, gameStatusManager, keyManager });
if (score.efficiency > 100) {
// Inefficient movement
}
// Final score available only after game-won
if (gameStatus === 'game-won') {
console.log(`Final Score: ${score.finalScore}`);
}
useHero()
Player position and movement logic.
Purpose: Manages hero position, handles cell interactions (key pickup, exit), animates movement.
Parameters:
useHero(
board: BoardManager,
gameStatusManager: GameStatusManager
): UseHeroType
Return Type: UseHeroType
Features:
- Tracks player position (row, col)
- Validates moves through maze navigation
- Handles key pickup event
- Handles exit reached event
- Animates multi-step movements
- Prevents movement without key to exit
Cell Interactions:
- Key: Transitions to "has-key" state, removes key from maze
- Exit:
- Without key: Invalid move
- With key: Transitions to "game-won"
- Wall: Blocked by path validation
Example:
const hero = useHero(board, gameStatusManager);
// Query position
const pos = hero.heroPos; // { row, col } | null
// Direct movement
hero.moveTo({ row: 10, col: 15 });
// Check validity
if (hero.canMoveTo(coord)) {
hero.moveTo(coord);
}
// Reset on game end
hero.reset();
useVimMotions()
Vim keybinding and motion system.
Purpose: Processes vim keys, manages motion count prefix, tracks last motion for repeat.
Parameters: None (hook state managed internally)
Return Type: VimMotionSystem
API:
{
processKey: (key: string) => { type: 'move'|'anchor'|'repeat'|'count', data: any } | null;
resetCount: () => void;
getCount: () => number; // Returns 1 if no count
hasCount: () => boolean; // True if user entered count
setLastMotion: (motion: Motion) => void;
repeatLastMotion: () => Motion | null;
}
Supported Keys:
Move: h (left), j (down), k (up), l (right)
Anchor: ^ (start), $ (end), g (top), G (bottom)
Repeat: . (repeat last motion)
Count: 0-9 (prefix for count)
Count Behavior:
5j= move down 5 times0alone = anchor to start (special case)50j= move down 50 times- Motion repeats use stored count
Example:
const motions = useVimMotions();
const result = motions.processKey('5'); // { type: 'count', data: '5' }
const result = motions.processKey('j'); // { type: 'move', data: { dr: 1, ... } }
motions.setLastMotion({ dr: 0, dc: 1, steps: 5 });
const last = motions.repeatLastMotion(); // Re-executes the motion
useGameKeys()
Game-specific key input handling.
Purpose: Bridges vim motion system with game cursor movement.
Parameters:
useGameKeys({
cursor: CursorManager;
gameStatusManager: GameStatusManager;
}): GameKeyManager
Return Type: GameKeyManager
Responsibilities:
- Listens for keyboard input
- Routes keys through vim motion processor
- Executes corresponding cursor movements
- Tracks keystroke history
- Handles game-specific keys (space, q, p)
Example:
const keyManager = useGameKeys({ cursor, gameStatusManager });
// Keystroke history available
keyManager.keyLog; // { key: string, timestamp: number }[]
keyManager.getKeyLog();
useAnimation()
Multi-step movement animation system.
Purpose: Animates hero movement over multiple cells smoothly.
Parameters: None
Return Type:
{
isAnimating: () => boolean;
animateMovement: (
from: Coord,
to: Coord,
steps: number,
onStep: (coord: Coord) => void,
onComplete: () => void
) => void;
cancelAnimation: () => void;
}
Behavior:
- Animates from
fromtotocoordinate overstepscells - Calls
onStepfor each intermediate position (enables smooth animation) - Calls
onCompletewhen finished - Prevents overlapping animations
Example:
const animation = useAnimation();
animation.animateMovement(
{ row: 5, col: 5 },
{ row: 5, col: 10 },
5,
(coord) => {
// Called 5 times for visualization
updateHeroDisplay(coord);
},
() => {
// Called when complete
handleCellInteraction();
}
);
useHeroRender()
DOM rendering for hero character.
Purpose: Renders hero position on the game board DOM.
Parameters:
useHeroRender({
gameStatus: GameStatus;
board: BoardManager;
hero: UseHeroType;
}): HeroRenderer
Return Type: HeroRenderer
Features:
- Renders hero at current position
- Updates hero position on DOM
- Shows/hides coordinate display
- Syncs with board rendering
useTimer()
Timer state management.
Purpose: Tracks elapsed game time.
Return Type:
{
timeValue: number; // Milliseconds elapsed
startTimer: () => void;
stopTimer: () => void;
resetTimer: () => void;
}
useScorePaths()
Pathfinding for distance calculations.
Purpose: Calculates distances for scoring.
Parameters:
useScorePaths({
board: BoardManager;
heroPos: Coord | null;
}): { entranceToKey, entranceToExit, heroToKey, heroToExit }
Returns: Path distances or 0 if not calculable
useMazeNavigation()
Navigation and validation utilities.
Purpose: Validates moves, finds paths, checks cell accessibility.
Methods:
{
validatePath: (from: Coord, direction: { row, col }, steps: number) => boolean;
isValidMove: (from: Coord, to: Coord) => boolean;
findAnchorTarget: (from: Coord, direction: 'top'|'bottom'|'left'|'right') => Coord | null;
}
Implementation Examples
Minimal Game Setup
import { useGame } from '@vim-maze/src';
function MyGame() {
const gameManager = useGame({ cols: 32, rows: 24 });
const { containerRef, gameStatus, startGame } = gameManager;
return (
<div>
<div ref={containerRef} />
<button onClick={startGame}>Start</button>
<p>Status: {gameStatus}</p>
</div>
);
}
Complete Feature Example with Scoring
import { useGame } from '@vim-maze/src';
function GameWithScoring() {
const gameManager = useGame(
{ cols: 32, rows: 24 },
platformHook // Optional
);
const {
containerRef,
gameStatus,
startGame,
cursor,
hero,
scoreManager,
keyLog,
} = gameManager;
const {
timeValue,
efficiency,
optimalSteps,
keystrokes,
finalScore,
} = scoreManager;
return (
<div>
<div ref={containerRef} />
{gameStatus === 'waiting' && (
<button onClick={startGame}>Start Game</button>
)}
{(gameStatus === 'started' || gameStatus === 'has-key') && (
<div>
<p>Time: {formatTime(timeValue)}</p>
<p>Efficiency: {efficiency}% {efficiency > 150 && '(Game Over!)'}</p>
<p>Keystrokes: {keystrokes} / {optimalSteps}</p>
</div>
)}
{gameStatus === 'game-won' && (
<div>
<h2>You Won!</h2>
<p>Final Score: {finalScore}</p>
</div>
)}
</div>
);
}
Custom Platform Hook Integration
function customPlatformHook(gameManager: GameManager) {
// Called after game initialization
// Track analytics
console.log('Game initialized:', {
cols: gameManager.cursor.position().col,
rows: gameManager.cursor.position().row,
});
// Add custom key handlers
window.addEventListener('keydown', (e) => {
if (e.key === 'p') {
gameManager.togglePause();
}
});
// Monitor game events
const interval = setInterval(() => {
if (gameManager.gameStatus === 'game-won') {
console.log('Victory:', gameManager.scoreManager.finalScore);
clearInterval(interval);
}
}, 100);
}
const gameManager = useGame({ cols: 32, rows: 24 }, customPlatformHook);
Integration Points
For New Games
- Initialize: Call
useGame(options, platformHook?) - Render Container: Attach
gameManager.containerRefto DOM - Handle Status: React to
gameManager.gameStatuschanges - Access Managers:
- Movement:
gameManager.cursor - Scoring:
gameManager.scoreManager - Input:
gameManager.keyLog - Hero:
gameManager.hero
- Movement:
For Platform Integration
Implement a platform hook function that receives the complete GameManager:
type PlatformHook = (gameManager: GameManager) => void;
The hook can:
- Add custom key bindings
- Track analytics
- Synchronize with platform APIs
- Monitor game state
- Control game lifecycle
Customization Points
The current implementation supports:
- Board Size: Adjust
colsandrowsinGameOptions - Key Bindings: Via
useGameKeysin custom implementations - Rendering: Override via
useHeroRenderfor custom visuals - Scoring: Modify
useScorefor different scoring formulas - Maze Generation: Extend
MazeGeneratorfor different algorithms
Data Flow
Game Initialization
↓
useGame() composes hooks
├→ useBoard() generates maze
├→ useCursor() initializes input
├→ useGameStatus() initializes state
└→ useScore() initializes scoring
User Input
↓
useGameKeys() processes keystroke
↓
useVimMotions() interprets vim command
↓
useCursor() executes movement
↓
useHero() validates and animates
↓
useScore() updates metrics
↓
Rendering updates via refs
Type Safety Patterns
Hook Return Types
All hook return types are explicitly defined in src/types.ts. Import them directly:
import type { GameManager, CursorManager, ScoreManager } from './types';
import type { TimerManager, HeroRenderManager, AnimationSystem } from './types';
Type Imports
Always import types with import type:
import type { GameManager, CursorManager, Coord } from './types';
import { useGame } from './useGame';
Coordinate Operations
Common patterns for working with coordinates:
// Validate before moving
if (hero.canMoveTo(targetCoord)) {
hero.moveTo(targetCoord);
}
// Calculate distance
const distance = Math.abs(pos1.row - pos2.row) +
Math.abs(pos1.col - pos2.col);
// Check if equal
const same = pos1.row === pos2.row && pos1.col === pos2.col;
Summary
The vim-maze unified API provides a composable, type-safe game engine:
- Single Entry Point:
useGame()for all initialization - Modular Design: Independent, reusable hooks
- Type Safe: Comprehensive TypeScript support
- Extensible: Platform hook system for customization
- Well Structured: Clear separation of concerns
For detailed implementation, refer to source files in src/.