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:
+107
-1
@@ -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...*
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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,
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export { useGameStatus } from './useGameStatus';
|
||||
export type { GameStatus, GameStatusManager } from './types';
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user