In brief
Slot mathematics is a specification plus a probability distribution—not a number typed into an RTP field. Define what each symbol does, how wins are counted, what a mode costs and when a round ends. Then test the implementation against those rules.
- Separate payout multipliers from wallet currency units.
- Treat every mode as its own distribution.
- Keep the frontend's rules and paytable synchronized with the math.
Write the rules before changing Python
For our Harbor Signals exercise, begin with a five-reel, three-row lines game. Specify whether wins run left-to-right, the minimum matching length, wild substitution rules, scatter behavior, and whether overlapping lines pay independently. Add a worked example for each rule.
A line is a row index on each reel. For instance, [1, 1, 1, 1, 1] follows the middle row when rows are zero-indexed. The board may be stored by reel rather than as three visual rows; confirm that orientation before writing a fixture. Transposing it can produce plausible-looking but incorrect wins.
Use stable internal IDs such as H1, L1, W and S. The display artwork can change from a lantern to a compass without changing the meaning of H1. A visual name is not a new math identity unless the rules also change.
Where the sample stores those decisions
The pinned lines game configuration contains dimensions, paytable, paylines, special symbols, free-spin triggers, reel files and BetMode definitions. Its own values are sample values, not a recommendation for your game. In particular, the sample RTP of 0.967 is not a universal approval promise.
Copy the example into a new game folder before developing your concept. Update its game identity and review generated paths. Change one rule at a time. A changed symbol payout can affect base wins, bonus wins, maximum-win behavior, optimization constraints and text shown to the player.
Reel strips matter too. Adding more occurrences of a symbol can change its opportunities to appear. But the final weighted outcome table—not a casual count of symbols on the strips—is what determines the published distribution in this architecture.
For a concrete learning change, reduce the copied sample's three-H1 paytable entry from its original value to 8.0. Inside your copied GameConfig.__init__, after the table is defined, the assignment is:
self.paytable[(3, "H1")] = 8.0
This is a one-line exercise, not a balanced replacement model. Replay a hand-built three-H1 case, regenerate the affected results, compare the report with the baseline, and update the displayed paytable. Leave unrelated rules unchanged so the effect can be isolated. Do not infer the final RTP from the size of that one edit.
RTP with a small worked example
Let each outcome have a non-negative weight w, payout x measured in multiples of the base bet, and mode cost c measured in base bets:
probability(i) = weight(i) / sum(all weights)
RTP = sum(probability(i) × payout(i)) / mode cost
This follows the weighted-distribution approach described in the Engine optimization paper, with the explicit mode-cost denominator added here to keep units clear.
For a teaching distribution costing one base bet:
| Outcome | Weight | Payout in base-bet multiples |
|---|---|---|
| No payout | 70 | 0 |
| Half-bet payout | 20 | 0.5 |
| Larger payout | 10 | 8.5 |
The expected payout is (70×0 + 20×0.5 + 10×8.5) / 100 = 0.95, giving 95% RTP. This is an arithmetic exercise, not a production slot model or an approved target.
The non-zero payout frequency is 30%, but the frequency of a payout greater than the stake is only 10%. Calling both numbers “win rate” would confuse readers and players. Define your labels precisely. A half-bet return is a net loss on a one-bet round; do not present it as a profit.
For a mode costing 100 base bets, an average payout of 95 base bets also represents 95% RTP. Dividing by one instead of 100 would report a meaningless 9,500% figure.
Three different units you must not mix
In the math file format, a stored payoutMultiplier of 1150 represents 11.5 times the base bet for a cost-one round. That is a scale of 100. Wallet amounts use a different scale, explained in part 9. UI labels may display ordinary currency values.
Put conversions at explicit boundaries. Name variables payoutHundredths, baseBetAmount or walletUnits instead of a vague value. Never “fix” an apparent payout mismatch by changing a divisor until the displayed total looks right.
Tests worth writing immediately
Prepare hand-checkable boards: no match, exactly three matches, a longer match, wild substitution, scatter threshold, one below the threshold, multiple simultaneous wins and the cap boundary. For each, record the expected paying positions and amount before running the implementation.
For a bonus, specify initial spins, retriggers, multiplier timing and termination. A retrigger must not create an unbounded loop. Test the condition that stops a capped round and ensure later animations do not imply additional payable wins.
These tests are original engineering recommendations. They complement, rather than replace, current Engine validation and specialist review.
Finish this part
Produce a rules document, symbol-to-asset mapping and a small set of expected-result fixtures. You are ready for simulation only when another developer can use those documents to explain why each fixture pays its stated amount.
