Files
ark-stronghold-protocol/public/dev/simcheck/node.mjs
T
sganggs 221e6b8dd2 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.
2026-10-12 14:07:08 +08:00

105 lines
6.7 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.
// simcheck's Node side: the reference (every scenario traced in Node, core.js), headless runs of the check page in
// several engines, and the field-by-field comparison of a dump a browser downloaded with Node's run of the same ticks.
// Engines: Playwright's chromium / firefox / webkit when the `playwright` package is installed (its browsers fetched
// with `npx playwright install firefox webkit`), else a local Chrome through puppeteer-core ($CHROME_PATH or the usual
// install paths).
//
// Written for Stronghold Protocol (GPL-3.0-or-later) and Duel Channel (AGPL-3.0-or-later) by the same author, and kept
// the same in both.
import { existsSync, readFileSync } from 'node:fs';
import { tracer, diffDumps, VERSION } from './core.js';
/**
* Trace every scenario in Node: the reference a page compares with.
* @param {object} adapter @param {{ id: string }[]} scenarios
* @param {{ every?: number, project: string, made?: object, onProgress?: (i: number, n: number, id: string) => void }} o
*/
export function buildReference(adapter, scenarios, { every = 30, deepEvery = every * 5, project, made = {}, onProgress } = {}) {
const out = {};
scenarios.forEach((sc, i) => {
if (onProgress) onProgress(i, scenarios.length, sc.id);
const { ticks, blocks, views, deeps, result } = tracer(adapter, sc, { every, deepEvery }).finish();
out[sc.id] = { ticks, blocks, views, deeps, result };
});
return { simcheck: VERSION, project, every, deepEvery, made: { engine: `Node ${process.version}`, date: new Date().toISOString().slice(0, 10), ...made }, scenarios: out };
}
const CHROMES = [process.env.CHROME_PATH, 'C:/Program Files/Google/Chrome/Application/chrome.exe', 'C:/Program Files (x86)/Google/Chrome/Application/chrome.exe',
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', '/usr/bin/google-chrome', '/usr/bin/google-chrome-stable', '/usr/bin/chromium', '/usr/bin/chromium-browser'];
async function tryImport(name) { try { return await import(name); } catch { return null; } }
/**
* Open the check page (url, with ?auto=1 and the subset) in each engine and wait for its results.
* @param {string} url @param {string[]} engines 'chromium' | 'firefox' | 'webkit' | 'chrome' (a local Chrome)
* @returns {Promise<Record<string, { version?: string, status: string, summary?: object, results?: object[], report?: string, error?: string }>>}
*/
export async function runEngines(url, engines, { timeoutMs = 30 * 60000, log = () => {} } = {}) {
const pw = await tryImport('playwright'), pp = pw ? null : await tryImport('puppeteer-core');
const out = {};
for (const name of engines) {
let browser = null;
try {
if (pw && pw[name === 'chrome' ? 'chromium' : name]) {
const type = pw[name === 'chrome' ? 'chromium' : name], exe = CHROMES.find((p) => p && existsSync(p));
// chromium / chrome: Playwright's own build when it is installed, else the local Chrome
try { browser = await type.launch(name === 'chrome' && exe ? { executablePath: exe } : {}); } catch (e) {
if (type !== pw.chromium || !exe) throw e;
browser = await type.launch({ executablePath: exe });
}
} else if (pp && (name === 'chrome' || name === 'chromium')) {
const exe = CHROMES.find((p) => p && existsSync(p));
if (!exe) throw new Error('no Chrome found: set CHROME_PATH');
browser = await (pp.default || pp).launch({ executablePath: exe, headless: true });
} else throw new Error(pw ? `playwright has no ${name}` : `${name} needs the playwright package (npm i -D playwright && npx playwright install ${name})`);
const page = await browser.newPage();
const errors = [];
page.on('pageerror', (e) => errors.push(String(e.message || e)));
await page.goto(url, { timeout: 120000 });
const t0 = Date.now();
let last = -1;
for (;;) {
const st = await page.evaluate(() => { const s = globalThis.__simcheck; return s ? { status: s.status, n: s.results.length } : null; });
if (st && ['done', 'stopped', 'error'].includes(st.status)) break;
if (errors.length) throw new Error(errors.join('; '));
if (st && st.n !== last && st.n % 25 === 0) { log(` ${name}: ${st.n}`); last = st.n; }
if (Date.now() - t0 > timeoutMs) throw new Error('timed out');
await new Promise((r) => setTimeout(r, 1000));
}
const res = await page.evaluate(() => { const s = globalThis.__simcheck; return { status: s.status, expected: s.expected, completed: s.completed, error: s.error, summary: s.summary, results: s.results, report: s.report }; });
out[name] = { version: browser.version ? await browser.version() : '', ...res, errors };
} catch (e) {
out[name] = { status: 'error', error: String(e && e.message || e) };
} finally { if (browser) await browser.close().catch(() => {}); }
}
return out;
}
/**
* A dump a browser downloaded (the page's 下载这一段的状态) against Node's run of the same ticks: the differing entries,
* first ones first, as printable lines.
* @param {string} file @param {object} adapter @param {{ id: string }[]} scenarios
*/
export function diffDumpFile(file, adapter, scenarios, { every = 30, deepEvery = every * 5, limit = 20, identity = null } = {}) {
const d = JSON.parse(readFileSync(file, 'utf8'));
if (d.simcheck !== VERSION || !d.dumps || typeof d.dumps !== 'object' || !Array.isArray(d.window)) throw new Error('invalid dump format');
if (identity && d.identity !== identity) throw new Error('dump and reference inputs differ');
const sc = scenarios.find((s) => s.id === d.id);
if (!sc) throw new Error(`no scenario ${d.id} here`);
const ref = tracer(adapter, sc, { every, deepEvery, dump: d.window }).finish();
const diffs = diffDumps(ref.dumps, d.dumps, limit);
const lines = [`${d.id}, ticks ${d.window[0]}–${d.window[1]}: Node ${process.version} vs ${d.engine}`];
if (!diffs.length) lines.push(' the same in every entry of these ticks (the difference is in what the state vector does not hold: view / deep)');
for (const x of diffs) lines.push(x.index < 0 ? ` tick ${x.tick}: ${x.label}` : ` tick ${x.tick} ${x.label}: ${x.ref} (${x.refBits}) vs ${x.got} (${x.gotBits})`);
return lines;
}
/** One line per engine for a console. */
export function formatEngines(res) {
return Object.entries(res).map(([name, r]) => {
if (r.status === 'error') return `${name}: error — ${r.error}`;
const s = r.summary || {};
const bad = (r.results || []).filter((x) => !x.c.ok).map((x) => x.id);
return `${name} ${r.version || ''}: ${s.ok}/${s.n} the same${bad.length ? ` — different: ${bad.slice(0, 12).join(', ')}${bad.length > 12 ? ' …' : ''}` : ''}${r.errors && r.errors.length ? ` (page errors: ${r.errors.length})` : ''}`;
});
}