refactor: use GameHost for simplification

This commit is contained in:
hypercross 2026-04-04 01:11:49 +08:00
parent 32509d7812
commit 4fa06098ba
2 changed files with 65 additions and 72 deletions

View File

@ -1,21 +1,22 @@
import { h, render } from 'preact'; import { h, render } from 'preact';
import { signal, computed } from '@preact/signals-core'; import { signal } from '@preact/signals-core';
import { useEffect, useState, useCallback } from 'preact/hooks'; import { useEffect, useState, useCallback } from 'preact/hooks';
import Phaser from 'phaser'; import Phaser from 'phaser';
import { createGameContext } from 'boardgame-core'; import { createGameHost } from 'boardgame-core';
import { GameUI, PromptDialog, CommandLog, createPromptHandler } from 'boardgame-phaser'; import { GameUI, PromptDialog, CommandLog } from 'boardgame-phaser';
import { createInitialState, registry, type TicTacToeState } from './game/tic-tac-toe'; import { createInitialState, registry, type TicTacToeState } from './game/tic-tac-toe';
import { GameScene } from './scenes/GameScene'; import { GameScene } from './scenes/GameScene';
import './style.css'; import './style.css';
const gameContext = createGameContext<TicTacToeState>(registry, createInitialState); // 创建 GameHost 实例,自动管理状态和 prompt
const gameHost = createGameHost(
{ registry, createInitialState },
'setup',
{ autoStart: false }
);
const commandLog = signal<Array<{ input: string; result: string; timestamp: number }>>([]); const commandLog = signal<Array<{ input: string; result: string; timestamp: number }>>([]);
// Single PromptHandler — the only consumer of promptQueue
let promptHandler: ReturnType<typeof createPromptHandler> | null = null;
const promptSignal = signal<import('boardgame-core').PromptEvent | null>(null);
// 记录命令日志的辅助函数 // 记录命令日志的辅助函数
function logCommand(input: string, result: { success: boolean; result?: unknown; error?: string }) { function logCommand(input: string, result: { success: boolean; result?: unknown; error?: string }) {
commandLog.value = [ commandLog.value = [
@ -33,6 +34,7 @@ function App() {
const [game, setGame] = useState<Phaser.Game | null>(null); const [game, setGame] = useState<Phaser.Game | null>(null);
const [scene, setScene] = useState<GameScene | null>(null); const [scene, setScene] = useState<GameScene | null>(null);
const [gameState, setGameState] = useState<TicTacToeState | null>(null); const [gameState, setGameState] = useState<TicTacToeState | null>(null);
const [promptSchema, setPromptSchema] = useState<any>(null);
useEffect(() => { useEffect(() => {
const phaserConfig: Phaser.Types.Core.GameConfig = { const phaserConfig: Phaser.Types.Core.GameConfig = {
@ -45,81 +47,70 @@ function App() {
}; };
const phaserGame = new Phaser.Game(phaserConfig); const phaserGame = new Phaser.Game(phaserConfig);
// 通过 init 传递 gameContext // 通过 init 传递 gameHost
const gameScene = new GameScene(); const gameScene = new GameScene();
phaserGame.scene.add('GameScene', gameScene, true, { gameContext }); phaserGame.scene.add('GameScene', gameScene, true, { gameHost });
setGame(phaserGame); setGame(phaserGame);
setScene(gameScene); setScene(gameScene);
setPhaserReady(true); setPhaserReady(true);
return () => { return () => {
gameHost.dispose();
phaserGame.destroy(true); phaserGame.destroy(true);
}; };
}, []); }, []);
useEffect(() => { useEffect(() => {
if (phaserReady && scene) { if (phaserReady && scene) {
// Initialize the single PromptHandler // 监听 prompt 状态变化
promptHandler = createPromptHandler({ const disposePromptSchema = gameHost.activePromptSchema.subscribe((schema) => {
commands: gameContext.commands, setPromptSchema(schema);
onPrompt: (prompt) => { scene.promptSchema.current = schema;
promptSignal.value = prompt;
// Also update the scene's prompt reference
scene.promptSignal.current = prompt;
},
onCancel: () => {
promptSignal.value = null;
scene.promptSignal.current = null;
},
});
promptHandler.start();
// Wire the scene's submit function to this PromptHandler
scene.setSubmitPrompt((cmd: string) => {
const error = promptHandler!.submit(cmd);
if (error === null) {
logCommand(cmd, { success: true });
promptSignal.value = null;
scene.promptSignal.current = null;
} else {
logCommand(cmd, { success: false, error });
}
return error;
}); });
// 监听状态变化 // 监听状态变化
const dispose = gameContext.state.subscribe(() => { const disposeState = gameHost.state.subscribe(() => {
setGameState({ ...gameContext.state.value }); setGameState(gameHost.state.value as TicTacToeState);
}); });
// 运行游戏设置 // 运行游戏设置
gameContext.commands.run('setup').then(result => { gameHost.setup('setup').then(() => {
logCommand('setup', result); logCommand('setup', { success: true });
}).catch(err => {
logCommand('setup', { success: false, error: err.message });
}); });
return () => { return () => {
dispose(); disposePromptSchema();
promptHandler?.destroy(); disposeState();
promptHandler = null;
}; };
} }
}, [phaserReady, scene]); }, [phaserReady, scene]);
const handlePromptSubmit = useCallback((input: string) => { const handlePromptSubmit = useCallback((input: string) => {
if (promptHandler) { const error = gameHost.onInput(input);
promptHandler.submit(input); if (error === null) {
logCommand(input, { success: true });
setPromptSchema(null);
if (scene) {
scene.promptSchema.current = null;
}
} else {
logCommand(input, { success: false, error });
} }
}, []); }, []);
const handlePromptCancel = useCallback(() => { const handlePromptCancel = useCallback(() => {
if (promptHandler) { gameHost.commands._cancel('User cancelled');
promptHandler.cancel('User cancelled'); setPromptSchema(null);
if (scene) {
scene.promptSchema.current = null;
} }
}, []); }, []);
const handleReset = useCallback(() => { const handleReset = useCallback(() => {
gameContext.commands.run('reset').then(result => { gameHost.commands.run('reset').then(result => {
logCommand('reset', result); logCommand('reset', result);
}); });
}, []); }, []);
@ -147,7 +138,7 @@ function App() {
)} )}
<PromptDialog <PromptDialog
prompt={promptSignal.value} prompt={promptSchema ? { schema: promptSchema, tryCommit: () => null, cancel: () => {} } : null}
onSubmit={handlePromptSubmit} onSubmit={handlePromptSubmit}
onCancel={handlePromptCancel} onCancel={handlePromptCancel}
/> />

View File

@ -1,5 +1,6 @@
import Phaser from 'phaser'; import Phaser from 'phaser';
import type { TicTacToeState, TicTacToePart, PlayerType } from '@/game/tic-tac-toe'; import type { TicTacToeState, TicTacToePart, PlayerType } from '@/game/tic-tac-toe';
import type { GameHost, CommandSchema, IGameContext, MutableSignal } from 'boardgame-core';
import { ReactiveScene, bindRegion, createInputMapper, InputMapper } from 'boardgame-phaser'; import { ReactiveScene, bindRegion, createInputMapper, InputMapper } from 'boardgame-phaser';
const CELL_SIZE = 120; const CELL_SIZE = 120;
@ -11,13 +12,31 @@ export class GameScene extends ReactiveScene<TicTacToeState> {
private gridGraphics!: Phaser.GameObjects.Graphics; private gridGraphics!: Phaser.GameObjects.Graphics;
private inputMapper!: InputMapper; private inputMapper!: InputMapper;
private turnText!: Phaser.GameObjects.Text; private turnText!: Phaser.GameObjects.Text;
/** Receives the active prompt from the single PromptHandler in main.tsx */ /** Receives the active prompt schema from main.tsx */
promptSignal: { current: any } = { current: null }; promptSchema: { current: CommandSchema | null } = { current: null };
/** GameHost instance passed from main.tsx */
private gameHost!: GameHost<TicTacToeState>;
constructor() { constructor() {
super('GameScene'); super('GameScene');
} }
init(data: { gameHost: GameHost<TicTacToeState> } | { gameContext: IGameContext<TicTacToeState> }): void {
if ('gameHost' in data) {
this.gameHost = data.gameHost;
// Create a compatible gameContext from GameHost
this.gameContext = {
state: this.gameHost.state as MutableSignal<TicTacToeState>,
commands: this.gameHost.commands,
} as IGameContext<TicTacToeState>;
this.state = this.gameContext.state;
this.commands = this.gameContext.commands;
} else {
// Fallback for direct gameContext passing
super.init(data);
}
}
protected onStateReady(_state: TicTacToeState): void { protected onStateReady(_state: TicTacToeState): void {
} }
@ -84,10 +103,8 @@ export class GameScene extends ReactiveScene<TicTacToeState> {
private setupInput(): void { private setupInput(): void {
this.inputMapper = createInputMapper(this, { this.inputMapper = createInputMapper(this, {
onSubmit: (cmd: string) => { onSubmit: (cmd: string) => {
// Delegate to the single PromptHandler via the shared commands reference. // Directly submit to GameHost
// The actual PromptHandler instance lives in main.tsx and is set up once. return this.gameHost.onInput(cmd);
// We call through a callback that main.tsx provides via the scene's public interface.
return this.submitToPrompt(cmd);
} }
}); });
@ -106,21 +123,6 @@ export class GameScene extends ReactiveScene<TicTacToeState> {
); );
} }
/**
* Called by main.tsx to wire up the single PromptHandler's submit function.
*/
private _submitToPrompt: ((cmd: string) => string | null) | null = null;
setSubmitPrompt(fn: (cmd: string) => string | null): void {
this._submitToPrompt = fn;
}
private submitToPrompt(cmd: string): string | null {
return this._submitToPrompt
? this._submitToPrompt(cmd)
: null; // no handler wired yet, accept silently
}
private drawGrid(): void { private drawGrid(): void {
const g = this.gridGraphics; const g = this.gridGraphics;
g.lineStyle(3, 0x6b7280); g.lineStyle(3, 0x6b7280);