with one click
create-new-game
// 为本项目创建新游戏或先做新游戏资源/data intake。当用户要求新增游戏,或只给图片/位置就希望先开工时使用。基于 dicethrone/summonerwars/smashup 的真实模式,分阶段完成,并带启动询问、素材 intake 与验收门禁。
// 为本项目创建新游戏或先做新游戏资源/data intake。当用户要求新增游戏,或只给图片/位置就希望先开工时使用。基于 dicethrone/summonerwars/smashup 的真实模式,分阶段完成,并带启动询问、素材 intake 与验收门禁。
[HINT] Download the complete skill directory including SKILL.md and all related files
| name | create-new-game |
| description | 为本项目创建新游戏或先做新游戏资源/data intake。当用户要求新增游戏,或只给图片/位置就希望先开工时使用。基于 dicethrone/summonerwars/smashup 的真实模式,分阶段完成,并带启动询问、素材 intake 与验收门禁。 |
核心原则:每个阶段独立可验证、独立可提交。阶段之间不留 TODO 缺口。AI 必须在完成当前阶段验收后才能进入下一阶段。
本 skill 只做“分阶段流程 + 验收门禁 + 最小闭环”。 任何规范/红线/最佳实践若在下列文档中已有定义,必须以它们为准;本 skill 不重复展开。
AGENTS.mddocs/ai-rules/engine-systems.mddocs/ai-rules/ui-ux.mddocs/ai-rules/golden-rules.mddocs/ai-rules/animation-effects.mddocs/ai-rules/data-entry.mddocs/ai-rules/asset-pipeline.mddocs/audio/audio-usage.md(新增音频资产流程:docs/audio/add-audio.md)docs/tools.mddocs/games/smashup/workflows/smashup-faction-intake.mddocs/ai-rules/doc-index.md开始任何目录创建、规则录入、素材落盘前,先做以下确认。这是本 skill 的默认提问模板,不等用户自己提醒:
main 基线。main;默认推荐“是”,尤其是准备新开 feat/game-<gameId> 时。AGENTS.md:从 main 创建,且先征得用户确认。npm run assets:download -- --check 或 npm run assets:download,把远端资源拉到本地。common/ 资源、或当前游戏已有远端图片资产。mobileProfile / preferredOrientation / mobileLayoutPreset,并写入游戏专属 UI 规范。收集以下信息后才能开始。已有信息直接使用,缺失项回问用户,不猜测:
smashup)[2]、[2,3,4])rule/ 目录下)先查已有字段:阅读 src/games/manifest.types.ts 确认可用字段,避免重复询问。
当用户只是问“添加新游戏该怎么做”“我想做一个新游戏”“给你素材能不能做”时,AI 必须主动给出可执行 intake 指南,而不是只说“提供规则和素材即可”。
前置 1.4 转 Markdown。mobileLayoutPreset。我会先做这几步:
1. 规则转 Markdown,并保留来源和质量说明。
2. 盘点素材,找出主地图/主棋盘/主面板作为 UI 设计参考。
3. 判断是否有空间载体:没有就跳过地图/点位问题;有则继续分为网格、区域地图、点位网络、轨道或桌面区位。
4. 如果有地图、棋盘、轨道或桌面区位,先记录坐标/槽位合同需求与工具需求;默认等真实 Board/地图壳初版可运行后,再做嵌入式校准工具。
5. 产出 design-system/games/<gameId>.md,包含桌面与移动端方案。
6. 再进入骨架、数据结构、规则实现、UI 闭环。
新游戏 UI 设计不得脱离素材凭空发挥。只要用户提供了图片目录或图片文件,AI 在生成 design-system/games/<gameId>.md 前必须先做:
src/games/summonerwars/ui/MapContainer.tsx 的交互模式。id/name/type/adjacentRegionIds、导出 JSON。piece/card/token/resource/city/population/control/marker;具体 role 由当前游戏定义,不能写成所有游戏通用字段。landscape-adapted + map-shell;固定牌桌默认优先 landscape-adapted + board-shell;天然单列轻游戏才考虑 portrait-simple。当用户提供的是“规则 PDF + 图片素材目录”,或明确要求先判断新游戏是否可做时,先完成本阶段,不要直接进入游戏骨架实现。
npm run pdf:md -- "<输入PDF>" -o "src/games/<gameId>/rule/<游戏名>规则.md"。temp/<gameId>-intake/。temp/<gameId>-intake/image-inventory.tsv 或等价清单,至少包含原文件名、尺寸、类型、疑似用途。npm run assets:manifest 与 npm run assets:validate。npm run assets:check;发现远端缺失时继续 npm run assets:upload,并抽查代表性远端 URL 返回 200。evidence/<gameId>/<gameId>-feasibility-<date>.md 写结论,至少覆盖:核心机制、引擎原语映射、状态模型难点、UI/资源难点、MVP 切分、主要风险与建议阶段。rule/<游戏名>规则.md 存在且可读。compressed/*.webp 或明确说明为何不能压缩。evidence/<gameId>/,并能指导后续是否开分支建骨架。当用户还没把完整规则讲完,但已经给了图片路径、裁片位置、行列数、顺序说明或对象定位时,可以直接启动资源 intake。不要要求“先把所有规则补齐再开始”。
gameIdnpm run compress:images -- public/assets/<gameId> 或最小必要子目录。npm run assets:check;只要本轮新增/变更了运行时资源且远端有缺口,就必须继续执行上传闭环,直到远端对象可访问,不能停留在“本地已落盘”。| 素材类型 | 默认命名 | 默认目录 |
|---|---|---|
| 缩略图 | cover.png | public/assets/<gameId>/thumbnails/ |
| 卡牌 atlas | <batch>.png | public/assets/i18n/zh-CN/<gameId>/cards/ |
| 基地 atlas | <batch>_base.png | public/assets/i18n/zh-CN/<gameId>/base/ |
| 角色/英雄面板 | <entityId>-board.png | public/assets/i18n/zh-CN/<gameId>/hero/ |
| 棋盘/地图/公共插图 | 按语义命名 | public/assets/i18n/zh-CN/<gameId>/board/ 或 common/ |
| 图集配置 | <name>.atlas.json | public/assets/atlas-configs/<gameId>/ |
若用户没有给命名方案,AI 默认采用上述语义命名;若用户已给命名规则,以用户规则为准。
IMG_1234.png、新建文件夹 (2).png、scan0007.png、image(1).png 这类随机名/默认名不能直接沿用为长期正式命名。cover<batch>、<batch>_basedefId目标:建立完整目录结构与最小占位实现,npm run generate:manifests 可成功运行。
默认拆分:中等以上复杂度游戏(命令数 ≥5 或有多阶段回合)从第一天就用拆分结构。
src/games/<gameId>/
manifest.ts # 清单元数据
game.ts # 引擎适配器组装(只做组装,不写逻辑)
Board.tsx # UI 布局组装(逻辑拆到 hooks/,子组件拆到 ui/)
thumbnail.tsx # 缩略图组件
tutorial.ts # 教学配置(占位)
audio.config.ts # 音频配置(占位)
criticalImageResolver.ts # 关键图片预加载(若有精灵图)
domain/
index.ts # 领域内核入口
types.ts # re-export barrel(导出 core-types + commands + events)
core-types.ts # 状态接口(PlayerState, GameCore, 基础类型)
commands.ts # 命令类型 + XX_COMMANDS 常量
events.ts # 事件类型 + XX_EVENTS 常量
ids.ts # 领域 ID 常量表
utils.ts # 游戏内共享工具(从第一天就建立)
rule/
<游戏名>规则.md # 规则文档占位
hooks/ # 游戏业务 hooks
ui/ # 游戏 UI 子组件
__tests__/
smoke.test.ts # 冒烟测试占位
import type { GameManifestEntry } from '../manifest.types';
const entry: GameManifestEntry = {
id: '<gameId>',
type: 'game',
enabled: true,
titleKey: 'games.<gameId>.title',
descriptionKey: 'games.<gameId>.description',
category: 'strategy', // strategy | casual | party | abstract
playersKey: 'games.<gameId>.players',
icon: '🎮',
thumbnailPath: '<gameId>/thumbnails/cover',
allowLocalMode: false, // 默认仅联机
playerOptions: [2], // 可选 [2,3,4]
tags: [], // dice_driven | card_driven | tactical 等
bestPlayers: [2],
};
export const <GAME_ID>_MANIFEST: GameManifestEntry = entry;
export default entry;
core-types.ts — 状态接口:
import type { PlayerId } from '../../../engine/types';
export type GamePhase = 'factionSelect' | 'startTurn' | 'playCards' | ...;
export const PHASE_ORDER: GamePhase[] = [...];
export interface PlayerState { id: PlayerId; /* ... */ }
export interface <GameId>Core {
players: Record<PlayerId, PlayerState>;
turnNumber: number;
gameResult?: { winner?: string; draw?: boolean };
}
commands.ts — 命令类型:
import type { Command } from '../../../engine/types';
export const XX_COMMANDS = { DO_SOMETHING: 'DO_SOMETHING', ... } as const;
export interface DoSomethingCommand extends Command<'DO_SOMETHING'> { payload: { ... }; }
export type <GameId>Command = DoSomethingCommand | ...;
events.ts — 事件类型:
import type { GameEvent } from '../../../engine/types';
export const XX_EVENTS = { SOMETHING_DONE: 'SOMETHING_DONE', ... } as const;
export interface SomethingDoneEvent extends GameEvent<'SOMETHING_DONE'> { payload: { ... }; }
export type <GameId>Event = SomethingDoneEvent | ...;
types.ts — re-export barrel:
export * from './core-types';
export * from './commands';
export * from './events';
所有稳定 ID 必须在此定义,禁止字符串字面量。
import type { DomainCore, PlayerId, RandomFn, GameOverResult } from '../../../engine/types';
import type { <GameId>Core } from './types';
export const <GameId>Domain: DomainCore<<GameId>Core> = {
gameId: '<gameId>',
setup: (playerIds: PlayerId[], random: RandomFn): <GameId>Core => ({
// 最小初始状态
players: Object.fromEntries(playerIds.map(pid => [pid, createPlayerState(pid)])),
turnNumber: 1,
// ...其他必要字段
}),
validate: (state, command) => ({ valid: true }), // 占位
execute: (state, command, random) => [], // 占位
reduce: (core, event) => core, // 占位
isGameOver: (core) => core.gameResult,
};
import { createGameEngine, createBaseSystems, createFlowSystem } from '../../engine';
import { <GameId>Domain } from './domain';
import type { <GameId>Core } from './domain/types';
// FlowHooks 占位(阶段 4 实现)
const flowHooks = {
initialPhase: '<firstPhase>',
getNextPhase: () => '<firstPhase>',
getActivePlayerId: ({ state }) => Object.keys(state.core.players)[0],
};
const systems = [
createFlowSystem<<GameId>Core>({ hooks: flowHooks }),
...createBaseSystems<<GameId>Core>(),
];
export const <GameId> = createGameEngine<<GameId>Core>({
domain: <GameId>Domain,
systems,
minPlayers: 2,
maxPlayers: 2,
commandTypes: [], // 阶段 4 填充
});
export default <GameId>;
import React from 'react';
import type { GameBoardProps } from '../../engine/transport/protocol';
import type { <GameId>Core } from './domain/types';
type Props = GameBoardProps<<GameId>Core>;
const <GameId>Board: React.FC<Props> = ({ G, playerID }) => {
return <div className="p-4 text-white">
<h1>{'<gameId> - 骨架占位'}</h1>
<p>当前玩家:{playerID ?? 'observer'}</p>
<pre>{JSON.stringify(G.core, null, 2)}</pre>
</div>;
};
export default <GameId>Board;
ManifestGameThumbnail 组件TutorialManifest({ id: '<gameId>-basic', steps: [] })GameAudioConfigpublic/assets/<gameId>/
thumbnails/.gitkeep
images/.gitkeep
创建 public/locales/zh-CN/game-<gameId>.json 和 public/locales/en/game-<gameId>.json,包含 title/description/players。
npm run generate:manifests # 成功生成清单
npx vitest run src/games/<gameId> # 冒烟测试通过
npm run dev # 编译无报错(游戏可在大厅列表看到)
目标:在录入数据前,先将游戏机制分解为引擎原语组合,设计面向百游戏的通用数据结构,避免后期重构。
核心原则:面向百游戏设计,不依赖现有游戏的具体实现。
将新游戏的核心机制分解为引擎原语的组合:
| 机制类别 | 游戏中的表现 | 引擎原语映射 | 是否需要扩展 |
|---|---|---|---|
| 随机性 | 骰子/抽牌/洗牌 | dice.ts / zones.ts | ? |
| 资源管理 | 魔力/行动点/金币 | resources.ts | ? |
| 状态效果 | buff/debuff/标记 | tags.ts (层数/持续时间/层级匹配) | ? |
| 数值修改 | 攻击力加成/伤害减免 | modifier.ts (flat/percent/priority) | ? |
| 动态属性 | 可被 buff 修改的属性 | attribute.ts (base + modifiers) | ? |
| 能力系统 | 技能/被动/光环 | ability.ts (注册/查找/执行器) | ? |
| 目标选择 | 选择敌人/友军/格子 | target.ts | ? |
| 条件判断 | 触发条件/激活条件 | condition.ts + expression.ts | ? |
| 效果执行 | 伤害/治疗/移动/抽牌 | effects.ts | ? |
| 空间关系 | 棋盘/网格/区域 | zones.ts (grid/stack/hand) | ? |
| 伤害计算 | 伤害修正/防御/护甲 | damageCalculation.ts | ? |
输出产物:
核心原则:数据结构必须支持未来扩展,面向百游戏设计,禁止"先录入再重构"。
在录入具体数据前,先设计通用数据结构:
列出游戏中的所有实体类型(如卡牌/单位/骰子/资源/状态),对每种实体类型回答:
resources.ts,buff 用 tags.ts)// ❌ 错误示例:字段不完整,未来需要重构
interface Card {
id: string;
name: string;
cost: number;
}
// ✅ 正确示例:考虑扩展性,字段完整,面向百游戏
interface Card {
id: string;
name: string;
type: 'action' | 'unit' | 'event'; // 类型区分(必须)
cost: number; // 费用(若游戏有资源系统)
// 可选字段(根据游戏机制决定)
rarity?: 'common' | 'rare' | 'epic'; // 稀有度
tags?: string[]; // 标签(如 'magic' | 'melee')
abilities?: string[]; // 技能 ID 引用(不嵌套对象)
effects?: Effect[]; // 效果定义(结构化)
// 触发条件(若有)
trigger?: {
phase?: GamePhase; // 触发阶段
event?: string; // 触发事件
condition?: Condition; // 触发条件
};
// 使用限制(若有)
usageLimit?: {
perTurn?: number; // 每回合次数
perGame?: number; // 每局次数
cooldown?: number; // 冷却回合
};
// 目标选择(若有)
targeting?: {
type: 'self' | 'opponent' | 'any_unit' | 'any_cell';
filter?: Condition; // 目标过滤条件
count?: number | 'all'; // 目标数量
};
}
在设计数据结构时,必须避免以下反模式:
| 反模式 | 错误示例 | 正确做法 | 为什么错误 |
|---|---|---|---|
| 硬编码技能逻辑 | validate() 中 switch (abilityId) 每个技能一个 case | 技能定义包含 validation 配置,使用通用验证函数 | 第 100 个游戏会有 10000 行 switch |
| UI 状态混入 core | core.lastPlayedCard(纯展示) | 通过 EventStream 传递给 UI | core 应该只包含规则判定需要的数据 |
| 交互状态混入 core | core.pendingAttack(等待输入) | 使用 sys.interaction | 交互状态是系统层职责,不是领域层 |
| 缺少目标字段 | grantStatus: { statusId, value } + 执行层猜测目标 | grantStatus: { statusId, value, target: 'opponent' } | 数据不完整导致执行层需要"猜测" |
| 缺少触发条件 | 技能只有效果描述,触发条件在代码里硬编码 | 技能定义包含 trigger: { phase, event } | 触发逻辑应该数据驱动,不是代码驱动 |
| 缺少使用限制 | 技能只有费用,没有"每回合一次"等限制 | 技能定义包含 usageLimit: { perTurn: 1 } | 限制规则应该在数据中声明 |
| 对象嵌套引用 | card.abilities: Ability[](嵌套对象) | card.abilities: string[](ID 引用) | 嵌套导致数据冗余和更新不一致 |
| 散落的状态字段 | unit.stunned, unit.poisoned, unit.buffed | unit.tags: TagContainer | 第 100 个游戏会有 100 种状态字段 |
| ad-hoc 修正字段 | unit.attackBonus, unit.defenseBonus | unit.attack: Attribute (base + modifiers) | 修正逻辑应该用 modifier 系统 |
设计完数据结构后,逐项检查:
id: string)type: 'xxx')Attribute 或 resources.tstargeting 字段)usageLimit)trigger 字段)TagContainer,不是散落的布尔字段modifier.ts,不是 ad-hoc 的 xxxBonus 字段对照 src/engine/primitives/ 和 src/engine/systems/,列出:
输出产物:
核心问题:如果未来有 100 个游戏,这个设计会不会导致代码爆炸?
对每个设计决策,问自己:
如果有 100 个游戏,每个游戏都这样做,会发生什么?
validate() 中加 100 行 switch → 10000 行 switch这个字段/逻辑是游戏特有的,还是可以抽象为通用能力?
core.diceThroneSpecificField → 只有一个游戏用core.resources: ResourceContainer → 所有游戏都能用这个实现是否依赖"已有游戏的具体实现"?
CombatAbilityManager" → 耦合具体游戏engine/primitives/ability.ts" → 依赖通用抽象如果规则变化,需要改多少地方?
trigger 字段 → 单一数据源目标:完成规则文档录入、静态游戏数据录入、核心类型定义,不写业务逻辑。需要新增引擎原语的部分可标记延后。
核心原则:不猜测、不编造、不跳过。缺什么问什么,问清楚再录。
规则文档不完整或有歧义时:
// TODO: 待确认 — <具体问题>,不填默认值数据量大、用户只提供了部分时:
素材数据核对(强制,遵循 AGENTS.md):
需要新增引擎原语的数据:
src/engine/primitives/ 中没有现成实现// DEFERRED: 需新增引擎原语 <xxx>,暂用占位对照源主动确认(强制):
将规则书/规则图片内容结构化录入 src/games/<gameId>/rule/ 下的 Markdown 文件,拆解为:
规则文档质量要求:
根据规则文档,将所有实体数据录入代码。不只是名称+描述,必须录入影响游戏机制的全部必要信息。
未来的游戏机制不可预知,不预设具体字段清单。用以下原则判断一条信息是否必须录入:
validate() / execute() / reduce() / isGameOver() 需要读取该信息来做决策,则必须录入。
反面判断——可以不录入的:
核心问题:素材/规则书上的每一条信息,是否都已在代码中有对应字段? AI 容易漏录"不好结构化"的信息(如图标表示的触发条件、符号表示的骰面组合)。必须逐项核对,不能只录"好录的"。
每录入一个实体类型,执行以下自检:
典型漏录场景(警示):
validate() 无法判断触发条件按游戏复杂度选择合适的数据组织方式:
简单游戏:直接在 domain 中定义。
中等游戏:
data/
cards.ts # 实体定义与查询函数
factions/ # 按分组组织数据
复杂游戏:
config/ 或 heroes/ # 按实体大类拆分
factions/ # 按阵营/角色进一步拆分
具体目录名和文件拆分方式由游戏的实体结构决定,不预设。
domain/ids.ts(as const),禁止字符串字面量TODO: 待确认 的条目列表DEFERRED 的引擎原语需求列表根据录入的数据,补充 domain/types.ts(或拆分文件):
PlayerState(根据游戏需要的状态字段)
TagContainer 表达(engine/primitives/tags.ts),避免散落的 statusEffects: Record<string, number> / tempAbilities: string[]<GameId>Core(玩家状态/回合信息/游戏特有状态等)XX_COMMANDS 常量对象)XX_EVENTS 常量对象)对照规则,在引擎层检索可复用实现:
src/engine/primitives/dice.tssrc/engine/primitives/resources.tssrc/engine/primitives/tags.tssrc/engine/primitives/modifier.tssrc/engine/primitives/attribute.tssrc/engine/primitives/ability.tssrc/engine/primitives/zones.tssrc/engine/primitives/condition.ts + expression.tssrc/engine/primitives/target.tssrc/engine/primitives/effects.ts强制要求(新游戏):
AGENTS.md 与 docs/ai-rules/engine-systems.md)。若缺口存在:优先补充 src/engine/primitives/(通用工具函数);领域语义放在游戏层(src/games/<gameId>/domain/)。若工作量大,记入延后清单,在后续阶段补充。
完整规范见
docs/ai-rules/engine-systems.md「领域建模前置审查」节。 核心原则:规则文本 → 领域模型 → 实现,禁止跳过建模直接写实现。
产出:术语→事件映射表 + 决策点清单 + 引擎缺口清单。
rule/*.md,覆盖所有阶段/实体/操作/结算/特殊机制新游戏默认遵循这条原则:能继续兼容就继续兼容,真缺关键能力才提示。
/play/:gameId/* 的拦截条件。matchMedia、监听 API 差异(addEventListener('change') vs addListener)这类能力,优先在通用工具层或游戏层补 fallback。/play/* 的统一硬门槛。gameId 或页面前缀精确判断。ResizeObserver 视为高风险能力,但不是全站默认门槛
ResizeObserver,且缺失后会导致棋盘/地图/主操作区明显错位或不可操作时,才允许把它加入该游戏的拦截条件。目标:完成确定性核心逻辑,测试通过。
// domain/commands.ts 或 domain/validate.ts
export function validate(state: MatchState<Core>, command: Command): ValidationResult {
// 1. 检查是否是当前玩家的回合
// 2. 检查当前阶段是否允许此命令
// 3. 检查命令参数合法性
// 4. 检查资源/条件是否满足
}
三个游戏共同模式:
domain/commands.ts → validateCommand()domain/validate.ts → validateCommand()domain/commands.ts → validate()// domain/execute.ts 或 domain/reducer.ts
export function execute(state: MatchState<Core>, command: Command, random?: RandomFn): GameEvent[] {
// 根据 command.type 分发处理
// 返回一系列事件(不直接修改状态)
}
// domain/reducer.ts
export function reduce(core: Core, event: GameEvent): Core {
switch (event.type) {
case 'DAMAGE_DEALT': {
// ✅ 结构共享:只 spread 变更路径
const { targetId, amount } = event.payload;
const target = core.players[targetId];
if (!target) return core;
return {
...core,
players: {
...core.players,
[targetId]: { ...target, hp: Math.max(0, target.hp - amount) },
},
};
}
// 每种事件类型一个 case
default: return core;
}
}
关键约束:
JSON.parse(JSON.stringify())(性能灾难)。只 spread 变更路径,未变路径保持原引用。updatePlayer() 等 helper 到 domain/utils.ts。docs/ai-rules/engine-systems.md「Reducer 结构共享范例」。isGameOver: (core): GameOverResult | undefined => {
// 检查胜利条件
// 返回 { winner: playerId } 或 { draw: true } 或 undefined
}
在 __tests__/ 创建测试文件,覆盖:
测试辅助模式(参考 smashup/tests/helpers.ts):
export function makePlayer(id: string, overrides?: Partial<PlayerState>): PlayerState { ... }
export function makeState(overrides?: Partial<Core>): Core { ... }
export function makeMatchState(core: Core): MatchState<Core> { ... }
npx vitest run src/games/<gameId> # 所有测试通过
核心规则正常 + 异常场景有覆盖。
目标:接入 FlowSystem 完成阶段流转,game.ts 组装完毕。
创建 domain/flowHooks.ts(参考 summonerwars/domain/flowHooks.ts):
import type { FlowHooks, PhaseExitResult } from '../../../engine/systems/FlowSystem';
export const flowHooks: FlowHooks<Core> = {
// 初始阶段(通常为 factionSelect 或第一个游戏阶段)
initialPhase: 'factionSelect',
// 是否允许推进
canAdvance: ({ state }) => ({ ok: true }),
// 下一阶段计算
getNextPhase: ({ state, from }) => {
const idx = PHASE_ORDER.indexOf(from as GamePhase);
return PHASE_ORDER[(idx + 1) % PHASE_ORDER.length];
},
// 当前活跃玩家
getActivePlayerId: ({ state }) => state.core.currentPlayer,
// 阶段退出副作用(如:抽牌/切换回合/结算伤害)
onPhaseExit: ({ state, from }): PhaseExitResult => {
const events: GameEvent[] = [];
// 按阶段处理副作用
return { events };
},
// 阶段进入副作用(如:回合开始事件/状态重置)
onPhaseEnter: ({ state, from, to }): GameEvent[] => {
const events: GameEvent[] = [];
// 按阶段处理副作用
return events;
},
// 自动推进检查(如:非交互阶段自动跳过)
onAutoContinueCheck: ({ state, events }) => {
// 如 startTurn/endTurn 等纯自动阶段
return undefined;
},
};
三个游戏的 FlowHooks 复杂度对比:
domain/index.ts 内联(~150 行),阶段退出处理记分逻辑domain/flowHooks.ts(~250 行),阶段进退处理抽牌/换人/技能触发game.ts 内联(~500 行),最复杂,攻防阶段有大量分支// 系统选择模式(三个游戏共同模式)
const systems = [
createFlowSystem<Core>({ hooks: flowHooks }),
// 方式 A:逐个选择(dicethrone/summonerwars 风格,精细控制)
createEventStreamSystem(),
createLogSystem(),
createActionLogSystem({ commandAllowlist: ACTION_ALLOWLIST, formatEntry }),
createUndoSystem({ snapshotCommandAllowlist: UNDO_ALLOWLIST }),
createInteractionSystem(),
createRematchSystem(),
createResponseWindowSystem({ // 需要响应窗口时配置注入
allowedCommands: ['PLAY_CARD'], // 响应期间允许的游戏命令
responseAdvanceEvents: [ // 触发响应者推进的事件
{ eventType: 'CARD_PLAYED' },
],
// interactionLock: { ... }, // 多步交互锁定(可选)
}),
createTutorialSystem(),
createCheatSystem<Core>(cheatModifier),
// 方式 B:默认集合(smashup 风格,简洁)
// ...createBaseSystems<Core>(),
// createCheatSystem<Core>(cheatModifier),
];
// 命令类型(只列业务命令,系统命令由 adapter 自动合并)
const commandTypes = [
...Object.values(XX_COMMANDS),
];
参考 summonerwars/game.ts 的 summonerWarsCheatModifier,至少实现:
getResource / setResourcesetPhasedealCardByIndex(如有牌库)强制先读(权威单一来源):
docs/ai-rules/engine-systems.md(ActionLogSystem 使用规范)evidence/dicethrone/action-log-card-preview.md(卡牌预览注册表模式 + 数据流说明)你在新游戏里只需要做这些(最小闭环):
game.ts 配置 createActionLogSystem({ commandAllowlist, formatEntry }),formatEntry 产出包含 segments 的 ActionLogEntry。ui/cardPreviewHelper.ts 提供 cardId → CardPreviewRef 查询,并在 game.ts 文件末尾调用 registerCardPreviewGetter(gameId, getter) 注册。关键点:Vite SSR 的函数提升陷阱与“注册必须放文件末尾”的原因,详见
AGENTS.md/docs/ai-rules/golden-rules.md。
npx vitest run src/games/<gameId>/__tests__/flow.test.ts
npm run generate:manifests # 清单生成成功
npx vitest run src/games/<gameId> # 所有测试通过
npm run dev # 游戏可从大厅创建对局,基础回合可推进
目标:提供最小可玩 UI,完成交互闭环。
强制先读(权威单一来源):
docs/ai-rules/ui-ux.md.windsurf/skills/boardgame-ui-imagegen/SKILL.mddocs/ai-rules/animation-effects.mddocs/ai-rules/golden-rules.md每个游戏的视觉风格各不相同,禁止直接复用已有游戏的样式规范。必须为新游戏生成独立的设计规范。
design-system/games/<gameId>.md 必须写明至少 1 张主视觉素材的路径、尺寸和肉眼观察结论。boardgame-ui-imagegen 输出 UI 元素拆解:素材已有 UI、规则必须常驻、按需展开、禁止出现;未完成拆解不得直接调用 imagegen。--design-system:根据新游戏的类型、题材、美术风格生成专属设计系统:
python3 skills/ui-ux-pro-max/scripts/search.py "<游戏类型> <题材> <风格关键词>" --design-system --persist -p "<游戏名>" --page "game-board"
design-system/games/<gameId>.md:作为该游戏的 UI 权威参考,后续 Board/组件开发以此为准。mobileProfile / preferredOrientation / mobileLayoutPreset 推荐值。design-system/game-ui/MASTER.md 中的交互原则(反馈/状态清晰/动画时长等)仍然适用,但配色/字体/视觉风格以游戏专属规范为准。三个游戏的 Board 共同模式:
const Board: React.FC<Props> = ({ G, moves, playerID, ctx }) => {
const core = G.core;
const phase = G.sys.phase;
const gameMode = useGameMode();
const { t } = useTranslation('game-<gameId>');
// 1. 基础状态
const isGameOver = ctx.gameover;
const isMyTurn = playerID === core.currentPlayer;
// 2. 教学系统集成
useTutorialBridge(G.sys.tutorial, moves as Record<string, unknown>);
const { isActive: isTutorialActive, currentStep: tutorialStep } = useTutorial();
// 3. 音效系统
useGameAudio({ config: AUDIO_CONFIG, gameId: MANIFEST.id, G: core, ctx: { ... } });
// 4. 事件消费 → 动画驱动
const gameEvents = useGameEvents({ G, myPlayerId: playerID || '0' });
// 5. 阵营/角色选择阶段
if (isInSelectionPhase) {
return <FactionSelection ... />;
}
// 6. 游戏主 UI
return (
<div className="...">
{/* 棋盘/基地/卡牌区域 */}
{/* 手牌区 */}
{/* 阶段指示/操作按钮 */}
{/* 结算覆盖层 */}
{isGameOver && <EndgameOverlay ... />}
</div>
);
};
当 Board.tsx 超过 300 行时,按职责拆分到 ui/ 目录:
参考 summonerwars/ui/:
BoardGrid.tsx — 棋盘网格渲染HandArea.tsx — 手牌区PhaseTracker.tsx — 阶段指示器PlayerInfo.tsx — 玩家信息面板GameButton.tsx — 游戏操作按钮useGameEvents.ts — 事件消费 hookuseCellInteraction.ts — 格子交互 hookBoardEffects.tsx — 特效层FactionSelection.tsx — 阵营选择 UI参考 smashup/ui/:
HandArea.tsx — 手牌区FactionSelection.tsx — 派系选择PromptOverlay.tsx — 提示覆盖层useGameEvents.ts — 事件消费BoardEffects.tsx — 特效层所有用户操作通过 moves[COMMAND_TYPE](payload) 触发:
三个游戏共同模式:初始阶段是 factionSelect/setup,通过 FlowHooks 的 onAutoContinueCheck 在所有玩家准备后自动推进到游戏阶段。
UI 侧使用 TutorialSelectionGate(框架组件)或自定义选择组件。
目标:补齐 i18n、测试、教学、音效。
补齐 public/locales/{zh-CN,en}/game-<gameId>.json 中的所有文案:
参考 smashup/tutorial.ts 的模式:
highlightTarget + blockedCommands)requireAction: true + allowedCommands + advanceOnEvents强制先读(权威单一来源,避免本文档过时):
AGENTS.md「音频资源架构(强制)」docs/ai-rules/asset-pipeline.md「🔊 音频资源规范」docs/audio/audio-usage.md(新增音频资产流程见 docs/audio/add-audio.md)你在新游戏里只需要做这些(最小闭环):
src/games/<gameId>/audio.config.ts,导出 GameAudioConfig:
feedbackResolver(event): SoundKey | null:无动画事件返回 SoundKey;有动画事件返回 null,音效交给动画层 onImpact() 播放criticalSounds:进入游戏后立即预加载的高频音效 key(建议 5~15)contextualPreloadKeys:根据上下文增量预热docs/audio/audio-usage.md 为准)public/assets/common/audio/registry.json。
basePath/soundscompressed/click/dice_roll),必须使用 registry 的完整 keyfeedbackResolver / FX FeedbackPack / 动画 onImpact / UI GameButton / playDeniedSound())。参考实现:
src/games/smashup/audio.config.ts/src/games/summonerwars/audio.config.ts。
强制先读(权威单一来源):
docs/ai-rules/asset-pipeline.md(critical/warm 规则、路径格式、门禁与验收清单)你在新游戏里只需要做这些(最小闭环):
criticalImageResolver.ts,返回 { critical, warm },并按“选择阶段 vs 游戏阶段”动态解析。game.ts(或游戏入口约定的位置)注册 resolver。参考实现:
src/games/smashup/criticalImageResolver.ts/src/games/summonerwars/criticalImageResolver.ts/src/games/dicethrone/criticalImageResolver.ts。
若需要调试面板,创建 debug-config.tsx 提供游戏专属调试选项。
调试面板规范:
GameDebugPanel 组件挂载在 Board 内,不得创建新的全局入口。SYS_CHEAT_* 指令(依赖 CheatSystem),禁止直接修改 core。cover、<batch>、<entityId>-board)public/assets/<gameId>/thumbnails/cover.pngnpm run compress:images -- public/assets/<gameId>/thumbnailsmanifest.ts 中 thumbnailPath 使用 <gameId>/thumbnails/coverpublic/assets/i18n/zh-CN/<gameId>/<category>/public/assets/atlas-configs/<gameId>/npm run assets:download -- --checknpm run assets:check200/206,再算交付完成npm run generate:manifests # 清单生成成功
npx vitest run src/games/<gameId> # 所有测试通过
npm run typecheck # 类型检查通过
npm run assets:check # 若本轮新增了运行时资源,检查远端缺口
npm run assets:upload # check 发现本轮运行时资源远端缺失时必须执行
npm run dev # 大厅可见、可创建对局、可完整游玩
权威来源:系统清单/红线/反模式以 AGENTS.md + docs/ai-rules/engine-systems.md 为准,本节不再重复抄写。
createBaseSystems() 默认包含:EventStream + Log + ActionLog + Undo + Interaction + Rematch + ResponseWindow + TutorialcreateBaseSystems() 不包含 FlowSystem / CheatSystem:需要自行追加commandTypes 只列业务命令:系统命令由 adapter 自动合并allowedCommands / responseAdvanceEvents(禁止改引擎文件)engine/primitives/ability.tsengine/primitives/tags.tsengine/primitives/modifier.tsengine/primitives/attribute.ts(纯资源消耗仍用 resources.ts)src/games/dicethrone/(角色系统/骰子/攻防/状态效果/Token响应)src/games/summonerwars/(网格棋盘/单位管理/阵营牌组/技能系统)src/games/smashup/(多人支持/基地记分/派系混搭/持续效果)src/components/game/framework/src/engine/systems/src/engine/primitives/import manifest from './manifest';
import { ManifestGameThumbnail } from '../../components/lobby/thumbnails';
export default function Thumbnail() {
return <ManifestGameThumbnail manifest={manifest} />;
}
manifest.ts 中配置 thumbnailPath: '<gameId>/thumbnails/cover'(不含扩展名、不含 compressed/)。npm run compress:images -- public/assets/<gameId>/thumbnails 压缩。