docs: keep docs shorter
This commit is contained in:
parent
ceaf4e8ded
commit
d7d484e4d3
505
README.md
505
README.md
|
|
@ -13,522 +13,37 @@
|
||||||
- **游戏生命周期管理**:`GameHost` 类提供清晰的游戏设置/重置/销毁生命周期
|
- **游戏生命周期管理**:`GameHost` 类提供清晰的游戏设置/重置/销毁生命周期
|
||||||
- **确定性 RNG**:Mulberry32 种子伪随机数生成器,用于可复现的游戏状态
|
- **确定性 RNG**:Mulberry32 种子伪随机数生成器,用于可复现的游戏状态
|
||||||
|
|
||||||
## 安装
|
## 快速开始
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install boardgame-core
|
npm install boardgame-core
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 使用 GameHost
|
|
||||||
|
|
||||||
`GameHost` 是游戏运行的核心容器,负责管理游戏状态、命令执行和玩家交互的生命周期。
|
|
||||||
|
|
||||||
### 创建 GameHost
|
|
||||||
|
|
||||||
通过 `createGameHost` 传入一个 GameModule 来创建:
|
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
import { createGameHost } from 'boardgame-core';
|
import { createGameHost } from 'boardgame-core';
|
||||||
import * as tictactoe from 'boardgame-core/samples/tic-tac-toe';
|
import * as tictactoe from 'boardgame-core/samples/tic-tac-toe';
|
||||||
|
|
||||||
const host = createGameHost(tictactoe);
|
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');
|
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(() => {
|
effect(() => {
|
||||||
const state = host.state.value;
|
const state = host.state.value;
|
||||||
console.log(`${state.currentPlayer}'s turn (turn ${state.turn + 1})`);
|
console.log(`${state.currentPlayer}'s turn`);
|
||||||
if (state.winner) {
|
|
||||||
console.log('Winner:', state.winner);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 启动游戏
|
// 处理玩家输入
|
||||||
await host.setup('setup');
|
const error = host.onInput('play X 1 2');
|
||||||
|
|
||||||
// 游戏循环:等待提示 → 提交输入
|
|
||||||
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();
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
## 文档导航
|
||||||
|
|
||||||
## 编写 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<string, Part>,
|
|
||||||
currentPlayer: 'X' as PlayerType,
|
|
||||||
winner: null as WinnerType,
|
|
||||||
turn: 0,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. 创建命令注册表并注册命令
|
|
||||||
const registration = createGameCommandRegistry<ReturnType<typeof createInitialState>>();
|
|
||||||
export const registry = registration.registry;
|
|
||||||
|
|
||||||
// 注册命令
|
|
||||||
registration.add('setup', async function () {
|
|
||||||
// ... 命令逻辑
|
|
||||||
});
|
|
||||||
|
|
||||||
registration.add('play <player> <row:number> <col:number>', 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<string, TicTacToePart>,
|
|
||||||
currentPlayer: 'X' as PlayerType,
|
|
||||||
winner: null as WinnerType,
|
|
||||||
turn: 0,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
export type GameState = ReturnType<typeof createInitialState>;
|
|
||||||
```
|
|
||||||
|
|
||||||
状态通常包含:
|
|
||||||
- **Region**:用 `createRegion()` 创建的空间区域
|
|
||||||
- **parts**:`Record<string, Part>` 游戏棋子集合
|
|
||||||
- 游戏特有的字段:当前玩家、分数、回合数等
|
|
||||||
|
|
||||||
### 注册命令
|
|
||||||
|
|
||||||
使用 `registration.add()` 注册命令。Schema 字符串定义了命令格式:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
registration.add('play <player> <row:number> <col:number>', async function (cmd) {
|
|
||||||
const [player, row, col] = cmd.params as [PlayerType, number, number];
|
|
||||||
|
|
||||||
// this.context 是 MutableSignal<GameState>
|
|
||||||
this.context.produce(state => {
|
|
||||||
state.parts[piece.id] = piece;
|
|
||||||
});
|
|
||||||
|
|
||||||
return { winner: null };
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Schema 语法
|
|
||||||
|
|
||||||
| 语法 | 含义 |
|
|
||||||
|---|---|
|
|---|---|
|
||||||
| `name` | 命令名 |
|
| [使用 GameHost](docs/game-host.md) | GameHost 生命周期、响应式状态、事件处理 |
|
||||||
| `<param>` | 必填参数(字符串) |
|
| [编写 GameModule](docs/game-module.md) | 定义状态、注册命令、prompt 系统、Part/Region 使用 |
|
||||||
| `<param:number>` | 必填参数(自动转为数字) |
|
| [API 参考](docs/api-reference.md) | 所有导出 API 的完整列表 |
|
||||||
| `[--flag]` | 可选标志 |
|
| [开发指南](docs/development.md) | 安装、构建脚本、测试命令 |
|
||||||
| `[-x:number]` | 可选选项(带类型) |
|
|
||||||
|
|
||||||
#### 命令处理器中的 this
|
|
||||||
|
|
||||||
命令处理器中的 `this` 是 `CommandRunnerContext<MutableSignal<TState>>`:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
registration.add('myCommand <arg>', async function (cmd) {
|
|
||||||
// 读取状态
|
|
||||||
const state = this.context.value;
|
|
||||||
|
|
||||||
// 修改状态
|
|
||||||
this.context.produce(draft => {
|
|
||||||
draft.currentPlayer = 'O';
|
|
||||||
});
|
|
||||||
|
|
||||||
// 提示玩家输入
|
|
||||||
const result = await this.prompt(
|
|
||||||
'confirm <action>',
|
|
||||||
(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 <player> <turn:number>', async function (cmd) {
|
|
||||||
const [turnPlayer, turnNumber] = cmd.params as [PlayerType, number];
|
|
||||||
|
|
||||||
// 等待玩家输入
|
|
||||||
const playCmd = await this.prompt(
|
|
||||||
'play <player> <row:number> <col:number>', // 期望的输入格式
|
|
||||||
(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<TState>()` | 创建命令注册表,返回带 `.add()` 的对象 |
|
|
||||||
| `createGameModule(module)` | 辅助函数,标记一个对象为 GameModule |
|
|
||||||
| `GameHost<TState>` | 游戏生命周期管理类 |
|
|
||||||
| `createGameHost(module, options?)` | 从 GameModule 创建 GameHost |
|
|
||||||
| `GameHostStatus` | 类型: `'created' \| 'running' \| 'disposed'` |
|
|
||||||
|
|
||||||
### 棋子 (Parts)
|
|
||||||
|
|
||||||
| 导出 | 说明 |
|
|
||||||
|---|---|
|
|
||||||
| `Part<TMeta>` | 游戏棋子类型 |
|
|
||||||
| `PartTemplate<TMeta>` | 创建棋子的模板类型 |
|
|
||||||
| `PartPool<TMeta>` | 棋子池 |
|
|
||||||
| `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 类型检查
|
|
||||||
```
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
# API 参考
|
||||||
|
|
||||||
|
## 核心
|
||||||
|
|
||||||
|
| 导出 | 说明 |
|
||||||
|
|---|---|
|
||||||
|
| `IGameContext` | 游戏上下文基础接口 |
|
||||||
|
| `createGameContext(registry, initialState?)` | 创建游戏上下文实例 |
|
||||||
|
| `createGameCommandRegistry<TState>()` | 创建命令注册表,返回带 `.add()` 的对象 |
|
||||||
|
| `createGameModule(module)` | 辅助函数,标记一个对象为 GameModule |
|
||||||
|
| `GameHost<TState>` | 游戏生命周期管理类 |
|
||||||
|
| `createGameHost(module, options?)` | 从 GameModule 创建 GameHost |
|
||||||
|
| `GameHostStatus` | 类型: `'created' \| 'running' \| 'disposed'` |
|
||||||
|
|
||||||
|
## 棋子 (Parts)
|
||||||
|
|
||||||
|
| 导出 | 说明 |
|
||||||
|
|---|---|
|
||||||
|
| `Part<TMeta>` | 游戏棋子类型 |
|
||||||
|
| `PartTemplate<TMeta>` | 创建棋子的模板类型 |
|
||||||
|
| `PartPool<TMeta>` | 棋子池 |
|
||||||
|
| `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 |
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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();
|
||||||
|
```
|
||||||
|
|
@ -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<string, Part>,
|
||||||
|
currentPlayer: 'X' as PlayerType,
|
||||||
|
winner: null as WinnerType,
|
||||||
|
turn: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 创建命令注册表并注册命令
|
||||||
|
const registration = createGameCommandRegistry<ReturnType<typeof createInitialState>>();
|
||||||
|
export const registry = registration.registry;
|
||||||
|
|
||||||
|
// 注册命令
|
||||||
|
registration.add('setup', async function () {
|
||||||
|
// ... 命令逻辑
|
||||||
|
});
|
||||||
|
|
||||||
|
registration.add('play <player> <row:number> <col:number>', 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<string, TicTacToePart>,
|
||||||
|
currentPlayer: 'X' as PlayerType,
|
||||||
|
winner: null as WinnerType,
|
||||||
|
turn: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
export type GameState = ReturnType<typeof createInitialState>;
|
||||||
|
```
|
||||||
|
|
||||||
|
状态通常包含:
|
||||||
|
- **Region**:用 `createRegion()` 创建的空间区域
|
||||||
|
- **parts**:`Record<string, Part>` 游戏棋子集合
|
||||||
|
- 游戏特有的字段:当前玩家、分数、回合数等
|
||||||
|
|
||||||
|
## 注册命令
|
||||||
|
|
||||||
|
使用 `registration.add()` 注册命令。Schema 字符串定义了命令格式:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
registration.add('play <player> <row:number> <col:number>', async function (cmd) {
|
||||||
|
const [player, row, col] = cmd.params as [PlayerType, number, number];
|
||||||
|
|
||||||
|
// this.context 是 MutableSignal<GameState>
|
||||||
|
this.context.produce(state => {
|
||||||
|
state.parts[piece.id] = piece;
|
||||||
|
});
|
||||||
|
|
||||||
|
return { winner: null };
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Schema 语法
|
||||||
|
|
||||||
|
| 语法 | 含义 |
|
||||||
|
|---|---|
|
||||||
|
| `name` | 命令名 |
|
||||||
|
| `<param>` | 必填参数(字符串) |
|
||||||
|
| `<param:number>` | 必填参数(自动转为数字) |
|
||||||
|
| `[--flag]` | 可选标志 |
|
||||||
|
| `[-x:number]` | 可选选项(带类型) |
|
||||||
|
|
||||||
|
### 命令处理器中的 this
|
||||||
|
|
||||||
|
命令处理器中的 `this` 是 `CommandRunnerContext<MutableSignal<TState>>`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
registration.add('myCommand <arg>', async function (cmd) {
|
||||||
|
// 读取状态
|
||||||
|
const state = this.context.value;
|
||||||
|
|
||||||
|
// 修改状态
|
||||||
|
this.context.produce(draft => {
|
||||||
|
draft.currentPlayer = 'O';
|
||||||
|
});
|
||||||
|
|
||||||
|
// 提示玩家输入
|
||||||
|
const result = await this.prompt(
|
||||||
|
'confirm <action>',
|
||||||
|
(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 <player> <turn:number>', async function (cmd) {
|
||||||
|
const [turnPlayer, turnNumber] = cmd.params as [PlayerType, number];
|
||||||
|
|
||||||
|
// 等待玩家输入
|
||||||
|
const playCmd = await this.prompt(
|
||||||
|
'play <player> <row:number> <col:number>', // 期望的输入格式
|
||||||
|
(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 棋盘区域
|
||||||
|
- 玩家轮流输入
|
||||||
|
- 胜负判定
|
||||||
|
- 完整的游戏循环
|
||||||
Loading…
Reference in New Issue