# 编写 GameModule GameModule 是定义游戏逻辑的模块,包含状态定义和命令注册。 ## GameModule 结构 一个 GameModule 必须导出两个东西: ```ts 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, currentPlayer: 'X' as PlayerType, winner: null as WinnerType, turn: 0, }; } const registration = createGameCommandRegistry>(); export const registry = registration.registry; registration.add('setup', async function () { /* ... */ }); registration.add('play ', async function (cmd) { /* ... */ }); ``` 也可用 `createGameModule` 辅助函数包装: ```ts export const gameModule = createGameModule({ registry: registration.registry, createInitialState, }); ``` ## 定义游戏状态 建议用 `ReturnType` 推导状态类型: ```ts export type GameState = ReturnType; ``` 状态通常包含 Region、parts(`Record`)以及游戏专属字段(当前玩家、分数等)。 ## 注册命令 使用 `registration.add()` 注册命令。Schema 字符串定义了命令格式: ```ts registration.add('play ', async function (cmd) { const [player, row, col] = cmd.params as [PlayerType, number, number]; // this.context 是 MutableSignal this.context.produce(state => { state.parts[piece.id] = piece; }); return { winner: null }; }); ``` ### Schema 语法 | 语法 | 含义 | |---|---| | `name` | 命令名 | | `` | 必填参数(字符串) | | `` | 必填参数(自动转为数字) | | `[--flag]` | 可选标志 | | `[-x:number]` | 可选选项(带类型) | ### 命令处理器中的 this 命令处理器中的 `this` 是 `CommandRunnerContext>`: ```ts registration.add('myCommand ', async function (cmd) { const state = this.context.value; // 读取状态 this.context.produce(d => { d.currentPlayer = 'O'; }); // 修改状态 const result = await this.prompt('confirm ', validator, currentPlayer); const subResult = await this.run<{ score: number }>(`score ${player}`); return { success: true }; }); ``` 详见 [API 参考](./api-reference.md)。 ## 使用 prompt 等待玩家输入 `this.prompt()` 暂停命令执行,等待外部通过 `host.onInput()` 提交输入: ```ts const playCmd = await this.prompt( 'play ', (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` 作为入口点驱动游戏循环: ```ts 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](./parts-regions-rng.md)。 ## 完整示例 参考 [`src/samples/tic-tac-toe.ts`](../src/samples/tic-tac-toe.ts),包含: - 2D 棋盘区域 - 玩家轮流输入 - 胜负判定 - 完整的游戏循环