2026-04-03 15:18:47 +08:00
|
|
|
import { h, render } from 'preact';
|
2026-04-03 19:39:07 +08:00
|
|
|
import { signal, computed } from '@preact/signals-core';
|
|
|
|
|
import { useEffect, useState, useCallback } from 'preact/hooks';
|
2026-04-03 15:18:47 +08:00
|
|
|
import Phaser from 'phaser';
|
|
|
|
|
import { createGameContext } from 'boardgame-core';
|
2026-04-03 19:39:07 +08:00
|
|
|
import { GameUI, PromptDialog, CommandLog, createPromptHandler } from 'boardgame-phaser';
|
2026-04-03 15:18:47 +08:00
|
|
|
import { createInitialState, registry, type TicTacToeState } from './game/tic-tac-toe';
|
2026-04-03 19:13:12 +08:00
|
|
|
import { GameScene } from './scenes/GameScene';
|
2026-04-03 15:18:47 +08:00
|
|
|
import './style.css';
|
|
|
|
|
|
|
|
|
|
const gameContext = createGameContext<TicTacToeState>(registry, createInitialState);
|
|
|
|
|
|
|
|
|
|
const commandLog = signal<Array<{ input: string; result: string; timestamp: number }>>([]);
|
|
|
|
|
|
2026-04-03 19:39:07 +08:00
|
|
|
// 创建 PromptHandler 用于处理 UI 层的 prompt
|
|
|
|
|
let promptHandler: ReturnType<typeof createPromptHandler<TicTacToeState>> | null = null;
|
|
|
|
|
const promptSignal = signal<null | Awaited<ReturnType<typeof gameContext.commands.promptQueue.pop>>>(null);
|
2026-04-03 15:18:47 +08:00
|
|
|
|
2026-04-03 19:39:07 +08:00
|
|
|
// 记录命令日志的辅助函数
|
|
|
|
|
function logCommand(input: string, result: { success: boolean; result?: unknown; error?: string }) {
|
2026-04-03 15:18:47 +08:00
|
|
|
commandLog.value = [
|
|
|
|
|
...commandLog.value,
|
|
|
|
|
{
|
|
|
|
|
input,
|
|
|
|
|
result: result.success ? `OK: ${JSON.stringify(result.result)}` : `ERR: ${result.error}`,
|
|
|
|
|
timestamp: Date.now(),
|
|
|
|
|
},
|
|
|
|
|
];
|
2026-04-03 19:39:07 +08:00
|
|
|
}
|
2026-04-03 15:18:47 +08:00
|
|
|
|
2026-04-03 16:18:44 +08:00
|
|
|
function App() {
|
|
|
|
|
const [phaserReady, setPhaserReady] = useState(false);
|
|
|
|
|
const [game, setGame] = useState<Phaser.Game | null>(null);
|
2026-04-03 19:39:07 +08:00
|
|
|
const [scene, setScene] = useState<GameScene | null>(null);
|
|
|
|
|
const [gameState, setGameState] = useState<TicTacToeState | null>(null);
|
2026-04-03 16:18:44 +08:00
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const phaserConfig: Phaser.Types.Core.GameConfig = {
|
|
|
|
|
type: Phaser.AUTO,
|
|
|
|
|
width: 560,
|
|
|
|
|
height: 560,
|
|
|
|
|
parent: 'phaser-container',
|
|
|
|
|
backgroundColor: '#f9fafb',
|
|
|
|
|
scene: [],
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const phaserGame = new Phaser.Game(phaserConfig);
|
2026-04-03 19:13:12 +08:00
|
|
|
// 通过 init 传递 gameContext
|
2026-04-03 19:39:07 +08:00
|
|
|
const gameScene = new GameScene();
|
|
|
|
|
phaserGame.scene.add('GameScene', gameScene, true, { gameContext });
|
2026-04-03 15:18:47 +08:00
|
|
|
|
2026-04-03 16:18:44 +08:00
|
|
|
setGame(phaserGame);
|
2026-04-03 19:39:07 +08:00
|
|
|
setScene(gameScene);
|
2026-04-03 16:18:44 +08:00
|
|
|
setPhaserReady(true);
|
2026-04-03 15:18:47 +08:00
|
|
|
|
2026-04-03 16:18:44 +08:00
|
|
|
return () => {
|
|
|
|
|
phaserGame.destroy(true);
|
|
|
|
|
};
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
2026-04-03 19:39:07 +08:00
|
|
|
if (phaserReady && scene) {
|
|
|
|
|
// 初始化 PromptHandler
|
|
|
|
|
promptHandler = createPromptHandler(scene, gameContext.commands, {
|
|
|
|
|
onPrompt: (prompt) => {
|
|
|
|
|
promptSignal.value = prompt;
|
|
|
|
|
},
|
|
|
|
|
onCancel: () => {
|
|
|
|
|
promptSignal.value = null;
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
promptHandler.start();
|
|
|
|
|
|
|
|
|
|
// 监听状态变化
|
|
|
|
|
const dispose = gameContext.state.subscribe(() => {
|
|
|
|
|
setGameState({ ...gameContext.state.value });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 运行游戏设置
|
|
|
|
|
gameContext.commands.run('setup').then(result => {
|
|
|
|
|
logCommand('setup', result);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
dispose();
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}, [phaserReady, scene]);
|
|
|
|
|
|
|
|
|
|
const handlePromptSubmit = useCallback((input: string) => {
|
|
|
|
|
if (promptHandler) {
|
|
|
|
|
const error = promptHandler.submit(input);
|
|
|
|
|
if (error === null) {
|
|
|
|
|
logCommand(input, { success: true });
|
|
|
|
|
promptSignal.value = null;
|
|
|
|
|
} else {
|
|
|
|
|
logCommand(input, { success: false, error });
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
const handlePromptCancel = useCallback(() => {
|
|
|
|
|
if (promptHandler) {
|
|
|
|
|
promptHandler.cancel('User cancelled');
|
|
|
|
|
promptSignal.value = null;
|
2026-04-03 16:18:44 +08:00
|
|
|
}
|
2026-04-03 19:39:07 +08:00
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
const handleReset = useCallback(() => {
|
|
|
|
|
gameContext.commands.run('reset').then(result => {
|
|
|
|
|
logCommand('reset', result);
|
|
|
|
|
});
|
|
|
|
|
}, []);
|
2026-04-03 16:18:44 +08:00
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex flex-col h-screen">
|
|
|
|
|
<div className="flex-1 relative">
|
|
|
|
|
<div id="phaser-container" className="w-full h-full" />
|
2026-04-03 19:39:07 +08:00
|
|
|
|
|
|
|
|
{/* 游戏状态显示 */}
|
|
|
|
|
{gameState && !gameState.winner && (
|
|
|
|
|
<div className="absolute top-4 left-1/2 transform -translate-x-1/2 bg-white px-4 py-2 rounded-lg shadow-lg z-10">
|
|
|
|
|
<span className="text-lg font-semibold text-gray-800">
|
|
|
|
|
{gameState.currentPlayer}'s Turn
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{gameState?.winner && (
|
|
|
|
|
<div className="absolute top-4 left-1/2 transform -translate-x-1/2 bg-white px-4 py-2 rounded-lg shadow-lg z-10">
|
|
|
|
|
<span className="text-lg font-semibold text-yellow-600">
|
|
|
|
|
{gameState.winner === 'draw' ? "It's a Draw!" : `${gameState.winner} Wins!`}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
2026-04-03 16:18:44 +08:00
|
|
|
<PromptDialog
|
|
|
|
|
prompt={promptSignal.value}
|
2026-04-03 19:39:07 +08:00
|
|
|
onSubmit={handlePromptSubmit}
|
|
|
|
|
onCancel={handlePromptCancel}
|
2026-04-03 16:18:44 +08:00
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="p-4 bg-gray-100 border-t">
|
2026-04-03 19:39:07 +08:00
|
|
|
<div className="flex justify-between items-center mb-2">
|
|
|
|
|
<span className="text-sm font-medium text-gray-700">Command Log</span>
|
|
|
|
|
<button
|
|
|
|
|
className="px-3 py-1 text-xs font-medium text-white bg-blue-600 rounded-md hover:bg-blue-700"
|
|
|
|
|
onClick={handleReset}
|
|
|
|
|
>
|
|
|
|
|
Reset Game
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
2026-04-03 16:18:44 +08:00
|
|
|
<CommandLog entries={commandLog} />
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-04-03 15:18:47 +08:00
|
|
|
|
|
|
|
|
const ui = new GameUI({
|
|
|
|
|
container: document.getElementById('ui-root')!,
|
2026-04-03 16:18:44 +08:00
|
|
|
root: <App />,
|
2026-04-03 15:18:47 +08:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
ui.mount();
|