From a05cc7b7fa12799942817e622033fe84c7063ec8 Mon Sep 17 00:00:00 2001 From: Joywayer Date: Wed, 26 Aug 2026 21:46:41 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BA=8C=E4=B8=83=E7=8E=8B=EF=BC=9A=E5=B8=B8?= =?UTF-8?q?=E9=87=8F=E5=B1=82=E6=9C=BA=E6=A2=B0=E5=AE=88=E5=8D=AB=E6=B5=8B?= =?UTF-8?q?=E8=AF=95=E8=A1=A5=20ctx.w/ctx.h=20=E6=B1=82=E8=A7=A3=E5=86=92?= =?UTF-8?q?=E7=83=9F=E5=85=9C=E5=BA=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 求解冒烟第一次跑真实跑出 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) --- client/tests/test_constants.js | 178 +++++++++++++++++++++++++++++++++ 1 file changed, 178 insertions(+) create mode 100644 client/tests/test_constants.js diff --git a/client/tests/test_constants.js b/client/tests/test_constants.js new file mode 100644 index 0000000..9009a8e --- /dev/null +++ b/client/tests/test_constants.js @@ -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);