override spike-first(创始人 06-23 裁),全栈铺 Phaser tier2 引擎。recon→契约→6模块并行→集成 workflow 产出 ~60 文件,全 6c6g 静态校验过、红线零碰 Tier0/1 产线: - 契约(6):tier2-source-project.schema / tier2-verdict.schema(fork·round放开·三层校验+富游戏门)/ toolkit 签名 / 探针钩子 / boot-phaser-host.d.ts / mini-肥鹅 fixture 规格 - M1 Phaser scaffold+装载 host+引擎能力面(19;logic-smoke 13/13:五耦合点真接线+赢输双路径+latch不回弹) - M2 CDP 探针 Phaser 重写+business-sim driver+九门+富游戏三门(5;verdict 过 schema) - M3 Python 单写 ReAct agent loop+9 工具 toolkit+M3 Anthropic接法+四熔断(9;mini-desktop 真 2.0.2 venv 验:import/9工具/中间件注册 OK) - M4 datatable schema+金标+资产占位(4)/ M5 prompt+Phaser skill+rag(13)/ M6 成本 RecordingChatModel+trace adapter(3) - 集成:run_engine.py 入口 + 接线断点已修 + RUN-ON-MINI-DESKTOP.md 待 mini-desktop 真跑(esbuild build + CDP 九门 + M3 生成 = 本质即 0号 spike)。AgentScope 2.0.2 API 逐条对 /root/oss/agentscope 源码核验。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
12 KiB
name, description
| name | description |
|---|---|
| phaser-rich-game | 教 agent 在 Phaser 3 上写多系统经营富游戏(合成/订单/资源耦合)——scene 树搭建、输入命中、纯逻辑系统与渲染分离、与 tier2 第二装载分支宿主契约对接(boot/readState/destroy、latch 终态、语义 state 导出)、esbuild 多文件构建、避开十类常见崩点。tier2 单写 ReAct agent 写 Phaser 工程时按名取它。 |
在 Phaser 3 上写经营富游戏
这份 skill 教你把一款多系统经营富游戏(合成 / 订单 / 资源相互咬合)写成一个 Phaser 3 多文件 src/ 源工程,并让它对接 tier2 第二装载分支宿主、过验收门。它假设你已经读了 code-writer-system 的工作纪律(九工具、ReAct 循环、五条铁律);本 skill 补的是「具体到 Phaser 怎么写」那一层。
拿不准某个 Phaser API 的确切签名时,查本 skill 的 rag/ 语料(精选的 Phaser 关键 API 与范式片段),别凭记忆编不存在的 API——Phaser 的 API 面很大,编出来的 API 跑不起来会浪费一整轮真玩。
一、工程骨架长什么样
tier2 的 Phaser 工程是多文件 src/,入口恒为 src/main.js。scaffold_init 已经给你铺好先天过 boot 的骨架(scene 树脚手架 + 三系统骨架 + 留空数据表)。典型文件树:
src/
main.js # 入口:esbuild 从这里起手。默认导出工厂 (opts?) => PhaserGameInstance
scenes/
BootScene.js # 载资产、建系统、置全局上下文,然后 start PlayScene
PlayScene.js # 主玩法画面 + 输入(棋盘、点击命中、合成动画)
UIScene.js # 叠层 HUD / 订单面板(与 PlayScene 并行运行,不是切换)
systems/
resource-system.js # 纯逻辑:货币 + 库存,命令式 API(addCoins/spendCoins/consume/add)
merge-system.js # 纯逻辑:3×3 棋盘合成判定,真调 resource 命令
order-system.js # 纯逻辑:订单 patience 倒计时,完单真调 resource.addCoins
data/
tables.js # 数据表:mergeChains / orders / 物品 / 平衡数值
GameInstance.js # 实现宿主契约 boot/readState/injectInput?/destroy 的工厂主体
role 字段(fileTree):main.js=entry,scenes/=scene,systems/=system,data/*=config,GameInstance.js=lib。
二、最该守住的一条:纯逻辑系统与渲染彻底分离
这是富游戏写对的命门,也是三联动门、经济门、latch 门能不能过的根。
systems/ 下的文件是纯逻辑:零 DOM、零 canvas、零 Phaser import、零裸随机。它们维护游戏状态、暴露命令式 API、返回 {ok, reason},全程不知道有没有屏幕。这样三件事才成立:
- 真接线可被机器验证:合成消耗真调
resource.consumeIngredient(itemId, n)、完单真调resource.addCoins(n)——系统内部记调用计数,三联动门据计数证「真调了资源系统」,而非自绘伪装。如果你在 scene 里直接this.coins += 10,三联动门会判你没真接线。 - 确定性可重放:随机经注入的受控源(
{ next: () => number }),不裸Math.random()。harness 用固定 seed 驱动,每次跑出一样的结果。 - 逻辑可静态检查:数据表可达性(每个订单要的物品都能被合成链产出)、合成 DAG 无环,都能脱离渲染静态算出来。
scenes/ 下的文件是渲染 + 输入:画棋盘格、画 HUD、把点击坐标映射到棋盘格、播合成动画。scene 调系统的命令式 API(mergeSystem.mergeAt(i, j)),拿返回值决定怎么播反馈,绝不在 scene 里重写逻辑或直接改系统内部状态。
参照 tier2/fixtures/mini-fei-e/src/systems/ 三个已预建的纯逻辑系统——它们就是这条分离的样板(命令式 API + {ok,reason} + 调用计数 + 受控随机)。你写表现层时调它们,不重写它们。
三、scene 树怎么搭
// src/main.js —— 工厂:返回实现宿主契约的实例。esbuild 入口。
import Phaser from 'phaser';
import { createGameInstance } from './GameInstance.js';
// 默认导出工厂;esbuild 打包后挂 buildProfile.globalName,宿主取它建实例。
export default function gameFactory(opts) {
return createGameInstance(opts);
}
三个要点:
- BootScene 建系统、置全局上下文:在 BootScene 里
new出三个纯逻辑系统,按game.registry(Phaser 的跨 scene 数据仓)或挂到一个共享上下文对象上,让 PlayScene / UIScene 都拿得到同一组系统实例。系统是 single source of truth,多个 scene 共享它。 - UIScene 叠层并行,不是切换:
this.scene.launch('UIScene')让 UIScene 与 PlayScene 同时运行(HUD 浮在玩法画面上);this.scene.start('Xxx')是切换(停当前、起新的)。订单面板 / 金币 HUD 用 launch 叠层。 - 资产在 preload 载、create 里建对象:
preload()里this.load.image(key, url),create()里this.add.image/sprite/rectangle。资产从 boot 注入的assets来(按 ref 键),缺失则降级用this.add.rectangle画色块占位——别因为缺图就崩。
四、输入命中:把点击坐标映射到棋盘格
经营富游戏最常见的输入是「点棋盘格」「点订单」。Phaser 的命中有两条路,挑一条用:
- 交互对象路(推荐):给每个棋盘格建一个
this.add.rectangle(x, y, w, h).setInteractive(),监听'pointerdown',回调里已知这是哪个格(闭包捕获 i,j)。命中由 Phaser 算,不用自己做坐标反算。 - 全局指针路:监听
this.input.on('pointerdown', p => ...),用p.x, p.y自己反算落在哪个格(col = Math.floor((p.x - boardX) / cellW))。灵活但易错,注意 Phaser 指针坐标已是逻辑像素(scale 处理过),别再除像素比。
命中到格后,调系统命令(mergeSystem.selectCell(i,j) 或 mergeSystem.mergeAt(...)),据返回值播反馈。点击只触发逻辑命令,不直接改状态。
逻辑像素 390×844 竖屏:Phaser scale 用 { mode: Phaser.Scale.FIT, width: 390, height: 844 },这样你按 390×844 布局,Phaser 自动缩放适配真实屏幕,指针坐标也归一到这个逻辑空间。
五、与宿主契约对接(A4 第二装载分支)
宿主契约在 tier2/contracts/boot-phaser-host.d.ts。你的 GameInstance.js 要返回一个实现这四个方法的对象:
// src/GameInstance.js —— 实现 PhaserGameInstance 契约。
export function createGameInstance(opts) {
let phaserGame = null;
let systems = null; // { resource, merge, order } —— 纯逻辑系统,readState 的真相源
return {
// boot(ctx):一次性启动。ctx = { game?, canvas, seed, assets? }。
// 注意:tier2 宿主可能注入它已建好的 Phaser.Game(ctx.game),也可能让你自建——
// 按宿主实际约定来(看 boot-phaser-host.js)。用 ctx.seed 派生所有随机,禁裸 Math.random。
async boot(ctx) {
systems = buildSystems(ctx.seed); // 建三个纯逻辑系统(注入受控随机)
phaserGame = new Phaser.Game({ // 或用 ctx.game(按宿主约定)
type: Phaser.AUTO,
scale: { mode: Phaser.Scale.FIT, width: 390, height: 844 },
scene: [BootScene, PlayScene, UIScene],
parent: ctx.canvas?.parentElement,
callbacks: { postBoot: g => g.registry.set('systems', systems) }, // 共享系统给各 scene
});
},
// readState():返回可观测语义 state。phase 是跨品类终态不变量;其余 per-品类。
// 只读、只供测试——渲染层与玩家界面绝不可显示它、不得据它给提示。
readState() {
return {
phase: systems.order.phase, // 'title'|'play'|'win'|'lose'|'gameover'(latch 终态)
coins: systems.resource.state.coins, // per-品类语义字段
ingredients: { ...systems.resource.state.ingredients },
orders: systems.order.snapshot(),
};
},
// injectInput(ev)(可选):harness 驱动入口。若 scene 已订阅 Phaser input,可省。
injectInput(ev) { /* 把 tap/key/drag 转成对系统命令的调用,或经 Phaser input 投递 */ },
// destroy():卸载卫生,幂等。必须 game.destroy(true) 释放 Phaser 资源(多游戏复位/session 切换)。
destroy() { if (phaserGame) { phaserGame.destroy(true); phaserGame = null; } },
};
}
六、latch 终态(游戏无 emit 通道,硬约束)
游戏没有 emit 通道向宿主报「我赢了」。终态走 latch:
- 跑到赢(金币达阈值)或输(连续 3 订单流失)时,把
readState().phase焊成终态值('win'/'lose'),并驻留不回弹——后续帧 readState 读到的 phase 恒定,绝不改回'play'。 - 宿主每帧轮询
readState().phase判终态落定。latch 门会多帧轮询验「终态不回弹」。 - 实现上:在 order-system 里维护一个
_phase,落定终态后加锁(if (this._phase === 'win' || this._phase === 'lose') return;在更新开头早返回),后续任何输入都不改它。
七、esbuild 多文件构建
build 工具用 esbuild 把 src/main.js 起手的多文件工程打成一个 iife bundle(入口 main.js,挂 buildProfile.globalName,默认沿用 __GameBundle)。你只管写对 import 关系:
- 用 ES module import(
import Phaser from 'phaser'、import { createMergeSystem } from '../systems/merge-system.js')。相对路径要对、带.js后缀(esbuild 默认不补)。 - import 图必须无环(A → B → A 会让 headless_check 挂)。系统之间用依赖注入(merge 收 resource 作为构造参数),别互相 import 成环。
- phaser 是外部依赖(depLock.phaser 锁版本,如
3.80.1),由 buildProfile 决定打进 bundle 还是外置。别 import 不在 depLock 里的包。
构建失败时 build 回你完整 esbuild 报错——按报错的文件:行定位,改 import 路径 / 语法,别整文件重写。
八、十类常见崩点(踩过的坑,绕开)
- 在 scene 里写逻辑 / 直接改系统状态 → 三联动门挂。逻辑只在 systems/,scene 只调命令。
- 裸
Math.random()→ 确定性重放失败、harness 驱动不可复现。随机从注入 seed 派生。 - import 路径漏
.js后缀或写错相对层级 → esbuild 报 "Could not resolve"。 - import 成环(系统互相 import)→ headless_check 挂。用依赖注入破环。
- 终态 phase 回弹(落定 win 后又被更新改回 play)→ latch 门挂。终态加锁早返回。
scene.start当scene.launch用(HUD 切换掉了玩法画面)→ 画面只剩 HUD。叠层用 launch。- 系统实例没在 scene 间共享(各 scene 各 new 一份)→ HUD 显示的金币和玩法里的对不上。系统建一次,经 registry / 共享上下文给所有 scene。
- readState 暴露给玩家界面 / 据它给提示 → 违反「只供测试」铁律。语义 state 渲染层不显示。
- 缺资产就崩(preload 的图加载失败未兜底)→ A_boot 挂。缺图降级用
add.rectangle色块占位。 - destroy 不幂等 / 不
game.destroy(true)→ 多游戏切换内存泄漏、canvas 残留。destroy 必须幂等且真释放 Phaser.Game。
九、靶子参照
mini-肥鹅 是 0号 spike 的对照靶子(tier2/contracts/mini-fei-e-fixture-spec.md):三系统(资源 / 合成 / 订单)+ 五耦合点 + 12 物品 / 6 合成链 / 5 订单,赢=金币 20→100,输=连续 3 流失。tier2/fixtures/mini-fei-e/src/systems/ 是它三个纯逻辑系统的预建样板——你写 Phaser 表现层时调它们,照它的命令式 API + {ok,reason} + 调用计数范式,把这套分离推广到任意经营题面。