diff --git a/test/golden-diy.test.js b/test/golden-diy.test.js new file mode 100644 index 00000000..55bfafcd --- /dev/null +++ b/test/golden-diy.test.js @@ -0,0 +1,32 @@ +// The 自选 golden match covers what it claims (test/golden/README.md, tools/golden.mjs — 0.2.0 自选编队): reads the stored +// digest only (test/golden.test.js recomputes it). Its human seat (AI 托管) slots 推进之王 and prototypes; its shop draws +// its 自选 pieces (and only from the slots' 调度中心 levels), its AI fields 推进之王 — the operator kit — in battle, the +// stock ends where the held copies leave it, and the match ends without errors. +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 MATCHES = read('./golden/matches.json'); +const BACKUPS = read('../data/backups.json'); + +test('matches: the 自选 match\'s human seat draws its 自选 pieces in its shop and fields 推进之王 in battle', () => { + const [id, dg] = Object.entries(MATCHES.scenarios).find(([k]) => /-diy$/.test(k)) || []; + assert.ok(dg, 'scenario present'); + assert.deepEqual(dg.errors, { engine: 0, logged: 0, sim: 0, dispatcher: 0 }, id); + assert.deepEqual(Object.keys(dg.diy.picks).sort(), Object.keys(BACKUPS.diy.slots).sort(), 'all four slots filled'); + assert.equal(dg.diy.picks.chess_char_5_diy1_a.charId, 'char_112_siege'); + const draws = dg.diy.rounds.flatMap(([, d]) => (d ? d.split(' ') : [])); + assert.ok(draws.length >= 2, `shop draws: ${draws.join(', ')}`); + for (const d of draws) assert.ok(Object.hasOwn(BACKUPS.diy.slots, `chess_char_${d}`), d); + const fielded = dg.diy.rounds.filter(([, , f]) => f).flatMap(([r, , f]) => f.split(' ').map((x) => `${r}:${x}`)); + assert.ok(fielded.some((x) => x.endsWith('5_diy1_a→char_112_siege')), `fielded: ${fielded.join(', ')}`); + for (const [r, , f] of dg.diy.rounds) { + for (const x of f ? f.split(' ') : []) { + const [slot, charId] = x.split('→'); + assert.equal(dg.diy.picks[`chess_char_${slot}`].charId, charId, `round ${r}: ${x} fights as its pick`); + } + } + // the stock: never above its cap (8 at tier 5, 5 at tier 6) + for (const [slot, left] of Object.entries(dg.diy.stock)) assert.ok(left >= 0 && left <= (BACKUPS.diy.slots[slot].tier === 5 ? 8 : 5), slot); +}); diff --git a/test/golden/README.md b/test/golden/README.md index 6e070f89..47246ed8 100644 --- a/test/golden/README.md +++ b/test/golden/README.md @@ -17,7 +17,7 @@ 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` | 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`) | +| `matches` | 18 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), 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`) — and one whose human seat slots 自选 picks (推进之王 and prototypes) with its 调度中心 at level 5 from the first prep: its own shop draws them and its AI fields 推进之王 (its digest lists per round the 自选 shop draws and the fielded pieces, `diy`) | | `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 | | `diy` | 6 battles of 自选 pieces (a DIY slot with its `diy` pick, `shared/diy.js`): every owned 6★ with an operator kit (`kits/index.js OPERATOR_KITS` — 推进之王 so far) in each form of tiers 5 and 6 (normal; elite with no module and with each module) under each skill, then every prototype pick with a kit at its locked selection, normal and elite, 12 per battle against the round's real wave; a new operator kit adds its scenarios | @@ -31,8 +31,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, six matches, the five stand-in -battles of the normal records, the first 自选 battle of the operator kits and of the prototypes — 61 of the 150). `GOLDEN_FULL=1` checks everything. +every stage and every non-leader enemy kind, every bond at its top tier, six fields, seven matches, the five stand-in +battles of the normal records, the first 自选 battle of the operator kits and of the prototypes — 62 of the 151). `GOLDEN_FULL=1` checks everything. ## Workflow diff --git a/test/golden/matches.json b/test/golden/matches.json index 7a21806c..18744e06 100644 --- a/test/golden/matches.json +++ b/test/golden/matches.json @@ -1611,6 +1611,130 @@ [13,"3_12_a→char_608_acpion 6_05_a→char_611_acnipe 6_17_a→char_608_acpion"] ] } + }, + "coop2-NORMAL-70-diy": { + "about": "coop NORMAL, 1 bot seat(s) + 1 human seat (AI 托管) with 4 自选 picks and its 调度中心 at level 5 from the first prep, seed 70", + "fast": true, + "mode": "coop", + "difficulty": "NORMAL", + "seats": 1, + "seed": 70, + "combat": "client", + "boost": null, + "setup": {"stageId":"act1autochess_m03","bossId":"boss_7","hiddenBossId":"boss_10","factions":["ELEMENT","INVISIBLE","REFLECTION"]}, + "end": {"victory":true,"roundsPassed":14,"hiddenReached":false,"hiddenCleared":false,"reason":"victory","teamLp":55,"bossPoolLeft":0,"virtualMs":122900}, + "rng": {"setup":25,"shop":461,"waves":0,"draft":34,"bots":16433,"meta":20}, + "errors": {"engine":0,"logged":0,"sim":0,"dispatcher":0}, + "rounds": [ + {"round":1,"h_0":[1,27,4,0,0,5,1,"3e4697a0",0,"",5,5,0,3],"ai_0":[1,28,4,0,0,1,2,"a1734bc6",0,"sargonShip 0/2, soloShip 1/0",5,5,0,4]}, + {"round":2,"h_0":[1,27,5,0,0,5,3,"ba528c80",0,"",8,8,0,8],"ai_0":[1,28,5,0,0,1,4,"f66f48b3",0,"sargonShip 0/2, soloShip 1/0",8,8,0,9]}, + {"round":3,"h_0":[1,27,6,0,0,5,4,"43d0e2bf",0,"",7,7,0,13],"ai_0":[1,28,6,0,0,1,7,"0d31196a",0,"sargonShip 0/2, soloShip 1/0, yanShip 0/1",7,7,0,15]}, + { + "round": 4, + "h_0": [1,27,7,0,0,5,6,"2d3d3501",10,"preciShip 1/2, sargonShip 1/8",20,20,0,20], + "ai_0": [1,28,7,0,0,2,8,"bf509782",0,"sargonShip 0/2, soloShip 1/0, yanShip 0/2",20,20,0,22] + }, + { + "round": 5, + "h_0": [1,27,8,0,0,5,8,"1a62176b",31,"indomShip 1/0, preciShip 0/2, sargonShip 1/29, steadShip 1/1, victoriaShip 1/1",27,27,0,28], + "ai_0": [1,28,8,0,1,2,8,"e0bfc937",0,"sargonShip 0/2, skillfulShip 1/0, soloShip 1/0, suntShip 1/0, yanShip 0/6",27,27,0,29] + }, + { + "round": 6, + "h_0": [1,27,9,0,1,5,8,"7e9969de",84,"arcaneShip 0/2, indomShip 1/4, preciShip 1/15, sargonShip 1/49, steadShip 1/6, victoriaShip 1/6, visiShip 1/4",22,22,0,38], + "ai_0": [1,28,10,0,1,3,8,"ea720e1b",0,"sargonShip 0/4, skillfulShip 1/0, soloShip 1/0, suntShip 1/0, yanShip 0/6",23,24,1,39] + }, + { + "round": 7, + "h_0": [1,27,11,0,1,5,8,"f502b39b",103,"arcaneShip 0/2, indomShip 1/4, preciShip 1/21, sargonShip 1/60, steadShip 1/11, victoriaShip 1/7, visiShip 0/4",26,26,0,50], + "ai_0": [1,28,11,0,1,4,8,"7f20e163",0,"sargonShip 0/4, skillfulShip 1/0, soloShip 1/0, suntShip 1/0, yanShip 0/6",28,28,0,49] + }, + { + "round": 8, + "h_0": [1,27,12,0,0,5,8,"b6d1cc82",197,"arcaneShip 0/2, indomShip 1/16, preciShip 2/55, sargonShip 1/94, soloShip 1/0, steadShip 2/32, victoriaShip 0/19, visiShip 0/4",18,18,0,63], + "ai_0": [1,28,12,0,1,4,8,"854ef8a8",16,"miraShip 1/0, sargonShip 0/4, skillfulShip 1/3, soloShip 1/0, steadShip 1/0, suntShip 1/0, yanShip 1/13",17,18,1,61] + }, + { + "round": 9, + "h_0": [1,27,12,0,2,5,8,"6405e223",267,"arcaneShip 0/2, indomShip 1/16, preciShip 2/81, sargonShip 1/125, steadShip 1/37, victoriaShip 0/19, visiShip 1/8",23,23,0,77], + "ai_0": [1,28,13,0,2,4,8,"1875f830",30,"sargonShip 0/4, siracusaShip 1/8, skillfulShip 1/3, soloShip 1/0, suntShip 1/0, yanShip 1/19",14,23,9,74] + }, + { + "round": 10, + "h_0": [1,27,16,0,2,5,8,"c899fa07",312,"arcaneShip 0/2, indomShip 1/16, preciShip 2/97, sargonShip 1/133, soloShip 1/0, steadShip 2/46, suntShip 1/0, victoriaShip 0/19, visiShip 1/20",31,31,0,91], + "ai_0": [1,28,14,0,0,5,8,"0d283e5f",35,"sargonShip 0/4, siracusaShip 1/8, skillfulShip 1/6, soloShip 1/0, suntShip 2/0, yanShip 1/21",31,31,0,87] + }, + { + "round": 11, + "h_0": [1,27,14,0,2,5,8,"7c42dc27",357,"arcaneShip 0/2, indomShip 1/16, preciShip 2/109, sargonShip 1/137, skillfulShip 1/8, steadShip 1/51, suntShip 1/0, victoriaShip 0/19, visiShip 1/36",19,19,0,105], + "ai_0": [1,28,12,0,0,5,8,"fe5ac1a4",53,"sargonShip 0/4, siracusaShip 1/12, skillfulShip 1/12, soloShip 1/0, suntShip 2/0, visiShip 1/0, yanShip 1/29",19,19,0,99] + }, + { + "round": 12, + "h_0": [1,27,16,0,2,6,8,"3cdb76eb",388,"arcaneShip 0/2, indomShip 2/16, preciShip 2/141, sargonShip 1/179, skillfulShip 0/8, steadShip 0/51, suntShip 1/0, victoriaShip 0/19, visiShip 1/52",33,33,0,122], + "ai_0": [1,28,12,0,0,6,8,"547fcf92",67,"sargonShip 0/8, siracusaShip 1/16, skillfulShip 1/18, soloShip 1/0, suntShip 2/0, yanShip 1/33",15,33,18,112] + }, + { + "round": 13, + "h_0": [1,27,16,0,0,6,8,"d0f5bd71",508,"arcaneShip 0/2, indomShip 1/16, preciShip 2/173, sargonShip 1/239, skillfulShip 0/8, steadShip 0/51, suntShip 1/0, swiftShip 1/12, victoriaShip 0/19, visiShip 1/68",50,50,0,138], + "ai_0": [1,28,12,0,0,6,8,"d2ff3200",90,"sargonShip 0/8, siracusaShip 1/24, skillfulShip 1/24, soloShip 1/0, suntShip 2/0, victoriaShip 0/5, yanShip 1/42",31,50,19,125] + } + ], + "players": { + "h_0": { + "alive": true, + "victory": true, + "roundsPassed": 14, + "eliminatedRound": null, + "lp": 27, + "bandId": "band_yu", + "title": {"id":"comment_1","name":"卫戍之星","picId":"comment_icon_1","text":"对敌方领袖造成伤害最高(仅胜利时)"}, + "trophies": 4, + "reward": 400, + "stats": {"activatedLayers":534,"bossDamage":572474,"dmgDealt":4854054,"gold":154,"itemsEquipped":23,"kills":351,"leaks":0,"lpLost":0,"merges":4,"perfectRounds":13,"refreshes":23}, + "lineup": ["5_21_b@12,8[3_07_e_b+1_05_e_a]","5_22_a@11,7","4_20_b@11,8","5_02_a@10,5[5_01_e_a+6_01_e_a]","6_05_a@10,6[2_01_e_a+4_08_e_a]","6_06_a@10,8[1_01_e_a+3_04_e_a]","4_07_b@9,8[5_03_e_a+4_11_e_b]","4_23_b@9,9[4_04_e_a]"], + "bonds": ["arcaneShip 0/2","indomShip 1/16","preciShip 2/173","sargonShip 1/239","skillfulShip 0/8","steadShip 0/51","suntShip 1/0","swiftShip 1/22","victoriaShip 0/19","visiShip 1/84"] + }, + "ai_0": { + "alive": true, + "victory": true, + "roundsPassed": 14, + "eliminatedRound": null, + "lp": 28, + "bandId": "band_mlyss", + "title": {"id":"comment_3","name":"坚若磐石","picId":"comment_icon_3","text":"目标生命值损失最少"}, + "trophies": 4, + "reward": 400, + "stats": {"activatedLayers":113,"bossDamage":215026,"dmgDealt":2608447,"gold":137,"itemsEquipped":20,"kills":245,"leaks":48,"lpLost":0,"merges":6,"perfectRounds":8,"refreshes":13}, + "lineup": ["6_05_a@12,8[3_07_e_a+6_09_e_a]","2_04_b@11,7","1_07_b@11,8","6_11_a@10,6","3_03_b@10,8[3_11_e_b+2_04_e_a]","5_02_a@9,7[1_02_e_b]","1_08_b@9,8[4_06_e_a+1_05_e_a]","3_02_b@9,9[4_10_e_a+4_11_e_a]"], + "bonds": ["maniShip 1/0","preciShip 1/0","sargonShip 1/8","siracusaShip 1/24","skillfulShip 1/30","soloShip 1/0","suntShip 2/0","victoriaShip 0/10","yanShip 1/51"] + } + }, + "diy": { + "picks": { + "chess_char_5_diy1_a": {"charId":"char_112_siege","skillIndex":2,"uniEquipId":"uniequip_002_siege"}, + "chess_char_5_diy2_a": {"charId":"char_609_acguad","skillIndex":2,"uniEquipId":"uniequip_002_acguad"}, + "chess_char_6_diy1_a": {"charId":"char_609_acguad","skillIndex":2,"uniEquipId":"uniequip_002_acguad"}, + "chess_char_6_diy2_a": {"charId":"char_617_sharp2","skillIndex":0,"uniEquipId":"uniequip_002_sharp2"} + }, + "banned": [], + "rounds": [ + [1,"",""], + [2,"",""], + [3,"5_diy1_a","5_diy1_a→char_112_siege"], + [4,"","5_diy1_a→char_112_siege"], + [5,"","5_diy1_a→char_112_siege"], + [6,"","5_diy1_a→char_112_siege"], + [7,"5_diy2_a","5_diy1_a→char_112_siege"], + [8,"",""], + [9,"",""], + [10,"",""], + [11,"",""], + [12,"",""], + [13,"",""] + ], + "stock": {"chess_char_5_diy1_a":8,"chess_char_5_diy2_a":8,"chess_char_6_diy1_a":5,"chess_char_6_diy2_a":5} + } } } } diff --git a/tools/golden.mjs b/tools/golden.mjs index 52075050..921f4c31 100644 --- a/tools/golden.mjs +++ b/tools/golden.mjs @@ -25,10 +25,12 @@ // 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 17 matches run to the end in virtual time with the match's default bot rehearsal: 16 bot-only (solo 标准 / +// matches 18 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) +// first prep so they reach the Hidden Core), 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) — and +// one whose human seat slots 自选 picks (推进之王 and prototypes; its 调度中心 at level 5 from the first prep): +// its shop sells them and its AI fields them (0.2.0 自选编队, the digest's `diy`) // 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 @@ -713,9 +715,29 @@ export function matchScenarios() { out.push({ id: 'coop2-ABYSS-10-boosted', mode: 'coop', difficulty: 'ABYSS', bots: 2, seed: 10, boost: { lp: 400, layers: 300 } }); // 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` : ''}` })); + // 0.2.0 自选编队: a human seat (AI 托管, offline) that slots 推进之王 and prototypes into the four DIY slots + // (its 调度中心 starts at level 5, like the boosted runs' raised LP: the 自选 pieces are sold from level 5 / 6, which the + // AI otherwise reaches only with a full board, where a single new operator rarely improves its lineup) + out.push({ id: `coop2-NORMAL-${MATCH_DIY_SEED}-diy`, mode: 'coop', difficulty: 'NORMAL', bots: 1, seed: MATCH_DIY_SEED, human: { diy: MATCH_DIY, shopLevel: 5 } }); + const humanAbout = (h) => (h.notOwned ? ` + 1 human seat (AI 托管) without ${h.notOwned.length} operators (补位 stand-ins)` + : ` + 1 human seat (AI 托管) with ${Object.keys(h.diy || {}).length} 自选 picks${h.shopLevel ? ` and its 调度中心 at level ${h.shopLevel} from the first prep` : ''}`); + return out.map((m) => ({ ...m, family: 'matches', kind: 'match', about: `${m.mode} ${m.difficulty}, ${m.bots} bot seat(s)${m.human ? humanAbout(m.human) : ''}, 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 自选 match's human seat (0.2.0 自选编队, shared/diy.js): 推进之王 (an owned 6★ with its operator kit; S3 and SOL-X) in a + * tier-5 slot, Sharp (a prototype: its locked S3 + SOL-X) in the other tier-5 slot and a tier-6 one, 领主·Sharp in the last + * — its shop sells them from 调度中心 level 5 / 6 and its AI fields them (the digest's `diy` lists the shop draws and the + * fielded pieces per round). + */ +const MATCH_DIY = Object.freeze({ + chess_char_5_diy1_a: { charId: 'char_112_siege', skillIndex: 2, uniEquipId: 'uniequip_002_siege' }, + chess_char_5_diy2_a: { charId: 'char_609_acguad' }, + chess_char_6_diy1_a: { charId: 'char_609_acguad' }, + chess_char_6_diy2_a: { charId: 'char_617_sharp2' }, +}); +const MATCH_DIY_SEED = 70; + /** * 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). @@ -737,8 +759,14 @@ export function runMatch(cfg) { const sched = new VirtualScheduler(); const seats = []; // 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] }); + // fight as their 补位 stand-ins, its 自选 picks join its shop + if (cfg.human) { + seats.push({ + seat: 0, playerId: 'h_0', name: 'H-1', isBot: false, connected: false, + ...(cfg.human.notOwned ? { notOwned: [...cfg.human.notOwned] } : null), + ...(cfg.human.diy ? { diy: JSON.parse(JSON.stringify(cfg.human.diy)) } : null), + }); + } 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 = []; @@ -752,10 +780,24 @@ export function runMatch(cfg) { const rounds = []; // 补位: per round at SETTLE, the human's board pieces that fought as stand-ins (chess id → stand-in charId) const standIns = []; + // 自选编队: per round, the human's shop draws of its 自选 pieces and, at SETTLE, its board pieces that fought as their + // operators (slot id → charId) — an instance wrapper that only observes (as server/match/audit.js does) + const diyRounds = []; + const diyRolls = new Map(); + if (cfg.human?.diy) { + const h = m.players.get('h_0'); + const roll = h._rollChessSlot.bind(h); + h._rollChessSlot = () => { + const slot = roll(); + if (slot && h.diyPickOf(slot.id)) diyRolls.set(m.round, [...(diyRolls.get(m.round) || []), slot.id.replace(/^chess_char_/, '')]); + return slot; + }; + } let last = ''; m.start(); const setup = { stageId: m.stageId, bossId: m.bossId, hiddenBossId: m.hiddenBossId, factions: [...(m.factions || [])] }; let boosted = !cfg.boost; + let levelled = !cfg.human?.shopLevel; const prepFunds = {}; sched.runUntil(() => { const key = `${m.phase}:${m.round}`; @@ -769,6 +811,14 @@ export function runMatch(cfg) { ps.recompute(); } } + // the human's 调度中心 raised at the first prep (its shop rerolled there; the next level's price follows) + if (!levelled && m.phase === 'PREP') { + levelled = true; + const h = m.players.get('h_0'); + h.shop.level = cfg.human.shopLevel; + h.shop.upgradePrice = m.gd.upgradeBase(h.shop.level) ?? 0; + h.rollShop({ keepFrozen: false }); + } // funds when the prep opens (income in, nothing bought yet: the bots play their prep in later callbacks) if (m.phase === 'PREP') for (const ps of m.order) prepFunds[ps.playerId] = ps.funds; if (m.phase === 'SETTLE') { @@ -782,11 +832,16 @@ export function runMatch(cfg) { ]; } rounds.push(row); - if (cfg.human) { + if (cfg.human?.notOwned) { 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(' ')]); } + if (cfg.human?.diy) { + const h = m.players.get('h_0'); + const fielded = [...h.board.values()].filter((p) => p.kind === 'chess' && h.diyPickOf(p.id)).map((p) => `${p.id.replace(/^chess_char_/, '')}→${h.diyPickOf(p.id).charId}`).sort(byId); + diyRounds.push([m.round, (diyRolls.get(m.round) || []).sort(byId).join(' '), fielded.join(' ')]); + } } } return summary != null; @@ -817,7 +872,13 @@ export function runMatch(cfg) { rounds, players, }; - if (cfg.human) digest.standIns = { notOwned: [...m.players.get('h_0').standIns], rounds: standIns }; + if (cfg.human?.notOwned) digest.standIns = { notOwned: [...m.players.get('h_0').standIns], rounds: standIns }; + if (cfg.human?.diy) { + const h = m.players.get('h_0'); + // [round, this round's shop draws of its 自选 pieces (slot ids), the 自选 pieces fielded at SETTLE (slot → operator)]; + // the stock left at the end + digest.diy = { picks: JSON.parse(JSON.stringify(h.diy)), banned: [...h.diyBanned], rounds: diyRounds, stock: h.diyStock.snapshot() }; + } m.dispose(); return digest; } @@ -961,7 +1022,7 @@ const ABOUT = { * 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', 'coop2-NORMAL-14-standins']); +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', `coop2-NORMAL-${MATCH_DIY_SEED}-diy`]); 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);