refactor: simplify scoring to remove efficiency-based complexity

- Remove efficiency calculation and game-over at 150%
- Remove optimal steps tracking and complex logic timing
- Simplify to time + keystrokes only scoring
- New formula: score = min(1000, (1000 - time_penalty - keystroke_penalty) × size_multiplier)
  - time_penalty = seconds / 10
  - keystroke_penalty = keystrokes / 2
  - size_multiplier = max(1.0, (rows × cols) / 500)
- Maze size-based multiplier rewards larger mazes
- Remove optimalSteps and efficiency from ScoreManager type
- Pass rows and cols to useScore for size calculation
- Align with vim-sudoku's simplified scoring approach
This commit is contained in:
2025-10-22 22:03:44 +01:00
parent 75ff8bc76e
commit 7ccf7cdd55
8 changed files with 16 additions and 2241 deletions
-191
View File
@@ -1,191 +0,0 @@
# Development Guidelines & Agent Instructions
## Documentation Management
### Automatic Documentation Updates
- **UNIFIED_API.md**: Automatically update after any implementation changes
- **Track**: Implementation status, API changes, new types, completed features
- **No prompting required**: Keep docs in sync with codebase proactively
## Code Conventions
### File Structure & Organization
```
src/
├── useBoard/ # Board/level management
│ ├── index.ts # Public exports
│ ├── useBoard.ts # Main hook implementation
│ ├── MazeGenerator.ts
│ └── MazeRenderer.ts
├── useCursor/ # Player/actor management
│ ├── index.ts # Public exports
│ └── useCursor.ts # Main hook implementation
├── types.ts # All shared TypeScript types
├── useGame.ts # Main orchestrator hook
└── index.ts # Main package exports
```
### TypeScript Patterns
- **Type-first approach**: Define types before implementation
- **Explicit imports**: Always import types with `import type`
- **Shared types**: Keep all types in `types.ts` for consistency
- **Return type annotations**: Use `ReturnType<typeof hookName>` for hook return types
### Hook Structure
```tsx
// Standard hook pattern
export function useHookName(param: Type): ReturnType {
// 1. Refs for persistent state
const ref = useRef<Type>(initialValue);
// 2. State for reactive values
const [state, setState] = useState<Type>(initialValue);
// 3. Internal functions (private)
function internalFunction() {
// Implementation
}
// 4. Public API functions
function publicMethod() {
// Implementation
}
// 5. Return object with explicit interface
return {
// Public API only
};
}
// Export type for consumers
export type HookManager = ReturnType<typeof useHookName>;
```
### Naming Conventions
- **Hooks**: `use[FeatureName]` (e.g., `useBoard`, `useCursor`, `useGame`)
- **Types**: `PascalCase` with descriptive names (e.g., `GameManager`, `BoardManager`)
- **Functions**: `camelCase` with clear verbs (e.g., `renderBoard`, `movePlayer`)
- **Refs**: `[concept]Ref` pattern (e.g., `containerRef`, `mazeRef`)
- **Constants**: `UPPER_SNAKE_CASE`
### Import Organization
```tsx
// 1. React imports
import { useRef, useState } from 'react';
import type { RefObject } from 'react';
// 2. Internal type imports
import type { Cursor, BoardManager } from '../types';
// 3. Internal implementation imports
import { MazeGenerator } from './MazeGenerator';
import { MazeRenderer } from './MazeRenderer';
```
### Code Style
- **No comments**: Avoid adding comments unless explicitly requested
- **Minimal whitespace**: Clean, concise formatting
- **Explicit returns**: Always return objects from hooks
- **Destructuring**: Use object destructuring for parameters and returns
- **Arrow functions**: Prefer arrow functions for consistency
- **No useCallback/useMemo**: React 19 optimization makes these hooks unnecessary
- **useEffect dependencies**: Only use values, never functions as dependencies
### Error Handling
- **Early returns**: Guard clauses at function start
- **Null checks**: Always validate refs and state before use
- **Graceful failures**: Return early if conditions aren't met
## Architecture Principles
### Unified API Design
- **Single entry point**: `useGame(options?, platformHook?)` for all games
- **Composable hooks**: Three core hooks (`useBoard`, `useCursor`, `useGame`)
- **Platform agnostic**: Games work across platforms without modification
- **Type safety**: Full TypeScript support throughout
### Hook Composition Pattern
```tsx
// Standard composition in useGame
export function useGame(options: GameOptions, platformHook?: unknown): GameManager {
const boardManager = useBoard(cols, rows);
const cursor = useCursor(boardManager);
// Orchestrate and return unified interface
return {
// Expose necessary functionality
};
}
```
### Platform Integration
- **Platform hook pattern**: Optional function parameter for platform-specific logic
- **Non-intrusive**: Core game logic works without platform integration
- **Extensible**: Platform can add analytics, custom bindings, etc.
## Development Workflow
### Implementation Order
1. **Types first**: Define interfaces in `types.ts`
2. **Core hooks**: Implement `useBoard`, `useCursor`
3. **Orchestrator**: Implement `useGame`
4. **Integration**: Add platform hook support
5. **Features**: Add advanced features (scoring, animations, etc.)
### Testing & Validation
- **Type checking**: Run TypeScript compiler after changes
- **Build verification**: Ensure build process works
- **Example testing**: Test with example application
### File Management
- **Prefer editing**: Modify existing files over creating new ones
- **Never create docs**: Don't create README files unless explicitly requested
- **Consistent exports**: Use `index.ts` files for clean public APIs
## Agent Behavior Rules
### Proactive Documentation
- Update `UNIFIED_API.md` after every meaningful change
- Track implementation status changes
- Document new types and interfaces
- Note API modifications and architectural decisions
### Code Generation
- Follow existing patterns exactly
- Mimic code style from surrounding files
- Use established naming conventions
- Maintain type safety throughout
### Communication
- **Concise responses**: Keep explanations brief and direct
- **Focus on code**: Prioritize implementation over explanation
- **No elaboration**: Avoid unnecessary preamble or summary
- **Direct answers**: Respond to specific requests without extra context
### Quality Assurance
- **Type safety first**: Ensure all TypeScript types are correct
- **Import consistency**: Follow established import patterns
- **Code style matching**: Mirror existing code style exactly
- **Error prevention**: Write defensive code with proper validation
## Forbidden Patterns
### Do NOT
- Add comments to code (unless requested)
- Create documentation files (except UNIFIED_API.md updates)
- Use generic variable names (`data`, `item`, etc.)
- Mix import styles (some type imports, some runtime)
- Break existing patterns without reason
- Write overly complex implementations
- Add unnecessary features
- Use `useCallback` or `useMemo` anywhere in the codebase
- Use functions as useEffect dependencies
### Always DO
- Follow existing code patterns exactly
- Use descriptive, meaningful names
- Maintain type safety
- Keep implementations simple and focused
- Update documentation proactively
- Test builds after changes
- Use plain functions instead of `useCallback`/`useMemo` (React 19 optimization)
-134
View File
@@ -1,134 +0,0 @@
# 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)
-292
View File
@@ -1,292 +0,0 @@
# Vim-Maze Architecture Refactoring Plan
## Current Problems Identified
### 1. Dual Position System
- `useCursor` maintains logical cursor position
- `useHero` maintains actual hero position
- They sync but can diverge during animations
- Creates confusion about single source of truth
### 2. Mixed Responsibilities
- **useCursor**: Input abstraction + maze geometry + event dispatching
- **useHero**: Game logic + DOM manipulation + animation + visual feedback
- **useHeroRender**: Hero rendering + coordinate system + DOM querying
### 3. Anti-Patterns
- Global events for component communication (`maze-invalid`)
- Direct DOM manipulation breaking React abstraction
- Hard-coded CSS selectors and maze structure assumptions
- Debug code left in production (`console.log`)
## Target Architecture
### Clear Separation of Concerns
```
useCursor → Pure input abstraction (Vim motions → coordinates)
useHero → Pure game logic (position validation, state transitions)
useHeroRender → Pure visual layer (CSS classes, coordinate display)
```
### Single Source of Truth
- Hero position = game state (authoritative)
- Cursor position = input intent (derived from hero)
- Render position = derived from hero position
### React-First Architecture
- State-driven rendering instead of DOM manipulation
- Callback props instead of global events
- Ref-based access instead of direct queries
## Detailed Refactoring Steps
### Phase 1: Extract Specialized Hooks
#### 1.1 Create `useMazeNavigation`
**Location:** `src/useBoard/useMazeNavigation.ts`
**Responsibilities:**
- Wall detection and boundary finding
- Path validation for movements
- Anchor target detection (line start/end, top/bottom)
- Pure functions, no side effects
**Interface:**
```tsx
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;
}
```
#### 1.2 Create `useVimMotions`
**Location:** `src/useCursor/useVimMotions.ts`
**Responsibilities:**
- Parse Vim-style input (hjkl, counts, gg, G, ^, $, .)
- Convert motions to coordinate changes
- Manage count accumulation and last motion tracking
- Pure input parsing, no game logic
**Interface:**
```tsx
type VimMotionSystem = {
processKey: (key: string) => { type: 'move'|'anchor'|'repeat', data: any } | null;
resetCount: () => void;
getCount: () => number;
setLastMotion: (motion: Motion) => void;
repeatLastMotion: () => Motion | null;
}
```
#### 1.3 Create `useAnimation`
**Location:** `src/useCursor/useAnimation.ts`
**Responsibilities:**
- Smooth interpolated movement for multi-step actions
- Animation timing and frame management
- Cancellation handling (Escape key)
- Trail effects during animation
**Interface:**
```tsx
type AnimationSystem = {
animateMovement: (from: Coord, to: Coord, steps: number, onComplete: () => void) => void;
cancelAnimation: () => void;
isAnimating: () => boolean;
}
```
### Phase 2: Refactor Core Hooks
#### 2.1 Simplify `useCursor`
**New Responsibilities:**
- Coordinate input from Vim motions
- Delegate navigation to `useMazeNavigation`
- Delegate animation to `useAnimation`
- Maintain cursor state (synced with hero)
**New Interface:**
```tsx
type CursorManager = {
position: () => Coord;
mode: () => CursorMode;
move: (direction: Coord, count?: number) => boolean;
moveToAnchor: (target: 'start'|'end'|'top'|'bottom') => boolean;
repeatLastMotion: () => boolean;
}
```
#### 2.2 Refactor `useHero`
**New Responsibilities:**
- Pure game logic and state management
- Position validation and game rule enforcement
- Game state transitions (key pickup, victory)
- No DOM manipulation or visual effects
**New Interface:**
```tsx
type HeroManager = {
position: Coord | null;
canMoveTo: (coord: Coord) => boolean;
moveTo: (coord: Coord) => void;
pickupKey: () => void;
reachExit: () => void;
reset: () => void;
}
```
#### 2.3 Refactor `useHeroRender`
**New Responsibilities:**
- Pure visual presentation
- CSS class management for hero position
- Coordinate system display
- No game logic or state management
**New Interface:**
```tsx
type HeroRenderer = {
render: () => void;
updatePosition: (coord: Coord) => void;
showCoordinates: (show: boolean) => void;
}
```
### Phase 3: Fix Communication Patterns
#### 3.1 Replace Global Events
**Current:** `window.dispatchEvent(new Event("maze-invalid"))`
**New:** Callback props and return values
```tsx
// Instead of events, use return values
const canMove = cursor.move(direction, count);
if (!canMove) {
// Handle invalid move in caller
flashInvalid();
}
```
#### 3.2 Remove Direct DOM Manipulation
**Current:** `getCellEl().classList.add("hero")`
**New:** State-driven rendering
```tsx
// In render component
<div className={`cell ${heroPosition === coord ? 'hero' : ''}`}>
```
#### 3.3 Replace Direct Queries
**Current:** `container.querySelector("#maze")`
**New:** Ref-based access
```tsx
const mazeRef = useRef<HTMLElement>(null);
// Use mazeRef.current instead of queries
```
### Phase 4: Update Integration Points
#### 4.1 Fix `useGame` Integration
```tsx
export function useGame(options: GameOptions, platformHook?: unknown): GameManager {
const board = useBoard(cols, rows);
const gameStatusManager = useGameStatus();
// New clean composition
const hero = useHero(board, gameStatusManager);
const cursor = useCursor(board, hero, gameStatusManager);
const renderer = useHeroRender(board, hero);
const keyManager = useGameKeys({ cursor, gameStatusManager });
const gameManager = {
containerRef: board.containerRef,
renderBoard: board.renderBoard,
cursor,
hero,
renderer,
...keyManager,
...gameStatusManager,
};
if (typeof platformHook === 'function') {
platformHook(gameManager);
}
return gameManager;
}
```
#### 4.2 Update Platform Hook Interface
```tsx
type GameManager = {
containerRef: RefObject<HTMLDivElement | null>;
renderBoard: () => void;
cursor: CursorManager;
hero: HeroManager;
renderer: HeroRenderer;
} & GameStatusManager & GameKeyManager;
```
### Phase 5: Clean Up and Optimize
#### 5.1 Remove Debug Code
- Remove `console.log('gameStatus', gameStatus)` from `useHeroRender`
- Remove any other debug statements
#### 5.2 Update Type Definitions
- Update all interfaces in `types.ts`
- Ensure proper export of new types
- Update `UNIFIED_API.md` documentation
#### 5.3 Update Tests and Examples
- Fix any broken tests
- Update example usage
- Ensure platform hooks still work
## Implementation Order
1. **Create new specialized hooks** (Phase 1)
2. **Refactor useCursor** (Phase 2.1)
3. **Refactor useHero** (Phase 2.2)
4. **Refactor useHeroRender** (Phase 2.3)
5. **Fix communication patterns** (Phase 3)
6. **Update integration points** (Phase 4)
7. **Clean up and optimize** (Phase 5)
## Success Criteria
- ✅ Single source of truth for position (hero position)
- ✅ Clear separation of concerns between hooks
- ✅ No global events or direct DOM manipulation
- ✅ All functionality preserved (Vim motions, animation, etc.)
- ✅ Type safety maintained throughout
- ✅ Platform hooks continue to work
- ✅ Documentation updated to reflect new architecture
## Files to Modify
### New Files
- `src/useBoard/useMazeNavigation.ts`
- `src/useCursor/useVimMotions.ts`
- `src/useCursor/useAnimation.ts`
### Modified Files
- `src/types.ts`
- `src/useCursor/useCursor.ts`
- `src/useCursor/useHero.ts`
- `src/useCursor/useHeroRender.ts`
- `src/useGame.ts`
- `UNIFIED_API.md`
### Files to Remove
- None (all existing functionality preserved)
## Risk Mitigation
- **Incremental changes**: Implement one phase at a time
- **Backward compatibility**: Ensure platform hooks still work
- **Type safety**: Use TypeScript to catch issues early
- **Testing**: Verify functionality after each phase
- **Documentation**: Keep docs in sync with changes
This refactoring will result in a cleaner, more maintainable architecture while preserving all existing functionality.
-1016
View File
File diff suppressed because it is too large Load Diff
-519
View File
@@ -1,519 +0,0 @@
# 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/field
- `useCursor` - Manages player/actor/selector movement
- `useGame` - Orchestrates everything together
## Primary API
### `useGame(gameOptions?, platformHook?)`
The main entry point for consuming any game.
```tsx
const gameManager = useGame(gameOptions?, platformHook?)
```
**Parameters:**
- `gameOptions?: GameOptions` - Game-specific configuration
- `platformHook?: (gameManager: GameManager) => void` - Platform-specific integration
**Returns:** `GameManager` - Complete game management interface
## Core Types
### `GameOptions`
```tsx
type GameOptions = {
rows: number;
cols: number;
};
```
### `GameManager`
```tsx
type GameManager = {
containerRef: RefObject<HTMLDivElement | null>;
renderBoard: () => void;
cursor: CursorManager;
hero: HeroManager;
renderer: HeroRenderer;
} & GameStatusManager & GameKeyManager;
```
### `GameStatus`
```tsx
type GameStatus = 'waiting' | 'started' | 'has-key' | 'paused' | 'game-over' | 'game-won'
```
### `CursorManager`
```tsx
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`
```tsx
type HeroManager = {
position: Coord | null;
canMoveTo: (coord: Coord) => boolean;
moveTo: (coord: Coord) => void;
pickupKey: () => void;
reachExit: () => void;
reset: () => void;
}
```
### `HeroRenderer`
```tsx
type HeroRenderer = {
render: () => void;
updatePosition: (coord: Coord) => void;
showCoordinates: (show: boolean) => void;
}
```
### `UseHeroType`
```tsx
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)
```tsx
type Cursor = CursorManager; // For backward compatibility
```
### `CursorMode`
```tsx
type CursorMode = 'normal' | 'insert' | 'replace' | 'visual' | 'visual-line';
```
### `Coord`
```tsx
type Coord = { row: number, col: number };
```
### `GameStatusManager`
```tsx
type GameStatusManager = {
gameStatus: GameStatus;
setGameStatus: (status: GameStatus) => void;
startGame: () => void;
togglePause: (pause?: boolean) => void;
quitGame: () => void;
};
```
### `GameKeyManager`
```tsx
type GameKeyManager = {
keyLog: KeyLogEntry[];
clearKeyLog: () => void;
getKeyLog: () => KeyLogEntry[];
};
```
### `CursorBindingContext`
```tsx
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`
```tsx
type UseKeyBindingsParams = {
cursor: CursorBindingContext;
gameStatus: GameStatus;
};
```
## Core Hooks Architecture
### `useBoard(cols, rows) -> BoardManager`
Manages the game board/level structure.
**Returns:**
- `containerRef` - DOM container reference
- `mazeRef` - Board data reference
- `mazeInstanceRef` - Board generator instance
- `rendererRef` - Renderer instance
- `renderBoard()` - 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 `keyManager` for 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 methods
- `gameStatusManager` - Game status manager for state checking
**Returns:**
- `keyLog` - Array of key press entries with timestamps
- `clearKeyLog()` - Clear the key log
- `getKeyLog()` - 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 state
- `setGameStatus` - State setter
- `startGame()` - Start the game
- `togglePause(pause?)` - Toggle or set pause state
- `quitGame()` - 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
```tsx
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
1. **Extend `GameOptions`** with game-specific configuration
2. **Extend `GameStatus`** with game-specific states
3. **Implement game logic** in core hooks
4. **Maintain same external API** - `useGame(options, platformHook)`
### Example: Maze Game
```tsx
// 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 `useGame` call
- **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)
- ✅ **Hero movement implementation** - Complete (hjkl, anchors, repeat with counts)
- ✅ **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
```tsx
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
```tsx
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
```tsx
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
```tsx
// Inside useGame - simplified integration
const { keyManager, ...cursor } = useCursor(board, gameStatusManager);
const gameManager = {
containerRef,
renderBoard,
cursor,
...keyManager,
...gameStatusManager,
};
```
### Responsive Game Sizing
```tsx
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:
```tsx
const platformHook = (gameManager: GameManager) => {
// Custom VIM-style bindings
const { gameStatus, cursor, startGame, quitGame } = gameManager;
// Implementation details...
};
const gameManager = useGame(gameOptions, platformHook);
```
### Analytics Platform Hook
```tsx
const analyticsHook = (gameManager: GameManager) => {
const { gameStatus } = gameManager;
useEffect(() => {
analytics.track('game_status_change', { status: gameStatus });
}, [gameStatus]);
};
```
### Save/Load Platform Hook
```tsx
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:
```tsx
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()` and `getKeyLog()`
+1 -1
View File
@@ -14,7 +14,7 @@ export function useGame(options: GameOptions, platformHook?: unknown): GameManag
const hero = cursor.hero;
const renderer = useHeroRender({ gameStatus: gameStatusManager.gameStatus, board, hero });
const scoreManager = useScore({ board, hero, gameStatusManager, keyManager });
const scoreManager = useScore({ board, hero, gameStatusManager, keyManager, rows, cols });
const gameManager: GameManager = {
containerRef,
-2
View File
@@ -20,7 +20,5 @@ export type ScoreManager = {
distToKey: number;
distToExit: number;
keystrokes: number;
optimalSteps: number;
efficiency: number;
finalScore: number | null;
};
+15 -86
View File
@@ -1,4 +1,4 @@
import { useEffect, useRef, useState } from "react";
import { useEffect, useState } from "react";
import { useTimer } from "./useTimer";
import { useScoreTime } from "./useScoreTime";
import { useScorePaths } from "./useScorePaths";
@@ -11,11 +11,13 @@ type UseScoreParams = {
hero: UseHeroType;
gameStatusManager: GameStatusManager;
keyManager?: { keyLog: any[] };
rows: number;
cols: number;
};
export function useScore({ board, hero, gameStatusManager, keyManager }: UseScoreParams) {
export function useScore({ board, hero, gameStatusManager, keyManager, rows, cols }: UseScoreParams) {
const timer = useTimer();
const { gameStatus, setGameStatus } = gameStatusManager;
const { gameStatus } = gameStatusManager;
const { timeValue, startTimer, stopTimer, resetTimer } = timer;
const {
@@ -29,96 +31,25 @@ export function useScore({ board, hero, gameStatusManager, keyManager }: UseScor
const distToExit = entranceToExit || heroToExit;
const [finalScore, setFinalScore] = useState<number | null>(null);
const optimalRef = useRef<number>(0);
const keystrokes = keyManager ? keyManager.keyLog.length : 0;
useScoreTime({ gameStatus, timer });
useEffect(() => {
const maze = board.mazeInstanceRef.current;
if (!maze) return;
const k = maze.getDistance("entrance", "key");
const e = maze.getDistance("key", "exit");
const total = k + e;
if (Number.isFinite(total) && total > 0) {
optimalRef.current = total;
}
}, [gameStatus, board.mazeInstanceRef]);
const optimalSteps = optimalRef.current;
const efficiency =
optimalSteps > 0 ? Math.round((keystrokes / optimalSteps) * 100) : 0;
const [logicFirst, setLogicFirst] = useState<number | null>(null);
const [logicLast, setLogicLast] = useState<number | null>(null);
useEffect(() => {
if (gameStatus === "started") {
setLogicFirst(null);
setLogicLast(null);
setPlayerSteps(0);
prevPosRef.current = null;
}
}, [gameStatus]);
useEffect(() => {
const onTick = (e: Event) => {
const t =
(e as CustomEvent<{ t?: number }>).detail?.t ?? performance.now();
setLogicFirst((prev) => prev ?? t);
setLogicLast(t);
};
window.addEventListener("maze-logic-tick", onTick as EventListener);
return () =>
window.removeEventListener("maze-logic-tick", onTick as EventListener);
}, []);
const [, setPlayerSteps] = useState(0);
const prevPosRef = useRef<{ row: number; col: number } | null>(null);
useEffect(() => {
const pos = hero.heroPos;
if (!pos) {
prevPosRef.current = null;
return;
}
if (!["started", "has-key"].includes(gameStatus)) {
prevPosRef.current = pos;
return;
}
const prev = prevPosRef.current;
prevPosRef.current = pos;
if (!prev) return;
const delta = Math.abs(pos.row - prev.row) + Math.abs(pos.col - prev.col);
if (delta > 0) setPlayerSteps((s) => s + delta);
}, [gameStatus, hero.heroPos]);
useEffect(() => {
if (gameStatus === "started" && efficiency > 150) {
setGameStatus("game-over");
}
}, [efficiency, gameStatus, setGameStatus]);
useEffect(() => {
if (gameStatus !== "game-won") return;
const logicMs =
logicFirst != null && logicLast != null
? Math.max(1, logicLast - logicFirst)
: timeValue;
const optimalMs = optimalSteps * 200;
const ratio = Math.max(0.1, optimalMs / logicMs);
const score = Math.min(100000, Math.round(ratio * 100000));
const seconds = timeValue / 1000;
const mazeSize = rows * cols;
const timePenalty = seconds / 10;
const keystrokePenalty = keystrokes / 2;
const sizeMultiplier = Math.max(1.0, mazeSize / 500);
const baseScore = 1000 - timePenalty - keystrokePenalty;
const score = Math.min(1000, Math.max(0, Math.round(baseScore * sizeMultiplier)));
setFinalScore(score);
}, [gameStatus, logicFirst, logicLast, timeValue, optimalSteps]);
}, [gameStatus, timeValue, keystrokes, rows, cols]);
return {
timeValue,
@@ -128,8 +59,6 @@ export function useScore({ board, hero, gameStatusManager, keyManager }: UseScor
distToKey,
distToExit,
keystrokes,
optimalSteps,
efficiency,
finalScore,
};
}