feedback5: the boss-field browser test checks 限伤 on a fixed heavy-hitter copy of its four fields (withHeavyHitters: the first player's first five operators become elite 焰影苇草, 洛洛, 深靛, 刺玫, 远牙 with a 维式重锤 on the same tiles, bonds recomputed at 999 — 精准 3, 维多利亚 3, 奥术 2), compared Chrome vs Node like the captured and 催泪瓦斯 copies; since CB3's 受到攻击时 change (5877800a) re-drafted seed 22 the bots' best leader hit is 141845, not ≥ 300000 (the old 663300 / 733317 came from a 维多利亚 + 精准 + 奥术 board the bots no longer draft); the cancel path is unchanged (37e7f002's four captures cancel the same 86 hits with the same digests on 2e83a6b's code); golden: unchanged (test only)

This commit is contained in:
sganggs committed 2026-10-07 06:28:50 +08:00
1 parent 2e83a6b977
commit 2802a368c3
1 file changed
+45 -6
+45 -6
View File
@@ -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 奥术');