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:
@@ -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)
|
|
||||||
@@ -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)
|
|
||||||
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -14,7 +14,7 @@ export function useGame(options: GameOptions, platformHook?: unknown): GameManag
|
|||||||
|
|
||||||
const hero = cursor.hero;
|
const hero = cursor.hero;
|
||||||
const renderer = useHeroRender({ gameStatus: gameStatusManager.gameStatus, board, 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 = {
|
const gameManager: GameManager = {
|
||||||
containerRef,
|
containerRef,
|
||||||
|
|||||||
@@ -20,7 +20,5 @@ export type ScoreManager = {
|
|||||||
distToKey: number;
|
distToKey: number;
|
||||||
distToExit: number;
|
distToExit: number;
|
||||||
keystrokes: number;
|
keystrokes: number;
|
||||||
optimalSteps: number;
|
|
||||||
efficiency: number;
|
|
||||||
finalScore: number | null;
|
finalScore: number | null;
|
||||||
};
|
};
|
||||||
|
|||||||
+15
-86
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useTimer } from "./useTimer";
|
import { useTimer } from "./useTimer";
|
||||||
import { useScoreTime } from "./useScoreTime";
|
import { useScoreTime } from "./useScoreTime";
|
||||||
import { useScorePaths } from "./useScorePaths";
|
import { useScorePaths } from "./useScorePaths";
|
||||||
@@ -11,11 +11,13 @@ type UseScoreParams = {
|
|||||||
hero: UseHeroType;
|
hero: UseHeroType;
|
||||||
gameStatusManager: GameStatusManager;
|
gameStatusManager: GameStatusManager;
|
||||||
keyManager?: { keyLog: any[] };
|
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 timer = useTimer();
|
||||||
const { gameStatus, setGameStatus } = gameStatusManager;
|
const { gameStatus } = gameStatusManager;
|
||||||
const { timeValue, startTimer, stopTimer, resetTimer } = timer;
|
const { timeValue, startTimer, stopTimer, resetTimer } = timer;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -29,96 +31,25 @@ export function useScore({ board, hero, gameStatusManager, keyManager }: UseScor
|
|||||||
const distToExit = entranceToExit || heroToExit;
|
const distToExit = entranceToExit || heroToExit;
|
||||||
|
|
||||||
const [finalScore, setFinalScore] = useState<number | null>(null);
|
const [finalScore, setFinalScore] = useState<number | null>(null);
|
||||||
const optimalRef = useRef<number>(0);
|
|
||||||
const keystrokes = keyManager ? keyManager.keyLog.length : 0;
|
const keystrokes = keyManager ? keyManager.keyLog.length : 0;
|
||||||
|
|
||||||
useScoreTime({ gameStatus, timer });
|
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(() => {
|
useEffect(() => {
|
||||||
if (gameStatus !== "game-won") return;
|
if (gameStatus !== "game-won") return;
|
||||||
|
|
||||||
const logicMs =
|
const seconds = timeValue / 1000;
|
||||||
logicFirst != null && logicLast != null
|
const mazeSize = rows * cols;
|
||||||
? Math.max(1, logicLast - logicFirst)
|
|
||||||
: timeValue;
|
const timePenalty = seconds / 10;
|
||||||
|
const keystrokePenalty = keystrokes / 2;
|
||||||
const optimalMs = optimalSteps * 200;
|
const sizeMultiplier = Math.max(1.0, mazeSize / 500);
|
||||||
const ratio = Math.max(0.1, optimalMs / logicMs);
|
|
||||||
const score = Math.min(100000, Math.round(ratio * 100000));
|
const baseScore = 1000 - timePenalty - keystrokePenalty;
|
||||||
|
const score = Math.min(1000, Math.max(0, Math.round(baseScore * sizeMultiplier)));
|
||||||
|
|
||||||
setFinalScore(score);
|
setFinalScore(score);
|
||||||
}, [gameStatus, logicFirst, logicLast, timeValue, optimalSteps]);
|
}, [gameStatus, timeValue, keystrokes, rows, cols]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
timeValue,
|
timeValue,
|
||||||
@@ -128,8 +59,6 @@ export function useScore({ board, hero, gameStatusManager, keyManager }: UseScor
|
|||||||
distToKey,
|
distToKey,
|
||||||
distToExit,
|
distToExit,
|
||||||
keystrokes,
|
keystrokes,
|
||||||
optimalSteps,
|
|
||||||
efficiency,
|
|
||||||
finalScore,
|
finalScore,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user