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

75 lines
4.5 KiB
JavaScript
Raw Permalink 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.
// Stage selection per match (waves.js setupMatchWaves; user playtest: "三局都是同一张地图"). Official mode data
// (data/config.json modes[].stages, stage weight 50 each):
// * 标准模拟 (FUNNY, solo & co-op) and 入门协议 use ONLY 战场#01 (act1autochess_m01) — official, kept: the lobby's default
// difficulty is 标准, so back-to-back 标准 matches always play 战场#01;
// * 险境模拟 (NORMAL) picks uniformly among 8 stages (战场#01–#08), 绝境 / 终极 (HARD / ABYSS) among 7 (no 战场#01),
// with the match seed (Match: createRng(deriveSeed(seed, 'setup')), a fresh random seed per lobby match).
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { GameData } from '../../server/match/gamedata.js';
import { setupMatchWaves } from '../../server/match/waves.js';
import { createRng, deriveSeed } from '../../server/sim/rng.js';
import { DATA, makeMatch } from './harness.js';
const M01 = 'act1autochess_m01';
const REST = ['act1autochess_m02', 'act1autochess_m03', 'act1autochess_m04', 'act2autochess_m01', 'act2autochess_m02', 'act2autochess_m03', 'act2autochess_m04'];
const pick = (modeId, seed) => setupMatchWaves(new GameData(DATA, modeId), createRng(deriveSeed(seed >>> 0, 'setup'))).stageId;
test('mode stage lists: 标准 / 入门 = 战场#01 only; 险境 = 8 stages; 绝境 / 终极 = 7 (no 战场#01); all weight 50', () => {
const stagesOf = (m) => DATA.config.modes[m].stages.slice().sort();
for (const t of ['single', 'multi']) {
assert.deepEqual(stagesOf(`mode_${t}_funny`), [M01], `${t} 标准`);
assert.deepEqual(stagesOf(`mode_${t}_normal`), [M01, ...REST].sort(), `${t} 险境`);
assert.deepEqual(stagesOf(`mode_${t}_hard`), REST.slice().sort(), `${t} 绝境`);
assert.deepEqual(stagesOf(`mode_${t}_abyss`), REST.slice().sort(), `${t} 终极`);
}
assert.deepEqual(stagesOf('mode_training_1'), [M01]);
for (const id of [M01, ...REST]) assert.ok(DATA.stages[id].active && DATA.stages[id].weight === 50, id);
});
test('险境 / 绝境 / 终极: the match seed picks uniformly among the mode stages; 标准 always 战场#01', () => {
const N = 1600;
for (const t of ['single', 'multi']) {
for (const d of ['normal', 'hard', 'abyss']) {
const modeId = `mode_${t}_${d}`;
const list = DATA.config.modes[modeId].stages;
const count = new Map(list.map((s) => [s, 0]));
for (let i = 0; i < N; i++) {
const s = pick(modeId, Math.imul(i + 1, 2654435761));
assert.ok(count.has(s), `${modeId}: ${s}`);
count.set(s, count.get(s) + 1);
}
const exp = N / list.length;
for (const [s, n] of count) assert.ok(n > exp * 0.75 && n < exp * 1.25, `${modeId} ${s}: ${n} of ${N} (uniform ≈ ${exp.toFixed(0)})`);
}
for (let i = 0; i < 50; i++) assert.equal(pick(`mode_${t}_funny`, i * 7919 + 1), M01, `${t} 标准`);
}
});
test('different match seeds give different stages (no fixed map), the same seed the same stage', () => {
// consecutive lobby seeds are independent uint32 draws: 3 matches in a row on one stage is ~1/64 in 险境
let same3 = 0, runs = 0;
for (let i = 0; i < 300; i += 3, runs++) {
const a = pick('mode_multi_normal', i * 104729 + 17), b = pick('mode_multi_normal', (i + 1) * 104729 + 17), c = pick('mode_multi_normal', (i + 2) * 104729 + 17);
if (a === b && b === c) same3++;
}
assert.ok(same3 <= 6, `${same3} of ${runs} triples on one stage`);
const first10 = new Set(Array.from({ length: 10 }, (_, i) => pick('mode_single_hard', 1000 + i)));
assert.ok(first10.size >= 4, `${first10.size} stages over 10 seeds`);
assert.equal(pick('mode_multi_abyss', 424242), pick('mode_multi_abyss', 424242));
});
test('Match: the stage comes from the match seed (险境 co-op / 绝境 solo vary, 标准 fixed)', () => {
const seen = new Set();
for (let seed = 1; seed <= 12; seed++) {
const h = makeMatch({ mode: 'coop', difficulty: 'NORMAL', humans: 1, bots: 1, seed, fake: true });
assert.equal(h.m.stageId, pick('mode_multi_normal', seed), `seed ${seed}`);
seen.add(h.m.stageId);
}
assert.ok(seen.size >= 4, `${seen.size} stages over 12 co-op 险境 matches`);
const solo = new Set();
for (let seed = 1; seed <= 12; seed++) solo.add(makeMatch({ mode: 'solo', difficulty: 'HARD', seed, fake: true }).m.stageId);
assert.ok(solo.size >= 4 && !solo.has(M01), `solo 绝境: ${[...solo].join(' ')}`);
for (let seed = 1; seed <= 6; seed++) assert.equal(makeMatch({ mode: 'solo', difficulty: 'FUNNY', seed, fake: true }).m.stageId, M01);
});