168 lines
5.0 KiB
Markdown
168 lines
5.0 KiB
Markdown
# 编写 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<string, Part>,
|
||
currentPlayer: 'X' as PlayerType,
|
||
winner: null as WinnerType,
|
||
turn: 0,
|
||
};
|
||
}
|
||
|
||
export const registry = createGameCommandRegistry<ReturnType<typeof createInitialState>>();
|
||
|
||
registry.register('setup', async function (game) { /* ... */ });
|
||
registry.register('play <player> <row:number> <col:number>', async function (game, cmd) { /* ... */ });
|
||
```
|
||
|
||
也可用 `createGameModule` 辅助函数包装:
|
||
|
||
```ts
|
||
export const gameModule = createGameModule({
|
||
registry,
|
||
createInitialState,
|
||
});
|
||
```
|
||
|
||
## 定义游戏状态
|
||
|
||
建议用 `ReturnType` 推导状态类型:
|
||
|
||
```ts
|
||
export type GameState = ReturnType<typeof createInitialState>;
|
||
```
|
||
|
||
状态通常包含 Region、parts(`Record<string, Part>`)以及游戏专属字段(当前玩家、分数等)。
|
||
|
||
## 注册命令
|
||
|
||
使用 `registry.register()` 注册命令。Schema 字符串定义了命令格式:
|
||
|
||
```ts
|
||
registry.register('play <player> <row:number> <col:number>', async function (game, player, row, col) {
|
||
// game 是 IGameContext<TState>,可访问和修改状态
|
||
game.produce(state => {
|
||
// state.parts[...].position = [row, col];
|
||
});
|
||
|
||
return { success: true };
|
||
});
|
||
```
|
||
|
||
### Schema 语法
|
||
|
||
| 语法 | 含义 |
|
||
|---|---|
|
||
| `name` | 命令名 |
|
||
| `<param>` | 必填参数(字符串) |
|
||
| `<param:number>` | 必填参数(自动转为数字) |
|
||
| `[--flag]` | 可选标志 |
|
||
| `[-x:number]` | 可选选项(带类型) |
|
||
|
||
### 命令处理器函数签名
|
||
|
||
命令处理器接收 `game`(`IGameContext<TState>`)作为第一个参数,后续参数来自命令解析:
|
||
|
||
```ts
|
||
registry.register('myCommand <arg>', async function (game, arg) {
|
||
const state = game.value; // 读取状态
|
||
game.produce(d => { d.currentPlayer = 'O'; }); // 同步修改状态
|
||
await game.produceAsync(d => { /* ... */ }); // 异步修改(等待动画)
|
||
|
||
const result = await game.prompt('confirm <action>', validator, currentPlayer);
|
||
const subResult = await subCommand(game, player); // 调用子命令
|
||
return { success: true };
|
||
});
|
||
```
|
||
|
||
`registry.register()` 返回一个可调用函数,可在其他命令中直接调用:
|
||
|
||
```ts
|
||
const subCommand = registry.register('sub <player>', 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 <player> <row:number> <col:number>',
|
||
(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 <player>', 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 游戏:六边形棋盘、推动机制、小猫升级
|