# 编写 GameModule GameModule 是定义游戏逻辑的模块,包含状态定义和命令注册。 ## GameModule 结构 一个 GameModule 必须导出两个东西: ```ts import { createGameCommandRegistry, createRegion } from 'boardgame-core'; // 1. 定义游戏状态 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, }; } // 2. 创建命令注册表并注册命令 const registration = createGameCommandRegistry>(); export const registry = registration.registry; // 注册命令 registration.add('setup', async function () { // ... 命令逻辑 }); registration.add('play ', async function (cmd) { // ... 命令逻辑 }); ``` 也可以使用 `createGameModule` 辅助函数: ```ts import { createGameModule, createGameCommandRegistry, createRegion } from 'boardgame-core'; export const gameModule = createGameModule({ registry: registration.registry, createInitialState, }); ``` ## 定义游戏状态 游戏状态是一个普通对象,通过 `createInitialState()` 工厂函数创建。建议使用 `ReturnType` 推导类型: ```ts 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 type GameState = ReturnType; ``` 状态通常包含: - **Region**:用 `createRegion()` 创建的空间区域 - **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(draft => { draft.currentPlayer = 'O'; }); // 提示玩家输入 const result = await this.prompt( 'confirm ', (command) => { // 验证函数:返回 null 表示有效,返回 string 表示错误信息 return null; }, this.context.value.currentPlayer // currentPlayer 参数可选 ); // 调用子命令 const subResult = await this.run<{ score: number }>(`score ${player}`); if (subResult.success) { console.log(subResult.result.score); } // 返回命令结果 return { success: true }; }); ``` ## 使用 prompt 等待玩家输入 `this.prompt()` 是处理玩家输入的核心方法。它会暂停命令执行,等待外部通过 `host.onInput()` 提交输入: ```ts registration.add('turn ', async function (cmd) { const [turnPlayer, turnNumber] = cmd.params as [PlayerType, number]; // 等待玩家输入 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: (${row}, ${col})`; } if (isCellOccupied(this.context, row, col)) { return `Cell (${row}, ${col}) is already occupied`; } return null; // 验证通过 }, this.context.value.currentPlayer // 可选:标记当前等待的玩家 ); // 验证通过后,playCmd 是已解析的命令对象 const [player, row, col] = playCmd.params as [PlayerType, number, number]; // 执行放置 placePiece(this.context, row, col, player); return { winner: checkWinner(this.context) }; }); ``` 验证函数返回 `null` 表示输入有效,返回 `string` 表示错误信息。 ## 使用 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 ### 创建和放置 Part ```ts import { createPart, createRegion, moveToRegion } from 'boardgame-core'; // 创建区域 const board = createRegion('board', [ { name: 'row', min: 0, max: 2 }, { name: 'col', min: 0, max: 2 }, ]); // 创建棋子 const piece = createPart<{ owner: string }>( { regionId: 'board', position: [1, 1], owner: 'white' }, 'piece-1' ); // 放入状态 state.produce(draft => { draft.parts[piece.id] = piece; draft.board.childIds.push(piece.id); draft.board.partMap['1,1'] = piece.id; }); ``` ### 创建 Part 池 ```ts import { createPartPool } from 'boardgame-core'; // 从池中抽取 const pool = createPartPool<{ type: string }>( { regionId: 'supply', type: 'kitten' }, 10, 'kitten' ); const piece = pool.draw(); // 取出一个 pool.return(piece); // 放回 pool.remaining(); // 剩余数量 ``` ### 区域操作 ```ts import { applyAlign, shuffle, moveToRegion, isCellOccupied } from 'boardgame-core'; // 检查格子是否被占用 if (isCellOccupied(state.parts, 'board', [1, 1])) { ... } // 对齐排列(紧凑排列) applyAlign(handRegion, state.parts); // 打乱位置 shuffle(deckRegion, state.parts, rng); // 移动到其他区域 moveToRegion(piece, sourceRegion, targetRegion, [0, 0]); ``` ## 使用 RNG ```ts import { createRNG } from 'boardgame-core'; const rng = createRNG(12345); // 种子 rng.nextInt(6); // 0-5 rng.next(); // [0, 1) ``` ## 完整示例:井字棋 参考 [`src/samples/tic-tac-toe.ts`](../src/samples/tic-tac-toe.ts),包含: - 2D 棋盘区域 - 玩家轮流输入 - 胜负判定 - 完整的游戏循环