Mechanic Math — hold-and-win — Draft

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 weight
40
28
16
10
10×5
20×1

Total weight 100. Expected regular value:

E[regular] = (1·40 + 2·28 + 3·16 + 5·10 + 10·5 + 20·1) / 100
           = (40 + 56 + 48 + 50 + 50 + 20) / 100
           = 264 / 100
           = 2.64× total bet

Landed prize types (independent pool):

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

Total weight 1026. Expected landed prize value:

E[prize] = (1000·2.64 + 20·10 + 5·25 + 1·100) / 1026
         = (2640 + 200 + 125 + 100) / 1026
         = 3065 / 1026
         ≈ 2.9873× total bet

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 (150000 = 15%) 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 Draft evidence: evidence/simulation/2026-08-09-draft-v0.1-2m.md — 2,000,000 rounds, 8 merged shards, master seed haw-draft-v0.1-sim, backend 13700ae, config SHA-256 888f134c…917d88.

Base RTP and trigger probability are EXACT (enumeration of all 8,000 reachable base grids). Every mechanic figure is ESTIMATED — Monte Carlo, reported with a standard error.

MetricTargetExactSimulation ± 95% CIStatusEvidence
Total RTP96%not enumerable154.8803% ± 2.46ppoutsidesim report
Base RTP76%75.1160%75.1025%inside bandprobe + sim
Mechanic RTP20%not enumerable79.7860%outside, ≈4.0×sim report
Trigger frequency1 in 1501 in 155.41 in 154.74 [152.12, 157.44]inside bandprobe + sim
Conditional mean30×not enumerable123.4600× ± 3.12outside, ≈4.1×sim report
Conditional medianno targetin [10×, 50×) band (69.4% mass)measuredsim report
Standard deviationno target17.7776 per roundmeasuredsim report
Observed/reachable max500×500× by construction500.0000× observed, never exceededreachable and containedsim report
Mean steps per featureno target8.8711measuredsim report
Reset rate per stepno target0.4865measuredsim report
Full-grid probabilityno target0.187079measuredsim report
Prize-kind frequencymatches weightsregular 0.9750 / mini 0.0190 / minor 0.0049 / major 0.0010agrees with 1000:20:5:1sim report

Decomposition residual −0.0082 pp: base and mechanic reconcile to the total.

Observed drift and its attribution

The mechanic overshoots its RTP target by ≈4×. The prize tables are NOT the cause.

RegimeRateContribution to the 123.46× conditional mean
Ordinary settlement0.812929.92× — the 30× target, essentially exactly
Full grid (Grand 500×)0.187193.54× — the entire overshoot

18.71% of triggered features fill all 15 cells. The mechanism is the landing chance against the reset rule: at 150000 PPM a step over 9 empty cells lands nothing with probability 0.85⁹ = 0.2316, so the counter resets about half the time per step, a feature runs 8.87 steps and reaches 12.81 of 15 cells. A 500× top award reached almost one time in five is a jackpot in name only.

No parameter was changed in response. Tuning is prohibited before DRAFT MATH approval.

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.