Classic Fruits 3×3 — Math Specification v2 (Draft v0.1)
DRAFT v0.1 — NOT FROZEN, NOT CERTIFIED, NOT PUBLISHED. This document describes a new canonical math version of the same Game ID that implements GDD
0.2.0. Frozen v1 and its spec are unchanged and remain the historical record of the superseded 95.3967% / 65× game. The 10,000,000-round run below is a measurement, not a balancing pass and not certification evidence.
1. Identity and provenance
The config SHA-256 hashes the embedded designer YAML. The canonical hash is what a runtime gate compares and is unaffected by comments or formatting.
Those two sentences did work on 2026-08-23: the config SHA-256 was re-recorded from 770f7a53… to
the value above after a comment-only correction to the pack's gdd: header, and the canonical hash
did not change. The math is the same math; every figure below still describes it. A change to the
canonical hash would not be that, and would invalidate all of them.
Frozen v1 remains at libs/mathengine/games/classic_fruits_3x3/ with config SHA-256
735ef19c8acee914c04aca048e36f54296538c94653efe14cd916aac6fe69efa, untouched.
2. Why this is a new version rather than an edit
GDD 0.2.0 changes every payout-affecting rule of frozen v1: nine regular symbols instead of eight
plus a Wild, full diagonals instead of two V-shapes, three-of-a-kind only instead of a two-symbol
L1 award, a count-anywhere jackpot that v1 does not have, and a 1300× ceiling instead of 65×.
Nothing about v1 survives except the grid size and the payline count, so this is a new math version
by definition. v1's hash, golden vectors and exact evidence are cited by its own spec and remain
valid statements about v1.
2a. GDD 0.2.1 ratifies the encoding
GDD 0.2.1 added §5 Backend paytable units, which states the ×5 per-line storage convention as a
normative rule rather than an inference: the YAML stores each displayed multiplier times five, the
conversion happens exactly once, and jackpot awards are neither multiplied nor divided by the
payline count. §7 and §8 were amended to match, and §17 blocker 4 now requires that the internal
IsScatter configuration must not leak to the player as a Scatter feature.
Nothing in this specification changed as a result — the pack already implemented all of it, and
0.2.1 is a clarification of 0.2.0, not a rule change. It is recorded here because the encoding
stopped being this document's assumption and became the GDD's.
§5 was subsequently narrowed to say what it meant. It now scopes the 60× rule to what the player
is SHOWN — paytable, help screen, win presentation — states that wins[].multiplier is a legacy
per-line field and not a display value, and names spin.payout as the single source of the round
total. The wire was not repriced, because the units are the same for every line game in the
catalogue and one game must not create a second dialect. See §10.6.
3. Bet and payout model
For line awards (L_1,\dots,L_5) in per-line units and jackpot award (J) in total-bet units:
[ X = \min\left(1300,; \frac{\sum_{i=1}^{5}L_i}{5} + J\right) ]
GDD §7 requires that line awards are not divided by the number of active paylines. The engine
divides unconditionally, so the config carries every GDD figure ×5: a SEVEN three-of-a-kind is
300 in the pack and pays 300/5 = 60× of total bet. This reproduces the GDD exactly without an
engine change, and it is what makes the declared maximum work out:
4. Symbols and paytable
No Wild, no substitution, no two-symbol awards, no bonus symbol. Only Payouts[3] is declared, so
a two-of-a-kind line resolves to zero by construction rather than by a rule the engine has to apply.
5. Paylines
All five are equally likely, and that is a property of the reel design rather than a coincidence: every stop carries weight 1 and all three reels carry identical symbol counts, so each row's marginal distribution is the same and a diagonal is worth exactly what a horizontal is worth.
6. Reel model
Each reel is a 70-stop virtual strip with weight 1 on every stop. One weighted draw per reel picks a
stop; the visible column is that stop and the next two, wrapping. Rows within a column are therefore
correlated, and only 70 columns per reel are reachable. State space is 70³ = 343,000, which
mathengine.ExactCalculate enumerates exactly — every figure in §7 is closed form, with no sampling
error.
Per-reel symbol counts, identical on all three reels (orderings differ):
Two strip invariants carry the jackpot math and are pinned by TestStripInvariants:
- Each reel holds exactly one contiguous, isolated run of three
SEVENs. Hence per reel P(three SEVENs in the column) = 1/70, P(exactly two) = 2/70, P(exactly one) = 2/70. - No other symbol occupies three consecutive stops, so
SEVENis the only symbol that can fill a column and therefore the only one that can win all five paylines at once.
The correlation is load-bearing. Under an independent-cell draw the Super jackpot would need P(SEVEN)⁹ ≈ 1 in 10¹² at this symbol frequency and would be unreachable in practice; the stop-window model is what puts 7/8/9 SEVENS at a designable frequency at all.
7. Exact results
Enumerated over all 343,000 states; probability mass 1.0 to 1e-13.
SEVENS jackpot
Closed form, with c = 1/70 the per-reel probability of a full-SEVEN column:
Against the GDD target
8. 10,000,000-round simulation
A cross-check that the runtime spin path reproduces the enumeration, and an observation of the jackpot tiers at their real frequencies. Not a balancing pass and not certification evidence.
Per-payline win frequency
Jackpot frequency
All three are within Poisson noise of the enumerated frequency.
Visible SEVENS distribution
9. Verification performed
The exact figures were additionally reproduced by an independent enumerator written outside the Go
engine; it agreed with ExactCalculate on RTP, hit frequency, σ and the maximum to every printed
digit.
10. Open items
-
Hit frequency is 11.75%, against 33.48% for frozen v1. This is structural — three reels, three-of-a-kind only, nine symbols and no wild — not a tuning slip, but the GDD sets no hit-rate target and Product has not accepted this one. σ = 4.32 puts the game well above v1's 2.57.
-
RTP is 0.0115 pp below the GDD's 96.08% in exact terms. That target is FireFruit Sevens Classic's published figure, not a number our reels owe anything to; the deployed help screen must show our measured RTP (GDD §14).
-
GDD §7's payline-division question is resolved in the config, not in the GDD. The ×5 per-line convention is recorded here and pinned by a test, but the GDD still carries the open sentence "must be confirmed against a captured reference round".
-
Jackpot additivity and the 1300× composition are still design inferences. GDD §8 flags both as pending verification against the reference wire result or an authoritative PAR sheet.
-
Client naming: closed, on both halves of the contract.
SEVENcarriesIsScatter: trueas the engine hook for count-anywhere evaluation, and neither that flag norScatterPayoutswas renamed — both are hashed into the identity of every math version ever recorded. What changed is how they are PUBLISHED:- the spin payload answers
wins[].type: "jackpot"and never"scatter"; GET /config/{game_id}publishesjackpot_symbols: ["SEVEN"]andjackpot_payouts, withscatter_symbolsempty and noscatter_payoutsat all.
The split is decided by
mathengine.JackpotSymbolIDsand applied in BOTH halves — the win event and the config contract read the same verdict, so ajackpotentry always names ajackpot_payoutstable the client was given. A symbol whose count-anywhere award opens nothing is a jackpot; one named by ANY feature — free spins, hold-and-win, hold-and-spin, bonus, super scatter, expanding symbol, coin collect, respin wild, multiplier charge, layout multiplier — is not.It fails closed, and the default is the safety property: every feature type the engine knows is answered explicitly and anything else denies, so a mechanic added later cannot quietly promote its trigger symbol to a jackpot. A declared feature whose trigger symbols cannot be resolved denies too. The error being avoided has a player-visible cost — the symbol would be dropped from
scatter_symbolsand its table moved, so the help screen would stop saying how the feature is entered.This closes GDD
0.2.1§17.4. No config byte and no canonical hash moved;config_hashis unchanged for every game without a jackpot, measured rather than assumed. - the spin payload answers
-
wins[].payoutis per-payline for line entries and does not sum tospin.payout; on the 1300× round the array sums to 2500×. Pre-existing for every line game in the catalogue, so the decision was to document rather than reprice — one game must not create a second dialect of one field.openapi.yamlnow states that the round total has exactly one source,spin.payout, and that this array must never be summed; GDD §5 was narrowed to match, scoping its60×rule to what the player is shown and namingwins[].multiplieras a legacy field that is not a display value. Pinned byTestClassicFruitsV2_LineWinUnitsDoNotReconcileandTestLineWinWireUnitsArePreExisting. -
Merged, not published. The pack is on backend
mainand DEV runs the code. It is registered where evidence is graded — the canonical pack-hash gate, the parity corpus and the math-studio authoring registry — and nowhere that serves a player: no migration seeds it, no config cache resolves it, no dev gate admits it, and the Game IDclassic-fruits-3x3still resolves to frozen v1 on every tier.Publishing is
scripts/seed-classic-fruits-v2.sh(doc/dev-api.md§6a.3). Two properties of that act belong in this specification because they are properties of the math version, not of the script: v2 shares its Game ID with frozen v1, so a tier serves one or the other and never both; and the published row must not beclassic-fruits-3x3-v1, becausegame_configs.versionis a primary key and publishing there overwrites frozen v1's config in place instead of superseding it. The script enforces the second and states the first. -
No PAR sheet, no certification evidence, no compliance review.
11. Lifecycle
draft — not frozen. The config may still move. When it does, every figure in this document, the
simulation report and the golden vectors describe the old bytes and must be re-derived.