fix(tier2): fixture readState 投影成 driver 契约形状——破 board is not iterable

C_frame 后 harness 抛「board is not iterable」:business-sim.driver findMergePair
对 state.board 做 for...of,但 game-core readState 旧导出 board=merge.boardView()
={size,cells[i][j],selected}(对象,非数组,无可点坐标)→ 抛错、其后八门全「门未执行」。

根因=fixture 导出态与 driver 契约(business-sim.driver §读什么 state)不一致。改:
- 抽 layout.js 单一源(棋盘 BOARD_*/CELL + 订单面板 PANEL_* 逻辑坐标),game-core 与
  play-scene 同源不漂移。
- game-core readState 投影成 driver 契约形状:board=扁平格数组[{idx,i,j,x,y,item}]、
  orders 补 id 别名=instId、新增 submitTargets[{orderId,x,y}]、mergeChains、
  lostStreak(=failStreak 别名)、selected(I 门读)。
- play-scene 填平台侧 TODO-4 订单命中:面板行 k → ordersView()[k].instId →
  core.fulfillOrder,与 submitTargets 同源,driver 交单 tap 真可达。
内部网格视图(merge.boardView)仍供 scene 渲染,两者各取所需不冲突。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
zizi 2026-06-23 20:31:25 +00:00
parent da184f067c
commit 712547d212
3 changed files with 104 additions and 16 deletions

View File

@ -31,6 +31,7 @@ import { createOrderSystem } from './systems/order-system.js';
import {
computeReachableSet, checkRequiresReachable, checkDagAcyclic,
} from './systems/reachability.js';
import { cellCenter, orderRowCenter } from './layout.js';
/**
* 创建 mini-肥鹅 游戏核心
@ -91,17 +92,55 @@ export function createGameCore(deps) {
/**
* 可观测状态快照A6 语义 state _forensicsView().state() 同源
* PhaserGameInstance.readState / play-scene HUD / latch 门都读它单独暴露省去每处都 _forensicsView().state()
* @returns {{phase:string, coins:number, ingredients:object, board:object, orders:object[], failStreak:number}}
* PhaserGameInstance.readState / play-scene HUD / latch / harness driver 都读它
*
* 形状对齐 harness driver 契约business-sim.driver.js §读什么 state
* driver state 自适应出招,需要每个棋盘格 / 订单的可点逻辑坐标才能 tap,故本视图把内部
* 网格态投影成 driver 契约形状:
* - board: 扁平格数组 [{idx,i,j,x,y,item}](x/y=格中心逻辑坐标, play-scene 渲染/命中同源 layout.js)
* - orders: [{id,instId,requires,reward,patience,status}](id=instId 别名;driver id/status 选可交单)
* - submitTargets: [{orderId,x,y}](每单交单可点坐标;driver 据它点交单, hitTest 订单命中区同源)
* - mergeChains: [{from,to,cost}](driver 据它把盲合成 steer 合成出订单要的物品)
* - lostStreak: 连续流失计数别名(driver 破产路判 lose;= failStreak)
* :内部网格视图仍经 core.systems.merge.boardView() play-scene 渲染(二维 cells),两者各取所需不冲突
* @returns {{phase:string, coins:number, ingredients:object, board:object[], orders:object[], submitTargets:object[], mergeChains:object[], lostStreak:number, failStreak:number}}
*/
readState() {
const bv = merge.boardView(); // { size, cells[i][j], selected }
const size = bv.size;
// 扁平化棋盘 → driver 可点格数组(带格中心逻辑坐标,layout 单一源)。
const board = [];
for (let i = 0; i < size; i++) {
for (let j = 0; j < size; j++) {
const c = cellCenter(i, j);
board.push({ idx: i * size + j, i, j, x: c.x, y: c.y, item: bv.cells[i][j] });
}
}
const ordersRaw = order.ordersView(); // [{instId,tmplId,requires,reward,patience,remainMs,status}]
// 订单投影成 driver 契约形状(补 id 别名 = instId;只把 active 当可交单)。
const orders = ordersRaw.map((o) => ({
id: o.instId, instId: o.instId, requires: { ...o.requires },
reward: o.reward, patience: o.patience, remainMs: o.remainMs, status: o.status,
}));
// 交单坐标:每个在场订单一行(与 play-scene 订单命中区 orderRowCenter 同源),driver 据它点交单。
const submitTargets = ordersRaw.map((o, k) => {
const r = orderRowCenter(k);
return { orderId: o.instId, x: r.x, y: r.y };
});
// 合成链(driver steer 用;cost 透传,unlockCoins 对 driver 选格不必需故不带)。
const mergeChains = MERGE_CHAINS.map((c) => ({ from: c.from, to: c.to, cost: c.cost }));
const failStreak = order.failStreak();
return {
phase: state.phase,
coins: resource.state.coins,
ingredients: { ...resource.state.ingredients },
board: merge.boardView(),
orders: order.ordersView(),
failStreak: order.failStreak(),
board,
orders,
submitTargets,
mergeChains,
selected: bv.selected, // 控制体(I 门 controlCheck 读 selected 判点击响应)
lostStreak: failStreak, // driver 破产路读 lostStreak(= failStreak 别名)
failStreak,
};
},

View File

@ -0,0 +1,41 @@
/**
* src/layout.js mini-肥鹅 布局常量单一源(棋盘 + 订单面板逻辑坐标平台预建)
* ownertier2 引擎线(M1 · scaffold platform 预建部分)
*
*
* 这份是什么 · 为什么单独抽一份
* driver(business-sim.driver.js)读语义 state 自适应出招它需要每个棋盘格 / 每个订单的可点逻辑坐标 x/y
* 才能 tap这套坐标原先只活在 play-scene BOARD_X/BOARD_Y/CELL,game-core 导出 state 时拿不到 导出的
* board 是无坐标的 {cells[i][j]} 网格,driver 无法点 + `for...of` 一个对象直接抛board is not iterable
*
* 抽成单一源:game-core 据它把 board/订单坐标算进 readState(),play-scene 据它渲染 + hitTest
* 两边坐标系同源不漂移(渲染坐标 = 命中坐标 = driver tap 坐标)
*
* 坐标系逻辑视口像素(390×844; host viewport / CDP 取证视口对齐)
*
*/
'use strict';
// 棋盘 3×3 居中偏上,每格 110px,左上角原点 (BOARD_X, BOARD_Y)。
export const BOARD_X = 30;
export const BOARD_Y = 120;
export const CELL = 110;
export const BOARD_SIZE = 3;
// 订单面板:屏下半部竖排,每单一行可点(交单命中区)。driver 据导出的 submitTargets 点这里交单。
// 面板从 PANEL_Y 起,每行 PANEL_ROW_H 高,可点区横向 [PANEL_X, PANEL_X+PANEL_W]。
export const PANEL_X = 20;
export const PANEL_Y = 470;
export const PANEL_W = 350;
export const PANEL_ROW_H = 64;
/** 棋盘格 (i 行, j 列) → 格中心逻辑坐标 {x, y}(渲染 / 命中 / driver tap 同源)。 */
export function cellCenter(i, j) {
return { x: BOARD_X + j * CELL + CELL / 2, y: BOARD_Y + i * CELL + CELL / 2 };
}
/** 订单第 k 行(0 基)→ 交单可点区中心逻辑坐标 {x, y}。 */
export function orderRowCenter(k) {
return { x: PANEL_X + PANEL_W / 2, y: PANEL_Y + k * PANEL_ROW_H + PANEL_ROW_H / 2 };
}

View File

@ -23,6 +23,12 @@
'use strict';
// 布局常量单一源(棋盘 + 订单面板逻辑坐标;与 game-core readState 导出坐标同源,见 layout.js)。
import {
BOARD_X, BOARD_Y, CELL,
PANEL_X, PANEL_Y, PANEL_W, PANEL_ROW_H,
} from '../layout.js';
/**
* 创建主玩法 scene返回 Phaser.Scene 配置对象不顶层 import phaser deps.Phaser 注入
* @param {Object} deps
@ -42,11 +48,7 @@ export function createPlayScene(deps) {
const VIEW_W = 390;
const VIEW_H = 844;
// 棋盘布局常量命中映射用平台预建定坐标系LLM 渲染须按此映射对齐)。
// 3×3 棋盘居中偏上,每格 110px左上角原点 (BOARD_X, BOARD_Y)。
const BOARD_X = 30;
const BOARD_Y = 120;
const CELL = 110;
// 棋盘 / 订单面板布局常量从 layout.js 单一源取(命中映射用;与 game-core readState 导出坐标同源,driver tap 对齐)。
// scene 内部句柄create 时填;占位渲染用)。
let _sceneRef = null;
@ -166,12 +168,18 @@ export function createPlayScene(deps) {
return { kind: 'cell', i, j };
}
}
// ════════════════════════════════════════════════════════════════════
// 【LLM 填表现层 · TODO-4 · 订单命中映射】
// LLM 定了订单面板布局TODO-2在此把面板区域内的点击映射到 { kind:'order', instId }。
// 约定:返回 core.systems.order.ordersView() 里对应订单的 instIddriver 会点这块区域交单。
// spike 第一段driver 走 host.injectInput 直接给订单区坐标,故此映射须与 TODO-2 面板布局对齐。
// ════════════════════════════════════════════════════════════════════
// ── 订单面板命中(平台预建·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;
},
};