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-referencefor the reusablehold_and_winmechanic, from the design spec approved by the user on 2026-08-09 (workspace commitc44bc01). - Authored GDD
draft-v0.1, whole-game Math Specdraft-v0.1, mechanic Draft mathdraft-v0.1, the mechanic changelog and an unsealed handoff protocolv0.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
bde8c0539f8b621c4d6d13e5834be56741a2862fand RNGsha256-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.mdorgames/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.
- 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/MergeBatchespath, 8 merged shards, master seedhaw-draft-v0.1-sim, backend13700ae. 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%] needsPPM ≥ ≈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.1is 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:
LandingChancePPM150000 → 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.1preserved unchanged. Config SHA-256 regenerated toa22129d9…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, thenAWAITING_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 changed — LandingChancePPM is still 43,000 and all
draft-v0.2 evidence stands unmodified.
- Trigger routing. A paid base spin with 6+
BONUSopens 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_winis now stripped from the player config the wayfree_spinsalready was:HoldAndWinExecutorsettles 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
/spinrequest carryingexpected_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.mdwas 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
RunHoldAndWinToCompletionat 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_winpromotedSupportPartial→SupportFullin 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_enabledandhold_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 pushmainfirst. The seal is therefore computed against the post-merge state. - Backend
cd4c093fast-forward merged tomainand pushed. Becausemainwas still at the branch's baselinebde8c05, the merge produced no new commit — the SHA sealed in the protocol is exactly the SHA onmain. math/mechanics/hold-and-win/hold-and-win-math-final-v1.0.mdcreated fromdraft-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 gateplaceholder. 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.mdas 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.
LandingChancePPMis 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
lintstage failed oncd4c093: the OpenAPI drift test importedgopkg.in/yaml.v3, which onlyapps/auth-servicerequires.go.workunions the build list across modules, so it compiled locally; CI lints each module on its own — because golangci-lint v1 does not understandgo.work— and there the import does not resolve.go mod tidyhad nothing to report for the same reason. - Fixed by moving the test onto
sigs.k8s.io/yaml(already used bylibs/mathenginefor these pack YAMLs) and declaring it inapps/game-engine/go.mod. Reproduced locally withGOWORK=off: failing before, clean after. - The seal is unaffected. All eight bound backend artifacts are byte-identical between
cd4c093andb55426d, checked per file.final-v1.0is 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
lintstage failed again, on sixunusedfindings inapps/game-engine. None is new code: both files were last touched by0abdac4and265abd2, which are ancestors of the branch baselinebde8c05. - 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:unusedand a reason, not by deleting. Each symbol is retained on purpose and says so in its own comment, andround_service.goalready carries the identical directive onexecuteRoundDisabled— 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
cd4c093andf80cb60, checked per file.final-v1.0untouched. - Protocol SHA-256 after this correction:
6adc767752c03b79205ae074199d0dfa70612d61bd3756eedd54230517611b9e
2026-08-09 — pipeline green, dev deployed
- The
mainpipeline forf80cb60passed and the dev auto-deploy completed (confirmed by the user; I have no API access to observe it — the PAT inpass/gitlab.mdreturns 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
testandbuildstages 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_enabledandhold_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.