Files
vim-maze/REFACTORING_PLAN.md
T
pastilhas 70a8167fb0 refactor: implement clean architecture with separation of concerns
- 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
2025-10-19 13:27:35 +01:00

7.9 KiB

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:

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) 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.