Files
erqiwang_youle/client/tests/test_constants.js
T
joywayerandClaude Opus 5 a05cc7b7fa 二七王:常量层机械守卫测试补 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>
2026-08-26 21:46:41 +08:00

179 lines
8.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 常量层机械守卫:号段合规、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);