From b8e4f731269824fc014d00f2f91d7940e64bf6e3 Mon Sep 17 00:00:00 2001 From: hyper Date: Fri, 10 Apr 2026 12:47:00 +0800 Subject: [PATCH] refactor: rewritten skill --- .qwen/skills/create-game-module/SKILL.md | 246 ++++++ .../create-game-module/references/api.md | 759 ++++++++++++++++++ skills/game-module.md | 117 --- 3 files changed, 1005 insertions(+), 117 deletions(-) create mode 100644 .qwen/skills/create-game-module/SKILL.md create mode 100644 .qwen/skills/create-game-module/references/api.md delete mode 100644 skills/game-module.md diff --git a/.qwen/skills/create-game-module/SKILL.md b/.qwen/skills/create-game-module/SKILL.md new file mode 100644 index 0000000..60d57e0 --- /dev/null +++ b/.qwen/skills/create-game-module/SKILL.md @@ -0,0 +1,246 @@ +--- +name: Create Game Module +description: Create a runnable logic module for a board game with 'boardgame-core', '@preact/signals' and 'mutative' +--- +# 如何编写游戏模组 + +## 要求 + +游戏模组需要导出以下接口: + +```typescript +import { IGameContext, createGameCommandRegistry } from '@/index'; + +// 定义类型 +export type GameState = { + //... +}; +export type Game = IGameContext; + +// 创建 mutative 游戏初始状态 +export function createInitialState(): GameState { + //... +} + +// 创建命令注册表(可选) +export const registry = createGameCommandRegistry(); + +// 运行游戏 +export async function start(game: Game) { + // ... +} +``` + +或者导出为 `GameModule` 对象: + +```typescript +import { GameModule } from '@/index'; + +export const gameModule: GameModule = { + registry, + createInitialState, + start, +}; +``` + +## 流程 + +### 0. 确认规则 + +规则应当存放在 `rule.md`。 + +描述一个桌面游戏的以下要素: +- **主题**:游戏的世界观和背景 +- **配件**:棋子、卡牌、骰子等物理组件 +- **游戏布置形式**:棋盘、版图、卡牌放置区等 +- **游戏流程**:回合结构、阶段划分 +- **玩家行动**:每回合玩家可以做什么 +- **胜利条件与终局结算**:如何判定胜负 + +### 1. 创建类型 + +创建 `types.ts` 并导出游戏所用的类型。 + +- 为游戏概念创建字符串枚举类型(如 `PlayerType = 'X' | 'O'`) +- 使用 `Part` 为游戏配件创建对象类型 +- 使用 `Region` 为游戏区域创建容器类型 +- 设计游戏的全局状态类型 + +游戏使用 `mutative` 不可变类型驱动。状态类型必须是**可序列化的**(不支持函数、`Map`、`Set` 等)。 + +```typescript +import { Part, Region } from '@/index'; + +export type PlayerType = 'X' | 'O'; + +export type PieceMeta = { + owner: PlayerType; +}; + +export type Piece = Part; + +export type GameState = { + board: Region; + pieces: Record; + currentPlayer: PlayerType; + turn: number; + winner: PlayerType | null; +}; +``` + +### 2. 创建游戏配件表 + +若某种游戏配件数量较大,可使用csv文件进行配置,否则在代码中inline创建。 + +csv文件遵循以下要求: +- 从`#`开头的内容会被当作注释忽略 +- 第二行为数据类型,会用于生成.d.ts文件 +- 可以有空行 + +```csv +# parts.csv +type,player,count +string,string,int +kitten,white,8 +kitten,black,8 +cat,white,8 +cat,black,8 +- ``` + +```typescript +import parts from "./parts.csv"; +const pieces = createPartsFromTable( + parts, + (item, index) => `${item.player}-${item.type}-${index + 1}`, + (item) => item.count +) as Record; +``` + +### 3. 创建 Prompts + +使用 prompt 来描述需要玩家进行的行动命令 schema。 + +- prompt 包含一个 schema 和若干参数 +- 每个参数通常指定某个配件 ID、某个枚举字符串、或者某个数字 +- 参数类型必须是原始类型(`string`、`number`)或字符串枚举 + +```typescript +import { createPromptDef } from '@/index'; + +export const prompts = { + play: createPromptDef<[PlayerType, number, number]>( + 'play ', + '选择下子位置' + ), +}; +``` + +Prompt schema 语法: +- `` - 必需参数 +- `[param]` - 可选参数 +- `[param:type]` - 带类型验证的参数(如 `[count:number]`) + +### 3. 创建游戏流程 + +游戏主循环负责协调游戏进程、等待玩家输入、更新状态。 + +```typescript +export async function start(game: Game) { + while (true) { + // game.value 可获取当前的全局状态 + const currentPlayer = game.value.currentPlayer; + const turnNumber = game.value.turn + 1; + const turnOutput = await turn(game, currentPlayer, turnNumber); + + // 更新状态 + await game.produceAsync((state) => { + state.winner = turnOutput.winner; + if (!state.winner) { + state.currentPlayer = state.currentPlayer === 'X' ? 'O' : 'X'; + state.turn = turnNumber; + } + }); + + // 检查游戏结束条件 + if (game.value.winner) break; + } + + return game.value; +} +``` + +回合逻辑示例: + +```typescript +async function turn(game: Game, turnPlayer: PlayerType, turnNumber: number) { + // 获取玩家输入 + const { player, row, col } = await game.prompt( + prompts.play, + (player, row, col) => { + if (player !== turnPlayer) { + throw `无效的玩家: ${player}。应为 ${turnPlayer}。`; + } else if (!isValidMove(row, col)) { + throw `无效位置: (${row}, ${col})。必须在 0 到 ${BOARD_SIZE - 1} 之间。`; + } else if (isCellOccupied(game, row, col)) { + throw `格子 (${row}, ${col}) 已被占用。`; + } else { + return { player, row, col }; + } + }, + game.value.currentPlayer + ); + + // 执行放置逻辑 + placePiece(game, row, col, turnPlayer); + + // 返回回合结果 + return { winner: checkWinner(game) }; +} +``` + +**注意事项:** +- `game.produce(fn)` 用于同步更新状态 +- `game.produceAsync(fn)` 用于异步更新状态(会等待中断 Promise 完成,适用于播放动画) +- 验证器函数中抛出字符串错误会返回给玩家,玩家可重新输入 +- 循环必须有明确的退出条件,避免无限循环 +- 玩家取消输入时,`game.prompt()` 会抛出异常,需要适当处理 + +### 4. 创建测试 + +基于 `createGameContext` 来测试游戏逻辑。 + +- 为每种游戏结束条件准备至少一条测试 +- 为每种玩家行动准备至少一条测试 +- 使用 `createTestContext()` 和 `createTestRegion()` 测试辅助函数 + +```typescript +import { createGameContext } from '@/core/game'; +import { createInitialState, registry, start } from './my-game'; + +describe('My Game', () => { + it('should detect horizontal win for X', async () => { + const ctx = createGameContext({ + initialState: createInitialState(), + registry, + }); + + // 执行一系列操作 + await ctx.run('play X 0 0'); + await ctx.run('play O 1 0'); + await ctx.run('play X 0 1'); + await ctx.run('play O 1 1'); + await ctx.run('play X 0 2'); + + expect(ctx.value.winner).toBe('X'); + }); +}); +``` + +## 完整示例 + +参考 `src/samples/boop/` 获取完整的`boop`游戏实现。 + +## 相关资源 + +- [API 参考](./references/api.md) - 完整的 API 文档 +- [AGENTS.md](../../AGENTS.md) - 项目代码规范和架构说明 \ No newline at end of file diff --git a/.qwen/skills/create-game-module/references/api.md b/.qwen/skills/create-game-module/references/api.md new file mode 100644 index 0000000..0582868 --- /dev/null +++ b/.qwen/skills/create-game-module/references/api.md @@ -0,0 +1,759 @@ +# API 参考 + +本文档记录游戏模组开发者需要使用的公共 API。 + +## 核心接口 + +### `IGameContext` + +游戏运行的核心上下文,提供状态访问、随机数、命令执行和提示系统。 + +```typescript +interface IGameContext = {}> { + readonly value: TState; // 当前游戏状态(只读) + readonly rng: ReadonlyRNG; // 随机数生成器(只读) + + // 状态更新 + produce(fn: (draft: TState) => void): void; // 同步变更状态(基于 mutative) + produceAsync(fn: (draft: TState) => void): Promise; // 异步变更状态(等待中断) + + // 命令执行 + run(input: string): Promise>; // 执行命令字符串 + runParsed(command: Command): Promise>; // 执行已解析的命令 + + // 提示系统 + prompt( + def: PromptDef, + validator: PromptValidator, + currentPlayer?: string | null + ): Promise; +} +``` + +**使用示例:** + +```typescript +// 读取状态 +const currentPlayer = game.value.currentPlayer; + +// 同步更新状态 +game.produce((state) => { + state.score += 10; +}); + +// 异步更新状态(等待动画完成) +await game.produceAsync((state) => { + state.phase = 'next'; +}); + +// 等待玩家输入 +const result = await game.prompt( + prompts.move, + (from, to) => { + if (!isValidMove(from, to)) { + throw '无效移动'; + } + return { from, to }; + } +); +``` + +--- + +### `GameModule` + +游戏模块的类型定义,这是开发者创建游戏时需要导出的核心结构。 + +```typescript +type GameModule, TResult = unknown> = { + registry?: CommandRegistry>; // 可选的命令注册表 + createInitialState: () => TState; // 创建初始状态 + start: (ctx: IGameContext) => Promise; // 游戏主循环 +} +``` + +--- + +### `createGameCommandRegistry()` + +创建游戏命令注册表,游戏模组用它来注册自定义命令。 + +```typescript +function createGameCommandRegistry = {}>(): CommandRegistry> +``` + +**使用示例:** + +```typescript +import { createGameCommandRegistry, IGameContext } from '@/index'; + +export type GameState = { score: number }; +export const registry = createGameCommandRegistry(); + +// 注册命令 +registry.register('addScore ', async function(ctx, amount) { + ctx.produce((state) => { + state.score += amount; + }); + return { success: true, result: undefined }; +}); +``` + +--- + +## 提示系统 + +### `createPromptDef(schema, hintText?)` + +从字符串模式创建 `PromptDef`。 + +```typescript +function createPromptDef( + schema: CommandSchema | string, + hintText?: string +): PromptDef +``` + +**使用示例:** + +```typescript +import { createPromptDef } from '@/index'; + +export const prompts = { + // 必需参数 + play: createPromptDef<[PlayerType, number, number]>( + 'play ', + '选择下子位置' + ), + + // 可选参数 + draw: createPromptDef<[number?]>( + 'draw [count:number]', + '抽牌' + ), + + // 带选项 + trade: createPromptDef<[string, string]>( + 'trade [--force]', + '交易' + ), +}; +``` + +--- + +### `PromptDef` + +提示定义,用于 `context.prompt()` 方法。 + +```typescript +type PromptDef = { + schema: CommandSchema; // 命令模式定义 + hintText?: string; // 可选的提示文本 +} +``` + +--- + +### `PromptValidator` + +提示验证函数类型。验证器函数接收解析后的参数,应返回结果或抛出字符串错误。 + +```typescript +type PromptValidator = (...params: TArgs) => TResult; +``` + +**验证器规则:** +- 返回任意值表示验证成功,该值将作为 `prompt()` 的返回值 +- 抛出字符串错误表示验证失败,错误消息会返回给玩家,玩家可重新输入 +- 玩家取消输入时,`prompt()` 会抛出异常 + +--- + +### `PromptEvent` + +提示事件对象,通过 `commandRunnerContext.on('prompt', handler)` 监听。 + +```typescript +type PromptEvent = { + schema: CommandSchema; + hintText?: string; + currentPlayer: string | null; + tryCommit: (commandOrInput: Command | string) => string | null; // null=成功,string=错误消息 + cancel: (reason?: string) => void; +} +``` + +--- + +## 零件系统 (Part) + +### `Part` + +游戏中的可操作物件(棋子、卡牌、骰子等)。 + +```typescript +type Part = { + id: string; // 唯一标识 + sides?: number; // 总面数(用于骰子/多面牌) + side?: number; // 当前面 + alignments?: string[]; // 可用对齐方式 + alignment?: string; // 当前对齐方式 + regionId: string; // 所属区域 ID + position: number[]; // 在区域中的位置坐标 +} & Immutable; // 自定义元数据(不可变) +``` + +**使用示例:** + +```typescript +import { Part } from '@/index'; + +export type PieceMeta = { + owner: 'X' | 'O'; + type: 'pawn' | 'king'; +}; + +export type Piece = Part; + +// 访问元数据 +const piece: Piece = ...; +console.log(piece.owner); // 'X' +console.log(piece.type); // 'pawn' +``` + +--- + +### 零件操作函数 + +#### `flip(part)` + +翻转到下一面(循环)。 + +```typescript +function flip(part: Part): void +``` + +#### `flipTo(part, side)` + +翻转到指定面。 + +```typescript +function flipTo(part: Part, side: number): void +``` + +#### `roll(part, rng)` + +用 RNG 随机掷骰子。 + +```typescript +function roll(part: Part, rng: RNG): void +``` + +--- + +## 零件工厂 (Part Factory) + +### `createParts(item, getId, count?)` + +创建多个相同类型的零件。 + +```typescript +function createParts( + item: T, + getId: (index: number) => string, + count?: number +): Record> +``` + +**使用示例:** + +```typescript +import { createParts } from '@/index'; + +const pieces = createParts( + { owner: 'X', type: 'pawn' }, + (i) => `piece-x-${i}`, + 5 // 创建 5 个 +); +``` + +--- + +### `createPartsFromTable(items, getId, getCount?)` + +从配置表批量创建零件。 + +```typescript +function createPartsFromTable( + items: readonly T[], + getId: (item: T, index: number) => string, + getCount?: ((item: T) => number) | number +): Record> +``` + +**使用示例:** + +```typescript +import { createPartsFromTable } from '@/index'; + +const cardTable = [ + { name: 'fireball', damage: 3 }, + { name: 'shield', defense: 2 }, +]; + +const cards = createPartsFromTable( + cardTable, + (item) => item.name, + (item) => item.name === 'fireball' ? 4 : 2 // 每种卡牌的数量 +); +``` + +--- + +## 区域系统 (Region) + +### `Region` + +游戏区域(棋盘、手牌区等)。 + +```typescript +type Region = { + id: string; // 区域 ID + axes: RegionAxis[]; // 坐标轴定义 + childIds: string[]; // 包含的零件 ID 列表 + partMap: Record; // 位置 -> 零件 ID 映射 +} +``` + +--- + +### `RegionAxis` + +区域的一个坐标轴。 + +```typescript +type RegionAxis = { + name: string; + min?: number; + max?: number; + align?: 'start' | 'end' | 'center'; // 对齐方式 +} +``` + +--- + +### `createRegion(id, axes)` + +创建区域。 + +```typescript +function createRegion(id: string, axes: RegionAxis[]): Region +``` + +**使用示例:** + +```typescript +import { createRegion, createRegionAxis } from '@/index'; + +// 创建 3x3 棋盘 +const board = createRegion('board', [ + createRegionAxis('row', 0, 2), + createRegionAxis('col', 0, 2), +]); + +// 或简写 +const board = createRegion('board', [ + { name: 'row', min: 0, max: 2 }, + { name: 'col', min: 0, max: 2 }, +]); +``` + +--- + +### `createRegionAxis(name, min?, max?, align?)` + +创建坐标轴。 + +```typescript +function createRegionAxis( + name: string, + min?: number, + max?: number, + align?: 'start' | 'end' | 'center' +): RegionAxis +``` + +--- + +### 区域操作函数 + +#### `applyAlign(region, parts)` + +根据轴的 `align` 配置重新排列零件位置。 + +```typescript +function applyAlign(region: Region, parts: Record>): void +``` + +#### `shuffle(region, parts, rng)` + +在区域内随机打乱零件位置。 + +```typescript +function shuffle(region: Region, parts: Record>, rng: RNG): void +``` + +#### `moveToRegion(part, sourceRegion, targetRegion, position?)` + +将零件从一个区域移动到另一个区域。 + +```typescript +function moveToRegion( + part: Part, + sourceRegion: Region | null, + targetRegion: Region | null, + position?: number[] +): void +``` + +--- + +## 命令系统 (Command System) + +### `Command` + +解析后的命令对象。 + +```typescript +type Command = { + name: string; // 命令名 + flags: Record; // 标志(如 --verbose) + options: Record; // 选项(如 --player X) + params: unknown[]; // 位置参数 +} +``` + +--- + +### `CommandSchema` + +命令模式定义,用于验证和解析。 + +```typescript +type CommandSchema = { + name: string; + params: CommandParamSchema[]; + options: Record; + flags: Record; +} +``` + +--- + +### `CommandResult` + +命令执行结果(判别联合类型)。 + +```typescript +type CommandResult = + | { success: true; result: T } + | { success: false; error: string } +``` + +**使用示例:** + +```typescript +const result = await game.run('move piece1 piece2'); + +if (result.success) { + console.log('命令执行成功', result.result); +} else { + console.error('命令执行失败', result.error); +} +``` + +--- + +### `CommandDef` + +命令定义对象,用于 `registry.register()`。 + +```typescript +type CommandDef> = { + schema: string | CommandSchema; + run: TFunc; +} + +type CommandFunction = (ctx: TContext, ...args: any[]) => Promise; +``` + +--- + +### `CommandRegistry` + +命令注册表。 + +```typescript +class CommandRegistry extends Map> { + register( + ...args: [schema: CommandSchema | string, run: TFunc] | [CommandDef] + ): (ctx, ...args) => Promise +} +``` + +**注册命令的两种方式:** + +```typescript +// 方式 1:直接传入 schema 和函数 +registry.register('move ', async function(ctx, from, to) { + ctx.produce((state) => { /* 修改状态 */ }); + return { success: true, result: undefined }; +}); + +// 方式 2:使用 CommandDef 对象 +registry.register({ + schema: 'move ', + run: async function(ctx, from, to) { + ctx.produce((state) => { /* 修改状态 */ }); + return { success: true, result: undefined }; + } +}); +``` + +--- + +### `parseCommand(input, schema?)` + +解析命令字符串为 `Command` 对象。 + +```typescript +function parseCommand(input: string, schema?: CommandSchema): Command +``` + +--- + +### `parseCommandSchema(schemaStr, name?)` + +从字符串模式解析命令模式。 + +```typescript +function parseCommandSchema(schemaStr: string, name?: string): CommandSchema +``` + +**Schema 语法:** +- `` - 必需参数 +- `[param]` - 可选参数 +- `[param:type]` - 带类型验证的参数(如 `[count:number]`) +- `--option:value` - 必需选项 +- `[-o value]` - 可选选项 +- `[--flag]` - 可选标志 + +--- + +## 随机数生成器 (RNG) + +### `ReadonlyRNG` + +只读 RNG 接口(`IGameContext.rng` 返回此类型)。 + +```typescript +interface ReadonlyRNG { + next(max?: number): number; // [0,1) 随机数,或 [0,max) + nextInt(max: number): number; // [0,max) 随机整数 +} +``` + +--- + +### `RNG` + +可设置种子的 RNG 接口。 + +```typescript +interface RNG extends ReadonlyRNG { + setSeed(seed: number): void; + getSeed(): number; +} +``` + +**使用示例:** + +```typescript +// 在 IGameContext 中使用 +const roll = game.rng.nextInt(6) + 1; // 1-6 的随机数 + +// 在区域操作中使用时 +shuffle(region, parts, rng); // 需要传入 RNG +``` + +--- + +## 可变信号 (MutableSignal) + +### `MutableSignal` + +扩展自 Preact Signal 的可变信号类,支持 mutative-style 的 `produce` 方法。 + +```typescript +class MutableSignal extends Signal { + produce(fn: (draft: T) => void): void; + addInterruption(promise: Promise): void; // 添加中断 Promise(用于动画等待) + clearInterruptions(): void; // 清除所有中断 + produceAsync(fn: (draft: T) => void): Promise; // 等待中断后更新状态 +} +``` + +--- + +### `mutableSignal(initial?, options?)` + +创建可变信号。 + +```typescript +function mutableSignal(initial?: T, options?: SignalOptions): MutableSignal +``` + +--- + +## 游戏主机 (GameHost) + +### `GameHost` + +游戏会话的生命周期管理器。 + +```typescript +class GameHost { + readonly state: ReadonlySignal; // 游戏状态(响应式) + readonly status: ReadonlySignal; // 运行状态 + readonly activePromptSchema: ReadonlySignal; // 当前活动提示的模式 + readonly activePromptPlayer: ReadonlySignal; // 当前等待输入的玩家 + readonly activePromptHint: ReadonlySignal; // 当前提示文本 + + tryInput(input: string): string | null; // 尝试提交输入,返回错误信息或 null + tryAnswerPrompt(def: PromptDef, ...args: TArgs): void; // 尝试回答提示 + addInterruption(promise: Promise): void; // 注册中断 Promise(用于动画) + clearInterruptions(): void; // 清除所有中断 + start(seed?: number): Promise; // 启动游戏 + dispose(): void; // 销毁游戏 + on(event: 'start' | 'dispose', listener: () => void): () => void; // 注册事件监听 +} +``` + +--- + +### `GameHostStatus` + +```typescript +type GameHostStatus = 'created' | 'running' | 'disposed'; +``` + +--- + +### `createGameHost(gameModule)` + +从游戏模块创建 `GameHost` 实例。 + +```typescript +function createGameHost>( + gameModule: GameModule +): GameHost +``` + +**使用示例:** + +```typescript +import { createGameHost } from '@/index'; +import { gameModule } from './my-game'; + +const host = createGameHost(gameModule); + +// 启动游戏 +const result = await host.start(42); // 传入种子 + +// 提交玩家输入 +const error = host.tryInput('play X 0 0'); +if (error) { + console.error('输入错误:', error); +} + +// 监听事件 +host.on('start', () => console.log('游戏开始')); +host.on('dispose', () => console.log('游戏结束')); + +// 销毁游戏 +host.dispose(); +``` + +--- + +## Preact Signals 重新导出 + +```typescript +export * from '@preact/signals-core'; +``` + +开发者可直接使用 `@preact/signals-core` 的所有导出,包括: +- `Signal` - 基础信号类 +- `ReadonlySignal` - 只读信号类型 +- `signal(value)` - 创建信号 +- `computed(fn)` - 创建计算信号 +- `effect(fn)` - 创建副作用 +- `batch(fn)` - 批量更新 +- `untracked(fn)` - 非追踪读取 + +--- + +## 测试辅助函数 + +以下函数主要用于测试代码: + +### `createGameContext(options)` + +创建游戏上下文实例。 + +```typescript +function createGameContext(options: { + initialState: TState; + registry?: CommandRegistry>; + rng?: RNG; +}): IGameContext +``` + +**使用示例:** + +```typescript +import { createGameContext } from '@/core/game'; +import { createInitialState, registry } from './my-game'; + +const ctx = createGameContext({ + initialState: createInitialState(), + registry, +}); + +// 执行命令 +await ctx.run('move piece1 piece2'); + +// 断言状态 +expect(ctx.value.score).toBe(10); +``` + +--- + +### `createTestContext()` + +创建用于测试的游戏上下文(简化版)。 + +```typescript +function createTestContext(initialState: TState): IGameContext +``` + +--- + +### `createTestRegion()` + +创建用于测试的区域。 + +```typescript +function createTestRegion(): Region +``` diff --git a/skills/game-module.md b/skills/game-module.md deleted file mode 100644 index 79f3341..0000000 --- a/skills/game-module.md +++ /dev/null @@ -1,117 +0,0 @@ -# 如何编写游戏模组 - -## 要求 - -游戏模组需要以下接口导出: -``` -import {createGameCommandRegistry, IGameContext} from "boardgame-core"; - -// 定义类型 -export type GameState = { - //... -} -export type Game = IGameContext; - -// 创建mutative游戏初始状态 -export function createInitialState(): GameState { - //... -} - -// 运行游戏 -export async function start(game: Game) { - // ... -} -``` - -## 流程 - -0. 确认规则。 - -规则应当存放在`rule.md`。 - -描述一个桌面游戏的以下要素: -- 主题 -- 配件 -- 游戏布置形式 -- 游戏流程 -- 玩家行动 -- 胜利条件与终局结算 - -1. 创建类型:创建`types.ts`并导出游戏所用的类型。 - -- 为游戏概念创建字符串枚举类型。 -- 使用`@/core/part.ts`为游戏配件创建对象类型。 -- 使用`@/core/region.ts`为游戏区域创建容器类型。 -- 设计游戏的全局状态类型。 - -游戏使用`mutative`不可变类型驱动。 - -2. 创建prompts: - -使用prompt来描述需要玩家进行的行动命令schema。 - -- prompt包含一个id和若干params。 -- 每个param通常指定某个配件id、某个枚举字符串、或者某个数字。 - -```typescript -export const prompts = { - play: createPromptDef<[PlayerType, number, number]>( - 'play ', - '选择下子位置') -} -``` - -3. 创建游戏流程: - -```typescript -export async function start(game: TicTacToeGame) { - while (true) { - // game.value可获取当前的全局状态 - const currentPlayer = game.value.currentPlayer; - const turnNumber = game.value.turn + 1; - const turnOutput = await turn(game, currentPlayer, turnNumber); - - // 更新状态 - await game.produceAsync(state => { - state.winner = turnOutput.winner; - if (!state.winner) { - state.currentPlayer = state.currentPlayer === 'X' ? 'O' : 'X'; - state.turn = turnNumber; - } - }); - if (game.value.winner) break; - } - - return game.value; -} -``` - -```typescript -async function run(game: TicTacToeGame, turnPlayer: PlayerType, turnNumber: number) { - // 获取玩家输入 - const {player, row, col} = await game.prompt( - prompts.play, - (player, row, col) => { - if (player !== turnPlayer) { - throw `Invalid player: ${player}. Expected ${turnPlayer}.`; - } else if (!isValidMove(row, col)) { - throw `Invalid position: (${row}, ${col}). Must be between 0 and ${BOARD_SIZE - 1}.`; - } else if (isCellOccupied(game, row, col)) { - throw `Cell (${row}, ${col}) is already occupied.`; - } else { - return {player, row, col}; - } - }, - game.value.currentPlayer - ); - - placePiece(game, row, col, turnPlayer); -} -``` - -4. 创建测试 - -基于`@/core/game.ts`的createGameContext来测试。 - -为每种游戏结束条件准备至少一条测试。 -为每种玩家行动至少准备一条测试。 \ No newline at end of file