fix: fix entrance rig and details
This commit is contained in:
parent
01407b5ede
commit
df2b839e07
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,6 +38,11 @@ const sceneData: GameSceneData = {
|
||||||
commands: gameContext.commands,
|
commands: gameContext.commands,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
const [phaserReady, setPhaserReady] = useState(false);
|
||||||
|
const [game, setGame] = useState<Phaser.Game | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
const phaserConfig: Phaser.Types.Core.GameConfig = {
|
const phaserConfig: Phaser.Types.Core.GameConfig = {
|
||||||
type: Phaser.AUTO,
|
type: Phaser.AUTO,
|
||||||
width: 560,
|
width: 560,
|
||||||
|
|
@ -44,33 +52,49 @@ const phaserConfig: Phaser.Types.Core.GameConfig = {
|
||||||
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');
|
|
||||||
|
|
|
||||||
|
|
@ -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}`;
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue