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> </head>
<body> <body>
<div id="app"> <div id="app">
<div id="phaser-container"></div>
<div id="ui-root"></div> <div id="ui-root"></div>
</div> </div>
<script type="module" src="/src/main.tsx"></script> <script type="module" src="/src/main.tsx"></script>

View File

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

View File

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

View File

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