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