refactor: update api

This commit is contained in:
2026-04-06 11:31:51 +08:00
parent 0e7aaea7da
commit fa104d10f2
12 changed files with 165 additions and 229 deletions
+21 -25
View File
@@ -1,6 +1,6 @@
import {
createGameCommandRegistry, Part, createRegion, createPart, isCellOccupied as isCellOccupiedUtil,
IGameContext, Command
createGameCommandRegistry, Part, createRegion,
IGameContext, createRegionAxis, GameModule
} from 'boardgame-core';
const BOARD_SIZE = 3;
@@ -18,14 +18,15 @@ const WINNING_LINES: number[][][] = [
export type PlayerType = 'X' | 'O';
export type WinnerType = PlayerType | 'draw' | null;
export type TicTacToePart = Part<{ player: PlayerType }>;
export type TicTacToeState = ReturnType<typeof createInitialState>;
export type TicTacToeGame = IGameContext<TicTacToeState>;
export function createInitialState() {
return {
board: createRegion('board', [
{ name: 'x', min: 0, max: BOARD_SIZE - 1 },
{ name: 'y', min: 0, max: BOARD_SIZE - 1 },
createRegionAxis('x', 0, BOARD_SIZE - 1),
createRegionAxis('y', 0, BOARD_SIZE - 1),
]),
parts: {} as Record<string, TicTacToePart>,
currentPlayer: 'X' as PlayerType,
@@ -33,19 +34,16 @@ export function createInitialState() {
turn: 0,
};
}
export type TicTacToeState = ReturnType<typeof createInitialState>;
export type TicTacToeGame = IGameContext<TicTacToeState>;
export const registry = createGameCommandRegistry<TicTacToeState>();
async function setup(game: TicTacToeGame) {
export async function start(game: TicTacToeGame) {
while (true) {
const currentPlayer = game.value.currentPlayer;
const turnNumber = game.value.turn + 1;
const turnOutput = await turnCommand(game, currentPlayer, turnNumber);
if (!turnOutput.success) throw new Error(turnOutput.error);
const turnOutput = await turn(game, currentPlayer, turnNumber);
game.produce((state: TicTacToeState) => {
state.winner = turnOutput.result.winner;
state.winner = turnOutput.winner;
if (!state.winner) {
state.currentPlayer = state.currentPlayer === 'X' ? 'O' : 'X';
state.turn = turnNumber;
@@ -56,14 +54,11 @@ async function setup(game: TicTacToeGame) {
return game.value;
}
registry.register('setup', setup);
async function turn(game: TicTacToeGame, turnPlayer: PlayerType, turnNumber: number) {
async function handleTurn(game: TicTacToeGame, turnPlayer: PlayerType, turnNumber: number) {
const {player, row, col} = await game.prompt(
'play <player> <row:number> <col:number>',
(command: Command) => {
const [player, row, col] = command.params as [PlayerType, number, number];
(player: PlayerType, row: number, col: number) => {
if (player !== turnPlayer) {
throw `Invalid player: ${player}. Expected ${turnPlayer}.`;
} else if (!isValidMove(row, col)) {
@@ -85,14 +80,14 @@ async function turn(game: TicTacToeGame, turnPlayer: PlayerType, turnNumber: num
return { winner: null };
}
const turnCommand = registry.register('turn <player:string> <turnNumber:int>', turn);
const turn = registry.register('turn <player:string> <turnNumber:int>', handleTurn);
function isValidMove(row: number, col: number): boolean {
return !isNaN(row) && !isNaN(col) && row >= 0 && row < BOARD_SIZE && col >= 0 && col < BOARD_SIZE;
}
export function isCellOccupied(host: TicTacToeGame, row: number, col: number): boolean {
return isCellOccupiedUtil(host.value.parts, 'board', [row, col]);
return host.value.board.partMap[`${row},${col}`] !== undefined;
}
export function hasWinningLine(positions: number[][]): boolean {
@@ -120,10 +115,10 @@ export function checkWinner(host: TicTacToeGame): WinnerType {
export function placePiece(host: TicTacToeGame, row: number, col: number, player: PlayerType) {
const board = host.value.board;
const moveNumber = Object.keys(host.value.parts).length + 1;
const piece = createPart<{ player: PlayerType }>(
{ regionId: 'board', position: [row, col], player },
`piece-${player}-${moveNumber}`
);
const piece = {
regionId: 'board', position: [row, col], player,
id: `piece-${player}-${moveNumber}`
};
host.produce((state: TicTacToeState) => {
state.parts[piece.id] = piece;
board.childIds.push(piece.id);
@@ -131,13 +126,14 @@ export function placePiece(host: TicTacToeGame, row: number, col: number, player
});
}
export const gameModule = {
export const gameModule: GameModule<TicTacToeState> = {
registry,
createInitialState,
start
};
export const commands = {
export const prompts = {
play: (player: PlayerType, row: number, col: number) => {
return `play ${player} ${row} ${col}`;
},
};
};
+3 -3
View File
@@ -2,7 +2,7 @@ import Phaser from 'phaser';
import type {TicTacToeState, TicTacToePart} from '@/game/tic-tac-toe';
import { GameHostScene } from 'boardgame-phaser';
import { spawnEffect, type Spawner } from 'boardgame-phaser';
import {commands} from "@/game/tic-tac-toe";
import {prompts} from "@/game/tic-tac-toe";
const CELL_SIZE = 120;
const BOARD_OFFSET = { x: 100, y: 100 };
@@ -61,7 +61,7 @@ export class GameScene extends GameHostScene<TicTacToeState> {
if (this.state.winner) return;
if (this.isCellOccupied(row, col)) return;
const cmd = commands.play(this.state.currentPlayer, row, col);
const cmd = prompts.play(this.state.currentPlayer, row, col);
const error = this.gameHost.onInput(cmd);
if (error) {
console.warn('Invalid move:', error);
@@ -133,7 +133,7 @@ export class GameScene extends GameHostScene<TicTacToeState> {
).setInteractive({ useHandCursor: true });
bg.on('pointerdown', () => {
this.gameHost.setup('setup');
this.gameHost.start();
});
this.winnerOverlay.add(bg);
+7 -13
View File
@@ -13,24 +13,13 @@ export default function App<TState extends Record<string, unknown>>(props: { gam
const scene = useComputed(() => new props.gameScene());
const handleReset = async () => {
gameHost.value.gameHost.setup('setup').then(result => {
if(!result.success) {
console.error(result.error);
}else{
console.log('Game finished!', result.result);
}
});
const handleReset = () => {
gameHost.value.gameHost.start();
};
const label = useComputed(() => gameHost.value.gameHost.status.value === 'running' ? 'Restart' : 'Start');
return (
<div className="flex flex-col h-screen">
<div className="flex-1 relative">
<PhaserGame>
<PhaserScene sceneKey="GameScene" scene={scene.value} autoStart data={gameHost.value} />
</PhaserGame>
</div>
<div className="p-4 bg-gray-100 border-t border-gray-200">
<button
onClick={handleReset}
@@ -39,6 +28,11 @@ export default function App<TState extends Record<string, unknown>>(props: { gam
{label}
</button>
</div>
<div className="flex-1 flex relative justify-center items-center">
<PhaserGame>
<PhaserScene sceneKey="GameScene" scene={scene.value} autoStart data={gameHost.value} />
</PhaserGame>
</div>
</div>
);
}