Blazor slot machine project (Wild Spinner).
This is a Blazor Server demo slot machine app ("Wild Spinner"). The README below explains how the game mechanics work, where the payout and free-spin logic lives, and how to run/tune the project and simulator.
- Build locally:
dotnet build -c Debug- Run the app (Blazor Server):
dotnet run --project "WildWinner.csproj"- Simulator
There's a small RTP simulator under tools/rtp-sim. Example:
# from repo root
dotnet run --project "tools/rtp-sim" -- 100 --seed 12345Use the in-app admin UI to Export Config for Simulator which writes tools/rtp-sim/config.json. The simulator can then use the exact runtime config for more accurate RTP sweeps.
- Layout: 6 reels; each reel has a variable row count (Megaways-style) between 2 and 6 rows per spin.
- Symbols: configured in
Services/SlotConfig.cs(fieldSymbols). - Symbol weights (rarity), baseline multipliers and floor multipliers are all configurable in
Services/SlotConfig.cs. - Wilds:
images/wilds.PNGact as substitutes for other symbols when evaluating streaks. - Diamonds: special bonus symbol used to award Free Spins (see below).
The core payout calculation is implemented in Services/SlotPayoutEvaluator.cs and mirrored in Components/Pages/SlotMachine.razor for UI logic. Steps:
- For each non-wild symbol
Sexamine reels left-to-right and count the streak (number of consecutive reels from reel 0 containing S or a Wild). Streaks of 3 or more are eligible. - Baseline multiplier: use the symbol's baseline (e.g. 0.40× for banana) for a 3-match. Larger streaks grow exponentially using a growth factor of 2.5 per extra reel: growth = 2.5^(streak-3).
- Raw line payout = betSize × lineMultiplier (baseline × growth).
- Combination count: for each reel in the streak multiply the count of matching symbols + wilds on that reel → comboCount. This accounts for multiple symbol occurrences per reel.
- Scaled payout = rawLinePayout × payoutScale × max(1, comboCount).
payoutScaleis a global tuning scalar (default inSlotConfig≈ 0.129). - Minimum floor: ensure scaled payout is at least betSize × MinMultiplierByStreak[streak] × comboCount × payoutScale (floors live in
SlotConfig). - Special-case: diamonds have a deterministic cash prize tied to the free-spin bonus (see below) — calculated differently than normal symbol payouts.
Services/SlotConfig.cs— primary tuning surface (symbols,SymbolMultipliers,SymbolWeights,MinMultiplierByStreak,FreeSpinWeightMultiplier,PayoutScale).Services/SlotPayoutEvaluator.cs— canonical evaluation function used by the UI to decide payouts and wins.Components/Pages/SlotMachine.razor— full gameplay UI and orchestration (spin loop, visuals, free-spins runner, admin UI hooks).tools/rtp-sim— command-line simulator for large-scale RTP testing.
- Trigger: if a spin finishes with 3 or more distinct reels containing the diamond symbol
images/diamond.png, the player is awarded free spins. - Award formula: 3 diamonds → 10 free spins; each additional diamond adds +5 free spins (4→15, 5→20, 6→25). Award is capped at 25.
- Pending state: awarded free spins are stored in
pendingFreeSpinsand the UI shows a confirmation modalFree Spins Awarded. The user must confirm to start them (this prevents unexpected autospin interruptions). - Free spin mode rules:
- Free spins do NOT deduct the player's balance.
- Free spins use a slightly boosted symbol weight table (see
FreeSpinWeightMultiplierinSlotConfig) so wins are more likely. - Each free spin increases a progressive multiplier: starts at 1× for the first free spin and increases by +1 for each subsequent free spin. The multiplier is applied to the payouts during the free-spin session when accumulating the session total.
- Wins during free spins are accumulated into
freeSpinAccumulatedand added to the player's balance at the end of the session.
- Toggle the debug/admin panel in the app (click the ⚙ icon) to access live tuning:
Payout Scaleslider — changepayoutScaleat runtime to affect RTP.Export Config for Simulator— writestools/rtp-sim/config.jsonfor simulator use.- Several debug helper buttons exist (force test spins, export debug reels, etc.) to aid testing.
- For large RTP sweeps use the simulator project
tools/rtp-sim. Export the live config from the app and place it attools/rtp-sim/config.json(admin UI provides that export). Then run the simulator with the desired spin count.
Example:
# export config from the app (admin panel) then:
dotnet run --project "tools/rtp-sim" -- 1000000- The
payoutScaletuned inSlotConfigis intentionally conservative; the in-app slider allows faster experimentation (simulator provides the best statistical view). - The UI shows smaller wins inline and larger wins in a modal with animations; autospin waits for large-win displays unless interrupted.
- The tease animation for diamond bonus is purely cosmetic and does not change the final reels/outcome.
- If you change tuning values in
Services/SlotConfig.csyou can export the runtime config from the app and run the simulator to re-check RTP.
If you'd like me to commit the README changes and push them, say so and I will run the git commands for you.
This project is a small demo — adapt as needed. If you want additional documentation or diagrams (probability trees, prize tables), I can add them.
I ran several large simulator sweeps using the bundled simulator (tools/rtp-sim) after tuning PayoutScale to target an RTP ≈ 0.98. The generated charts are committed under tools/rtp-sim/plots and a short summary follows.
- Latest tuned
PayoutScaleused by the app and simulator:0.0578(committed inServices/SlotConfig.cs). - Representative 1,000,000-spin runs:
- seed
22222: RTP = 0.9801 (98.01%) — 1,000,000 spins. See plots below. - seed
12345: RTP = 2.1383 (213.83%) — earlier run showing the pre-tuning behavior.
- seed
Plots (generated by tools/rtp-sim/plot_sim.py):
Summary file:
tools/rtp-sim/plots/summary.txt
If you'd like, I can copy these images into a top-level docs/ folder and add a dedicated docs/simulator-results.md page with the full set of run output files (CSV, detailed stats), or generate higher-resolution SVG plots for embedding.
- Tag format: use
vMAJOR.MINOR.PATCH(for example:v1.0.0). - Pushing a tag will trigger the Release workflow and create a GitHub Release with an attached
publish.tar.gzcontaining the published app. - Release notes: they are auto-generated from commits between the previous tag and the pushed tag. Prefer concise commit messages (one-line summary) to produce clean release notes.
Example:
git tag v1.0.0
git push origin v1.0.0Below are approximate probabilities and a prize table to help reason about expected outcomes. These are computed from the runtime tuning in Services/SlotConfig.cs (weights, multipliers and floors). Important notes:
- These are approximate: symbol selection is weighted and some runtime rules alter exact probabilities (e.g. diamonds are limited to at most one per reel, wilds are excluded from reel 0, and free-spin mode temporarily boosts certain weights).
- Values are shown per 1€ bet for clarity (multiply by your
betSizeto get actual payout). The runtimepayoutScale(default:0.129) is applied.
Symbol selection (approx per-row)
Total base weight (all reels): 57
| Symbol | Weight | Approx per-row probability |
|---|---|---|
apple |
6 | 10.53% |
banana |
5 | 8.77% |
bars |
6 | 10.53% |
bells |
4 | 7.02% |
cherry |
6 | 10.53% |
crown |
3 | 5.26% |
grapes |
5 | 8.77% |
lemon |
5 | 8.77% |
melon |
5 | 8.77% |
orange |
5 | 8.77% |
sevens |
3 | 5.26% |
wilds |
2 | 3.51% |
jackpot.png |
1 | 1.75% |
diamond.png |
1 | 1.75% |
Special-case notes:
- Reel 0 excludes wilds (so per-row probabilities on reel 0 are computed from total weight = 55).
- Diamonds cannot appear more than once on the same reel (generation removes diamond from choices after the first diamond on a reel).
- In free-spin mode the
FreeSpinWeightMultiplier(default 1.15) is applied to weights and wilds gain an extra +2 weight, increasing win frequency.
Using the per-row diamond weight above and the runtime behavior (2–6 rows per reel, equal probability), the approximate probability that any single reel contains at least one diamond is ~6.9% (averaged across 2..6 rows). Treating reels as independent for a rough estimate, the probability of finding diamonds on 3 or more distinct reels (the condition that awards Free Spins) is approximately 0.48% per spin (about 1 in 208 spins). This is an approximation; exact simulator runs are recommended for precise RTP/odds.
The table below shows the payout per 1€ bet for a single-line match (combo multiplicity = 1). The runtime evaluator applies a global payoutScale (default: 0.129) and also enforces minimum floor multipliers per streak length (these floors cause many lower baselines to use the floor values for streaks >3). Diamonds are a special-case (they award a fixed cash prize tied to the free-spin award): 3→10×, 4→15×, 5→20×, 6→25× (per bet).
| Symbol | Baseline (3‑match ×bet) | Payout (3-match) | Payout (4-match) | Payout (5-match) | Payout (6-match) |
|---|---|---|---|---|---|
banana |
0.40× | 0.194 € | 1.290 € | 2.580 € | 8.000 € |
cherry |
0.60× | 0.194 € | 1.290 € | 2.580 € | 8.000 € |
grapes |
0.80× | 0.194 € | 1.290 € | 2.580 € | 8.000 € |
lemon |
1.00× | 0.194 € | 1.290 € | 2.580 € | 8.000 € |
apple |
1.50× | 0.194 € | 1.290 € | 2.580 € | 8.000 € |
melon |
2.00× | 0.258 € | 1.290 € | 2.580 € | 8.000 € |
orange |
2.40× | 0.310 € | 1.290 € | 2.580 € | 8.000 € |
bars |
3.20× | 0.413 € | 1.290 € | 2.580 € | 8.000 € |
crown |
3.80× | 0.490 € | 1.290 € | 3.064 € | 8.000 € |
bells |
4.50× | 0.581 € | 1.451 € | 3.626 € | 9.070 € |
wilds |
5.50× | 0.710 € | 1.774 € | 4.434 € | 11.086 € |
sevens |
8.00× | 1.032 € | 2.580 € | 6.450 € | 16.125 € |
jackpot.png |
14.00× | 1.806 € | 4.515 € | 11.288 € | 28.219 € |
diamond.png (special) |
1.20× (special) | 10.00 €* | 15.00 €* | 20.00 €* | 25.00 €* |
*Diamonds use a deterministic cash prize tied to the free-spin award (see Free Spins section) rather than the standard scaling rules above.
- The numbers are for a single-line win where each reel in the streak contributes one matching symbol (combo multiplicity = 1). If a reel contains multiple matching symbols or wilds, the evaluator multiplies payouts by the combination count (product of counts on each reel), which can increase payouts.
- The
payoutScaleacts as a global tuning scalar; raising it increases all payouts proportionally (and affects RTP). Use the in-app admin slider for quick experimentation and the simulator for large-sample verification.
- Generate a full prize matrix that includes common combo multiplicities for 2–6 rows per reel (more work but useful for exact expected-value tables).
- Add an SVG/PNG probability chart exported from the simulator results for visual RTP breakdown.
Spin flow (simplified):
flowchart TD
A[User clicks Spin] --> B[Deduct bet]
B --> C[Generate reels 2to6]
C --> D[Optional tease]
D --> E[Evaluate wins]
E --> F{Payout}
F -->|Yes| G[Show win overlay]
F -->|No| H[No win]
E --> I{Free spins pending}
I -->|Yes| J[Show Free Spins modal]
G --> K[Add payout]
J --> K
K --> L[Ready]
Free-spin flow:
flowchart TD
A[User confirms Free Spins] --> B[Set freeSpinCount]
B --> C[Disable turbo and set multiplier=1]
C --> D[Run free spin loop]
D --> E[Use boosted weights and accumulate payout]
E --> F[Increment multiplier and decrement count]
F --> G{freeSpinCount == 0}
G -->|Yes| H[Show summary and add total to balance]
G -->|No| D

