fix(tier2): 抽平台命脉进锁定 play-runtime.js + play-scene 契约门(根因3 另一半)
spike feie-003 实证(lock+schema 都修对后):F_wiring 仍 calls=0、E_live distinct=1/N、 latch=false。diff 出真因——M3 重写 play-scene 时把两件平台命脉一起改没了: ① 输入路由(doHitTest/doHandleInput/this.input.on pointerdown):没了或坐标错位 (用 (col,row) 调 core.tapCell 而非 (i,j))→ driver 注入的 touch 落空/落错格 → 没合成 → F/G 门挂。 ② 逐帧 core.update(dt):没了 → 订单耐心不倒计时、赢/输 latch 永不触发 → E_live/H/经济/latch 全挂。 这俩原先混在 agent 可写的 play-scene 里、只靠注释「别动」——无机器门=必被 M3 改掉。 现抽进平台锁定文件 src/scenes/play-runtime.js(bindInput 输入路由 + tick 逐帧推进), 加进 LOCKED_PLATFORM_FILES;fixture play-scene 重构成 create 调 bindInput(this,core)、 update 调 tick(core,delta),薄委托命中给锁定实现(坐标 layout 同源、driver-tap 焊死)。 加 validate_play_scene 契约门(build 段跑):play-scene 必须 import play-runtime 且调 bindInput+tick, 缺了响亮回喂精确指引。prompt/hint 同步:你只管画 render,别碰输入路由与 tick。 门未放松(judge 纯代码判);fixture 重构后须重跑金标 smoke 验仍 ACCEPT(下一步)。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f8ec30c875
commit
0fd0d4fb10
108
tier2/fixtures/mini-fei-e/src/scenes/play-runtime.js
Normal file
108
tier2/fixtures/mini-fei-e/src/scenes/play-runtime.js
Normal file
@ -0,0 +1,108 @@
|
||||
/**
|
||||
* src/scenes/play-runtime.js —— mini-肥鹅 表现层【平台锁定运行时插座】(平台预建 · 写死不动)
|
||||
* owner:tier2 引擎线(M1 · scaffold platform 预建)| 这是 agent 改不动的锁定文件
|
||||
*
|
||||
* ════════════════════════════════════════════════════════════════════════════
|
||||
* 【为什么有这份 · spike feie-002/003 实证根因 3】
|
||||
* 原先「输入路由 + 逐帧 core.update tick」混在 agent 可写的 play-scene.js 里,只靠注释「别动」约束。
|
||||
* 实测 M3 重写 play-scene 时把这两件平台命脉一起改没了:
|
||||
* - 输入路由没了 / 坐标错位(用 (col,row) 调 core.tapCell 而非 (i,j),且 render 坐标与 layout 不同源)
|
||||
* → driver 注入的 touch 落空 / 落错格 → 没有合成 → F 门 calls=0、G/H 门连环挂。
|
||||
* - 逐帧 core.update(dt) 没了 → 订单耐心不倒计时、赢/输 latch 永不触发
|
||||
* → E_live distinct=1/N、H_progress latch=false、富[economy]/富[latch] 全挂。
|
||||
* 故把这两件平台命脉抽进本锁定文件,play-scene 只需在 create/update 各调一次。agent 写不动它,
|
||||
* 无论怎么画表现层,driver-tap 路径与终态推进都焊死可达。
|
||||
*
|
||||
* 【输入与逻辑分离铁律】
|
||||
* 表现层只「命中测试 → core 逻辑命令」,绝不自改 state(状态在 game-core,探针只读 readState)。
|
||||
* 命中映射坐标系 = 逻辑视口像素,与 layout.js 单一源(也是 game-core 导出 state.board[].x/y 的源、
|
||||
* 也是 harness driver tap 的源)—— 三处同源,driver tap 必落对格。
|
||||
* ════════════════════════════════════════════════════════════════════════════
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
// 布局常量单一源(命中映射;与 game-core readState 导出坐标、harness driver tap 同源)。
|
||||
import {
|
||||
BOARD_X, BOARD_Y, CELL, BOARD_SIZE,
|
||||
PANEL_X, PANEL_Y, PANEL_W, PANEL_ROW_H,
|
||||
} from '../layout.js';
|
||||
|
||||
/**
|
||||
* 命中测试:屏幕逻辑坐标 → 命中目标(棋盘格 (i,j) / 订单 instId)。
|
||||
* @param {object} core game-core(读 systems.order.ordersView 映射订单行)
|
||||
* @param {number} x @param {number} y
|
||||
* @returns {{kind:'cell', i:number, j:number} | {kind:'order', instId:string} | null}
|
||||
*/
|
||||
export function hitTest(core, x, y) {
|
||||
// 棋盘格命中(与渲染/导出坐标同源;注意 i=行=y 方向、j=列=x 方向,别搞反)。
|
||||
const lx = x - BOARD_X;
|
||||
const ly = y - BOARD_Y;
|
||||
if (lx >= 0 && ly >= 0) {
|
||||
const j = Math.floor(lx / CELL); // 列(x 方向)
|
||||
const i = Math.floor(ly / CELL); // 行(y 方向)
|
||||
if (i >= 0 && i < BOARD_SIZE && j >= 0 && j < BOARD_SIZE) {
|
||||
return { kind: 'cell', i, j };
|
||||
}
|
||||
}
|
||||
// 订单面板命中(driver 据 game-core 导出的 submitTargets 第 k 行点这里 → 真交单)。
|
||||
if (x >= PANEL_X && x <= PANEL_X + PANEL_W && y >= PANEL_Y) {
|
||||
const k = Math.floor((y - PANEL_Y) / PANEL_ROW_H);
|
||||
const orders = core.systems.order.ordersView();
|
||||
if (k >= 0 && k < orders.length) {
|
||||
return { kind: 'order', instId: orders[k].instId };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 归一化输入处理:命中后调 core 逻辑命令(tap 合成格两次合成 / tap 订单交单)。
|
||||
* host.injectInput 路径与 Phaser pointerdown 路径共用本实现。
|
||||
* @param {object} core
|
||||
* @param {{type:'tap'|'key'|'drag', x?:number, y?:number, key?:string}} ev
|
||||
*/
|
||||
export function handleInput(core, ev) {
|
||||
if (!core || !ev || ev.type !== 'tap') return;
|
||||
const hit = hitTest(core, ev.x, ev.y);
|
||||
if (!hit) return;
|
||||
if (hit.kind === 'cell') {
|
||||
// 命中棋盘格 → 合成逻辑命令((i,j) 顺序;逻辑在 core/merge-system)。
|
||||
core.tapCell(hit.i, hit.j);
|
||||
} else if (hit.kind === 'order') {
|
||||
// 命中订单 → 交单逻辑命令。
|
||||
core.fulfillOrder(hit.instId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 【平台锁定 · play-scene 在 create() 里调一次】绑定驱动可达的归一化输入路由。
|
||||
* 把 Phaser 指针事件 → handleInput → core 逻辑命令。坐标系 = 逻辑视口像素(layout 同源)。
|
||||
* harness driver 注入的真 touch 事件经此路径命中,**与表现层怎么画无关**。
|
||||
* @param {object} scene 活跃的 Phaser scene(取 scene.input)
|
||||
* @param {object} core game-core
|
||||
*/
|
||||
export function bindInput(scene, core) {
|
||||
try {
|
||||
// 单一集中订阅:整屏指针 → 命中映射 → core 逻辑命令(driver-tap 路径焊死)。
|
||||
scene.input.on('pointerdown', (p) => {
|
||||
handleInput(core, { type: 'tap', x: p.x, y: p.y });
|
||||
});
|
||||
} catch (e) {
|
||||
// headless 无 input 时吞错(逻辑核心仍可经 host.injectInput 驱动)。
|
||||
if (typeof console !== 'undefined') console.warn('[play-runtime] bindInput 异常:' + (e && e.message));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 【平台锁定 · play-scene 在 update(_time, delta) 里调一次】逐帧推进 core 逻辑。
|
||||
* 把帧时间换算 dt 秒喂给 core.update —— core 内部推进订单耐心倒计时、判赢/输并 latch phase。
|
||||
* **这步丢了游戏就永不到终态**(E_live/H_progress/经济/latch 门全挂),故焊进锁定文件。
|
||||
* @param {object} core
|
||||
* @param {number} delta 本帧毫秒(Phaser 给)
|
||||
*/
|
||||
export function tick(core, delta) {
|
||||
if (!core) return;
|
||||
const dt = (typeof delta === 'number' ? delta : 16.67) / 1000;
|
||||
core.update(dt);
|
||||
}
|
||||
@ -28,6 +28,9 @@ import {
|
||||
BOARD_X, BOARD_Y, CELL,
|
||||
PANEL_X, PANEL_Y, PANEL_W, PANEL_ROW_H,
|
||||
} from '../layout.js';
|
||||
// 平台锁定运行时插座(写死、agent 不许动):输入路由 + 逐帧 core.update tick。
|
||||
// create 调 bindInput(driver-tap 路径焊死)、update 调 tick(终态推进焊死);命中委托 hitTest/handleInput。
|
||||
import { bindInput, tick, hitTest as platformHitTest, handleInput as platformHandleInput } from './play-runtime.js';
|
||||
|
||||
/**
|
||||
* 创建主玩法 scene(返回 Phaser.Scene 配置对象;不顶层 import phaser,经 deps.Phaser 注入)。
|
||||
@ -54,54 +57,11 @@ export function createPlayScene(deps) {
|
||||
let _sceneRef = null;
|
||||
let _hudText = null;
|
||||
|
||||
/**
|
||||
* 命中测试:屏幕坐标 → 命中目标(棋盘格 / 订单面板;平台预建)。坐标系 = 逻辑视口像素(与 layout 同源)。
|
||||
* 定义为【闭包函数】而非对象方法——Phaser 用「配置对象」实例化 scene 时,只把 create/update/preload
|
||||
* 拷成生命周期回调,不会把对象上的其它方法搬到 scene 实例上,故 create 内 `this.hitTest` 会是 undefined。
|
||||
* 抽成闭包函数后,对象方法(host.injectInput 路径)与 Phaser pointerdown 路径都调它、不依赖 `this`。
|
||||
* @param {number} x @param {number} y
|
||||
* @returns {{kind:'cell', i:number, j:number} | {kind:'order', instId:string} | null}
|
||||
*/
|
||||
function doHitTest(x, y) {
|
||||
// 棋盘格命中(平台预建:与渲染坐标同源,确定性,driver 友好)。
|
||||
const lx = x - BOARD_X;
|
||||
const ly = y - BOARD_Y;
|
||||
if (lx >= 0 && ly >= 0) {
|
||||
const j = Math.floor(lx / CELL);
|
||||
const i = Math.floor(ly / CELL);
|
||||
if (i >= 0 && i < 3 && j >= 0 && j < 3) {
|
||||
return { kind: 'cell', i, j };
|
||||
}
|
||||
}
|
||||
// 订单面板命中(平台预建·driver 交单可达):第 k 行可点区映射到 ordersView()[k] 的 instId
|
||||
// (与 game-core 导出的 submitTargets 第 k 行同源),driver 据 submitTargets 点这里 → core.fulfillOrder 真交单。
|
||||
// 注:LLM 的 TODO-2/TODO-5 渲染订单面板时须用同一 layout(PANEL_*),才与本命中区 + driver tap 对齐。
|
||||
if (x >= PANEL_X && x <= PANEL_X + PANEL_W && y >= PANEL_Y) {
|
||||
const k = Math.floor((y - PANEL_Y) / PANEL_ROW_H);
|
||||
const orders = core.systems.order.ordersView();
|
||||
if (k >= 0 && k < orders.length) {
|
||||
return { kind: 'order', instId: orders[k].instId };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 归一化输入处理(闭包函数;命中后调 core 逻辑命令)。host.injectInput 与 Phaser pointerdown 共用。
|
||||
* @param {{type:'tap'|'key'|'drag', x?:number, y?:number, key?:string}} ev
|
||||
*/
|
||||
function doHandleInput(ev) {
|
||||
if (!core || !ev || ev.type !== 'tap') return;
|
||||
const hit = doHitTest(ev.x, ev.y);
|
||||
if (!hit) return;
|
||||
if (hit.kind === 'cell') {
|
||||
// 命中棋盘格 → 合成逻辑命令(两次点同物品合成,逻辑在 core/merge-system)。
|
||||
core.tapCell(hit.i, hit.j);
|
||||
} else if (hit.kind === 'order') {
|
||||
// 命中订单 → 交单逻辑命令。
|
||||
core.fulfillOrder(hit.instId);
|
||||
}
|
||||
}
|
||||
// 命中测试 / 归一化输入处理已抽进平台锁定的 play-runtime.js(写死、agent 不动)。
|
||||
// 这里只留薄委托闭包,既供 Phaser pointerdown 路径,也供下方对象方法(host.injectInput 路径)调用,
|
||||
// 两条路径同一实现、坐标 layout 同源 → driver-tap 必落对格。
|
||||
function doHitTest(x, y) { return platformHitTest(core, x, y); }
|
||||
function doHandleInput(ev) { platformHandleInput(core, ev); }
|
||||
|
||||
return {
|
||||
// Phaser scene 配置:key 由 main.js 注册时给('PlayScene');本对象作为 sceneConfig 传 scene.add。
|
||||
@ -131,13 +91,8 @@ export function createPlayScene(deps) {
|
||||
if (typeof console !== 'undefined') console.warn('[play-scene] create 占位渲染异常:' + (e && e.message));
|
||||
}
|
||||
|
||||
// Phaser 指针输入订阅(真玩时玩家点屏;坐标交命中 → core 逻辑命令)。
|
||||
// 调闭包 doHandleInput 而非 this.handleNormalizedInput——配置对象式 scene 的方法不在实例上(见 doHitTest 注释)。
|
||||
try {
|
||||
this.input.on('pointerdown', (p) => {
|
||||
doHandleInput({ type: 'tap', x: p.x, y: p.y });
|
||||
});
|
||||
} catch (e) { /* headless 无 input 时吞错 */ }
|
||||
// Phaser 指针输入订阅 → 平台锁定运行时插座(driver-tap 路径焊死;agent 改不动)。
|
||||
bindInput(this, core);
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════
|
||||
// 【LLM 填表现层 · TODO-1 · 棋盘格真渲染】
|
||||
@ -170,9 +125,9 @@ export function createPlayScene(deps) {
|
||||
* @param {number} delta 本帧毫秒(Phaser 给)
|
||||
*/
|
||||
update(_time, delta) {
|
||||
// 推进核心逻辑(dt 秒;core 内部判赢/输并 latch phase)。
|
||||
const dt = (typeof delta === 'number' ? delta : 16.67) / 1000;
|
||||
if (core) core.update(dt);
|
||||
// 推进核心逻辑 → 平台锁定运行时插座(终态推进焊死;agent 改不动)。
|
||||
// core 内部推进订单耐心倒计时、判赢/输并 latch phase。丢了这步游戏永不到终态。
|
||||
tick(core, delta);
|
||||
|
||||
// 平台预建:刷新最小 HUD 占位(金币 + phase)。LLM 的 TODO-3 会替换成真 HUD。
|
||||
if (_hudText && core) {
|
||||
|
||||
@ -96,6 +96,12 @@ FIXTURE_HINT_BUSINESS_SIM = """\
|
||||
**填完先调 validate_datatable**(平台预检:查 schema/DAG/可达性),过了再 build——build 也会自动跑这道预检。
|
||||
② 在 src/scenes/play-scene.js 的 TODO 处填表现层(约 56%):3×3 棋盘格真渲染、订单面板、金币/库存 HUD、合成命中反馈。
|
||||
逻辑命令(core.tapCell/core.fulfillOrder)、命中映射坐标系(BOARD_X/Y/CELL)、readState 语义约定都不要改。
|
||||
**两件平台命脉绝不能删**(它俩已抽进锁定文件 src/scenes/play-runtime.js,你写不动它):
|
||||
- create() 里必须保留 `bindInput(this, core)` —— 接 driver-tap 输入路由。删了它 driver 点击不进 core,
|
||||
合成永不发生,F/G 门挂、calls=0。
|
||||
- update(_time, delta) 里必须保留 `tick(core, delta)` —— 逐帧推进 core(订单耐心倒计时 + 赢/输 latch)。
|
||||
删了它游戏永不到终态,E_live/H_progress/经济/latch 门全挂。
|
||||
你只管画(render),别碰输入路由与 tick;它俩从 play-runtime.js import,坐标与 driver 同源、已对齐。
|
||||
语义 state(readState 已导出,别改):{phase, coins, ingredients, board, orders, failStreak} —— 富游戏三门据它真玩取证。
|
||||
"""
|
||||
|
||||
|
||||
@ -182,6 +182,13 @@ def build(game_id: str, entry: str = "src/main.js", *, global_name: str = DEFAUL
|
||||
+ "\n".join(f" - {e}" for e in dv["errors"])
|
||||
+ ("\n" + dv["expectedSchemaHint"] if dv.get("expectedSchemaHint") else ""))
|
||||
return {"ok": False, "bundlePath": None, "log": log, "durationMs": 0}
|
||||
# play-scene 契约预检(根因 3 另一半):必须接平台锁定的 bindInput(输入路由)+ tick(终态推进),
|
||||
# 否则游戏画得再好也过不了 F/G/H/经济/latch 门。同样是诚实早断 + 精确反馈,不放松门。
|
||||
pv = validate_play_scene(game_id)
|
||||
if not pv["ok"]:
|
||||
log = ("play-scene 未过平台契约预检(esbuild 未执行)。逐项问题:\n"
|
||||
+ "\n".join(f" - {e}" for e in pv["errors"]))
|
||||
return {"ok": False, "bundlePath": None, "log": log, "durationMs": 0}
|
||||
if not PHASER_BUILD_SCRIPT.exists():
|
||||
# 诚实失败:脚本未就位则给落地指引,不假装成功。
|
||||
return {"ok": False, "bundlePath": None, "durationMs": 0,
|
||||
@ -362,6 +369,42 @@ def validate_datatable(game_id: str) -> dict:
|
||||
return {"ok": not errs, "errors": errs, "expectedSchemaHint": schema_hint if errs else ""}
|
||||
|
||||
|
||||
def validate_play_scene(game_id: str) -> dict:
|
||||
"""校验 agent 写的 play-scene.js 是否仍接平台锁定的两件命脉:输入路由 bindInput + 逐帧 tick。
|
||||
|
||||
为什么要它(spike feie-002/003 实证根因 3 的另一半):driver-tap 路径与终态推进原先混在 agent 可写的
|
||||
play-scene 里,M3 重写时把它俩一起改没了:
|
||||
- 没接 bindInput → driver 注入的 touch 不路由到 core 逻辑命令 → 没合成 → F/G 门挂、calls=0;
|
||||
- 没调 tick(core.update) → 订单耐心不倒计时、赢/输 latch 永不触发 → E_live/H_progress/经济/latch 门挂。
|
||||
这两件已抽进平台锁定文件 play-runtime.js,agent 只需在 play-scene 的 create 调 bindInput(this, core)、
|
||||
update 调 tick(core, delta)。本检查确认这两个调用在,缺了就响亮告诉 agent 补上(否则它再怎么画表现层也过不了门)。
|
||||
|
||||
防 Goodhart:只查「这两个平台调用是否在场」(结构性,文本扫描即可判),不替 agent 设计表现层、不放松门。
|
||||
|
||||
Returns:
|
||||
{ok, errors:[人读字符串]}。
|
||||
"""
|
||||
wd = _workdir(game_id)
|
||||
ps = wd / "src" / "scenes" / "play-scene.js"
|
||||
if not ps.exists():
|
||||
return {"ok": False, "errors": ["缺 src/scenes/play-scene.js(表现层场景文件)。"]}
|
||||
src = ps.read_text(encoding="utf-8")
|
||||
errs: list[str] = []
|
||||
# 必须从锁定运行时插座 import 这两件(允许命名导入;名字本身就够判)。
|
||||
if "play-runtime" not in src:
|
||||
errs.append("play-scene.js 没 import './play-runtime.js'——平台锁定的输入路由 + tick 在那;"
|
||||
"必须 `import { bindInput, tick } from './play-runtime.js'`。")
|
||||
# create 里必须接输入路由(driver-tap 路径焊死)。
|
||||
if "bindInput(" not in src:
|
||||
errs.append("play-scene.js 的 create() 必须调 bindInput(this, core) 接平台输入路由——"
|
||||
"否则 harness driver 注入的点击不会路由到 core 逻辑命令,合成永不发生(F/G 门挂、calls=0)。")
|
||||
# update 里必须逐帧推进 core(终态推进焊死)。
|
||||
if "tick(" not in src:
|
||||
errs.append("play-scene.js 的 update(_time, delta) 必须调 tick(core, delta) 逐帧推进 core——"
|
||||
"否则订单耐心不倒计时、赢/输 latch 永不触发(E_live/H_progress/经济/latch 门挂)。")
|
||||
return {"ok": not errs, "errors": errs}
|
||||
|
||||
|
||||
def _derive_richgame_from_datatable(wd: Path) -> dict | None:
|
||||
"""从工程内 data/datatable.gold.json 派生三联动门静态校验所需的 richGame.{mergeChains,orders}。
|
||||
|
||||
|
||||
@ -41,6 +41,7 @@ LOCKED_PLATFORM_FILES = frozenset({
|
||||
"src/systems/merge-system.js", # 合成系统纯逻辑(tapCell 两次点同物品合成)
|
||||
"src/systems/order-system.js", # 订单系统纯逻辑
|
||||
"src/systems/reachability.js", # 可达性纯逻辑
|
||||
"src/scenes/play-runtime.js", # 表现层平台锁定运行时插座(输入路由 + 逐帧 core.update tick)
|
||||
})
|
||||
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user