# 编写 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, }; } export const registry = createGameCommandRegistry>(); registry.register('setup', async function (game) { /* ... */ }); registry.register('play ', async function (game, cmd) { /* ... */ }); ``` 也可用 `createGameModule` 辅助函数包装: ```ts export const gameModule = createGameModule({ registry, createInitialState, }); ``` ## 定义游戏状态 建议用 `ReturnType` 推导状态类型: ```ts export type GameState = ReturnType; ``` 状态通常包含 Region、parts(`Record`)以及游戏专属字段(当前玩家、分数等)。 ## 注册命令 使用 `registry.register()` 注册命令。Schema 字符串定义了命令格式: ```ts registry.register('play ', async function (game, player, row, col) { // game 是 IGameContext,可访问和修改状态 game.produce(state => { // state.parts[...].position = [row, col]; }); return { success: true }; }); ``` ### Schema 语法 | 语法 | 含义 | |---|---| | `name` | 命令名 | | `` | 必填参数(字符串) | | `` | 必填参数(自动转为数字) | | `[--flag]` | 可选标志 | | `[-x:number]` | 可选选项(带类型) | ### 命令处理器函数签名 命令处理器接收 `game`(`IGameContext`)作为第一个参数,后续参数来自命令解析: ```ts registry.register('myCommand ', async function (game, arg) { const state = game.value; // 读取状态 game.produce(d => { d.currentPlayer = 'O'; }); // 同步修改状态 await game.produceAsync(d => { /* ... */ }); // 异步修改(等待动画) const result = await game.prompt('confirm ', validator, currentPlayer); const subResult = await subCommand(game, player); // 调用子命令 return { success: true }; }); ``` `registry.register()` 返回一个可调用函数,可在其他命令中直接调用: ```ts const subCommand = registry.register('sub ', async function (game, player) { return { score: 10 }; }); // 在另一个命令中使用 registry.register('main', async function (game) { const result = await subCommand(game, 'X'); // result = { success: true, result: { score: 10 } } }); ``` 详见 [API 参考](./api-reference.md)。 ## 使用 prompt 等待玩家输入 `game.prompt()` 暂停命令执行,等待外部通过 `host.onInput()` 提交输入: ```ts const playCmd = await game.prompt( 'play ', (command) => { const [player, row, col] = command.params as [PlayerType, number, number]; if (player !== turnPlayer) throw `Invalid player: ${player}`; if (row < 0 || row > 2 || col < 0 || col > 2) throw `Invalid position`; if (isCellOccupied(game, row, col)) throw `Cell occupied`; return { player, row, col }; // 验证通过,返回所需数据 }, game.value.currentPlayer ); // playCmd = { player, row, col } ``` 验证函数中 `throw` 字符串会触发重新提示,返回非 null 值表示验证通过并通过该值 resolve Promise。 ## 使用 setup 驱动游戏循环 `setup` 作为入口点驱动游戏循环,通过调用其他命令函数实现: ```ts // 注册 turn 命令并获取可调用函数 const turnCommand = registry.register('turn ', async function (game, player) { // ... 执行回合逻辑 return { winner: null as WinnerType }; }); // 注册 setup 命令 registry.register('setup', async function (game) { while (true) { const currentPlayer = game.value.currentPlayer; const turnOutput = await turnCommand(game, currentPlayer); if (!turnOutput.success) throw new Error(turnOutput.error); game.produce(state => { state.winner = turnOutput.result.winner; if (!state.winner) { state.currentPlayer = state.currentPlayer === 'X' ? 'O' : 'X'; } }); if (game.value.winner) break; } return game.value; }); ``` ## Part、Region 和 RNG 详见 [棋子、区域与 RNG](./parts-regions-rng.md)。 ## 完整示例 参考以下示例: - [`src/samples/tic-tac-toe.ts`](../src/samples/tic-tac-toe.ts) - 井字棋:2D 棋盘、玩家轮流输入、胜负判定 - [`src/samples/boop/`](../src/samples/boop/) - Boop 游戏:六边形棋盘、推动机制、小猫升级