Vault Breakers: Super Score — Evidence Manifest, draft-v0.2

DEV ONLY · NOT FOR PRODUCTION · NOT FOR CERTIFICATION. Binds a set of Draft results to one config and one backend commit. Not a freeze manifest; confers no lifecycle status.

Status: DRAFT MATH APPROVED 2026-08-19. Not frozen, not final, not certified, and not yet publishable to DEV — see the Freeze exit criteria below.

Supersedes manifest-draft-v0.1.md, the APPROVED unbalanced reference baseline, whose evidence is retained unchanged.

Bound identities

FieldValue
Game IDvault-breakers-super-score
Go packagevault_breakers_super_score
Math versiondraft-v0.2
Lifecycle statedraft — mutable
Canonical configlibs/mathengine/games/vault_breakers_super_score/vault-breakers-super-score.yaml
Config SHA-256fa6d0410d7de619e68b42083e7012b975d70be572a252d0dd60f3debe2ba4fc8
Backend Git SHA5d56362542460e934517e94a4b69968654b72e6b
Backend branchfeat/vault-breakers-draft-math — not merged, not deployed
RNG algorithm/versionsha256-trunc64-be/go1-alfg#1
Mechaniccount_anywhere_tumble/v1
Superseded versiondraft-v0.1, config SHA-256 9631c0afdf4ba5553ab52d86e20242b8221cd90b1e807452b8d80036f45e8933
git -C /Users/admin/kiro/backend rev-parse HEAD
shasum -a 256 /Users/admin/kiro/backend/libs/mathengine/games/vault_breakers_super_score/vault-breakers-super-score.yaml

Hashed from the bytes the binary embeds (RawYAML()); pinned by identity_test.go.

What changed from the approved baseline

draft-v0.1draft-v0.2
Free-spin reelsthe base reelsown table, charge at 7,250/50,000
Standard buy at 100×15.52%96.42%
Super buy at 500×10.94%97.05%
Whole-game RTP95.8775%95.8973%
Base regular46.9290%76.5053%
Base charges36.3648%5.0105%
Free spins8.9272%12.5570%
Super Scatter3.6565%1.8246%
Free-spins trigger1 in 169.11 in 749.7
σ per round16.112313.5313

The buys hold their reference prices because free spins are now worth buying, not because the prices were moved to fit a weak feature.

Documents

Reproduction

cd /Users/admin/kiro/backend
go test -race -count=1 ./libs/mathengine/games/vault_breakers_super_score
go test -race -count=1 ./libs/... ./apps/...
make test-parity
TEST_DATABASE_URL='postgres://game:game@127.0.0.1:5432/game_engine?sslmode=disable' \
  go test -tags integration -race -count=1 ./apps/game-engine/... ./apps/math-studio/...
go test -tags vbdraft -count=1 -v -timeout 180m \
  ./libs/mathengine/games/vault_breakers_super_score -run Draft

Results

MetricMethodValueUncertaintyTargetVerdict
Base first-snapshot regular RTPEXACT56.2502%none
Base first-snapshot hit rateEXACT19.7599%none
FS first-snapshot regular RTPEXACT22.1446%none
FS first-snapshot hit rateEXACT8.2331%none
P(4+ scatter-class), one snapshotEXACT1 in 1,014.2none
P(3+ scatter-class), one snapshotEXACT1 in 100.9none
E[instant prize | qualifying snapshot]EXACT14.210410×none
Whole-game RTPestimated95.8973%SE 0.1353 pp, CI [95.6321%, 96.1625%]96.0000%PASS at −0.76 SE
base regular-symbol RTPestimated76.5053%inherits
base multiplier contributionestimated5.0105%inherits
free spins — regular partderived0.5714%inherits
free spins — charge partderived11.9856%inherits
Super Scatter instant prizeestimated1.8246%inherits
σ per roundestimated13.531314high volatilityconsistent
Hit frequencyestimated19.8284%
Zero-return frequencyestimated80.1716%
Longest dry streakobserved78 rounds
Maximum observed awardobserved23,662.9900×sample max, not a bound
Declared maximumWITNESSED50,000×none50,000×, reachablePASS
Free-spins triggerestimated1 in 749.7
Super Scatter prizeestimated1 in 6,112.5
Retrigger frequencyestimated0.011043 per free spin
Standard buy at 100×estimated96.4173%SE 0.6320 on the award~96%PASS
Super buy at 500×estimated97.0453%SE 1.8361 on the award~96%PASS

Percentiles — estimated, 5,000,000 rounds

P50P75P90P95P99P99.9
0.0000×0.0000×2.4000×5.0800×12.6900×42.3700×

Exceedance — estimated, 100,000,000 rounds

≥1×≥10×≥100×≥500×≥1,000×
0.131008980.018148640.000516520.000049690.00001249

Seeds and sample sizes

RunSeedSizeShards
A — whole-game confirmationvault-breakers-draft-v0.1-confirmation100,000,000 rounds16 × 6,250,000
B — distribution and chargesvault-breakers-draft-v0.1-distribution5,000,000 rounds
C — feature and buysvault-breakers-draft-v0.1-feature200,000 sequences × 2 modes16 workers
Max-win witnessconstructed grid, no seed1 deterministic evaluation

The seed strings still say v0.1. That is the seed scheme, not a version claim, and it is left alone deliberately: changing it would make the two versions differ by both the config and the seeds, and only one of those is under test.

Method boundaries

  • The whole game is not enumerable; CheckEnumerable refuses this config and the refusal is asserted rather than worked around.
  • What IS exact follows from the per-cell independent draw model alone, and applies to both reel tables.
  • Exact results come from the same engine that executes the game. Internally exact; not independent verification.
  • Max win is a constructed reachability witness plus a containment argument.
  • The recorded band is the interval the confirmation run supports. ±0.10 pp needs roughly 733 million rounds at this σ.

Unresolved and out of scope

  1. A free-spin snapshot pays on 8.23% of snapshots against the base game's 19.76%, because charges occupy 14.5% of the grid, and 95%+ of a sequence's award is the charge part. A deliberate high-volatility shape, recorded rather than smoothed, and worth a product decision before this leaves Draft.
  2. No math-studio authoring preset. DrawModel needs a column in studio_reel_sets and a migration (LayoutType is a column; InitialStops is already dropped by that store), plus multiplier_charge and super_scatter as domain types with validation, build_config translation, feature-store and handler decoding, and count_anywhere in the accepted win types. Until then the pack cannot be seeded through versions/from-pack, the route scripts/seed-magic-vault.sh uses to publish to DEV.
  3. No game-engine DEV admission gate. Deliberate — publishing is out of scope until this Draft is approved.
  4. The free-spins regular/charge split is derived from Run C, not measured inside Run A.
  5. Charges collected inside free spins are not itemised; only base-game charges are.

Freeze exit criteria

Freeze is blocked until every one of these is true. They are listed so the next person does not have to reconstruct them.

  1. DEV playtest of the free-spins shape completed, with a recorded product decision. The charge-dominant shape is approved for Draft only, and no retune may precede the playtest.
  2. The math-studio publishing path task is complete, all seven parts: count_anywhere accepted as a win type; reelset fields persisted without silent loss (DrawModel needs a column and a migration, and InitialStops is already dropped by that store); multiplier_charge and super_scatter supported as domain feature configs; AtomicSimUsesVariants and BuyOnly preserved through the round trip; the exact canonical config hash fa6d0410d7de619e68b42083e7012b975d70be572a252d0dd60f3debe2ba4fc8 reproduced; versions/from-pack output proved equal to the approved backend pack; and no existing game hash changed.
  3. A game-engine DEV admission gate exists, denies by default, and pins the canonical hash — the shape magic_vault_dev_gate.go already uses.
  4. Published to DEV and exercised on the real runtime: a natural trigger opening a durable free-spin sequence, a retrigger inside it, both buys, a reconnect mid-sequence, and a replay under the same Idempotency-Key.
  5. ValidateFreezeEvidence satisfied server-side — validation clean, exact-or-simulation evidence, a max-win finding, the config hash, and ParityPassed. Assembled by the server, never asserted by the caller.
  6. Every unresolved item in this manifest closed or explicitly accepted by name.
  7. Any config change re-opens all of the above. A new hash means new evidence; nothing above carries across a config that moved.

Status

draft-v0.2. DRAFT MATH APPROVED 2026-08-19. Not frozen. Not final. Not certified. Not yet publishable to DEV.

Approved: whole-game RTP 95.8973% against a 96.00% target; Standard Buy at 100× returning 96.4173%; Super Buy at 500× returning 97.0453%; max win 50,000×; and the high-volatility, charge-dominant free-spins shape.

The free-spins shape is approved FOR DRAFT with a condition: it must pass a separate DEV playtest before Freeze. Do not smooth it and do not retune it without playtest results.

Not published. Not enabled for any operator. Internal simulation is not external certification.

AWAITING_USER_APPROVAL — DEV PUBLISHING PLAN. (granted 2026-08-20 — see the addendum below)


Addendum — 2026-08-20: DEV publishing plan approved

DEV PUBLISHING PLAN APPROVED 2026-08-20. Scope: DEV only. Not freeze, not stage, not prod.

Nothing above is rewritten. This records what changed after the draft-v0.2 approval was written.

Residual item 2 is closed

The manifest above says InitialStops "is already dropped by that store", and item 2 lists the math-studio publishing path as open. Both were true when written. They are no longer:

MigrationColumnTable
007draw_modelstudio_reelsets
008initial_stopsstudio_reelsets
009win_tiersstudio_paytables

The table is studio_reelsets, not studio_reel_sets as written above and in the Math Spec.

InitialStops was one of two silent losses, not one. The second was Paytable.WinTiers, which decides RoundOutcome.WinTier — the big/ultra/mega label a client renders. The recorded attribution in legacy_frompack_versions.json ("the feature store's JSON round-trip is lossy for free-spins variants") was wrong: a typed FreeSpinsConfig with variants, a nested reelset and initial stops survives its own JSON round trip. Both causes were found by comparing the two assemblies field by field rather than by comparing their hashes.

With both closed, book_style_v2's Postgres assembly reproduces exactly its already-recorded config_hash 5ec2df7fc62b1669431928253300b8dc5cab8735a601ab22ab153e8249699de5. The divergence closed rather than moved. The superseded value is preserved as pg_config_hash_before_store_fixes.

That hash belongs to book_style_v2. It is not a Vault Breakers identity and must never be quoted as one.

A third identity defect, found by the identity gate itself

The gate the DEV publishing plan asks for — in-memory, Postgres and published config hashes all equal — failed on its first honest run, after the field-by-field comparison had passed:

Paytable.LineDefinitions:  null (approved)  vs  []  (published)

BuildMathVersionConfig allocated the slice unconditionally, so a paytable defining no lines published an empty list of them rather than none. The same math, two canonical identities, depending on which side of the store it was read from. It had never surfaced because every prior game is a lines game with at least one line — Vault Breakers is the first count-anywhere pack and the first with zero.

Fixed by leaving the slice nil when there is nothing to convert. The in-memory value did not move; the store path was corrected to reproduce it.

Canonical identities, full values

IdentityValue
Canonical pack YAML SHA-256 (pinned by identity_test.go)fa6d0410d7de619e68b42083e7012b975d70be572a252d0dd60f3debe2ba4fc8
Canonical config hash, in memory6acdcb03a109ece7d6aad6113e9519eba72e4a8d08886c67bad04228425a9235
Canonical config hash, assembled from Postgres6acdcb03a109ece7d6aad6113e9519eba72e4a8d08886c67bad04228425a9235
Canonical config hash, published on DEVto be read after publication; must equal the above

These are two different identities and are not interchangeable. The YAML hash is the bytes the binary embeds; the config hash is the canonicalized assembled struct, and it is the one the publishing path, freeze evidence and handoff manifest refer to.

Residual item 3 is closed

The game-engine DEV admission gate exists, denies by default, pins the canonical hash, and requires ten conditions including RequireRuntimeSupport — so a binary older than the config it loaded refuses to serve rather than silently omitting a mechanic.

Freeze blockers that remain

  1. DEV playtest of the free-spins shape, with a recorded product decision. Not to be smoothed or retuned before playtest results.
  2. Live DEV exercise of the runtime.
  3. ValidateFreezeEvidence satisfied server-side.
  4. Every unresolved manifest item closed or explicitly accepted.

Criteria 2 and 3 are complete. Criterion 7 (a config change re-opens everything) stands as a rule.

The runbook for the publication itself is backend/doc/vault-breakers-dev-publication-runbook.md.

Still not frozen. Still not final. Still not certified. Internal evidence is not certification.

AWAITING_USER_APPROVAL — DEV PLAYTEST.


Addendum — 2026-08-20: published to DEV, and one mechanic does not pay there

Published as vault-breakers-super-score-v1 on the dev tier through the official versions/from-pack path. All four identities matched:

IdentityValue
canonical pack YAML SHA-256fa6d0410d7de619e68b42083e7012b975d70be572a252d0dd60f3debe2ba4fc8
config hash, in memory6acdcb03a109ece7d6aad6113e9519eba72e4a8d08886c67bad04228425a9235
config hash, assembled from Postgres6acdcb03a109ece7d6aad6113e9519eba72e4a8d08886c67bad04228425a9235
config hash, published on DEV6acdcb03a109ece7d6aad6113e9519eba72e4a8d08886c67bad04228425a9235
gate hash, what the runtime compares (canonical.Value)2453541de31352c80e482d8591aaf1fdd40e82492704571d4b2577ede12914ab

The published config is the approved config. That is not in question. What follows is about what the runtime does with it.

BLOCKING: the Super Scatter instant prize never pays on the player path

A base spin never evaluates the published config. The orchestrator strips free_spins first, because the durable runtime plays the sequence one request at a time and the atomic executor would otherwise pay it twice. SuperScatterExecutor resolves its verdict through the free-spins trigger resolution, so with free_spins removed it does not fire at all.

full configplayer config (free_spins stripped)
mock 8×H1 + 4 SUPER_SCATTER50,000×8.45×
400,000 drawn roundsprize paid 55 timespaid 0 times
DEV, mock 8×H1 + 4 SUPER_SCATTER8.45×, features: ["free_spins"]

What this does to the recorded evidence. Every figure in this manifest remains a correct statement about the approved math. It is no longer a correct statement about what the dev runtime serves:

FigureApproved (this manifest)What the runtime serves
Super Scatter contribution1.8246%0%
Whole-game RTP95.8973%94.07%
Max win50,000×, witnessedunreachable — the witness is four Super Scatters on one base-game snapshot, the exact payout that does not happen
P(win ≥ 500×), P(≥1,000×)as recordedlower; the instant prize is a large part of that tail

Nothing above is rewritten. The evidence describes the math that was approved; the gap is in the runtime, and closing it is what makes the evidence true of the product again.

This is the STOP condition the original brief named — a mechanic that works in the atomic simulator and not on the player path — and it is a Freeze blocker on its own.

Why the parity gate did not catch it

RuntimeBaseComparable() returns !HasFreeSpins(cfg), so every vector whose config carries free_spins is skipped by the runtime half of the corpus. The super_scatter_prize vector rides a config that has free spins, so its recorded FinalPayout: 50000 has never once been produced through the real orchestrator — only through the reference engine.

make test-parity was never wrong. It does not compare these. Tracked as backend TODO GE7.

Fix options, none chosen

  1. Pay it orchestrator-side, the way the free-spins trigger is already resolved against the unstripped config. Config hash unmoved, so this manifest survives intact.
  2. Make the executor independent of free_spins via ScatterClassIDs / ScatterClassMin — two fields the design named and the published config does not carry at all, which suggests this was the original intent. Moves the pack, and therefore re-opens every figure recorded here.
  3. Strip more narrowly: remove the atomic executor's payout, not the feature another executor reads.

Freeze blockers now

  1. DEV playtest of the free-spins shape — still open, and now qualified: playtest data on pacing and free-spins shape is valid; data on how big wins feel is not, because the largest payouts are the ones not being paid.
  2. Live DEV exercise of the runtime — partially done, stopped on the finding above. Ten checks pass: the count-anywhere boundary at 7 → 8, the paytable bands at 8 and 12, multi-step tumbles, the mixed scatter-class trigger to 15 free spins, Super Scatter paying nothing below class 4, one debit per spin, and publishing-alone-enables-nothing.
  3. ValidateFreezeEvidence — open.
  4. Unresolved manifest items — open, and this addendum adds one.
  5. NEW: the Super Scatter runtime gap must be closed and re-verified on DEV.

Still Draft. Still not frozen, not final, not certified. Internal evidence is not certification.

AWAITING_USER_APPROVAL — SUPER SCATTER RUNTIME FIX.


Addendum — 2026-08-22: the Super Scatter runtime gap is closed in the backend

The blocker recorded in the previous addendum is fixed. DEV re-verification is NOT done — blocker 7 asked for the gap to be closed and re-verified on DEV, and only the first half of that is complete.

Backend documentation: backend/doc/vault-breakers-super-scatter-runtime-fix.md.

What was done

Fix option 1, the one this manifest listed as leaving the config hash unmoved. The verdict is resolved orchestrator-side against the unstripped config, the same way the free-spins trigger already was, through the ENGINE's own resolveScatterTrigger over the per-snapshot census the round already records. No second rule is written anywhere, and stripFreeSpinsFromCfg is untouched.

Option 2 — ScatterClassIDs / ScatterClassMin on the executor — was not taken, for the reason stated here when the options were listed: it moves the pack and re-opens every figure below.

Identity, re-checked

IdentityValueMoved?
canonical pack YAML SHA-256fa6d0410d7de619e68b42083e7012b975d70be572a252d0dd60f3debe2ba4fc8no
gate hash (canonical.Value)2453541de31352c80e482d8591aaf1fdd40e82492704571d4b2577ede12914abno

Both re-computed on the commit before the change and after it, and now pinned by a test (libs/mathengine/canonical/pack_hashes_test.go) alongside the other four shipped packs. The change adds fields to RoundOutcome, which is a result; canonical.Value hashes MathVersionConfig, which is untouched.

What this restores

Every figure in this manifest is a correct statement about the approved math, and was never rewritten. It is now also a correct statement about what the runtime serves:

FigureApprovedRuntime beforeRuntime now
Super Scatter contribution1.8246%0%1.8246%
Whole-game RTP95.8973%≈ 94.07%95.8973%
Max win 50,000×witnessedunreachablereachable

Awards graded against drawn seeds, not hand-placed grids: 1 Super → 100×, 2 → 500×, 3 → 5,000×, and 4, 5 and 6 → the banded 4+ entry of 50,000×, which settles at the ceiling. A sequence opened with no Super Scatter on the triggering snapshot pays nothing, and inside free spins the award stays out (BaseGameOnly).

GE7, the parity gap

Also closed. RuntimeScatterComparable() grades the scatter verdict and the award on free-spins vectors whose totals still are not comparable, so super_scatter_prize's recorded InstantPrizeX: 100 is now produced through the real orchestrator for the first time. The gate was verified non-vacuous by suppressing the award and watching both vectors fail.

Freeze blockers now

  1. DEV playtest of the free-spins shape — still open, and no longer qualified: big-win data is now worth collecting, because the largest payouts are being paid.
  2. Live DEV exercise of the runtime — still open, and must be re-run in full against the fix.
  3. ValidateFreezeEvidence — open.
  4. Unresolved manifest items — open.
  5. Super Scatter runtime gap — closed in the backend, DEV re-verification outstanding.

Still Draft. Still not frozen, not final, not certified. Internal evidence is not certification.