diff --git a/README.md b/README.md index c677389..52c61b2 100644 --- a/README.md +++ b/README.md @@ -13,522 +13,37 @@ - **游戏生命周期管理**:`GameHost` 类提供清晰的游戏设置/重置/销毁生命周期 - **确定性 RNG**:Mulberry32 种子伪随机数生成器,用于可复现的游戏状态 -## 安装 +## 快速开始 ```bash npm install boardgame-core ``` ---- - -## 使用 GameHost - -`GameHost` 是游戏运行的核心容器,负责管理游戏状态、命令执行和玩家交互的生命周期。 - -### 创建 GameHost - -通过 `createGameHost` 传入一个 GameModule 来创建: - ```ts import { createGameHost } from 'boardgame-core'; import * as tictactoe from 'boardgame-core/samples/tic-tac-toe'; const host = createGameHost(tictactoe); -``` - -### 响应式状态 - -GameHost 暴露的所有属性都是响应式 Signal,可以直接用于 UI 渲染或 `effect()`: - -```ts -import { effect } from '@preact/signals-core'; - -// 游戏状态 -effect(() => { - console.log(host.state.value.currentPlayer); - console.log(host.state.value.winner); -}); - -// 生命周期状态: 'created' | 'running' | 'disposed' -effect(() => { - console.log('Status:', host.status.value); -}); - -// 当前等待的玩家输入 schema -effect(() => { - const schema = host.activePromptSchema.value; - if (schema) { - console.log('Waiting for:', schema.name, schema.params); - } -}); - -// 当前等待的玩家 -effect(() => { - console.log('Current player prompt:', host.activePromptPlayer.value); -}); -``` - -### 启动游戏 - -调用 `setup()` 并传入初始化命令名来启动游戏: - -```ts await host.setup('setup'); -``` - -这会重置游戏状态、取消当前活动提示、运行指定的 setup 命令,并将状态设为 `'running'`。 - -### 处理玩家输入 - -当命令通过 `this.prompt()` 等待玩家输入时,使用 `onInput()` 提交输入: - -```ts -// 提交玩家操作,返回错误信息或 null -const error = host.onInput('play X 1 2'); - -if (error) { - console.log('输入无效:', error); - // 玩家可以重新输入 -} else { - // 输入已被接受,命令继续执行 -} -``` - -### 监听事件 - -```ts -// 监听游戏设置完成 -host.on('setup', () => { - console.log('Game initialized'); -}); - -// 监听游戏销毁 -host.on('dispose', () => { - console.log('Game disposed'); -}); - -// on() 返回取消订阅函数 -const unsubscribe = host.on('setup', handler); -unsubscribe(); // 取消监听 -``` - -### 重新开始游戏 - -```ts -// 取消当前命令,重置状态,重新运行 setup 命令 -await host.setup('setup'); -``` - -### 销毁游戏 - -```ts -host.dispose(); -``` - -销毁后会取消所有活动命令、清理事件监听器,并将状态设为 `'disposed'`。销毁后无法再次使用。 - -### 完整示例 - -```ts -import { effect } from '@preact/signals-core'; -import { createGameHost } from 'boardgame-core'; -import * as tictactoe from 'boardgame-core/samples/tic-tac-toe'; - -const host = createGameHost(tictactoe); // 监听状态变化 effect(() => { const state = host.state.value; - console.log(`${state.currentPlayer}'s turn (turn ${state.turn + 1})`); - if (state.winner) { - console.log('Winner:', state.winner); - } + console.log(`${state.currentPlayer}'s turn`); }); -// 启动游戏 -await host.setup('setup'); - -// 游戏循环:等待提示 → 提交输入 -while (host.status.value === 'running' && host.activePromptSchema.value) { - const schema = host.activePromptSchema.value!; - console.log('Waiting for input:', schema.name); - - // 这里可以从 UI/网络等获取输入 - const input = await getPlayerInput(); - const error = host.onInput(input); - - if (error) { - console.log('Invalid:', error); - } -} - -// 游戏结束后可以重新开始 -// await host.setup('setup'); - -// 或彻底销毁 -// host.dispose(); +// 处理玩家输入 +const error = host.onInput('play X 1 2'); ``` ---- +## 文档导航 -## 编写 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, - currentPlayer: 'X' as PlayerType, - winner: null as WinnerType, - turn: 0, - }; -} - -// 2. 创建命令注册表并注册命令 -const registration = createGameCommandRegistry>(); -export const registry = registration.registry; - -// 注册命令 -registration.add('setup', async function () { - // ... 命令逻辑 -}); - -registration.add('play ', 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, - currentPlayer: 'X' as PlayerType, - winner: null as WinnerType, - turn: 0, - }; -} -export type GameState = ReturnType; -``` - -状态通常包含: -- **Region**:用 `createRegion()` 创建的空间区域 -- **parts**:`Record` 游戏棋子集合 -- 游戏特有的字段:当前玩家、分数、回合数等 - -### 注册命令 - -使用 `registration.add()` 注册命令。Schema 字符串定义了命令格式: - -```ts -registration.add('play ', async function (cmd) { - const [player, row, col] = cmd.params as [PlayerType, number, number]; - - // this.context 是 MutableSignal - this.context.produce(state => { - state.parts[piece.id] = piece; - }); - - return { winner: null }; -}); -``` - -#### Schema 语法 - -| 语法 | 含义 | +| 文档 | 说明 | |---|---| -| `name` | 命令名 | -| `` | 必填参数(字符串) | -| `` | 必填参数(自动转为数字) | -| `[--flag]` | 可选标志 | -| `[-x:number]` | 可选选项(带类型) | - -#### 命令处理器中的 this - -命令处理器中的 `this` 是 `CommandRunnerContext>`: - -```ts -registration.add('myCommand ', async function (cmd) { - // 读取状态 - const state = this.context.value; - - // 修改状态 - this.context.produce(draft => { - draft.currentPlayer = 'O'; - }); - - // 提示玩家输入 - const result = await this.prompt( - 'confirm ', - (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 ', 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 > 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 棋盘区域 -- 玩家轮流输入 -- 胜负判定 -- 完整的游戏循环 - ---- - -## API 参考 - -### 核心 - -| 导出 | 说明 | -|---|---| -| `IGameContext` | 游戏上下文基础接口 | -| `createGameContext(registry, initialState?)` | 创建游戏上下文实例 | -| `createGameCommandRegistry()` | 创建命令注册表,返回带 `.add()` 的对象 | -| `createGameModule(module)` | 辅助函数,标记一个对象为 GameModule | -| `GameHost` | 游戏生命周期管理类 | -| `createGameHost(module, options?)` | 从 GameModule 创建 GameHost | -| `GameHostStatus` | 类型: `'created' \| 'running' \| 'disposed'` | - -### 棋子 (Parts) - -| 导出 | 说明 | -|---|---| -| `Part` | 游戏棋子类型 | -| `PartTemplate` | 创建棋子的模板类型 | -| `PartPool` | 棋子池 | -| `createPart(template, id)` | 创建单个棋子 | -| `createParts(template, count, idPrefix)` | 批量创建相同棋子 | -| `createPartPool(template, count, idPrefix)` | 创建棋子池 | -| `mergePartPools(...pools)` | 合并多个棋子池 | -| `findPartById(parts, id)` | 按 ID 查找棋子 | -| `isCellOccupied(parts, regionId, position)` | 检查格子是否被占用 | -| `flip(part)` / `flipTo(part, side)` / `roll(part, rng)` | 翻面/随机面 | - -### 区域 (Regions) - -| 导出 | 说明 | -|---|---| -| `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)` | 从区域移除棋子 | - -### 命令 (Commands) - -| 导出 | 说明 | -|---|---| -| `parseCommand(input)` | 解析命令字符串 | -| `parseCommandSchema(schema)` | 解析 Schema 字符串 | -| `validateCommand(cmd, schema)` | 验证命令 | -| `Command` / `CommandSchema` / `CommandResult` | 命令相关类型 | -| `PromptEvent` | 玩家输入提示事件 | -| `createRNG(seed?)` | 创建种子 RNG | - ---- - -## 脚本 - -```bash -npm run build # 构建 ESM bundle + 类型声明到 dist/ -npm run test # 以 watch 模式运行测试 -npm run test:run # 运行测试一次 -npm run typecheck # TypeScript 类型检查 -``` +| [使用 GameHost](docs/game-host.md) | GameHost 生命周期、响应式状态、事件处理 | +| [编写 GameModule](docs/game-module.md) | 定义状态、注册命令、prompt 系统、Part/Region 使用 | +| [API 参考](docs/api-reference.md) | 所有导出 API 的完整列表 | +| [开发指南](docs/development.md) | 安装、构建脚本、测试命令 | ## License diff --git a/docs/api-reference.md b/docs/api-reference.md new file mode 100644 index 0000000..2908b38 --- /dev/null +++ b/docs/api-reference.md @@ -0,0 +1,51 @@ +# API 参考 + +## 核心 + +| 导出 | 说明 | +|---|---| +| `IGameContext` | 游戏上下文基础接口 | +| `createGameContext(registry, initialState?)` | 创建游戏上下文实例 | +| `createGameCommandRegistry()` | 创建命令注册表,返回带 `.add()` 的对象 | +| `createGameModule(module)` | 辅助函数,标记一个对象为 GameModule | +| `GameHost` | 游戏生命周期管理类 | +| `createGameHost(module, options?)` | 从 GameModule 创建 GameHost | +| `GameHostStatus` | 类型: `'created' \| 'running' \| 'disposed'` | + +## 棋子 (Parts) + +| 导出 | 说明 | +|---|---| +| `Part` | 游戏棋子类型 | +| `PartTemplate` | 创建棋子的模板类型 | +| `PartPool` | 棋子池 | +| `createPart(template, id)` | 创建单个棋子 | +| `createParts(template, count, idPrefix)` | 批量创建相同棋子 | +| `createPartPool(template, count, idPrefix)` | 创建棋子池 | +| `mergePartPools(...pools)` | 合并多个棋子池 | +| `findPartById(parts, id)` | 按 ID 查找棋子 | +| `isCellOccupied(parts, regionId, position)` | 检查格子是否被占用 | +| `flip(part)` / `flipTo(part, side)` / `roll(part, rng)` | 翻面/随机面 | + +## 区域 (Regions) + +| 导出 | 说明 | +|---|---| +| `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)` | 从区域移除棋子 | + +## 命令 (Commands) + +| 导出 | 说明 | +|---|---| +| `parseCommand(input)` | 解析命令字符串 | +| `parseCommandSchema(schema)` | 解析 Schema 字符串 | +| `validateCommand(cmd, schema)` | 验证命令 | +| `Command` / `CommandSchema` / `CommandResult` | 命令相关类型 | +| `PromptEvent` | 玩家输入提示事件 | +| `createRNG(seed?)` | 创建种子 RNG | diff --git a/docs/development.md b/docs/development.md new file mode 100644 index 0000000..38dd8a5 --- /dev/null +++ b/docs/development.md @@ -0,0 +1,32 @@ +# 开发指南 + +## 安装 + +```bash +npm install boardgame-core +``` + +## 脚本 + +```bash +npm run build # 构建 ESM bundle + 类型声明到 dist/ +npm run test # 以 watch 模式运行测试 +npm run test:run # 运行测试一次 +npm run typecheck # TypeScript 类型检查 +``` + +### 运行单个测试文件 + +```bash +npx vitest run tests/samples/tic-tac-toe.test.ts +``` + +### 按名称运行单个测试 + +```bash +npx vitest run -t "should detect horizontal win for X" +``` + +## License + +MIT diff --git a/docs/game-host.md b/docs/game-host.md new file mode 100644 index 0000000..d7cbacc --- /dev/null +++ b/docs/game-host.md @@ -0,0 +1,147 @@ +# 使用 GameHost + +`GameHost` 是游戏运行的核心容器,负责管理游戏状态、命令执行和玩家交互的生命周期。 + +## 创建 GameHost + +通过 `createGameHost` 传入一个 GameModule 来创建: + +```ts +import { createGameHost } from 'boardgame-core'; +import * as tictactoe from 'boardgame-core/samples/tic-tac-toe'; + +const host = createGameHost(tictactoe); +``` + +## 响应式状态 + +GameHost 暴露的所有属性都是响应式 Signal,可以直接用于 UI 渲染或 `effect()`: + +```ts +import { effect } from '@preact/signals-core'; + +// 游戏状态 +effect(() => { + console.log(host.state.value.currentPlayer); + console.log(host.state.value.winner); +}); + +// 生命周期状态: 'created' | 'running' | 'disposed' +effect(() => { + console.log('Status:', host.status.value); +}); + +// 当前等待的玩家输入 schema +effect(() => { + const schema = host.activePromptSchema.value; + if (schema) { + console.log('Waiting for:', schema.name, schema.params); + } +}); + +// 当前等待的玩家 +effect(() => { + console.log('Current player prompt:', host.activePromptPlayer.value); +}); +``` + +## 启动游戏 + +调用 `setup()` 并传入初始化命令名来启动游戏: + +```ts +await host.setup('setup'); +``` + +这会重置游戏状态、取消当前活动提示、运行指定的 setup 命令,并将状态设为 `'running'`。 + +## 处理玩家输入 + +当命令通过 `this.prompt()` 等待玩家输入时,使用 `onInput()` 提交输入: + +```ts +// 提交玩家操作,返回错误信息或 null +const error = host.onInput('play X 1 2'); + +if (error) { + console.log('输入无效:', error); + // 玩家可以重新输入 +} else { + // 输入已被接受,命令继续执行 +} +``` + +## 监听事件 + +```ts +// 监听游戏设置完成 +host.on('setup', () => { + console.log('Game initialized'); +}); + +// 监听游戏销毁 +host.on('dispose', () => { + console.log('Game disposed'); +}); + +// on() 返回取消订阅函数 +const unsubscribe = host.on('setup', handler); +unsubscribe(); // 取消监听 +``` + +## 重新开始游戏 + +```ts +// 取消当前命令,重置状态,重新运行 setup 命令 +await host.setup('setup'); +``` + +## 销毁游戏 + +```ts +host.dispose(); +``` + +销毁后会取消所有活动命令、清理事件监听器,并将状态设为 `'disposed'`。销毁后无法再次使用。 + +## 完整示例 + +```ts +import { effect } from '@preact/signals-core'; +import { createGameHost } from 'boardgame-core'; +import * as tictactoe from 'boardgame-core/samples/tic-tac-toe'; + +const host = createGameHost(tictactoe); + +// 监听状态变化 +effect(() => { + const state = host.state.value; + console.log(`${state.currentPlayer}'s turn (turn ${state.turn + 1})`); + if (state.winner) { + console.log('Winner:', state.winner); + } +}); + +// 启动游戏 +await host.setup('setup'); + +// 游戏循环:等待提示 → 提交输入 +while (host.status.value === 'running' && host.activePromptSchema.value) { + const schema = host.activePromptSchema.value!; + console.log('Waiting for input:', schema.name); + + // 这里可以从 UI/网络等获取输入 + const input = await getPlayerInput(); + const error = host.onInput(input); + + if (error) { + console.log('Invalid:', error); + } +} + +// 游戏结束后可以重新开始 +// await host.setup('setup'); + +// 或彻底销毁 +// host.dispose(); +``` diff --git a/docs/game-module.md b/docs/game-module.md new file mode 100644 index 0000000..2469984 --- /dev/null +++ b/docs/game-module.md @@ -0,0 +1,293 @@ +# 编写 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, + currentPlayer: 'X' as PlayerType, + winner: null as WinnerType, + turn: 0, + }; +} + +// 2. 创建命令注册表并注册命令 +const registration = createGameCommandRegistry>(); +export const registry = registration.registry; + +// 注册命令 +registration.add('setup', async function () { + // ... 命令逻辑 +}); + +registration.add('play ', 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, + currentPlayer: 'X' as PlayerType, + winner: null as WinnerType, + turn: 0, + }; +} +export type GameState = ReturnType; +``` + +状态通常包含: +- **Region**:用 `createRegion()` 创建的空间区域 +- **parts**:`Record` 游戏棋子集合 +- 游戏特有的字段:当前玩家、分数、回合数等 + +## 注册命令 + +使用 `registration.add()` 注册命令。Schema 字符串定义了命令格式: + +```ts +registration.add('play ', async function (cmd) { + const [player, row, col] = cmd.params as [PlayerType, number, number]; + + // this.context 是 MutableSignal + this.context.produce(state => { + state.parts[piece.id] = piece; + }); + + return { winner: null }; +}); +``` + +### Schema 语法 + +| 语法 | 含义 | +|---|---| +| `name` | 命令名 | +| `` | 必填参数(字符串) | +| `` | 必填参数(自动转为数字) | +| `[--flag]` | 可选标志 | +| `[-x:number]` | 可选选项(带类型) | + +### 命令处理器中的 this + +命令处理器中的 `this` 是 `CommandRunnerContext>`: + +```ts +registration.add('myCommand ', async function (cmd) { + // 读取状态 + const state = this.context.value; + + // 修改状态 + this.context.produce(draft => { + draft.currentPlayer = 'O'; + }); + + // 提示玩家输入 + const result = await this.prompt( + 'confirm ', + (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 ', 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 > 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 棋盘区域 +- 玩家轮流输入 +- 胜负判定 +- 完整的游戏循环