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