docs: sync updated boardgame-core

This commit is contained in:
hypercross 2026-04-03 17:45:56 +08:00
parent 4d4889f825
commit cbee709a27
2 changed files with 51 additions and 36 deletions

View File

@ -57,6 +57,9 @@ const hand = createRegion('hand', [
**区域操作:** **区域操作:**
```ts ```ts
// 注意parts 现在是 Record<string, Part> 格式
const parts: Record<string, Part> = { ... };
// 对齐/紧凑排列(根据 axis.align 自动调整位置) // 对齐/紧凑排列(根据 axis.align 自动调整位置)
applyAlign(hand, parts); applyAlign(hand, parts);
@ -78,7 +81,7 @@ removeFromRegion(part, region);
`Part<TMeta>` 表示游戏中的一个部件(棋子、卡牌等)。 `Part<TMeta>` 表示游戏中的一个部件(棋子、卡牌等)。
```ts ```ts
import { createPart, createParts, createPartPool, flip, flipTo, roll } from 'boardgame-core'; import { createPart, createParts, createPartPool, flip, flipTo, roll, mergePartPools } from 'boardgame-core';
// 创建单个部件 // 创建单个部件
const piece = createPart( const piece = createPart(
@ -86,7 +89,7 @@ const piece = createPart(
'piece-1' 'piece-1'
); );
// 批量创建(生成 piece-pawn-0, piece-pawn-1, ... // 批量创建(生成 piece-pawn-1, piece-pawn-2, ...
const pawns = createParts( const pawns = createParts(
{ regionId: 'supply', position: [0, 0] }, { regionId: 'supply', position: [0, 0] },
8, 8,
@ -141,6 +144,12 @@ roll(dice, rng);
```ts ```ts
import { findPartById, isCellOccupied, getPartAtPosition } from 'boardgame-core'; import { findPartById, isCellOccupied, getPartAtPosition } from 'boardgame-core';
// Parts 现在是 Record<string, Part> 格式
const parts: Record<string, Part> = {
'piece-1': piece1,
'piece-2': piece2,
};
// 按 ID 查找 // 按 ID 查找
const piece = findPartById(parts, 'piece-1'); const piece = findPartById(parts, 'piece-1');
@ -213,18 +222,18 @@ registration.add('turn <player> <turn:number>', async function(cmd) {
registration.add('my-command <arg>', async function(cmd) { registration.add('my-command <arg>', async function(cmd) {
// this.context — 游戏上下文MutableSignal<GameState> // this.context — 游戏上下文MutableSignal<GameState>
const state = this.context.value; const state = this.context.value;
// this.context.produce — 更新状态 // this.context.produce — 更新状态
this.context.produce(draft => { this.context.produce(draft => {
draft.score += 1; draft.score += 1;
}); });
// this.run — 运行子命令 // this.run — 运行子命令
const result = await this.run<PlaceResult>(`place 2 3`); const result = await this.run<PlaceResult>(`place 2 3`);
if (result.success) { if (result.success) {
console.log(result.result.row); console.log(result.result.row);
} }
// this.prompt — 等待玩家输入 // this.prompt — 等待玩家输入
const playCmd = await this.prompt( const playCmd = await this.prompt(
'play <player> <row:number> <col:number>', 'play <player> <row:number> <col:number>',
@ -237,7 +246,7 @@ registration.add('my-command <arg>', async function(cmd) {
return null; return null;
} }
); );
return { success: true }; return { success: true };
}); });
``` ```
@ -267,7 +276,7 @@ if (result.success) {
// 方法 1内部使用命令处理器中 // 方法 1内部使用命令处理器中
registration.add('turn <player>', async function(cmd) { registration.add('turn <player>', async function(cmd) {
const player = cmd.params[0] as string; const player = cmd.params[0] as string;
// 等待玩家输入,带验证 // 等待玩家输入,带验证
const playCmd = await this.prompt( const playCmd = await this.prompt(
'play <row:number> <col:number>', 'play <row:number> <col:number>',
@ -279,7 +288,7 @@ registration.add('turn <player>', async function(cmd) {
return null; return null;
} }
); );
// 继续处理... // 继续处理...
const [row, col] = playCmd.params; const [row, col] = playCmd.params;
placePiece(this.context, row, col, player); placePiece(this.context, row, col, player);
@ -358,7 +367,7 @@ export function createInitialState() {
{ name: 'x', min: 0, max: BOARD_SIZE - 1 }, { name: 'x', min: 0, max: BOARD_SIZE - 1 },
{ name: 'y', min: 0, max: BOARD_SIZE - 1 }, { name: 'y', min: 0, max: BOARD_SIZE - 1 },
]), ]),
parts: [] as TicTacToePart[], parts: {} as Record<string, TicTacToePart>,
currentPlayer: 'X' as PlayerType, currentPlayer: 'X' as PlayerType,
winner: null as PlayerType | 'draw' | null, winner: null as PlayerType | 'draw' | null,
turn: 0, turn: 0,
@ -376,12 +385,12 @@ registration.add('setup', async function() {
while (true) { while (true) {
const currentPlayer = context.value.currentPlayer; const currentPlayer = context.value.currentPlayer;
const turnNumber = context.value.turn + 1; const turnNumber = context.value.turn + 1;
const turnOutput = await this.run<{winner: PlayerType | 'draw' | null}>( const turnOutput = await this.run<{winner: PlayerType | 'draw' | null}>(
`turn ${currentPlayer} ${turnNumber}` `turn ${currentPlayer} ${turnNumber}`
); );
if (!turnOutput.success) throw new Error(turnOutput.error); if (!turnOutput.success) throw new Error(turnOutput.error);
context.produce(state => { context.produce(state => {
state.winner = turnOutput.result.winner; state.winner = turnOutput.result.winner;
if (!state.winner) { if (!state.winner) {
@ -389,7 +398,7 @@ registration.add('setup', async function() {
state.turn = turnNumber; state.turn = turnNumber;
} }
}); });
if (context.value.winner) break; if (context.value.winner) break;
} }
return context.value; return context.value;
@ -398,44 +407,44 @@ registration.add('setup', async function() {
// 单个回合 // 单个回合
registration.add('turn <player> <turn:number>', async function(cmd) { registration.add('turn <player> <turn:number>', async function(cmd) {
const [turnPlayer, turnNumber] = cmd.params as [PlayerType, number]; const [turnPlayer, turnNumber] = cmd.params as [PlayerType, number];
// 等待玩家输入 // 等待玩家输入
const playCmd = await this.prompt( const playCmd = await this.prompt(
'play <player> <row:number> <col:number>', 'play <player> <row:number> <col:number>',
(command) => { (command) => {
const [player, row, col] = command.params as [PlayerType, number, number]; const [player, row, col] = command.params as [PlayerType, number, number];
if (player !== turnPlayer) { if (player !== turnPlayer) {
return `Invalid player: ${player}`; return `Invalid player: ${player}`;
} }
if (row < 0 || row >= BOARD_SIZE || col < 0 || col >= BOARD_SIZE) { if (row < 0 || row >= BOARD_SIZE || col < 0 || col >= BOARD_SIZE) {
return `Invalid position: (${row}, ${col})`; return `Invalid position: (${row}, ${col})`;
} }
if (isCellOccupied(this.context, row, col)) { if (isCellOccupied(this.context.value.parts, 'board', [row, col])) {
return `Cell (${row}, ${col}) is occupied`; return `Cell (${row}, ${col}) is occupied`;
} }
return null; return null;
} }
); );
const [player, row, col] = playCmd.params as [PlayerType, number, number]; const [player, row, col] = playCmd.params as [PlayerType, number, number];
// 放置棋子 // 放置棋子
const piece = createPart( const piece = createPart(
{ regionId: 'board', position: [row, col], player }, { regionId: 'board', position: [row, col], player },
`piece-${player}-${turnNumber}` `piece-${player}-${turnNumber}`
); );
this.context.produce(state => { this.context.produce(state => {
state.parts.push(piece); state.parts[piece.id] = piece;
state.board.childIds.push(piece.id); state.board.childIds.push(piece.id);
state.board.partMap[`${row},${col}`] = piece.id; state.board.partMap[`${row},${col}`] = piece.id;
}); });
// 检查胜负 // 检查胜负
const winner = checkWinner(this.context); const winner = checkWinner(this.context);
if (winner) return { winner }; if (winner) return { winner };
if (turnNumber >= BOARD_SIZE * BOARD_SIZE) return { winner: 'draw' }; if (turnNumber >= BOARD_SIZE * BOARD_SIZE) return { winner: 'draw' };
return { winner: null }; return { winner: null };
}); });
@ -486,12 +495,12 @@ const game = createGameContextFromModule(ticTacToe);
| `PartTemplate<TMeta>` | 部件模板(创建时排除 id | | `PartTemplate<TMeta>` | 部件模板(创建时排除 id |
| `PartPool<TMeta>` | 部件池draw/return/remaining | | `PartPool<TMeta>` | 部件池draw/return/remaining |
| `createPart(template, id)` | 创建单个部件 | | `createPart(template, id)` | 创建单个部件 |
| `createParts(template, count, idPrefix)` | 批量创建部件 | | `createParts(template, count, idPrefix)` | 批量创建部件ID 从 1 开始:`prefix-1`, `prefix-2`, ... |
| `createPartPool(template, count, idPrefix)` | 创建部件池 | | `createPartPool(template, count, idPrefix)` | 创建部件池 |
| `mergePartPools(...pools)` | 合并部件池 | | `mergePartPools(...pools)` | 合并部件池 |
| `findPartById(parts, id)` | 按 ID 查找 | | `findPartById(parts, id)` | 按 ID 查找`parts` 为 `Record<string, Part>` |
| `isCellOccupied(parts, regionId, position)` | 检查位置占用 | | `isCellOccupied(parts, regionId, position)` | 检查位置占用`parts` 为 `Record<string, Part>` |
| `getPartAtPosition(parts, regionId, position)` | 获取位置上的部件 | | `getPartAtPosition(parts, regionId, position)` | 获取位置上的部件`parts` 为 `Record<string, Part>` |
| `flip(part)` | 翻面 | | `flip(part)` | 翻面 |
| `flipTo(part, side)` | 翻到指定面 | | `flipTo(part, side)` | 翻到指定面 |
| `roll(part, rng)` | 随机面 | | `roll(part, rng)` | 随机面 |
@ -503,10 +512,10 @@ const game = createGameContextFromModule(ticTacToe);
| `Region` | 区域类型 | | `Region` | 区域类型 |
| `RegionAxis` | 坐标轴定义 | | `RegionAxis` | 坐标轴定义 |
| `createRegion(id, axes)` | 创建区域 | | `createRegion(id, axes)` | 创建区域 |
| `applyAlign(region, parts)` | 对齐/紧凑排列 | | `applyAlign(region, parts)` | 对齐/紧凑排列`parts` 为 `Record<string, Part>` |
| `shuffle(region, parts, rng)` | 随机打乱位置 | | `shuffle(region, parts, rng)` | 随机打乱位置`parts` 为 `Record<string, Part>` |
| `moveToRegion(part, sourceRegion?, targetRegion, position?)` | 移动部件 | | `moveToRegion(part, sourceRegion?, targetRegion, position?)` | 移动部件`sourceRegion` 可选) |
| `moveToRegionAll(parts, sourceRegion?, targetRegion, positions?)` | 批量移动 | | `moveToRegionAll(parts, sourceRegion?, targetRegion, positions?)` | 批量移动`parts` 为 `Record<string, Part>``sourceRegion` 可选) |
| `removeFromRegion(part, region)` | 移除部件 | | `removeFromRegion(part, region)` | 移除部件 |
### Command ### Command
@ -520,12 +529,18 @@ const game = createGameContextFromModule(ticTacToe);
| `parseCommandSchema(schema)` | 解析模式字符串 | | `parseCommandSchema(schema)` | 解析模式字符串 |
| `validateCommand(cmd, schema)` | 验证命令 | | `validateCommand(cmd, schema)` | 验证命令 |
| `parseCommandWithSchema(cmd, schema)` | 解析并验证 | | `parseCommandWithSchema(cmd, schema)` | 解析并验证 |
| `applyCommandSchema(cmd, schema)` | 应用模式验证并返回验证后的命令 |
| `createCommandRegistry<TContext>()` | 创建命令注册表 | | `createCommandRegistry<TContext>()` | 创建命令注册表 |
| `registerCommand(registry, runner)` | 注册命令 | | `registerCommand(registry, runner)` | 注册命令 |
| `unregisterCommand(registry, name)` | 取消注册命令 |
| `hasCommand(registry, name)` | 检查命令是否存在 | | `hasCommand(registry, name)` | 检查命令是否存在 |
| `getCommand(registry, name)` | 获取命令处理器 |
| `runCommand(registry, context, input)` | 解析并运行命令 | | `runCommand(registry, context, input)` | 解析并运行命令 |
| `runCommandParsed(registry, context, command)` | 运行已解析的命令 |
| `createCommandRunnerContext(registry, context)` | 创建命令运行器上下文 |
| `PromptEvent` | Prompt 事件tryCommit/cancel | | `PromptEvent` | Prompt 事件tryCommit/cancel |
| `CommandRunnerContext<TContext>` | 命令运行器上下文 | | `CommandRunnerContext<TContext>` | 命令运行器上下文 |
| `CommandRunnerContextExport<TContext>` | 导出的命令运行器上下文(含 `promptQueue` |
### Utilities ### Utilities
@ -541,16 +556,20 @@ const game = createGameContextFromModule(ticTacToe);
- **总是使用 `produce()`** 更新状态,不要直接修改 `.value` - **总是使用 `produce()`** 更新状态,不要直接修改 `.value`
- `produce()` 内部是 draft 模式,可以直接修改属性 - `produce()` 内部是 draft 模式,可以直接修改属性
- **Parts 使用 Record 格式**,通过 ID 作为键访问
```ts ```ts
// ✅ 正确 // ✅ 正确
state.produce(draft => { state.produce(draft => {
draft.score += 1; draft.score += 1;
draft.parts.push(newPart); draft.parts[newPart.id] = newPart;
}); });
// ❌ 错误 — 会破坏响应式 // ❌ 错误 — 会破坏响应式
state.value.score = 10; state.value.score = 10;
// ❌ 错误 — parts 是 Record 不是数组
state.parts.push(newPart);
``` ```
### 命令设计 ### 命令设计

View File

@ -39,11 +39,10 @@ export function bindRegion<TState, TMeta>(
container: Phaser.GameObjects.Container, container: Phaser.GameObjects.Container,
): { cleanup: () => void; objects: Map<string, Phaser.GameObjects.GameObject> } { ): { cleanup: () => void; objects: Map<string, Phaser.GameObjects.GameObject> } {
const objects = new Map<string, Phaser.GameObjects.GameObject>(); const objects = new Map<string, Phaser.GameObjects.GameObject>();
const effects: DisposeFn[] = [];
const offset = options.offset ?? { x: 0, y: 0 }; const offset = options.offset ?? { x: 0, y: 0 };
function syncParts() { const dispose = effect(function(this: { dispose: () => void }) {
const parts = partsGetter(state.value); const parts = partsGetter(state.value);
const currentIds = new Set(region.childIds); const currentIds = new Set(region.childIds);
for (const [id, obj] of objects) { for (const [id, obj] of objects) {
@ -73,14 +72,11 @@ export function bindRegion<TState, TMeta>(
} }
} }
} }
} });
const e = effect(syncParts);
effects.push(e);
return { return {
cleanup: () => { cleanup: () => {
for (const e of effects) e(); dispose();
for (const [, obj] of objects) obj.destroy(); for (const [, obj] of objects) obj.destroy();
objects.clear(); objects.clear();
}, },