From 2802a368c3952e4dae79773827d305a38584f5c6 Mon Sep 17 00:00:00 2001 From: sganggs Date: Wed, 7 Oct 2026 06:28:50 +0800 Subject: [PATCH] =?UTF-8?q?feedback5:=20the=20boss-field=20browser=20test?= =?UTF-8?q?=20checks=20=E9=99=90=E4=BC=A4=20on=20a=20fixed=20heavy-hitter?= =?UTF-8?q?=20copy=20of=20its=20four=20fields=20(withHeavyHitters:=20the?= =?UTF-8?q?=20first=20player's=20first=20five=20operators=20become=20elite?= =?UTF-8?q?=20=E7=84=B0=E5=BD=B1=E8=8B=87=E8=8D=89,=20=E6=B4=9B=E6=B4=9B,?= =?UTF-8?q?=20=E6=B7=B1=E9=9D=9B,=20=E5=88=BA=E7=8E=AB,=20=E8=BF=9C?= =?UTF-8?q?=E7=89=99=20with=20a=20=E7=BB=B4=E5=BC=8F=E9=87=8D=E9=94=A4=20o?= =?UTF-8?q?n=20the=20same=20tiles,=20bonds=20recomputed=20at=20999=20?= =?UTF-8?q?=E2=80=94=20=E7=B2=BE=E5=87=86=203,=20=E7=BB=B4=E5=A4=9A?= =?UTF-8?q?=E5=88=A9=E4=BA=9A=203,=20=E5=A5=A5=E6=9C=AF=202),=20compared?= =?UTF-8?q?=20Chrome=20vs=20Node=20like=20the=20captured=20and=20=E5=82=AC?= =?UTF-8?q?=E6=B3=AA=E7=93=A6=E6=96=AF=20copies;=20since=20CB3's=20?= =?UTF-8?q?=E5=8F=97=E5=88=B0=E6=94=BB=E5=87=BB=E6=97=B6=20change=20(58778?= =?UTF-8?q?00a)=20re-drafted=20seed=2022=20the=20bots'=20best=20leader=20h?= =?UTF-8?q?it=20is=20141845,=20not=20=E2=89=A5=20300000=20(the=20old=20663?= =?UTF-8?q?300=20/=20733317=20came=20from=20a=20=E7=BB=B4=E5=A4=9A?= =?UTF-8?q?=E5=88=A9=E4=BA=9A=20+=20=E7=B2=BE=E5=87=86=20+=20=E5=A5=A5?= =?UTF-8?q?=E6=9C=AF=20board=20the=20bots=20no=20longer=20draft);=20the=20?= =?UTF-8?q?cancel=20path=20is=20unchanged=20(37e7f002's=20four=20captures?= =?UTF-8?q?=20cancel=20the=20same=2086=20hits=20with=20the=20same=20digest?= =?UTF-8?q?s=20on=202e83a6b's=20code);=20golden:=20unchanged=20(test=20onl?= =?UTF-8?q?y)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/sim/bossfield.browser.test.js | 51 ++++++++++++++++++++++++++---- 1 file changed, 45 insertions(+), 6 deletions(-) diff --git a/test/sim/bossfield.browser.test.js b/test/sim/bossfield.browser.test.js index fef482c2..545a2f5b 100644 --- a/test/sim/bossfield.browser.test.js +++ b/test/sim/bossfield.browser.test.js @@ -2,7 +2,7 @@ // clients simulate the boss fields; the server re-simulates them for SP_VERIFY and takeovers). A real bot co-op match // (终极, seed 22 since 0.1.1 — the bots and the rules of 0.1.1 play seed 12 to boards without a 麻痹 or a 剑 / 锤 // transfer on every field, and seed 14 since the 22-match R11 drafts to boards without a 麻痹; seed 22's pair fields -// carry 奥术, 催泪瓦斯 and 限伤-sized hits) is played to the Hidden Core +// carry 奥术 and 剑 / 锤 transfers) is played to the Hidden Core // with every active bond at the official 999 layers (DESIGN §20.12); its four captured boss specs (R14 假想敌:胄 boss_1, R15 隐秘核心 boss_8) run to // the end in Node and in headless Chrome through the client's own loader (public/js/battle/runner.js loadBrowserSim) // with the field's LocalBossPool, and must give the same result digest, the same pool and the same fx per kind. @@ -11,6 +11,10 @@ // 瘫痪 (fx 'palsy': a 麻痹 stack interrupts an enemy attack) is checked on the same four specs with a golden 催泪瓦斯 in // every operator's second slot (withTearGas), compared Chrome vs Node like the others: the bots' only 麻痹 had been one // 5 % 催泪瓦斯 on one operator — a single hit at 0.1.4, none since 0.2.0's enemy damage frame (WE1) re-timed every fight. +// 限伤 likewise, on a third copy of the four whose first player fields a fixed board of 999-layer heavy hitters +// (withHeavyHitters): the bots' own hits of ≥ 300000 came from whatever 维多利亚 / 精准 / 奥术 mix they drafted — none +// since 0.2.0's CB3 (the 受到攻击时 counters answer every damage instance) re-drafted seed 22, whose best hit is now +// about half the limit; the cancel path itself is unchanged (the earlier captures still cancel the same hits). // // Opt-in (starts Chrome): SIM_E2E=1 node --test test/sim/bossfield.browser.test.js (or RENDER_E2E=1) // Run browser test files one at a time. Chrome path: $CHROME_PATH or the macOS default. @@ -23,6 +27,9 @@ import { fileURLToPath } from 'node:url'; import { PHASE, BOND_LAYER_CAP } from '../../shared/constants.js'; import { createBattleFromSpec, resultDigest } from '../../server/sim/spec.js'; import { DataSource } from '../../server/sim/simdata.js'; +import { GameData } from '../../server/match/gamedata.js'; +import { computeBonds, bondSnapshot } from '../../server/match/bondsMeta.js'; +import { tileKey } from '../../server/match/board.js'; import { makeMatch, DATA } from '../match/harness.js'; const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..'); @@ -77,6 +84,35 @@ function withTearGas(spec) { return s; } +/** + * A copy of `spec` whose first player's first five operators become a fixed 限伤 board on the same tiles, each with a + * 维式重锤: elite 焰影苇草 (维多利亚 + 精准), 洛洛 (维多利亚 + 奥术), 深靛 (奥术 + 精准), 刺玫 (维多利亚) and 远牙 (精准) — all + * ranged, so any tile takes them; the tokens of the operators replaced go with them. That player's bonds are recomputed + * for the new board as the match computes them (server/match/bondsMeta.js computeBonds), every active one at 999 + * layers: 精准 (3: every ranged operator) ATK ×13, 维多利亚 ×9.2 damage with equipment and 奥术 ×11 法术伤害 taken on the + * leader put the arts hits of 焰影苇草, 洛洛 and 刺玫 at up to 1.3 M — hundreds per field past 限伤's 300000, whatever + * the bots drafted (every field of seeds 1–40 at 0.2.0). + */ +const HEAVY_HITTERS = ['chess_char_6_08_b', 'chess_char_2_10_b', 'chess_char_1_17_b', 'chess_char_1_06_b', 'chess_char_4_20_b']; +const HAMMER = 'chess_item_1_01_e_a'; // 维式重锤: 攻击力+15% +function withHeavyHitters(spec) { + const s = JSON.parse(JSON.stringify(spec)); + const p = s.players[0]; + const replaced = new Set(); + p.units = p.units.map((u) => { + if (u.kind !== 'chess' || replaced.size >= HEAVY_HITTERS.length) return u; + const chessId = HEAVY_HITTERS[replaced.size]; + replaced.add(u.uid); + return { uid: u.uid, kind: 'chess', chessId, row: u.row, col: u.col, dir: u.dir, items: [HAMMER] }; + }).filter((u) => !(u.kind === 'token' && replaced.has(u.ownerUid))); + const gd = new GameData(DATA, s.modeId); + const board = new Map(p.units.filter((u) => u.kind === 'chess').map((u) => [tileKey(u.row, u.col), { kind: 'chess', id: u.chessId, items: (u.items || []).map((id) => ({ id })) }])); + const layers = Object.fromEntries(gd.bondIds.map((id) => [id, BOND_LAYER_CAP])); + p.bonds = bondSnapshot(computeBonds(gd, { board, hand: [], layers, bondCountBonus: {} })); + for (const b of Object.values(p.bonds)) if (!b.active) b.layers = 0; + return s; +} + /** * Runs a spec to the end (the same code in Node and in the page: `S` = spec module, `ds` = data source) and returns * its digest, the local pool, the fx count per kind and the leader-side checks (奥术 instances, 无来源 HP losses). @@ -118,8 +154,9 @@ describe('Final Assault / Hidden Core fields in the browser sim', { skip }, () = test('real boss specs at 999 layers: Chrome and Node give the same digest, pool and fx; 限伤, drones, 瘫痪 and 奥术 all occur', { timeout: 180000 }, async () => { const specs = captureBossSpecs(); assert.deepEqual(specs.map((s) => `${s.kind}:${s.bossId}`).sort(), ['boss:boss_1', 'boss:boss_1', 'hidden:boss_8', 'hidden:boss_8']); - // the four as captured, then the same four with a 麻痹 source on every field (withTearGas) - const runs = [...specs, ...specs.map(withTearGas)]; + // the four as captured, the same four with a 麻痹 source on every field (withTearGas), and with 限伤-sized hits + // on every field (withHeavyHitters) + const runs = [...specs, ...specs.map(withTearGas), ...specs.map(withHeavyHitters)]; const ds = new DataSource(DATA, null); const node = runs.map((spec) => runField({ createBattleFromSpec, resultDigest }, ds, spec, MAX_SECONDS)); @@ -139,8 +176,9 @@ describe('Final Assault / Hidden Core fields in the browser sim', { skip }, () = await page.close(); assert.deepEqual(problems, []); + const copies = ['', ' +催泪瓦斯', ' +heavy hitters']; runs.forEach((spec, i) => { - const at = `${spec.kind} ${spec.fieldId} ${spec.bossId}${i >= specs.length ? ' +催泪瓦斯' : ''}`; + const at = `${spec.kind} ${spec.fieldId} ${spec.bossId}${copies[Math.floor(i / specs.length)]}`; assert.equal(node[i].reason, 'cleared', `${at}: the 999-layer pair clears the field`); assert.equal(chrome[i].json, node[i].json, `${at}: browser and Node results are identical`); assert.equal(chrome[i].hash, node[i].hash, at); @@ -150,9 +188,10 @@ describe('Final Assault / Hidden Core fields in the browser sim', { skip }, () = assert.ok(node[i].maxArcane <= 1, `${at}: one 奥术 instance on the leader at a time (${node[i].maxArcane})`); }); const real = node.slice(0, specs.length); - const gassed = node.slice(specs.length); + const gassed = node.slice(specs.length, 2 * specs.length); + const heavy = node.slice(2 * specs.length); const sum = (rows, k) => rows.reduce((a, r) => a + (r.fx[k] || 0), 0); - assert.ok(sum(real, 'hitCap') >= 1, `a 999-layer hit reached 300000 and was cancelled (限伤: ${sum(real, 'hitCap')})`); + assert.ok(sum(heavy, 'hitCap') >= 1, `a 999-layer hit reached 300000 and was cancelled (限伤: ${heavy.map((r) => r.fx.hitCap || 0)} with the heavy hitters, ${sum(real, 'hitCap')} on the bots' own boards)`); assert.ok(sum(gassed, 'palsy') >= 1, `瘫痪 occurred with the 催泪瓦斯 (${gassed.map((r) => r.fx.palsy || 0)})`); assert.ok(real.every((r) => r.hpLoss >= 1), `every field cost the leader 无来源 HP (drone links / 剑 · 锤 transfers: ${real.map((r) => r.hpLoss)})`); assert.ok(real.some((r) => r.maxArcane === 1), 'the leader carried 奥术');