4.3 KiB
4.3 KiB
编写 GameModule
GameModule 是定义游戏逻辑的模块,包含状态定义和命令注册。
GameModule 结构
一个 GameModule 必须导出两个东西:
import { createGameCommandRegistry, createRegion } from 'boardgame-core';
export function createInitialState() {
return {
board: createRegion('board', [
{ name: 'x', min: 0, max: 2 },
{ name: 'y', min: 0, max: 2 },
]),
parts: {} as Record<string, Part>,
currentPlayer: 'X' as PlayerType,
winner: null as WinnerType,
turn: 0,
};
}
const registration = createGameCommandRegistry<ReturnType<typeof createInitialState>>();
export const registry = registration.registry;
registration.add('setup', async function () { /* ... */ });
registration.add('play <player> <row:number> <col:number>', async function (cmd) { /* ... */ });
也可用 createGameModule 辅助函数包装:
export const gameModule = createGameModule({
registry: registration.registry,
createInitialState,
});
定义游戏状态
建议用 ReturnType 推导状态类型:
export type GameState = ReturnType<typeof createInitialState>;
状态通常包含 Region、parts(Record<string, Part>)以及游戏专属字段(当前玩家、分数等)。
注册命令
使用 registration.add() 注册命令。Schema 字符串定义了命令格式:
registration.add('play <player> <row:number> <col:number>', async function (cmd) {
const [player, row, col] = cmd.params as [PlayerType, number, number];
// this.context 是 MutableSignal<GameState>
this.context.produce(state => {
state.parts[piece.id] = piece;
});
return { winner: null };
});
Schema 语法
| 语法 | 含义 |
|---|---|
name |
命令名 |
<param> |
必填参数(字符串) |
<param:number> |
必填参数(自动转为数字) |
[--flag] |
可选标志 |
[-x:number] |
可选选项(带类型) |
命令处理器中的 this
命令处理器中的 this 是 CommandRunnerContext<MutableSignal<TState>>:
registration.add('myCommand <arg>', async function (cmd) {
const state = this.context.value; // 读取状态
this.context.produce(d => { d.currentPlayer = 'O'; }); // 修改状态
const result = await this.prompt('confirm <action>', validator, currentPlayer);
const subResult = await this.run<{ score: number }>(`score ${player}`);
return { success: true };
});
详见 API 参考。
使用 prompt 等待玩家输入
this.prompt() 暂停命令执行,等待外部通过 host.onInput() 提交输入:
const playCmd = await this.prompt(
'play <player> <row:number> <col:number>',
(command) => {
const [player, row, col] = command.params as [PlayerType, number, number];
if (player !== turnPlayer) return `Invalid player: ${player}`;
if (row < 0 || row > 2 || col < 0 || col > 2) return `Invalid position`;
if (isCellOccupied(this.context, row, col)) return `Cell occupied`;
return null;
},
this.context.value.currentPlayer
);
验证函数返回 null 表示有效,返回 string 表示错误信息。验证通过后 playCmd 是已解析的命令对象。
使用 setup 驱动游戏循环
setup 作为入口点驱动游戏循环:
registration.add('setup', async function () {
const { context } = this;
while (true) {
const currentPlayer = context.value.currentPlayer;
const turnNumber = context.value.turn + 1;
const turnOutput = await this.run<{ winner: WinnerType }>(`turn ${currentPlayer} ${turnNumber}`);
if (!turnOutput.success) throw new Error(turnOutput.error);
context.produce(state => {
state.winner = turnOutput.result.winner;
if (!state.winner) {
state.currentPlayer = state.currentPlayer === 'X' ? 'O' : 'X';
state.turn = turnNumber;
}
});
if (context.value.winner) break;
}
return context.value;
});
Part、Region 和 RNG
详见 棋子、区域与 RNG。
完整示例
参考 src/samples/tic-tac-toe.ts,包含:
- 2D 棋盘区域
- 玩家轮流输入
- 胜负判定
- 完整的游戏循环