0.2.5-dev: community round 30 and follow-ups
- Stock: fixed trait gifts (缪尔赛思 / 变形者集群 变形同构体, 耶拉 不融冰, 蜜蜡 / 卡涅利安 迅捷作战粮, 诗怀雅 盟约之币) still occupy stock but are granted past the cap; an operator card already shown in the shop or a reward can be taken after the pool runs out and overdraws (GitHub #498, the owner's decision); item buys and item rewards keep the stock check. 洛洛 / 焰尾 sub-pools use the measured weights (PR #497 by @2321Robin; the occupancy idea of PR #496 by @2321Robin re-implemented differently). - Boss pairs are drawn at random once per match and fixed to battle start; the Hidden Core reuses them; a leaver does not split a full pair (the owner's decision). - 阿戈尔 in 联防 no longer re-applies the opening devour HP loss or a false revive; Touch's team medic stays for the match after its holder leaves (owner-approved, official lifetime unverified). - Auto-skill ranges: 司霆惊蛰 S2, 斩业星熊 S3 against air, 予愿安洁莉娜 S3; 自在's post-rebirth invulnerability is also untargetable (#510); Horn / Clementia / catapult / 忍冬 first hit (PR #502 by @Xinglan233); 不融冰 / tear gas trigger on real damage. - Bond layer-up sound in round start / draft / prep / battle (#482): one channel, a new cue replaces the old, no extra cooldown; a prep update that adds layers and activates a bond plays one cue. - Client: multi-swing fix (#506), refresh single-flight (PR #507 by @robll-v1), LP tower keeps the settled value with a −N tick (PR #504 by @robll-v1), export with only 潜能 / 练度 changes (PR #503 by @Xinglan233), spectator rosters list DIY members (PR #505 by @Xinglan233), banned members beside bond tooltips (PR #494 by @YuriRestia), recovery action above the away overlay (PR #511 by @RainoutOvO), the /dev/sim-check.html consistency page (PR #492 by @Cloudnyco), Korean (PR #490 by @D-Roote) and English Victoria wording (PR #499 by @YuriRestia). - golden: 45 of 287 scenarios move against 0.2.4 (roster 17, bonds 3, fields 6, matches 17, standins 2), each attributed in docs/history/0.2.5.md §30.9 / §30.16.
This commit is contained in:
1 parent
bc50cac01f
commit
221e6b8dd2
134 files changed
+4590
-1201
No files matched your search
@@ -0,0 +1,83 @@
|
||||
// Preparation counts and converted battle memberships through the actual browser simulation loader.
|
||||
import { test, before, after, describe } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { existsSync } from 'node:fs';
|
||||
import { getData } from '../../server/data.js';
|
||||
import { GameData } from '../../server/match/gamedata.js';
|
||||
import { computeBonds } from '../../server/match/bondsMeta.js';
|
||||
import { getDefaultSource } from '../../server/sim/simdata.js';
|
||||
import { buildBattleSpec } from '../../server/sim/spec.js';
|
||||
import { Grid } from '../../server/sim/grid.js';
|
||||
import { GEO } from '../../shared/constants.js';
|
||||
|
||||
const CHROME = process.env.CHROME_PATH || '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
|
||||
const enabled = process.env.SIM_E2E === '1' && existsSync(CHROME);
|
||||
const raw = getData({ log: { info() {}, warn() {}, error() {} } });
|
||||
const gd = new GameData(raw, 'mode_multi_hard');
|
||||
const grid = new Grid(getDefaultSource().getStage('act1autochess_m01'), GEO.NORMAL_RECT);
|
||||
function fixture(ids, items) {
|
||||
const taken = new Set(), board = new Map(), units = [];
|
||||
ids.forEach((id, i) => {
|
||||
const c = raw.chess[id];
|
||||
let tile;
|
||||
for (let r = 9; r <= 12 && !tile; r++) for (let col = 2; col <= 10 && !tile; col++) {
|
||||
if (!taken.has(`${r},${col}`) && grid.canStand(r, col, { ranged: c.position === 'RANGED' })) tile = [r, col];
|
||||
}
|
||||
assert.ok(tile, id); taken.add(tile.join(','));
|
||||
const its = i === ids.length - 1 ? items : [];
|
||||
board.set(tile.join(','), { uid: i + 1, kind: 'chess', id, items: its.map(id => ({ id })) });
|
||||
units.push({ uid: i + 1, chessId: id, row: tile[0], col: tile[1], items: its });
|
||||
});
|
||||
const bonds = computeBonds(gd, { board, hand: [], layers: {} });
|
||||
return { spec: buildBattleSpec({ battleId: 'morph', fieldId: 'n:p1', kind: 'normal', modeId: 'mode_multi_hard',
|
||||
seed: 29, stageId: 'act1autochess_m01', timeLimit: 10, flags: { dpInit: 999 },
|
||||
players: [{ playerId: 'p1', bonds, units }],
|
||||
}), bonds, uid: ids.length };
|
||||
}
|
||||
|
||||
describe('converted bonds in the actual browser battle', { skip: !enabled }, () => {
|
||||
let srv, browser, page;
|
||||
before(async () => {
|
||||
const { startServer } = await import('../../server/index.js');
|
||||
const P = (await import('puppeteer-core')).default;
|
||||
srv = await startServer({ port: 0, host: '127.0.0.1', quiet: true });
|
||||
browser = await P.launch({ executablePath: CHROME, headless: true, args: ['--no-sandbox'] });
|
||||
page = await browser.newPage(); await page.goto(`http://127.0.0.1:${srv.port}/sim/spec.js`);
|
||||
});
|
||||
after(async () => { await browser?.close(); await srv?.close(); });
|
||||
async function inspect(f) {
|
||||
return page.evaluate(async ({ spec, uid }) => {
|
||||
const { loadBrowserSim } = await import('/js/battle/runner.js');
|
||||
const { spec: S, ds } = await loadBrowserSim();
|
||||
const { unitBonds, isMember } = await import('/sim/content/support/index.js');
|
||||
const b = S.createBattleFromSpec(spec, ds, { quiet: true, recordEvents: false });
|
||||
for (let i = 0; i < 30; i++) b.step();
|
||||
const u = b.allyUnits.find(u => u.kind === 'op' && u.uid === uid);
|
||||
return { own: unitBonds(u), members: Object.keys(spec.players[0].bonds).filter(id => isMember(b, u, id)),
|
||||
yan: b.getPlayer('p1').bonds.yanShip, atk: u.s.atk, baseAtk: u.base.atk,
|
||||
yanyou: b.allyUnits.filter(u => u.alive && u.mem.bondYan).length, errors: b.errors };
|
||||
}, f);
|
||||
}
|
||||
for (const mu of ['chess_char_6_11_a', 'chess_char_6_11_b']) test(`7 Yan plus converted ${mu}: nine Yan really summons two 炎佑`, async () => {
|
||||
const seven = Object.values(raw.chess).filter(c => c.visible && !c.isGolden && c.bonds.includes('yanShip')).slice(0, 7).map(c => c.chessId);
|
||||
const f = fixture([...seven, mu], ['chess_item_6_09_e_a', 'chess_item_3_04_e_a']);
|
||||
assert.equal(f.bonds.yanShip.count, 9);
|
||||
const out = await inspect(f);
|
||||
assert.equal(out.yan.count, 9); assert.equal(out.yan.tier, 3);
|
||||
assert.ok(out.own.includes('yanShip')); assert.ok(out.atk > out.baseAtk);
|
||||
assert.equal(out.yanyou, 2); assert.deepEqual(out.errors, []);
|
||||
});
|
||||
test('all 36 normal/advanced conversion item pairings retain battle membership', async () => {
|
||||
const grants = Object.values(raw.items).filter(r => r.giveBondId);
|
||||
assert.equal(grants.length, 36);
|
||||
for (const it of grants) {
|
||||
const id = it.giveBondId;
|
||||
const two = Object.values(raw.chess).filter(c => c.visible && !c.isGolden && c.bonds.includes(id) && !c.bonds.includes('maniShip')).slice(0, 2).map(c => c.chessId);
|
||||
const outsider = Object.values(raw.chess).find(c => c.visible && !c.isGolden && c.tier <= 2 && !c.bonds.includes(id) && !c.bonds.includes('maniShip'));
|
||||
const f = fixture([...two, outsider.chessId], [it.id, it.isGolden ? 'chess_item_6_09_e_b' : 'chess_item_6_09_e_a']);
|
||||
const out = await inspect(f);
|
||||
assert.ok(out.own.includes(id) && out.members.includes(id), `${it.id}: ${JSON.stringify(out)}`);
|
||||
assert.deepEqual(out.errors, []);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,101 @@
|
||||
// The cross-engine check (tools/sim-check.mjs, public/dev/simcheck — docs/SIM.md「11. Tools」): the hashes are pinned
|
||||
// (references made before still compare; the same values as the copy of core.js in 争锋频道 / Duel Channel), a change in
|
||||
// one tick is found in its block and named by the dump comparison, the deep walk sees another realm's Map / Set, the
|
||||
// check page loads the data files the client loads, and the corpus covers the golden battle families, traces a battle
|
||||
// the same whatever ran before it, and ends it with the result digest a plain run gives.
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import vm from 'node:vm';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { hex, hashNumbers, hashValue, deepHash, trace, tracer, compare, diffDumps, selectScenarios, textFingerprint } from '../../public/dev/simcheck/core.js';
|
||||
import { SIM_DATA_FILES, scenariosOf } from '../../public/dev/simcheck/stronghold.js';
|
||||
import { corpusSpecs, nodeAdapter, FAMILIES, parseOptions, bundleIdentity } from '../../tools/sim-check.mjs';
|
||||
import { createBattleFromSpec, resultDigest } from '../../server/sim/spec.js';
|
||||
import { DataSource, getDefaultSource } from '../../server/sim/simdata.js';
|
||||
|
||||
test('simcheck dumps preserve special IEEE values through JSON and still locate a one-bit difference', () => {
|
||||
const adapter = {
|
||||
start: () => ({ t: 0 }), step: r => ++r.t <= 1,
|
||||
state: () => [-0, Infinity, -Infinity, NaN, 1 + Number.EPSILON], result: () => 'same',
|
||||
};
|
||||
const a = trace(adapter, { id: 'bits' }, { dump: [1, 1] }).dumps;
|
||||
const b = JSON.parse(JSON.stringify(a));
|
||||
assert.deepEqual(diffDumps(a, b), []);
|
||||
assert.deepEqual(b[1].state.slice(0, 4), ['-0', 'Infinity', '-Infinity', 'NaN']);
|
||||
b[1].state[0] = 0; b[1].state[4] = 1;
|
||||
const diff = diffDumps(a, b);
|
||||
assert.deepEqual(diff.map(x => x.index), [0, 4]);
|
||||
assert.notEqual(diff[0].refBits, diff[0].gotBits);
|
||||
});
|
||||
|
||||
test('simcheck rejects incomplete runs and invalid sampling/dump parameters', () => {
|
||||
const adapter = { maxTicks: 2, start: () => ({}), step: () => true, state: () => [1], result: () => 'not finished' };
|
||||
assert.throws(() => trace(adapter, { id: 'loop' }), /maxTicks/);
|
||||
assert.throws(() => tracer(adapter, { id: 'bad' }, { every: 0 }), /interval/);
|
||||
assert.throws(() => tracer(adapter, { id: 'bad' }, { dump: [2, 1] }), /window/);
|
||||
assert.throws(() => tracer(adapter, { id: 'bad' }, { dump: [1, Infinity] }), /window/);
|
||||
});
|
||||
|
||||
test('simcheck selection and CLI reject empty, mismatched and misspelled requests', () => {
|
||||
const scenarios = [{ id: 'a', family: 'one' }, { id: 'b', family: 'two' }];
|
||||
const subsets = [{ id: 'one', pick: s => s.family === 'one' }];
|
||||
assert.equal(selectScenarios(scenarios, subsets, 'one').list.length, 1);
|
||||
assert.throws(() => selectScenarios(scenarios, subsets, 'typo'), /unknown subset/);
|
||||
assert.throws(() => selectScenarios(scenarios, subsets, 'one', 'typo'), /unknown scenario/);
|
||||
assert.throws(() => selectScenarios(scenarios, subsets, 'one', 'b'), /no scenarios/);
|
||||
for (const args of [['--engines'], ['--engines', 'oops'], ['--subset', 'all'], ['--engines', 'chrome', '--subset', 'typo'], ['--oops', 'x']]) assert.throws(() => parseOptions(args));
|
||||
});
|
||||
|
||||
test('simcheck bundle identity changes with corpus or dirty runtime/data content', () => {
|
||||
const specs = [{ id: 'a', spec: { seed: 1 } }], files = [{ url: '/data/chess.json', hash: textFingerprint('old') }];
|
||||
const a = bundleIdentity(specs, files);
|
||||
assert.notEqual(a, bundleIdentity([{ id: 'a', spec: { seed: 2 } }], files));
|
||||
assert.notEqual(a, bundleIdentity(specs, [{ url: files[0].url, hash: textFingerprint('new') }]));
|
||||
assert.equal(a, bundleIdentity(JSON.parse(JSON.stringify(specs)), JSON.parse(JSON.stringify(files))));
|
||||
});
|
||||
|
||||
test('simcheck hashes: pinned values, NaN as one value, −0 apart from 0', () => {
|
||||
assert.equal(hex(hashNumbers([0, -0, 1, 0.1, -2.5e-300, 1e300, NaN, Infinity])), '8b3e38ba');
|
||||
assert.equal(hex(hashValue({ a: [1, 'x', null, true], b: { c: 0.5 } })), 'a024c2ef');
|
||||
assert.equal(hex(hashNumbers([NaN])), hex(hashNumbers([0 / 0])));
|
||||
assert.notEqual(hex(hashNumbers([0])), hex(hashNumbers([-0])));
|
||||
assert.equal(hex(deepHash([{ s: new Set([1, 2]), m: new Map([['k', 3]]), arr: [4.5] }])), '92f56136');
|
||||
const other = vm.runInNewContext('({ s: new Set([1, 2]), m: new Map([["k", 3]]), arr: [4.5] })');
|
||||
assert.equal(hex(deepHash([other])), '92f56136', 'another realm\'s Map / Set');
|
||||
});
|
||||
|
||||
test('simcheck compare: a one-ulp change at tick 45 is found in its block and named by the dumps', () => {
|
||||
const toy = (bump) => ({
|
||||
start: () => ({ n: 0, x: 0.1 }),
|
||||
step: (r) => { if (r.n >= 100) return false; r.n++; r.x = r.x * 1.01 + 0.1; if (bump && r.n === 45) r.x += r.x * Number.EPSILON; return true; },
|
||||
state: (r) => [r.n, r.x], labels: () => ['n', 'x'], view: (r) => ({ x: Math.round(r.x) }), deep: (r) => hashNumbers([r.x]), result: (r) => String(Math.round(r.x)),
|
||||
});
|
||||
const ref = trace(toy(false), { id: 't' });
|
||||
assert.deepEqual(compare(ref, trace(toy(false), { id: 't' })), { ok: true, at: null, result: false, ticks: null });
|
||||
assert.deepEqual(compare(ref, trace(toy(true), { id: 't' })).at, { level: 'state', block: 1, from: 31, to: 60 });
|
||||
const a = tracer(toy(false), { id: 't' }, { dump: [31, 60] }).finish(), b = tracer(toy(true), { id: 't' }, { dump: [31, 60] }).finish();
|
||||
const d = diffDumps(a.dumps, b.dumps);
|
||||
assert.equal(d[0].tick, 45);
|
||||
assert.equal(d[0].label, 'x');
|
||||
});
|
||||
|
||||
test('simcheck page: the data files are the browser runner\'s', () => {
|
||||
const src = readFileSync(new URL('../../public/js/battle/runner.js', import.meta.url), 'utf8');
|
||||
const m = /export const SIM_DATA_FILES = Object\.freeze\((\[[^\]]*\])\)/.exec(src);
|
||||
assert.ok(m, 'runner.js SIM_DATA_FILES');
|
||||
assert.deepEqual([...SIM_DATA_FILES], JSON.parse(m[1].replace(/'/g, '"')));
|
||||
});
|
||||
|
||||
test('simcheck corpus: the golden battle families, a battle traced the same whatever ran before, the plain run\'s result', () => {
|
||||
const doc = { specs: corpusSpecs() }, sc = scenariosOf(doc);
|
||||
assert.deepEqual([...new Set(sc.map((s) => s.family))], [...FAMILIES]);
|
||||
assert.equal(new Set(sc.map((s) => s.id)).size, sc.length);
|
||||
assert.ok(sc.some((s) => s.fast) && sc.some((s) => !s.fast));
|
||||
const pick = sc.filter((s) => s.family === 'bonds')[0], other = sc.filter((s) => s.family === 'roster')[0];
|
||||
const alone = trace(nodeAdapter(doc), pick), ad = nodeAdapter(doc);
|
||||
trace(ad, other);
|
||||
assert.deepEqual(trace(ad, pick), alone);
|
||||
const spec = doc.specs.find((s) => s.id === pick.id).spec;
|
||||
const plain = resultDigest(createBattleFromSpec(spec, new DataSource(getDefaultSource().raw, null), { quiet: true }).runToEnd(200000)).hash;
|
||||
assert.equal(alone.result, plain);
|
||||
});
|
||||
Reference in new issue
Block a user