From 111d2e69eb2f70b7f577e50c3c67b87778fac237 Mon Sep 17 00:00:00 2001 From: hypercross Date: Mon, 6 Apr 2026 14:08:25 +0800 Subject: [PATCH] chore: just remove all the docs --- docs/animation-sync.md | 90 -------------------- docs/api-reference.md | 142 -------------------------------- docs/development.md | 32 -------- docs/game-host.md | 165 ------------------------------------- docs/game-module.md | 167 -------------------------------------- docs/parts-regions-rng.md | 116 -------------------------- 6 files changed, 712 deletions(-) delete mode 100644 docs/animation-sync.md delete mode 100644 docs/api-reference.md delete mode 100644 docs/development.md delete mode 100644 docs/game-host.md delete mode 100644 docs/game-module.md delete mode 100644 docs/parts-regions-rng.md diff --git a/docs/animation-sync.md b/docs/animation-sync.md deleted file mode 100644 index 2f496a7..0000000 --- a/docs/animation-sync.md +++ /dev/null @@ -1,90 +0,0 @@ -# 动画与状态更新同步 - -命令执行时,效应函数如果通过 `produce()` 立即更新状态,UI 层只能看到最终结果, -无法在中间插入动画。为了解决这个问题,`MutableSignal` 提供了动画中断机制。 - -## 基本原理 - -核心思路:**逻辑层将 `produce` 替换为 `produceAsync`,UI 层负责注册动画 interruption。** - -时序如下: - -``` -逻辑层: produceAsync(fn1) produceAsync(fn2) produceAsync(fn3) - ↓ (无 interruption, ↓ 等待 anim1 完成 ↓ 等待 anim2 完成 - 立即更新状态 1) - -UI层: effect 检测到状态 1 变化 effect 检测到状态 2 变化 effect 检测到状态 3 变化 - 播放动画 1 播放动画 2 播放动画 3 - addInterruption(anim1) addInterruption(anim2) addInterruption(anim3) -``` - -1. 第一个 `produceAsync` 没有 interruption 可等,立即更新状态 -2. UI 层通过 `effect` 检测到状态变化,播放动画并调用 `addInterruption` -3. 第二个 `produceAsync` 被前一步注册的 interruption 阻塞,等待动画完成后再更新状态 -4. 依此类推,形成链式等待 - -## 逻辑层:将 `produce` 替换为 `produceAsync` - -```ts -// 之前 -async function turn(game: BoopGame, turnPlayer: PlayerType) { - game.produce(state => { - game.scores[turnPlayer] ++; - }); - // 这里不能触发动画等待 - game.produce(state => { - game.currentPlayer = turnPlayer; - }); -}; - -// 之后:改为 produceAsync -async function turn(game: BoopGame, turnPlayer: PlayerType) { - await game.produceAsync(state => { - game.scores[turnPlayer] ++; - }); - // 这里会等待interruption结束再继续 - await game.produceAsync(state => { - game.currentPlayer = turnPlayer; - }); -}; -``` - -## UI 层:监听状态变化并注册 interruption - -```ts -import { effect } from '@preact/signals-core'; - -const host = createGameHost(module); - -effect(() => { - const state = host.context.value; - // 每次 produceAsync 更新状态后,这里会被触发 - // 播放对应的动画 - const animation = playAnimationForState(state); - - // 为下一个 produceAsync 注册 interruption - // 注意:animation 必须是 Promise,在动画完成时 resolve - host.addInterruption(animation); -}); -``` - -> **注意**:`playAnimationForState` 函数需要返回 `Promise`,在动画播放完成并 resolve 后,下一个 `produceAsync` 才会继续执行状态更新。 - -## 中断 API - -`GameHost` 直接暴露了以下方法,供 UI 层调用: - -| 方法 | 说明 | -|---|---| -| `addInterruption(promise: Promise)` | 注册中断,下一个 `produceAsync` 会等待它 | -| `clearInterruptions()` | 清除所有未完成的中断 | - -`MutableSignal` 上还有 `produceAsync`,逻辑层使用。 - -## 注意事项 - -- `produce()` 仍然保持同步,适合不需要动画的场景(如 setup 阶段) -- `produceAsync()` 使用 `Promise.allSettled` 等待所有 interruption,即使某个动画 reject 也不会阻止状态更新 -- 不要忘记 `await` `produceAsync()`,否则多个效应可能并发执行导致竞态 -- 第一个 `produceAsync` 总是立即执行(无前序 interruption),从第二个开始等待动画 diff --git a/docs/api-reference.md b/docs/api-reference.md deleted file mode 100644 index db48d1e..0000000 --- a/docs/api-reference.md +++ /dev/null @@ -1,142 +0,0 @@ -# API 参考 - -## 核心 - -| 导出 | 说明 | -|---|---| -| `GameHost` | 游戏生命周期管理类 | -| `createGameHost(module)` | 从 GameModule 创建 GameHost | -| `GameHostStatus` | 类型: `'created' \| 'running' \| 'disposed'` | -| `GameModule` | 游戏模块类型,包含 `registry` 和 `createInitialState` | -| `createGameModule(module)` | 辅助函数,标记 GameModule | -| `createGameCommandRegistry()` | 创建游戏命令注册表 | - -### GameHost - -| 成员 | 说明 | -|---|---| -| `context: IGameContext` | 游戏上下文,含状态和命令运行能力 | -| `status: Signal` | 当前状态 | -| `activePromptSchema: Signal` | 当前等待的 prompt schema | -| `activePromptPlayer: Signal` | 当前等待的玩家 | -| `setup(setupCommand: string)` | 启动游戏,运行 setup 命令 | -| `onInput(input: string)` | 提交玩家输入到当前 prompt | -| `addInterruption(promise)` / `clearInterruptions()` | 动画中断控制 | -| `on(event, listener)` | 监听 `setup` / `dispose` 事件 | -| `dispose()` | 销毁游戏实例 | - -### IGameContext - -| 成员 | 说明 | -|---|---| -| `value: TState` | 当前游戏状态 | -| `produce(fn)` | 同步更新状态 | -| `produceAsync(fn)` | 等待动画中断后更新状态 | -| `run(input)` / `runParsed(command)` | 运行命令 | -| `prompt(schema, validator, currentPlayer?)` | 等待玩家输入 | -| `addInterruption(promise)` | 注册动画中断 | - -## 棋子与区域 - -### Part - -| 导出 | 说明 | -|---|---| -| `Part` | 棋子类型,含 `id`、`regionId`、`position`、`side` 等 | -| `PartTemplate` / `PartPool` | 棋子模板和棋子池类型 | -| `createPart(template, id)` | 创建单个棋子 | -| `createParts(template, count, idPrefix)` | 批量创建棋子 | -| `createPartPool(template, count, idPrefix)` | 创建棋子池,支持 `draw()` / `return()` / `remaining()` | -| `mergePartPools(...pools)` | 合并棋子池 | -| `createPartsFromTable(items, getId, getCount?)` | 从表格数据创建棋子 | -| `findPartById(parts, id)` | 按 ID 查找 | -| `getPartAtPosition(parts, regionId, position)` | 获取位置上的棋子 | -| `isCellOccupied(parts, regionId, position)` | 检查格子占用 | -| `flip(part)` / `flipTo(part, side)` / `roll(part, rng)` | 翻面/掷骰 | - -### Region - -| 导出 | 说明 | -|---|---| -| `Region` / `RegionAxis` | 区域类型 | -| `createRegion(id, axes)` | 创建区域 | -| `applyAlign(region, parts)` | 按 axis 配置紧凑排列棋子 | -| `shuffle(region, parts, rng)` | 打乱区域内棋子位置 | -| `moveToRegion(part, source?, target, position?)` | 移动棋子到区域 | -| `isCellOccupiedByRegion(region, position)` | O(1) 检查格子占用(使用 region.partMap) | -| `getPartAtPositionInRegion(region, parts, position)` | O(1) 获取棋子(使用 region.partMap) | - -## 命令系统 - -### 基础类型 - -| 导出 | 说明 | -|---|---| -| `Command` | 解析后的命令对象,含 `name`、`params`、`options`、`flags` | -| `CommandSchema` | 命令 schema 定义 | -| `CommandResult` | 命令执行结果: `{ success: true, result: T } | { success: false, error: string }` | - -### 注册与运行 - -| 导出 | 说明 | -|---|---| -| `CommandRegistry` | 命令注册表,继承自 `Map`,含 `register(schema, handler)` 快捷方法 | -| `CommandRunner` | 命令运行器,含 `schema` 和 `run` | -| `CommandRunnerContext` | 命令运行器上下文,提供 `run`、`prompt`、`on`、`off` | -| `PromptEvent` | prompt 事件,含 `schema`、`currentPlayer`、`tryCommit`、`cancel` | -| `PromptValidator` | prompt 验证器: `(command) => T`,throw 字符串表示验证失败 | -| `createCommandRegistry()` | 创建命令注册表 | -| `registerCommand(registry, runner)` | 注册命令 | -| `unregisterCommand(registry, name)` / `hasCommand(registry, name)` / `getCommand(registry, name)` | 命令管理 | -| `runCommand(registry, context, input)` | 解析并运行命令 | - -### 命令解析 - -| 导出 | 说明 | -|---|---| -| `parseCommand(input)` | 解析命令字符串为 `Command` 对象 | -| `parseCommandSchema(schemaStr)` | 解析 schema 字符串为 `CommandSchema` | -| `validateCommand(command, schema)` | 验证命令是否符合 schema | -| `parseCommandWithSchema(input, schemaStr)` | 解析并验证命令 | -| `applyCommandSchema(command, schema)` | 应用 schema 到命令,含类型转换 | - -### Game Command Registry - -通过 `createGameCommandRegistry()` 创建的注册表有快捷 `register` 方法: - -```ts -const moveCmd = registry.register('move ', async (ctx, from, to) => { - ctx.produce(state => { /* ... */ }); -}); - -// 作为子命令调用 -await moveCmd(ctx, 'A1', 'A2'); -``` - -处理器签名: `(ctx, ...args) => Promise`。 -`game.prompt()` 等待玩家输入,验证器 throw 字符串触发重新提示,返回非 null 值表示通过。 - -## MutableSignal - -| 导出 | 说明 | -|---|---| -| `MutableSignal` | 支持突变和动画中断的响应式信号,继承 Preact Signal | -| `mutableSignal(initial?, options?)` | 创建 MutableSignal | -| `EntityCollection` / `createEntityCollection()` | 实体集合辅助函数,管理 `MutableSignal` 字典 | - -| 成员 | 说明 | -|---|---| -| `value: T` | 获取/设置当前值 | -| `produce(fn)` | 同步不可变更新(使用 mutative) | -| `produceAsync(fn)` | 等待所有 interruption 完成后更新状态 | -| `addInterruption(promise)` | 注册中断,下一个 produceAsync 会等待 | -| `clearInterruptions()` | 清除所有未完成的中断 | - -详见 [动画与状态更新同步](./animation-sync.md)。 - -## 工具 - -| 导出 | 说明 | -|---|---| -| `RNG` | 随机数生成器接口: `setSeed`、`getSeed`、`next`、`nextInt` | -| `createRNG(seed?)` / `Mulberry32RNG` | Mulberry32 算法 PRNG | diff --git a/docs/development.md b/docs/development.md deleted file mode 100644 index 38dd8a5..0000000 --- a/docs/development.md +++ /dev/null @@ -1,32 +0,0 @@ -# 开发指南 - -## 安装 - -```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 deleted file mode 100644 index 7abd20b..0000000 --- a/docs/game-host.md +++ /dev/null @@ -1,165 +0,0 @@ -# 使用 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.context.value.currentPlayer); - console.log(host.context.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.context.value; - console.log(`${state.currentPlayer}'s turn (turn ${state.turn + 1})`); - if (state.winner) { - console.log('Winner:', state.winner); - } -}); - -// 启动游戏 -await host.setup('setup'); - -// 游戏循环:等待提示 → 提交输入 -// 注意:setup() 会立即返回,但 prompt 可能需要一些时间才能激活 -// 实际应用中应该等待 activePromptSchema 变为非 null -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(); -``` - -## 动画同步 - -如需在状态更新之间播放动画,参考 [动画与状态更新同步](./animation-sync.md)。 - -`GameHost` 提供了两个方法: - -| 方法 | 说明 | -|---|---| -| `addInterruption(promise: Promise)` | 注册动画中断,下一个 `produceAsync` 会等待它 | -| `clearInterruptions()` | 清除所有未完成的中断 | - -```ts -// UI 层:检测到状态变化后播放动画并注册中断 -host.addInterruption(playAnimation('place', data)); -``` diff --git a/docs/game-module.md b/docs/game-module.md deleted file mode 100644 index 7e0371c..0000000 --- a/docs/game-module.md +++ /dev/null @@ -1,167 +0,0 @@ -# 编写 GameModule - -GameModule 是定义游戏逻辑的模块,包含状态定义和命令注册。 - -## GameModule 结构 - -一个 GameModule 必须导出两个东西: - -```ts -import { createGameCommandRegistry, createRegion } from 'boardgame-core'; - -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 const registry = createGameCommandRegistry>(); - -registry.register('setup', async function (game) { /* ... */ }); -registry.register('play ', async function (game, cmd) { /* ... */ }); -``` - -也可用 `createGameModule` 辅助函数包装: - -```ts -export const gameModule = createGameModule({ - registry, - createInitialState, -}); -``` - -## 定义游戏状态 - -建议用 `ReturnType` 推导状态类型: - -```ts -export type GameState = ReturnType; -``` - -状态通常包含 Region、parts(`Record`)以及游戏专属字段(当前玩家、分数等)。 - -## 注册命令 - -使用 `registry.register()` 注册命令。Schema 字符串定义了命令格式: - -```ts -registry.register('play ', async function (game, player, row, col) { - // game 是 IGameContext,可访问和修改状态 - game.produce(state => { - // state.parts[...].position = [row, col]; - }); - - return { success: true }; -}); -``` - -### Schema 语法 - -| 语法 | 含义 | -|---|---| -| `name` | 命令名 | -| `` | 必填参数(字符串) | -| `` | 必填参数(自动转为数字) | -| `[--flag]` | 可选标志 | -| `[-x:number]` | 可选选项(带类型) | - -### 命令处理器函数签名 - -命令处理器接收 `game`(`IGameContext`)作为第一个参数,后续参数来自命令解析: - -```ts -registry.register('myCommand ', async function (game, arg) { - const state = game.value; // 读取状态 - game.produce(d => { d.currentPlayer = 'O'; }); // 同步修改状态 - await game.produceAsync(d => { /* ... */ }); // 异步修改(等待动画) - - const result = await game.prompt('confirm ', validator, currentPlayer); - const subResult = await subCommand(game, player); // 调用子命令 - return { success: true }; -}); -``` - -`registry.register()` 返回一个可调用函数,可在其他命令中直接调用: - -```ts -const subCommand = registry.register('sub ', async function (game, player) { - return { score: 10 }; -}); - -// 在另一个命令中使用 -registry.register('main', async function (game) { - const result = await subCommand(game, 'X'); - // result = { success: true, result: { score: 10 } } -}); -``` - -详见 [API 参考](./api-reference.md)。 - -## 使用 prompt 等待玩家输入 - -`game.prompt()` 暂停命令执行,等待外部通过 `host.onInput()` 提交输入: - -```ts -const playCmd = await game.prompt( - 'play ', - (command) => { - const [player, row, col] = command.params as [PlayerType, number, number]; - if (player !== turnPlayer) throw `Invalid player: ${player}`; - if (row < 0 || row > 2 || col < 0 || col > 2) throw `Invalid position`; - if (isCellOccupied(game, row, col)) throw `Cell occupied`; - return { player, row, col }; // 验证通过,返回所需数据 - }, - game.value.currentPlayer -); - -// playCmd = { player, row, col } -``` - -验证函数中 `throw` 字符串会触发重新提示,返回非 null 值表示验证通过并通过该值 resolve Promise。 - -## 使用 setup 驱动游戏循环 - -`setup` 作为入口点驱动游戏循环,通过调用其他命令函数实现: - -```ts -// 注册 turn 命令并获取可调用函数 -const turnCommand = registry.register('turn ', async function (game, player) { - // ... 执行回合逻辑 - return { winner: null as WinnerType }; -}); - -// 注册 setup 命令 -registry.register('setup', async function (game) { - while (true) { - const currentPlayer = game.value.currentPlayer; - const turnOutput = await turnCommand(game, currentPlayer); - if (!turnOutput.success) throw new Error(turnOutput.error); - - game.produce(state => { - state.winner = turnOutput.result.winner; - if (!state.winner) { - state.currentPlayer = state.currentPlayer === 'X' ? 'O' : 'X'; - } - }); - if (game.value.winner) break; - } - return game.value; -}); -``` - -## Part、Region 和 RNG - -详见 [棋子、区域与 RNG](./parts-regions-rng.md)。 - -## 完整示例 - -参考以下示例: -- [`src/samples/tic-tac-toe.ts`](../src/samples/tic-tac-toe.ts) - 井字棋:2D 棋盘、玩家轮流输入、胜负判定 -- [`src/samples/boop/`](../src/samples/boop/) - Boop 游戏:六边形棋盘、推动机制、小猫升级 diff --git a/docs/parts-regions-rng.md b/docs/parts-regions-rng.md deleted file mode 100644 index dc8ede3..0000000 --- a/docs/parts-regions-rng.md +++ /dev/null @@ -1,116 +0,0 @@ -# 棋子、区域与 RNG - -## 创建和放置 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: '', position: [], owner: 'white' }, - 'piece-1' -); - -state.produce(draft => { - draft.parts[piece.id] = piece; - // 推荐使用 moveToRegion 自动维护 childIds 和 partMap - moveToRegion(piece, null, draft.board, [1, 1]); -}); - -// 或者手动操作(不推荐,容易出错): -// state.produce(draft => { -// draft.parts[piece.id] = piece; -// draft.board.childIds.push(piece.id); -// draft.board.partMap['1,1'] = piece.id; -// piece.regionId = 'board'; -// piece.position = [1, 1]; -// }); -``` - -## Part 池 - -```ts -const pool = createPartPool<{ type: string }>( - { regionId: 'supply', type: 'kitten' }, - 10, - 'kitten' -); - -const piece = pool.draw(); // 取出一个 -pool.return(piece); // 放回 -pool.remaining(); // 剩余数量 -``` - -## 从表格数据创建 Part - -```ts -const parts = createPartsFromTable( - [ - { id: 'p1', regionId: 'board', position: [0, 0], owner: 'white' }, - { id: 'p2', regionId: 'board', position: [1, 1], owner: 'black' }, - ], - (item, index) => item.id, // 返回 ID 的函数 - // 可选:每个 item 创建几个,默认 1 - 1 -); - -// parts = { -// 'p1': { id: 'p1', regionId: 'board', position: [0, 0], owner: 'white' }, -// 'p2': { id: 'p2', regionId: 'board', position: [1, 1], owner: 'black' }, -// } -``` - -`createPartsFromTable` 接受对象数组,每个对象的所有字段都会被展开到 Part 中。 - -## 查询棋子 - -```ts -// O(n) 遍历查找 -findPartById(state.parts, 'piece-1'); -getPartAtPosition(state.parts, 'board', [1, 1]); -isCellOccupied(state.parts, 'board', [1, 1]); - -// O(1) 使用 Region.partMap 查找 -getPartAtPositionInRegion(board, state.parts, [1, 1]); -isCellOccupiedByRegion(board, [1, 1]); -``` - -## 区域操作 - -```ts -import { applyAlign, shuffle, moveToRegion } from 'boardgame-core'; - -applyAlign(handRegion, state.parts); // 紧凑排列 -shuffle(deckRegion, state.parts, rng); // 打乱 - -// 移动棋子:sourceRegion 为 null 表示棋子当前不在区域中 -moveToRegion(piece, sourceRegion, targetRegion, [0, 0]); -moveToRegion(piece, null, boardRegion, [0, 0]); // 从外部放入区域 -moveToRegion(piece, boardRegion, null); // 从区域中移除(返回外部) -``` - -## 翻面与掷骰 - -```ts -import { flip, flipTo, roll } from 'boardgame-core'; - -flip(piece); // 翻到下一面 -flipTo(piece, 2); // 翻到指定面 -roll(piece, rng); // 随机面 -``` - -## RNG - -```ts -import { createRNG } from 'boardgame-core'; - -const rng = createRNG(12345); -rng.nextInt(6); // 0-5 -rng.next(); // [0, 1) -``` - -完整 API 列表详见 [API 参考](./api-reference.md)。