boardgame-core/docs/game-module.md

152 lines
4.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 编写 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,
};
}
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` 辅助函数包装:
```ts
export const gameModule = createGameModule({
registry: registration.registry,
createInitialState,
});
```
## 定义游戏状态
建议用 `ReturnType` 推导状态类型:
```ts
export type GameState = ReturnType<typeof createInitialState>;
```
状态通常包含 Region、parts`Record<string, Part>`)以及游戏专属字段(当前玩家、分数等)。
## 注册命令
使用 `registration.add()` 注册命令。Schema 字符串定义了命令格式:
```ts
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>>`
```ts
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 参考](./api-reference.md)。
## 使用 prompt 等待玩家输入
`this.prompt()` 暂停命令执行,等待外部通过 `host.onInput()` 提交输入:
```ts
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` 作为入口点驱动游戏循环:
```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 棋盘区域
- 玩家轮流输入
- 胜负判定
- 完整的游戏循环