Compare commits
2 Commits
a8774f34bc
...
111d2e69eb
| Author | SHA1 | Date |
|---|---|---|
|
|
111d2e69eb | |
|
|
aa7e99bd0f |
|
|
@ -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<void>,在动画完成时 resolve
|
||||
host.addInterruption(animation);
|
||||
});
|
||||
```
|
||||
|
||||
> **注意**:`playAnimationForState` 函数需要返回 `Promise<void>`,在动画播放完成并 resolve 后,下一个 `produceAsync` 才会继续执行状态更新。
|
||||
|
||||
## 中断 API
|
||||
|
||||
`GameHost` 直接暴露了以下方法,供 UI 层调用:
|
||||
|
||||
| 方法 | 说明 |
|
||||
|---|---|
|
||||
| `addInterruption(promise: Promise<void>)` | 注册中断,下一个 `produceAsync` 会等待它 |
|
||||
| `clearInterruptions()` | 清除所有未完成的中断 |
|
||||
|
||||
`MutableSignal` 上还有 `produceAsync`,逻辑层使用。
|
||||
|
||||
## 注意事项
|
||||
|
||||
- `produce()` 仍然保持同步,适合不需要动画的场景(如 setup 阶段)
|
||||
- `produceAsync()` 使用 `Promise.allSettled` 等待所有 interruption,即使某个动画 reject 也不会阻止状态更新
|
||||
- 不要忘记 `await` `produceAsync()`,否则多个效应可能并发执行导致竞态
|
||||
- 第一个 `produceAsync` 总是立即执行(无前序 interruption),从第二个开始等待动画
|
||||
|
|
@ -1,142 +0,0 @@
|
|||
# API 参考
|
||||
|
||||
## 核心
|
||||
|
||||
| 导出 | 说明 |
|
||||
|---|---|
|
||||
| `GameHost<TState>` | 游戏生命周期管理类 |
|
||||
| `createGameHost(module)` | 从 GameModule 创建 GameHost |
|
||||
| `GameHostStatus` | 类型: `'created' \| 'running' \| 'disposed'` |
|
||||
| `GameModule` | 游戏模块类型,包含 `registry` 和 `createInitialState` |
|
||||
| `createGameModule(module)` | 辅助函数,标记 GameModule |
|
||||
| `createGameCommandRegistry<TState>()` | 创建游戏命令注册表 |
|
||||
|
||||
### GameHost
|
||||
|
||||
| 成员 | 说明 |
|
||||
|---|---|
|
||||
| `context: IGameContext<TState>` | 游戏上下文,含状态和命令运行能力 |
|
||||
| `status: Signal<GameHostStatus>` | 当前状态 |
|
||||
| `activePromptSchema: Signal<CommandSchema \| null>` | 当前等待的 prompt schema |
|
||||
| `activePromptPlayer: Signal<string \| null>` | 当前等待的玩家 |
|
||||
| `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<TMeta>` | 棋子类型,含 `id`、`regionId`、`position`、`side` 等 |
|
||||
| `PartTemplate<TMeta>` / `PartPool<TMeta>` | 棋子模板和棋子池类型 |
|
||||
| `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<T>` | 命令执行结果: `{ success: true, result: T } | { success: false, error: string }` |
|
||||
|
||||
### 注册与运行
|
||||
|
||||
| 导出 | 说明 |
|
||||
|---|---|
|
||||
| `CommandRegistry<TContext>` | 命令注册表,继承自 `Map<string, CommandRunner>`,含 `register(schema, handler)` 快捷方法 |
|
||||
| `CommandRunner<TContext, TResult>` | 命令运行器,含 `schema` 和 `run` |
|
||||
| `CommandRunnerContext<TContext>` | 命令运行器上下文,提供 `run`、`prompt`、`on`、`off` |
|
||||
| `PromptEvent` | prompt 事件,含 `schema`、`currentPlayer`、`tryCommit`、`cancel` |
|
||||
| `PromptValidator<T>` | 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 <from> <to>', async (ctx, from, to) => {
|
||||
ctx.produce(state => { /* ... */ });
|
||||
});
|
||||
|
||||
// 作为子命令调用
|
||||
await moveCmd(ctx, 'A1', 'A2');
|
||||
```
|
||||
|
||||
处理器签名: `(ctx, ...args) => Promise<TResult>`。
|
||||
`game.prompt()` 等待玩家输入,验证器 throw 字符串触发重新提示,返回非 null 值表示通过。
|
||||
|
||||
## MutableSignal
|
||||
|
||||
| 导出 | 说明 |
|
||||
|---|---|
|
||||
| `MutableSignal<T>` | 支持突变和动画中断的响应式信号,继承 Preact Signal |
|
||||
| `mutableSignal(initial?, options?)` | 创建 MutableSignal |
|
||||
| `EntityCollection<T>` / `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 |
|
||||
|
|
@ -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
|
||||
|
|
@ -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<void>)` | 注册动画中断,下一个 `produceAsync` 会等待它 |
|
||||
| `clearInterruptions()` | 清除所有未完成的中断 |
|
||||
|
||||
```ts
|
||||
// UI 层:检测到状态变化后播放动画并注册中断
|
||||
host.addInterruption(playAnimation('place', data));
|
||||
```
|
||||
|
|
@ -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<string, Part>,
|
||||
currentPlayer: 'X' as PlayerType,
|
||||
winner: null as WinnerType,
|
||||
turn: 0,
|
||||
};
|
||||
}
|
||||
|
||||
export const registry = createGameCommandRegistry<ReturnType<typeof createInitialState>>();
|
||||
|
||||
registry.register('setup', async function (game) { /* ... */ });
|
||||
registry.register('play <player> <row:number> <col:number>', async function (game, cmd) { /* ... */ });
|
||||
```
|
||||
|
||||
也可用 `createGameModule` 辅助函数包装:
|
||||
|
||||
```ts
|
||||
export const gameModule = createGameModule({
|
||||
registry,
|
||||
createInitialState,
|
||||
});
|
||||
```
|
||||
|
||||
## 定义游戏状态
|
||||
|
||||
建议用 `ReturnType` 推导状态类型:
|
||||
|
||||
```ts
|
||||
export type GameState = ReturnType<typeof createInitialState>;
|
||||
```
|
||||
|
||||
状态通常包含 Region、parts(`Record<string, Part>`)以及游戏专属字段(当前玩家、分数等)。
|
||||
|
||||
## 注册命令
|
||||
|
||||
使用 `registry.register()` 注册命令。Schema 字符串定义了命令格式:
|
||||
|
||||
```ts
|
||||
registry.register('play <player> <row:number> <col:number>', async function (game, player, row, col) {
|
||||
// game 是 IGameContext<TState>,可访问和修改状态
|
||||
game.produce(state => {
|
||||
// state.parts[...].position = [row, col];
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
});
|
||||
```
|
||||
|
||||
### Schema 语法
|
||||
|
||||
| 语法 | 含义 |
|
||||
|---|---|
|
||||
| `name` | 命令名 |
|
||||
| `<param>` | 必填参数(字符串) |
|
||||
| `<param:number>` | 必填参数(自动转为数字) |
|
||||
| `[--flag]` | 可选标志 |
|
||||
| `[-x:number]` | 可选选项(带类型) |
|
||||
|
||||
### 命令处理器函数签名
|
||||
|
||||
命令处理器接收 `game`(`IGameContext<TState>`)作为第一个参数,后续参数来自命令解析:
|
||||
|
||||
```ts
|
||||
registry.register('myCommand <arg>', async function (game, arg) {
|
||||
const state = game.value; // 读取状态
|
||||
game.produce(d => { d.currentPlayer = 'O'; }); // 同步修改状态
|
||||
await game.produceAsync(d => { /* ... */ }); // 异步修改(等待动画)
|
||||
|
||||
const result = await game.prompt('confirm <action>', validator, currentPlayer);
|
||||
const subResult = await subCommand(game, player); // 调用子命令
|
||||
return { success: true };
|
||||
});
|
||||
```
|
||||
|
||||
`registry.register()` 返回一个可调用函数,可在其他命令中直接调用:
|
||||
|
||||
```ts
|
||||
const subCommand = registry.register('sub <player>', 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 <player> <row:number> <col:number>',
|
||||
(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 <player>', 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 游戏:六边形棋盘、推动机制、小猫升级
|
||||
|
|
@ -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)。
|
||||
|
|
@ -10,6 +10,7 @@ export default defineConfig({
|
|||
dts: true,
|
||||
clean: true,
|
||||
sourcemap: true,
|
||||
external: ['@preact/signals-core', 'mutative', 'inline-schema'],
|
||||
plugins: [csvLoader()],
|
||||
esbuildOptions(options) {
|
||||
options.alias = {
|
||||
|
|
|
|||
Loading…
Reference in New Issue