- op-chen.js (char_010_chen, 剑豪): S1 鞘击 one sheath strike (her Skill clip: one OnAttack) + stun; S2 赤霄·拔刀 up to max_target enemies of 3-12, flyers too, arts then physical (PRTS 备注); S3 赤霄·绝影 10 slashes on the nearest ground enemy of x-1 at the Skill_3 clip's OnAttack times, the last stuns, 2.933 s of 无敌 / 无法阻挡 / stun and freeze immunity, ends at once with no target before the 10th (PRTS 备注), not opened with only flyers in range [ASSUMED]; 呵斥 per-deployment timer +1 attack / hurt SP to the field's allies (SWO-X 3: every 3 s, +1 more for her); 持刀格斗术; SWO-X x1.1 during her skill (PRTS 修正 技能期间), SWO-Y ignores 70 DEF. - op-irene.js (char_4009_irene, 剑豪): 审判之火 as PRTS 备注 (a 物理穿透 charge: air units always and without a draw, 浮空 enemies count, ground at 50 %, spent only by a landing instance); SWO-X 3: 55 % and +6 SP after a skill that knocked out an air unit; 净化之剑 doubled while a 【海怪】 is on the field; S1 起风 strike + 浮空 + second strike; S2 裂潮 2 charges, ground only, 重量 <= 3 float, not opened with only flyers in range [ASSUMED]; S3 判决 3.5 s (PRTS): opening strike + 浮空, 10 bombardments every 0.3 s, radius 1.1, air too; SWO-X x1.1 skill damage, SWO-Y 70 DEF, ISW-A attributes only (its talent parts are 集成战略-only). - op-helage.js (char_188_helage, 武者): S1 新月 two hits, S2 弦月 二连击 + 75 % dodge, S3 满月 range +1 / 3 targets; the 武者 heal per damage instance (the kit adds one per further hit of an attack); 坚忍 ASPD (SBL-X 3: +130 at 50 %); 运筹帷幄 生命回复速度 while not blocking [ASSUMED: no PRTS 备注], SBL-Y 3 also below 30 % HP (80 / s, never both); SBL-X 庇护 25 % below 50 % HP; SBL-Y once per deployment back to 30 % HP. - kits/index.js OPERATOR_KIT_FILES: the three files appended. Tests: op_chen / op_irene / op_helage (10 each, every form x module x skill, numbers from data/backups.json). - Golden: only the diy family moved — the packing now fields the new kits: diy-003 … diy-006 (the prototypes before) and the new diy-007 … diy-009 field 陈 / 艾丽妮 / 赫拉格 in every form x module x skill, the prototypes moved to the new diy-010 … diy-013; diy-001 / diy-002 (推进之王) and roster / bonds / fields / matches / standins unchanged. test/golden/README.md: the diy row no longer counts battles or names 推进之王 alone.
61 lines
5.8 KiB
Markdown
61 lines
5.8 KiB
Markdown
# Golden results
|
||
|
||
The simulation is deterministic: the same seed and inputs give the same battle and the same match, bit for bit. These
|
||
files record the exact outcome of a fixed corpus of seeded scenarios, so a change that alters gameplay — even one meant
|
||
to be a pure refactor that only reorders random draws, hook order or iteration order — fails loudly instead of slipping
|
||
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`, `standins`, `diy`).
|
||
- `test/golden.test.js` recomputes them and compares. On a mismatch it prints the scenario, the field and old → new.
|
||
|
||
## The corpus
|
||
|
||
| family | what runs |
|
||
|---|---|
|
||
| `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`) |
|
||
| `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` | 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`) 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 (the battle count grows with the kits) |
|
||
|
||
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
|
||
test-harness default never moves a digest.
|
||
|
||
What a digest ignores on purpose (it does not affect gameplay): object key order, engine unit ids (snapshot ids are
|
||
renumbered by first appearance, units are named by board uid or def id), board piece uids, the order of client events
|
||
within a tick (they are counted, not hashed) and wall-clock time. Damage / healing sums are rounded to integers and HP
|
||
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.
|
||
|
||
## Workflow
|
||
|
||
```sh
|
||
GOLDEN_FULL=1 node --test test/golden.test.js # the whole corpus (≈ 20 s with 4 worker threads)
|
||
node --test test/golden.test.js # the fast subset
|
||
npm run golden # the same comparison from the tool (all families, a short report)
|
||
npm run golden:update # recompute and rewrite test/golden/*.json
|
||
node tools/golden.mjs --twice # determinism: the corpus twice in one process, the second pass reversed
|
||
```
|
||
|
||
- **A refactor commit must never change these files.** Run `GOLDEN_FULL=1 node --test test/golden.test.js` before
|
||
committing it. If a digest moves, the refactor changed behaviour: find out why (the first differing value of `snaps`
|
||
says when two runs parted, in 10-game-second steps) instead of updating the files.
|
||
- **An intended gameplay change** (a fix, new content, a data rebuild): run `npm run golden:update` and commit the
|
||
changed files together with the change, saying in the commit message which scenarios moved and why.
|
||
- **Reviewing the diff:** every scenario is a block of its own and every unit / enemy / round is one line, so a diff
|
||
shows which scenarios moved and which values changed. A change confined to the scenarios that contain the touched
|
||
operator, enemy or rule is expected; a change everywhere (every `rngDraws`, every `snaps`) means something global
|
||
moved — the RNG draw order, the tick order, a shared rule.
|
||
- **Adding to the corpus** (a new family, more scenarios, a new digest field) changes the files by design: do it in a
|
||
commit of its own, never together with a refactor.
|
||
- A new engine hook, event or chess record does not change the digests of the existing scenarios by itself — the
|
||
hook list is frozen in the tool and the corpus is regenerated only on `golden:update` — but new data records do join
|
||
the corpus there (the roster family enumerates `data/chess.json`).
|