From 29725e3a3995d0525a4a5dd5ecb42ccc04b3aae9 Mon Sep 17 00:00:00 2001 From: sganggs Date: Mon, 5 Oct 2026 19:42:49 +0800 Subject: [PATCH] =?UTF-8?q?feedback5=20WG:=20golden=20=E2=80=94=20a=20stan?= =?UTF-8?q?dins=20family=20(10=20battles:=20every=20NORMAL=20chess=20recor?= =?UTF-8?q?d,=20normal=20and=20elite,=20fielded=20as=20its=20=E8=A1=A5?= =?UTF-8?q?=E4=BD=8D=20stand-in=20on=20real=20stages=20and=20waves=20plus?= =?UTF-8?q?=20ground=20enemies;=20all=2017=20stand-ins,=20every=20active?= =?UTF-8?q?=20backup=20skill=20cast)=20and=20one=20co-op=20match=20whose?= =?UTF-8?q?=20AI-=E6=89=98=E7=AE=A1=20human=20seat=20does=20not=20own=207?= =?UTF-8?q?=20chess=20(coop2-NORMAL-14-standins);=20the=20133=20existing?= =?UTF-8?q?=20digests=20unchanged=20(roster=20/=20bonds=20/=20fields=20byt?= =?UTF-8?q?e-identical,=20the=2016=20old=20matches=20identical)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/golden-standins.test.js | 66 ++++ test/golden/README.md | 8 +- test/golden/matches.json | 131 +++++++- test/golden/standins.json | 578 +++++++++++++++++++++++++++++++++++ tools/golden.mjs | 118 ++++++- 5 files changed, 882 insertions(+), 19 deletions(-) create mode 100644 test/golden-standins.test.js create mode 100644 test/golden/standins.json diff --git a/test/golden-standins.test.js b/test/golden-standins.test.js new file mode 100644 index 00000000..28dea690 --- /dev/null +++ b/test/golden-standins.test.js @@ -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); + } +}); diff --git a/test/golden/README.md b/test/golden/README.md index 8c6b35d8..c5514de8 100644 --- a/test/golden/README.md +++ b/test/golden/README.md @@ -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/.json` hold the digests (`roster`, `bonds`, `fields`, `matches`). +- `test/golden/.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 diff --git a/test/golden/matches.json b/test/golden/matches.json index 7c5b455f..e22616a1 100644 --- a/test/golden/matches.json +++ b/test/golden/matches.json @@ -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"] + ] + } } } } diff --git a/test/golden/standins.json b/test/golden/standins.json new file mode 100644 index 00000000..d637788f --- /dev/null +++ b/test/golden/standins.json @@ -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" + } + } +} diff --git a/tools/golden.mjs b/tools/golden.mjs index 20081225..fecfe60f 100644 --- a/tools/golden.mjs +++ b/tools/golden.mjs @@ -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 +// options: --family roster,bonds,fields,matches,standins --fast (the test's default subset) --only // --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;