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 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,
|
||||
|
||||
@@ -20,7 +20,5 @@ export type ScoreManager = {
|
||||
distToKey: number;
|
||||
distToExit: number;
|
||||
keystrokes: number;
|
||||
optimalSteps: number;
|
||||
efficiency: number;
|
||||
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 { 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,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user