boardgame-phaser/docs/boardgame-core-guide.md

647 lines
17 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.

# boardgame-core 使用指南
## 概述
`boardgame-core` 是一个基于 Preact Signals 的桌游状态管理库,提供响应式状态、实体集合、空间区域系统和命令驱动的游戏循环。
## 核心概念
### 1. MutableSignal — 响应式状态容器
`MutableSignal<T>` 扩展了 Preact Signal增加了通过 Mutative 进行不可变状态更新的能力。
```ts
import { mutableSignal } from 'boardgame-core';
// 创建响应式状态
const state = mutableSignal({ score: 0, players: [] });
// 读取状态
console.log(state.value.score);
// 更新状态 — 使用 produce 进行不可变更新
state.produce(draft => {
draft.score += 10;
draft.players.push('Alice');
});
```
**关键 API**
- `.value` — 访问当前状态值
- `.produce(fn)` — 通过 Mutative 更新状态(类似 Immer 的 `produce`
### 3. Region System — 空间区域管理
`Region` 用于管理游戏部件的空间位置和分组。
```ts
import { createRegion, applyAlign, shuffle, moveToRegion } from 'boardgame-core';
// 创建区域
const board = createRegion('board', [
{ name: 'x', min: 0, max: 7 },
{ name: 'y', min: 0, max: 7 },
]);
const hand = createRegion('hand', [
{ name: 'x', min: 0, max: 10, align: 'start' },
]);
// 区域属性:
// - id: 区域标识
// - axes: 坐标轴定义name, min, max, align
// - childIds: 包含的部件 ID 列表
// - partMap: 位置 → ID 映射
```
**区域操作:**
```ts
// 对齐/紧凑排列(根据 axis.align 自动调整位置)
applyAlign(hand, parts);
// 随机打乱位置(需要 RNG
shuffle(board, parts, rng);
// 移动部件到目标区域
moveToRegion(part, sourceRegion, targetRegion, [2, 3]);
// 批量移动
moveToRegionAll(parts, sourceRegion, targetRegion, positions);
// 从区域移除部件
removeFromRegion(part, region);
```
### 4. Part System — 游戏部件系统
`Part<TMeta>` 表示游戏中的一个部件(棋子、卡牌等)。
```ts
import { createPart, createParts, createPartPool, flip, flipTo, roll } from 'boardgame-core';
// 创建单个部件
const piece = createPart(
{ regionId: 'board', position: [0, 0], player: 'X' },
'piece-1'
);
// 批量创建(生成 piece-pawn-0, piece-pawn-1, ...
const pawns = createParts(
{ regionId: 'supply', position: [0, 0] },
8,
'piece-pawn'
);
// 部件池(用于抽牌堆等)
const deck = createPartPool(
{ regionId: 'deck', sides: 4 }, // template
52, // count
'card' // id prefix
);
// 抽牌
const card = deck.draw();
// 返回部件到池中
deck.return(card);
// 剩余数量
console.log(deck.remaining());
// 合并多个牌池
const merged = mergePartPools(deck1, deck2);
```
**部件属性:**
- `id` — 唯一标识
- `sides` — 面数(如骰子 6 面,卡牌 2 面)
- `side` — 当前朝向0-based 索引)
- `regionId` — 所属区域
- `position` — 位置坐标(数组,长度 = axes 数量)
- `alignments` — 对齐方式列表
- `alignment` — 当前对齐方式
- `...TMeta` — 自定义元数据
**部件操作:**
```ts
// 翻面(循环到下一面)
flip(card);
// 翻到指定面
flipTo(card, 1);
// 随机面(使用 RNG
roll(dice, rng);
```
**部件查询:**
```ts
import { findPartById, isCellOccupied, getPartAtPosition } from 'boardgame-core';
// 按 ID 查找
const piece = findPartById(parts, 'piece-1');
// 检查位置是否被占用
if (isCellOccupied(parts, 'board', [2, 3])) {
// 位置已被占用
}
// 获取指定位置的部件
const part = getPartAtPosition(parts, 'board', [2, 3]);
```
### 5. Command System — 命令系统
CLI 风格的命令解析、验证和执行系统。
#### 命令字符串格式
```
commandName <param1> <param2:type> [--option value] [-s short] [--flag]
```
**示例:**
```
place 2 3:number --force -x 10
move card1 hand --type=kitten
turn X 1
```
#### 定义命令注册表
```ts
import { createGameCommandRegistry, MutableSignal } from 'boardgame-core';
type GameState = {
board: Region;
parts: Part[];
currentPlayer: 'X' | 'O';
winner: string | null;
};
// 创建注册表
const registration = createGameCommandRegistry<GameState>();
export const registry = registration.registry;
// 注册命令(链式 API
registration.add('place <row:number> <col:number>', async function(cmd) {
const [row, col] = cmd.params as [number, number];
const player = this.context.value.currentPlayer;
// 更新状态
this.context.produce(state => {
state.parts.push({ id: `piece-${row}-${col}`, regionId: 'board', position: [row, col], player });
});
return { success: true, row, col };
});
// 继续注册更多命令
registration.add('turn <player> <turn:number>', async function(cmd) {
// ...
});
```
#### 命令处理器的 `this` 上下文
命令处理函数中,`this` 是 `CommandRunnerContext`,提供:
```ts
registration.add('my-command <arg>', async function(cmd) {
// this.context — 游戏上下文MutableSignal<GameState>
const state = this.context.value;
// this.context.produce — 更新状态
this.context.produce(draft => {
draft.score += 1;
});
// this.run — 运行子命令
const result = await this.run<PlaceResult>(`place 2 3`);
if (result.success) {
console.log(result.result.row);
}
// this.prompt — 等待玩家输入
const playCmd = await this.prompt(
'play <player> <row:number> <col:number>',
(command) => {
// 验证函数:返回 null 表示有效,返回 string 表示错误消息
const [player, row, col] = command.params;
if (player !== state.currentPlayer) {
return `Invalid player: ${player}`;
}
return null;
}
);
return { success: true };
});
```
#### 运行命令
```ts
import { createGameContext } from 'boardgame-core';
const game = createGameContext(registry, createInitialState);
// 运行命令(返回 Promise<CommandResult>
const result = await game.commands.run('place 2 3');
if (result.success) {
console.log('Command succeeded:', result.result);
} else {
console.error('Command failed:', result.error);
}
```
#### Prompt 系统 — 等待玩家输入
某些命令需要等待玩家输入(如选择落子位置)。使用 `this.prompt()``promptQueue`
```ts
// 方法 1内部使用命令处理器中
registration.add('turn <player>', async function(cmd) {
const player = cmd.params[0] as string;
// 等待玩家输入,带验证
const playCmd = await this.prompt(
'play <row:number> <col:number>',
(command) => {
const [row, col] = command.params;
if (isCellOccupied(this.context.value.parts, 'board', [row, col])) {
return `Cell (${row}, ${col}) is occupied`;
}
return null;
}
);
// 继续处理...
const [row, col] = playCmd.params;
placePiece(this.context, row, col, player);
});
// 方法 2外部使用UI/网络层)
const game = createGameContext(registry, initialState);
// 启动需要玩家输入的命令
const runPromise = game.commands.run('turn X');
// 等待 prompt 事件
const promptEvent = await game.commands.promptQueue.pop();
console.log('Expected input:', promptEvent.schema);
// 提交玩家输入
const error = promptEvent.tryCommit('play 2 3');
if (error) {
console.log('Invalid move:', error);
// 可以再次尝试
const error2 = promptEvent.tryCommit('play 1 1');
} else {
// 输入已接受,命令继续执行
const result = await runPromise;
}
// 或者取消
promptEvent.cancel('player quit');
```
### 6. Random Number Generation — 确定性随机数
使用 Mulberry32 算法提供可重现的随机数生成。
```ts
import { createRNG } from 'boardgame-core';
// 创建 RNG可选种子
const rng = createRNG(12345);
// 获取 [0, 1) 随机数
const r1 = rng.next();
// 获取 [0, max) 随机数
const r2 = rng.next(100);
// 获取 [0, max) 随机整数
const dice = rng.nextInt(6); // 0-5
// 重新设置种子
rng.setSeed(999);
// 获取当前种子
const seed = rng.getSeed();
```
## 完整示例:井字棋
```ts
import {
createGameCommandRegistry,
createRegion,
createPart,
MutableSignal,
isCellOccupied as isCellOccupiedUtil,
} from 'boardgame-core';
const BOARD_SIZE = 3;
type PlayerType = 'X' | 'O';
type TicTacToePart = Part<{ player: PlayerType }>;
export function createInitialState() {
return {
board: createRegion('board', [
{ name: 'x', min: 0, max: BOARD_SIZE - 1 },
{ name: 'y', min: 0, max: BOARD_SIZE - 1 },
]),
parts: [] as TicTacToePart[],
currentPlayer: 'X' as PlayerType,
winner: null as PlayerType | 'draw' | null,
turn: 0,
};
}
export type TicTacToeState = ReturnType<typeof createInitialState>;
const registration = createGameCommandRegistry<TicTacToeState>();
export const registry = registration.registry;
// 游戏主循环
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: PlayerType | 'draw' | null}>(
`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;
});
// 单个回合
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 >= BOARD_SIZE || col < 0 || col >= BOARD_SIZE) {
return `Invalid position: (${row}, ${col})`;
}
if (isCellOccupied(this.context, row, col)) {
return `Cell (${row}, ${col}) is occupied`;
}
return null;
}
);
const [player, row, col] = playCmd.params as [PlayerType, number, number];
// 放置棋子
const piece = createPart(
{ regionId: 'board', position: [row, col], player },
`piece-${player}-${turnNumber}`
);
this.context.produce(state => {
state.parts.push(piece);
state.board.childIds.push(piece.id);
state.board.partMap[`${row},${col}`] = piece.id;
});
// 检查胜负
const winner = checkWinner(this.context);
if (winner) return { winner };
if (turnNumber >= BOARD_SIZE * BOARD_SIZE) return { winner: 'draw' };
return { winner: null };
});
function isCellOccupied(host: MutableSignal<TicTacToeState>, row: number, col: number): boolean {
return isCellOccupiedUtil(host.value.parts, 'board', [row, col]);
}
function checkWinner(host: MutableSignal<TicTacToeState>): PlayerType | 'draw' | null {
// 实现胜负判断...
return null;
}
```
## 从模块创建游戏上下文
```ts
import { createGameContextFromModule } from 'boardgame-core';
import * as ticTacToe from './tic-tac-toe';
const game = createGameContextFromModule(ticTacToe);
// 等同于:
// const game = createGameContext(ticTacToe.registry, ticTacToe.createInitialState());
```
## API 参考
### 核心
| 导出 | 说明 |
|---|---|
| `IGameContext<TState>` | 游戏上下文接口(包含 state 和 commands |
| `createGameContext(registry, initialState?)` | 创建游戏上下文实例 |
| `createGameContextFromModule(module)` | 从模块registry + createInitialState创建游戏上下文 |
| `createGameCommandRegistry<TState>()` | 创建命令注册表(带 `.add()` 链式 API |
### MutableSignal
| 导出 | 说明 |
|---|---|
| `MutableSignal<T>` | 响应式信号类型,扩展 Preact Signal |
| `mutableSignal(initial?)` | 创建 MutableSignal |
### Part
| 导出 | 说明 |
|---|---|
| `Part<TMeta>` | 部件类型 |
| `PartTemplate<TMeta>` | 部件模板(创建时排除 id |
| `PartPool<TMeta>` | 部件池draw/return/remaining |
| `createPart(template, id)` | 创建单个部件 |
| `createParts(template, count, idPrefix)` | 批量创建部件 |
| `createPartPool(template, count, idPrefix)` | 创建部件池 |
| `mergePartPools(...pools)` | 合并部件池 |
| `findPartById(parts, id)` | 按 ID 查找 |
| `isCellOccupied(parts, regionId, position)` | 检查位置占用 |
| `getPartAtPosition(parts, regionId, position)` | 获取位置上的部件 |
| `flip(part)` | 翻面 |
| `flipTo(part, side)` | 翻到指定面 |
| `roll(part, rng)` | 随机面 |
### Region
| 导出 | 说明 |
|---|---|
| `Region` | 区域类型 |
| `RegionAxis` | 坐标轴定义 |
| `createRegion(id, axes)` | 创建区域 |
| `applyAlign(region, parts)` | 对齐/紧凑排列 |
| `shuffle(region, parts, rng)` | 随机打乱位置 |
| `moveToRegion(part, sourceRegion?, targetRegion, position?)` | 移动部件 |
| `moveToRegionAll(parts, sourceRegion?, targetRegion, positions?)` | 批量移动 |
| `removeFromRegion(part, region)` | 移除部件 |
### Command
| 导出 | 说明 |
|---|---|
| `Command` | 命令对象name, params, options, flags |
| `CommandSchema` | 命令模式定义 |
| `CommandResult<T>` | 命令结果(成功/失败联合类型) |
| `parseCommand(input)` | 解析命令字符串 |
| `parseCommandSchema(schema)` | 解析模式字符串 |
| `validateCommand(cmd, schema)` | 验证命令 |
| `parseCommandWithSchema(cmd, schema)` | 解析并验证 |
| `createCommandRegistry<TContext>()` | 创建命令注册表 |
| `registerCommand(registry, runner)` | 注册命令 |
| `hasCommand(registry, name)` | 检查命令是否存在 |
| `runCommand(registry, context, input)` | 解析并运行命令 |
| `PromptEvent` | Prompt 事件tryCommit/cancel |
| `CommandRunnerContext<TContext>` | 命令运行器上下文 |
### Utilities
| 导出 | 说明 |
|---|---|
| `createRNG(seed?)` | 创建随机数生成器 |
| `Mulberry32RNG` | Mulberry32 PRNG 类 |
| `AsyncQueue<T>` | 异步队列(用于 promptQueue |
## 最佳实践
### 状态更新
- **总是使用 `produce()`** 更新状态,不要直接修改 `.value`
- `produce()` 内部是 draft 模式,可以直接修改属性
```ts
// ✅ 正确
state.produce(draft => {
draft.score += 1;
draft.parts.push(newPart);
});
// ❌ 错误 — 会破坏响应式
state.value.score = 10;
```
### 命令设计
- **命令应该是纯操作**:只修改状态,不处理 UI
- **使用 schema 验证**:在命令定义中声明参数类型
- **使用 prompt 处理玩家输入**:不要假设输入顺序
```ts
// ✅ 好的命令设计
registration.add('move <from> <to>', async function(cmd) {
// 只负责移动逻辑
});
// ❌ 避免在命令中处理 UI
registration.add('show-alert', async function(cmd) {
alert('Hello'); // 不要这样做
});
```
### 错误处理
- **命令返回 `CommandResult`**:使用 `{ success, result/error }` 模式
- **使用 try/catch 包装外部调用**:捕获错误并返回失败结果
```ts
registration.add('risky-op', async function(cmd) {
try {
// 可能失败的操作
const data = await fetchSomething();
return { success: true, data };
} catch (e) {
const error = e as Error;
return { success: false, error: error.message };
}
});
```
### Prompt 验证器
- **验证器返回 `null` 表示有效**
- **验证器返回 `string` 表示错误消息**
```ts
const cmd = await this.prompt(
'play <row:number>',
(command) => {
const [row] = command.params;
if (row < 0 || row > 2) {
return `Row must be 0-2, got ${row}`;
}
return null; // 验证通过
}
);
```
### 确定性游戏
- **使用固定种子的 RNG**:保证游戏可重现
- **所有随机操作都通过 RNG**:不要用 `Math.random()`
```ts
const rng = createRNG(12345); // 固定种子
shuffle(deck, parts, rng); // 使用 RNG 打乱
roll(dice, rng); // 使用 RNG 掷骰子
```
## 与 boardgame-phaser 集成
在 boardgame-phaser 中使用 boardgame-core
```ts
import { createGameContextFromModule } from 'boardgame-core';
import { ReactiveScene } from 'boardgame-phaser';
import * as ticTacToe from './tic-tac-toe';
class TicTacToeScene extends ReactiveScene<typeof ticTacToe.createInitialState> {
protected onStateReady() {
// 初始化 Phaser 对象
}
protected setupBindings() {
// 绑定信号到 Phaser 对象
bindSignal(this, () => this.gameContext.state.value.winner, (winner) => {
if (winner) this.showWinMessage(winner);
});
}
}
// 创建场景
const gameContext = createGameContextFromModule(ticTacToe);
const scene = new TicTacToeScene(gameContext);
```