feedback5 WG: golden — a standins family (10 battles: every NORMAL chess record, normal and elite, fielded as its 补位 stand-in on real stages and waves plus ground enemies; all 17 stand-ins, every active backup skill cast) and one co-op match whose AI-托管 human seat does not own 7 chess (coop2-NORMAL-14-standins); the 133 existing digests unchanged (roster / bonds / fields byte-identical, the 16 old matches identical)

This commit is contained in:
sganggs committed 2026-10-05 19:42:49 +08:00
1 parent 4b57f8120c
commit 29725e3a39
5 files changed
+882 -19

No files matched your search

+66
View File
@@ -0,0 +1,66 @@
// The 补位 golden corpus covers what it claims (test/golden/README.md, tools/golden.mjs — 0.2.0): reads the stored
// digests only (test/golden.test.js recomputes them). The `standins` family fields every NORMAL chess record (normal and
// elite) as its stand-in — the battle unit's def is the chess id, its loadout the backup selection — so all 17 stand-ins
// run; every active skill a chess names for its stand-in is cast in some battle (the ON_DEPLOY passives of 预备干员-特种
// and Misery count no cast: their units deploy); no battle reports a content error. The 补位 match fields stand-ins.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
const read = (p) => JSON.parse(readFileSync(new URL(p, import.meta.url), 'utf8'));
const CHESS = read('../data/chess.json');
const BACKUPS = read('../data/backups.json');
const STANDINS = read('./golden/standins.json');
const MATCHES = read('./golden/matches.json');
const COL = Object.fromEntries(STANDINS.columns.units.map((k, i) => [k, i]));
const normal = Object.values(CHESS).filter((c) => c.chessType === 'NORMAL' && c.backup && c.backup.charId !== c.charId);
test('standins: every NORMAL chess record (normal + elite) is fielded once, with its backup skill, without content errors', () => {
assert.equal(normal.length, 110);
const seen = new Map();
for (const [id, sc] of Object.entries(STANDINS.scenarios)) {
assert.equal(sc.errors.count, 0, `${id}: ${sc.errors.kinds.join('; ')}`);
for (const u of sc.units) if (u[COL.kind] === 'op') seen.set(u[COL.def], u);
}
assert.deepEqual([...seen.keys()].sort(), normal.map((c) => c.chessId).sort());
for (const c of normal) {
const u = seen.get(c.chessId);
assert.equal(u[COL.skill], c.backup.skillIndex, `${c.chessId}: the backup skill`);
assert.ok(u[COL.deploys] >= 1, `${c.chessId}: deployed`);
}
assert.equal(new Set(normal.map((c) => c.backup.charId)).size, 17, 'all 17 stand-ins');
});
test('standins: every active skill a chess names for its stand-in is cast; passive ones deploy', () => {
const cast = new Map();
for (const sc of Object.values(STANDINS.scenarios)) {
for (const u of sc.units) {
if (u[COL.kind] !== 'op') continue;
const c = CHESS[u[COL.def]];
const k = `${c.backup.charId}|${u[COL.skill]}`;
cast.set(k, (cast.get(k) || 0) + (u[COL.casts] || 0));
}
}
for (const c of normal) {
const k = `${c.backup.charId}|${c.backup.skillIndex}`;
const form = Object.values(BACKUPS.units[c.backup.charId].forms)[0];
const sk = form.skills.find((s) => s.index === c.backup.skillIndex);
if (sk.skillType === 'PASSIVE' || sk.spType === 'ON_DEPLOY') continue;
assert.ok(cast.get(k) > 0, `${c.backup.charId} S${c.backup.skillIndex + 1} (${sk.name}) never cast`);
}
});
test('matches: the 补位 match\'s human seat fields stand-ins (listed per round) and the match ends without errors', () => {
const dg = MATCHES.scenarios['coop2-NORMAL-14-standins'];
assert.ok(dg, 'scenario present');
assert.deepEqual(dg.errors, { engine: 0, logged: 0, sim: 0, dispatcher: 0 });
const fielded = dg.standIns.rounds.filter(([, s]) => s).flatMap(([, s]) => s.split(' '));
assert.ok(new Set(fielded).size >= 3, `stand-ins fielded: ${[...new Set(fielded)].join(', ')}`);
for (const f of fielded) {
const [id, charId] = f.split('→');
const c = CHESS[`chess_char_${id}`];
assert.ok(dg.standIns.notOwned.includes(c.baseId || c.chessId), `${f}: not owned`);
assert.equal(charId, c.backup.charId);
}
});
+5 -3
View File
@@ -7,7 +7,7 @@ through.
- `tools/golden.mjs` builds the corpus from `data/*.json` (fixed order, fixed seeds) and reduces every scenario to a
digest. Its header documents the families and the digest fields.
- `test/golden/<family>.json` hold the digests (`roster`, `bonds`, `fields`, `matches`).
- `test/golden/<family>.json` hold the digests (`roster`, `bonds`, `fields`, `matches`, `standins`).
- `test/golden.test.js` recomputes them and compares. On a mismatch it prints the scenario, the field and old → new.
## The corpus
@@ -17,7 +17,8 @@ through.
| `roster` | 49 battles: every visible chess record (normal and elite) with every selectable skill and module (DESIGN §16), 12 operators per battle on a real stage (all 11 in turn) against the round's real wave three times over, every non-leader enemy kind of `data/enemies.json` as extra spawns (half of them bounties), placeable summons on the board, every equipment item, band, battle-side 机变 card and stage map card in turn, bonds from the board |
| `bonds` | 46 battles: every bond at its activation threshold (1 layer) and at its top tier (999 layers) |
| `fields` | 22 battles: every Final Assault / Hidden Core leader on a pair and a solo template (shared pool, 200 s cap) and the 联防 field with 1 and 2 helpers on its own map (carried HP / SP, a knocked-out operator, two leakers' enemies) |
| `matches` | 16 bot-only matches to the end in virtual time: solo 标准 / 险境 / 绝境 / 终极 × 2 seeds, co-op 2 / 3 / 4, one server-run combat match, two runs boosted to the Hidden Core |
| `matches` | 17 matches to the end in virtual time: 16 bot-only (solo 标准 / 险境 / 绝境 / 终极 × 2 seeds, co-op 2 / 3 / 4, one server-run combat match, two runs boosted to the Hidden Core) and one co-op match whose human seat (AI 托管, offline) does not own 7 NORMAL chess — they fight as their 补位 stand-ins (its digest lists them per round, `standIns`) |
| `standins` | 10 battles: every NORMAL chess record (normal and elite, 110) fielded as its 补位 stand-in (`standIn: true`: the stand-in's body, backup skill / module and kit — all 17 stand-ins, every skill a chess names for them), 12 per battle by strength band, laid out by the stand-in's position on a real stage, against the round's real wave three times over plus 8 ground enemy kinds (melee stand-ins always meet an enemy), an item each, bonds from the board |
Battles go through the production BattleSpec path (`server/sim/spec.js`, as browsers and the server's headless fields
run them); matches construct `Match` directly with a `VirtualScheduler`. Every option is explicit, so a change to a
@@ -29,7 +30,8 @@ within a tick (they are counted, not hashed) and wall-clock time. Damage / heali
to 2 decimals.
`npm test` runs the fast subset (the scenarios marked `"fast": true`: every chess record with its default loadout,
every stage and every non-leader enemy kind, every bond at its top tier, six fields, five matches — 53 of the 133). `GOLDEN_FULL=1` checks everything.
every stage and every non-leader enemy kind, every bond at its top tier, six fields, six matches, the five stand-in
battles of the normal records — 59 of the 144). `GOLDEN_FULL=1` checks everything.
## Workflow
+130 -1
View File
@@ -1,6 +1,6 @@
{
"family": "matches",
"about": "bot-only matches run to the end in virtual time (solo ×4 difficulties ×2 seeds, co-op 2/3/4, one server-run, two boosted to the Hidden Core)",
"about": "matches run to the end in virtual time: bot-only (solo ×4 difficulties ×2 seeds, co-op 2/3/4, one server-run, two boosted to the Hidden Core) and one co-op match whose human seat fields 补位 stand-ins",
"note": "Generated by `npm run golden:update` (tools/golden.mjs). A refactor must never change this file; see test/golden/README.md.",
"columns": {
"rounds": ["alive","lp","prepFunds","funds","pending","level","deployed","board","layers","bonds","killed","total","leaks","gold"]
@@ -1476,6 +1476,135 @@
"bonds": ["arcaneShip 0/302","deputShip 1/328","egirShip 0/300","emptyShip 0/300","indomShip 1/326","investShip 0/300","kazimierzShip 0/300","kjeragShip 0/300","lateranoShip 0/300","maniShip 0/300","miraShip 0/300","preciShip 0/309","raidShip 0/300","sargonShip 0/300","siracusaShip 0/300","skillfulShip 1/314","soloShip 1/318","steadShip 2/384","suntShip 1/310","swiftShip 0/300","victoriaShip 1/342","visiShip 0/306","yanShip 0/310"]
}
}
},
"coop2-NORMAL-14-standins": {
"about": "coop NORMAL, 1 bot seat(s) + 1 human seat (AI 托管) without 7 operators (补位 stand-ins), seed 14",
"fast": true,
"mode": "coop",
"difficulty": "NORMAL",
"seats": 1,
"seed": 14,
"combat": "client",
"boost": null,
"setup": {"stageId":"act1autochess_m01","bossId":"boss_5","hiddenBossId":"boss_10","factions":["DOT","INVISIBLE","REFLECTION"]},
"end": {"victory":true,"roundsPassed":14,"hiddenReached":false,"hiddenCleared":false,"reason":"victory","teamLp":2,"bossPoolLeft":0,"virtualMs":116550},
"rng": {"setup":25,"shop":495,"waves":0,"draft":34,"bots":17791,"meta":11},
"errors": {"engine":0,"logged":0,"sim":0,"dispatcher":0},
"rounds": [
{
"round": 1,
"h_0": [1,28,4,0,0,1,2,"58414d69",0,"arcaneShip 0/2, preciShip 0/2",5,5,0,4],
"ai_0": [1,23,4,0,0,1,2,"40f57285",0,"kazimierzShip 0/2, swiftShip 0/2",5,5,0,4]
},
{
"round": 2,
"h_0": [1,28,5,0,0,1,4,"f52e5aa9",0,"arcaneShip 0/4, preciShip 0/4",6,6,0,9],
"ai_0": [1,23,5,0,0,1,4,"1c3bb4d9",4,"kazimierzShip 0/4, swiftShip 1/4",6,6,0,9]
},
{
"round": 3,
"h_0": [1,28,6,0,0,1,7,"77be43d5",0,"arcaneShip 0/4, kazimierzShip 0/4, preciShip 0/4, swiftShip 0/4, yanShip 0/1",6,6,0,15],
"ai_0": [1,23,6,0,0,1,7,"751ce61a",4,"kazimierzShip 0/4, sargonShip 0/2, swiftShip 1/4",6,6,0,15]
},
{
"round": 4,
"h_0": [1,28,7,0,0,2,8,"7cb948b2",8,"arcaneShip 0/4, kazimierzShip 0/8, preciShip 0/4, raidShip 1/8, soloShip 1/0, swiftShip 0/8, yanShip 0/1",9,9,0,22],
"ai_0": [1,23,7,0,0,1,8,"b679c297",10,"arcaneShip 0/2, deputShip 1/2, kazimierzShip 0/4, preciShip 0/2, sargonShip 0/2, swiftShip 1/8",9,9,0,22]
},
{
"round": 5,
"h_0": [1,28,8,0,0,3,8,"d543ab44",16,"arcaneShip 0/4, kazimierzShip 0/8, preciShip 0/4, raidShip 1/16, soloShip 1/0, swiftShip 0/8, yanShip 0/1",21,21,0,30],
"ai_0": [1,23,8,0,0,2,8,"b5d78234",34,"arcaneShip 0/2, deputShip 1/4, kazimierzShip 0/8, lateranoShip 1/5, preciShip 1/7, sargonShip 0/2, suntShip 1/2, swiftShip 1/16",21,21,0,30]
},
{
"round": 6,
"h_0": [1,28,9,0,2,3,8,"9b4b0e34",36,"arcaneShip 0/4, kazimierzShip 0/8, preciShip 0/4, raidShip 1/28, soloShip 1/0, swiftShip 1/8, yanShip 0/1",31,31,0,39],
"ai_0": [1,23,9,0,1,3,8,"c6b88ba7",52,"arcaneShip 0/2, deputShip 2/8, kazimierzShip 0/8, lateranoShip 1/15, preciShip 0/10, sargonShip 0/2, suntShip 1/6, swiftShip 1/23",31,31,0,39]
},
{
"round": 7,
"h_0": [1,28,12,0,2,4,8,"f09ca8b6",44,"arcaneShip 0/4, kazimierzShip 0/8, preciShip 0/4, raidShip 1/36, soloShip 1/0, swiftShip 1/8, yanShip 0/1",34,34,0,50],
"ai_0": [1,23,11,0,1,4,8,"f6f38045",77,"arcaneShip 0/2, deputShip 2/12, kazimierzShip 0/8, lateranoShip 1/25, preciShip 0/10, sargonShip 0/2, suntShip 1/10, swiftShip 1/30",34,34,0,50]
},
{
"round": 8,
"h_0": [1,25,13,0,2,4,8,"1070819b",63,"arcaneShip 1/7, kazimierzShip 0/8, preciShip 0/4, raidShip 1/48, skillfulShip 1/0, swiftShip 1/8, yanShip 0/1",12,15,3,62],
"ai_0": [1,23,12,0,0,4,8,"87633ca6",106,"arcaneShip 0/2, deputShip 2/16, kazimierzShip 0/8, lateranoShip 1/35, preciShip 0/10, sargonShip 0/2, skillfulShip 1/4, suntShip 1/14, swiftShip 1/37",15,15,0,62]
},
{
"round": 9,
"h_0": [1,23,14,0,0,4,8,"70a00d2f",50,"arcaneShip 1/10, kazimierzShip 1/12, preciShip 1/9, raidShip 0/48, skillfulShip 1/3, soloShip 1/0, swiftShip 1/16, yanShip 0/1",21,23,2,76],
"ai_0": [1,21,12,0,0,4,8,"8dc0d798",161,"arcaneShip 0/2, deputShip 1/18, kazimierzShip 0/8, lateranoShip 1/47, preciShip 1/20, sargonShip 0/2, skillfulShip 1/11, suntShip 1/16, swiftShip 1/49",21,23,2,75]
},
{
"round": 10,
"h_0": [1,23,12,0,0,5,8,"59b986b7",73,"arcaneShip 1/10, kazimierzShip 1/19, preciShip 1/14, raidShip 0/48, skillfulShip 1/6, soloShip 1/3, swiftShip 1/21, yanShip 0/1",31,31,0,87],
"ai_0": [1,21,12,0,0,5,8,"fa71d132",188,"arcaneShip 0/2, deputShip 0/18, kazimierzShip 0/8, lateranoShip 1/71, preciShip 1/25, sargonShip 0/2, skillfulShip 1/16, suntShip 1/16, swiftShip 1/60",31,31,0,88]
},
{
"round": 11,
"h_0": [1,19,12,0,0,5,8,"b7278144",146,"arcaneShip 0/10, kazimierzShip 1/34, preciShip 2/19, raidShip 1/61, skillfulShip 0/9, soloShip 1/6, suntShip 1/0, swiftShip 1/26, yanShip 0/1",21,25,4,99],
"ai_0": [1,19,12,0,0,5,8,"e240abda",226,"arcaneShip 0/2, deputShip 0/18, kazimierzShip 0/8, lateranoShip 1/79, preciShip 1/33, raidShip 1/5, sargonShip 0/2, skillfulShip 1/21, suntShip 1/16, swiftShip 1/67, visiShip 1/5",23,25,2,100]
},
{
"round": 12,
"h_0": [1,12,12,0,0,6,8,"955db8af",172,"arcaneShip 0/10, kazimierzShip 1/42, preciShip 2/24, raidShip 1/69, skillfulShip 0/9, soloShip 1/6, suntShip 1/0, swiftShip 1/31, yanShip 0/1",24,31,7,111],
"ai_0": [1,9,12,0,2,6,8,"3c27819a",264,"arcaneShip 0/2, deputShip 0/18, kazimierzShip 0/8, lateranoShip 1/87, preciShip 1/41, raidShip 1/10, sargonShip 0/2, skillfulShip 1/26, suntShip 1/16, swiftShip 1/74, visiShip 1/10",17,31,14,113]
},
{
"round": 13,
"h_0": [1,2,12,0,0,6,8,"1bd1e6d0",244,"arcaneShip 1/10, kazimierzShip 1/54, preciShip 2/34, raidShip 1/105, skillfulShip 0/9, soloShip 0/6, suntShip 1/0, swiftShip 1/41, yanShip 0/1",20,57,37,122],
"ai_0": [0,0,14,0,0,6,0,"811c9dc5",0,"arcaneShip 0/2, deputShip 0/18, kazimierzShip 0/8, lateranoShip 0/95, preciShip 0/49, raidShip 0/15, sargonShip 0/2, skillfulShip 0/31, suntShip 0/16, swiftShip 0/81, visiShip 0/15",15,57,42,126]
}
],
"players": {
"h_0": {
"alive": true,
"victory": true,
"roundsPassed": 14,
"eliminatedRound": null,
"lp": 2,
"bandId": "band_amedic",
"title": {"id":"comment_1","name":"卫戍之星","picId":"comment_icon_1","text":"对敌方领袖造成伤害最高(仅胜利时)"},
"trophies": 4,
"reward": 400,
"stats": {"activatedLayers":276,"bossDamage":390000,"dmgDealt":2811982,"gold":135,"itemsEquipped":23,"kills":254,"leaks":53,"lpLost":26,"merges":5,"perfectRounds":8,"refreshes":21},
"lineup": ["6_05_a@12,3[3_08_e_a+3_05_e_a]","4_14_b@11,3[3_11_e_b+6_05_e_a]","1_17_b@10,3","6_19_a@10,4","6_17_a@10,7[4_08_e_a+1_05_e_b]","6_06_a@10,8[1_01_e_a+4_04_e_a]","3_12_a@9,4[4_04_e_b+1_02_e_b]","3_13_b@9,8[2_01_e_a+4_03_e_a]"],
"bonds": ["arcaneShip 0/14","kazimierzShip 1/62","kjeragShip 0/8","preciShip 2/48","raidShip 1/105","sargonShip 1/2","skillfulShip 0/9","soloShip 0/6","suntShip 1/0","swiftShip 1/59","yanShip 0/1"]
},
"ai_0": {
"alive": false,
"victory": false,
"roundsPassed": 12,
"eliminatedRound": 13,
"lp": 0,
"bandId": "band_chiave",
"title": {"id":"comment_6","name":"挥金如土","picId":"comment_icon_6","text":"消耗资金最多"},
"trophies": 3,
"reward": 280,
"stats": {"activatedLayers":0,"bossDamage":0,"dmgDealt":2138966,"gold":126,"itemsEquipped":15,"kills":234,"leaks":60,"lpLost":24,"merges":3,"perfectRounds":9,"refreshes":23},
"lineup": [],
"bonds": ["arcaneShip 0/2","deputShip 0/18","kazimierzShip 0/8","lateranoShip 0/95","preciShip 0/49","raidShip 0/15","sargonShip 0/2","skillfulShip 0/31","suntShip 0/16","swiftShip 0/81","visiShip 0/15"]
}
},
"standIns": {
"notOwned": ["chess_char_3_12_a","chess_char_3_18_a","chess_char_5_20_a","chess_char_6_05_a","chess_char_6_06_a","chess_char_6_17_a","chess_char_6_19_a"],
"rounds": [
[1,""],
[2,""],
[3,""],
[4,"3_12_a→char_608_acpion"],
[5,"3_12_a→char_608_acpion"],
[6,"3_12_a→char_608_acpion"],
[7,"3_12_a→char_608_acpion"],
[8,"3_12_a→char_608_acpion"],
[9,"3_12_a→char_608_acpion"],
[10,"3_12_a→char_608_acpion"],
[11,"3_12_a→char_608_acpion 6_05_a→char_611_acnipe"],
[12,"3_12_a→char_608_acpion 6_05_a→char_611_acnipe"],
[13,"3_12_a→char_608_acpion 6_05_a→char_611_acnipe 6_17_a→char_608_acpion"]
]
}
}
}
}
+578
View File
@@ -0,0 +1,578 @@
{
"family": "standins",
"about": "every NORMAL chess record (normal + elite) fielded as its 补位 stand-in (all 17 stand-ins and the skills the chess name for them), 12 per battle on real stages and waves",
"note": "Generated by `npm run golden:update` (tools/golden.mjs). A refactor must never change this file; see test/golden/README.md.",
"columns": {
"units": ["player","ref","kind","def","skill","module","hp","alive","deploys","kills","dmg","taken","heal","attacks","casts"],
"enemies": ["spawned","killed","leaked","dmg","taken"],
"hooks": ["battleStart","deploy","tick","beforeAttack","attack","hit","damaged","heal","kill","death","skillStart","skillEnd","ammoUsed","spGain","statusApplied","blocked","enemySpawn","enemyLeak","battleEnd","fatal","layerGain","elementBurst","dodge"]
},
"scenarios": {
"standin-01": {
"about": "normal band 4 as stand-ins: chess_char_3_12_a→char_608_acpion chess_char_4_09_a→char_617_sharp2 chess_char_3_05_a→char_601_cguard chess_char_3_18_a→char_609_acguad chess_char_4_04_a→char_607_cspec chess_char_4_07_a→char_601_cguard chess_char_4_16_a→char_615_acspec chess_char_3_01_a→char_603_csnipe chess_char_3_04_a→char_605_cmedic chess_char_3_21_a→char_611_acnipe chess_char_4_02_a→char_611_acnipe chess_char_4_13_a→char_614_acsupo",
"fast": true,
"kind": "normal",
"stageId": "act1autochess_m02",
"modeId": "mode_single_normal",
"round": 8,
"seed": 2069558672,
"waveId": "act1autochess_h01",
"time": 170,
"ticks": 5100,
"reason": "timeout",
"killed": 21,
"total": 50,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 402,
"errors": {"count":0,"kinds":[]},
"players": {
"p1": {
"killed": 21,
"total": 50,
"perfect": false,
"leaks": 29,
"leaksUncounted": 0,
"coins": 4,
"dmg": 284159,
"bossDmg": 0,
"heal": 26726,
"deaths": 22,
"layerGains": {"raidShip":14,"visiShip":2}
}
},
"units": [
["p1",1,"op","chess_char_3_12_a",2,null,0,0,67,4,56082,3680,0,37,3],
["p1",2,"op","chess_char_4_09_a",0,null,0,0,77,3,56268,5968,0,34,2],
["p1",3,"op","chess_char_3_05_a",2,null,2109.99,1,74,4,33622,8823,0,39,2],
["p1",4,"op","chess_char_3_18_a",2,null,0,0,7,3,33048,9522,0,77,2],
["p1",5,"op","chess_char_4_04_a",1,null,435.7,1,80,0,2375,13673,0,113,0],
["p1",6,"op","chess_char_4_07_a",1,null,0,0,71,1,16766,11343,0,37,1],
["p1",7,"op","chess_char_4_16_a",1,null,807.8,1,85,3,27738,6036,0,83,0],
["p1",8,"op","chess_char_3_01_a",2,null,1271,1,5,1,16680,0,0,152,3],
["p1",9,"op","chess_char_3_04_a",2,null,1218,1,8,0,0,587,26726,59,3],
["p1",10,"op","chess_char_3_21_a",2,null,1272,1,84,1,21722,1272,0,62,1],
["p1",11,"op","chess_char_4_02_a",1,null,1272,1,82,1,9400,1724,0,71,1],
["p1",12,"op","chess_char_4_13_a",1,null,0,0,78,0,10458,1624,0,28,1]
],
"enemies": {
"enemy_10001_trslim": [1,1,0,8,4860],
"enemy_1000_gopro_2": [1,1,0,22,1836],
"enemy_10018_sgrobh": [1,1,0,2668,8100],
"enemy_1001_bigbo": [1,0,1,0,2592],
"enemy_10027_vtsk": [1,0,1,11845,916],
"enemy_10031_cnvsld": [1,0,1,0,0],
"enemy_10034_cnvsax": [1,0,1,0,0],
"enemy_10042_prtrop": [1,0,1,808,3807],
"enemy_1116_liprr_2": [21,12,9,5388,124143],
"enemy_1119_vofsd": [12,5,6,21861,99812],
"enemy_1121_lifbos_2": [9,1,7,13749,38094]
},
"hooks": [1,85,5100,943,943,1330,1321,3766,43,76,19,28,0,21170,28,44,50,27,1,43,6,0,9],
"events": {"atk":1028,"bounty":2,"deploy":35,"die":49,"dmg":1321,"fx":68,"heal":59,"layer":6,"leak":27,"skill":35,"spawn":62,"status":134},
"fx": {"blink":6,"dodge":9,"dp":7,"garrison":6,"liberate":15,"slash":24,"telegraph":1},
"snaps": "e6528b42 398001f2 644c5ee8 df7b48f9 a8a51847 554e1092 13c60701 49f04e5d fb95fb75 38fa86a2 687082e0 9c39cb98 7dd1bdd1 a74eebb6 b9c1e236 8fc6086d 944683e9"
},
"standin-02": {
"about": "normal band 5 as stand-ins: chess_char_4_17_a→char_610_acfend chess_char_4_18_a→char_602_cdfend chess_char_5_05_a→char_602_cdfend chess_char_4_19_a→char_608_acpion chess_char_5_01_a→char_600_cpione chess_char_4_22_a→char_609_acguad chess_char_4_23_a→char_609_acguad chess_char_5_06_a→char_609_acguad chess_char_4_20_a→char_603_csnipe chess_char_4_24_a→char_604_ccast chess_char_5_02_a→char_604_ccast chess_char_5_03_a→char_612_accast",
"fast": true,
"kind": "normal",
"stageId": "act1autochess_m05",
"modeId": "mode_multi_abyss",
"round": 10,
"seed": 3710344357,
"waveId": "act1autochess_h03",
"time": 190,
"ticks": 5700,
"reason": "timeout",
"killed": 9,
"total": 68,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 113,
"errors": {"count":0,"kinds":[]},
"players": {
"p1": {"killed":9,"total":68,"perfect":false,"leaks":59,"leaksUncounted":0,"coins":0,"dmg":98109,"bossDmg":0,"heal":0,"deaths":23,"layerGains":{"preciShip":12}}
},
"units": [
["p1",1,"op","chess_char_4_17_a",1,null,0,0,3,0,57,2604,0,4,1],
["p1",2,"op","chess_char_4_18_a",1,null,0,0,78,0,2350,6810,0,6,1],
["p1",3,"op","chess_char_5_05_a",2,null,0,0,70,0,14,4256,0,1,0],
["p1",4,"op","chess_char_4_19_a",1,null,0,0,66,0,316,3058,0,13,1],
["p1",5,"op","chess_char_5_01_a",2,null,0,0,91,0,35,3675,0,2,0],
["p1",6,"op","chess_char_4_22_a",1,null,0,0,71,0,5165,5276,0,9,0],
["p1",7,"op","chess_char_4_23_a",1,null,0,0,64,0,5497,5276,0,9,0],
["p1",8,"op","chess_char_5_06_a",2,null,0,0,1,0,0,2638,0,0,0],
["p1",9,"op","chess_char_4_20_a",1,null,1271,1,92,1,11656,2542,0,25,0],
["p1",10,"op","chess_char_4_24_a",1,null,0,0,90,0,3736,3399,0,8,0],
["p1",11,"op","chess_char_5_02_a",2,null,0,0,84,3,13253,2266,0,25,1],
["p1",12,"op","chess_char_5_03_a",2,null,0,0,4,5,56032,1372,0,10,1]
],
"enemies": {
"enemy_10042_prtrop_2": [1,0,1,4567,14393],
"enemy_10043_sailor": [1,1,0,1137,23649],
"enemy_10044_wintun": [1,0,1,4374,12094],
"enemy_10045_parrot": [1,0,1,0,4099],
"enemy_10054_cjhot": [1,0,1,1676,1665],
"enemy_1005_yokai": [27,8,19,0,29182],
"enemy_1005_yokai_2": [27,0,24,11401,2816],
"enemy_1005_yokai_3": [6,0,6,20017,14371],
"enemy_10065_ftzlc": [1,0,1,0,560],
"enemy_10067_ftsjc": [1,0,1,0,680],
"enemy_1006_shield": [1,0,1,0,760]
},
"hooks": [1,92,5700,337,337,558,523,0,32,88,5,5,0,12532,1,13,68,56,1,32,3,0,41],
"events": {"atk":345,"deploy":24,"die":32,"dmg":523,"fx":81,"layer":3,"leak":56,"skill":10,"spawn":80,"status":2},
"fx": {"aoe":4,"dodge":41,"dp":4,"explode":27,"garrison":3,"garrisonGrant":1,"zone":1},
"snaps": "9d2468c6 9a32f006 99b2aa33 a01ecc39 97bb00b0 2fcede4b fe594f15 377f7691 e87b0fd3 af44759d 7b13ea89 61cd9b40 4f9d1182 5905fb57 d697ca06 c37e9513 3102b854 296cdd49 4d5efa28"
},
"standin-03": {
"about": "normal band 5 as stand-ins: chess_char_5_08_a→char_610_acfend chess_char_5_14_a→char_608_acpion chess_char_5_19_a→char_617_sharp2 chess_char_5_07_a→char_601_cguard chess_char_5_13_a→char_609_acguad chess_char_5_17_a→char_615_acspec chess_char_5_10_a→char_606_csuppo chess_char_5_11_a→char_613_acmedc chess_char_5_12_a→char_614_acsupo chess_char_5_15_a→char_603_csnipe chess_char_5_18_a→char_611_acnipe chess_char_5_20_a→char_614_acsupo",
"fast": true,
"kind": "normal",
"stageId": "act2autochess_m01",
"modeId": "mode_multi_funny",
"round": 10,
"seed": 3083705594,
"waveId": "act1autochess_h03",
"time": 190,
"ticks": 5700,
"reason": "timeout",
"killed": 37,
"total": 74,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 374,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":37,"total":74,"perfect":false,"leaks":37,"leaksUncounted":0,"coins":8,"dmg":331499,"bossDmg":0,"heal":49226,"deaths":12,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_5_08_a",2,null,0,0,6,0,4409,3444,0,59,1],
["p1",2,"op","chess_char_5_14_a",2,null,0,0,86,16,131680,8403,0,53,6],
["p1",3,"op","chess_char_5_19_a",0,null,0,0,90,8,38187,15644,0,91,2],
["p1",4,"op","chess_char_5_07_a",2,null,0,0,7,2,18936,10110,0,69,2],
["p1",5,"op","chess_char_5_13_a",2,null,1872.24,1,11,5,68168,12979,0,127,4],
["p1",6,"op","chess_char_5_17_a",2,null,0,0,81,2,7848,5571,0,85,0],
["p1",7,"op","chess_char_5_10_a",2,null,932,1,2,1,8617,0,0,74,2],
["p1",8,"op","chess_char_5_11_a",2,null,1224,1,3,0,0,0,49226,72,2],
["p1",9,"op","chess_char_5_12_a",2,null,1160,1,4,0,22812,0,0,88,2],
["p1",10,"op","chess_char_5_15_a",2,null,0,0,85,2,8089,12138,0,87,2],
["p1",11,"op","chess_char_5_18_a",2,null,0,0,1,1,13952,1272,0,34,1],
["p1",12,"op","chess_char_5_20_a",2,null,742,1,91,0,8800,3202,0,43,1]
],
"enemies": {
"enemy_1006_shield_2": [1,1,0,679,8000],
"enemy_1006_shield_3": [1,0,1,11277,13035],
"enemy_10073_mpcar": [1,1,0,12,1600],
"enemy_1007_slime": [1,1,0,0,440],
"enemy_10081_mpplai": [1,1,0,0,2000],
"enemy_10087_hlchgr": [1,1,0,58,4800],
"enemy_10094_crstf": [1,1,0,28,3200],
"enemy_10097_crshd": [1,0,1,936,5046],
"enemy_1166_dusbr_2": [42,27,15,3496,107410],
"enemy_1170_dushld_2": [12,2,8,27941,44857],
"enemy_9011_acrefr": [12,2,4,28337,141112]
},
"hooks": [1,91,5700,1118,1118,1648,1663,132,49,78,25,27,0,27793,3468,54,74,29,1,49,0,1,9],
"events": {"atk":1244,"bounty":4,"deploy":17,"die":49,"dmg":1661,"fx":130,"heal":126,"leak":29,"skill":50,"spawn":86,"status":661},
"fx": {"aoe":30,"burst":1,"displace":1,"dodge":16,"dp":22,"phase":1,"slash":48,"sp":1,"spGain":10},
"snaps": "dbcb5cec 56a8a8f6 e8d97a5d d2594a66 b6f753a4 6b5239d5 0d94f1a8 53bcdab2 582202c7 d547ef42 438122c0 f5345cd2 dbac677c 38aa1103 537a6dbf c9e9e9aa 7c7ed647 9cfd6d5b 94abfb24"
},
"standin-04": {
"about": "normal band 6 as stand-ins: chess_char_6_03_a→char_610_acfend chess_char_6_02_a→char_608_acpion chess_char_6_07_a→char_617_sharp2 chess_char_6_10_a→char_617_sharp2 chess_char_6_11_a→char_608_acpion chess_char_6_06_a→char_609_acguad chess_char_5_22_a→char_611_acnipe chess_char_6_01_a→char_611_acnipe chess_char_6_04_a→char_614_acsupo chess_char_6_05_a→char_611_acnipe chess_char_6_08_a→char_612_accast chess_char_6_09_a→char_613_acmedc",
"fast": true,
"kind": "normal",
"stageId": "act2autochess_m04",
"modeId": "mode_single_hard",
"round": 12,
"seed": 428893583,
"waveId": "act1autochess_h05",
"time": 201.7,
"ticks": 6051,
"reason": "cleared",
"killed": 136,
"total": 158,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 644,
"errors": {"count":0,"kinds":[]},
"players": {
"p1": {
"killed": 136,
"total": 158,
"perfect": false,
"leaks": 22,
"leaksUncounted": 0,
"coins": 8,
"dmg": 1309822,
"bossDmg": 0,
"heal": 23527,
"deaths": 12,
"layerGains": {"sargonShip":5}
}
},
"units": [
["p1",1,"op","chess_char_6_03_a",2,null,0,0,174,0,698,7812,0,6,0],
["p1",2,"op","chess_char_6_02_a",2,null,1529,1,179,1,2975,3058,0,7,2],
["p1",3,"op","chess_char_6_07_a",0,null,0,0,172,9,135322,3730,0,74,1],
["p1",4,"op","chess_char_6_10_a",0,null,0,0,166,5,94409,3730,0,75,2],
["p1",5,"op","chess_char_6_11_a",2,null,1026.4,1,178,11,100320,8547,0,40,4],
["p1",6,"op","chess_char_6_06_a",2,null,2080.4,1,156,9,111400,19283,0,111,4],
["p1",7,"op","chess_char_5_22_a",2,null,1272,1,3,2,25528,0,0,19,2],
["p1",8,"op","chess_char_6_01_a",2,null,1272,1,4,34,325813,0,0,164,2],
["p1",9,"op","chess_char_6_04_a",2,null,1160,1,13,7,65845,0,0,121,2],
["p1",10,"op","chess_char_6_05_a",2,null,1272,1,14,18,234561,2453,0,122,2],
["p1",11,"op","chess_char_6_08_a",2,null,1372,1,5,39,212952,0,0,57,2],
["p1",12,"op","chess_char_6_09_a",2,null,1224,1,11,0,0,0,23527,37,2],
["-","~1","device","trap_1105_accrate",null,null,100,1,1,0,0,0,0,0,null],
["-","~2","device","trap_1105_accrate",null,null,100,1,2,0,0,0,0,0,null]
],
"enemies": {
"enemy_10098_crhro": [1,1,0,3450,41472],
"enemy_10099_crvln": [1,1,0,2453,20736],
"enemy_1009_lurker": [1,1,0,16,3041],
"enemy_1010_demon": [1,1,0,0,10368],
"enemy_1010_demon_2": [28,22,6,15447,543909],
"enemy_10112_ymgds": [1,1,0,0,6912],
"enemy_10116_ymgtop": [1,1,0,2924,13824],
"enemy_10118_ymgprc": [1,0,1,3965,18459],
"enemy_1071_dftman": [63,63,0,447,243855],
"enemy_1072_dlancer": [60,45,15,19911,429319]
},
"hooks": [1,181,6051,918,918,1945,1944,62,148,170,25,24,0,31170,4160,44,158,22,1,149,2,0,13],
"events": {"atk":1080,"bounty":4,"deploy":21,"die":148,"dmg":1944,"fx":87,"heal":45,"layer":2,"leak":22,"skill":49,"spawn":172,"status":440},
"fx": {"aoe":7,"dodge":13,"dp":18,"garrison":2,"revive":1,"slash":29,"sp":12,"spGain":3,"telegraph":2},
"snaps": "29a29463 6fc96b2d 184b448a fce3c126 45d67357 2b2b80de f966fc58 66e1b5c9 ce0eff29 79805bf9 289d6433 dbecb111 d3142708 bec781dd 26e4cc8e 0c071920 829264cd 362b2f26 16b5df6a 9fda7720 7730ec59"
},
"standin-05": {
"about": "normal band 6 as stand-ins: chess_char_6_17_a→char_608_acpion chess_char_6_13_a→char_615_acspec chess_char_6_19_a→char_615_acspec chess_char_6_12_a→char_612_accast chess_char_6_15_a→char_614_acsupo chess_char_6_18_a→char_612_accast chess_char_6_20_a→char_613_acmedc",
"fast": true,
"kind": "normal",
"stageId": "act1autochess_m03",
"modeId": "mode_multi_normal",
"round": 12,
"seed": 4096833445,
"waveId": "act1autochess_h05",
"time": 214.33,
"ticks": 6430,
"reason": "cleared",
"killed": 7,
"total": 104,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 246,
"errors": {"count":0,"kinds":[]},
"players": {
"p1": {"killed":7,"total":104,"perfect":false,"leaks":97,"leaksUncounted":0,"coins":0,"dmg":426178,"bossDmg":0,"heal":1353,"deaths":22,"layerGains":{"raidShip":3}}
},
"units": [
["p1",1,"op","chess_char_6_17_a",2,null,0,0,128,1,103267,9556,0,43,1],
["p1",2,"op","chess_char_6_13_a",2,null,0,0,132,1,25092,9232,0,87,0],
["p1",3,"op","chess_char_6_19_a",2,null,0,0,134,2,29265,9232,0,141,0],
["p1",4,"op","chess_char_6_12_a",2,null,671.78,1,7,1,143681,975,0,85,2],
["p1",5,"op","chess_char_6_15_a",2,null,0,0,133,2,54479,4965,0,113,2],
["p1",6,"op","chess_char_6_18_a",2,null,1372,1,85,0,70393,1372,0,42,2],
["p1",7,"op","chess_char_6_20_a",2,null,1224,1,4,0,0,1353,1353,3,1],
["-","~1","device","trap_1105_accrate",null,null,100,1,1,0,0,0,0,0,null],
["-","~2","device","trap_1105_accrate",null,null,100,1,2,0,0,0,0,0,null],
["-","~3","device","trap_1105_accrate",null,null,100,1,3,0,0,0,0,0,null]
],
"enemies": {
"enemy_10122_uacann_2": [1,0,1,6380,12182],
"enemy_10124_uashld_2": [1,0,1,0,5764],
"enemy_10127_rkmbst_2": [1,0,1,834,0],
"enemy_10138_xdsnow": [1,0,1,0,1276],
"enemy_10141_xdpeng_2": [1,0,1,4211,14094],
"enemy_10144_xdelk_2": [1,0,1,0,42],
"enemy_10156_mncrer": [1,0,1,0,738],
"enemy_10159_mntrjn": [1,0,1,0,0],
"enemy_1381_winman": [27,5,22,3499,98764],
"enemy_1381_winman_2": [39,1,38,5799,153206],
"enemy_1387_winshd": [30,1,29,15960,140112]
},
"hooks": [1,134,6430,620,620,988,988,6,29,128,8,24,0,14868,2437,28,104,97,1,29,1,0,0],
"events": {"atk":651,"deploy":27,"die":31,"dmg":901,"fx":58,"heal":3,"layer":1,"leak":97,"skill":16,"spawn":114,"status":514},
"fx": {"aoe":5,"blink":2,"camouflage":1,"displace":22,"dodge":3,"dp":2,"explode":6,"garrison":1,"garrisonGrant":2,"slash":8,"zone":6},
"snaps": "8d7b3223 b6dafa64 3940bc8e fd79b9e2 e5e85618 6fcfc479 90958489 71bfbb91 d12c224c cd2c9c7f 4a9fff33 68e48a86 4b97e027 5f8e8dad da62239a 8601c0dc 73717b6d 9e4f784f 818e7ed1 c1f2a855 33118eab 9bafc0bc"
},
"standin-06": {
"about": "elite band 5 as stand-ins: chess_char_3_12_b→char_608_acpion chess_char_4_09_b→char_617_sharp2 chess_char_3_05_b→char_601_cguard chess_char_3_18_b→char_609_acguad chess_char_4_04_b→char_607_cspec chess_char_4_07_b→char_601_cguard chess_char_4_16_b→char_615_acspec chess_char_3_01_b→char_603_csnipe chess_char_3_04_b→char_605_cmedic chess_char_3_21_b→char_611_acnipe chess_char_4_02_b→char_611_acnipe chess_char_4_13_b→char_614_acsupo",
"fast": false,
"kind": "normal",
"stageId": "act1autochess_m06",
"modeId": "mode_multi_hard",
"round": 10,
"seed": 1442657168,
"waveId": "act1autochess_h03",
"time": 185.83,
"ticks": 5575,
"reason": "cleared",
"killed": 40,
"total": 98,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 293,
"errors": {"count":0,"kinds":[]},
"players": {
"p1": {"killed":40,"total":98,"perfect":false,"leaks":58,"leaksUncounted":0,"coins":8,"dmg":553335,"bossDmg":0,"heal":19916,"deaths":32,"layerGains":{"raidShip":24}}
},
"units": [
["p1",1,"op","chess_char_3_12_b",2,"none",1904,1,135,10,142984,5468,880,20,3],
["p1",2,"op","chess_char_4_09_b",0,"none",0,0,6,7,58726,3510,0,13,1],
["p1",3,"op","chess_char_3_05_b",2,"none",0,0,90,1,27607,9128,0,18,2],
["p1",4,"op","chess_char_3_18_b",2,"none",0,0,89,3,34118,9190,2808,27,2],
["p1",5,"op","chess_char_4_04_b",1,"none",0,0,134,1,8258,8846,0,21,0],
["p1",6,"op","chess_char_4_07_b",1,"none",0,0,93,0,19765,10676,0,20,1],
["p1",7,"op","chess_char_4_16_b",1,"none",0,0,133,2,17401,11631,0,26,0],
["p1",8,"op","chess_char_3_01_b",2,"none",0,0,128,1,21787,3020,0,27,1],
["p1",9,"op","chess_char_3_04_b",2,"none",0,0,4,0,0,6998,16229,17,2],
["p1",10,"op","chess_char_3_21_b",2,"none",0,0,114,11,155605,7911,0,47,1],
["p1",11,"op","chess_char_4_02_b",1,"none",0,0,2,3,47003,1483,0,29,1],
["p1",12,"op","chess_char_4_13_b",1,"none",0,0,96,1,20081,3262,0,25,2]
],
"enemies": {
"enemy_10162_mnctpt": [1,1,0,7990,39813],
"enemy_1019_jshoot": [1,1,0,423,4479],
"enemy_1019_jshoot_2": [1,1,0,1526,6718],
"enemy_1021_bslime": [1,1,0,3434,6121],
"enemy_1023_jmage": [1,1,0,2086,5474],
"enemy_1025_reveng": [1,1,0,0,12442],
"enemy_1026_aghost": [1,1,0,0,9456],
"enemy_1043_zomsbr": [1,0,1,0,0],
"enemy_1251_lysyta_2": [57,25,32,15811,231796],
"enemy_1252_lysytb_2": [21,7,14,21523,186411],
"enemy_1329_cbshld_2": [12,1,11,28332,50624]
},
"hooks": [1,135,5575,471,471,727,756,61,72,134,16,33,0,6314,36,49,98,58,1,81,3,0,6],
"events": {"atk":553,"bounty":4,"deploy":37,"die":76,"dmg":756,"fx":57,"heal":58,"layer":3,"leak":58,"skill":32,"spawn":110,"status":119},
"fx": {"blink":4,"camouflage":1,"dodge":11,"dp":13,"explode":1,"garrison":3,"slash":24},
"snaps": "2e7a8cb5 d50cd79b d7949cae 1b9749a6 6e3d202d f87f73b6 45e24574 d9fad5af 5495e86c 3357c483 195f9b7f 05cf2852 258f0a18 7db86d77 fcd4dcb8 b89c90a0 d44a9d5e 0de542a5 f00c7481"
},
"standin-07": {
"about": "elite band 6 as stand-ins: chess_char_4_17_b→char_610_acfend chess_char_4_18_b→char_602_cdfend chess_char_5_05_b→char_602_cdfend chess_char_4_19_b→char_608_acpion chess_char_5_01_b→char_600_cpione chess_char_4_22_b→char_609_acguad chess_char_4_23_b→char_609_acguad chess_char_5_06_b→char_609_acguad chess_char_4_20_b→char_603_csnipe chess_char_4_24_b→char_604_ccast chess_char_5_02_b→char_604_ccast chess_char_5_03_b→char_612_accast",
"fast": false,
"kind": "normal",
"stageId": "act2autochess_m02",
"modeId": "mode_single_normal",
"round": 12,
"seed": 816286799,
"waveId": "act1autochess_h05",
"time": 222.97,
"ticks": 6689,
"reason": "cleared",
"killed": 8,
"total": 80,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 80,
"errors": {"count":0,"kinds":[]},
"players": {
"p1": {
"killed": 8,
"total": 80,
"perfect": false,
"leaks": 72,
"leaksUncounted": 0,
"coins": 4,
"dmg": 126420,
"bossDmg": 0,
"heal": 0,
"deaths": 22,
"layerGains": {"preciShip":8,"sargonShip":18}
}
},
"units": [
["p1",1,"op","chess_char_4_17_b",1,"none",0,0,3,0,42,3211,0,2,1],
["p1",2,"op","chess_char_4_18_b",1,"none",0,0,83,0,30,9306,0,1,1],
["p1",3,"op","chess_char_5_05_b",2,"none",0,0,97,0,572,5816,0,1,1],
["p1",4,"op","chess_char_4_19_b",1,"none",1847.63,1,107,1,367,3864,0,5,1],
["p1",5,"op","chess_char_5_01_b",2,"none",1036.53,1,105,0,2197,3985,0,5,3],
["p1",6,"op","chess_char_4_22_b",1,"none",3191,1,109,0,1178,14884,0,5,1],
["p1",7,"op","chess_char_4_23_b",1,"none",0,0,67,0,3484,6610,0,8,1],
["p1",8,"op","chess_char_5_06_b",2,"uniequip_002_acguad",0,0,10,0,0,3820,0,0,0],
["p1",9,"op","chess_char_4_20_b",1,"none",1255.06,1,106,2,16976,4326,0,31,1],
["p1",10,"op","chess_char_4_24_b",1,"none",1007.59,1,108,0,5460,3072,0,18,1],
["p1",11,"op","chess_char_5_02_b",2,"none",0,0,102,1,8032,4080,0,19,1],
["p1",12,"op","chess_char_5_03_b",2,"uniequip_002_accast",0,0,4,4,88082,1749,0,10,1],
["-","~1","device","trap_1105_accrate",null,null,100,1,1,0,0,0,0,0,null],
["-","~2","device","trap_1105_accrate",null,null,100,1,2,0,0,0,0,0,null]
],
"enemies": {
"enemy_1005_yokai_2": [18,2,16,3894,7075],
"enemy_1041_lazerd_2": [24,3,21,41810,33530],
"enemy_1042_frostd": [30,1,29,0,24206],
"enemy_1045_hammer": [1,1,0,1914,15552],
"enemy_1045_hammer_2": [1,0,1,6132,25057],
"enemy_1046_agent": [1,1,0,57,3888],
"enemy_1050_lslime": [1,0,1,9425,1320],
"enemy_1061_zomshd": [1,0,1,31,9241],
"enemy_1062_rager_2": [1,0,1,1459,14551],
"enemy_1067_snslime": [1,0,1,0,0],
"enemy_1071_dftman": [1,0,1,0,0]
},
"hooks": [1,109,6689,304,304,581,573,378,30,102,13,13,0,8920,9,11,80,72,1,33,2,0,8],
"events": {"atk":330,"bounty":2,"deploy":27,"die":30,"dmg":573,"fx":22,"layer":2,"leak":72,"skill":26,"spawn":94,"status":103},
"fx": {"aoe":1,"dodge":8,"dp":6,"garrison":2,"garrisonGrant":1,"palsy":1,"revive":3},
"snaps": "ef9ff66a 4f4ac6fc 133f7f1c a187a8c8 46d89701 4bb496a8 0dfccf57 81173658 8c42876d 4718c77c d3b80c8e df3c692d 0576a74b 7684de92 e59b0c43 849795d5 e99812de 3f883cd6 6bb9cdf6 b246f597 289cd790 2f34e6ff 997f51f3"
},
"standin-08": {
"about": "elite band 6 as stand-ins: chess_char_5_08_b→char_610_acfend chess_char_5_14_b→char_608_acpion chess_char_5_19_b→char_617_sharp2 chess_char_5_07_b→char_601_cguard chess_char_5_13_b→char_609_acguad chess_char_5_17_b→char_615_acspec chess_char_5_10_b→char_606_csuppo chess_char_5_11_b→char_613_acmedc chess_char_5_12_b→char_614_acsupo chess_char_5_15_b→char_603_csnipe chess_char_5_18_b→char_611_acnipe chess_char_5_20_b→char_614_acsupo",
"fast": false,
"kind": "normal",
"stageId": "act1autochess_m01",
"modeId": "mode_multi_abyss",
"round": 12,
"seed": 2456427706,
"waveId": "act1autochess_h05",
"time": 230,
"ticks": 6900,
"reason": "timeout",
"killed": 32,
"total": 179,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 1052,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":32,"total":179,"perfect":false,"leaks":147,"leaksUncounted":0,"coins":2,"dmg":1703220,"bossDmg":0,"heal":106481,"deaths":9,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_5_08_b",2,"uniequip_002_acfend",3285.08,1,201,4,80453,15794,0,116,3],
["p1",2,"op","chess_char_5_14_b",2,"uniequip_002_acpion",1904,1,200,8,678474,11374,0,31,9],
["p1",3,"op","chess_char_5_19_b",0,"uniequip_002_sharp2",4154.5,1,202,4,187598,17962,0,51,2],
["p1",4,"op","chess_char_5_07_b",2,"none",3334,1,199,2,46104,18401,0,36,3],
["p1",5,"op","chess_char_5_13_b",2,"uniequip_002_acguad",0,0,160,5,101185,25518,0,50,3],
["p1",6,"op","chess_char_5_17_b",2,"uniequip_002_acspec",1448,1,8,2,21853,46495,0,206,0],
["p1",7,"op","chess_char_5_10_b",2,"none",1131,1,4,1,46614,0,0,155,4],
["p1",8,"op","chess_char_5_11_b",2,"uniequip_002_acmedc",2262.4,1,5,0,0,0,106481,74,3],
["p1",9,"op","chess_char_5_12_b",2,"uniequip_002_acsupo",1430,1,11,2,97118,0,0,130,3],
["p1",10,"op","chess_char_5_15_b",2,"none",1510,1,13,0,84181,0,0,228,4],
["p1",11,"op","chess_char_5_18_b",2,"uniequip_002_acnipe",2380.5,1,6,4,265046,0,0,156,3],
["p1",12,"op","chess_char_5_20_b",2,"uniequip_002_acsupo",1430,1,14,0,94594,0,0,125,3],
["-","~1","device","trap_1105_accrate",null,null,100,1,1,0,0,0,0,0,null],
["-","~2","device","trap_1105_accrate",null,null,100,1,2,0,0,0,0,0,null],
["-","~3","device","trap_1105_accrate",null,null,100,1,3,0,0,0,0,0,null]
],
"enemies": {
"enemy_1072_dlancer": [1,1,0,3008,37150],
"enemy_1081_sotisd": [1,1,0,39568,92876],
"enemy_1092_mdgint": [1,0,1,0,67],
"enemy_1116_liprr": [1,0,1,0,7702],
"enemy_1116_liprr_2": [1,0,1,0,1829],
"enemy_1118_lidbox_2": [1,0,1,0,1344],
"enemy_1119_vofsd": [1,0,1,0,959],
"enemy_1121_lifbos": [1,0,1,0,922],
"enemy_1249_lysdb_2": [60,9,49,23770,693077],
"enemy_1251_lysyta": [63,20,43,6676,488381],
"enemy_1254_lypa_2": [48,1,47,62523,378913]
},
"hooks": [1,202,6900,1760,1760,3076,3094,145,41,186,40,39,0,26130,16747,54,179,145,1,45,0,0,8],
"events": {"atk":1979,"bounty":1,"deploy":20,"die":41,"dmg":3094,"fx":274,"heal":126,"leak":145,"skill":79,"spawn":194,"status":1237},
"fx": {"aoe":84,"dodge":29,"dp":17,"shieldBreak":60,"slash":72,"sp":5,"spGain":7},
"snaps": "4d4dc5ab 98e9e455 46d76035 722418a9 b67e68a3 866e42e8 786ec7e2 2c2a7307 3657963b 8fad1c0c 513f1b73 84a84fa5 d1698d65 9ae19dde 551dcc8c 8e2b8aef a8f4f525 70f7b5a7 f8b9e9b9 0c6627fe 9c4013d8 fa4c2add 6f3c1f53"
},
"standin-09": {
"about": "elite band 7 as stand-ins: chess_char_6_03_b→char_610_acfend chess_char_6_02_b→char_608_acpion chess_char_6_07_b→char_617_sharp2 chess_char_6_10_b→char_617_sharp2 chess_char_6_11_b→char_608_acpion chess_char_6_06_b→char_609_acguad chess_char_5_22_b→char_611_acnipe chess_char_6_01_b→char_611_acnipe chess_char_6_04_b→char_614_acsupo chess_char_6_05_b→char_611_acnipe chess_char_6_08_b→char_612_accast chess_char_6_09_b→char_613_acmedc",
"fast": false,
"kind": "normal",
"stageId": "act1autochess_m04",
"modeId": "mode_multi_funny",
"round": 13,
"seed": 2308517178,
"waveId": "act1autochess_h06",
"time": 199.53,
"ticks": 5986,
"reason": "cleared",
"killed": 158,
"total": 158,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 279,
"errors": {"count":0,"kinds":[]},
"players": {
"p1": {"killed":158,"total":158,"perfect":true,"leaks":0,"leaksUncounted":0,"coins":8,"dmg":924432,"bossDmg":0,"heal":29522,"deaths":0,"layerGains":{"sargonShip":16}}
},
"units": [
["p1",1,"op","chess_char_6_03_b",2,"uniequip_002_acfend",4974.95,1,8,0,0,0,0,0,0],
["p1",2,"op","chess_char_6_02_b",2,"uniequip_002_acpion",1904,1,6,0,0,0,0,0,0],
["p1",3,"op","chess_char_6_07_b",0,"uniequip_002_sharp2",2494,1,9,0,0,0,0,0,0],
["p1",4,"op","chess_char_6_10_b",0,"uniequip_002_sharp2",2494,1,11,24,119442,0,0,173,3],
["p1",5,"op","chess_char_6_11_b",2,"uniequip_002_acpion",1624,1,13,38,184185,11513,0,52,20],
["p1",6,"op","chess_char_6_06_b",2,"uniequip_002_acguad",3396,1,14,19,66075,855,0,156,4],
["p1",7,"op","chess_char_5_22_b",2,"uniequip_002_acnipe",1587,1,4,0,0,0,0,0,0],
["p1",8,"op","chess_char_6_01_b",2,"uniequip_002_acnipe",1643,1,5,0,0,0,0,0,0],
["p1",9,"op","chess_char_6_04_b",2,"uniequip_002_acsupo",83.4,1,15,11,95944,2382,0,135,2],
["p1",10,"op","chess_char_6_05_b",2,"uniequip_002_acnipe",1643,1,7,19,77978,0,0,93,2],
["p1",11,"op","chess_char_6_08_b",2,"uniequip_002_accast",1819,1,10,47,380809,0,0,82,2],
["p1",12,"op","chess_char_6_09_b",2,"uniequip_002_acmedc",1324,1,12,0,0,17709,29522,83,2],
["-","~1","device","trap_1105_accrate",null,null,100,1,1,0,0,0,0,0,null],
["-","~2","device","trap_1105_accrate",null,null,100,1,2,0,0,0,0,0,null],
["-","~3","device","trap_1105_accrate",null,null,100,1,3,0,0,0,0,0,null]
],
"enemies": {
"enemy_1121_lifbos_2": [1,1,0,0,24000],
"enemy_1148_dssbr": [1,1,0,0,2688],
"enemy_1148_dssbr_2": [1,1,0,0,3456],
"enemy_1158_divman": [1,1,0,195,3168],
"enemy_1160_hvyslr": [1,1,0,0,9600],
"enemy_1160_hvyslr_2": [1,1,0,0,13440],
"enemy_1161_tidmag": [1,1,0,1363,5760],
"enemy_1161_tidmag_2": [1,1,0,824,7680],
"enemy_1249_lysdb_2": [60,60,0,1374,345600],
"enemy_1251_lysyta": [51,51,0,0,134640],
"enemy_1329_cbshld": [39,39,0,3643,374400]
},
"hooks": [1,173,5986,867,867,1620,1744,149,158,158,35,34,0,31554,3298,63,158,0,1,158,2,1,21],
"events": {"atk":965,"bounty":4,"deploy":12,"die":158,"dmg":1722,"fx":309,"heal":116,"layer":2,"skill":69,"spawn":173,"status":425},
"fx": {"aoe":2,"burst":1,"dodge":27,"dp":58,"garrison":2,"shieldBreak":60,"slash":146,"sp":13},
"snaps": "268546d4 ec010f3d d556f759 749d85b5 fd1848fa 9a69b0ed 35a96974 ceeb72dd adfeed08 f497b058 ffade80c ec17b5c7 49c46293 8eebc960 34dab525 36f0055b b3c7d1bd 036e5819 41df2c8f 33def1f4"
},
"standin-10": {
"about": "elite band 7 as stand-ins: chess_char_6_17_b→char_608_acpion chess_char_6_13_b→char_615_acspec chess_char_6_19_b→char_615_acspec chess_char_6_12_b→char_612_accast chess_char_6_15_b→char_614_acsupo chess_char_6_18_b→char_612_accast chess_char_6_20_b→char_613_acmedc",
"fast": false,
"kind": "normal",
"stageId": "act1autochess_m07",
"modeId": "mode_single_hard",
"round": 13,
"seed": 3949114127,
"waveId": "act1autochess_h06",
"time": 216.2,
"ticks": 6486,
"reason": "cleared",
"killed": 25,
"total": 131,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 245,
"errors": {"count":0,"kinds":[]},
"players": {
"p1": {"killed":25,"total":131,"perfect":false,"leaks":106,"leaksUncounted":0,"coins":0,"dmg":508719,"bossDmg":0,"heal":10260,"deaths":25,"layerGains":{"raidShip":6}}
},
"units": [
["p1",1,"op","chess_char_6_17_b",2,"uniequip_002_acpion",0,0,164,8,199340,14855,0,64,9],
["p1",2,"op","chess_char_6_13_b",2,"uniequip_002_acspec",0,0,163,6,43893,11872,0,99,0],
["p1",3,"op","chess_char_6_19_b",2,"uniequip_002_acspec",1484,1,165,3,65411,16324,0,126,0],
["p1",4,"op","chess_char_6_12_b",2,"uniequip_002_accast",2265.49,1,4,4,78264,281,0,32,2],
["p1",5,"op","chess_char_6_15_b",2,"uniequip_002_acsupo",0,0,147,2,52466,6493,0,38,3],
["p1",6,"op","chess_char_6_18_b",2,"uniequip_002_accast",1819,1,8,2,69344,618,0,44,2],
["p1",7,"op","chess_char_6_20_b",2,"uniequip_002_acmedc",1464,1,9,0,0,3373,10260,17,1],
["-","~1","device","trap_1105_accrate",null,null,100,1,1,0,0,0,0,0,null],
["-","~2","device","trap_1105_accrate",null,null,100,1,2,0,0,0,0,0,null]
],
"enemies": {
"enemy_1006_shield_3": [21,1,20,14948,163592],
"enemy_1043_zomsbr": [66,20,46,789,124139],
"enemy_1162_magmot": [1,0,1,4063,11534],
"enemy_1165_duhond": [1,1,0,139,4645],
"enemy_1165_duhond_2": [1,0,1,0,0],
"enemy_1166_dusbr": [1,0,1,0,0],
"enemy_1166_dusbr_2": [1,0,1,0,0],
"enemy_1168_dumage": [1,1,0,1476,7465],
"enemy_1168_dumage_2": [1,0,1,3178,0],
"enemy_1169_duphlx": [1,0,1,0,0],
"enemy_1402_tgshd_2": [36,2,34,29224,197345]
},
"hooks": [1,165,6486,549,549,865,877,6809,50,159,17,36,0,8486,1125,49,131,106,1,50,1,1,0],
"events": {"atk":554,"deploy":32,"die":53,"dmg":802,"fx":530,"heal":21,"layer":1,"leak":106,"skill":34,"spawn":140,"status":378},
"fx": {"aoe":4,"blink":3,"burst":1,"displace":17,"dodge":3,"dp":17,"expose":414,"garrison":1,"garrisonGrant":3,"slash":67},
"snaps": "80709a69 1b180600 2040e410 16970933 0b0b610d 4e3060e6 d2f5fa1d d015c650 26f1c266 9350b3c3 ad710861 7c181d0b d41c00a0 64db1e01 858cfe5f 40faa038 5cf23687 498f5f47 50bc9ad2 848c94df 4806ebb3 9bfdaf83"
}
}
}
+103 -15
View File
@@ -8,7 +8,7 @@
// Usage:
// node tools/golden.mjs compare the corpus with test/golden/*.json (exit 1 on a difference)
// node tools/golden.mjs --update recompute and rewrite test/golden/*.json (npm run golden:update)
// options: --family roster,bonds,fields,matches --fast (the test's default subset) --only <id,id>
// options: --family roster,bonds,fields,matches,standins --fast (the test's default subset) --only <id,id>
// --jobs N (worker threads, default: up to 4) --list --coverage (skills cast, stages, enemies…)
// --twice determinism: compute twice in this process (the second pass in reverse order) and compare
//
@@ -25,9 +25,14 @@
// ended at 200 game s) and 联防 fields (1 and 2 helpers on the 联防 map of their count — boards laid out on a
// battle stage —, carried HP / SP, a knocked-out operator, two leakers' enemies with a summoned-only kind and
// a bounty)
// matches 16 bot-only matches (solo 标准 / 险境 / 绝境 / 终极 ×2 seeds, co-op 2 / 3 / 4, one server-run combat match,
// two with LP and layers raised at the first prep so they reach the Hidden Core) run to the end in virtual
// time with the match's default bot rehearsal
// matches 17 matches run to the end in virtual time with the match's default bot rehearsal: 16 bot-only (solo 标准 /
// 险境 / 绝境 / 终极 ×2 seeds, co-op 2 / 3 / 4, one server-run combat match, two with LP and layers raised at the
// first prep so they reach the Hidden Core) and one co-op match whose human seat (AI 托管, offline: its
// battles run on the server) does not own a few NORMAL chess — they fight as their 补位 stand-ins (0.2.0)
// standins 10 battles: every NORMAL chess record (normal + elite, 110) fielded as its 补位 stand-in (PlayerBattleInput
// standIn: true — DATA.md §18: the stand-in's body, its backup skill / module, its kit by charId; all 17
// stand-ins and every skill a chess names for them), 12 per battle by strength band, laid out by the stand-in's
// position on a real stage, against the round's real wave three times over, an item each, bonds from the board
// Battles run through the production BattleSpec path (server/sim/spec.js buildBattleSpec → createBattleFromSpec, the
// path browsers and the server's headless fields use) with every option explicit; matches construct Match directly
// with a VirtualScheduler (as tools/botbench.mjs) — test-harness defaults never move a digest.
@@ -68,7 +73,7 @@ import { GEO } from '../shared/constants.js';
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
export const GOLDEN_DIR = join(ROOT, 'test', 'golden');
export const FAMILY_NAMES = Object.freeze(['roster', 'bonds', 'fields', 'matches']);
export const FAMILY_NAMES = Object.freeze(['roster', 'bonds', 'fields', 'matches', 'standins']);
const QUIET = Object.freeze({ warn() {}, error() {}, info() {}, log() {}, debug() {} });
const data = getData({ log: QUIET });
@@ -323,11 +328,13 @@ function layout(gd, stageId, wanted, { field = 'normal', colOffset = 0, max = 12
const rank = (r, c) => { const d = dist(r, c); return d === 0 ? 1.5 : d; }; // beside a path first, then on it
return out.sort((a, b) => rank(a[0], a[1]) - rank(b[0], b[1]));
};
const isMelee = (w) => positionClass(data.chess[w.chessId], w.moduleId) === 'melee';
const order = wanted.slice().sort((a, b) => (isMelee(b) - isMelee(a)) || (isMelee(a) ? (data.chess[b.chessId].stats.blockCnt || 0) - (data.chess[a.chessId].stats.blockCnt || 0) : 0));
// a 补位 unit (`standIn`) is laid out by its stand-in's body (position, block count); every other by its chess record
const recOf = (w) => (w.standIn ? gd.standIn(w.chessId) || data.chess[w.chessId] : data.chess[w.chessId]);
const isMelee = (w) => positionClass(recOf(w), w.moduleId) === 'melee';
const order = wanted.slice().sort((a, b) => (isMelee(b) - isMelee(a)) || (isMelee(a) ? (recOf(b).stats.blockCnt || 0) - (recOf(a).stats.blockCnt || 0) : 0));
let uid = uid0;
for (const w of order) {
const rec = data.chess[w.chessId];
const rec = recOf(w);
const free = units.length < max ? tiles(positionClass(rec, w.moduleId)) : [];
if (!free.length) { rest.push(w); continue; }
const [r, c] = free[0];
@@ -336,12 +343,14 @@ function layout(gd, stageId, wanted, { field = 'normal', colOffset = 0, max = 12
const u = { uid: uid++, kind: 'chess', chessId: w.chessId, row: r, col: c, dir, items: w.items ?? [] };
if (w.skillIndex != null) u.skillIndex = w.skillIndex;
if (w.moduleId != null) u.moduleId = w.moduleId;
if (w.standIn) u.standIn = true;
if (w.carryState) u.carryState = w.carryState;
units.push(u);
}
// placeable summons of the placed operators
// placeable summons of the placed operators (a stand-in makes none: none of the 17 has one)
const tokens = [];
for (const u of units) {
if (u.standIn) continue;
for (const { tokenId, count } of gd.placeableTokens(u.chessId, { skillIndex: u.skillIndex ?? null })) {
const trec = data.tokens[tokenId];
let allowed = null;
@@ -625,6 +634,60 @@ export function fieldScenarios() {
return scenarios;
}
// ---------------------------------------------------------------------------------------------------------------
// family: standins (0.2.0 补位)
/** The NORMAL chess records (normal + elite) that have a stand-in (shared/standIn.js: PRESET / 自选 have none). */
const STANDIN_CHESS = CHESS.filter((c) => c.chessType === 'NORMAL' && !c.isDiy && c.backup && c.backup.charId && c.backup.charId !== c.charId);
export function standInScenarios() {
// normal records first, then the elites; within them by strength band (tier + elite) and id; a battle takes up to 12
// with distinct chess ids and bands within one of the first waiting record's
const queue = STANDIN_CHESS.map((rec) => ({ chessId: rec.chessId, standIn: true, elite: rec.isGolden ? 1 : 0, band: rec.tier + (rec.isGolden ? 1 : 0) }));
queue.sort((a, b) => a.elite - b.elite || a.band - b.band || byId(a.chessId, b.chessId));
// every battle also gets 8 ground enemy kinds as extra spawns: a 飞行 round wave would leave the melee stand-ins
// nobody to block, and their skills (cast with an enemy in range) uncast
const ground = cursor(EXTRA_ENEMIES.filter((k) => !data.enemies[k].tokenOnly && !isFlyKey(gdFor('mode_multi_normal'), k)));
const items = cursor(EQUIPS);
const bands = cursor(BANDS.slice().reverse());
const scenarios = [];
let n = 0;
while (queue.length) {
const i = n++;
const stageId = STAGES[(i * 3 + 1) % STAGES.length];
const modeId = ROSTER_MODES[(i + 2) % ROSTER_MODES.length];
const gd = gdFor(modeId);
const first = queue[0];
const picked = [];
for (const w of queue) {
if (picked.length >= 12) break;
if (picked.some((x) => x.chessId === w.chessId) || Math.abs(w.band - first.band) > 1 || w.elite !== first.elite) continue;
picked.push(w);
}
const chunk = picked.map((w) => ({ ...w, items: [items.next()] }));
const { units } = layout(gd, stageId, chunk, { max: 12 });
if (!units.length) throw new Error(`standins: nothing fits on ${stageId}`);
const placed = new Set(units.map((u) => u.chessId));
for (let q = queue.length - 1; q >= 0; q--) if (picked.includes(queue[q]) && placed.has(queue[q].chessId)) queue.splice(q, 1);
if (!placed.size) throw new Error('standins: no progress');
const band = Math.max(...picked.filter((w) => placed.has(w.chessId)).map((w) => w.band));
const round = BAND_ROUND[band];
const seed = deriveSeed(20261005, `standins:${i}`);
const wave = normalWave(gd, round, seed);
const pid = 'p1';
scenarios.push({
id: `standin-${String(i + 1).padStart(2, '0')}`, family: 'standins', kind: 'normal', modeId, round, stageId, seed, pass: first.elite,
rect: { ...GEO.NORMAL_RECT }, timeLimit: wave.timeLimit, routes: wave.routes, waveId: wave.templateId, enemyOverrides: wave.overrides,
flags: { layerGainsEnabled: true, ...gd.dp },
players: [playerInput(pid, 0, units, { bonds: bondsOf(gd, units, LAYER_STEPS[i % LAYER_STEPS.length]), bandId: bands.next() })],
spawns: [0, 1, 2].flatMap((k) => wave.spawns.map((sp) => ({ ...sp, time: sp.time + k * Math.round(wave.timeLimit / 4), ownerPlayerId: pid })))
.concat(extraSpawns(gd, wave.routes, Array.from({ length: 8 }, () => ground.next()), round, pid, 6, 6)),
about: `${first.elite ? 'elite' : 'normal'} band ${band} as stand-ins: ${units.map((u) => `${u.chessId}→${gd.standIn(u.chessId)?.charId ?? '?'}`).join(' ')}`,
});
}
return scenarios;
}
// ---------------------------------------------------------------------------------------------------------------
// family: matches
@@ -641,9 +704,19 @@ export function matchScenarios() {
// match reaches the Final Assault and — Σ activated layers over the threshold — the Hidden Core
out.push({ id: 'solo-HARD-9-boosted', mode: 'solo', difficulty: 'HARD', bots: 1, seed: 9, boost: { lp: 400, layers: 300 } });
out.push({ id: 'coop2-ABYSS-10-boosted', mode: 'coop', difficulty: 'ABYSS', bots: 2, seed: 10, boost: { lp: 400, layers: 300 } });
return out.map((m) => ({ ...m, family: 'matches', kind: 'match', about: `${m.mode} ${m.difficulty}, ${m.bots} bot seat(s), seed ${m.seed}${m.clientCombat === false ? ', server-run combat' : ''}${m.boost ? `, LP ${m.boost.lp} and ${m.boost.layers} layers per bond from the first prep` : ''}` }));
// 0.2.0 补位: a human seat (AI 托管, offline: its battles run on the server) that does not own a few NORMAL chess
out.push({ id: 'coop2-NORMAL-14-standins', mode: 'coop', difficulty: 'NORMAL', bots: 1, seed: 14, human: { notOwned: MATCH_NOT_OWNED } });
return out.map((m) => ({ ...m, family: 'matches', kind: 'match', about: `${m.mode} ${m.difficulty}, ${m.bots} bot seat(s)${m.human ? ` + 1 human seat (AI 托管) without ${m.human.notOwned.length} operators (补位 stand-ins)` : ''}, seed ${m.seed}${m.clientCombat === false ? ', server-run combat' : ''}${m.boost ? `, LP ${m.boost.lp} and ${m.boost.layers} layers per bond from the first prep` : ''}` }));
}
/**
* The NORMAL chess the 补位 match's human seat does not own: operators its AI fields with seed 14 (瑕光 → 郁金香 from
* round 4, 异客 → Stormeye, 耀骑士临光 → 郁金香 later; the digest's `standIns` lists them per round).
*/
const MATCH_NOT_OWNED = Object.freeze([
'chess_char_3_12_a', 'chess_char_3_18_a', 'chess_char_5_20_a', 'chess_char_6_05_a', 'chess_char_6_06_a', 'chess_char_6_17_a', 'chess_char_6_19_a',
]);
const pieceStr = (p, r, c) => `${p.id.replace(/^chess_char_/, '')}@${r},${c}${p.dir && p.dir !== 'RIGHT' ? p.dir[0] : ''}${p.items && p.items.length ? `[${p.items.map((i) => i.id.replace(/^chess_item_/, '')).join('+')}]` : ''}`;
function boardStr(ps) {
const out = [];
@@ -656,7 +729,10 @@ const MATCH_ROW_COLS = Object.freeze(['alive', 'lp', 'prepFunds', 'funds', 'pend
export function runMatch(cfg) {
const sched = new VirtualScheduler();
const seats = [];
for (let i = 0; i < cfg.bots; i++) seats.push({ seat: i, playerId: `ai_${i}`, name: `AI-${i + 1}`, isBot: true, connected: true });
// a human seat (cfg.human): offline (its battles run on the server) and on AI 托管 from the start; its not-owned chess
// fight as their 补位 stand-ins
if (cfg.human) seats.push({ seat: 0, playerId: 'h_0', name: 'H-1', isBot: false, connected: false, notOwned: [...cfg.human.notOwned] });
for (let i = 0; i < cfg.bots; i++) seats.push({ seat: seats.length, playerId: `ai_${i}`, name: `AI-${i + 1}`, isBot: true, connected: true });
let summary = null;
const logged = [];
const log = { info() {}, debug() {}, warn() {}, error: (...a) => logged.push(a.map(String).join(' ').slice(0, 120)) };
@@ -665,7 +741,10 @@ export function runMatch(cfg) {
send: () => true, broadcast: () => {}, onEnd: (s) => { summary = s; },
clientCombat: cfg.clientCombat !== false, verify: 'off',
});
if (cfg.human) m.players.get('h_0').autoplay = true;
const rounds = [];
// 补位: per round at SETTLE, the human's board pieces that fought as stand-ins (chess id → stand-in charId)
const standIns = [];
let last = '';
m.start();
const setup = { stageId: m.stageId, bossId: m.bossId, hiddenBossId: m.hiddenBossId, factions: [...(m.factions || [])] };
@@ -696,6 +775,11 @@ export function runMatch(cfg) {
];
}
rounds.push(row);
if (cfg.human) {
const h = m.players.get('h_0');
const fielded = [...h.board.values()].filter((p) => p.kind === 'chess' && h.fieldsStandIn(p.id)).map((p) => `${p.id.replace(/^chess_char_/, '')}→${m.gd.standIn(p.id)?.charId}`).sort(byId);
standIns.push([m.round, fielded.join(' ')]);
}
}
}
return summary != null;
@@ -726,6 +810,7 @@ export function runMatch(cfg) {
rounds,
players,
};
if (cfg.human) digest.standIns = { notOwned: [...m.players.get('h_0').standIns], rounds: standIns };
m.dispose();
return digest;
}
@@ -733,22 +818,25 @@ export function runMatch(cfg) {
// ---------------------------------------------------------------------------------------------------------------
// families, fast subset, comparison
const GENERATORS = { roster: rosterScenarios, bonds: bondScenarios, fields: fieldScenarios, matches: matchScenarios };
const GENERATORS = { roster: rosterScenarios, bonds: bondScenarios, fields: fieldScenarios, matches: matchScenarios, standins: standInScenarios };
const ABOUT = {
roster: 'every visible chess record × every selectable skill / module, every stage, every non-leader enemy kind, items, bands, 机变 cards, map cards, placeable summons',
bonds: 'every bond at its activation threshold (layers 1) and at its top tier (layers 999)',
fields: 'Final Assault / Hidden Core leaders (pair + solo templates, shared pool, 200 s cap) and 联防 fields (1 / 2 helpers)',
matches: 'bot-only matches run to the end in virtual time (solo ×4 difficulties ×2 seeds, co-op 2/3/4, one server-run, two boosted to the Hidden Core)',
matches: 'matches run to the end in virtual time: bot-only (solo ×4 difficulties ×2 seeds, co-op 2/3/4, one server-run, two boosted to the Hidden Core) and one co-op match whose human seat fields 补位 stand-ins',
standins: 'every NORMAL chess record (normal + elite) fielded as its 补位 stand-in (all 17 stand-ins and the skills the chess name for them), 12 per battle on real stages and waves',
};
/**
* The default test subset (GOLDEN_FULL=1 runs everything): the pass-0 roster battles (every chess record with its
* default loadout, every stage, every non-leader enemy kind), every bond at its top tier, both 联防 fields, four leader
* fields (one of them a Hidden Core) and five matches (solo 标准 / 绝境, co-op 2 / 4, the boosted Hidden Core run).
* fields (one of them a Hidden Core), six matches (solo 标准 / 绝境, co-op 2 / 4, the boosted Hidden Core run, the 补位
* match) and the normal-record stand-in battles.
*/
const FAST_MATCHES = new Set(['solo-FUNNY-1', 'solo-HARD-1', 'coop2-NORMAL-3', 'coop4-ABYSS-7', 'solo-HARD-9-boosted']);
const FAST_MATCHES = new Set(['solo-FUNNY-1', 'solo-HARD-1', 'coop2-NORMAL-3', 'coop4-ABYSS-7', 'solo-HARD-9-boosted', 'coop2-NORMAL-14-standins']);
const FAST_FIELDS = new Set(['boss-boss_1-pair', 'boss-boss_4-solo', 'boss-boss_7-pair', 'hidden-boss_9-pair', 'unite-1', 'unite-2']);
export function isFast(sc) {
if (sc.family === 'matches') return FAST_MATCHES.has(sc.id);
if (sc.family === 'standins') return sc.pass === 0; // the normal records (the elites with GOLDEN_FULL)
if (sc.family === 'fields') return FAST_FIELDS.has(sc.id);
if (sc.family === 'bonds') return sc.id.endsWith('-high');
return sc.pass === 0;