二七王:常量层机械守卫测试补 ctx.w/ctx.h 求解冒烟兜底
求解冒烟第一次跑真实跑出 9 个节点(叫分档位分数文字、花色数量文字、牌角标、
结算文案等)因配置有意不写 w/h(真实宽高等文案渲染出来才知道)而求解失败;
裁决方向是给 LayoutSolver 补 ctx.w/ctx.h 运行时注入通道(已由 6390ce7 落地,
与既有 ctx.target 对称)。本次让冒烟测试的合成 ctx 对称跟进:仅在配置(含
bySeat 覆盖后)确实缺失 w/h 时才注入假尺寸,不覆盖配置自带的值,保证两条
路径都被真实覆盖到。60 个布局节点、84 次求解全部通过。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,178 @@
|
||||
// 常量层机械守卫:号段合规、ID 不重复、布局引用可解析
|
||||
const { load } = require('./_load');
|
||||
const t = require('./_assert')();
|
||||
|
||||
load('client/js/01_SubGame/codes/config/Layers.js');
|
||||
load('client/js/01_SubGame/codes/config/Groups.js');
|
||||
load('client/js/01_SubGame/codes/config/ImageResources.js');
|
||||
load('client/js/01_SubGame/codes/config/SoundResources.js');
|
||||
load('client/js/01_SubGame/codes/config/Sprites_Table.js');
|
||||
load('client/js/01_SubGame/codes/config/Sprites_Cards.js');
|
||||
load('client/js/01_SubGame/codes/config/Sprites_Action.js');
|
||||
load('client/js/01_SubGame/codes/config/Sprites_Result.js');
|
||||
load('client/js/01_SubGame/codes/config/Sprites_CreateRoom.js');
|
||||
load('client/js/01_SubGame/codes/config/LayoutConstants.js');
|
||||
load('client/js/01_SubGame/codes/config/Layout_Table.js');
|
||||
load('client/js/01_SubGame/codes/config/Layout_Cards.js');
|
||||
load('client/js/01_SubGame/codes/config/Layout_Action.js');
|
||||
load('client/js/01_SubGame/codes/config/Layout_Result.js');
|
||||
load('client/js/01_SubGame/codes/config/Layout_CreateRoom.js');
|
||||
load('client/js/01_SubGame/codes/config/AnimConstants.js');
|
||||
load('client/js/01_SubGame/codes/core/SpriteIndex.js');
|
||||
|
||||
// ---- 图层号段:101–200 / 301–400 / 501–600 / 701+(client 02 §1)----
|
||||
// 平台图层 27(建房)是引用而非子游戏自建,单独排除
|
||||
const layerVals = Object.keys(EQW_Layers).filter(k => k !== 'PLATFORM_CREATE_ROOM').map(k => EQW_Layers[k]);
|
||||
const layerOk = layerVals.every(v =>
|
||||
(v >= 101 && v <= 200) || (v >= 301 && v <= 400) || (v >= 501 && v <= 600) || v >= 701);
|
||||
t.eq('图层落在子游戏段', layerOk, true);
|
||||
t.eq('图层 27 是平台的建房界面', EQW_Layers.PLATFORM_CREATE_ROOM, 27);
|
||||
|
||||
// ---- 群组 ≥201 ----
|
||||
const groupVals = Object.keys(EQW_Groups).map(k => EQW_Groups[k]);
|
||||
t.eq('群组均 >= 201', groupVals.every(v => v >= 201), true);
|
||||
|
||||
// ---- 图片资源 ≥501;声音 ≥101 ----
|
||||
const imgVals = Object.keys(EQW_Images).map(k => EQW_Images[k]);
|
||||
t.eq('图片资源均 >= 501', imgVals.every(v => v >= 501), true);
|
||||
const soundVals = Object.keys(EQW_Sounds).map(k => EQW_Sounds[k]);
|
||||
t.eq('声音资源均 >= 101', soundVals.every(v => v >= 101), true); // T-20 未定时为空数组,恒真
|
||||
|
||||
// ---- 精灵 ID 1001–2999(段内 3000 已被平台占用,清单 §0.3)----
|
||||
const index = EQW_SpriteIndex.build(EQW_Sprites);
|
||||
const spriteVals = Object.keys(index).map(k => index[k]);
|
||||
t.eq('精灵数量 > 200', spriteVals.length > 200, true);
|
||||
const spriteOk = spriteVals.every(v => v >= 1001 && v <= 2999);
|
||||
if (!spriteOk) { console.log('越界精灵: ' + JSON.stringify(spriteVals.filter(v => v < 1001 || v > 2999))); }
|
||||
t.eq('精灵落在 1001-2999', spriteOk, true);
|
||||
|
||||
// ---- 各类 ID 全局不重复 ----
|
||||
function dupsOf(vals) {
|
||||
const seen = {}, dups = [];
|
||||
vals.forEach(v => { if (seen[v]) { dups.push(v); } seen[v] = true; });
|
||||
return dups;
|
||||
}
|
||||
t.eq('精灵 ID 无重复', dupsOf(spriteVals), []);
|
||||
t.eq('图层 ID 无重复', dupsOf(layerVals), []);
|
||||
t.eq('群组 ID 无重复', dupsOf(groupVals), []);
|
||||
t.eq('图片资源 ID 无重复', dupsOf(imgVals), []);
|
||||
|
||||
// ---- 布局配置里每个 attach.target 都能解析 ----
|
||||
// target 可指向三类之一:子游戏精灵键名 / EQW_Layout 的布局节点名 / 平台精灵键名
|
||||
// (平台头像框落在框架保留段 1–1000,独立登记、不参与子游戏号段校验)
|
||||
const resolvable = {};
|
||||
Object.keys(index).forEach(k => { resolvable[k] = true; });
|
||||
Object.keys(EQW_Layout).forEach(k => { resolvable[k] = true; });
|
||||
Object.keys(EQW_PlatformSprites).forEach(k => { resolvable[k] = true; });
|
||||
|
||||
// 收集所有布局节点,供解析检查与求解冒烟共用
|
||||
const allNodes = []; // { path, node }
|
||||
function collectNodes(node, path) {
|
||||
if (!node || typeof node !== 'object' || Array.isArray(node)) { return; }
|
||||
if (node.kind) { allNodes.push({ path, node }); }
|
||||
Object.keys(node).forEach(k => {
|
||||
if (k !== 'bySeat' && node[k] && typeof node[k] === 'object' && !Array.isArray(node[k])) {
|
||||
collectNodes(node[k], path + '.' + k);
|
||||
}
|
||||
});
|
||||
}
|
||||
Object.keys(EQW_Layout).forEach(k => collectNodes(EQW_Layout[k], k));
|
||||
|
||||
const missing = [];
|
||||
allNodes.forEach(entry => {
|
||||
const targets = [];
|
||||
if (entry.node.target) { targets.push([entry.path, entry.node.target]); }
|
||||
if (entry.node.bySeat) {
|
||||
Object.keys(entry.node.bySeat).forEach(s => {
|
||||
const v = entry.node.bySeat[s];
|
||||
if (v && v.target) { targets.push([entry.path + '.bySeat.' + s, v.target]); }
|
||||
});
|
||||
}
|
||||
targets.forEach(pair => { if (!resolvable[pair[1]]) { missing.push(pair[0] + ' -> ' + pair[1]); } });
|
||||
});
|
||||
if (missing.length) { console.log('无法解析的 attach.target:\n ' + missing.join('\n ')); }
|
||||
t.eq('attach.target 全部可解析', missing, []);
|
||||
|
||||
// ---- 求解冒烟:每个布局节点都真的能被求解器算出来 ----
|
||||
// 这是配置与求解器之间契约的最强守卫——字段名写错、缺 w/h、kind 不认识、
|
||||
// bySeat 缺座位,全都会在这里当场抛错,而不是等到界面上静默摆歪。
|
||||
load('client/js/gameabc-framework/ui/AlignmentUtils.js');
|
||||
load('client/js/01_SubGame/codes/ui/LayoutSolver.js');
|
||||
|
||||
// 合成一个万能 ctx:所有可解析的键都给一个矩形,count 给一个合理值
|
||||
const fakeRects = {};
|
||||
Object.keys(resolvable).forEach(k => { fakeRects[k] = { x: 100, y: 100, width: 80, height: 40 }; });
|
||||
|
||||
const solveFailures = [];
|
||||
allNodes.forEach(entry => {
|
||||
// 有 bySeat 的按三个显示位各求一次,没有的求一次
|
||||
const seats = entry.node.bySeat ? Object.keys(entry.node.bySeat) : [null];
|
||||
seats.forEach(seat => {
|
||||
const ctx = { rects: fakeRects, count: 8 };
|
||||
if (seat) { ctx.seat = seat; }
|
||||
// 无 target/w/h 的 attach 模板节点:运行时由调用方注入,这里补假值——
|
||||
// 只在配置(含 bySeat 覆盖后)确实没写时才补,不覆盖配置里已有的值,
|
||||
// 否则冒烟就测不到「配置自带尺寸」那条真实路径
|
||||
const seatOverride = seat && entry.node.bySeat[seat] ? entry.node.bySeat[seat] : null;
|
||||
const seatTarget = seatOverride ? seatOverride.target : null;
|
||||
const seatW = seatOverride ? seatOverride.w : undefined;
|
||||
const seatH = seatOverride ? seatOverride.h : undefined;
|
||||
const effW = (seatW !== undefined) ? seatW : entry.node.w;
|
||||
const effH = (seatH !== undefined) ? seatH : entry.node.h;
|
||||
if (entry.node.kind === 'attach' && !entry.node.target && !seatTarget) {
|
||||
ctx.target = Object.keys(fakeRects)[0];
|
||||
}
|
||||
if (entry.node.kind === 'attach' && typeof effW !== 'number') { ctx.w = 60; }
|
||||
if (entry.node.kind === 'attach' && typeof effH !== 'number') { ctx.h = 24; }
|
||||
const label = entry.path + (seat ? '[' + seat + ']' : '');
|
||||
try {
|
||||
const r = EQW_LayoutSolver.solve(entry.node, ctx);
|
||||
if (!Array.isArray(r)) { solveFailures.push(label + ' -> 返回值不是数组'); return; }
|
||||
r.forEach((rect, i) => {
|
||||
if (typeof rect.x !== 'number' || isNaN(rect.x) ||
|
||||
typeof rect.y !== 'number' || isNaN(rect.y)) {
|
||||
solveFailures.push(label + ' -> 第' + i + '项坐标非法: ' + JSON.stringify(rect));
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
solveFailures.push(label + ' -> ' + e.message);
|
||||
}
|
||||
});
|
||||
});
|
||||
if (solveFailures.length) { console.log('求解失败的节点:\n ' + solveFailures.join('\n ')); }
|
||||
t.eq('全部布局节点可求解且坐标合法', solveFailures, []);
|
||||
t.eq('布局节点数量 > 40', allNodes.length > 40, true);
|
||||
|
||||
// ---- 布局文件是纯数据:不含函数 ----
|
||||
const funcs = [];
|
||||
function checkPure(node, path) {
|
||||
if (!node || typeof node !== 'object') { return; }
|
||||
Object.keys(node).forEach(k => {
|
||||
const v = node[k];
|
||||
if (typeof v === 'function') { funcs.push(path + '.' + k); }
|
||||
else if (v && typeof v === 'object') { checkPure(v, path + '.' + k); }
|
||||
});
|
||||
}
|
||||
Object.keys(EQW_Layout).forEach(k => checkPure(EQW_Layout[k], k));
|
||||
t.eq('布局配置不含函数', funcs, []);
|
||||
|
||||
// ---- 每个布局节点的 kind 合法 ----
|
||||
const badKinds = [];
|
||||
function checkKind(node, path) {
|
||||
if (!node || typeof node !== 'object') { return; }
|
||||
if (node.kind && ['point', 'line', 'fan', 'grid', 'attach'].indexOf(node.kind) < 0) {
|
||||
badKinds.push(path + ' -> ' + node.kind);
|
||||
}
|
||||
Object.keys(node).forEach(k => {
|
||||
if (node[k] && typeof node[k] === 'object' && !Array.isArray(node[k])) { checkKind(node[k], path + '.' + k); }
|
||||
});
|
||||
}
|
||||
Object.keys(EQW_Layout).forEach(k => checkKind(EQW_Layout[k], k));
|
||||
t.eq('布局 kind 全部合法', badKinds, []);
|
||||
|
||||
// ---- 牌尺寸档引用的资源键存在 ----
|
||||
['L', 'M', 'S'].forEach(size => {
|
||||
t.eq('CARD_SIZE.' + size + ' 的资源存在', EQW_Images.hasOwnProperty(EQW_Layout.CARD_SIZE[size].res), true);
|
||||
});
|
||||
|
||||
process.exit(t.done('constants') ? 0 : 1);
|
||||
Reference in New Issue
Block a user