- Add useMazeNavigation for pure maze geometry and path validation - Add useVimMotions for pure Vim-style input parsing - Add useAnimation for pure movement animation system - Refactor useCursor to use specialized hooks with cleaner interface - Refactor useHero to pure game logic without DOM manipulation - Refactor useHeroRender to pure visual presentation layer - Fix dual position system - hero position now authoritative - Replace global events with callback props and return values - Implement state-driven rendering instead of direct DOM queries - Update GameManager interface with new HeroManager and HeroRenderer - Maintain backward compatibility with legacy Cursor type - Update UNIFIED_API.md with new architecture and interfaces
7.9 KiB
7.9 KiB
Vim-Maze Architecture Refactoring Plan
Current Problems Identified
1. Dual Position System
useCursormaintains logical cursor positionuseHeromaintains 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:
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:
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:
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:
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:
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:
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
// 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
// In render component
<div className={`cell ${heroPosition === coord ? 'hero' : ''}`}>
3.3 Replace Direct Queries
Current: container.querySelector("#maze")
New: Ref-based access
const mazeRef = useRef<HTMLElement>(null);
// Use mazeRef.current instead of queries
Phase 4: Update Integration Points
4.1 Fix useGame Integration
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
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)fromuseHeroRender - 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.mddocumentation
5.3 Update Tests and Examples
- Fix any broken tests
- Update example usage
- Ensure platform hooks still work
Implementation Order
- Create new specialized hooks (Phase 1)
- Refactor useCursor (Phase 2.1)
- Refactor useHero (Phase 2.2)
- Refactor useHeroRender (Phase 2.3)
- Fix communication patterns (Phase 3)
- Update integration points (Phase 4)
- 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.tssrc/useCursor/useVimMotions.tssrc/useCursor/useAnimation.ts
Modified Files
src/types.tssrc/useCursor/useCursor.tssrc/useCursor/useHero.tssrc/useCursor/useHeroRender.tssrc/useGame.tsUNIFIED_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.