docs: some fixes
This commit is contained in:
parent
bd2988902a
commit
f5830ea637
|
|
@ -1,6 +1,6 @@
|
||||||
# 动画与状态更新同步
|
# 动画与状态更新同步
|
||||||
|
|
||||||
命令执行时,效应函数通过 `produce()` 立即更新状态,UI 层只能看到最终结果,
|
命令执行时,效应函数如果通过 `produce()` 立即更新状态,UI 层只能看到最终结果,
|
||||||
无法在中间插入动画。为了解决这个问题,`MutableSignal` 提供了动画中断机制。
|
无法在中间插入动画。为了解决这个问题,`MutableSignal` 提供了动画中断机制。
|
||||||
|
|
||||||
## 基本原理
|
## 基本原理
|
||||||
|
|
@ -14,33 +14,40 @@
|
||||||
↓ (无 interruption, ↓ 等待 anim1 完成 ↓ 等待 anim2 完成
|
↓ (无 interruption, ↓ 等待 anim1 完成 ↓ 等待 anim2 完成
|
||||||
立即更新状态 1)
|
立即更新状态 1)
|
||||||
|
|
||||||
UI层: effect 检测到状态 1 变化 addInterruption(anim1) addInterruption(anim2)
|
UI层: effect 检测到状态 1 变化 effect 检测到状态 2 变化 effect 检测到状态 3 变化
|
||||||
播放动画 1 播放动画 2 播放动画 3
|
播放动画 1 播放动画 2 播放动画 3
|
||||||
|
addInterruption(anim1) addInterruption(anim2) addInterruption(anim3)
|
||||||
```
|
```
|
||||||
|
|
||||||
1. 第一个 `produceAsync` 没有 interruption 可等,立即更新状态
|
1. 第一个 `produceAsync` 没有 interruption 可等,立即更新状态
|
||||||
2. UI 层通过 `effect` 检测到状态变化,播放动画并调用 `addInterruption`
|
2. UI 层通过 `effect` 检测到状态变化,播放动画并调用 `addInterruption`
|
||||||
3. 第二个 `produceAsync` 被前一步的 interruption 阻塞,等待动画完成后再更新
|
3. 第二个 `produceAsync` 被前一步注册的 interruption 阻塞,等待动画完成后再更新状态
|
||||||
4. 依此类推,形成链式等待
|
4. 依此类推,形成链式等待
|
||||||
|
|
||||||
## 逻辑层:将 `produce` 替换为 `produceAsync`
|
## 逻辑层:将 `produce` 替换为 `produceAsync`
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
// 之前
|
// 之前
|
||||||
registration.add('turn <player>', async function(cmd) {
|
async function turn(game: BoopGame, turnPlayer: PlayerType) {
|
||||||
const playCmd = await this.prompt('play <player> <row:number> <col:number>', validator, currentPlayer);
|
game.produce(state => {
|
||||||
|
game.scores[turnPlayer] ++;
|
||||||
placePiece(this.context, row, col, pieceType); // 内部调用 produce
|
});
|
||||||
applyBoops(this.context, row, col, pieceType); // 内部调用 produce
|
// 这里不能触发动画等待
|
||||||
});
|
game.produce(state => {
|
||||||
|
game.currentPlayer = turnPlayer;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// 之后:改为 produceAsync
|
// 之后:改为 produceAsync
|
||||||
registration.add('turn <player>', async function(cmd) {
|
async function turn(game: BoopGame, turnPlayer: PlayerType) {
|
||||||
const playCmd = await this.prompt('play <player> <row:number> <col:number>', validator, currentPlayer);
|
await game.produceAsync(state => {
|
||||||
|
game.scores[turnPlayer] ++;
|
||||||
await placePieceAsync(this.context, row, col, pieceType); // 内部改用 produceAsync
|
});
|
||||||
await applyBoopsAsync(this.context, row, col, pieceType); // 内部改用 produceAsync
|
// 这里会等待interruption结束再继续
|
||||||
});
|
await game.produceAsync(state => {
|
||||||
|
game.currentPlayer = turnPlayer;
|
||||||
|
});
|
||||||
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
## UI 层:监听状态变化并注册 interruption
|
## UI 层:监听状态变化并注册 interruption
|
||||||
|
|
@ -51,28 +58,18 @@ import { effect } from '@preact/signals-core';
|
||||||
const host = createGameHost(module);
|
const host = createGameHost(module);
|
||||||
|
|
||||||
effect(() => {
|
effect(() => {
|
||||||
const state = host.state.value;
|
const state = host.context.value;
|
||||||
// 每次 produceAsync 更新状态后,这里会被触发
|
// 每次 produceAsync 更新状态后,这里会被触发
|
||||||
// 播放对应的动画
|
// 播放对应的动画
|
||||||
const animation = playAnimationForState(state);
|
const animation = playAnimationForState(state);
|
||||||
|
|
||||||
// 为下一个 produceAsync 注册 interruption
|
// 为下一个 produceAsync 注册 interruption
|
||||||
|
// 注意:animation 必须是 Promise<void>,在动画完成时 resolve
|
||||||
host.addInterruption(animation);
|
host.addInterruption(animation);
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
## 辅助函数示例
|
> **注意**:`playAnimationForState` 函数需要返回 `Promise<void>`,在动画播放完成并 resolve 后,下一个 `produceAsync` 才会继续执行状态更新。
|
||||||
|
|
||||||
```ts
|
|
||||||
// 将 produce 包装为 produceAsync 的辅助函数
|
|
||||||
async function placePieceAsync(context: MutableSignal<GameState>, row: number, col: number) {
|
|
||||||
await context.produceAsync(state => {
|
|
||||||
state.parts[piece.id] = piece;
|
|
||||||
board.childIds.push(piece.id);
|
|
||||||
board.partMap[`${row},${col}`] = piece.id;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 中断 API
|
## 中断 API
|
||||||
|
|
||||||
|
|
@ -89,6 +86,5 @@ async function placePieceAsync(context: MutableSignal<GameState>, row: number, c
|
||||||
|
|
||||||
- `produce()` 仍然保持同步,适合不需要动画的场景(如 setup 阶段)
|
- `produce()` 仍然保持同步,适合不需要动画的场景(如 setup 阶段)
|
||||||
- `produceAsync()` 使用 `Promise.allSettled` 等待所有 interruption,即使某个动画 reject 也不会阻止状态更新
|
- `produceAsync()` 使用 `Promise.allSettled` 等待所有 interruption,即使某个动画 reject 也不会阻止状态更新
|
||||||
- `clearInterruptions()` 会丢弃所有未完成的中断,建议在回合/阶段结束时调用
|
|
||||||
- 不要忘记 `await` `produceAsync()`,否则多个效应可能并发执行导致竞态
|
- 不要忘记 `await` `produceAsync()`,否则多个效应可能并发执行导致竞态
|
||||||
- 第一个 `produceAsync` 总是立即执行(无前序 interruption),从第二个开始等待动画
|
- 第一个 `produceAsync` 总是立即执行(无前序 interruption),从第二个开始等待动画
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@
|
||||||
| `PartPool<TMeta>` | 棋子池类型 |
|
| `PartPool<TMeta>` | 棋子池类型 |
|
||||||
| `createPart(template, id)` | 创建单个棋子 |
|
| `createPart(template, id)` | 创建单个棋子 |
|
||||||
| `createParts(template, count, idPrefix)` | 批量创建相同棋子 |
|
| `createParts(template, count, idPrefix)` | 批量创建相同棋子 |
|
||||||
| `createPartsFromTable(template, table, idField?)` | 从表格数据创建棋子 |
|
| `createPartsFromTable(items, getId, getCount?)` | 从表格数据创建棋子 |
|
||||||
| `createPartPool(template, count, idPrefix)` | 创建棋子池 |
|
| `createPartPool(template, count, idPrefix)` | 创建棋子池 |
|
||||||
| `mergePartPools(...pools)` | 合并多个棋子池 |
|
| `mergePartPools(...pools)` | 合并多个棋子池 |
|
||||||
| `findPartById(parts, id)` | 按 ID 查找棋子 |
|
| `findPartById(parts, id)` | 按 ID 查找棋子 |
|
||||||
|
|
@ -60,19 +60,32 @@
|
||||||
| 导出 | 说明 |
|
| 导出 | 说明 |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `CommandRunner<TContext, TResult>` | 命令运行器类型 |
|
| `CommandRunner<TContext, TResult>` | 命令运行器类型 |
|
||||||
| `CommandRunnerHandler` | 命令处理器 |
|
| `CommandRunnerHandler<TContext, TResult>` | 命令处理器类型 |
|
||||||
| `CommandRunnerContext` / `CommandRunnerContextExport` | 命令运行器上下文 |
|
| `CommandRunnerContext<TContext>` | 命令运行器上下文类型 |
|
||||||
| `CommandRegistry` | 命令注册表类型 |
|
| `CommandRunnerContextExport<TContext>` | 导出的命令运行器上下文(含内部方法) |
|
||||||
|
| `CommandRegistry<TContext>` | 命令注册表类型 |
|
||||||
| `PromptEvent` / `CommandRunnerEvents` | 提示事件类型 |
|
| `PromptEvent` / `CommandRunnerEvents` | 提示事件类型 |
|
||||||
| `PromptValidator<T>` | 提示验证器类型 |
|
| `PromptValidator<T>` | 提示验证器类型 |
|
||||||
| `createCommandRegistry()` | 创建命令注册表 |
|
| `createCommandRegistry()` | 创建命令注册表 |
|
||||||
| `registerCommand(registry, name, handler)` | 注册命令 |
|
| `registerCommand(registry, runner)` | 注册命令运行器 |
|
||||||
| `unregisterCommand(registry, name)` | 注销命令 |
|
| `unregisterCommand(registry, name)` | 注销命令 |
|
||||||
| `hasCommand(registry, name)` | 检查命令是否存在 |
|
| `hasCommand(registry, name)` | 检查命令是否存在 |
|
||||||
| `getCommand(registry, name)` | 获取命令 |
|
| `getCommand(registry, name)` | 获取命令 |
|
||||||
| `runCommand(ctx, input)` | 运行命令 |
|
| `runCommand(registry, context, input)` | 运行命令 |
|
||||||
| `runCommandParsed(ctx, cmd)` | 运行已解析命令 |
|
| `runCommandParsed(registry, context, command)` | 运行已解析命令 |
|
||||||
| `createCommandRunnerContext(registry, ctx)` | 创建命令运行器上下文 |
|
| `createCommandRunnerContext(registry, context)` | 创建命令运行器上下文 |
|
||||||
|
|
||||||
|
### Game Command Registry
|
||||||
|
|
||||||
|
游戏专用命令注册表(通过 `createGameCommandRegistry` 创建,类型为 `CommandRegistry<IGameContext<TState>>`):
|
||||||
|
|
||||||
|
| 方法 | 说明 |
|
||||||
|
|---|---|
|
||||||
|
| `registry.register(schema, handler)` | 注册命令并返回可调用命令对象 |
|
||||||
|
|
||||||
|
`registry.register` 接受命令 Schema(字符串或 `CommandSchema` 对象)和处理器函数,返回一个可调用函数。处理器函数签名为 `(ctx, ...args) => Promise<TResult>`。
|
||||||
|
|
||||||
|
在 GameModule 中使用 `game.prompt()` 等待玩家输入,验证函数中 `throw` 字符串会触发重新提示,返回非 null 值表示验证通过。子命令可以通过 `await subCommand(game, ...args)` 方式调用。
|
||||||
|
|
||||||
## MutableSignal
|
## MutableSignal
|
||||||
|
|
||||||
|
|
@ -95,6 +108,7 @@
|
||||||
### GameHost 中断代理
|
### GameHost 中断代理
|
||||||
|
|
||||||
`GameHost` 直接代理 `addInterruption` 和 `clearInterruptions`,供 UI 层使用。
|
`GameHost` 直接代理 `addInterruption` 和 `clearInterruptions`,供 UI 层使用。
|
||||||
|
`IGameContext` 提供 `produce()`、`produceAsync()` 和 `addInterruption()` 方法。
|
||||||
详见 [动画与状态更新同步](./animation-sync.md)。
|
详见 [动画与状态更新同步](./animation-sync.md)。
|
||||||
|
|
||||||
## 工具
|
## 工具
|
||||||
|
|
|
||||||
|
|
@ -22,8 +22,8 @@ import { effect } from '@preact/signals-core';
|
||||||
|
|
||||||
// 游戏状态
|
// 游戏状态
|
||||||
effect(() => {
|
effect(() => {
|
||||||
console.log(host.state.value.currentPlayer);
|
console.log(host.context.value.currentPlayer);
|
||||||
console.log(host.state.value.winner);
|
console.log(host.context.value.winner);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 生命周期状态: 'created' | 'running' | 'disposed'
|
// 生命周期状态: 'created' | 'running' | 'disposed'
|
||||||
|
|
@ -53,7 +53,7 @@ effect(() => {
|
||||||
await host.setup('setup');
|
await host.setup('setup');
|
||||||
```
|
```
|
||||||
|
|
||||||
这会重置游戏状态、取消当前活动提示、运行指定的 setup 命令,并将状态设为 `'running'`。
|
这会重置游戏状态、取消当前活动提示、在后台启动指定的 setup 命令(不等待完成),并将状态设为 `'running'`。
|
||||||
|
|
||||||
## 处理玩家输入
|
## 处理玩家输入
|
||||||
|
|
||||||
|
|
@ -115,7 +115,7 @@ const host = createGameHost(tictactoe);
|
||||||
|
|
||||||
// 监听状态变化
|
// 监听状态变化
|
||||||
effect(() => {
|
effect(() => {
|
||||||
const state = host.state.value;
|
const state = host.context.value;
|
||||||
console.log(`${state.currentPlayer}'s turn (turn ${state.turn + 1})`);
|
console.log(`${state.currentPlayer}'s turn (turn ${state.turn + 1})`);
|
||||||
if (state.winner) {
|
if (state.winner) {
|
||||||
console.log('Winner:', state.winner);
|
console.log('Winner:', state.winner);
|
||||||
|
|
@ -126,6 +126,8 @@ effect(() => {
|
||||||
await host.setup('setup');
|
await host.setup('setup');
|
||||||
|
|
||||||
// 游戏循环:等待提示 → 提交输入
|
// 游戏循环:等待提示 → 提交输入
|
||||||
|
// 注意:setup() 会立即返回,但 prompt 可能需要一些时间才能激活
|
||||||
|
// 实际应用中应该等待 activePromptSchema 变为非 null
|
||||||
while (host.status.value === 'running' && host.activePromptSchema.value) {
|
while (host.status.value === 'running' && host.activePromptSchema.value) {
|
||||||
const schema = host.activePromptSchema.value!;
|
const schema = host.activePromptSchema.value!;
|
||||||
console.log('Waiting for input:', schema.name);
|
console.log('Waiting for input:', schema.name);
|
||||||
|
|
|
||||||
|
|
@ -22,18 +22,17 @@ export function createInitialState() {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const registration = createGameCommandRegistry<ReturnType<typeof createInitialState>>();
|
export const registry = createGameCommandRegistry<ReturnType<typeof createInitialState>>();
|
||||||
export const registry = registration.registry;
|
|
||||||
|
|
||||||
registration.add('setup', async function () { /* ... */ });
|
registry.register('setup', async function (game) { /* ... */ });
|
||||||
registration.add('play <player> <row:number> <col:number>', async function (cmd) { /* ... */ });
|
registry.register('play <player> <row:number> <col:number>', async function (game, cmd) { /* ... */ });
|
||||||
```
|
```
|
||||||
|
|
||||||
也可用 `createGameModule` 辅助函数包装:
|
也可用 `createGameModule` 辅助函数包装:
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
export const gameModule = createGameModule({
|
export const gameModule = createGameModule({
|
||||||
registry: registration.registry,
|
registry,
|
||||||
createInitialState,
|
createInitialState,
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
@ -50,18 +49,16 @@ export type GameState = ReturnType<typeof createInitialState>;
|
||||||
|
|
||||||
## 注册命令
|
## 注册命令
|
||||||
|
|
||||||
使用 `registration.add()` 注册命令。Schema 字符串定义了命令格式:
|
使用 `registry.register()` 注册命令。Schema 字符串定义了命令格式:
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
registration.add('play <player> <row:number> <col:number>', async function (cmd) {
|
registry.register('play <player> <row:number> <col:number>', async function (game, player, row, col) {
|
||||||
const [player, row, col] = cmd.params as [PlayerType, number, number];
|
// game 是 IGameContext<TState>,可访问和修改状态
|
||||||
|
game.produce(state => {
|
||||||
// this.context 是 MutableSignal<GameState>
|
// state.parts[...].position = [row, col];
|
||||||
this.context.produce(state => {
|
|
||||||
state.parts[piece.id] = piece;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return { winner: null };
|
return { success: true };
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -75,66 +72,87 @@ registration.add('play <player> <row:number> <col:number>', async function (cmd)
|
||||||
| `[--flag]` | 可选标志 |
|
| `[--flag]` | 可选标志 |
|
||||||
| `[-x:number]` | 可选选项(带类型) |
|
| `[-x:number]` | 可选选项(带类型) |
|
||||||
|
|
||||||
### 命令处理器中的 this
|
### 命令处理器函数签名
|
||||||
|
|
||||||
命令处理器中的 `this` 是 `CommandRunnerContext<MutableSignal<TState>>`:
|
命令处理器接收 `game`(`IGameContext<TState>`)作为第一个参数,后续参数来自命令解析:
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
registration.add('myCommand <arg>', async function (cmd) {
|
registry.register('myCommand <arg>', async function (game, arg) {
|
||||||
const state = this.context.value; // 读取状态
|
const state = game.value; // 读取状态
|
||||||
this.context.produce(d => { d.currentPlayer = 'O'; }); // 修改状态
|
game.produce(d => { d.currentPlayer = 'O'; }); // 同步修改状态
|
||||||
|
await game.produceAsync(d => { /* ... */ }); // 异步修改(等待动画)
|
||||||
|
|
||||||
const result = await this.prompt('confirm <action>', validator, currentPlayer);
|
const result = await game.prompt('confirm <action>', validator, currentPlayer);
|
||||||
const subResult = await this.run<{ score: number }>(`score ${player}`);
|
const subResult = await subCommand(game, player); // 调用子命令
|
||||||
return { success: true };
|
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)。
|
详见 [API 参考](./api-reference.md)。
|
||||||
|
|
||||||
## 使用 prompt 等待玩家输入
|
## 使用 prompt 等待玩家输入
|
||||||
|
|
||||||
`this.prompt()` 暂停命令执行,等待外部通过 `host.onInput()` 提交输入:
|
`game.prompt()` 暂停命令执行,等待外部通过 `host.onInput()` 提交输入:
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
const playCmd = await this.prompt(
|
const playCmd = await game.prompt(
|
||||||
'play <player> <row:number> <col:number>',
|
'play <player> <row:number> <col:number>',
|
||||||
(command) => {
|
(command) => {
|
||||||
const [player, row, col] = command.params as [PlayerType, number, number];
|
const [player, row, col] = command.params as [PlayerType, number, number];
|
||||||
if (player !== turnPlayer) return `Invalid player: ${player}`;
|
if (player !== turnPlayer) throw `Invalid player: ${player}`;
|
||||||
if (row < 0 || row > 2 || col < 0 || col > 2) return `Invalid position`;
|
if (row < 0 || row > 2 || col < 0 || col > 2) throw `Invalid position`;
|
||||||
if (isCellOccupied(this.context, row, col)) return `Cell occupied`;
|
if (isCellOccupied(game, row, col)) throw `Cell occupied`;
|
||||||
return null;
|
return { player, row, col }; // 验证通过,返回所需数据
|
||||||
},
|
},
|
||||||
this.context.value.currentPlayer
|
game.value.currentPlayer
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// playCmd = { player, row, col }
|
||||||
```
|
```
|
||||||
|
|
||||||
验证函数返回 `null` 表示有效,返回 `string` 表示错误信息。验证通过后 `playCmd` 是已解析的命令对象。
|
验证函数中 `throw` 字符串会触发重新提示,返回非 null 值表示验证通过并通过该值 resolve Promise。
|
||||||
|
|
||||||
## 使用 setup 驱动游戏循环
|
## 使用 setup 驱动游戏循环
|
||||||
|
|
||||||
`setup` 作为入口点驱动游戏循环:
|
`setup` 作为入口点驱动游戏循环,通过调用其他命令函数实现:
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
registration.add('setup', async function () {
|
// 注册 turn 命令并获取可调用函数
|
||||||
const { context } = this;
|
const turnCommand = registry.register('turn <player>', async function (game, player) {
|
||||||
|
// ... 执行回合逻辑
|
||||||
|
return { winner: null as WinnerType };
|
||||||
|
});
|
||||||
|
|
||||||
|
// 注册 setup 命令
|
||||||
|
registry.register('setup', async function (game) {
|
||||||
while (true) {
|
while (true) {
|
||||||
const currentPlayer = context.value.currentPlayer;
|
const currentPlayer = game.value.currentPlayer;
|
||||||
const turnNumber = context.value.turn + 1;
|
const turnOutput = await turnCommand(game, currentPlayer);
|
||||||
const turnOutput = await this.run<{ winner: WinnerType }>(`turn ${currentPlayer} ${turnNumber}`);
|
|
||||||
if (!turnOutput.success) throw new Error(turnOutput.error);
|
if (!turnOutput.success) throw new Error(turnOutput.error);
|
||||||
|
|
||||||
context.produce(state => {
|
game.produce(state => {
|
||||||
state.winner = turnOutput.result.winner;
|
state.winner = turnOutput.result.winner;
|
||||||
if (!state.winner) {
|
if (!state.winner) {
|
||||||
state.currentPlayer = state.currentPlayer === 'X' ? 'O' : 'X';
|
state.currentPlayer = state.currentPlayer === 'X' ? 'O' : 'X';
|
||||||
state.turn = turnNumber;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (context.value.winner) break;
|
if (game.value.winner) break;
|
||||||
}
|
}
|
||||||
return context.value;
|
return game.value;
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -144,8 +162,6 @@ registration.add('setup', async function () {
|
||||||
|
|
||||||
## 完整示例
|
## 完整示例
|
||||||
|
|
||||||
参考 [`src/samples/tic-tac-toe.ts`](../src/samples/tic-tac-toe.ts),包含:
|
参考以下示例:
|
||||||
- 2D 棋盘区域
|
- [`src/samples/tic-tac-toe.ts`](../src/samples/tic-tac-toe.ts) - 井字棋:2D 棋盘、玩家轮流输入、胜负判定
|
||||||
- 玩家轮流输入
|
- [`src/samples/boop/`](../src/samples/boop/) - Boop 游戏:六边形棋盘、推动机制、小猫升级
|
||||||
- 胜负判定
|
|
||||||
- 完整的游戏循环
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
## 创建和放置 Part
|
## 创建和放置 Part
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
import { createPart, createRegion } from 'boardgame-core';
|
import { createPart, createRegion, moveToRegion } from 'boardgame-core';
|
||||||
|
|
||||||
const board = createRegion('board', [
|
const board = createRegion('board', [
|
||||||
{ name: 'row', min: 0, max: 2 },
|
{ name: 'row', min: 0, max: 2 },
|
||||||
|
|
@ -11,15 +11,24 @@ const board = createRegion('board', [
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const piece = createPart<{ owner: string }>(
|
const piece = createPart<{ owner: string }>(
|
||||||
{ regionId: 'board', position: [1, 1], owner: 'white' },
|
{ regionId: '', position: [], owner: 'white' },
|
||||||
'piece-1'
|
'piece-1'
|
||||||
);
|
);
|
||||||
|
|
||||||
state.produce(draft => {
|
state.produce(draft => {
|
||||||
draft.parts[piece.id] = piece;
|
draft.parts[piece.id] = piece;
|
||||||
draft.board.childIds.push(piece.id);
|
// 推荐使用 moveToRegion 自动维护 childIds 和 partMap
|
||||||
draft.board.partMap['1,1'] = piece.id;
|
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 池
|
## Part 池
|
||||||
|
|
@ -40,15 +49,23 @@ pool.remaining(); // 剩余数量
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
const parts = createPartsFromTable(
|
const parts = createPartsFromTable(
|
||||||
{ regionId: 'board', owner: 'white' },
|
|
||||||
[
|
[
|
||||||
{ id: 'p1', position: [0, 0] },
|
{ id: 'p1', regionId: 'board', position: [0, 0], owner: 'white' },
|
||||||
{ id: 'p2', position: [1, 1] },
|
{ id: 'p2', regionId: 'board', position: [1, 1], owner: 'black' },
|
||||||
],
|
],
|
||||||
'id'
|
(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
|
```ts
|
||||||
|
|
@ -69,7 +86,11 @@ import { applyAlign, shuffle, moveToRegion } from 'boardgame-core';
|
||||||
|
|
||||||
applyAlign(handRegion, state.parts); // 紧凑排列
|
applyAlign(handRegion, state.parts); // 紧凑排列
|
||||||
shuffle(deckRegion, state.parts, rng); // 打乱
|
shuffle(deckRegion, state.parts, rng); // 打乱
|
||||||
|
|
||||||
|
// 移动棋子:sourceRegion 为 null 表示棋子当前不在区域中
|
||||||
moveToRegion(piece, sourceRegion, targetRegion, [0, 0]);
|
moveToRegion(piece, sourceRegion, targetRegion, [0, 0]);
|
||||||
|
moveToRegion(piece, null, boardRegion, [0, 0]); // 从外部放入区域
|
||||||
|
moveToRegion(piece, boardRegion, null); // 从区域中移除(返回外部)
|
||||||
```
|
```
|
||||||
|
|
||||||
## 翻面与掷骰
|
## 翻面与掷骰
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue