boardgame-core/docs/game-module.md

294 lines
7.6 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';
// 1. 定义游戏状态
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,
};
}
// 2. 创建命令注册表并注册命令
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
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<string, TicTacToePart>,
currentPlayer: 'X' as PlayerType,
winner: null as WinnerType,
turn: 0,
};
}
export type GameState = ReturnType<typeof createInitialState>;
```
状态通常包含:
- **Region**:用 `createRegion()` 创建的空间区域
- **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(draft => {
draft.currentPlayer = 'O';
});
// 提示玩家输入
const result = await this.prompt(
'confirm <action>',
(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 <player> <turn:number>', async function (cmd) {
const [turnPlayer, turnNumber] = cmd.params as [PlayerType, number];
// 等待玩家输入
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: (${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 棋盘区域
- 玩家轮流输入
- 胜负判定
- 完整的游戏循环