# boardgame-core 使用指南 ## 概述 `boardgame-core` 是一个基于 Preact Signals 的桌游状态管理库,提供响应式状态、实体集合、空间区域系统和命令驱动的游戏循环。 ## 核心概念 ### 1. MutableSignal — 响应式状态容器 `MutableSignal` 扩展了 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 // 注意:parts 现在是 Record 格式 const parts: Record = { ... }; // 对齐/紧凑排列(根据 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` 表示游戏中的一个部件(棋子、卡牌等)。 ```ts import { createPart, createParts, createPartPool, flip, flipTo, roll, mergePartPools } from 'boardgame-core'; // 创建单个部件 const piece = createPart( { regionId: 'board', position: [0, 0], player: 'X' }, 'piece-1' ); // 批量创建(生成 piece-pawn-1, piece-pawn-2, ...) 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'; // Parts 现在是 Record 格式 const parts: Record = { 'piece-1': piece1, 'piece-2': piece2, }; // 按 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 [--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(); export const registry = registration.registry; // 注册命令(链式 API) registration.add('place ', 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 ', async function(cmd) { // ... }); ``` #### 命令处理器的 `this` 上下文 命令处理函数中,`this` 是 `CommandRunnerContext`,提供: ```ts 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 ', (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) 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 ', async function(cmd) { const player = cmd.params[0] as string; // 等待玩家输入,带验证 const playCmd = await this.prompt( 'play ', (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 Record, currentPlayer: 'X' as PlayerType, winner: null as PlayerType | 'draw' | null, turn: 0, }; } export type TicTacToeState = ReturnType; const registration = createGameCommandRegistry(); 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 ', 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.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[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 }; }); function isCellOccupied(host: MutableSignal, row: number, col: number): boolean { return isCellOccupiedUtil(host.value.parts, 'board', [row, col]); } function checkWinner(host: MutableSignal): 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` | 游戏上下文接口(包含 state 和 commands) | | `createGameContext(registry, initialState?)` | 创建游戏上下文实例 | | `createGameContextFromModule(module)` | 从模块(registry + createInitialState)创建游戏上下文 | | `createGameCommandRegistry()` | 创建命令注册表(带 `.add()` 链式 API) | ### MutableSignal | 导出 | 说明 | |---|---| | `MutableSignal` | 响应式信号类型,扩展 Preact Signal | | `mutableSignal(initial?)` | 创建 MutableSignal | ### Part | 导出 | 说明 | |---|---| | `Part` | 部件类型 | | `PartTemplate` | 部件模板(创建时排除 id) | | `PartPool` | 部件池(draw/return/remaining) | | `createPart(template, id)` | 创建单个部件 | | `createParts(template, count, idPrefix)` | 批量创建部件(ID 从 1 开始:`prefix-1`, `prefix-2`, ...) | | `createPartPool(template, count, idPrefix)` | 创建部件池 | | `mergePartPools(...pools)` | 合并部件池 | | `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)` | 随机面 | ### Region | 导出 | 说明 | |---|---| | `Region` | 区域类型 | | `RegionAxis` | 坐标轴定义 | | `createRegion(id, axes)` | 创建区域 | | `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 | 导出 | 说明 | |---|---| | `Command` | 命令对象(name, params, options, flags) | | `CommandSchema` | 命令模式定义 | | `CommandResult` | 命令结果(成功/失败联合类型) | | `parseCommand(input)` | 解析命令字符串 | | `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 | 导出 | 说明 | |---|---| | `createRNG(seed?)` | 创建随机数生成器 | | `Mulberry32RNG` | Mulberry32 PRNG 类 | | `AsyncQueue` | 异步队列(用于 promptQueue) | ## 最佳实践 ### 状态更新 - **总是使用 `produce()`** 更新状态,不要直接修改 `.value` - `produce()` 内部是 draft 模式,可以直接修改属性 - **Parts 使用 Record 格式**,通过 ID 作为键访问 ```ts // ✅ 正确 state.produce(draft => { draft.score += 1; draft.parts[newPart.id] = newPart; }); // ❌ 错误 — 会破坏响应式 state.value.score = 10; // ❌ 错误 — parts 是 Record 不是数组 state.parts.push(newPart); ``` ### 命令设计 - **命令应该是纯操作**:只修改状态,不处理 UI - **使用 schema 验证**:在命令定义中声明参数类型 - **使用 prompt 处理玩家输入**:不要假设输入顺序 ```ts // ✅ 好的命令设计 registration.add('move ', 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 ', (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, bindRegion, bindSignal } from 'boardgame-phaser'; import * as ticTacToe from './tic-tac-toe'; type GameState = ReturnType; class TicTacToeScene extends ReactiveScene { protected onStateReady() { // 初始化 Phaser 对象 } protected setupBindings() { // 绑定简单信号值 bindSignal(this.gameContext.state, state => state.winner, winner => { if (winner) this.showWinMessage(winner); }); // 绑定区域(响应 parts 数组变化) bindRegion( this.gameContext.state, state => state.parts, this.gameContext.state.value.board, { cellSize: { x: 100, y: 100 }, offset: { x: 50, y: 50 }, factory: (part, pos) => { return this.add.text(pos.x, pos.y, part.player); }, }, this.boardContainer, ); } } // 创建场景 const gameContext = createGameContextFromModule(ticTacToe); const scene = new TicTacToeScene(gameContext); ``` **bindRegion 参数说明:** - `state` — MutableSignal 游戏状态 - `state => state.parts` — 获取 parts 数组的 getter(effect 会追踪此依赖) - `region` — Region 对象 - `options` — 配置项(cellSize, offset, factory) - `container` — Phaser 容器