- Add useMazeNavigation for pure maze geometry and path validation - Add useVimMotions for pure Vim-style input parsing - Add useAnimation for pure movement animation system - Refactor useCursor to use specialized hooks with cleaner interface - Refactor useHero to pure game logic without DOM manipulation - Refactor useHeroRender to pure visual presentation layer - Fix dual position system - hero position now authoritative - Replace global events with callback props and return values - Implement state-driven rendering instead of direct DOM queries - Update GameManager interface with new HeroManager and HeroRenderer - Maintain backward compatibility with legacy Cursor type - Update UNIFIED_API.md with new architecture and interfaces
14 KiB
Unified API Design
Overview
The VIMazing game engine provides a uniform external API for building grid-based games. The architecture is designed around three core hooks that can be composed to create various game types (maze, puzzle, strategy, etc.).
Core Philosophy
Single Entry Point: Platforms consume games by calling useGame(gameOptions?, platformHook?) and receive everything needed to run the game.
Composable Architecture: Three fundamental hooks that work together:
useBoard- Manages the game board/level/fielduseCursor- Manages player/actor/selector movementuseGame- Orchestrates everything together
Primary API
useGame(gameOptions?, platformHook?)
The main entry point for consuming any game.
const gameManager = useGame(gameOptions?, platformHook?)
Parameters:
gameOptions?: GameOptions- Game-specific configurationplatformHook?: (gameManager: GameManager) => void- Platform-specific integration
Returns: GameManager - Complete game management interface
Core Types
GameOptions
type GameOptions = {
rows: number;
cols: number;
};
GameManager
type GameManager = {
containerRef: RefObject<HTMLDivElement | null>;
renderBoard: () => void;
cursor: CursorManager;
hero: HeroManager;
renderer: HeroRenderer;
} & GameStatusManager & GameKeyManager;
GameStatus
type GameStatus = 'waiting' | 'started' | 'has-key' | 'paused' | 'game-over' | 'game-won'
CursorManager
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;
moveToEnd: () => void;
moveToTop: () => void;
moveToBottom: () => void;
repeatLastMotion: () => void;
resetCount: () => void;
getCount: () => string;
setCount: (digit: string) => void;
setLastKey: (key: string) => void;
getLastKey: () => string;
hero?: UseHeroType;
}
HeroManager
type HeroManager = {
position: Coord | null;
canMoveTo: (coord: Coord) => boolean;
moveTo: (coord: Coord) => void;
pickupKey: () => void;
reachExit: () => void;
reset: () => void;
}
HeroRenderer
type HeroRenderer = {
render: () => void;
updatePosition: (coord: Coord) => void;
showCoordinates: (show: boolean) => void;
}
UseHeroType
type UseHeroType = {
heroPos: Coord | null;
setHeroPos: (pos: Coord | null) => void;
moveHero: (dr: number, dc: number, steps: number, gameStatus: GameStatus, setGameStatus: (s: GameStatus) => void) => void;
canMoveTo: (coord: Coord) => boolean;
moveTo: (coord: Coord) => void;
pickupKey: () => void;
reachExit: () => void;
reset: () => void;
}
Cursor (Legacy)
type Cursor = CursorManager; // For backward compatibility
CursorMode
type CursorMode = 'normal' | 'insert' | 'replace' | 'visual' | 'visual-line';
Coord
type Coord = { row: number, col: number };
GameStatusManager
type GameStatusManager = {
gameStatus: GameStatus;
setGameStatus: (status: GameStatus) => void;
startGame: () => void;
togglePause: (pause?: boolean) => void;
quitGame: () => void;
};
GameKeyManager
type GameKeyManager = {
keyLog: KeyLogEntry[];
clearKeyLog: () => void;
getKeyLog: () => KeyLogEntry[];
};
CursorBindingContext
type CursorBindingContext = {
moveLeft: (count?: number) => void;
moveRight: (count?: number) => void;
moveUp: (count?: number) => void;
moveDown: (count?: number) => void;
moveToStart: () => void;
moveToEnd: () => void;
moveToTop: () => void;
moveToBottom: () => void;
repeatLastMotion: () => void;
resetCount: () => void;
getCount: () => string;
setCount: (digit: string) => void;
setLastKey: (key: string) => void;
getLastKey: () => string;
};
UseKeyBindingsParams
type UseKeyBindingsParams = {
cursor: CursorBindingContext;
gameStatus: GameStatus;
};
Core Hooks Architecture
useBoard(cols, rows) -> BoardManager
Manages the game board/level structure.
Returns:
containerRef- DOM container referencemazeRef- Board data referencemazeInstanceRef- Board generator instancerendererRef- Renderer instancerenderBoard()- Initialize/render the board
useCursor(board: BoardManager, gameStatusManager: GameStatusManager) -> Cursor & { keyManager: GameKeyManager }
Manages player/actor movement and state, now includes integrated key management.
Returns:
- Position tracking
- Mode management
- Movement methods (with count support)
- Directional movement helpers
- Integrated
keyManagerfor key logging and input handling
useGameKeys({ cursor, gameStatusManager }) -> GameKeyManager
Internal hook that handles keyboard input and maps keys to cursor actions.
Parameters:
cursor- Cursor binding context with movement methodsgameStatusManager- Game status manager for state checking
Returns:
keyLog- Array of key press entries with timestampsclearKeyLog()- Clear the key loggetKeyLog()- Get current key log
Note: This is now integrated within useCursor and not exposed as a separate hook.
useGameStatus() -> GameStatusManager
Manages game state and lifecycle.
Returns:
gameStatus- Current game statesetGameStatus- State setterstartGame()- Start the gametogglePause(pause?)- Toggle or set pause statequitGame()- Quit/reset the game
useGame(options: GameOptions, platformHook?) -> GameManager
Orchestrates board, cursor, and game status, provides game lifecycle management.
Returns:
- Complete game management interface
- Game state control
- Platform integration point
Platform Integration
Platform Hook Pattern
const platformHook = (gameManager: GameManager) => {
// Platform-specific logic
// - Custom key bindings
// - Analytics tracking
// - Save/load functionality
// - Platform UI integration
};
const gameManager = useGame(gameOptions, platformHook);
Game Implementation Pattern
For New Game Types
- Extend
GameOptionswith game-specific configuration - Extend
GameStatuswith game-specific states - Implement game logic in core hooks
- Maintain same external API -
useGame(options, platformHook)
Example: Maze Game
// Platform consumes the game
const mazeGame = useGame({ rows: 20, cols: 16 }, platformHook);
// Everything needed is provided
return (
<div ref={mazeGame.containerRef}>
{/* Game renders here */}
<button onClick={mazeGame.startGame}>Start</button>
<div>Status: {mazeGame.gameStatus}</div>
</div>
);
Design Principles
1. Uniform Interface
All games expose the same GameManager interface regardless of internal complexity.
2. Composable Architecture
Core hooks can be mixed and matched for different game types.
3. Platform Agnostic
Games work across different platforms without modification.
4. Type Safety
Full TypeScript support with generated declarations.
5. Tree-Shakeable
Only used functionality is included in the final bundle.
Migration Path
From Old Architecture
- Old: Multiple specialized managers (
useMaze,usePlayer,useScore, etc.) - New: Three core hooks with unified interfaces
Benefits
- Simpler consumption: Single
useGamecall - Consistent API: Same pattern across all games
- Better maintainability: Clear separation of concerns
- Platform flexibility: Easy integration points
Future Extensibility
The unified API is designed to accommodate:
- Different game types (puzzles, strategy, arcade)
- Various input methods (touch, gamepad, voice)
- Multiple rendering backends (Canvas, WebGL, DOM)
- Cross-platform features (save states, leaderboards, multiplayer)
Implementation Status
- ✅ Type definitions - Complete
- ✅ Core architecture - Defined
- ✅ useGameStatus implementation - Complete
- ✅ useGameKeys implementation - Complete (integrated into useCursor)
- ✅ useCursor implementation - Complete (with integrated key management)
- ✅ useBoard implementation - Complete
- ✅ useGame implementation - Complete (with flattened GameManager interface)
- ✅ React 19 optimization - No useCallback/useMemo usage
- ✅ API refactoring - Complete (key management moved into cursor module)
- ✅ Architecture refactoring - Complete (separation of concerns implemented)
- ✅ Specialized hooks - Complete (useMazeNavigation, useVimMotions, useAnimation)
- ✅ Single source of truth - Complete (hero position as authoritative)
- ✅ React-first architecture - Complete (state-driven rendering)
- ✅ Platform integration - Ready
- ⏳ Advanced features - Pending (scoring, animations, etc.)
Recent Architecture Changes
Separation of Concerns (Phase 1-3 Complete)
- useMazeNavigation: Pure maze geometry and path validation
- useVimMotions: Pure Vim-style input parsing
- useAnimation: Pure movement animation system
- useCursor: Input coordination and cursor state
- useHero: Pure game logic and state management
- useHeroRender: Pure visual presentation layer
Single Source of Truth
- Hero position is now the authoritative game state
- Cursor position is derived from hero position
- All rendering is driven by state changes
Communication Patterns
- Global events replaced with callback props and return values
- Direct DOM manipulation moved to render layer
- State-driven rendering instead of imperative updates
Usage Examples
Basic Game Integration
import { useGame } from "@vimazing/vim-maze";
function App() {
const gameManager = useGame({ cols: 32, rows: 24 });
const { containerRef, gameStatus, keyManager } = gameManager;
return (
<div className="relative mx-auto my-4 w-fit">
<div ref={containerRef} className="relative" />
</div>
);
}
Platform Integration Pattern
import { useGame } from "@vimazing/vim-maze";
import { useKeyBindings } from "./useKeyBindings";
function App() {
const gameManager = useGame({ cols: 32, rows: 24 }, useKeyBindings);
const { containerRef, gameStatus, cursor, keyManager } = gameManager;
return (
<div className="relative mx-auto my-4 w-fit">
<div ref={containerRef} className="relative" />
</div>
);
}
Platform Hook Implementation
export const useKeyBindings = (gameManager: GameManager) => {
const { gameStatus, startGame, quitGame, cursor, clearKeyLog } = gameManager;
useEffect(() => {
const handler = (e: KeyboardEvent) => {
// Start game with 'i'
if (e.key === "i" || e.key === "I") {
if (["waiting", "game-over", "game-won"].includes(gameStatus)) {
clearKeyLog();
startGame();
return;
}
}
// Movement keys
if (gameStatus === "started") {
switch (e.key) {
case "h": cursor.moveLeft(); break;
case "j": cursor.moveDown(); break;
case "k": cursor.moveUp(); break;
case "l": cursor.moveRight(); break;
}
}
// Quit with 'q'
if (e.key === "q" || e.key === "Q") {
quitGame();
}
};
window.addEventListener("keydown", handler);
return () => window.removeEventListener("keydown", handler);
}, [gameStatus, startGame, quitGame, cursor, clearKeyLog]);
};
Internal Key Bindings Integration
// Inside useGame - simplified integration
const { keyManager, ...cursor } = useCursor(board, gameStatusManager);
const gameManager = {
containerRef,
renderBoard,
cursor,
...keyManager,
...gameStatusManager,
};
Responsive Game Sizing
function App() {
const isMobile = useIsMobile();
const cols = isMobile ? 12 : 32;
const rows = isMobile ? 8 : 24;
const gameManager = useGame({ cols, rows }, useKeyBindings);
const { containerRef, keyManager } = gameManager;
return (
<div className="relative mx-auto my-4 w-fit">
<div ref={containerRef} className="relative" />
</div>
);
}
Platform Integration Examples
Key Binding Platform Hook
Platforms can provide custom key bindings by passing a platform hook:
const platformHook = (gameManager: GameManager) => {
// Custom VIM-style bindings
const { gameStatus, cursor, startGame, quitGame } = gameManager;
// Implementation details...
};
const gameManager = useGame(gameOptions, platformHook);
Analytics Platform Hook
const analyticsHook = (gameManager: GameManager) => {
const { gameStatus } = gameManager;
useEffect(() => {
analytics.track('game_status_change', { status: gameStatus });
}, [gameStatus]);
};
Save/Load Platform Hook
const saveLoadHook = (gameManager: GameManager) => {
const { cursor, gameStatus } = gameManager;
const saveGame = () => {
localStorage.setItem('gameState', JSON.stringify({
position: cursor.position(),
status: gameStatus
}));
};
// Auto-save on status changes
useEffect(() => {
saveGame();
}, [gameStatus]);
};
Expected Platform Hook Interface
Platform hooks receive the complete GameManager interface:
type GameManager = {
containerRef: RefObject<HTMLDivElement | null>;
renderBoard: () => void;
cursor: CursorManager;
hero: HeroManager;
renderer: HeroRenderer;
} & GameStatusManager & GameKeyManager;
This allows platforms to:
- Add custom input handling
- Implement analytics tracking
- Add save/load functionality
- Integrate with platform-specific UI
- Extend game behavior without modifying core logic
- Access key logging functionality via
clearKeyLog()andgetKeyLog()