Implement platform-controlled GameOptions for complete game behavior customization

- Add GameOptions with gameMode, downwardSpeed, boardDimensions, and wordList
- Platform now controls ALL game behavior through options
- changeGameMode now accepts full GameOptions instead of just mode
- downwardSpeed as single px/s value applies to any mode
- wordList is mode-agnostic, platform provides what they need
- All options are fully optional with sensible defaults
- Update example app to use new options API
This commit is contained in:
2025-11-04 19:56:25 +00:00
parent 2944d43c4a
commit 157e2a71f4
6 changed files with 913 additions and 17 deletions
+5 -2
View File
@@ -61,7 +61,10 @@ export type FallingLetter = {
export type KeyLogEntry = { key: string; timestamp: number };
export type GameOptions = {
initialGameMode?: GameMode;
gameMode?: GameMode; // Game mode (defaults to 'letters')
downwardSpeed?: number; // Falling text speed in px/s (defaults to mode's built-in speed)
boardDimensions?: [number, number]; // Canvas dimensions [width, height] (defaults to [800, 600])
wordList?: string[]; // Custom word/sentence/paragraph list (optional, mode-agnostic)
};
// ============================================================================
@@ -171,7 +174,7 @@ export type GameManager = {
// Game-specific
handleTypedLetter: (letter: string) => void;
changeGameMode: (mode: GameMode) => void;
changeGameMode: (options: GameOptions) => void;
};
// ============================================================================
+6 -5
View File
@@ -6,16 +6,17 @@ import { useGameStatus } from "../useGameStatus";
import { useScore } from "../useScore";
export function useGame(
options?: GameOptions,
initialOptions?: GameOptions,
platformHook?: unknown
): GameManager {
const [keyLog, setKeyLog] = useState<KeyLogEntry[]>([]);
const [gameMode, setGameModeState] = useState<GameMode>(options?.initialGameMode ?? 'letters');
const [options, setOptions] = useState<GameOptions>(initialOptions ?? {});
const gameMode = options.gameMode ?? 'letters';
const gameModeRef = useRef<GameMode>(gameMode);
const boardManager = useBoard();
const cursorManager = useCursor();
const gameStatusManager = useGameStatus(undefined, gameMode);
const gameStatusManager = useGameStatus(undefined, options);
// Wrap renderBoard to include player position and lasers
const renderBoardWrapped = (letters: any = [], lasers: Laser[] = [], shipState: ShipState = 'normal', shipExplosionTime: number = 0) => {
@@ -68,8 +69,8 @@ export function useGame(
gameStatusManager.setGameMode(gameMode);
}, [gameMode]);
const changeGameMode = (mode: GameMode) => {
setGameModeState(mode);
const changeGameMode = (newOptions: GameOptions) => {
setOptions(newOptions);
// Don't quit - just switch modes and let game continue
};
+22 -5
View File
@@ -1,5 +1,5 @@
import { useState, useRef } from "react";
import type { GameStatus, FallingLetter, ShipState, GameMode, Laser } from "../types";
import { useState, useRef, useEffect } from "react";
import type { GameStatus, FallingLetter, ShipState, GameMode, Laser, GameOptions } from "../types";
import { getRandomWord } from "../wordLists";
const LETTER_SPEEDS: Record<GameMode, number> = {
@@ -15,7 +15,24 @@ const MARGIN_RIGHT = 50; // Safe margin from right edge
const CANVAS_WIDTH = 800;
const SPACESHIP_Y = 600 - 40; // Position of the ship on canvas
export function useGameStatus(_onGameLoopTick?: (deltaTime: number, letters: FallingLetter[], lasers: Laser[]) => void, gameMode: GameMode = 'letters') {
export function useGameStatus(_onGameLoopTick?: (deltaTime: number, letters: FallingLetter[], lasers: Laser[]) => void, options: GameOptions = {}) {
const gameMode = options.gameMode ?? 'letters';
const downwardSpeed = options.downwardSpeed;
const wordList = options.wordList;
// Store options in refs so they can be accessed in the game loop
const downwardSpeedRef = useRef<number | undefined>(downwardSpeed);
const wordListRef = useRef<string[] | undefined>(wordList);
// Update refs when options change
useEffect(() => {
downwardSpeedRef.current = downwardSpeed;
}, [downwardSpeed]);
useEffect(() => {
wordListRef.current = wordList;
}, [wordList]);
const [gameStatus, setGameStatus] = useState<GameStatus>("waiting");
const shipStateRef = useRef<ShipState>("normal");
const shipExplosionTimeRef = useRef<number>(0);
@@ -39,7 +56,7 @@ export function useGameStatus(_onGameLoopTick?: (deltaTime: number, letters: Fal
// Spawn new letter/word if enough time has passed and no letters active
if (currentTime - lastSpawnRef.current > SPAWN_INTERVAL && lettersRef.current.length < MAX_ACTIVE_LETTERS) {
const fullText = getRandomWord(gameModeRef.current);
const fullText = getRandomWord(gameModeRef.current, wordListRef.current);
const firstChar = fullText[0].toUpperCase();
// For sentences/paragraphs, spawn centered. For letters/words, spawn randomly
@@ -62,7 +79,7 @@ export function useGameStatus(_onGameLoopTick?: (deltaTime: number, letters: Fal
}
// Update letter positions
const speed = LETTER_SPEEDS[gameModeRef.current];
const speed = downwardSpeedRef.current ?? LETTER_SPEEDS[gameModeRef.current];
lettersRef.current.forEach(letter => {
letter.y += (speed * deltaTime) / 1000;
});
+4 -2
View File
@@ -41,7 +41,9 @@ export function getRandomItem<T>(array: readonly T[]): T {
return array[Math.floor(Math.random() * array.length)];
}
export function getRandomWord(gameMode: 'letters' | 'words' | 'sentences' | 'paragraphs'): string {
const list = wordLists[gameMode];
export function getRandomWord(gameMode: 'letters' | 'words' | 'sentences' | 'paragraphs', customList?: string[]): string {
// Use custom list if provided, otherwise use default for the game mode
// Letters mode always uses default alphabet
const list = customList && gameMode !== 'letters' ? customList : wordLists[gameMode];
return String(getRandomItem(list));
}