refactor: update api
This commit is contained in:
parent
0e7aaea7da
commit
fa104d10f2
|
|
@ -9,3 +9,4 @@ packages/sample-game/src/**/*.js
|
|||
packages/sample-game/src/**/*.js.map
|
||||
packages/sample-game/src/**/*.d.ts
|
||||
packages/sample-game/src/**/*.d.ts.map
|
||||
/packages/regicide-game/debug
|
||||
|
|
|
|||
|
|
@ -0,0 +1,54 @@
|
|||
# boardgame-phaser
|
||||
|
||||
基于Phaser3/boardgame-core的游戏框架
|
||||
|
||||
## 概述
|
||||
|
||||
项目使用pnpm monorepo管理,包含以下包:
|
||||
- `framework`:通用框架
|
||||
- `boop-game`:boop样例
|
||||
- `sample-game`:tic tac toe样例
|
||||
|
||||
游戏应当使用vite构建,基于`preact/signals`进行状态管理,使用`phaser3`实现游戏功能。
|
||||
|
||||
## boardgame-core
|
||||
|
||||
项目使用`boardgame-core`进行游戏定义。
|
||||
`boardgame-core`的内容可以在`framework/node_modules/boardgame-core`找到。
|
||||
这个文件夹被.gitignore忽略,查看时需要绕开这一限制。
|
||||
|
||||
## 编写游戏
|
||||
|
||||
游戏逻辑以gameModule的形式定义:
|
||||
|
||||
```typescript
|
||||
import {createGameCommandRegistry, IGameContext} from "boardgame-core";
|
||||
|
||||
// 创建mutative游戏初始状态
|
||||
export function createInitialState(): GameState {
|
||||
//...
|
||||
}
|
||||
|
||||
// 运行游戏
|
||||
export async function start(game: IGameContext<GameState>) {
|
||||
// ...
|
||||
}
|
||||
|
||||
// 可选
|
||||
export const registry = createGameCommandRegistry();
|
||||
```
|
||||
|
||||
使用以下步骤创建游戏:
|
||||
|
||||
### 1. 定义状态
|
||||
|
||||
游戏状态使用一个大的`mutative`对象来描述,所有的状态更新通过`game.produce`或`game.produceAsync`来实现。
|
||||
|
||||
通常使用一个`regions: Record<string, Region>`和一个`parts: Record<string, Part<TMeta>>`来记录桌游物件的摆放。
|
||||
|
||||
### 2. 定义流程
|
||||
|
||||
使用`async function start(game: IGameContext<GameState>)`作为入口。
|
||||
|
||||
需要等待玩家交互时,使用`await game.prompt(schema, validator, player)`。
|
||||
|
||||
|
|
@ -24,7 +24,7 @@ export const registry = createGameCommandRegistry<BoopState>();
|
|||
/**
|
||||
* 放置棋子到棋盘
|
||||
*/
|
||||
async function place(game: BoopGame, row: number, col: number, player: PlayerType, type: PieceType) {
|
||||
async function handlePlace(game: BoopGame, row: number, col: number, player: PlayerType, type: PieceType) {
|
||||
const value = game.value;
|
||||
// 从玩家supply中找到对应类型的棋子
|
||||
const part = findPartInRegion(game, player, type);
|
||||
|
|
@ -43,12 +43,12 @@ async function place(game: BoopGame, row: number, col: number, player: PlayerTyp
|
|||
|
||||
return { row, col, player, type, partId };
|
||||
}
|
||||
const placeCommand = registry.register( 'place <row:number> <col:number> <player> <type>', place);
|
||||
const place = registry.register( 'place <row:number> <col:number> <player> <type>', handlePlace);
|
||||
|
||||
/**
|
||||
* 执行boop - 推动周围棋子
|
||||
*/
|
||||
async function boop(game: BoopGame, row: number, col: number, type: PieceType) {
|
||||
async function handleBoop(game: BoopGame, row: number, col: number, type: PieceType) {
|
||||
const booped: string[] = [];
|
||||
|
||||
const toRemove = new Set<string>();
|
||||
|
|
@ -95,12 +95,12 @@ async function boop(game: BoopGame, row: number, col: number, type: PieceType) {
|
|||
|
||||
return { booped };
|
||||
}
|
||||
const boopCommand = registry.register('boop <row:number> <col:number> <type>', boop);
|
||||
const boop = registry.register('boop <row:number> <col:number> <type>', handleBoop);
|
||||
|
||||
/**
|
||||
* 检查是否有玩家获胜(三个猫连线)
|
||||
*/
|
||||
async function checkWin(game: BoopGame) {
|
||||
async function handleCheckWin(game: BoopGame) {
|
||||
for(const line of getLineCandidates()){
|
||||
let whites = 0;
|
||||
let blacks = 0;
|
||||
|
|
@ -119,12 +119,12 @@ async function checkWin(game: BoopGame) {
|
|||
}
|
||||
return null;
|
||||
}
|
||||
const checkWinCommand = registry.register('check-win', checkWin);
|
||||
const checkWin = registry.register('check-win', handleCheckWin);
|
||||
|
||||
/**
|
||||
* 检查并执行小猫升级(三个小猫连线变成猫)
|
||||
*/
|
||||
async function checkGraduates(game: BoopGame){
|
||||
async function handleCheckGraduates(game: BoopGame){
|
||||
const toUpgrade = new Set<string>();
|
||||
for(const line of getLineCandidates()){
|
||||
let whites = 0;
|
||||
|
|
@ -155,12 +155,12 @@ async function checkGraduates(game: BoopGame){
|
|||
}
|
||||
});
|
||||
}
|
||||
const checkGraduatesCommand = registry.register('check-graduates', checkGraduates);
|
||||
const checkGraduates = registry.register('check-graduates', handleCheckGraduates);
|
||||
|
||||
async function setup(game: BoopGame) {
|
||||
export async function start(game: BoopGame) {
|
||||
while (true) {
|
||||
const currentPlayer = game.value.currentPlayer;
|
||||
const { winner } = await turnCommand(game, currentPlayer);
|
||||
const { winner } = await turn(game, currentPlayer);
|
||||
|
||||
await game.produceAsync((state: BoopState) => {
|
||||
state.winner = winner;
|
||||
|
|
@ -173,9 +173,8 @@ async function setup(game: BoopGame) {
|
|||
|
||||
return game.value;
|
||||
}
|
||||
registry.register('setup', setup);
|
||||
|
||||
async function checkFullBoard(game: BoopGame, turnPlayer: PlayerType){
|
||||
async function handleCheckFullBoard(game: BoopGame, turnPlayer: PlayerType){
|
||||
// 检查8-piece规则: 如果玩家所有8个棋子都在棋盘上且没有获胜,强制升级一个小猫
|
||||
const playerPieces = Object.values(game.value.pieces).filter(
|
||||
(p: BoopPart) => p.player === turnPlayer && p.regionId === 'board'
|
||||
|
|
@ -186,8 +185,7 @@ async function checkFullBoard(game: BoopGame, turnPlayer: PlayerType){
|
|||
|
||||
const partId = await game.prompt(
|
||||
'play <player> <row:number> <col:number> [type:string]',
|
||||
(command: Command) => {
|
||||
const [player, row, col] = command.params as [PlayerType, number, number];
|
||||
(player: PlayerType, row: number, col: number, type?: PieceType) => {
|
||||
if (player !== turnPlayer) {
|
||||
throw `无效的玩家: ${player},期望的是 ${turnPlayer}。`;
|
||||
}
|
||||
|
|
@ -211,12 +209,12 @@ async function checkFullBoard(game: BoopGame, turnPlayer: PlayerType){
|
|||
moveToRegion(cat || part, null, state.regions[turnPlayer]);
|
||||
});
|
||||
}
|
||||
const checkFullBoard = registry.register('check-full-board', handleCheckFullBoard);
|
||||
|
||||
async function turn(game: BoopGame, turnPlayer: PlayerType) {
|
||||
async function handleTurn(game: BoopGame, turnPlayer: PlayerType) {
|
||||
const {row, col, type} = await game.prompt(
|
||||
'play <player> <row:number> <col:number> [type:string]',
|
||||
(command: Command) => {
|
||||
const [player, row, col, type] = command.params as [PlayerType, number, number, PieceType?];
|
||||
(player: PlayerType, row: number, col: number, type?: PieceType) => {
|
||||
const pieceType = type === 'cat' ? 'cat' : 'kitten';
|
||||
|
||||
if (player !== turnPlayer) {
|
||||
|
|
@ -239,17 +237,18 @@ async function turn(game: BoopGame, turnPlayer: PlayerType) {
|
|||
);
|
||||
const pieceType = type === 'cat' ? 'cat' : 'kitten';
|
||||
|
||||
await placeCommand(game, row, col, turnPlayer, pieceType);
|
||||
await boopCommand(game, row, col, pieceType);
|
||||
const winner = await checkWinCommand(game);
|
||||
await place(game, row, col, turnPlayer, pieceType);
|
||||
await boop(game, row, col, pieceType);
|
||||
const winner = await checkWin(game);
|
||||
if(winner) return { winner: winner as WinnerType };
|
||||
|
||||
await checkGraduatesCommand(game);
|
||||
await checkFullBoard(game, turnPlayer);
|
||||
await checkGraduates(game);
|
||||
await handleCheckFullBoard(game, turnPlayer);
|
||||
return { winner: null };
|
||||
}
|
||||
const turnCommand = registry.register('turn <player>', turn);
|
||||
export const commands = {
|
||||
const turn = registry.register('turn <player>', handleTurn);
|
||||
|
||||
export const prompts = {
|
||||
play: (player: PlayerType, row: number, col: number, type?: PieceType) => {
|
||||
if (type) {
|
||||
return `play ${player} ${row} ${col} ${type}`;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import type { BoopState } from '@/game';
|
||||
import { GameHostScene } from 'boardgame-phaser';
|
||||
import { commands } from '@/game';
|
||||
import { prompts } from '@/game';
|
||||
import { BoardRenderer } from './BoardRenderer';
|
||||
import { createPieceSpawner } from './PieceSpawner';
|
||||
import { SupplyUI } from './SupplyUI';
|
||||
|
|
@ -73,7 +73,7 @@ export class GameScene extends GameHostScene<BoopState> {
|
|||
|
||||
private handleCellClick(row: number, col: number): void {
|
||||
const selectedType = this.pieceTypeSelector.getSelectedType();
|
||||
const cmd = commands.play(this.state.currentPlayer, row, col, selectedType);
|
||||
const cmd = prompts.play(this.state.currentPlayer, row, col, selectedType);
|
||||
const error = this.gameHost.onInput(cmd);
|
||||
if (error) {
|
||||
this.errorOverlay.show(error);
|
||||
|
|
@ -82,7 +82,7 @@ export class GameScene extends GameHostScene<BoopState> {
|
|||
|
||||
private handlePieceClick(row: number, col: number): void {
|
||||
// 棋盘满时,点击棋子触发升级
|
||||
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) {
|
||||
this.errorOverlay.show(error);
|
||||
|
|
@ -90,10 +90,10 @@ export class GameScene extends GameHostScene<BoopState> {
|
|||
}
|
||||
|
||||
private startGame(): void {
|
||||
this.gameHost.setup('setup');
|
||||
this.gameHost.start();
|
||||
}
|
||||
|
||||
private restartGame(): void {
|
||||
this.gameHost.setup('setup');
|
||||
this.gameHost.start();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,19 +14,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);
|
||||
}
|
||||
});
|
||||
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">
|
||||
<div className="flex-1 flex relative justify-center items-center">
|
||||
<PhaserGame config={{ width: 640, height: 750 }}>
|
||||
<PhaserScene sceneKey="GameScene" scene={scene.value} autoStart data={gameHost.value} />
|
||||
</PhaserGame>
|
||||
|
|
|
|||
|
|
@ -1,147 +0,0 @@
|
|||
import Phaser from 'phaser';
|
||||
import { effect, type Signal } from '@preact/signals-core';
|
||||
import type { MutableSignal, Region, Part } from 'boardgame-core';
|
||||
|
||||
type DisposeFn = () => void;
|
||||
|
||||
export function bindSignal<T, K extends keyof T>(
|
||||
signal: MutableSignal<T>,
|
||||
getter: (state: T) => T[K],
|
||||
setter: (value: T[K]) => void,
|
||||
): DisposeFn {
|
||||
return effect(() => {
|
||||
const val = getter(signal.value);
|
||||
setter(val);
|
||||
});
|
||||
}
|
||||
|
||||
export function bindGameObjectProperty<T>(
|
||||
signal: Signal<T>,
|
||||
target: Phaser.GameObjects.GameObject,
|
||||
prop: string,
|
||||
): DisposeFn {
|
||||
return effect(() => {
|
||||
(target as unknown as Record<string, unknown>)[prop] = signal.value;
|
||||
});
|
||||
}
|
||||
|
||||
export interface BindRegionOptions<TPart extends Part> {
|
||||
cellSize: { x: number; y: number };
|
||||
offset?: { x: number; y: number };
|
||||
factory: (part: TPart, position: Phaser.Math.Vector2) => Phaser.GameObjects.GameObject;
|
||||
update?: (part: TPart, obj: Phaser.GameObjects.GameObject) => void;
|
||||
}
|
||||
|
||||
export function bindRegion<TState, TMeta>(
|
||||
state: MutableSignal<TState>,
|
||||
partsGetter: (state: TState) => Record<string, Part<TMeta>>,
|
||||
regionGetter: (state: TState) => Region,
|
||||
options: BindRegionOptions<Part<TMeta>>,
|
||||
container: Phaser.GameObjects.Container,
|
||||
): { cleanup: () => void; objects: Map<string, Phaser.GameObjects.GameObject> } {
|
||||
const objects = new Map<string, Phaser.GameObjects.GameObject>();
|
||||
|
||||
const offset = options.offset ?? { x: 0, y: 0 };
|
||||
|
||||
const dispose = effect(function(this: { dispose: () => void }) {
|
||||
const currentState = state.value;
|
||||
const parts = partsGetter(currentState);
|
||||
const region = regionGetter(currentState);
|
||||
const currentIds = new Set(region.childIds);
|
||||
|
||||
// 移除不在 region 中的对象
|
||||
for (const [id, obj] of objects) {
|
||||
if (!currentIds.has(id)) {
|
||||
obj.destroy();
|
||||
objects.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
// 同步 region 中的 parts
|
||||
for (const childId of region.childIds) {
|
||||
const part = parts[childId];
|
||||
if (!part) continue;
|
||||
|
||||
// 支持动态维度:取前两个维度作为 x, y
|
||||
// position[0] = row (y轴), position[1] = col (x轴)
|
||||
const pos = new Phaser.Math.Vector2(
|
||||
(part.position[1] ?? 0) * options.cellSize.x + offset.x,
|
||||
(part.position[0] ?? 0) * options.cellSize.y + offset.y,
|
||||
);
|
||||
|
||||
let obj = objects.get(childId);
|
||||
if (!obj) {
|
||||
obj = options.factory(part, pos);
|
||||
objects.set(childId, obj);
|
||||
container.add(obj);
|
||||
} else {
|
||||
// 更新位置
|
||||
if ('setPosition' in obj && typeof obj.setPosition === 'function') {
|
||||
(obj as any).setPosition(pos.x, pos.y);
|
||||
}
|
||||
// 调用自定义更新函数
|
||||
if (options.update) {
|
||||
options.update(part, obj);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
cleanup: () => {
|
||||
dispose();
|
||||
for (const [, obj] of objects) obj.destroy();
|
||||
objects.clear();
|
||||
},
|
||||
objects,
|
||||
};
|
||||
}
|
||||
|
||||
export interface BindCollectionOptions<T extends { id: string }> {
|
||||
factory: (item: T) => Phaser.GameObjects.GameObject;
|
||||
update?: (item: T, obj: Phaser.GameObjects.GameObject) => void;
|
||||
}
|
||||
|
||||
export function bindCollection<T extends { id: string }>(
|
||||
collection: Signal<Record<string, MutableSignal<T>>>,
|
||||
options: BindCollectionOptions<T>,
|
||||
container: Phaser.GameObjects.Container,
|
||||
): { cleanup: () => void; objects: Map<string, Phaser.GameObjects.GameObject> } {
|
||||
const objects = new Map<string, Phaser.GameObjects.GameObject>();
|
||||
const effects: DisposeFn[] = [];
|
||||
|
||||
function syncCollection() {
|
||||
const entries = Object.entries(collection.value);
|
||||
const currentIds = new Set(entries.map(([id]) => id));
|
||||
|
||||
for (const [id, obj] of objects) {
|
||||
if (!currentIds.has(id)) {
|
||||
obj.destroy();
|
||||
objects.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
for (const [id, signal] of entries) {
|
||||
let obj = objects.get(id);
|
||||
if (!obj) {
|
||||
obj = options.factory(signal.value);
|
||||
objects.set(id, obj);
|
||||
container.add(obj);
|
||||
} else if (options.update) {
|
||||
options.update(signal.value, obj);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const e = effect(syncCollection);
|
||||
effects.push(e);
|
||||
|
||||
return {
|
||||
cleanup: () => {
|
||||
for (const e of effects) e();
|
||||
for (const [, obj] of objects) obj.destroy();
|
||||
objects.clear();
|
||||
},
|
||||
objects,
|
||||
};
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import Phaser from 'phaser';
|
||||
import { effect, type ReadonlySignal } from '@preact/signals-core';
|
||||
import { effect } from '@preact/signals-core';
|
||||
|
||||
type GO = Phaser.GameObjects.GameObject;
|
||||
|
||||
|
|
@ -11,7 +11,7 @@ export interface Spawner<TData, TObject extends GO = GO> {
|
|||
/** 创建新对象 */
|
||||
onSpawn(t: TData): TObject | null;
|
||||
/** 销毁旧对象 */
|
||||
onDespawn(obj: TObject): void;
|
||||
onDespawn(obj: TObject, t: TData): void;
|
||||
/** 更新已有对象 */
|
||||
onUpdate(t: TData, obj: TObject): void;
|
||||
}
|
||||
|
|
@ -20,6 +20,7 @@ export function spawnEffect<TData, TObject extends GO = GO>(
|
|||
spawner: Spawner<TData, TObject>,
|
||||
): () => void {
|
||||
const objects = new Map<string, TObject>();
|
||||
const spawnData = new Map<string, TData>();
|
||||
|
||||
return effect(() => {
|
||||
const current = new Set<string>();
|
||||
|
|
@ -32,17 +33,21 @@ export function spawnEffect<TData, TObject extends GO = GO>(
|
|||
if (!existing) {
|
||||
const obj = spawner.onSpawn(t);
|
||||
if (obj) {
|
||||
spawnData.set(key, t);
|
||||
objects.set(key, obj);
|
||||
}
|
||||
} else {
|
||||
if(spawnData.get(key) === t) continue;
|
||||
spawner.onUpdate(t, existing);
|
||||
spawnData.set(key, t);
|
||||
}
|
||||
}
|
||||
|
||||
for (const [key, obj] of objects) {
|
||||
if (!current.has(key)) {
|
||||
spawner.onDespawn(obj);
|
||||
spawner.onDespawn(obj, spawnData.get(key)!);
|
||||
objects.delete(key);
|
||||
spawnData.delete(key);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import Phaser from 'phaser';
|
||||
import { signal, useSignal, useSignalEffect, type Signal } from '@preact/signals';
|
||||
import { signal, useSignal, useSignalEffect } from '@preact/signals';
|
||||
import { createContext, h } from 'preact';
|
||||
import { useContext } from 'preact/hooks';
|
||||
import {ReadonlySignal} from "@preact/signals-core";
|
||||
|
|
|
|||
|
|
@ -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,12 +126,13 @@ 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}`;
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -88,6 +88,46 @@ importers:
|
|||
specifier: ^3.2.4
|
||||
version: 3.2.4(lightningcss@1.32.0)
|
||||
|
||||
packages/regicide-game:
|
||||
dependencies:
|
||||
'@preact/signals-core':
|
||||
specifier: ^1.5.1
|
||||
version: 1.14.1
|
||||
boardgame-core:
|
||||
specifier: link:../../../boardgame-core
|
||||
version: link:../../../boardgame-core
|
||||
boardgame-phaser:
|
||||
specifier: workspace:*
|
||||
version: link:../framework
|
||||
mutative:
|
||||
specifier: ^1.3.0
|
||||
version: 1.3.0
|
||||
phaser:
|
||||
specifier: ^3.80.1
|
||||
version: 3.90.0
|
||||
preact:
|
||||
specifier: ^10.19.3
|
||||
version: 10.29.0
|
||||
devDependencies:
|
||||
'@preact/preset-vite':
|
||||
specifier: ^2.8.1
|
||||
version: 2.10.5(@babel/core@7.29.0)(preact@10.29.0)(rollup@4.60.1)(vite@5.4.21(lightningcss@1.32.0))
|
||||
'@preact/signals':
|
||||
specifier: ^2.9.0
|
||||
version: 2.9.0(preact@10.29.0)
|
||||
'@tailwindcss/vite':
|
||||
specifier: ^4.0.0
|
||||
version: 4.2.2(vite@5.4.21(lightningcss@1.32.0))
|
||||
tailwindcss:
|
||||
specifier: ^4.0.0
|
||||
version: 4.2.2
|
||||
typescript:
|
||||
specifier: ^5.3.3
|
||||
version: 5.9.3
|
||||
vite:
|
||||
specifier: ^5.1.0
|
||||
version: 5.4.21(lightningcss@1.32.0)
|
||||
|
||||
packages/sample-game:
|
||||
dependencies:
|
||||
'@preact/signals-core':
|
||||
|
|
|
|||
Loading…
Reference in New Issue