refactor: integrate key management into cursor module

- Move useKeyBindings functionality into useCursor as useGameKeys
- Flatten GameManager interface using intersection types
- Rename clearLog/getLog to clearKeyLog/getKeyLog for clarity
- Remove separate useKeyBindings module and exports
- Update all documentation to reflect new architecture
- Maintain backward compatibility for platform hooks
This commit is contained in:
2025-10-19 12:56:35 +01:00
parent 81489557c3
commit 7c36f9dc14
9 changed files with 55 additions and 79 deletions
+25 -31
View File
@@ -43,14 +43,9 @@ type GameOptions = {
```tsx
type GameManager = {
containerRef: RefObject<HTMLDivElement | null>;
gameStatus: GameStatus;
renderBoard: () => void;
startGame: () => void;
togglePause: (pause?: boolean) => void;
quitGame: () => void;
cursor: Cursor;
keyManager: GameKeyManager;
};
} & GameStatusManager & GameKeyManager;
```
### `GameStatus`
@@ -96,8 +91,8 @@ type GameStatusManager = {
```tsx
type GameKeyManager = {
keyLog: KeyLogEntry[];
clearLog: () => void;
getLog: () => KeyLogEntry[];
clearKeyLog: () => void;
getKeyLog: () => KeyLogEntry[];
};
```
@@ -141,26 +136,29 @@ Manages the game board/level structure.
- `rendererRef` - Renderer instance
- `renderBoard()` - Initialize/render the board
### `useCursor(board: BoardManager) -> Cursor`
Manages player/actor movement and state.
### `useCursor(board: BoardManager, gameStatusManager: GameStatusManager) -> Cursor & { keyManager: GameKeyManager }`
Manages player/actor movement and state, now includes integrated key management.
**Returns:**
- Position tracking
- Mode management
- Movement methods (with count support)
- Directional movement helpers
- Integrated `keyManager` for key logging and input handling
### `useKeyBindings({ cursor, gameStatus }) -> GameKeyManager`
Handles keyboard input and maps keys to cursor actions.
### `useGameKeys({ cursor, gameStatusManager }) -> GameKeyManager`
Internal hook that handles keyboard input and maps keys to cursor actions.
**Parameters:**
- `cursor` - Cursor binding context with movement methods
- `gameStatus` - Current game status for state checking
- `gameStatusManager` - Game status manager for state checking
**Returns:**
- `keyLog` - Array of key press entries with timestamps
- `clearLog()` - Clear the key log
- `getLog()` - Get current key log
- `clearKeyLog()` - Clear the key log
- `getKeyLog()` - Get current key log
**Note:** This is now integrated within `useCursor` and not exposed as a separate hook.
### `useGameStatus() -> GameStatusManager`
Manages game state and lifecycle.
@@ -260,11 +258,12 @@ The unified API is designed to accommodate:
- ✅ **Type definitions** - Complete
- ✅ **Core architecture** - Defined
- ✅ **useGameStatus implementation** - Complete
- ✅ **useKeyBindings implementation** - Complete
- ✅ **useCursor implementation** - Complete
- ✅ **useGameKeys implementation** - Complete (integrated into useCursor)
- ✅ **useCursor implementation** - Complete (with integrated key management)
- ✅ **useBoard implementation** - Complete
- ✅ **useGame implementation** - Complete
- ✅ **useGame implementation** - Complete (with flattened GameManager interface)
- ✅ **React 19 optimization** - No useCallback/useMemo usage
- ✅ **API refactoring** - Complete (key management moved into cursor module)
- ⏳ **Platform integration** - Pending
- ⏳ **Advanced features** - Pending (scoring, animations, etc.)
@@ -306,13 +305,14 @@ function App() {
### Platform Hook Implementation
```tsx
export const useKeyBindings = (gameManager: GameManager) => {
const { gameStatus, startGame, quitGame, cursor } = gameManager;
const { gameStatus, startGame, quitGame, cursor, clearKeyLog } = gameManager;
useEffect(() => {
const handler = (e: KeyboardEvent) => {
// Start game with 'i'
if (e.key === "i" || e.key === "I") {
if (["waiting", "game-over", "game-won"].includes(gameStatus)) {
clearKeyLog();
startGame();
return;
}
@@ -336,24 +336,21 @@ export const useKeyBindings = (gameManager: GameManager) => {
window.addEventListener("keydown", handler);
return () => window.removeEventListener("keydown", handler);
}, [gameStatus, startGame, quitGame, cursor]);
}, [gameStatus, startGame, quitGame, cursor, clearKeyLog]);
};
```
### Internal Key Bindings Integration
```tsx
// Inside useGame - simplified integration
const keyManager = useKeyBindings({ cursor, gameStatus });
const { keyManager, ...cursor } = useCursor(board, gameStatusManager);
const gameManager = {
containerRef,
gameStatus,
renderBoard,
startGame,
togglePause,
quitGame,
cursor,
keyManager,
...keyManager,
...gameStatusManager,
};
```
@@ -428,13 +425,9 @@ Platform hooks receive the complete `GameManager` interface:
```tsx
type GameManager = {
containerRef: RefObject<HTMLDivElement | null>;
gameStatus: GameStatus;
renderBoard: () => void;
startGame: () => void;
togglePause: (pause?: boolean) => void;
quitGame: () => void;
cursor: Cursor;
};
} & GameStatusManager & GameKeyManager;
```
This allows platforms to:
@@ -443,3 +436,4 @@ This allows platforms to:
- Add save/load functionality
- Integrate with platform-specific UI
- Extend game behavior without modifying core logic
- Access key logging functionality via `clearKeyLog()` and `getKeyLog()`
+2 -3
View File
@@ -2,15 +2,14 @@ import { useEffect } from "react";
import type { GameManager } from "../../src";
export const useKeyBindings = (gameManager: GameManager) => {
const { gameStatus, renderBoard, startGame, quitGame, keyManager } = gameManager;
const { clearLog } = keyManager;
const { gameStatus, renderBoard, startGame, quitGame, clearKeyLog } = gameManager;
useEffect(() => {
const handler = (ev: KeyboardEvent) => {
// start game
if (ev.code === "Space") {
if (["waiting", "game-over", "game-won"].includes(gameStatus)) {
clearLog();
clearKeyLog();
renderBoard();
if (gameStatus === "game-over" || gameStatus === "game-won") {
quitGame();
-1
View File
@@ -1,4 +1,3 @@
export * from './types';
export * from './useGame';
export * from './useGameStatus';
export * from './useKeyBindings';
+3 -9
View File
@@ -8,15 +8,9 @@ export type GameStatus = 'waiting' | 'started' | 'hasKey' | 'paused' | 'game-ove
export type GameManager = {
containerRef: RefObject<HTMLDivElement | null>;
gameStatus: GameStatus;
renderBoard: () => void;
startGame: () => void;
togglePause: (pause?: boolean) => void;
quitGame: () => void;
cursor: Cursor;
keyManager: GameKeyManager;
};
} & GameStatusManager & GameKeyManager;
export type BoardManager = {
@@ -55,8 +49,8 @@ export type KeyLogEntry = { key: string; timestamp: number };
export type GameKeyManager = {
keyLog: KeyLogEntry[];
clearLog: () => void;
getLog: () => KeyLogEntry[];
clearKeyLog: () => void;
getKeyLog: () => KeyLogEntry[];
};
export type GameStatusManager = {
+11 -11
View File
@@ -1,17 +1,18 @@
import { useRef, useEffect } from 'react'
import type { CursorMode, Coord, BoardManager, GameStatus } from '../types'
import { useHero } from './useHero'
import type { CursorMode, Coord, BoardManager, GameStatusManager } from '../types'
import { useHero } from './useHero';
import { useGameKeys } from './useGameKeys';
type Motion = { dr: number; dc: number; steps: number };
export function useCursor(board: BoardManager, gameStatus: GameStatus) {
export function useCursor(board: BoardManager, gameStatusManager: GameStatusManager) {
const position = useRef<Coord>({ row: 0, col: 0 })
const mode = useRef<CursorMode>('normal')
const countRef = useRef<string>("");
const lastMotionRef = useRef<Motion | null>(null);
const lastKeyRef = useRef<string>("");
const hero = useHero(board, gameStatus);
const hero = useHero(board, gameStatusManager);
// Sync cursor position with hero position
useEffect(() => {
@@ -219,7 +220,7 @@ export function useCursor(board: BoardManager, gameStatus: GameStatus) {
return lastKeyRef.current;
}
const cursorAPI = {
const cursor = {
position: () => position.current,
mode: () => mode.current,
move,
@@ -237,12 +238,11 @@ export function useCursor(board: BoardManager, gameStatus: GameStatus) {
setCount,
setLastKey,
getLastKey,
}
return {
...cursorAPI,
hero,
};
}
const keyManager = useGameKeys({ cursor, gameStatusManager });
return { ...cursor, keyManager };
}
@@ -1,12 +1,10 @@
import type { GameStatus, KeyLogEntry, UseHeroType, Cursor } from "../types";
import type { GameStatusManager, KeyLogEntry, Cursor } from "../types";
import { useEffect, useRef, useState } from "react";
type UseKeyBindingsParams = {
type UseGameKeysParams = {
cursor: Cursor;
hero: UseHeroType;
gameStatus: GameStatus;
setGameStatus: (status: GameStatus) => void;
gameStatusManager: GameStatusManager;
};
const RELEVANT_KEYS = new Set([
@@ -14,12 +12,14 @@ const RELEVANT_KEYS = new Set([
"1", "2", "3", "4", "5", "6", "7", "8", "9"
]);
export const useKeyBindings = ({ cursor, gameStatus, setGameStatus }: UseKeyBindingsParams) => {
export const useGameKeys = (props: UseGameKeysParams) => {
const { cursor, gameStatusManager } = props;
const { gameStatus, setGameStatus } = gameStatusManager;
const [keyLog, setKeyLog] = useState<KeyLogEntry[]>([]);
const logRef = useRef<KeyLogEntry[]>([]);
const { hero } = cursor;
const clearLog = () => {
const clearKeyLog = () => {
logRef.current = [];
setKeyLog([]);
};
@@ -116,8 +116,8 @@ export const useKeyBindings = ({ cursor, gameStatus, setGameStatus }: UseKeyBind
return {
keyLog,
clearLog,
getLog: () => logRef.current,
clearKeyLog,
getKeyLog: () => logRef.current,
};
};
+3 -3
View File
@@ -1,12 +1,12 @@
import { useRef, useState, useEffect } from "react";
import type { BoardManager, GameStatus, Coord } from "../types";
import type { BoardManager, GameStatusManager, GameStatus, Coord } from "../types";
import { useHeroRender } from "./useHeroRender";
export function useHero(board: BoardManager, gameStatus: GameStatus) {
export function useHero(board: BoardManager, gameStatusManager: GameStatusManager) {
const { mazeRef, containerRef } = board;
const [heroPos, setHeroPos] = useState<Coord | null>(null);
const animatingRef = useRef(false);
const { gameStatus } = gameStatusManager;
function getCellEl(r: number, c: number): HTMLElement | null {
const container = containerRef.current;
+3 -12
View File
@@ -1,7 +1,6 @@
import type { GameManager, GameOptions } from './types';
import { useBoard } from './useBoard';
import { useCursor } from './useCursor';
import { useKeyBindings } from './useKeyBindings';
import { useGameStatus } from './useGameStatus';
export function useGame(options: GameOptions, platformHook?: unknown): GameManager {
@@ -9,22 +8,14 @@ export function useGame(options: GameOptions, platformHook?: unknown): GameManag
const board = useBoard(cols, rows);
const { containerRef, renderBoard } = board;
const gameStatusManager = useGameStatus();
const { gameStatus, startGame, togglePause, quitGame, setGameStatus } = gameStatusManager;
const cursor = useCursor(board, gameStatus);
const keyManager = useKeyBindings({ cursor, gameStatus, setGameStatus });
const { keyManager, ...cursor } = useCursor(board, gameStatusManager);
const gameManager = {
containerRef,
gameStatus,
...keyManager,
...gameStatusManager,
renderBoard,
startGame,
togglePause,
quitGame,
cursor,
keyManager,
}
if (typeof platformHook === 'function') {
-1
View File
@@ -1 +0,0 @@
export * from './useKeyBindings';