Files
sganggs 01a44cd288 卫戍协议:盟约 web remake — initial import
Online co-op browser remake of Arknights' Stronghold Protocol: Alliance
(client-simulated combat, official waves/bonds/items/strategies, 3D board,
facing wheel, operator loadout, official emotes, deployment scripts).
2026-09-29 16:21:31 +08:00

66 lines
3.4 KiB
JavaScript

// test/match/fullmatchRun.js — shared runner of the full-match suites (fullmatch*.test.js): a whole match with the REAL
// simulation in virtual time, invariants at every phase change, JSON-safe frames, zero engine errors. Sim / content
// errors ('[sim] …', owned by the sim and content teams) are collected in `simIssues` and reported by the suites.
// Combat is client-side by default (DESIGN §14, the production mode): every human seat is a SimClient that simulates
// its specs and reports b.progress / b.result; honest clients must never be rejected or taken over, and no combat
// frame is streamed. `clientCombat: false` runs the server-run fallback.
import assert from 'node:assert/strict';
import { PHASE } from '../../shared/constants.js';
import { makeMatch, checkInvariants } from './harness.js';
export const SEEDS = Number(process.env.MATCH_SEEDS || 20);
export const simIssues = new Set();
export function reportSimIssues() {
if (simIssues.size) console.log(`sim/content errors seen during full matches (not match-engine errors):\n${[...simIssues].slice(0, 20).map((x) => ' ' + x).join('\n')}`);
}
export function runFull({ mode, difficulty, humans, bots, seed, boostLp = null, clientCombat = true, pace = 'instant' }) {
const h = makeMatch({ mode, difficulty, humans, bots, seed, captureFrames: false, checkFrames: true, clientCombat, pace });
h.autoHumans();
const m = h.m;
m.start();
let last = '';
let boosted = false;
let phases = 0;
h.run(() => {
const key = `${m.phase}:${m.round}`;
if (key !== last) {
last = key;
phases++;
checkInvariants(m);
if (boostLp && !boosted && m.phase === PHASE.PREP && m.round === 1) {
boosted = true;
for (const ps of m.players.values()) ps.lp = boostLp;
}
}
return h.ended != null;
}, { maxSteps: 5e6 });
assert.ok(h.ended, `${mode}/${difficulty}/${seed}: match did not end (${m.phase} R${m.round})`);
assert.equal(h.endedCount, 1);
assert.equal(m.phase, PHASE.RESULT);
assert.equal(m.errorCount, 0, `${mode}/${difficulty}/${seed}: match errors ${JSON.stringify(m.errors.slice(0, 2))}`);
assert.deepEqual(h.sched.errors, []);
// engine errors must be zero; errors raised inside the simulation / its content ('[sim] …', owned by the sim and
// content teams) are collected and reported as test diagnostics instead
const engineErrors = h.logs.error.filter((l) => !l.startsWith('[sim]'));
assert.deepEqual(engineErrors, [], `${mode}/${difficulty}/${seed}: logged errors`);
for (const l of h.logs.error) if (l.startsWith('[sim]')) simIssues.add(l.split(' | ')[0].slice(0, 160));
assert.deepEqual(h.badFrames, [], 'every frame is JSON-safe');
assert.equal(m.dispatcher.errors, 0);
checkInvariants(m);
if (clientCombat) {
assert.equal(m.verifyStats.rejected, 0, `${mode}/${difficulty}/${seed}: an honest client result was rejected`);
assert.equal(m.verifyStats.takeovers, 0, `${mode}/${difficulty}/${seed}: unexpected takeover`);
assert.equal(h.frames, 0, 'no b.snap / b.ev under client-side combat');
for (const c of h.clients.values()) assert.ok(c.log.some((x) => x.t === 'b.result'), 'every human reported its battles');
}
const res = h.lastTo('p_0', 'm.result');
assert.ok(res && res.playerId === 'p_0');
assert.ok(phases > 5);
m.dispose();
if (h.clients) h.clients.closeAll();
assert.equal(h.sched.pending(), 0);
return { h, m, res: h.ended };
}