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

FieldValue
Game IDclassic-fruits-3x3
Math versionv2-draft-0.1
GDDclassic-fruits-3x3-gdd.md — version 0.2.1
Canonical configbackend/libs/mathengine/games/classic_fruits_3x3_v2/classic-fruits-3x3-v2.yaml
Config SHA-256f429ff05acdd305fb2880066bbfa897492dea56006728b4db9c8941371ec552c
Canonical config hashf66827eccf7bfcbf2d9791b70ec9d8b4cfa3779839d64383b371c808b6efdfa8
Backend commit8c1ca280420b8ed7478ca17948e54dd0f33f0d63 (main)
Evidence recorded atf4cfa83a3560cfdad92bc667fbb8670c9d30599c — pack bytes identical to the merged commit
Reference modellibs/mathengine
RNG algorithmsha256-trunc64-be/go1-alfg#1
Game typeslot
Authored2026-08-23

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

PropertyDefinition
Active lines5, fixed
Config paytable unitPer-line multiplier
Display/result unitMultiplier of total bet
Line normalizationSum of line pays divided by 5 active lines
Jackpot unitMultiplier of total bet, not divided
Maximum-win cap1300× total bet
FeaturesNone

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:

Super jackpot                     1000×
five SEVEN lines   5 × (300/5)  =  300×
---------------------------------------
maximum                           1300×

4. Symbols and paytable

SymbolConfig Payouts[3] (per line)Total-bet multiplierGDD §5
SEVEN30060×60×
BELL20040×40×
BAR10020×20×
GRAPES7515×15×
WATERMELON7515×15×
PLUM35
LEMON35
ORANGE35
CHERRY35

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

LinePatternReel rowsExact win frequency
1Top horizontal[0,0,0]2.521866%
2Middle horizontal[1,1,1]2.521866%
3Bottom horizontal[2,2,2]2.521866%
4Descending diagonal[0,1,2]2.521866%
5Ascending diagonal[2,1,0]2.521866%

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):

SymbolStopsMarginal per cell
SEVEN34.2857%
BELL34.2857%
BAR34.2857%
GRAPES57.1429%
WATERMELON57.1429%
PLUM1318.5714%
LEMON1318.5714%
ORANGE1318.5714%
CHERRY1217.1429%
Total70100%

Two strip invariants carry the jackpot math and are pinned by TestStripInvariants:

  1. 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.
  2. No other symbol occupies three consecutive stops, so SEVEN is 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.

MetricExact value
RTP96.068513%
— payline component95.077259%
— SEVENS jackpot component0.991254%
Hit frequency11.748980% (1 in 8.51)
Mean win given a hit8.177× total bet
σ per round4.323568
Maximum win1300× reachable
Cap probability0

SEVENS jackpot

SEVENSTierAwardStatesExact probability1 inRTP contribution
7Major50×180.00524781%19,0560.262391%
8Grand250×60.00174927%57,1670.437318%
9Super1000×10.00029155%343,0000.291545%

Closed form, with c = 1/70 the per-reel probability of a full-SEVEN column:

P(9) = c³                      = 1/343,000
P(8) = 3·c²·(2c)               = 6/343,000
P(7) = 3·c²·(2c) + 3·c·(2c)²   = 18/343,000
jackpot RTP = (50·18 + 250·6 + 1000·1)/343,000 = 3400/343,000

Against the GDD target

ValueΔ vs GDD 96.08%
Exact RTP96.068513%−0.0115 pp

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.

ParameterValue
Rounds10,000,000
Run seedcf3x3-v2-draft-0.1
Shards16, fixed (not GOMAXPROCS)
Bet per round1.0
Harnesslibs/mathengine/games/classic_fruits_3x3_v2/sim_test.goTestDraftSimulation
Reportevidence/simulation/2026-08-23-draft-v0.1-10m.json
MetricMeasuredExactΔ
RTP95.97038%96.068513%−0.0981 pp (0.73 σ)
RTP 95% CI[95.70550%, 96.23526%]contains the exact value
Standard error0.135147 pp
— payline component95.02388%95.077259%−0.0534 pp
— jackpot component0.94650%0.991254%−0.0448 pp
Hit frequency11.74253%11.748980%−0.0065 pp
σ per round4.2737274.323568−0.049
Maximum win seen1300× (round 20,418)1300×equal
Rounds truncated by the cap00

Per-payline win frequency

LineWinsMeasuredExact
1 [0,0,0]252,0172.52017%2.521866%
2 [1,1,1]252,4292.52429%2.521866%
3 [2,2,2]251,7762.51776%2.521866%
4 [0,1,2]251,8702.51870%2.521866%
5 [2,1,0]251,9362.51936%2.521866%

Jackpot frequency

SEVENSTierHitsMeasured 1 inExact 1 inExpected hitsz
7Major54818,24819,056524.8+1.01
8Grand15763,69457,167174.9−1.36
9Super28357,143343,00029.2−0.22

All three are within Poisson noise of the enumerated frequency.

Visible SEVENS distribution

SEVENSRoundsFrequency
08,005,87480.05874%
1738,6237.38623%
2761,7867.61786%
3415,9894.15989%
446,3630.46363%
524,0490.24049%
66,5830.06583%
75480.00548%
81570.00157%
9280.00028%

9. Verification performed

CheckResultWhere
Config parses and validatesPassTestSmoke path via every test in the package
Grid and five paylines match GDD §6PassTestGridAndLinesMatchGDD
Nine-symbol paytable matches GDD §5PassTestPaytableMatchesGDD
Three-of-a-kind only, no wildPassTestPaytableMatchesGDD
Jackpot tiers match GDD §8PassTestJackpotMatchesGDD
No features declaredPassTestNoFeatures
Strip invariantsPassTestStripInvariants
Declared cap = reachable maximum, cap never truncatesPassTestMaxWinIsTheReachableCeiling
Exact figures pinnedPassTestExactFigures
Independent re-enumeration of RTP, hit, σ, per-line and jackpotPassTestExactBreakdownByEnumeration
Six golden vectors, payouts derived by hand from the GDDPassTestGoldenVectors
Determinism: same seed and round index reproduce grid and moneyPassTestDeterminism
Reproducibility: 1M run identical at GOMAXPROCS 4 and 11Passmanual, diff of two reports
Config SHA-256, canonical hash and RNG algorithm pinnedPassTestDraftIdentifiers, TestDraftConfigHash, TestDraftCanonicalHash
10M measurement lands on the exact RTPPass (0.73 σ)TestDraftSimulation
Reference↔runtime parity, six vectors, both sides of one corpusPassparity_test.go, parity_vectors_test.go
Payout composition: lines in total-bet units plus the jackpot, paid oncePassTestClassicFruitsV2_PayoutComposition
1300× arrives whole; the ceiling never truncates itPassTestClassicFruitsV2_MaxWinComposition
Replay reproduces the round and moves no moneyPassTestClassicFruitsV2_ReplayIsIdenticalAndPaysNothing
Forced-grid path evaluates the same rules, six SEVENS pays nothingPassTestClassicFruitsV2_MockGridPath
Removing, doubling or mis-scaling the jackpot fails the corpusPassTestClassicFruitsV2_JackpotAssertionsAreLoadBearing
Jackpot survives Postgres, settles at 130000 minor units, retry pays oncePassTestCF2Integration_* (live Postgres)
Jackpot is wired as jackpot, never scatterPassTestClassicFruitsV2_JackpotIsNotWiredAsScatter
Config endpoint publishes a jackpot, not a scatterPassTestClassicFruitsV2_ConfigEndpointPublishesAJackpotNotAScatter
The win type and the config contract agree on one symbol, both directionsPassTestWireWinType_AgreesWithTheConfigContract
A real scatter stays a scatter — nine other feature types includedPassTestScatterWinType_MatchesTheClassification, TestJackpotSymbolIDs_OtherFeatureTriggersAreNotJackpots
An unknown feature type denies; symbol-agnostic ones do notPassTestJackpotSymbolIDs_UnknownFeatureTypeDenies, ..._SymbolAgnosticFeaturesDoNotDeny
Two count-anywhere symbols emit in a fixed orderPassTestScatterWinOrder_IsDeterministic
The legacy exchange payload still carries the jackpot tablePassTestExchangeLegacyPayload_CarriesTheJackpotTable
A game with no jackpot serialises and hashes exactly as beforePassTestConfigEndpoint_NoJackpotSerialisesExactlyAsBefore, TestPlayerConfig_HashUnchangedForGamesWithoutAJackpot
config_hash covers the jackpot tablePassTestPlayerConfig_ConfigHashCoversTheJackpotTable
Studio preset canonicalizes to the embedded packPassTestClassicFruitsV2PackMatchesCanonical

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

  1. 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.

  2. 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).

  3. 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".

  4. 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.

  5. Client naming: closed, on both halves of the contract. SEVEN carries IsScatter: true as the engine hook for count-anywhere evaluation, and neither that flag nor ScatterPayouts was 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} publishes jackpot_symbols: ["SEVEN"] and jackpot_payouts, with scatter_symbols empty and no scatter_payouts at all.

    The split is decided by mathengine.JackpotSymbolIDs and applied in BOTH halves — the win event and the config contract read the same verdict, so a jackpot entry always names a jackpot_payouts table 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_symbols and 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_hash is unchanged for every game without a jackpot, measured rather than assumed.

  6. wins[].payout is per-payline for line entries and does not sum to spin.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.yaml now 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 its 60× rule to what the player is shown and naming wins[].multiplier as a legacy field that is not a display value. Pinned by TestClassicFruitsV2_LineWinUnitsDoNotReconcile and TestLineWinWireUnitsArePreExisting.

  7. Merged, not published. The pack is on backend main and 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 ID classic-fruits-3x3 still 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 be classic-fruits-3x3-v1, because game_configs.version is 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.

  8. 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.