From 44d1b455836f969f49796f1cd70970814cec3f61 Mon Sep 17 00:00:00 2001 From: zizi Date: Tue, 23 Jun 2026 20:43:10 +0000 Subject: [PATCH] =?UTF-8?q?fix(tier2):=20=E4=BF=AE=E7=9C=9F=E6=8E=A5?= =?UTF-8?q?=E7=BA=BF=E4=B8=89=E5=A4=84=E2=80=94=E2=80=94pointerdown=20?= =?UTF-8?q?=E9=97=AD=E5=8C=85/=E9=80=90=E5=B8=A7=E8=A1=A5=E6=A1=8C/patienc?= =?UTF-8?q?e=20=E7=BC=A9=E5=88=B0=E6=9C=89=E7=95=8C=E5=8F=AF=E8=BE=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CDP 探针实证 driver tap 抛「this.handleNormalizedInput is not a function」→ state0/state1 全 null、E_live=1、F_wiring=0、九门连锁红。三处根因+修: ① Phaser 用「配置对象」实例化 scene 时只搬 create/update/preload,不搬对象上其它 方法 → create 内 `this.handleNormalizedInput`/`this.hitTest` undefined,玩家点屏即抛。 修:把 hitTest/handleNormalizedInput 抽成闭包 doHitTest/doHandleInput,pointerdown 与 host.injectInput 都调闭包、不依赖 this(对象方法保留为薄委托)。 ② 合成清空棋盘格、产物只进库存不回落棋盘 → 棋盘很快只剩单件、driver 无对可合成、 赢路卡在中途(offline sim:coins 停 54)。修:core.update 逐帧 merge.refillFromInventory() (幂等,把库存里没上桌的基础料补进空格)→ offline sim 赢路达 coins116/win。 ③ gold patience 440~600s 在有界 driver run(~30s)里订单永不流失 → 经济门破产路 不可达。修:patience 缩到 16~20s(赢路~6s 完成远早于;破产路~20s 内 3 单流失=lose)。 忠实 offline sim 验:patience≤30s 两路都达终态,≥60s 破产路 stuck=play。 验证:headless-smoke + logic-smoke 全绿(赢/输双路可达 + latch 不回弹)。 Co-Authored-By: Claude Opus 4.8 (1M context) --- .../mini-fei-e/data/datatable.gold.json | 12 +-- tier2/fixtures/mini-fei-e/src/game-core.js | 4 + .../mini-fei-e/src/scenes/play-scene.js | 94 +++++++++++-------- 3 files changed, 66 insertions(+), 44 deletions(-) diff --git a/tier2/fixtures/mini-fei-e/data/datatable.gold.json b/tier2/fixtures/mini-fei-e/data/datatable.gold.json index cc56bf37..c752d42a 100644 --- a/tier2/fixtures/mini-fei-e/data/datatable.gold.json +++ b/tier2/fixtures/mini-fei-e/data/datatable.gold.json @@ -1,6 +1,6 @@ { "schemaVersion": "mini-fei-e-1.0", - "_note": "金标样例(人工预建)——供 driver / 三层校验门 / 富游戏三门做确定性对照测试,不依赖 LLM。本份满足所有 spike 门约束:① mergeChains 是 6 条不相交边(item-base→item-product)、DAG 无环;② 5 个订单的 requires 全是合成产出物(tier2),requiresReachable 通过;③ 开局 20→赢线 100、连续 3 流失=输线;④ unlockCoins 体现金币门控解锁(耦合点5)。主题=mini-肥鹅小卖部,照 wanglanmei-ref 砍小。", + "_note": "金标样例(人工预建)——供 driver / 三层校验门 / 富游戏三门做确定性对照测试,不依赖 LLM。本份满足所有 spike 门约束:① mergeChains 是 6 条不相交边(item-base→item-product)、DAG 无环;② 5 个订单的 requires 全是合成产出物(tier2),requiresReachable 通过;③ 开局 20→赢线 100、连续 3 流失=输线;④ unlockCoins 体现金币门控解锁(耦合点5)。主题=mini-肥鹅小卖部,照 wanglanmei-ref 砍小。patience 单位=秒(game-core remainMs=patience*1000),取 16~20s 是为两路都能在有界 driver run 里跑到终态:赢路约 6s 完成(远早于 patience),破产路只合成不交单 → 3 单约 16~20s 内耗尽流失=lose(原 440~600s 在有界 run 里无法流失,经济门破产路不可达,本份修正)。", "currencies": { "coins": { "id": "coins", "initial": 20, "label": "金币" }, "ingredients": { @@ -32,11 +32,11 @@ { "from": "egg", "to": "cake", "cost": 2, "unlockCoins": 80 } ], "orders": [ - { "id": "order-1", "requires": { "bread": 1 }, "reward": 18, "patience": 600, "customerAssetId": "ph-customer-kid" }, - { "id": "order-2", "requires": { "jam": 1 }, "reward": 22, "patience": 560, "customerAssetId": "ph-customer-lady" }, - { "id": "order-3", "requires": { "soda": 1, "bread": 1 }, "reward": 30, "patience": 520, "customerAssetId": "ph-customer-grandpa" }, - { "id": "order-4", "requires": { "cream": 1, "chocolate": 1 }, "reward": 40, "patience": 480, "customerAssetId": "ph-customer-auntie" }, - { "id": "order-5", "requires": { "cake": 1 }, "reward": 48, "patience": 440, "customerAssetId": "ph-customer-kid" } + { "id": "order-1", "requires": { "bread": 1 }, "reward": 18, "patience": 20, "customerAssetId": "ph-customer-kid" }, + { "id": "order-2", "requires": { "jam": 1 }, "reward": 22, "patience": 19, "customerAssetId": "ph-customer-lady" }, + { "id": "order-3", "requires": { "soda": 1, "bread": 1 }, "reward": 30, "patience": 18, "customerAssetId": "ph-customer-grandpa" }, + { "id": "order-4", "requires": { "cream": 1, "chocolate": 1 }, "reward": 40, "patience": 17, "customerAssetId": "ph-customer-auntie" }, + { "id": "order-5", "requires": { "cake": 1 }, "reward": 48, "patience": 16, "customerAssetId": "ph-customer-kid" } ], "winCondition": { "coinsTarget": 100 }, "loseCondition": { "consecutiveOrderFails": 3 } diff --git a/tier2/fixtures/mini-fei-e/src/game-core.js b/tier2/fixtures/mini-fei-e/src/game-core.js index 88c66f7c..c7b8d4cb 100644 --- a/tier2/fixtures/mini-fei-e/src/game-core.js +++ b/tier2/fixtures/mini-fei-e/src/game-core.js @@ -157,6 +157,10 @@ export function createGameCore(deps) { update(dtSeconds) { if (state.phase !== 'play') return; // title / 终态不推进逻辑 order.update(dtSeconds); + // 逐帧补桌:合成会清空棋盘格(产物只进库存、不回落棋盘),不补桌则棋盘很快只剩单件、driver 无对可合成、 + // 赢路径被卡(coins 停在中途)。refillFromInventory 幂等——只把库存里还没上桌的基础料补进空格, + // 棋盘满 / 库存耗尽即停。这是「棋盘=合成工作台、库存=料仓」设计下保正循环可持续的必需步(logic-smoke 同源)。 + merge.refillFromInventory(); state.coins = resource.state.coins; // 同步渲染缓存 _settle(); // 赢 / 输判定 + latch }, diff --git a/tier2/fixtures/mini-fei-e/src/scenes/play-scene.js b/tier2/fixtures/mini-fei-e/src/scenes/play-scene.js index 869153cf..19b21bde 100644 --- a/tier2/fixtures/mini-fei-e/src/scenes/play-scene.js +++ b/tier2/fixtures/mini-fei-e/src/scenes/play-scene.js @@ -54,6 +54,55 @@ 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); + } + } + return { // Phaser scene 配置:key 由 main.js 注册时给('PlayScene');本对象作为 sceneConfig 传 scene.add。 // 注:用对象式 scene(Phaser 支持 {create, update} 配置对象),避免顶层 class extends Phaser.Scene 依赖。 @@ -82,10 +131,11 @@ export function createPlayScene(deps) { if (typeof console !== 'undefined') console.warn('[play-scene] create 占位渲染异常:' + (e && e.message)); } - // Phaser 指针输入订阅(真玩时玩家点屏;坐标交 hitTest 命中 → core 逻辑命令)。 + // Phaser 指针输入订阅(真玩时玩家点屏;坐标交命中 → core 逻辑命令)。 + // 调闭包 doHandleInput 而非 this.handleNormalizedInput——配置对象式 scene 的方法不在实例上(见 doHitTest 注释)。 try { this.input.on('pointerdown', (p) => { - this.handleNormalizedInput({ type: 'tap', x: p.x, y: p.y }); + doHandleInput({ type: 'tap', x: p.x, y: p.y }); }); } catch (e) { /* headless 无 input 时吞错 */ } @@ -135,52 +185,20 @@ export function createPlayScene(deps) { /** * 归一化输入入口(main.js 的 PhaserGameInstance.injectInput 路由到这里;harness 驱动 + 真玩共用)。 - * 做命中测试:屏幕坐标 → 棋盘格 (i,j) / 订单 → 调 core 逻辑命令。 + * 委托给闭包 doHandleInput(不依赖 this,与 Phaser pointerdown 路径同一实现)。 * @param {{type:'tap'|'key'|'drag', x?:number, y?:number, key?:string}} ev */ handleNormalizedInput(ev) { - if (!core || !ev || ev.type !== 'tap') return; - const hit = this.hitTest(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); - } + doHandleInput(ev); }, /** - * 命中测试:屏幕坐标 → 命中目标(平台预建棋盘格映射;订单映射留 TODO 给 LLM 定面板布局后补)。 - * 坐标系 = 逻辑视口像素(与 BOARD_X/BOARD_Y/CELL 同源)。 + * 命中测试(外部可调;委托闭包 doHitTest)。坐标系 = 逻辑视口像素(与 layout 同源)。 * @param {number} x @param {number} y * @returns {{kind:'cell', i:number, j:number} | {kind:'order', instId:string} | null} */ hitTest(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 行的可点区 = [PANEL_X, PANEL_X+PANEL_W] × [PANEL_Y+k*ROW_H, +ROW_H]。 - // 把命中行 k 映射到 ordersView()[k] 的 instId(与 game-core 导出的 submitTargets 第 k 行同源), - // driver 据 submitTargets 点这里 → core.fulfillOrder(instId) 真交单。 - // 注: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; + return doHitTest(x, y); }, }; }