feat: implement Phase 30 - useGameStatus hook

Extract game status logic into dedicated hook following vim-maze pattern:

New Module Structure:
- src/useGameStatus/types.ts: GameStatus and GameStatusManager types
- src/useGameStatus/useGameStatus.ts: Hook implementation
- src/useGameStatus/index.ts: Public exports

Hook Implementation:
- State machine with 5 states (waiting/started/paused/game-over/game-won)
- Methods: startGame, togglePause, quitGame, setGameStatus
- Clean state transitions with useState
- Simpler than vim-maze (no intermediate 'has-key' state)

Updated Core Files:
- src/useGame.ts: Use useGameStatus() instead of inline useState
  - Compose gameStatusManager into GameManager
  - Cleaner separation of concerns

- src/types.ts: Import and re-export GameStatus types
  - GameManager extends GameStatusManager (intersection type)
  - Single source of truth for status types

- src/index.ts: Export useGameStatus hook publicly

Benefits:
- Reusable state machine
- Easier to test in isolation
- Follows unified API architecture
- Matches vim-maze patterns

State Transitions:
waiting → startGame() → started ↔ togglePause() ↔ paused
started → (win/lose) → game-won/game-over
* → quitGame() → waiting

Phase 30 complete. Next: Phase 40 (useCursor composition).
This commit is contained in:
2025-10-22 18:45:56 +01:00
parent 6f4a1accc4
commit 4e14d54122
8 changed files with 163 additions and 28 deletions
+107 -1
View File
@@ -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<GameStatus>`
- 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...*
+4 -2
View File
@@ -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
+2 -1
View File
@@ -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';
+3 -7
View File
@@ -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<HTMLDivElement | null>;
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;
+6 -17
View File
@@ -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<GameStatus>('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,
+2
View File
@@ -0,0 +1,2 @@
export { useGameStatus } from './useGameStatus';
export type { GameStatus, GameStatusManager } from './types';
+9
View File
@@ -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;
};
+30
View File
@@ -0,0 +1,30 @@
import { useState } from 'react';
import type { GameStatus } from './types';
export function useGameStatus() {
const [gameStatus, setGameStatus] = useState<GameStatus>('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,
};
}