Changelog — Hold & Win Reference

Game-level changes. Mechanic-math revisions have their own immutable history in math/mechanics/hold-and-win/hold-and-win-math-changelog.md.

2026-08-09 — draft-v0.1 created

  • Created the reference game hold-and-win-reference for the reusable hold_and_win mechanic, from the design spec approved by the user on 2026-08-09 (workspace commit c44bc01).
  • Authored GDD draft-v0.1, whole-game Math Spec draft-v0.1, mechanic Draft math draft-v0.1, the mechanic changelog and an unsealed handoff protocol v0.1.
  • Created evidence routing (evidence/simulation, evidence/parity, evidence/max-win) and the test-vector index. All are empty — no measurement or vector exists yet.
  • Recorded backend baseline bde8c0539f8b621c4d6d13e5834be56741a2862f and RNG sha256-trunc64-be/go1-alfg#1.
  • Status: DRAFT / NOT FOR CERTIFICATION. DRAFT MATH and FREEZE & HANDOFF gates are both awaiting.

Deliberately not done

  • No canonical config, reel strips or paytable in this workspace. Executable parameters are canonical in the Go backend; duplicating them here would create a second thing that can disagree.
  • No cross-links added to the workspace root README.md or games/README.md: both are untracked user files at the time of writing, and staging them would commit unrelated user work. The links remain to be added by their owner.

2026-08-09 — canonical config created (plan Task 6)

  • The Go backend now carries the executable pack. Recorded here as identity only; the parameters are not duplicated into this workspace.
FieldValue
Pathlibs/mathengine/games/hold_and_win_reference/hold-and-win-reference.yaml
SHA-256888f134c29a59434d259316e9b83a8ec760bec6bcfb182334c6e42f167917d88
Backend Git SHA88a730a8512995651c7c0791d25d435425c3a7a2
  • Base game: 3 reels x 5 rows, lines, 5 paylines, 20 weighted entries per reel with BONUS contiguous at indices 0-4, BONUS paying nothing on lines, round cap 500x.
  • Design-probe enumeration (exact for the base game and the trigger): base RTP 75.1160% against a 76% target, trigger 1 in 155.4 against 1 in 150.
  • Observed drift recorded, not tuned: mechanic RTP ~79% against a 20% target, attributed to an 18.6% full-grid rate contributing ~92.9x of a ~122.8x conditional mean. Ordinary settlement is on target at ~29.9x.
  • Nothing here is authoritative evidence. The seeded simulation at plan Task 10 is.

2026-08-09 — Draft simulation measured (plan Task 10)

  • Ran 2,000,000 seeded rounds through the canonical RunBatch/MergeBatches path, 8 merged shards, master seed haw-draft-v0.1-sim, backend 13700ae. Report: evidence/simulation/2026-08-09-draft-v0.1-2m.md.
  • On target: base RTP 75.1025% (exact 75.1160%, target 76%) and trigger 1 in 154.74 (exact 1 in 155.4, target 1 in 150). Both inside their draft bands.
  • Outside target: mechanic RTP 79.7860% vs 20%, conditional mean 123.4600× vs 30×, total RTP 154.8803% vs 96%.
  • Attribution: full grids at 18.71% per trigger contribute 93.54× of the 123.46× conditional mean; ordinary settlement contributes 29.92× against a 30× target.
  • 500× confirmed reachable and never exceeded. Prize-kind frequencies agree with the configured 1000:20:5:1 pool.
  • No parameter changed. Submitted as AWAITING_USER_APPROVAL — DRAFT MATH.

2026-08-09 — tuning pass 1 attempted, no revision created

  • DRAFT MATH approved with a direction to sweep only LandingChancePPM. Swept 30000/50000/70000/90000/110000, then refined 45000–64000. Evidence: evidence/simulation/2026-08-09-tuning-pass-1-pareto.md.
  • No value works. The full-grid band [0.05%, 0.25%] needs PPM ≤ ≈51,200; the total-RTP band [95.7%, 96.3%] needs PPM ≥ ≈64,700. Disjoint by ≈13,500 PPM.
  • Root cause: E[landed prize] = 2.9873× means a 32.4545× conditional mean requires 10.86 of 15 cells, and a grid that routinely reaches 10.9 of 15 cannot make a full grid rare.
  • Base RTP (75.1160%) and trigger (1 in 155.4035) confirmed unchanged, as expected — the parameter only acts after a trigger.
  • STOPPED before touching regular values or prize weights, per the approved direction. draft-v0.1 is unchanged and no new revision exists.

2026-08-09 — draft-v0.2 created (tuning pass 2, option A3)

  • DRAFT MATH approved with a tuning direction. Two OPEN families changed: LandingChancePPM 150000 → 43000, and the regular-value weights 40/28/16/10/5/1 → 30/25/18/15/10/2. Coin values, jackpot values and prize-type weights untouched, each pinned by a test.
  • E[regular] 2.64× → 3.49×, E[prize] 2.9873× → 3.8158×.
  • Confirmed on 2,000,000 rounds with an independent master seed: evidence/simulation/2026-08-09-draft-v0.2-confirmation-2m.md. Total RTP 96.0031% ±0.1189pp (band 95.7–96.3), feature 20.8871%, conditional mean 32.4593×, full-grid 0.0783% of triggers on 10 events (band 0.05–0.25%), max 500.0000× reachable.
  • Base RTP 75.1160% and trigger 1 in 155.4035 both exact and unchanged.
  • draft-v0.1 preserved unchanged. Config SHA-256 regenerated to a22129d9…8d0508; golden vectors re-recorded; the v0.1 simulation retained as the historical record.
  • Volatility fell σ 17.78 → 4.02 and duration 8.87 → 5.60 steps: the Grand is now rare and the coins carry the value.
  • Next: runtime routing, backend response contract (plan amendment ff17edc) and runtime parity, then AWAITING_USER_APPROVAL — FREEZE & HANDOFF.

2026-08-09 — full-grid frequency confirmed and a wording error corrected

  • Added a feature-conditioned confirmation run: 2,200,000 triggered features through the canonical RunHoldAndWinToCompletion, independent seeds. evidence/simulation/2026-08-09-draft-v0.2-fullgrid-confirmation.md. Full-grid 0.090053% conditional, SE 0.002022pp, CI [0.086089%, 0.094016%] — 1 in 1,110 triggered features, 1 in 172,570 base rounds. ~12× more precise than the 10-event round-level estimate, consistent with it, and closer to the 0.10% target. 43,000 PPM stands.
  • Corrected a wording error: the draft-v0.2 table had reported "1 in ~127,700 base rounds", which multiplied the 1-in-1,277 conditional figure by 100 instead of by the 155.4035-round trigger interval. The conditional rate, the per-trigger figure and the per-base-round figure are now always reported together so the two cannot be conflated.
  • No parameter changed. This is confirmation evidence, not a tuning pass.

2026-08-09 — runtime routing, live response contract and runtime parity

The mechanic is now played by the live spin orchestrator, which is what turns every RTP figure above from a statement about the mechanic into a statement about production play. Backend cd4c093. No math parameter changedLandingChancePPM is still 43,000 and all draft-v0.2 evidence stands unmodified.

  • Trigger routing. A paid base spin with 6+ BONUS opens the durable feature inside the paid gameplay transaction, alongside the round, its outbox event and any credit intent. Opening it afterwards would leave a window where a player was charged for a trigger no durable feature remembers.
  • No double payment. hold_and_win is now stripped from the player config the way free_spins already was: HoldAndWinExecutor settles a whole feature inside one round and is a simulation driver only. The one case that still pays at the trigger is a grid that arrives already full, where there is no respin to take.
  • Step API. A respin is a /spin request carrying expected_hold_and_win_step_index, takes no wager, and is idempotent on (sequence_id, step_index) — both halves server-owned. The replay window is exactly one step wide, because the durable row holds the state after the most recent step and cannot reproduce an earlier one; ahead and stale requests get distinct typed errors.
  • One wire contract. Trigger, respin, replay, reconnect and settlement all go through BuildHoldAndWinResponse. The earlier second projection was removed rather than left as a shape that could disagree with it.
  • Live fixtures. gdd/hold-and-win-backend-response.md was regenerated from fixtures captured on the live orchestrator path, not from the standalone mapper — a mapper fixture proves the projection is stable and says nothing about whether the runtime calls it. The OpenAPI example and the handler's serialized output are both bound to those files.
  • Runtime parity. The orchestrator's complete state is compared with RunHoldAndWinToCompletion at 0, 43,000 and 1,000,000 PPM. Both this and the no-double-pay claim were mutation-checked before being trusted.
  • Capability. hold_and_win promoted SupportPartialSupportFull in the same commit as that evidence. The detail still records that the mechanic is not enumerable: full support is a claim about the runtime, not about exactness.
  • Production state: dormant. hold_and_win_enabled and hold_and_win:<operator> both default to DENY and a nil gate denies, so nothing is enabled for any operator by this work.
  • Next: AWAITING_USER_APPROVAL — FREEZE & HANDOFF.

2026-08-09 — FREEZE & HANDOFF approved; final-v1.0 sealed

  • Approved by the user (Bogdan), verbatim APPROVE FREEZE & HANDOFF, with the instruction to merge and push main first. The seal is therefore computed against the post-merge state.
  • Backend cd4c093 fast-forward merged to main and pushed. Because main was still at the branch's baseline bde8c05, the merge produced no new commit — the SHA sealed in the protocol is exactly the SHA on main.
  • math/mechanics/hold-and-win/hold-and-win-math-final-v1.0.md created from draft-v0.2, verbatim, and immutable. Both drafts are preserved unchanged.
  • The handoff protocol is sealed: every bound artifact now carries its SHA-256 instead of a sealed at the gate placeholder. Living documents — the two changelogs, the README and the protocol itself — are deliberately unhashed, because a hash on a file that must keep growing is invalid the moment the next entry lands. Their integrity comes from Git history.
  • Protocol SHA-256 at approval: 3732ac9b45bf09121ad200a64b94a551ffffcb3a0a968f27f652e0ea13fb1ca1 (superseded by the build-SHA correction below; both are recorded so neither is mistaken for the other) (math/mechanics/hold-and-win/hold-and-win-math-handoff-v0.1.md as sealed). Recorded here rather than inside the protocol because a file cannot contain its own hash.
  • Developer-ready: YES. Not a certification claim, and not a switch-on: the durable path is dormant by construction and enabling it for an operator is a separate explicit decision.
  • No math parameter changed at this gate. LandingChancePPM is 43,000; total RTP 95.9947%; conditional mean 32.4461×; max award 500×.

2026-08-09 — CI lint fix on top of the seal (b55426d)

  • The lint stage failed on cd4c093: the OpenAPI drift test imported gopkg.in/yaml.v3, which only apps/auth-service requires. go.work unions the build list across modules, so it compiled locally; CI lints each module on its own — because golangci-lint v1 does not understand go.work — and there the import does not resolve. go mod tidy had nothing to report for the same reason.
  • Fixed by moving the test onto sigs.k8s.io/yaml (already used by libs/mathengine for these pack YAMLs) and declaring it in apps/game-engine/go.mod. Reproduced locally with GOWORK=off: failing before, clean after.
  • The seal is unaffected. All eight bound backend artifacts are byte-identical between cd4c093 and b55426d, checked per file. final-v1.0 is untouched — it is immutable and records the approval commit. Only the shipped-build SHA was corrected in the living documents.
  • Protocol SHA-256 after this correction: 40b181193e8e56445555b28b0851a241e1b6153233be066a631d7b4b2b0ad8eb

2026-08-09 — second CI lint fix (f80cb60)

  • After the yaml fix the lint stage failed again, on six unused findings in apps/game-engine. None is new code: both files were last touched by 0abdac4 and 265abd2, which are ancestors of the branch baseline bde8c05.
  • Why they surfaced only now: the CI cache key is [go.work, apps/game-engine/go.sum], and the yaml fix changed that go.sum. The pipeline therefore ran with a cold cache and re-analysed the module from scratch instead of reusing a prior result. The findings were always true; nothing had recomputed them.
  • Fixed with //nolint:unused and a reason, not by deleting. Each symbol is retained on purpose and says so in its own comment, and round_service.go already carries the identical directive on executeRoundDisabled — the sibling of these handlers, from the same commit that disabled them.
  • Verified with the real linter: golangci-lint v1.62 (the CI version) installed locally and run per module exactly as the pipeline does — all 15 modules clean — and mutation-checked, since a lint run that silently does nothing looks identical to a passing one.
  • Seal unaffected. All eight bound backend artifacts are byte-identical between cd4c093 and f80cb60, checked per file. final-v1.0 untouched.
  • Protocol SHA-256 after this correction: 6adc767752c03b79205ae074199d0dfa70612d61bd3756eedd54230517611b9e

2026-08-09 — pipeline green, dev deployed

  • The main pipeline for f80cb60 passed and the dev auto-deploy completed (confirmed by the user; I have no API access to observe it — the PAT in pass/gitlab.md returns 401 since its rotation).
  • This closes the one open question left by the two lint fixes. Both were verified locally against golangci-lint v1.62, the CI version, but the test and build stages had not been reproduced locally; the green pipeline is what confirms them.
  • Nothing changed for players. The deployed build ships the mechanic dormant: hold_and_win_enabled and hold_and_win:<operator> both default to DENY and a nil gate denies, so no operator can reach the feature. Enabling it remains a separate, explicit decision.
  • No artifact, hash or approval is affected by this entry — it records an observation, not a change.