Book of Odin — Math Spec, book-of-odin-v2

Draft draft-v0.1. DEV ONLY. NOT FOR PRODUCTION. NOT FOR CERTIFICATION. Every figure here was measured from the pack named below. Internal measurement is not external certification, and this version is not frozen.

1. Identity

FieldValue
Game IDbook-of-odin (same game as v1)
Config versionbook-of-odin-v2
Math versiondraft-v0.1
Canonical packlibs/mathengine/games/book_of_odin/book-of-odin.yaml
Pack artefact SHA-256d42a0d819b9fd9e8fa52b7646925778fd82192e4512be9dfd88839b2f85e8f01
Canonical config hashb3ac1b12f3225dafb6f098a01ba2ae3237383a523e217a84abe22d8544cc4f40
Seed migrationapps/game-engine/migrations/024_seed_book_of_odin_v2.sql
Mechanic versionhold_and_win/v2
RNG algorithmsha256-trunc64-be/go1-alfg#1

The canonical config hash is computed by libs/mathengine/canonical, the repository's single canonicalisation-and-hash implementation. The artefact SHA-256 identifies the source file; the canonical hash is the identity the runtime records on every durable feature row. They answer different questions and both are recorded.

book-of-odin-v1 is untouched. Migration 008 still hashes e87b3e29396ac8ff05196ae6d56fe46e7282a53b77388c1e0fc9af5d33346990. Both versions share one game_id and coexist as separate game_configs rows.

2. Reel model

Strips are weighted entries, not stops. One weighted draw per reel picks an entry index and the visible column is that entry plus the next two, wrapping. Rows on a reel are therefore correlated, and only 20 columns are reachable per reel.

The layout is design-locked so the trigger frequencies stay exactly analysable:

EntriesSymbolEffect
0, 1, 2COINstop 0 shows 3 COIN, stop 1 shows 2, stop 2 shows 1
18, 19regularthe wrap: stop 18 shows 1 COIN, stop 19 shows 2
10BOOKstops 8, 9 and 10 are the only ones that reveal it

So P(BOOK visible on a reel) = (w8 + w9 + w10) / W, and the COIN count per reel takes only five values. Both totals are exact integer convolutions of five per-reel distributions — no sampling. Pinned by TestReelLayoutIsDesignLocked and TestReelWindowCounts.

3. Paytable

Payouts are multipliers of the line stake; a round total divides by the 10 paylines.

Symbol345
BOOK (wild + scatter)2000
A25125500
B22110440
C1680325
D1470280
E840140
F735120
G630100
H52580
I31550
COIN

BOOK declares only a 5-of-a-kind, and that is structural rather than an omission. The line evaluator takes the first non-wild symbol as the match symbol, so leading BOOKs always fold into the symbol that follows them; BOOK is the match symbol only when the whole line is BOOK. Declaring 2-, 3- and 4-entries would advertise rows no grid can reach. Pinned by TestWildShortRunsAreStructurallyUnreachable.

COIN has no line payout at any count. A COIN line pay would add base value to exactly the rounds that trigger the Hold & Win, and no later measurement could separate the two.

4. Features

4.1 Free Spins

3+ BOOK award 10 free spins. One of AI is selected by the server before the series and expands for its whole duration. Retrigger is disabled in this version. The nine variants are equally weighted, so the selection-weighted aggregate is a plain mean.

expanding_symbol.TriggerSymbolID is deliberately empty. The selected symbol reaches the executor as an override; the configured value is what applies when there is no override — that is, in the base game. game-engine keeps expanding_symbol on the paid base path, so naming a real symbol there makes it expand on ordinary paid spins. Naming BOOK — the obvious choice, and what v1 does — takes the base game from 39.51% to 148.66% RTP. Measured, not estimated; pinned by TestBaseGameDoesNotExpand.

4.2 Hold & Win (hold_and_win/v2)

6+ COIN open the feature, from a base spin or a free spin. Triggering coins carry into their own cells. 3 respins; any new coin resets to 3; a miss decrements. Termination at 0 respins or 15 locked cells. Ordinary award is the sum of locked coin multipliers; a full grid pays that sum plus the GRAND.

ParameterValue
Landing chance43 000 PPM (4.3% per empty cell per respin)
Coin values / weights1×38, 2×27, 3×17, 5×11, 10×4, 20×2, 25×1, 50×1, 100×1 (total 102)
E[coin]4.4412×
Prize poolregular only — no MINI/MINOR/MAJOR
GRAND500×
Mechanic cap5000×

The v1 mechanic cannot express either the regular-only pool (it requires all four kinds with positive weight) or the additive GRAND (it replaces the sum). That is why hold_and_win/v2 exists. v1 is unchanged and still frozen behind magic-vault-5x3's and hold_and_win_reference's golden vectors.

5. RTP decomposition

Whole-game target 98.00%, tolerance ±0.10 pp.

5.1 Exact terms

Full enumeration of the reel-stop state space (20⁵ = 3 200 000 grids per reelset) and integer convolutions of the reel windows. No sampling error.

TermValueMethod
Base line RTP39.5138%enumeration, features stripped (what a paid base spin runs)
Free Spins line + expansion30.6726%enumeration of each variant's reelset with its symbol expanded
P(BOOK ≥ 3) base0.01090001 in 91.7 base spins
P(COIN ≥ 6) base0.00684781 in 146.0 base spins
P(COIN ≥ 6) per free spin0.0068478identical on every variant reelset
E[initial locked | trigger]6.3935 cells
Selection-weighted FS spin value2.813990×

Per-variant free-spin value and ceiling:

VariantSpin valueMax single spin
FS_BOOK_A3.832334500×
FS_BOOK_B3.641389440×
FS_BOOK_C3.631222325×
FS_BOOK_D4.283024280×
FS_BOOK_E3.120581246×
FS_BOOK_F2.616011246×
FS_BOOK_G1.658982246×
FS_BOOK_H1.446544247×
FS_BOOK_I1.095821248×

5.2 Sampled terms

Seeded Monte Carlo through RunHoldAndWinToCompletion — the same Start/Step state machine the durable runtime steps through one request at a time.

ParameterValue
Seedbook-of-odin-probe-hw (:base, :fs:<variant>)
Sample size5 000 000 features
Runnergo test -tags bodraft ./libs/mathengine/games/book_of_odin
Backend revisionecd6a70ade8a63e8ce35dde6b3a387f4d8df8504
TermValue95% CI
Conditional mean, base entry36.6231×[36.5914, 36.6549]
Conditional mean, FS entry36.6120×
Full-grid rate, base entry0.0007522
Base-origin H&W RTP25.0787%±0.0217 pp
FS-origin H&W RTP2.7328%±0.0024 pp
of which GRAND0.2869%inside the two above

5.3 Reconciliation

whole game = base lines + FS lines/expansion + base-origin H&W + FS-origin H&W
           = 0.395138   + 0.306726          + 0.250787        + 0.027328
           = 0.979979   =  97.9979%

Target 98.0000%, deviation −0.0021 pp, tolerance ±0.10 pp → within tolerance.

GRAND is not a fifth addend. Under v2 it is paid on top of the coin sum inside the feature, so it is already contained in the two Hold & Win terms; it is reported separately for auditability only.

5.4 Why the tolerance is ±0.10 pp

Three things bound how precisely this game can be placed on a target:

  1. Measurement. The line terms are exact, so the whole game's uncertainty is the Hold & Win conditional means', scaled by their trigger probabilities: ±0.0241 pp at 95% on 5M samples. A tolerance below about ±0.05 pp would assert a difference the evidence cannot resolve.
  2. The integer lattice. Payouts and reel weights are integers, so reachable RTP values form a lattice. The finest useful lever moves the total by 0.0124 pp per unit.
  3. Model residual. The free-spins term is E[max(pre-expansion, post-expansion)], because the expansion never reduces a win. That is linear in the paytable only where expansion helps, which is why the final tuning step was verified by re-enumerating rather than by arithmetic.

±0.10 pp is roughly four times the measurement uncertainty and eight times the lattice step.

6. Feature frequencies and nesting

QuantityValueMethod
Free Spins trigger1 in 91.7 base spinsexact
Hold & Win trigger, base1 in 146.0 base spinsexact
Simultaneous BOOK + COIN1 in 197 410 base spinsexact
E[H&W launches per 10-spin series]0.068478exact
P(0 H&W in a series)0.9335943exact, Binomial(10, p)
P(1 H&W in a series)0.0643712exact
P(2 H&W in a series)0.0019973exact
P(≥2 H&W in a series)0.0020344exact

Each free spin is an independent trial at the same probability — the nested feature consumes no free spin and does not change the variant reelset — so the count per series is exactly Binomial(10, p). The modal series launches none; two or more is a genuine tail at 1 in 491.

7. Max win

CeilingValueStatus
Round cap (Paytable.MaxWinMultiplier)5000×never binds
Max single base round259.4×exact, enumerated; golden witness
Max single free spin500×exact, enumerated; witnessed at seed bo-fs:FS_BOOK_A:293
Max Hold & Win award2000×bound: 15 cells × 100× + 500× GRAND

The Hold & Win ceiling is an analytic bound rather than a witnessed value, and the reason is recorded: all fifteen locked cells would have to draw the 100× coin, which is (1/102)¹⁵ ≈ 7.4 × 10⁻³¹ before even accounting for filling the grid. No seed search can exhibit it, and the feature is a stochastic state machine rather than a fixed state space, so enumeration is impossible. Per the change request, a documented reason is recorded in place of a proof.

No single round can reach the 5000× cap. That is what makes the decomposition valid: the cap never redistributes value between terms, so the four terms are genuinely additive.

8. Buy Free Spins

Priced per variant, because the player chooses which symbol expands and the variants are not equally rich. Each price is that variant's own expected value divided by the 98% target and rounded up, so no option can return more than the game does. There is no direct Buy Hold & Win in this version.

OptionPriceEVOption RTP
FS_BOOK_A41.67×40.8304×97.985%
FS_BOOK_B39.72×38.9210×97.988%
FS_BOOK_C39.62×38.8193×97.979%
FS_BOOK_D46.27×45.3373×97.984%
FS_BOOK_E34.41×33.7129×97.974%
FS_BOOK_F29.26×28.6672×97.974%
FS_BOOK_G19.49×19.0969×97.983%
FS_BOOK_H17.32×16.9725×97.994%
FS_BOOK_I13.75×13.4653×97.930%

A bought series is an ordinary free-spins series: it opens Hold & Win on the same 6+ COIN rule, and that term is included in every EV above.

9. Which driver may be trusted for which term

RunBatch cannot measure this pack's whole-game RTP, and the reason is a real limitation of the atomic free-spins driver rather than a property of these reels. FreeSpinsExecutor spins the base reel set and expands the configured symbol; it ignores FreeSpinsConfig.Variants entirely. Book of Odin's free spins run on the selected variant's reelset and expand the selected symbol, which is what the runtime does through RunFreeSpinRoundVariantExt.

mathengine is consistent about this elsewhere — the exact path refuses variant configs by name (ReasonFSVariants) rather than approximating them. The atomic driver does not refuse; it silently substitutes different reels. Proven, not described, by reference_driver_test.go:TestAtomicFreeSpinsDriverIgnoresVariants.

Every term above was therefore measured on a driver that is faithful to the runtime. Nothing in this spec comes from RunBatch.

10. Open items

  • The pack is Draft and unfrozen; no freeze evidence has been assembled.
  • Volatility metrics beyond the win-distribution ceilings are not yet recorded.
  • Whole-game simulation remains unavailable until the atomic free-spins driver learns variants (see §9). The decomposition is more precise than a simulation would be, but a simulation is still the independent cross-check a freeze would want.