Mechanic Math — hold-and-win — Final v1.0

Immutable. This is the snapshot approved at the FREEZE & HANDOFF gate on 2026-08-09. It is a verbatim copy of draft-v0.2 — every number, band, method and limitation below is the one that was approved, and no mathematics was changed in the making of this file. Only the front matter, this note and the closing status section differ from the draft.

Never edit this file. Any payout-affecting change starts a new Draft and eventually a new Final version; a Final that is edited in place is a Final that nobody can bind evidence to.

Source Draft: hold-and-win-math-draft-v0.2.md, preserved unchanged. Superseded Draft: hold-and-win-math-draft-v0.1.md, also preserved unchanged, and retained because it records why this revision exists.

Certification: internal approval is not external certification. Nothing in this document is a certification claim, and the mechanic's RTP contribution is a seeded Monte Carlo estimate with a stated interval, never an exact figure.

This revision changes exactly two OPEN parameter families, under the tuning direction approved at the DRAFT MATH gate on 2026-08-09 (option A3):

Parameterdraft-v0.1draft-v0.2
LandingChancePPM15000043000
regular weight, 1×4030
regular weight, 2×2825
regular weight, 3×1618
regular weight, 5×1015
regular weight, 10×510
regular weight, 20×12

Coin values (1×, 2×, 3×, 5×, 10×, 20×) are unchanged and remain design-locked. Jackpot values and the prize-type weights are unchanged. Base reels, trigger weights and the base paytable are unchanged, so base RTP and trigger frequency are bit-for-bit the same.

This document owns the mathematics of the Hold & Win mechanic. The game Math Spec owns totals and links here. Canonical executable parameters remain in the Go backend at /Users/admin/kiro/backend; this document records their identity, never a second copy.

Design contract

  • Purpose: a reusable persistent respin mechanic where coin symbols lock and keep buying respins, settling once into a single capped award.
  • Trigger and eligibility: at least 6 visible BONUS symbols on the base grid of a wagered round. No additional wager. Fewer than 6 consumes no mechanic RNG.
  • Initial state: the trigger grid's BONUS positions are locked and assigned prizes in normative cell order; respinsRemaining = 3; stepIndex = 0.
  • State transitions and evaluation order: one step visits every empty cell in normative order; each visited cell draws land/miss; a land draws prize type, then a regular value only when the type is Regular. Locked cells are skipped entirely.
  • Retrigger/reset rules: at least one newly landed prize in a step resets respinsRemaining to 3. A step with no lands decrements it by exactly 1.
  • Termination: respinsRemaining == 0, or all 15 cells locked.
  • Payout unit, award timing and aggregation: multiplier of total bet, awarded once at settlement. Ordinary settlement sums the locked prizes. A full grid awards exactly Grand 500×, replacing the sum rather than adding to it.
  • Rounding and max-win cap stage: multipliers are float64; money crosses to exact integer minor units only at the wallet boundary. The 500× cap applies once, at settlement, and the round-level cap bounds base + feature at the same 500×.
  • Recovery/replay state: mechanic version, grid dimensions, per-cell locked flag and prize, respins remaining, step index, accumulated award, settlement state, award and replay identity. A repeated step returns the persisted result and consumes no RNG. An unknown persisted mechanic version fails closed.

Normative cell order

15 cells, reel-major:

index:  0  1  2  3  4 |  5  6  7  8  9 | 10 11 12 13 14
cell : c0r0 c0r1 c0r2 c0r3 c0r4 | c1r0 … c1r4 | c2r0 … c2r4

This order is normative for masks, RNG draw sequence, persistence and replay vectors.

RNG contract

DrawDomain/mappingStable orderSubstreamConsumed when
Base grid stopsone weighted stop per reelreel 0 → 1 → 2round seedevery wagered round
Land / missIntn(1_000_000) < LandingChancePPMnormative cell order<featureSeed>:hold_and_win:step:<stepIndex>per empty cell per step
Prize typeweighted integer pool over Regular/Mini/Minor/Majorimmediately after that cell's landsame as the land drawonly on a land, and for each initial locked cell at Start
Regular valueweighted integer pool over 1×/2×/3×/5×/10×/20×immediately after the prize typesameonly when prize type is Regular

Sub-seed strings are exact and shared by production and simulation:

start : <featureSeed>:hold_and_win:start
step  : <featureSeed>:hold_and_win:step:<stepIndex>

Rules:

  1. Fewer than 6 BONUS consumes zero mechanic draws.
  2. Start visits the initial locked cells in normative order, drawing prize type then a regular value only for Regular.
  3. Step visits empty cells in normative order, drawing land/miss first.
  4. Locked cells consume no draws.
  5. Settlement consumes no draws.
  6. Weighted selection uses cumulative integer intervals over the configured weights. A raw draw is never modulo-mapped, and a non-positive or overflowing weight total is an error rather than a silent fallback.

Same mechanic version + initial state + step identity + RNG input reproduce the same state and result. A repeated step must not consume RNG again.

Reference math

These are design inputs, not measured results.

ParameterDraft value/rangeUnitStatusRationale
Mechanic RTP contribution≈20% wageropenDesign target from the approved spec
Trigger frequency≈1 in 150roundsopenDesign target; delivered through base reel strip construction
Conditional mean award≈30× total betopenDesign target for the feature given it triggered
Max-win contribution500× total betlockedFull-grid Grand and the settlement cap are the same number

Parameter tables

Regular coin values (multipliers of total bet):

Valuedraft-v0.1 weightdraft-v0.2 weight
4030
2825
1618
1015
10×510
20×12

Total weight remains 100. Expected regular value:

E[regular] = (1·30 + 2·25 + 3·18 + 5·15 + 10·10 + 20·2) / 100
           = (30 + 50 + 54 + 75 + 100 + 40) / 100
           = 349 / 100
           = 3.49× total bet          (draft-v0.1: 264/100 = 2.64×)

Landed prize types (independent pool):

Prize typeValueDraft weight
Regularfrom the regular table1000
Mini10×20
Minor25×5
Major100×1

Total weight 1026, unchanged in draft-v0.2. Expected landed prize value:

E[prize] = (1000·3.49 + 20·10 + 5·25 + 1·100) / 1026
         = (3490 + 200 + 125 + 100) / 1026
         = 3915 / 1026
         ≈ 3.815789× total bet        (draft-v0.1: 3065/1026 ≈ 2.9873×)

The prize-type pool moved only because E[regular] moved. No jackpot value, and no weight in this table, was touched.

Grand 500× is not in the landed pool. It is awarded only for a full grid.

Structural notes and dependencies

  • Landing chance is stored as integer parts-per-million (43000 = 4.3%) so the probability has no floating-point representation in the config or in the draw.
  • Expected prize count is not a closed form: the respin-reset rule makes the number of steps a random walk whose absorption depends on the current empty-cell count. A step over e empty cells lands nothing with probability (1-p)^e, so the reset probability rises steeply with e — early steps almost always reset, late steps almost never do. The award distribution is therefore measured, not derived.
  • Trigger probability is coarse-grained. The engine's reel model makes one weighted stop draw per reel and reads a contiguous, wrapping 5-row window of the strip (libs/mathengine/mathengine.go:1119). Rows on a reel are correlated, so reaching 6+ BONUS across 15 cells requires contiguous BONUS runs in the strips. The trigger rate moves in discrete jumps as run lengths and stop weights change; it is not a smooth knob.

Exact enumeration

Not available. Every empty cell consumes its own RNG draw, so the round is not grid-determined; hold_and_win is deliberately excluded from the engine's gridDeterministicFeatures allowlist. A tractable exact state-space method was not implemented, so the authoritative Draft evidence is a seeded backend simulation, labelled estimated. Monte Carlo is never presented as exact.

Backend implementation map

  • Config/schema and validation: libs/mathengine/config.go (HoldAndWinConfig, HoldAndWinWeightedValue, HoldAndWinPrizeWeight, HoldAndWinProbabilityScale), libs/mathengine/feature_def_json.go, validateHoldAndWin in libs/mathengine/mathengine.go, plus the authoring mirror in apps/math-studio/internal/domain/feature.go and build_config.go.
  • libs/mathengine executor/state: libs/mathengine/hold_and_win.go (StartHoldAndWin, StepHoldAndWin, SettleHoldAndWin, RunHoldAndWinToCompletion) and libs/mathengine/hold_and_win_executor.go.
  • Registration/evaluation order: getFeatureExecutor and RoundOutcome in libs/mathengine/mathengine.go; support grade in libs/mathengine/capabilities.go.
  • Simulation wiring: HoldAndWinBatchAgg in libs/mathengine/batch.go, merged by MergeBatches under the existing ShardSeed contract.
  • Runtime persistence/replay: required — execution spans requests. apps/game-engine/migrations/023_create_hold_and_win_sequences.sql, internal/domain/round/hold_and_win_sequence.go, internal/infra/postgres/hold_and_win_store.go, internal/app/service/spin_orchestrator_hold_and_win.go. The trigger opens the durable sequence inside the existing paid-round transaction; steps charge no wager and reuse the existing allocator latch and credit-intent infrastructure rather than adding a second one.

Verification contract

  • Failing tests to observe before implementation: config validation rejections; five-versus-six trigger boundary; reel-major lock import; weighted-interval boundaries; empty-cell-only redraw; reset versus decrement; duplicate step identity with an RNG that panics on read; zero-respin and full-grid termination; ordinary sum, single cap and full-grid Grand override; step execution equals RunToCompletion; real-PostgreSQL open, advance, replay, concurrency, quarantine and restart.
  • Golden/replay vectors: pack golden test plus additive entries in libs/mathengine/parity, each binding config hash, backend Git SHA, RNG algorithm ID, feature seed, incoming state, step ID and the exact outcome. Seeds are discovered by search, expectations reviewed by hand, then pinned as literals.
  • Exact calculation scope: not enumerable — see above.
  • Monte Carlo rounds/seeds/CI requirement: at least 1,000,000 rounds over recorded independent seeds/shards, reporting standard error and a 95% confidence interval.
  • Parity and regression scope: make test, make test-rng, make test-parity, go test -race ./apps/... ./libs/..., and byte-unchanged existing vectors for classic_fruits_3x3, parity and parsheet/testdata.
  • Reachable max or upper-bound method: 500× is a reachable value, not merely a bound — a full grid awards it exactly. Validation must observe it, and must also confirm no round exceeds it.

Canonical config identity

The executable parameters live in the Go backend. This workspace records their identity and never a second copy.

FieldValue
Pathlibs/mathengine/games/hold_and_win_reference/hold-and-win-reference.yaml
SHA-256888f134c29a59434d259316e9b83a8ec760bec6bcfb182334c6e42f167917d88
Backend Git SHA88a730a8512995651c7c0791d25d435425c3a7a2 (branch feat/hold-and-win-reference)
Game IDhold-and-win-reference
Mechanic versionhold_and_win/v1
RNG algorithmsha256-trunc64-be/go1-alfg#1

The authoring preset hold_and_win_reference in apps/math-studio/internal/domain/packs mirrors the same parameters. It is compared to the canonical YAML entry-for-entry, strip weights included, by TestHoldAndWinReferencePackMatchesCanonical — two independent copies of a paytable is how a simulated RTP comes to describe a config nobody ships.

Base-game structure

PropertyValue
Grid3 reels × 5 rows, lines
Paylines5, one per row
Strip length20 weighted entries per reel
BONUS placementcontiguous at entry indices 0-4 on every reel
Reachable base grids20 × 20 × 20 = 8000
BONUS line payoutnone — keeps base and feature RTP separable
Round cap500× total bet, equal to the mechanic cap and the grand

Because the engine draws one weighted stop per reel and reads a wrapping 5-row window, the visible BONUS count per reel is a function of distance from that run:

StopVisible BONUSStopVisible BONUS
05194
14183
23172
32161
415-150

Six BONUS across 15 cells therefore needs at least two reels to stop near their run, which is why trigger probability moves roughly with the square of the nine bonus-adjacent stop weights — in discrete jumps, not smoothly.

Design-probe enumeration

Reproduce with:

cd /Users/admin/kiro/backend
go test -tags hwdesign ./libs/mathengine/games/hold_and_win_reference -run DesignProbe -v

The probe enumerates all 8000 reachable base grids, so the base and trigger figures below are exact for what they cover. They are not the authoritative Draft evidence — that is the seeded simulation at plan Task 10.

QuantityMethodValueTarget
Base RTPexact enumeration75.1160%76%
Trigger probabilityexact enumeration1 in 155.41 in 150
Conditional feature meansampled, 20k per trigger count≈122.8×30×
Mechanic RTPexact mass × sampled mean≈79.0%20%
Total RTPhybrid≈154.1%96%

Observed drift and its attribution

The mechanic overshoots its RTP target by roughly 4×. The prize tables are not the cause. Splitting the conditional mean by settlement regime:

RegimeRateContribution to the conditional mean
Ordinary settlement81.4%≈29.9×
Full grid (Grand 500×)18.6%≈92.9×

Ordinary settlement is essentially exactly the 30× design target. The entire overshoot is the full-grid rate: a 500× top award reached almost one time in five is a jackpot in name only.

The mechanism is the landing chance against the reset rule. At 150000 PPM a step over 9 empty cells lands nothing only 23.2% of the time, so the respin counter resets far more often than it decays and a triggered feature runs ≈9.3 steps.

Landing chance is an OPEN parameter. It is deliberately not changed before DRAFT MATH approval; tuning is plan Task 11.

Results

Authoritative evidence: ../../../evidence/simulation/2026-08-09-draft-v0.2-confirmation-2m.md — 2,000,000 rounds, 8 merged shards, independent master seed haw-draft-v0.2-confirm-independent, backend a3f644e, config SHA-256 a22129d9…8d0508.

Base RTP and trigger probability are EXACT (enumeration of all 8,000 reachable base grids) and are unchanged from draft-v0.1 by construction: neither tuned parameter is read before the feature starts. Feature figures are ESTIMATED.

MetricTargetExactSimulated ± 95% CIStatus
Total RTP96.0% ±0.3pp96.0031% ± 0.1189pp, CI [95.7701%, 96.2362%]in band
Base RTP76%75.1160%74.9394% (noise)in band, unchanged
Feature RTP20.8840%20.8871% ± 0.1189ppon target
Trigger frequency1 in 1501 in 155.40351 in 156.58, CI [153.92, 159.33]in band, unchanged
Conditional mean32.4545×32.4593× ± 0.1848, CI [32.0972×, 32.8215×]on target
Full-grid, conditional0.10%0.090053% ± 0.002022pp, CI [0.086089%, 0.094016%]in band [0.05%, 0.25%]
Full-grid, per triggered feature1 in 1,110, CI [1 in 1,064, 1 in 1,162]
Full-grid, per base round1 in 172,570, CI [1 in 165,294, 1 in 180,515]
Mean steps per featureno target5.5999measured
Reset rate per stepno target0.2608measured
Mean cells at settlement8.3974 of 15measured
Max award500×500× by construction500.0000× observed, never exceededreachable and contained
Std deviation per roundno target4.0194measured
Hit frequencyno target14.4312%measured

Decomposition residual: 0.000000 pp.

Full-grid frequency — two runs, three figures

The full-grid rate is reported three ways every time, because a conditional rate and an unconditional one differ by the 155.4035-round trigger interval and are easy to conflate.

SourceConditionalPer triggered featurePer base round
Round-level, 2M rounds, 10 full grids0.0783%1 in 1,277~1 in 198,500
Feature-conditioned, 2.2M triggered features0.090053% ±0.002022pp1 in 1,1101 in 172,570

The round-level estimate rests on 10 events, and its own Poisson interval (~[0.030%, 0.127%]) comfortably contains the conditioned figure. The conditioned run is ~12× more precise and is the one to quote — see ../../../evidence/simulation/2026-08-09-draft-v0.2-fullgrid-confirmation.md. It confirms rather than contradicts the band, and sits closer to the 0.10% target, so 43,000 PPM stands.

How total RTP is quoted, and why

Total RTP is exact base RTP + exact trigger probability × simulated conditional mean. Base RTP and trigger probability are known exactly by enumeration; substituting sampled estimates for known constants adds variance and buys nothing.

The all-simulated figure for the same run is 95.6695% (SE 0.2842pp), which sits marginally below the band floor. The gap is fully attributable and is not a property of the game: the run's sampled base component came in 0.1766 pp light, and its sampled trigger count 0.75% light. Both figures, and this decomposition, are printed by the simulation runner.

Prize-kind frequency per locked cell

Weights unchanged, so these must still match the 1000:20:5:1 pool — and they do:

KindObservedConfigured
regular0.9739230.974659
mini0.0197560.019493
minor0.0052580.004873
major0.0010630.000975

Tuning pass history

PassChangeOutcome
1LandingChancePPM alone, 30000–110000No value worked. Full-grid band needs ≤~51,200; RTP band needs ≥~64,700. Disjoint by ~13,500 PPM. Reported as a Pareto table; see ../../../evidence/simulation/2026-08-09-tuning-pass-1-pareto.md
2 (option A3)LandingChancePPM and regular-value weightsBoth bands satisfied at 43000 PPM. This revision

The pass-1 diagnosis predicted this outcome quantitatively: it identified that a compliant full-grid rate allows ≈8.5 locked cells, so reaching 32.45× requires E[prize] ≈ 3.82×. Option A3 delivers 3.8158× and the measured conditional mean is 32.4593×.

Parameters and revisions

Locked

  • Grid 3 columns × 5 rows, 15 cells.
  • Normative reel-major cell order.
  • Trigger at 6 or more BONUS.
  • Initial respins 3; reset to 3 on any newly landed prize; decrement by 1 on a miss-only step.
  • Locked cells are never redrawn and consume no RNG.
  • Jackpot tier values: Mini 10×, Minor 25×, Major 100×, Grand 500×.
  • Grand is not a landed prize type; a full grid awards exactly 500×, replacing the sum.
  • The cap and the full-grid Grand each apply exactly once, at settlement.
  • 500× is a single whole-round bound (set 2026-08-09 by user).
  • No collector, multiplier, feature buy or additional wager in v0.1.
  • RNG draw order and sub-seed strings as specified above.

Open for tuning

  • Landing chance PPM (draft 150000).
  • Regular coin value weights.
  • Prize-type weights.
  • Base trigger weights, i.e. the pack's reel strips.

Nothing else may change without a new DESIGN approval.

RevisionChanged parametersConfig hashSeeds/roundsResultApproval
draft-v0.1Initial approved contract888f134c…917d88design probe: exact enumeration of 8000 base grids + 20k sampled feature runs per trigger countbase 75.1160% and trigger 1 in 155.4 both on target; mechanic RTP ≈79% against a 20% target, attributed to an 18.6% full-grid rateDESIGN approved 2026-08-09; DRAFT MATH pending

Approval history

GateStatusApproved scopeDate/ownerNotes
DESIGNapprovedDesign spec c44bc01 and the contract above2026-08-09 / userBackend implementation authorized
DRAFT MATHawaitingObserved behavior and tuning directionnot submittedTuning and handoff prohibited before approval
FREEZE & HANDOFFawaitingReconciled Final and protocol hashesnot submittedDeveloper-ready and freeze prohibited before approval

Risks and blockers

  • NOT FOR CERTIFICATION until the required internal gates and an authoritative external approval are recorded. Internal tests and simulations are not certification.
  • Trigger frequency is coarse-grained in strip-construction space, so hitting ≈1/150 exactly may not be achievable without accepting a nearby value. Recorded as expected drift for the DRAFT MATH gate.
  • No exact cross-check exists for the mechanic. The only defence against a systematic error in the state machine is the golden/parity corpus plus the reference-versus-runtime equality proof, not a second independent calculation.
  • The mechanic RTP contribution and the whole-game split are unmeasured targets until Task 10 reports them.

Finalization record

  • Source Draft path: games/hold-and-win-reference/math/mechanics/hold-and-win/hold-and-win-math-draft-v0.1.md
  • Source Draft hash: computed at the FREEZE & HANDOFF gate
  • Final path/version: not created
  • Final approval: awaiting
  • Final document hash: not created

Do not edit this Draft into Final in place. After final approval, create an immutable Final snapshot with resolved values and record both files in the mechanic changelog.


Final status

FieldValue
Versionfinal-v1.0
Approved at gateFREEZE & HANDOFF, 2026-08-09
Approveruser (Bogdan), verbatim APPROVE FREEZE & HANDOFF
Source Draftdraft-v0.2, unchanged
Backend Git SHAcd4c093c6b875376e547cd31123de254d2cb2f93 on main
Canonical config SHA-256a22129d99d21eb14acdb303efdf666f4065925fa461012d8e99f2144538d0508
Mechanic versionhold_and_win/v1
RNG algorithmsha256-trunc64-be/go1-alfg#1, unchanged by this work
Engine capabilitySupportFull — the live runtime plays it; still not enumerable
Developer-readyYES
Externally certifiedNO — and internal tests, simulations and this approval are not certification
Production statedormanthold_and_win_enabled and hold_and_win:<operator> both default to DENY

Developer-ready means the mathematics, the runtime that executes it, the wire contract and the evidence binding them are complete and reconciled under one approved manifest (hold-and-win-math-handoff-v0.1.md). It does not mean the feature is switched on for anyone: enabling it for an operator is a separate, explicit decision.

Any change to a bound path, version, hash, locked rule, RNG mapping, evaluation order, rounding or cap invalidates the approved handoff protocol. Start a new Draft and a new protocol version.