From 252027dab28caa3276f806b569aa54190da894da Mon Sep 17 00:00:00 2001 From: Andre Padez Date: Sun, 19 Oct 2025 15:55:52 +0100 Subject: [PATCH] no more ReturnType --- IMPLEMENTATION_INSTRUCTIONS.md | 134 ++++ UNIFIED_API.md | 987 ++++++++++++++++++++++++++++++ example/src/App.tsx | 1 + src/types.ts | 80 ++- src/useBoard/useMazeNavigation.ts | 22 +- src/useCursor/useAnimation.ts | 12 +- src/useCursor/useHero.ts | 8 +- src/useCursor/useHeroRender.ts | 19 +- src/useCursor/useVimMotions.ts | 17 +- src/useScore/useScore.ts | 26 +- src/useScore/useScorePaths.ts | 12 +- src/useScore/useScoreTime.ts | 5 +- src/useScore/useTimer.ts | 9 +- 13 files changed, 1242 insertions(+), 90 deletions(-) create mode 100644 IMPLEMENTATION_INSTRUCTIONS.md create mode 100644 UNIFIED_API.md diff --git a/IMPLEMENTATION_INSTRUCTIONS.md b/IMPLEMENTATION_INSTRUCTIONS.md new file mode 100644 index 0000000..f0d4621 --- /dev/null +++ b/IMPLEMENTATION_INSTRUCTIONS.md @@ -0,0 +1,134 @@ +# Implementation Instructions for New Games + +## Goal +Create a comprehensive `UNIFIED_API.md` that documents the current architecture in `src/` (not `src_old/`), replacing the old `UNIFIED_API_OLD.md`. This will serve as the blueprint for porting existing games and creating new games using the new unified architecture. + +## Files to Reference + +### Read These First +1. **AGENTS.md** - Contains all development guidelines, code conventions, naming patterns, hook structure, and architectural principles +2. **UNIFIED_API_OLD.md** - The old API documentation (for reference on what we're replacing) + +### Reference the New Codebase +- **src/useGame.ts** - Main orchestrator hook, entry point +- **src/useBoard/** - Board/level management + - useBoard.ts + - MazeGenerator.ts + - MazeRenderer.ts + - useMazeNavigation.ts +- **src/useCursor/** - Player/actor management + - useCursor.ts + - useHero.ts + - useHeroRender.ts + - useVimMotions.ts + - useGameKeys.ts + - useAnimation.ts +- **src/useScore/** - Scoring system + - useScore.ts + - useScorePaths.ts + - useScoreTime.ts + - useTimer.ts +- **src/useGameStatus.ts** - Game state management +- **src/types.ts** - All shared TypeScript types + +### Example Implementation +- **example/src/App.tsx** - Shows how to use the new unified API + +## What the New UNIFIED_API.md Should Cover + +### 1. Architecture Overview +- Single entry point: `useGame(options?, platformHook?)` +- Three core hooks composition pattern +- Scoring system integration +- Type-safe throughout + +### 2. Core Hooks Reference +For each hook, document: +- **Purpose** - What it does +- **Parameters** - What it accepts with types +- **Return Type** - What it returns with type definition +- **Dependencies** - What other hooks it depends on +- **Example Usage** - Real code example + +Core hooks to document: +- `useGame()` - Main orchestrator +- `useBoard()` - Board/maze management +- `useCursor()` - Player movement and controls +- `useScore()` - Scoring system +- `useGameStatus()` - Game state +- Supporting hooks in each module + +### 3. Type System Documentation +- Document all major types from `src/types.ts`: + - `GameManager` + - `BoardManager` + - `CursorManager` + - `ScoreManager` + - `GameStatusManager` + - `Coord`, `MazeCell`, etc. +- Show relationships between types +- Include examples of how types flow through the system + +### 4. File Structure & Organization +- Show the complete `src/` directory structure +- Explain the purpose of each module +- Show public exports from `index.ts` files + +### 5. Naming Conventions & Patterns +- Hook naming: `use[FeatureName]()` +- Type naming: `PascalCase` with descriptive names +- Function naming: `camelCase` with clear verbs +- Pattern examples from actual code + +### 6. Key Implementation Details +- How board generation and rendering works +- How player movement and vim motions are handled +- How scoring is calculated +- How game status transitions work +- Animation system for multi-step movements +- Keyboard input handling and key binding system + +### 7. Integration Points for New Games +- What you need to implement for a new game +- How to customize board generation +- How to customize player movement rules +- How to customize scoring logic +- Platform hook integration (if applicable) + +### 8. Code Examples +- Minimal game setup example +- Complete feature examples (movement, scoring, rendering) +- Type usage examples +- Common patterns and best practices + +### 9. Migration Guide (Optional) +- How to port games from `src_old/` structure to new structure +- Common gotchas and solutions +- Before/after code comparisons + +## Output Requirements + +The new `UNIFIED_API.md` should: +- Be comprehensive but readable +- Use clear sections and subsections +- Include code blocks for all examples +- Show actual types and function signatures from the codebase +- Be focused on developers porting games or creating new ones +- Be maintainable - designed to stay in sync with actual code in `src/` + +## Success Criteria + +When done, a developer should be able to: +1. Understand the complete architecture without reading source code +2. Know exactly what types and functions are available +3. Understand how hooks compose together +4. Have templates/examples for common tasks +5. Easily port a game from `src_old/` to new structure +6. Create a completely new game following this pattern + +## Notes + +- AGENTS.md already contains code conventions and patterns - reference it heavily +- Focus the new doc on WHAT each part does and HOW to use it, not just code style +- Keep the doc well-organized so it's easy to find specific information +- Show how everything connects together (data flow, composition) diff --git a/UNIFIED_API.md b/UNIFIED_API.md new file mode 100644 index 0000000..9b14f73 --- /dev/null +++ b/UNIFIED_API.md @@ -0,0 +1,987 @@ +# 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 + +1. [Architecture Overview](#architecture-overview) +2. [File Structure](#file-structure) +3. [Type System](#type-system) +4. [Core Hooks](#core-hooks) +5. [Supporting Hooks](#supporting-hooks) +6. [Implementation Examples](#implementation-examples) +7. [Integration Points](#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 + +```tsx +// 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()`. + +```typescript +type GameManager = { + // DOM reference for rendering + containerRef: RefObject; + + // 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. + +```typescript +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. + +```typescript +type BoardManager = { + containerRef: RefObject; + mazeRef: RefObject; + mazeInstanceRef: RefObject; + rendererRef: RefObject; + renderBoard: () => void; +}; +``` + +#### CursorManager +Handles player input and vim motion system. + +```typescript +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. + +```typescript +type HeroManager = { + position: Coord | null; + canMoveTo: (coord: Coord) => boolean; + moveTo: (coord: Coord) => void; + pickupKey: () => void; + reachExit: () => void; + reset: () => void; +}; +``` + +#### HeroRenderer +DOM rendering for hero. + +```typescript +type HeroRenderer = { + render: () => void; + updatePosition: (coord: Coord) => void; + showCoordinates: (show: boolean) => void; +}; +``` + +#### ScoreManager +Scoring system return type. + +```typescript +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. + +```typescript +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**: +```typescript +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 rendering +- `useCursor()` - Input and movement +- `useGameStatus()` - Game lifecycle +- `useScore()` - Scoring system +- `useHeroRender()` - Visual rendering + +**Example**: +```tsx +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**: +```typescript +useBoard(cols: number, rows: number): BoardManager +``` + +**Return Type**: `BoardManager` +```typescript +{ + containerRef: RefObject; + mazeRef: RefObject; + mazeInstanceRef: RefObject; + rendererRef: RefObject; + renderBoard: () => void; +} +``` + +**Dependencies**: +- `MazeGenerator` - Generates random maze +- `MazeRenderer` - Renders maze to DOM +- `useMazeNavigation` - Pathfinding utilities + +**Key Features**: +- Generates randomized maze on mount +- Places key randomly in maze +- Manages maze reference for navigation +- Handles board re-rendering + +**Example**: +```tsx +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**: +```typescript +useCursor( + board: BoardManager, + gameStatusManager: GameStatusManager +): CursorManager & { keyManager: GameKeyManager } +``` + +**Return Type**: +```typescript +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 end +- `g` - Move to maze top +- `G` - Move to maze bottom +- `.` - Repeat last motion +- Number prefixes for counts (e.g., `5j` = move down 5 cells) + +**Dependencies**: +- `useHero()` - Hero position management +- `useMazeNavigation()` - Path validation +- `useVimMotions()` - Vim key processing +- `useGameKeys()` - Game-specific key handling + +**Example**: +```tsx +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**: +```typescript +useGameStatus(): GameStatusManager +``` + +**Return Type**: +```typescript +{ + 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**: +```tsx +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**: +```typescript +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**: +```tsx +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**: +```typescript +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**: +```tsx +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**: +```typescript +{ + 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 times +- `0` alone = anchor to start (special case) +- `50j` = move down 50 times +- Motion repeats use stored count + +**Example**: +```tsx +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**: +```typescript +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**: +```tsx +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**: +```typescript +{ + isAnimating: () => boolean; + animateMovement: ( + from: Coord, + to: Coord, + steps: number, + onStep: (coord: Coord) => void, + onComplete: () => void + ) => void; + cancelAnimation: () => void; +} +``` + +**Behavior**: +- Animates from `from` to `to` coordinate over `steps` cells +- Calls `onStep` for each intermediate position (enables smooth animation) +- Calls `onComplete` when finished +- Prevents overlapping animations + +**Example**: +```tsx +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**: +```typescript +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**: +```typescript +{ + timeValue: number; // Milliseconds elapsed + startTimer: () => void; + stopTimer: () => void; + resetTimer: () => void; +} +``` + +--- + +### useScorePaths() + +Pathfinding for distance calculations. + +**Purpose**: Calculates distances for scoring. + +**Parameters**: +```typescript +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**: +```typescript +{ + 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 + +```tsx +import { useGame } from '@vim-maze/src'; + +function MyGame() { + const gameManager = useGame({ cols: 32, rows: 24 }); + const { containerRef, gameStatus, startGame } = gameManager; + + return ( +
+
+ +

Status: {gameStatus}

+
+ ); +} +``` + +### Complete Feature Example with Scoring + +```tsx +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 ( +
+
+ + {gameStatus === 'waiting' && ( + + )} + + {(gameStatus === 'started' || gameStatus === 'has-key') && ( +
+

Time: {formatTime(timeValue)}

+

Efficiency: {efficiency}% {efficiency > 150 && '(Game Over!)'}

+

Keystrokes: {keystrokes} / {optimalSteps}

+
+ )} + + {gameStatus === 'game-won' && ( +
+

You Won!

+

Final Score: {finalScore}

+
+ )} +
+ ); +} +``` + +### Custom Platform Hook Integration + +```tsx +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 + +1. **Initialize**: Call `useGame(options, platformHook?)` +2. **Render Container**: Attach `gameManager.containerRef` to DOM +3. **Handle Status**: React to `gameManager.gameStatus` changes +4. **Access Managers**: + - Movement: `gameManager.cursor` + - Scoring: `gameManager.scoreManager` + - Input: `gameManager.keyLog` + - Hero: `gameManager.hero` + +### For Platform Integration + +Implement a platform hook function that receives the complete `GameManager`: + +```typescript +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: + +1. **Board Size**: Adjust `cols` and `rows` in `GameOptions` +2. **Key Bindings**: Via `useGameKeys` in custom implementations +3. **Rendering**: Override via `useHeroRender` for custom visuals +4. **Scoring**: Modify `useScore` for different scoring formulas +5. **Maze Generation**: Extend `MazeGenerator` for 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: + +```typescript +import type { GameManager, CursorManager, ScoreManager } from './types'; +import type { TimerManager, HeroRenderManager, AnimationSystem } from './types'; +``` + +### Type Imports + +Always import types with `import type`: + +```typescript +import type { GameManager, CursorManager, Coord } from './types'; +import { useGame } from './useGame'; +``` + +### Coordinate Operations + +Common patterns for working with coordinates: + +```typescript +// 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/`. diff --git a/example/src/App.tsx b/example/src/App.tsx index 1ef555e..4339d62 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -63,6 +63,7 @@ function App() {
{gameStatus === "waiting" &&

Press space to start

} {gameStatus === "started" &&

Use hjkl to change direction • Press q to quit

} + {gameStatus === "has-key" &&

Use hjkl to change direction • Press q to quit

} {gameStatus === "game-over" &&

Game Over! Press space to restart

} {gameStatus === "game-won" &&

You Won! Press space to play again

}
diff --git a/src/types.ts b/src/types.ts index cf5bae9..9d84b02 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,6 +1,5 @@ import type { RefObject } from 'react'; import { MazeRenderer, MazeGenerator } from "./useBoard"; -import type { ScoreManager } from './useScore'; export * from './useGame'; export * from './useBoard'; export * from './useCursor'; @@ -35,7 +34,7 @@ export type CellTag = "wall" | "door" | PositionTag; export type MazeCell = CellTag[]; -export interface MazeData { +export type MazeData = { maze: MazeCell[][]; width: number; height: number; @@ -70,14 +69,15 @@ export type GameStatusManager = { export type CursorMode = 'normal' | 'insert' | 'replace' | 'visual' | 'visual-line'; export type UseHeroType = { - heroPos: Coord | null; - setHeroPos: (pos: Coord | null) => void; - moveHero: (dr: number, dc: number, steps: number, gameStatus: GameStatus, setGameStatus: (s: GameStatus) => void) => void; + position: Coord | null; canMoveTo: (coord: Coord) => boolean; moveTo: (coord: Coord) => void; pickupKey: () => void; reachExit: () => void; reset: () => void; + heroPos: Coord | null; + setHeroPos: (pos: Coord | null) => void; + moveHero: (dr: number, dc: number, steps: number, gameStatus: GameStatus, setGameStatus: (s: GameStatus) => void) => void; }; export type CursorManager = { @@ -93,13 +93,13 @@ export type CursorManager = { moveToTop: () => void; moveToBottom: () => void; repeatLastMotion: () => void; - resetCount: () => void; - getCount: () => string; - hasCount: () => boolean; - setCount: (digit: string) => void; - setLastKey: (key: string) => void; - getLastKey: () => string; - hero?: UseHeroType; + resetCount: () => void; + getCount: () => string; + hasCount: () => boolean; + setCount: (digit: string) => void; + setLastKey: (key: string) => void; + getLastKey: () => string; + hero?: UseHeroType; } export type HeroManager = { @@ -119,3 +119,59 @@ export type HeroRenderer = { // Legacy type for backward compatibility export type Cursor = CursorManager; + +export type Motion = { dr: number; dc: number; steps: number }; + +export type VimMotionSystem = { + processKey: (key: string) => { type: 'move'|'anchor'|'repeat'|'count', data: any } | null; + resetCount: () => void; + getCount: () => number; + hasCount: () => boolean; + setLastMotion: (motion: Motion) => void; + repeatLastMotion: () => Motion | null; +}; + +export type AnimationSystem = { + animateMovement: (from: Coord, to: Coord, steps: number, onStep: (coord: Coord) => void, onComplete: () => void) => void; + cancelAnimation: () => void; + isAnimating: () => boolean; +}; + +export type MazeNavigator = { + isValidMove: (from: Coord, to: Coord) => boolean; + findAnchorTarget: (from: Coord, direction: 'left'|'right'|'top'|'bottom') => Coord | null; + validatePath: (from: Coord, direction: Coord, steps: number) => boolean; +}; + +export type HeroRenderManager = { + render: () => void; + updatePosition: (coord: Coord) => void; + showCoordinates: (show: boolean) => void; +}; + +export type TimerManager = { + timeValue: number; + startTimer: () => void; + stopTimer: () => void; + resetTimer: () => void; +}; + +export type ScorePathsResult = { + entranceToKey: number; + entranceToExit: number; + heroToKey: number; + heroToExit: number; +}; + +export type ScoreManager = { + timeValue: number; + startTimer: () => void; + stopTimer: () => void; + resetTimer: () => void; + distToKey: number; + distToExit: number; + keystrokes: number; + optimalSteps: number; + efficiency: number; + finalScore: number | null; +}; diff --git a/src/useBoard/useMazeNavigation.ts b/src/useBoard/useMazeNavigation.ts index 0019a8a..19edd66 100644 --- a/src/useBoard/useMazeNavigation.ts +++ b/src/useBoard/useMazeNavigation.ts @@ -1,10 +1,6 @@ -import type { Coord, MazeCell } from '../types'; +import type { Coord, MazeCell, MazeNavigator } from '../types'; -export type MazeNavigator = { - isValidMove: (from: Coord, to: Coord) => boolean; - findAnchorTarget: (from: Coord, direction: 'left'|'right'|'top'|'bottom') => Coord | null; - validatePath: (from: Coord, direction: Coord, steps: number) => boolean; -}; +export type { MazeNavigator } from '../types'; export function useMazeNavigation(maze: MazeCell[][]): MazeNavigator { function isValidMove(_from: Coord, to: Coord): boolean { @@ -81,11 +77,9 @@ export function useMazeNavigation(maze: MazeCell[][]): MazeNavigator { return true; } - return { - isValidMove, - findAnchorTarget, - validatePath - }; -} - -export type MazeNavigationManager = ReturnType; \ No newline at end of file + return { + isValidMove, + findAnchorTarget, + validatePath + }; +} \ No newline at end of file diff --git a/src/useCursor/useAnimation.ts b/src/useCursor/useAnimation.ts index a20c939..8d61b50 100644 --- a/src/useCursor/useAnimation.ts +++ b/src/useCursor/useAnimation.ts @@ -1,11 +1,7 @@ import { useRef } from 'react'; -import type { Coord } from '../types'; +import type { Coord, AnimationSystem } from '../types'; -export type AnimationSystem = { - animateMovement: (from: Coord, to: Coord, steps: number, onStep: (coord: Coord) => void, onComplete: () => void) => void; - cancelAnimation: () => void; - isAnimating: () => boolean; -}; +export type { AnimationSystem } from '../types'; export function useAnimation(): AnimationSystem { const animatingRef = useRef(false); @@ -88,6 +84,4 @@ export function useAnimation(): AnimationSystem { cancelAnimation, isAnimating }; -} - -export type AnimationManager = ReturnType; \ No newline at end of file +} \ No newline at end of file diff --git a/src/useCursor/useHero.ts b/src/useCursor/useHero.ts index 1163ecf..51716d3 100644 --- a/src/useCursor/useHero.ts +++ b/src/useCursor/useHero.ts @@ -1,10 +1,10 @@ import { useState, useEffect } from "react"; -import type { BoardManager, GameStatusManager, GameStatus, Coord } from "../types"; +import type { BoardManager, GameStatusManager, GameStatus, Coord, UseHeroType } from "../types"; import { useHeroRender } from "./useHeroRender"; import { useMazeNavigation } from "../useBoard"; import { useAnimation } from "./useAnimation"; -export function useHero(board: BoardManager, gameStatusManager: GameStatusManager) { +export function useHero(board: BoardManager, gameStatusManager: GameStatusManager): UseHeroType { const { mazeRef } = board; const [heroPos, setHeroPos] = useState(null); const { gameStatus, setGameStatus } = gameStatusManager; @@ -185,7 +185,5 @@ export function useHero(board: BoardManager, gameStatusManager: GameStatusManage useHeroRender({ gameStatus, board, hero }); - return hero; + return hero; } - -export type UseHeroType = ReturnType; diff --git a/src/useCursor/useHeroRender.ts b/src/useCursor/useHeroRender.ts index 793acfa..783cd6b 100644 --- a/src/useCursor/useHeroRender.ts +++ b/src/useCursor/useHeroRender.ts @@ -1,6 +1,7 @@ import { useEffect } from "react"; -import type { BoardManager, GameStatus, Coord } from "../types"; -import type { UseHeroType } from "./useHero"; +import type { BoardManager, GameStatus, Coord, UseHeroType, HeroRenderManager } from "../types"; + +export type { HeroRenderManager } from "../types"; type UseHeroRenderParams = { gameStatus: GameStatus; @@ -9,7 +10,7 @@ type UseHeroRenderParams = { relative?: boolean; }; -export function useHeroRender(params: UseHeroRenderParams) { +export function useHeroRender(params: UseHeroRenderParams): HeroRenderManager { const { gameStatus, board, hero, relative = true } = params; const { containerRef } = board; const playerPos = hero.heroPos; @@ -157,11 +158,9 @@ export function useHeroRender(params: UseHeroRenderParams) { }; }, [playerPos, containerRef]); - return { - render, - updatePosition, - showCoordinates - }; + return { + render, + updatePosition, + showCoordinates + }; } - -export type UseHeroRenderType = ReturnType; diff --git a/src/useCursor/useVimMotions.ts b/src/useCursor/useVimMotions.ts index f478de0..9dacfe0 100644 --- a/src/useCursor/useVimMotions.ts +++ b/src/useCursor/useVimMotions.ts @@ -1,15 +1,8 @@ import { useRef } from 'react'; +import type { VimMotionSystem, Motion } from '../types'; -export type Motion = { dr: number; dc: number; steps: number }; - -export type VimMotionSystem = { - processKey: (key: string) => { type: 'move'|'anchor'|'repeat'|'count', data: any } | null; - resetCount: () => void; - getCount: () => number; - hasCount: () => boolean; - setLastMotion: (motion: Motion) => void; - repeatLastMotion: () => Motion | null; -}; +export type { Motion } from '../types'; +export type { VimMotionSystem } from '../types'; export function useVimMotions(): VimMotionSystem { const countRef = useRef(''); @@ -111,6 +104,4 @@ export function useVimMotions(): VimMotionSystem { setLastMotion, repeatLastMotion }; -} - -export type VimMotionManager = ReturnType; \ No newline at end of file +} \ No newline at end of file diff --git a/src/useScore/useScore.ts b/src/useScore/useScore.ts index 6740f7d..2de5491 100644 --- a/src/useScore/useScore.ts +++ b/src/useScore/useScore.ts @@ -118,18 +118,16 @@ export function useScore({ board, hero, gameStatusManager, keyManager }: UseScor setFinalScore(score); }, [gameStatus, logicFirst, logicLast, timeValue, optimalSteps]); - return { - timeValue, - startTimer, - stopTimer, - resetTimer, - distToKey, - distToExit, - keystrokes, - optimalSteps, - efficiency, - finalScore, - }; + return { + timeValue, + startTimer, + stopTimer, + resetTimer, + distToKey, + distToExit, + keystrokes, + optimalSteps, + efficiency, + finalScore, + }; } - -export type ScoreManager = ReturnType; diff --git a/src/useScore/useScorePaths.ts b/src/useScore/useScorePaths.ts index f06243c..03cd8e1 100644 --- a/src/useScore/useScorePaths.ts +++ b/src/useScore/useScorePaths.ts @@ -1,12 +1,14 @@ import { useEffect, useState } from "react"; -import type { BoardManager } from "../types"; +import type { BoardManager, Coord, ScorePathsResult } from "../types"; + +export type { ScorePathsResult } from "../types"; type UseScorePathsParams = { board: BoardManager; - heroPos: { row: number; col: number } | null; + heroPos: Coord | null; }; -export function useScorePaths({ board, heroPos }: UseScorePathsParams) { +export function useScorePaths({ board, heroPos }: UseScorePathsParams): ScorePathsResult { const { mazeInstanceRef } = board; const [distances, setDistances] = useState({ @@ -38,7 +40,5 @@ export function useScorePaths({ board, heroPos }: UseScorePathsParams) { })); }, [heroPos, mazeInstanceRef]); - return distances; + return distances; } - -export type UseScorePathsReturn = ReturnType; diff --git a/src/useScore/useScoreTime.ts b/src/useScore/useScoreTime.ts index 0127a77..d8e517a 100644 --- a/src/useScore/useScoreTime.ts +++ b/src/useScore/useScoreTime.ts @@ -1,10 +1,9 @@ import { useEffect, useRef } from "react"; -import type { UseTimerReturn } from "./useTimer"; -import type { GameStatus } from "../types"; +import type { TimerManager, GameStatus } from "../types"; type UseScoreTimeParams = { gameStatus: GameStatus; - timer: UseTimerReturn; + timer: TimerManager; }; export function useScoreTime({ gameStatus, timer }: UseScoreTimeParams) { diff --git a/src/useScore/useTimer.ts b/src/useScore/useTimer.ts index 29afc99..74fd42a 100644 --- a/src/useScore/useTimer.ts +++ b/src/useScore/useTimer.ts @@ -1,6 +1,9 @@ import { useState, useRef, useEffect } from "react"; +import type { TimerManager } from '../types'; -export function useTimer() { +export type { TimerManager } from '../types'; + +export function useTimer(): TimerManager { const [timeValue, setTimeValue] = useState(0); const startTimeRef = useRef(null); const frameRef = useRef(null); @@ -42,7 +45,5 @@ export function useTimer() { }; }, []); - return { timeValue, startTimer, stopTimer, resetTimer }; + return { timeValue, startTimer, stopTimer, resetTimer }; } - -export type UseTimerReturn = ReturnType;