- op-shwaz.js 黑: 破甲箭头 (one roll per attack, raised by S1 / S2 / S3; ×atk_scale, then defDown on a landed hit; numbers from the base talent + ARC-Y's named change — the data folds ARC-Y's hidden trait talent into talent 0 at stage 1), 交叉火力 (field aura; ARC-X stage 3 "携带": her player's snipers for the battle), ARC-Y 正前方 ×1.05 undodgeable, S3 3-2 + interval +0.4; triggers from the data (S3 SKILL_RANGE — reported). - op-orchd2.js 焰狐龙梓兰: 三连击 normal attack cut to 33.3 % after DEF (PRTS 特性备注), 强击瓶专家 stacks per round, 翔虫机动 (respawn −15 / −18 s, ATK +15 / 20 % for 30 s on a redeploy at her mark), S1 刚射 / 刚连射 (charges, refund without a target, stun per arrow), S2 飞翔瞪射 (free cast at every deployment, 起飞, three volleys on the 4-4, landing on the 1-3, timed by the official skeleton), S3 龙之箭 (3 s charge, piercing arrow at 10 tiles/s, strikes every 0.25 tile, push). - op-bgsnow.js 鸿雪: S1 toggle + proc, S2 点题 (立即, 普通 + 2 × 持续伤害), S3 3-1 with 正前方3格 scale, ARC-Y as 黑's; her placed “打字机” pieces get their kit at her install (25 s life, 40 s redeploy × S2's 65 / 60 %, 5 DP; the skill copies; 弱点速记 DEF cut 18 / 23 %, ARC-X stage 3 28 / 33 % with 八格). - kits/index.js OPERATOR_KIT_FILES: one line per file. Tests: test/content/op_shwaz / op_orchd2 / op_bgsnow (23). - Golden: only the diy family moved — diy-003 … diy-006 now hold the 66 new 自选 configurations (黑, 焰狐龙梓兰, 鸿雪) in place of the prototype picks, diy-007 … diy-012 are new (the prototype battles moved down); every other family is byte-identical. test/golden/README.md: 12 diy battles, 61 of 156.
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`).
|