diff --git a/IMPLEMENTATION.md b/IMPLEMENTATION.md index ee13a58..5614d5d 100644 --- a/IMPLEMENTATION.md +++ b/IMPLEMENTATION.md @@ -314,6 +314,112 @@ pattern(r, c) = (3 * (r % 3) + floor(r / 3) + c) % 9 + 1 --- -## Iteration 5: [Pending] +## Iteration 5: Phase 30 - useGameStatus Hook + +**Date**: 2025-01-22 + +### Goals +- Extract game status logic into dedicated hook +- Follow vim-maze architecture pattern +- Clean separation of concerns +- Reusable state machine + +### Tasks Completed + +#### 1. Created useGameStatus Module +**File Structure**: +``` +src/useGameStatus/ +├── index.ts # Public exports +├── types.ts # GameStatus, GameStatusManager +└── useGameStatus.ts # Hook implementation +``` + +#### 2. Implemented State Machine +**States**: +- `waiting` - Initial state, awaiting start +- `started` - Game in progress +- `paused` - Game temporarily paused +- `game-over` - Game lost +- `game-won` - Game completed + +**State Transitions**: +``` +waiting ──startGame()──> started + ▲ │ + │ │ togglePause() + │ ▼ + quitGame() paused + │ + │ togglePause() + ▼ + started +``` + +#### 3. Hook API +```typescript +const gameStatusManager = useGameStatus(); +const { + gameStatus, // Current state + setGameStatus, // Direct setter (for special cases) + startGame, // waiting → started + togglePause, // started ↔ paused + quitGame, // * → waiting +} = gameStatusManager; +``` + +#### 4. Updated useGame +- Removed inline `useState` +- Uses `useGameStatus()` hook +- Spreads `gameStatusManager` into `GameManager` +- Cleaner, more maintainable code + +#### 5. Updated Type System +**types.ts**: +- Imports `GameStatus` and `GameStatusManager` from useGameStatus +- Re-exports for public API +- `GameManager` extends `GameStatusManager` (intersection type) +- Avoids duplication, single source of truth + +### Technical Details + +**Simpler than vim-maze**: +- No intermediate states (no 'has-key' equivalent) +- Straightforward: waiting → started → won/over +- Pause is optional toggle, not a separate flow + +**Hook Composition**: +```typescript +// In useGame +const gameStatusManager = useGameStatus(); +const { gameStatus } = gameStatusManager; + +// Compose into GameManager +const gameManager = { + ...gameStatusManager, // gameStatus, startGame, etc. + // ... other managers +}; +``` + +**Type Safety**: +- `GameManager` extends `GameStatusManager` +- All status methods available on gameManager +- Full TypeScript inference throughout + +### Build Status +- ✅ TypeScript compiles cleanly +- ✅ Example app builds successfully +- ✅ No breaking changes to public API + +### Next Steps +- Phase 40: useCursor Hook Composition + - useCellEditor (analog to useHero) + - useVimMotions + - useGameKeys + - useCursorRender + +--- + +## Iteration 6: [Pending] *To be filled as implementation progresses...* diff --git a/IMPLEMENTATION_PLAN.md b/IMPLEMENTATION_PLAN.md index 8aa1922..84bad9f 100644 --- a/IMPLEMENTATION_PLAN.md +++ b/IMPLEMENTATION_PLAN.md @@ -93,10 +93,12 @@ src/ ### Phase 30: useGameStatus Hook -- [ ] Implement GameStatus state machine +- [x] Implement GameStatus state machine - 'waiting' → 'started' → 'game-won' / 'game-over' - 'paused' state support -- [ ] Methods: startGame, togglePause, quitGame, setGameStatus +- [x] Methods: startGame, togglePause, quitGame, setGameStatus +- [x] Extract from inline useState in useGame +- [x] Create dedicated hook following vim-maze pattern ### Phase 40: useCursor Hook Composition diff --git a/src/index.ts b/src/index.ts index 24c7e58..24233b0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,2 +1,3 @@ export { useGame } from './useGame'; -export type { GameManager, GameOptions, GameStatus, Difficulty } from './types'; +export { useGameStatus } from './useGameStatus'; +export type { GameManager, GameOptions, GameStatus, GameStatusManager, Difficulty } from './types'; diff --git a/src/types.ts b/src/types.ts index c584659..590f560 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,8 +1,9 @@ import type { RefObject } from 'react'; +import type { GameStatus, GameStatusManager } from './useGameStatus/types'; export type Difficulty = 'easy' | 'medium' | 'hard'; -export type GameStatus = 'waiting' | 'started' | 'paused' | 'game-over' | 'game-won'; +export type { GameStatus, GameStatusManager }; export type GameOptions = { difficulty?: Difficulty; @@ -13,11 +14,6 @@ export type KeyLogEntry = { key: string; timestamp: number }; export type GameManager = { containerRef: RefObject; renderBoard: () => void; - gameStatus: GameStatus; - setGameStatus: (status: GameStatus) => void; - startGame: () => void; - togglePause: (pause?: boolean) => void; - quitGame: () => void; cursor: any; scoreManager: { timeValue: number; @@ -27,4 +23,4 @@ export type GameManager = { keyLog: KeyLogEntry[]; clearKeyLog: () => void; getKeyLog: () => KeyLogEntry[]; -}; +} & GameStatusManager; diff --git a/src/useGame.ts b/src/useGame.ts index 4fe9c94..4cf0f01 100644 --- a/src/useGame.ts +++ b/src/useGame.ts @@ -1,13 +1,16 @@ import { useState, useEffect } from 'react'; -import type { GameManager, GameOptions, GameStatus } from './types'; +import type { GameManager, GameOptions } from './types'; import { useBoard } from './useBoard/useBoard'; +import { useGameStatus } from './useGameStatus'; export function useGame(options: GameOptions = {}, platformHook?: unknown): GameManager { const { difficulty = 'easy' } = options; const board = useBoard(); const { containerRef, renderBoard, generateBoard } = board; - const [gameStatus, setGameStatus] = useState('waiting'); + const gameStatusManager = useGameStatus(); + const { gameStatus } = gameStatusManager; + const [keyLog, setKeyLog] = useState<{ key: string; timestamp: number }[]>([]); useEffect(() => { @@ -26,21 +29,7 @@ export function useGame(options: GameOptions = {}, platformHook?: unknown): Game const gameManager: GameManager = { containerRef, renderBoard, - gameStatus, - setGameStatus, - startGame: () => { - setGameStatus('started'); - }, - togglePause: (pause?: boolean) => { - if (typeof pause === 'boolean') { - setGameStatus(pause ? 'paused' : 'started'); - } else { - setGameStatus(prev => prev === 'paused' ? 'started' : 'paused'); - } - }, - quitGame: () => { - setGameStatus('waiting'); - }, + ...gameStatusManager, cursor: {}, scoreManager: { timeValue: 0, diff --git a/src/useGameStatus/index.ts b/src/useGameStatus/index.ts new file mode 100644 index 0000000..23649c0 --- /dev/null +++ b/src/useGameStatus/index.ts @@ -0,0 +1,2 @@ +export { useGameStatus } from './useGameStatus'; +export type { GameStatus, GameStatusManager } from './types'; diff --git a/src/useGameStatus/types.ts b/src/useGameStatus/types.ts new file mode 100644 index 0000000..194e9bc --- /dev/null +++ b/src/useGameStatus/types.ts @@ -0,0 +1,9 @@ +export type GameStatus = 'waiting' | 'started' | 'paused' | 'game-over' | 'game-won'; + +export type GameStatusManager = { + gameStatus: GameStatus; + setGameStatus: (status: GameStatus) => void; + startGame: () => void; + togglePause: (pause?: boolean) => void; + quitGame: () => void; +}; diff --git a/src/useGameStatus/useGameStatus.ts b/src/useGameStatus/useGameStatus.ts new file mode 100644 index 0000000..24b56d6 --- /dev/null +++ b/src/useGameStatus/useGameStatus.ts @@ -0,0 +1,30 @@ +import { useState } from 'react'; +import type { GameStatus } from './types'; + +export function useGameStatus() { + const [gameStatus, setGameStatus] = useState('waiting'); + + function startGame() { + setGameStatus('started'); + } + + function togglePause(pause?: boolean) { + if (typeof pause === 'boolean') { + setGameStatus(pause ? 'paused' : 'started'); + } else { + setGameStatus(prevStatus => prevStatus === 'paused' ? 'started' : 'paused'); + } + } + + function quitGame() { + setGameStatus('waiting'); + } + + return { + gameStatus, + setGameStatus, + startGame, + togglePause, + quitGame, + }; +}