skull_raiders: free-spins totalWin = freegame_wins (feature-only) - #116
Closed
mdicillo wants to merge 19 commits into
Closed
skull_raiders: free-spins totalWin = freegame_wins (feature-only)#116mdicillo wants to merge 19 commits into
mdicillo wants to merge 19 commits into
Conversation
Full math-SDK port of Camp Deadwater: 5x5 lines game, merit-badge wild multiplier, additive Helping Hands tumble, 3 tiers, 2 antes, 3 buys, Dig Deeper tier upgrade. All 6 bet modes optimized to 96.00% RTP; wincap 5000x; buys 100/250/500. 3-star volatility verified (platform gates cost-normalized per mode). Generated library/ is gitignored (regenerate via `make run`). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Make the emitted book a superset of the client's internal event vocabulary so the TypeScript adapter (fromStakeBook) is a clean 1:1 map with no fidelity loss. Math is unchanged — only extra event fields are added; RTP holds at 0.96 on all 6 modes, SHA-256 + payout hash verify OK. - reveal: attach this spin's merit badge as `wildMultiplier` on every reveal (base + free, incl. losing/no-wild spins). Roll the badge before draw_board so it's set at reveal time (gamestate), attach in a draw_board override. - winInfo: attach `hasWildWin` so the client sets its callout multiplier exactly when a wild is in a winning line (meta.globalMult collapses 1x-wild and no-wild). - handClear: emit the `cleared` cells the hands pulled (unpadded board coords). - freeSpinTrigger: level, count, baseLevel (Dig Deeper upgrade), maxHands, tier floor multiplier — everything bonusTrigger + bonusStart need. - freeSpinRetrigger: level, added, capped (SDK never enforces bonus_max, so capped stays False — faithful to the certified math; do not wire the cap without re-cert). - updateFreeSpin: cumulative feature `totalWin`; freeSpinEnd: level + maxWin flag. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Regenerated BR0 / BR_ante2 / BR_ante3 from the updated TS model (npm run reels:export): highs 30->34, lows 70->68, base WILD 3.97->3.90. Feature strips (FR0 / FR_tier2 / FR_tier3 / FRWCAP) are byte-identical — the free-spins H/L densities are frozen, so feature EV and every buy mode stay put. 100k cert: all 6 modes optimize to 0.9600 RTP, SHA-256 + payout hashes OK, 5000x wincap preserved, all cost-normalized volatility gates pass (CVaR/cost < 800, ETL/cost < 0.9). run.py kept at 10k dev default. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
camp_deadwater: A-minus base-reel reweight (100k certified)
Sync the SDK game to the dev model's mid-juice + first-aid work (camp-deadwater #143/#144/#145) and add the mystery reveal. - Paytable: de-top-heavied to match SYMBOLS (WILD 5-OAK 60->45; H1 3/12/55->3/14/40; low ranks regrouped 1-3-1). - FIRSTAID mystery symbol on BR0/ANTE reels (re-exported), revealed before scoring: reveal_mystery() ports the TS "match & extend" (best-completing left-anchored run; cosmetic left-neighbour then a real-symbol fallback, so a kit never reveals as itself/wild/scatter). Registered via a create_symbol_map override (framework only auto-registers paytable + special symbols). - Buys draw a FIRSTAID-free base reel (BR0_buy) so a bought bonus never lands an unrevealed kit on its trigger board. - Reels re-exported (BONUS 1.47, FEATURE_WILD 30.5, ante mults). Re-cert: 100k, all 6 modes 0.9600 RTP, SHA-256 + payout hash OK; cost-normalized volatility within limits (buys 4.3-8.5 < 800, ante_allout 0.40 < 0.9). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
camp_deadwater: first-aid mystery re-cert (base + antes)
Ports the stake-game-two (Jelly Jamboree) TS fake-math into the math-SDK, following the camp_deadwater milestone pattern. A 5x3 243-ways CASCADING slot. Milestone A: grid, Rage-Quit paytable, exported reels (WILD->W / SCATTER->S / MYSTERY->M), 25,000x wincap, and the ways-evaluation + tumble loop in both base and free spins. The multiplier LADDER is neutralized (global_multiplier held at 1) and the mystery "?" is inert, so base ways + cascade math can be verified before the multiplier mechanics land. Verified against 2k debug sims: every win row equals paytable*ways*100 (book units), globalMult is 1 everywhere, and cascades chain (depth up to 25). Milestones B (ladder + WILD flat pay), C (wheel + tiers + retrigger), and D (six bet modes + optimizer + cert) follow. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Ladder: global_multiplier climbs +1 per winning tumble (opening drop 1x, then 2x, 3x, …) via update_global_mult in the base + free tumble loops. Reset each base spin; reset each free spin for now (tier persistence is C). - WILD flat pay: add_wild_pay() awards a flat 5x total bet ONCE when a wild sits on all five reels, x the ladder, stacked on the ways wins the wilds complete. Not a per-way pay, so it is appended to win_data directly. Verified: all symbol wins pay paytable*ways*ladder*100; globalMult climbs 1..25 across cascades; max payout now 343x (was 47x). Wild-pay path unit-tested directly (fires once at 5x*ladder on all-reels; no-ops otherwise) since a full 5-reel wild is naturally rare with base wilds pushed right. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…etrigger - Mystery "?" wheel folded into the tumble loop at the locked ordering: each "?" spins first (boosting the ladder), then the drop's win pays at the boosted ladder, then +1. Base activates a "?" only on a winning drop; free spins always. Multiple "?" stack. Each activated "?" is consumed. - Wheel results (add 5/10/20/50/100, mult x2) with the +5->Upgrade swap in a 3-scatter (untilUpgrade) free round, reverting to +5 once an Upgrade lands. - Three tiers by scatter count (3/4/5 -> t1/t2/t3). Ladder persistence: t2/t3 carry the ladder across the whole feature; t1 resets each spin until the Upgrade flips it persistent. Tier 3 opens with 1 free wheel spin. - Retrigger awards a flat +5 spins. Verified against debug sims: wheelSpin from->to math 0 errors (924 spins); every winning drop with a "?" pays at the boosted ladder (426/426); tier-3 opening spins fire once per tier-3 feature (10/10); start-multiplier sequences confirm persistence (t2 [1,6,6,22,32,36], t3 [21,31,51,57]) and t1 reset ([1,1,1,...]); 0 persistence bugs. Model is intentionally hot pre-optimizer. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds the three feature-entry bet modes on top of the base game:
- Bonus (100x): forces 3 scatters -> tier 1 (FR1, untilUpgrade).
- Super Bonus (200x): forces 4 scatters -> tier 2 (FR0, persistent).
- Mystery Bonus (500x): scatter_triggers {3:45,4:45,5:10} natively rolls the
tier 45/45/10 -> tier 1/2/3 (FR1/FR0/FR3).
Feature reel is tier-driven for natural AND bought features via fs_feature_reel
(get_current_distribution_conditions returns a per-tier copy — the TS provider
draws natural features from the per-tier pool too). A buy LOCKS its tier to the
forced opening scatter count; accumulation during the base cascade only upgrades
a natural trigger, never a buy (TS: level = boughtLevel).
Verified across debug sims: all four modes run clean; tier mixes are
bonus 100% t1, super_bonus 100% t2, mystery_bonus 46/43/11 (~45/45/10). Raw
(pre-optimizer) buy EVs are hot as expected (the optimizer down-weights to
0.967 in the full run).
Remaining for Milestone D: the two boost modes (3X Chance fee, Mystery Chance
stake), their reel exports + a WCAP reel, and the optimizer/cert run.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
All six SDK bet modes now exist. Adds: - 3X Chance (chance3x): a fee-priced (cost 3) base spin on a richer-scatter pool (BR0_chance3x, scatter 4.0 -> 7.4). Pays stay on the base bet. - Mystery Chance (mysteryChance): a stake-priced (cost 50) base spin. Priced via mode_bet_multiplier=50 — every pay scales x50 and evaluate_wincap scales the cap to 25,000x the 50x stake, so the cost cancels out of RTP and the thinned pool (BR0_mysteryChance) prices it. A "?" is planted on every base opening board (inside draw_board, before the reveal, so the client sees it). Boost reels were built from the TS model's own buildReelStrips (symbolDensity preserved) so the SDK opening boards match the fake-math. Verified across debug sims: Mystery Chance x50 pay scaling (2978/0) and a forced "?" on 100% of base boards; 3X Chance richer scatter (1.21 vs 0.86 scatters/board); all six modes run clean. Remaining for Milestone D: WCAP reel + wincap distributions, game_optimization per-mode targets, the full Rust `make run`, volatility gate, and locking the certified MODE_RTP. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Completes the cert pipeline: - WCAP reel (wild+mystery-rich) to force the 25,000x max-win tail; wired as the freegame reel for the wincap criteria via get_current_distribution_conditions. - wincap criteria added to every mode's distributions, with the stake-scaled cap (25,000x the 50x stake for Mystery Chance) matching win_manager's clamp. - game_optimization.py: per-mode RTP targets — base-style (wincap/0/freegame/ basegame, per-mode base/feature split) and buy-style (freegame + wincap tail), with far-tail suppression to hold the 3-star band. - run.py: all six modes at 100k (publish set). First cert result (20k/mode validation): the optimizer converged EVERY mode to RTP 0.967, and all six PASS the 3-star volatility gate after cost-normalization (cvar/cost <=229 vs 800; etl40b <=0.69 vs 0.9). Pricing validated end-to-end: average_win = 0.967*cost for fee (chance3x), stake (mysteryChance, cap 25,000x the 50x stake), and all buys. SHA-256 + payout hashes OK. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…6.70% 100k/mode publish run: every bet mode converges to RTP 0.967 and passes the 3-star volatility gate after cost-normalization (cvar/cost <=303 vs 800, etl40b <=0.69 vs 0.9, etl10k <=0.21 vs 0.8, prob5k <=0.001 vs 0.01). Pricing validated: avg payout = 0.967*cost for the fee mode (3X Chance), the stake mode (Mystery Chance, cap 25,000x the 50x stake) and the three buys. Publish files (books_*.jsonl.zst + lookUpTable_*_0.csv + index.json) generated. Only remaining item is locking the certified 96.70% into MODE_RTP in the game repo's gameConfig.ts (player-facing regulated copy). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add stake_game_two — certified math-SDK port (96.70%, all six modes)
* skull_raiders: Milestone A — base lines game (Stake port scaffold) New games/skull_raiders port of the Skull Raiders TS fake-math model. 5x5, 15 paylines, multiplier WILDs, wincap 10,000x, every mode targets 96%. Milestone A: grid + 15 paylines + paytable (displayed-x units, WILD pays on 5 only) + symbols + base (BR0) and feature (FR0, FR_tier3) reelstrips exported from the TS reel model (WILD->W, BONUS->S) + base LINES evaluation + stock free-spins loop + the base/feature multiplier-wild bags. Multiplier wilds roll in base too (not just feature); the stock "symbol" strategy already implements our rule (sum only >=2 wilds within a winning line, floor factor at 1). Units verified on 10k base sims: 10,472 unmultiplied line wins all exactly paytable*100; multiplied wins scale by the summed factor (e.g. H4 3-OAK 1x with two x2 wilds -> 200 = 1*100*2). Bonus mode raw feature EV ~98.5x at cost 100 (optimizer will pull to 96% in F). The forced-wincap distribution is intentionally deferred to Milestone F (no round can reach 10,000x until the feature + WCAP reel exist; forcing it now resamples forever). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * skull_raiders: Milestone C — three-tier free spins (clamp + retrigger cap + per-tier reels) update_freespin_amount override: scatter count 3/4/5 -> tier 1/2/3 (8/12/15 spins). A natural trigger (base game / ante) clamps to naturalMaxTier=2 so it can never award HIDDEN; a buy runs the exact forced tier (Mystery can roll tier 3). The feature reel follows (tier, is_buy): tier 3 -> FR3 (wild 22), bought tier 1/2 -> FRB (wild 14), natural tier 1/2 -> FR0 (wild 13.5), repointed per round in get_current_distribution_conditions. New FR_buy.csv reelstrip (exported via an added FR_buy branch in the game repo's export-reels.ts). update_fs_retrigger_amt override: retrigger adds the landed tier's spins, capped so total never exceeds bonus_max=30; once capped, further scatters add nothing. Verified on debug sims: base (natural) yields only tiers 1/2; bonus (buy) yields 1/2/3 with 8/12/15 spins; tot_fs never exceeds 30 (2 capped retriggers observed); tier-3 feature boards draw wild-22 density (6.13 wild/board incl padding) vs wild-14 for tiers 1/2 (~4.16). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * skull_raiders: Milestone D — raid wheel (ATTACK / STEAL custom event) The wheel is a base-game criteria (force_wheel in run_spin). A wheel round replaces the normal line spin: draw a land, strip scatters + natural wilds to lows, then ATTACK or STEAL (50/50) builds the whole win. ATTACK: redraw N paylines (weighted 1..5) — each with a payout symbol T filling the line and 1..3 wheel-multiplier wilds scattered along it — then score as ordinary line wins (one reel-0 conversion completes every payline through it, which is where the richness comes from). Land line-wins are broken first so only the redrawn lines pay; a full-wild fallback guarantees a win. STEAL: plant a present symbol on 3..5 cells + 1..3 banked wheel-multiplier wilds, then score position-agnostically (evaluate_steal in game_calculations: sweep every 3+ group, sum them, multiply by the summed wild factor; wilds are pure multipliers that don't pad counts and aren't stealable). Custom events wheelSpin / wheelConvert / wheelSteal (game_events.py) plus the standard win-accounting events so analysis/verification still computes payoutMultiplier. Verified on debug sims: attack 237/237 winInfo==finalWin; steal 263/263 board recompute match and event totals internally consistent; zero fs leak on wheel rounds; wheel win mean ~43x, max ~905x. The wheel criteria (quota 0.05 for now) is the optimizer's lever for the ~22.9% wheel RTP contribution. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * skull_raiders: Milestone E — all seven bet modes Define the full published mode set, matching the client's publishedModeName taxonomy (PR engineio#75) exactly: base 1x normal spin (wheel 0.0055, natural bonus clamped to tier 2) base_bonuschance 3x ante: boosted bonus frequency (natural path, clamped) base_wheelchance 5x ante: boosted wheel frequency base_bonuschance_wheelchance 8x ante: both boosts bonus_1 100x buy tier 1 (8 FS, FRB pool) bonus_2 150x buy tier 2 (12 FS, FRB pool) bonus_mystery 300x buy: tier roll {3:1,4:1,5:2} -> only route to HIDDEN (tier 3, FR3) Antes reuse the base spin math (same BR0/FR0 reels + wheel); they differ only in cost and in the per-criteria quotas that oversample the boosted feature (bonus for base_bonuschance, wheel for base_wheelchance). Buys force the feature and lock/roll the tier via scatter_triggers. Buy PRICES are the premium for immediacy and are decoupled from RTP — every mode targets 96%, weighted by the optimizer. Verified: all 7 modes generate books with no errors; index.json names + costs (1/3/5/8/100/150/300) match the client taxonomy; raw buy EVs ~95x/138x/266x are within optimizer reach of 96%. The forced wincap tail + per-mode opt targets come in Milestone F. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * skull_raiders: Milestone F — CERTIFIED (all seven modes 96%, 3-star gate) Feature wincap early-end (run_freespin stops + clamps at 10,000x) + a WILD-rich WCAP reel (43% wild) + a forced-max-win `wincap` distribution per mode, so every mode reaches exactly 10,000x fast (no resample hang). Optimizer targets (game_optimization.py): each mode's 0.96 is split across criteria that are BUCKETED by search_conditions (per docs/math_docs/optimization_section + force_info.md) — the fix that made the base modes converge: wincap by win value (first), freegame {"symbol":"scatter"}, wheel {"symbol":"wheel"} (via a record() tag added in run_wheel_round), "0" by win value, and basegame the searchless REMAINDER placed LAST (previously it greedily claimed freegame/wheel sims, collapsing them to ~0 and the mode to 0.35). RTP split follows the TS economics: base line ~30% / bonus feature ~43% / raid wheel ~23%; antes keep the non-advertised feature at natural rates and boost the advertised one; the combined ante boosts both. 100k-sims/mode cert run: ALL SEVEN MODES optimize to RTP 0.9600 and PASS the cost-normalized 3-star volatility gate (cvar/cost <=158, etl40b_n <=0.63, prob5k 0, rtp <=0.967) with no tail scaling needed. SHA-256 + payout hash OK (100000 entries/mode). publish_files + PAR sheet generated. index.json mode set + costs match the client publishedModeName taxonomy. The local un-normalized 3-star warnings are expected. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* skull_raiders: align book event schema with the client adapter (Phase 4/6)
Match the shapes the shipped client adapter (stakeFormat.ts StakeEvent union) reads, so book-player /
production replay of the SDK books is byte-identical to the fake-math path:
- wheelConvert cells: emit `symbol`/`mult` (were `name`/`multiplier`).
- wheelSteal wins: emit `count` (was `kind`).
- freeSpinRetrigger: add `level` and `added` (spins added this retrigger) alongside `capped`.
- updateFreeSpin: attach the running round `totalWin` so the feature's live win total renders (the
adapter falls back to 0 without it).
Pure event formatting — no RNG consumed, so outcomes/payouts and therefore every mode's optimized RTP
(0.96) and the volatility gate are unchanged; only the book bytes + verification hashes refresh on the
next run. Verified on debug sims: all five event shapes now match the adapter contract exactly.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* skull_raiders: STEAL must not emit winInfo (wilds blacked out on replay)
A STEAL wheel round pays via the wheelSteal event (every 3+ group x the summed wild factor), NOT via
paylines. _score_steal also emitted a winInfo (+ setWin), which the client maps to its ordinary line-win
presentation — lighting only the group cells and leaving the multiplier WILDs unlit ("blacked out"),
diverging from the fake-math path. The win value was already correct (wheelSteal.total includes the
multiplier); only the presentation was wrong.
Emit exactly the fake-math STEAL stream: reveal / wheelSpin / wheelSteal / setTotalWin / finalWin (no
winInfo, no setWin). ATTACK is unchanged — it pays as lines, so its winInfo is correct.
Pure event change (no RNG/payout change) — every mode's RTP stays 0.96; only the book bytes + hashes
refresh. Verified the STEAL sequence now matches fake-math exactly.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…ins (#6) The wilds stayed "blacked out" during a STEAL because the wheelSteal event's `positions` carried only the planted present-symbol cells (3 of them), but the client's stealReveal derives BOTH the non-wild ladder AND the WILD x-multiplier phase from `positions` (members = positions.map(...); wild = members.filter(W)). With no wild cell in positions, the WILD phase never ran and the dimmed wilds were never re-lit. evaluate_steal now returns the full stolen set — every winning group's cells plus every wild coin — and _score_steal passes that as the event positions (matching fake-math's evaluateSteal.positions, which includes the wild cells). Verified end-to-end on the book-player replaying a real SDK steal book: the WILD lights and the x3 multiplier applies (win 19.50x = 6.50 base x 3), where before it was blacked out. Pure event change (no RNG/payout change) — every mode's RTP stays 0.96. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…T.md) (#7) Adds a "Port lessons" note referencing the authoritative write-up in the game repo's docs/STAKE_PORT.md: read the SDK optimization + force docs first, bucket criteria by search_conditions (basegame remainder last), gate cert/upload on `npm run parity`, smoke-test the real paths, and never run the 100k cert to discover a bug. Recorded here so it's discoverable from the port itself on any machine. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…t running_bet_win update_freespin() annotated each updateFreeSpin event's live totalWin with the round-scoped running_bet_win (base + feature), so the free-spins counter opened on the triggering base-spin line win instead of 0 — leaking the base win into the feature tally. freegame_wins is the feature-only accumulator, matching the fake-math reference and the freeSpinEnd outro (which already uses it). Presentation-only: finalWin, payouts, RTP and the lookup tables are unchanged (only this event field's bytes differ). Books regenerated and re-verified; game-repo parity now passes its "first free-spin totalWin == 0" invariant. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Author
|
Opened against the wrong base — this game-specific change belongs in the fork, not upstream. Closing and re-opening within mdicillo/math-sdk. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
game_override.py::update_freespinannotated eachupdateFreeSpinevent's livetotalWinwith the round-scopedrunning_bet_win(base + feature). When a base spin paid a line and triggered the feature, the free-spins counter therefore opened on that base win instead of 0 — the base win leaked into the feature tally, and the on-screen TOTAL WIN dropped back at the outro.Fix: use the feature-scoped
freegame_wins, matching the fake-math reference model and thefreeSpinEndoutro (which already reportsfreegame_wins).Blast radius — presentation only
finalWin, per-book payouts, RTP and the optimized lookup tables are unchanged — only this one event field's bytes differ. The sims are seeded, so regenerating produced identical outcomes with the corrected annotation. Books regenerated viamake run GAME=skull_raiders; SHA-256 + payout hashes verified, 100k entries/mode.Verification
In the game repo (castle-raid),
npm run parityagainst the regenerated books:totalWin== 0": was violated in thousands of books, now 0 violations across all 7 modesfinalWinstill equals base + feature.Pairs with the castle-raid PR that reverts the interim client-side workaround and adds the parity value-check guard.
🤖 Generated with Claude Code