Skip to content

skull_raiders: Stake Engine math port (certified, all modes 96%) - #115

Closed
mdicillo wants to merge 19 commits into
engineio:mainfrom
mdicillo:skull_raiders-math-port
Closed

skull_raiders: Stake Engine math port (certified, all modes 96%)#115
mdicillo wants to merge 19 commits into
engineio:mainfrom
mdicillo:skull_raiders-math-port

Conversation

@mdicillo

@mdicillo mdicillo commented Sep 1, 2026

Copy link
Copy Markdown

Adds games/skull_raiders — the Stake Engine math-SDK port of the Skull Raiders (castle-raid) TS
fake-math model. 5×5, 15 paylines, multiplier WILDs, a three-tier free-spins bonus, a random raid-wheel
event (ATTACK/STEAL), two per-spin ante boosts, and a mystery bonus buy. Every bet mode certifies at
96.00% RTP
and passes the cost-normalized 3-star volatility gate. Wincap 10,000×.

Built and verified in milestones (one commit each):

  • A — base lines: grid, 15 paylines, paytable (displayed-× units, WILD pays on 5), base/feature
    reels exported from the TS reel model, multiplier-wild bags (base tame / feature fat, summed within a
    line via the "symbol" strategy). Units verified: line wins == paytable×100.
  • C — three-tier free spins: 3/4/5 scatters → tier 1/2/3 (8/12/15 spins); natural triggers clamp to
    tier 2 (HIDDEN is Mystery-only); retrigger capped at 30 spins; per-tier feature reels (natural FR0 /
    bought FRB / tier-3 FR3).
  • D — raid wheel: base-game criteria (force_wheel); ATTACK redraws whole paylines, STEAL sweeps
    every 3+ group position-agnostically (wilds as pure multipliers); custom wheelSpin/wheelConvert/ wheelSteal events + standard win-accounting. 500/500 recompute checks matched.
  • E — seven bet modes: base (1×), antes base_bonuschance (3×), base_wheelchance (5×),
    base_bonuschance_wheelchance (8×), buys bonus_1 (100×), bonus_2 (150×), bonus_mystery (300×).
    index.json names + costs match the client's publishedModeName taxonomy exactly.
  • F — certified: feature wincap early-end + a WILD-rich WCAP reel so every mode reaches 10,000×;
    per-mode opt_params summing to 0.96 with correct criteria bucketing via search_conditions (the fix
    that made the multi-criteria base modes converge — basegame is the searchless remainder, placed
    last; freegame/wheel matched by their record() tags). RTP split faithful to the TS economics
    (base line ~30% / feature ~43% / wheel ~23%).

Cert results (100k sims/mode)

mode cost RTP cvar/cost etl40b_n prob5k
base 1 0.9600 158 0.63 0
base_bonuschance 3 0.9600 85 0.47 0
base_wheelchance 5 0.9600 44 0.02 0
base_bonuschance_wheelchance 8 0.9600 35 0.01 0
bonus_1 100 0.9600 17 0.00 0
bonus_2 150 0.9600 12 0.00 0
bonus_mystery 300 0.9600 12 0.00 0

3-star limits (cost-normalized): cvar ≤ 800, etl40b ≤ 0.9, prob5k ≤ 1%, rtp ≤ 0.967 — all pass.
SHA-256 + payout hash OK (100000 entries/mode). Buy prices are the premium for immediacy, decoupled from
RTP — the optimizer weights each mode's outcomes to 96% independently. The local un-normalized 3-star
warnings make run prints for the high-cost modes are expected (platform gates on the normalized values).

Reels are regenerated from the game repo via npm run reels:export (library/ stays gitignored).

🤖 Generated with Claude Code

Mike Dicillo and others added 19 commits July 13, 2026 15:28
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)
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>
… 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>
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>
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>
…ate)

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>
@mdicillo

mdicillo commented Sep 1, 2026

Copy link
Copy Markdown
Author

Closing — opened against the wrong base repository by mistake. This belongs on the mdicillo/math-sdk fork, not upstream. Reopening there.

@mdicillo mdicillo closed this Sep 1, 2026
@mdicillo
mdicillo deleted the skull_raiders-math-port branch September 1, 2026 16:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant