fix: fix entrance rig and details

This commit is contained in:
hypercross 2026-04-03 16:18:44 +08:00
parent 01407b5ede
commit df2b839e07
4 changed files with 72 additions and 49 deletions

View File

@ -7,7 +7,6 @@
</head>
<body>
<div id="app">
<div id="phaser-container"></div>
<div id="ui-root"></div>
</div>
<script type="module" src="/src/main.tsx"></script>

View File

@ -3,6 +3,7 @@ import {
type Part,
createRegion,
type MutableSignal,
isCellOccupied as isCellOccupiedUtil,
} from 'boardgame-core';
const BOARD_SIZE = 3;
@ -21,7 +22,7 @@ const WINNING_LINES: number[][][] = [
export type PlayerType = 'X' | 'O';
export type WinnerType = PlayerType | 'draw' | null;
export type TicTacToePart = Part & { player: PlayerType };
export type TicTacToePart = Part<{ player: PlayerType }>;
export function createInitialState() {
return {
@ -29,7 +30,7 @@ export function createInitialState() {
{ name: 'x', min: 0, max: BOARD_SIZE - 1 },
{ name: 'y', min: 0, max: BOARD_SIZE - 1 },
]),
parts: {} as Record<string, TicTacToePart>,
parts: [] as TicTacToePart[],
currentPlayer: 'X' as PlayerType,
winner: null as WinnerType,
turn: 0,
@ -93,8 +94,7 @@ registration.add('turn <player> <turn:number>', async function (cmd) {
});
export function isCellOccupied(host: MutableSignal<TicTacToeState>, row: number, col: number): boolean {
const board = host.value.board;
return board.partMap[`${row},${col}`] !== undefined;
return isCellOccupiedUtil(host.value.parts, 'board', [row, col]);
}
export function hasWinningLine(positions: number[][]): boolean {
@ -106,7 +106,7 @@ export function hasWinningLine(positions: number[][]): boolean {
}
export function checkWinner(host: MutableSignal<TicTacToeState>): WinnerType {
const parts = Object.values(host.value.parts);
const parts = host.value.parts;
const xPositions = parts.filter((p: TicTacToePart) => p.player === 'X').map((p: TicTacToePart) => p.position);
const oPositions = parts.filter((p: TicTacToePart) => p.player === 'O').map((p: TicTacToePart) => p.position);
@ -119,8 +119,7 @@ export function checkWinner(host: MutableSignal<TicTacToeState>): WinnerType {
}
export function placePiece(host: MutableSignal<TicTacToeState>, row: number, col: number, player: PlayerType) {
const board = host.value.board;
const moveNumber = Object.keys(host.value.parts).length + 1;
const moveNumber = host.value.parts.length + 1;
const piece: TicTacToePart = {
id: `piece-${player}-${moveNumber}`,
regionId: 'board',
@ -128,8 +127,8 @@ export function placePiece(host: MutableSignal<TicTacToeState>, row: number, col
player,
};
host.produce(state => {
state.parts[piece.id] = piece;
board.childIds.push(piece.id);
board.partMap[`${row},${col}`] = piece.id;
state.parts.push(piece);
state.board.childIds.push(piece.id);
state.board.partMap[`${row},${col}`] = piece.id;
});
}

View File

@ -1,5 +1,6 @@
import { h, render } from 'preact';
import { signal } from '@preact/signals-core';
import { useEffect, useState } from 'preact/hooks';
import Phaser from 'phaser';
import { createGameContext } from 'boardgame-core';
import { GameUI, PromptDialog, CommandLog } from 'boardgame-phaser';
@ -12,10 +13,12 @@ const gameContext = createGameContext<TicTacToeState>(registry, createInitialSta
const promptSignal = signal<null | Awaited<ReturnType<typeof gameContext.commands.promptQueue.pop>>>(null);
const commandLog = signal<Array<{ input: string; result: string; timestamp: number }>>([]);
// 监听 prompt 事件
gameContext.commands.on('prompt', (event) => {
promptSignal.value = event;
});
// 包装 run 方法以记录命令日志
const originalRun = gameContext.commands.run.bind(gameContext.commands);
(gameContext.commands as any).run = async (input: string) => {
const result = await originalRun(input);
@ -35,42 +38,63 @@ const sceneData: GameSceneData = {
commands: gameContext.commands,
};
const phaserConfig: Phaser.Types.Core.GameConfig = {
type: Phaser.AUTO,
width: 560,
height: 560,
parent: 'phaser-container',
backgroundColor: '#f9fafb',
scene: [],
};
function App() {
const [phaserReady, setPhaserReady] = useState(false);
const [game, setGame] = useState<Phaser.Game | null>(null);
const game = new Phaser.Game(phaserConfig);
useEffect(() => {
const phaserConfig: Phaser.Types.Core.GameConfig = {
type: Phaser.AUTO,
width: 560,
height: 560,
parent: 'phaser-container',
backgroundColor: '#f9fafb',
scene: [],
};
game.scene.add('GameScene', GameScene, true, sceneData);
const phaserGame = new Phaser.Game(phaserConfig);
phaserGame.scene.add('GameScene', GameScene, true, sceneData);
setGame(phaserGame);
setPhaserReady(true);
return () => {
phaserGame.destroy(true);
};
}, []);
useEffect(() => {
if (phaserReady) {
gameContext.commands.run('setup');
}
}, [phaserReady]);
return (
<div className="flex flex-col h-screen">
<div className="flex-1 relative">
<div id="phaser-container" className="w-full h-full" />
<PromptDialog
prompt={promptSignal.value}
onSubmit={(input: string) => {
gameContext.commands._tryCommit(input);
promptSignal.value = null;
}}
onCancel={() => {
gameContext.commands._cancel('cancelled');
promptSignal.value = null;
}}
/>
</div>
<div className="p-4 bg-gray-100 border-t">
<CommandLog entries={commandLog} />
</div>
</div>
);
}
const ui = new GameUI({
container: document.getElementById('ui-root')!,
root: h('div', { className: 'flex flex-col h-screen' },
h('div', { className: 'flex-1 relative' },
h('div', { id: 'phaser-container', className: 'w-full h-full' }),
h(PromptDialog, {
prompt: promptSignal.value,
onSubmit: (input: string) => {
gameContext.commands._tryCommit(input);
promptSignal.value = null;
},
onCancel: () => {
gameContext.commands._cancel('cancelled');
promptSignal.value = null;
},
}),
),
h('div', { className: 'p-4 bg-gray-100 border-t' },
h(CommandLog, { entries: commandLog }),
),
),
root: <App />,
});
ui.mount();
gameContext.commands.run('setup');

View File

@ -1,5 +1,6 @@
import Phaser from 'phaser';
import type { TicTacToeState, TicTacToePart } from '@/game/tic-tac-toe';
import type { TicTacToeState, TicTacToePart, PlayerType } from '@/game/tic-tac-toe';
import { isCellOccupied } from 'boardgame-core';
import { ReactiveScene, bindRegion, createInputMapper, createPromptHandler } from 'boardgame-phaser';
import type { PromptEvent, MutableSignal, IGameContext } from 'boardgame-core';
@ -33,6 +34,7 @@ export class GameScene extends ReactiveScene<TicTacToeState> {
}
create(): void {
super.create();
this.boardContainer = this.add.container(0, 0);
this.gridGraphics = this.add.graphics();
this.drawGrid();
@ -49,18 +51,18 @@ export class GameScene extends ReactiveScene<TicTacToeState> {
this.updateTurnText(currentPlayer);
});
this.setupBindings();
this.setupInput();
}
protected setupBindings(): void {
bindRegion<TicTacToePart>(
bindRegion<TicTacToeState, { player: PlayerType }>(
this.state,
(state) => state.parts,
this.state.value.board,
this.state.value.parts,
{
cellSize: { x: CELL_SIZE, y: CELL_SIZE },
offset: BOARD_OFFSET,
factory: (part: TicTacToePart, pos: Phaser.Math.Vector2) => {
factory: (part, pos: Phaser.Math.Vector2) => {
const text = this.add.text(pos.x + CELL_SIZE / 2, pos.y + CELL_SIZE / 2, part.player, {
fontSize: '64px',
fontFamily: 'Arial',
@ -85,8 +87,7 @@ export class GameScene extends ReactiveScene<TicTacToeState> {
if (this.state.value.winner) return null;
const currentPlayer = this.state.value.currentPlayer;
const board = this.state.value.board;
if (board.partMap[`${row},${col}`]) return null;
if (isCellOccupied(this.state.value.parts, 'board', [row, col])) return null;
return `play ${currentPlayer} ${row} ${col}`;
},