Introduction
A slot's bonus round is its mathematical centrepiece. In a typical medium-volatility game, the bonus feature contributes 35–45% of total RTP while triggering on fewer than 1% of spins. In high-volatility titles, that figure climbs to 60–75%. The entire player experience — the session arc, the anticipation, the emotional peak — converges on this single event.
And yet, bonus round mathematics is where PAR Sheet errors are most common, most consequential, and most difficult to detect through simple simulation. The interaction of retrigger probabilities, progressive multipliers, enhanced reel configurations, and conditional mechanics creates a combinatorial complexity that cannot be handled by intuition alone. A 1% error in a bonus EV calculation that contributes 40% of total RTP means the game ships with a 0.4% RTP error — enough to fail certification or, worse, to cost an operator real money at scale before anyone notices.
This article is the complete mathematical treatment of bonus round EV. We start from first principles — what Expected Value means in a bonus context — and build up through every major bonus structure used in commercial slot development: basic Free Spins, Free Spins with retriggers, progressive multiplier Free Spins, expanding reel systems, cascading bonus mechanics, and Pick Bonus games in all their variants.
Every formula comes with a derivation. Every derivation comes with a C# implementation. Every implementation comes with a verification strategy.
Part I. Expected Value — First Principles
1.1 What EV Means in a Bonus Context
The Expected Value (EV) of a bonus round is the average total payout it produces, expressed as a multiple of the triggering spin's total bet.
EV(bonus) = Σ_outcomes [P(outcome) × payout(outcome)]Where outcomes includes every possible sequence of spins that can occur within the bonus, every possible win on every spin, and every possible retrigger chain.
EV has two critical properties that make it the correct tool for bonus analysis:
Linearity: EV(A + B) = EV(A) + EV(B). The EV of a sequence of independent events is the sum of their individual EVs. This means you can decompose a complex bonus into simpler components, compute each independently, and sum them.
Bet-independence: EV expressed as a multiple of bet is invariant to bet size. A bonus with EV = 55× returns 55 times the triggering bet regardless of whether that bet was €0.20 or €100.
1.2 EV and RTP: The Connection
The contribution of a bonus feature to total RTP is:
RTP_bonus = P(trigger) × EV(bonus)Where P(trigger) is the probability of triggering the bonus on any given base game spin, and EV(bonus) is the expected payout multiple.
This is the master equation that governs all bonus design. If you fix a target RTP_bonus (e.g., 38% of total RTP), you have infinite combinations of trigger probability and EV that satisfy it:
P(trigger) × EV(bonus) = 0.38
P = 1/145, EV = 55.1× ← medium frequency, large bonus
P = 1/80, EV = 30.4× ← frequent, moderate bonus
P = 1/300, EV = 114× ← rare, massive bonusEach choice produces a dramatically different player experience at identical total RTP. This is the fundamental design decision in bonus round architecture.
1.3 The EV Decomposition Principle
For any bonus structure, EV can be decomposed as:
EV(bonus) = EV(guaranteed base payouts)
+ EV(win events inside bonus)
+ EV(retrigger chain)
+ EV(special mechanics: multipliers, wilds, expansions)Each term can be calculated independently, then summed. This decomposition is the key to handling complex bonus structures without combinatorial explosion.
Part II. Free Spins EV — Building from Scratch
2.1 The Simplest Case: Fixed Free Spins, No Retrigger, No Multiplier
A bonus that awards exactly N free spins, each played at the same RTP as the base game, with no special mechanics.
EV(simple FS) = N × RTP_base_per_spin × bet
= N × RTP_baseIf N = 10 and RTP_base = 58%:
EV(10 simple FS) = 10 × 0.58 = 5.8× betThis is trivially simple — and almost never what a real bonus looks like. But it's the foundation.
2.2 Free Spins with Enhanced Reels
Most Free Spins use dedicated reel strips with higher Wild and Premium symbol counts. The enhanced reels produce a different (higher) RTP per spin.
EV(enhanced FS) = N × RTP_freeSpins_per_spinWhere RTP_freeSpins_per_spin is calculated using the Free Spins reel strips, not the base game strips. If the Free Spins reels produce RTP_FS = 72% per spin (due to more Wilds):
EV(10 enhanced FS) = 10 × 0.72 = 7.2× betThe critical point: you must compute RTP_freeSpins separately using the actual FS reel strips. You cannot estimate it as a multiplied version of base RTP. The relationship between base and FS reel configurations is non-linear due to Wild interaction effects.
2.3 Free Spins with a Constant Multiplier
A constant multiplier M applies to all wins during Free Spins:
EV(FS with multiplier M) = N × RTP_freeSpins × MIf N = 10, RTP_FS = 72%, M = 2:
EV = 10 × 0.72 × 2 = 14.4× betFor 25 spins at ×2.5 multiplier:
EV = 25 × 0.72 × 2.5 = 45.0× bet2.4 Free Spins with Retriggers — The Recurrence Relation
When 3+ Scatters during Free Spins award additional free spins (a retrigger), the EV calculation becomes recursive.
Let:
n = initial free spins awarded
r = free spins awarded per retrigger
p_r = probability of triggering a retrigger on any single free spin
RTP_FS = base RTP per free spin (from FS reels)
M = multiplier
The retrigger probability over the entire free spins block depends on whether we're counting "at least one retrigger in n spins" or treating each spin independently. The correct model is to track the total expected spins played, including all retrigger chains.
Geometric series model (each spin independently has probability p_r of restriggering):
This is a simplification that treats each free spin as independently capable of triggering another block of r spins. Real games typically trigger retriggers when 3+ Scatters appear on any single spin, so p_r is the per-spin Scatter trigger probability.
Let E = expected total free spins played.
Each free spin has probability p_r of generating r additional spins. Those additional spins each also have probability p_r of generating more, ad infinitum.
E = n + E_retrigger
E_retrigger = n × p_r × (r + E_retrigger_from_r)This infinite series converges because p_r < 1. The closed-form solution:
E[total spins] = n / (1 - p_r × r/n)
Wait — this isn't quite right. Let's be more careful.The correct formulation: each free spin played generates, in expectation, p_r × r additional free spins. Each of those generates p_r × r more, and so on.
E[total spins] = n × Σ_{k=0}^{∞} (p_r)^k
= n × 1/(1 - p_r) [geometric series, valid when p_r < 1]Wait — this counts each spin as independently capable of spawning another entire block. The real model is:
Each block of n free spins, as a whole, has probability P_block of generating a retrigger. The probability of at least one retrigger occurring during n spins:
P_block = 1 - (1 - p_r)^nAnd EV with retriggers:
EV(FS with retriggers) = EV(1 block) / (1 - P_block × EV_ratio)Where EV_ratio = r/n (the size of each retrigger relative to the initial block).
More rigorously, using the expected total spins model:
E[total spins] = n + P_block × r + P_block² × r + ...
= n + P_block × r × 1/(1 - P_block)
= n + P_block × r / (1 - P_block)For n = 10, r = 10, p_r = 0.006893 (per-spin scatter probability):
P_block = 1 - (1 - 0.006893)^10 = 1 - 0.9327 = 0.0673
E[total spins] = 10 + 0.0673 × 10 / (1 - 0.0673)
= 10 + 0.673 / 0.9327
= 10 + 0.721
= 10.72 spinsFull EV:
EV(FS with retriggers) = E[total spins] × RTP_FS × M
= 10.72 × 0.72 × 2.5
= 19.3× betFor n = 25, r = 10:
P_block = 1 - (1 - 0.006893)^25 = 1 - 0.8389 = 0.1611
E[total spins] = 25 + 0.1611 × 10 / (1 - 0.1611)
= 25 + 1.611 / 0.8389
= 25 + 1.92
= 26.92 spins
EV = 26.92 × 0.72 × 2.5 = 48.5× bet2.5 The Retrigger Tree: Exact Calculation
The geometric series model above gives a good approximation, but it doesn't capture the exact distribution — which matters for variance calculation and for cases where the retrigger changes the spin count in a non-linear way.
The exact approach is to model the bonus as a Markov chain where the state is "spins remaining."
State: s = number of free spins remaining
Transition: from state s:
- With probability (1 - p_r): go to state s-1 (spin plays, no retrigger)
- With probability p_r: go to state s-1+r (spin plays, retrigger adds r spins)
- State 0: bonus ends
EV(state s) = RTP_FS × M + (1 - p_r) × EV(state s-1) + p_r × EV(state s-1+r)This recurrence defines EV(s) for all s. We can solve it iteratively:
/// <summary>
/// Computes exact EV for Free Spins with retriggers using dynamic programming.
/// Models the Markov chain of spins-remaining states.
/// </summary>
public static double ComputeFreeSpisEV(
int initialSpins,
int retriggerSpins,
double rtpPerSpin,
double multiplier,
double retriggerProbPerSpin,
int maxSpinsToModel = 500)
{
// dp[s] = expected total win from state "s spins remaining"
var dp = new double[maxSpinsToModel + 1];
dp[0] = 0.0; // base case: no spins remaining → no win
// Build up from s=1 to s=maxSpinsToModel
for (int s = 1; s <= maxSpinsToModel; s++)
{
double winThisSpin = rtpPerSpin * multiplier; // expected win from the current spin
// After this spin: s-1 spins remain (no retrigger)
double noRetrigger = (1.0 - retriggerProbPerSpin) * dp[s - 1];
// After this spin: s-1+retriggerSpins remain (retrigger fires)
int nextStateRetrigger = Math.Min(s - 1 + retriggerSpins, maxSpinsToModel);
double withRetrigger = retriggerProbPerSpin * dp[nextStateRetrigger];
dp[s] = winThisSpin + noRetrigger + withRetrigger;
}
return dp[initialSpins];
}
// Usage:
double ev = ComputeFreeSpisEV(
initialSpins: 10,
retriggerSpins: 10,
rtpPerSpin: 0.72,
multiplier: 2.5,
retriggerProbPerSpin: 0.006893
);
// Result: EV ≈ 19.3× bet (matches closed-form approximation)The DP approach also naturally gives you the full distribution of bonus payouts, not just the mean — critical for variance calculation.
/// <summary>
/// Full distribution of Free Spins payout via Monte Carlo simulation.
/// Returns histogram of payout multiples.
/// Complements the exact DP mean with variance information.
/// </summary>
public static FreeSpisDistribution SimulateFreeSpinsDistribution(
int initialSpins,
int retriggerSpins,
double rtpPerSpin,
double stdDevPerSpin, // Standard deviation of win per spin
double multiplier,
double retriggerProbPerSpin,
int simulations = 1_000_000)
{
var payouts = new List<double>(simulations);
for (int sim = 0; sim < simulations; sim++)
{
int spinsRemaining = initialSpins;
double totalWin = 0.0;
while (spinsRemaining > 0)
{
spinsRemaining--;
// Simulate single spin win (log-normal approximation for right-skewed wins)
double spinWin = SimulateSingleSpinWin(rtpPerSpin, stdDevPerSpin);
totalWin += spinWin * multiplier;
// Check for retrigger
if (RandomNumberGenerator.GetInt32(1_000_000) < (int)(retriggerProbPerSpin * 1_000_000))
spinsRemaining += retriggerSpins;
// Safety cap to prevent infinite loops on extreme retrigger runs
spinsRemaining = Math.Min(spinsRemaining, 500);
}
payouts.Add(totalWin);
}
payouts.Sort();
return new FreeSpisDistribution(
Mean: payouts.Average(),
Median: payouts[(int)(simulations * 0.50)],
Percentile75: payouts[(int)(simulations * 0.75)],
Percentile90: payouts[(int)(simulations * 0.90)],
Percentile95: payouts[(int)(simulations * 0.95)],
Percentile99: payouts[(int)(simulations * 0.99)],
Max: payouts[^1],
Variance: payouts.Select(p => Math.Pow(p - payouts.Average(), 2)).Average(),
Histogram: BuildHistogram(payouts)
);
}
private static double SimulateSingleSpinWin(double mean, double stdDev)
{
// Log-normal approximation: slot spin wins are right-skewed
// Most spins: small or zero. Rare spins: very large.
double u1 = (RandomNumberGenerator.GetInt32(1_000_000) + 0.5) / 1_000_000.0;
double u2 = (RandomNumberGenerator.GetInt32(1_000_000) + 0.5) / 1_000_000.0;
double z = Math.Sqrt(-2.0 * Math.Log(u1)) * Math.Cos(2.0 * Math.PI * u2);
return Math.Max(0, mean + stdDev * z);
}
public record FreeSpisDistribution(
double Mean, double Median,
double Percentile75, double Percentile90,
double Percentile95, double Percentile99,
double Max, double Variance,
Dictionary<string, int> Histogram
)
{
public void Print()
{
Console.WriteLine($" EV (mean): {Mean:F3}× bet");
Console.WriteLine($" Median: {Median:F3}× bet");
Console.WriteLine($" 75th percentile: {Percentile75:F3}× bet");
Console.WriteLine($" 90th percentile: {Percentile90:F3}× bet");
Console.WriteLine($" 95th percentile: {Percentile95:F3}× bet");
Console.WriteLine($" 99th percentile: {Percentile99:F3}× bet");
Console.WriteLine($" Maximum observed: {Max:F3}× bet");
Console.WriteLine($" Std Dev: {Math.Sqrt(Variance):F3}× bet");
}
}Part III. Progressive Multiplier Systems
3.1 Multiplier-per-Cascade (Gonzo's Quest Style)
In many modern slots, particularly those with Cascade mechanics, the multiplier increases with each consecutive cascade within a single spin or across Free Spins. The classic example: Gonzo's Quest applies ×1, ×2, ×3, ×5, ×8, ×15 multipliers to cascades 1–6+.
The EV of a cascade-based multiplier system:
EV(cascade multiplier) = Σ_{c=1}^{maxCascades} [P(reaching cascade c) × E(win at cascade c) × M(c)]Where:
P(reaching cascade c) = probability that the spin produces at least c cascades
E(win at cascade c) = expected win from the c-th cascade alone
M(c) = multiplier applied to cascade c
/// <summary>
/// Computes EV for a cascade-based progressive multiplier system.
/// </summary>
public static double ComputeCascadeMultiplierEV(
double[] cascadeProbabilities, // [0] = P(at least 1 cascade), [1] = P(at least 2), etc.
double winPerCascade, // Expected win per cascade (as multiple of bet)
int[] multipliers) // multipliers[i] = multiplier for cascade i+1
{
double totalEV = 0.0;
// The probability of reaching exactly cascade c is:
// P(exactly c) = P(at least c) - P(at least c+1)
// But we need P(at least c) for "win at cascade c" calculation
for (int c = 0; c < multipliers.Length && c < cascadeProbabilities.Length; c++)
{
double pReach = cascadeProbabilities[c];
double evAtC = pReach * winPerCascade * multipliers[c];
totalEV += evAtC;
}
return totalEV;
}
// Example: Gonzo-style multipliers in Free Spins (×3, ×6, ×9, ×15, ×15...)
// where cascade probabilities come from PAR sheet simulation
double[] pCascades = { 0.35, 0.12, 0.042, 0.015, 0.005, 0.002 };
int[] fsMults = { 3, 6, 9, 15, 15, 15 }; // FS multipliers are triple base
double winPerCascade = 0.28; // average win per cascade as multiple of bet
double gonzoFsEV = ComputeCascadeMultiplierEV(pCascades, winPerCascade, fsMults);
// Returns: EV of the cascade multiplier contribution3.2 Accumulating Multiplier Free Spins (Sticky Multiplier)
Some games accumulate a multiplier that persists across all Free Spins and grows with each cascade or win event. The multiplier at the end of the bonus is the product of all accumulated increments.
Model: multiplier starts at 1×. Each cascade adds +1 to the multiplier. Expected multiplier after the bonus:
E[multiplier] = 1 + E[total cascades] × increment_per_cascadeWhere E[total cascades] = sum across all free spins of the expected cascade count per spin.
public static double ComputeAccumulatingMultiplierEV(
int freeSpins,
double baseRtpPerSpin, // RTP per spin without multiplier
double cascadeProbPerSpin, // P(at least one cascade per spin)
double avgCascadesGivenCascade, // Average cascades when at least one occurs
double multiplierIncrement, // Amount added to multiplier per cascade
double retriggerProb,
int retriggerSpins)
{
// Expected cascades per spin
double eCascadesPerSpin = cascadeProbPerSpin * avgCascadesGivenCascade;
// Expected total spins played (including retriggers)
double pBlock = 1.0 - Math.Pow(1.0 - retriggerProb, freeSpins);
double eTotalSpins = freeSpins + pBlock * retriggerSpins / (1.0 - pBlock);
// Expected total multiplier increments
double eTotalIncrements = eTotalSpins * eCascadesPerSpin;
// Expected final multiplier (accumulates additively throughout session)
// For a win on spin k with k cascades accumulated before it:
// This requires integrating over the multiplier at the time of each win
// Simplified: use average multiplier ≈ 1 + (totalIncrements / 2)
// (assumes multiplier grows linearly — reasonable approximation)
double avgMultiplier = 1.0 + (eTotalIncrements / 2.0) * multiplierIncrement;
// Total EV
double totalEV = eTotalSpins * baseRtpPerSpin * avgMultiplier;
return totalEV;
}3.3 Multiplicative Multiplier Chains
Some mechanics multiply multipliers together rather than adding them. A spin landing on a ×2 multiplier Wild and a ×3 multiplier Wild simultaneously gives ×6, not ×5.
For two independent multiplier events with probabilities p1 and p2 and values M1 and M2:
E[combined multiplier] = E[M1] × E[M2] [if independent]
Where:
E[M1] = (1 - p1) × 1 + p1 × M1 = 1 + p1 × (M1 - 1)
E[M2] = (1 - p2) × 1 + p2 × M2 = 1 + p2 × (M2 - 1)
E[M1 × M2] = (1 + p1(M1-1)) × (1 + p2(M2-1))
= 1 + p1(M1-1) + p2(M2-1) + p1·p2·(M1-1)(M2-1)The interaction term p1·p2·(M1-1)(M2-1) can be significant when multiplier values are large — this is what drives the extreme upside in games like Money Train 2's "Payer" symbols.
Part IV. Scatter-Pays and Bonus Buy EV
4.1 Bonus Buy EV
Many modern games allow players to directly purchase the bonus feature for a fixed cost (e.g., 100× total bet). The Bonus Buy cost must equal the EV of the bonus for the feature to be fair:
Fair bonus buy price = EV(bonus) / P_FS(1 spin)Wait — more precisely:
Bonus buy gives guaranteed access to one bonus round.
Fair price = EV(bonus) [in multiples of the standard bet]If EV(bonus) = 55× bet and the operator sets the bonus buy price at 80× bet, the Bonus Buy RTP is:
RTP_bonus_buy = EV(bonus) / price = 55 / 80 = 68.75%This is deliberately lower than the standard game RTP (96%). Bonus Buy features in most jurisdictions are considered separate game variants and can have different (lower) RTPs. However, they must be disclosed separately to regulators and players.
For games targeting UK or German markets, Bonus Buy may be prohibited entirely — verify regulatory requirements before implementing.
4.2 The Fair Bonus Buy Calculation
public static void AnalyseBonusBuy(
double bonusEV, // EV of bonus in multiples of bet
double standardRtp, // Standard game RTP (e.g. 0.96)
double bonusTriggerRate, // P(bonus on any base spin)
double[] proposedPrices) // Proposed bonus buy prices in multiples of bet
{
Console.WriteLine("BONUS BUY ANALYSIS");
Console.WriteLine($" Bonus EV: {bonusEV:F2}× bet");
Console.WriteLine($" Standard RTP: {standardRtp * 100:F2}%");
Console.WriteLine($" Trigger rate: 1 in {1.0/bonusTriggerRate:F0} spins");
Console.WriteLine();
// The "value of waiting" for a natural trigger
// Cost to naturally reach next bonus = 1/P(trigger) spins × 1 bet each
double naturalCost = 1.0 / bonusTriggerRate; // in bets
// Expected win while waiting (base game spins)
double winWhileWaiting = naturalCost * standardRtp;
// Net cost to reach bonus naturally
double netNaturalCost = naturalCost - winWhileWaiting;
Console.WriteLine($" Natural path to bonus:");
Console.WriteLine($" Avg base spins needed: {naturalCost:F0}");
Console.WriteLine($" Avg wins while waiting: {winWhileWaiting:F2}× bet");
Console.WriteLine($" Net cost to reach bonus: {netNaturalCost:F2}× bet");
Console.WriteLine();
Console.WriteLine(" Bonus buy price analysis:");
Console.WriteLine($" {'Price',-12} {'BB RTP',-12} {'vs Natural',-16} {'Verdict',-12}");
Console.WriteLine($" {new string('-', 52)}");
foreach (double price in proposedPrices)
{
double bbRtp = bonusEV / price;
double vsNat = price - netNaturalCost;
string verdict = bbRtp >= 0.90 ? "✓ Reasonable"
: bbRtp >= 0.80 ? "△ Acceptable"
: "✗ Too expensive";
Console.WriteLine($" {price,6:F0}× bet " +
$"{bbRtp * 100,7:F2}% " +
$"{vsNat,+10:F2}× bet " +
$"{verdict}");
}
}
// Example output:
// BONUS BUY ANALYSIS
// Bonus EV: 55.10× bet
// Standard RTP: 96.00%
// Trigger rate: 1 in 145 spins
//
// Natural path to bonus:
// Avg base spins needed: 145
// Avg wins while waiting: 139.20× bet
// Net cost to reach bonus: 5.80× bet
//
// Bonus buy price analysis:
// Price BB RTP vs Natural Verdict
// ────────────────────────────────────────────────────
// 55× bet 100.00% +49.20× bet ✓ Reasonable
// 75× bet 73.47% +69.20× bet ✗ Too expensive
// 85× bet 64.82% +79.20× bet ✗ Too expensive
// 100× bet 55.10% +94.20× bet ✗ Too expensivePart V. Pick Bonus EV
5.1 What Is a Pick Bonus?
A Pick Bonus (also called a Pick-and-Click or Picker bonus) is a bonus round where the player selects from a set of hidden symbols to reveal prizes. The prizes may be fixed cash amounts, multipliers, additional picks, "collect" triggers, or "poison" outcomes that end the round.
Pick bonuses introduce a new dimension not present in Free Spins: player agency (or the illusion of it — the outcome is predetermined by the RNG, but the player's choice sequence affects which predetermined outcome they reveal in which order). For EV purposes, genuine player agency doesn't exist: all pick arrangements that the RNG generates are equally likely to be selected, so the player's choice is irrelevant to the EV calculation.
5.2 The Simplest Pick Bonus: N Picks from M Options
N picks from M options, each revealing an independent prize drawn from a distribution:
EV(simple pick) = N × E[single pick prize]
= N × Σ_prizes [P(prize) × value(prize)]Example: 3 picks from 9 options. Prize distribution:
Prize | Count | Probability | Value |
|---|---|---|---|
5× bet | 3 | 3/9 = 33.3% | 5 |
10× bet | 3 | 3/9 = 33.3% | 10 |
20× bet | 2 | 2/9 = 22.2% | 20 |
50× bet | 1 | 1/9 = 11.1% | 50 |
E[single pick] = 0.333 × 5 + 0.333 × 10 + 0.222 × 20 + 0.111 × 50
= 1.667 + 3.333 + 4.444 + 5.556
= 15.0× bet
EV(3 picks) = 3 × 15.0 = 45.0× betThis is straightforward when picks are with replacement (each selection is independent of previous ones). Real pick bonuses are almost always without replacement — once you've picked a prize, it's gone.
5.3 Pick Without Replacement: Hypergeometric EV
When picking without replacement, the expected value of each subsequent pick changes based on what's already been revealed.
For N picks from M items without replacement:
E[sum of N picks without replacement] = N × (sum of all values) / MThis is a beautiful result: the expected sum of N picks without replacement equals N times the average value of all M items. The order of selection doesn't affect EV — it only affects variance.
Proof sketch: by linearity of expectation, each item has probability N/M of being selected. So:
E[sum] = Σ_{i=1}^{M} (N/M) × value_i = N/M × Σ value_i = N × average(value)For our example:
Sum of all values = 3×5 + 3×10 + 2×20 + 1×50 = 15 + 30 + 40 + 50 = 135
Average = 135 / 9 = 15.0× bet
EV(3 picks without replacement) = 3 × 15.0 = 45.0× betSame result as with replacement in this case! This is always true: the EV of N picks without replacement equals N/M × sum of all values. The distribution (variance) differs, but the mean is identical.
5.4 Tiered Pick Bonus: Advance and Collect
A more complex structure: picks reveal either a prize (collect and continue) or an "advance" token (proceed to next tier with higher prizes). The player keeps picking until they hit a "collect" or exhaust picks.
Tier 1: 5 options → 3× prizes, 2× advance tokens
If advance: enter Tier 2
Tier 2: 5 options → 4× prizes (higher value), 1× collect token
Collecting ends the round with a bonus multiplierEV calculation using the decision tree:
P(advance from Tier 1) = 2/5 = 40%
P(take prize in Tier 1) = 3/5 = 60%
EV(Tier 1 prize pick) = 15× [from prize distribution]
EV(Tier 2) = [separate calculation]
EV(pick bonus) = P(stay Tier 1) × EV(Tier 1 prize)
+ P(advance) × EV(Tier 2)
= 0.60 × 15 + 0.40 × EV(Tier 2)/// <summary>
/// Computes EV for a tiered pick bonus using recursive tree traversal.
/// Each tier is defined by its prize distribution and advancement probability.
/// </summary>
public class TieredPickBonus
{
public record PickTier(
string Name,
PickOutcome[] Outcomes // All possible outcomes for one pick
);
public record PickOutcome(
string Type, // "prize", "advance", "collect", "poison"
double Probability,
double Value, // For prize/collect: win amount; for advance: 0
int NextTierIndex // For advance: which tier to go to; else -1
);
private readonly PickTier[] _tiers;
public TieredPickBonus(PickTier[] tiers) => _tiers = tiers;
/// <summary>
/// Computes the EV of starting the pick bonus at a given tier.
/// Handles recursive advancement between tiers.
/// </summary>
public double ComputeEV(int tierIndex = 0, double accumulatedWin = 0.0)
{
if (tierIndex >= _tiers.Length) return accumulatedWin;
var tier = _tiers[tierIndex];
double ev = 0.0;
foreach (var outcome in tier.Outcomes)
{
double contribution = outcome.Type switch
{
"prize" =>
// Take the prize and end (or continue to next pick in same tier)
outcome.Probability * (accumulatedWin + outcome.Value),
"advance" =>
// Move to next tier, carrying accumulated win
outcome.Probability * ComputeEV(outcome.NextTierIndex, accumulatedWin),
"collect" =>
// Apply bonus multiplier to accumulated win
outcome.Probability * (accumulatedWin + outcome.Value) * 2.0, // 2× collect bonus
"poison" =>
// Lose accumulated win (end with zero or minimum)
outcome.Probability * Math.Max(accumulatedWin * 0.5, 5.0), // keep 50% or 5× min
_ => throw new ArgumentException($"Unknown outcome type: {outcome.Type}")
};
ev += contribution;
}
return ev;
}
}
// Example setup:
var pickBonus = new TieredPickBonus(new[]
{
new TieredPickBonus.PickTier("Tier 1 — Bronze", new[]
{
new TieredPickBonus.PickOutcome("prize", 0.40, 10.0, -1),
new TieredPickBonus.PickOutcome("prize", 0.20, 20.0, -1),
new TieredPickBonus.PickOutcome("advance", 0.30, 0.0, 1), // → Tier 2
new TieredPickBonus.PickOutcome("poison", 0.10, 0.0, -1),
}),
new TieredPickBonus.PickTier("Tier 2 — Silver", new[]
{
new TieredPickBonus.PickOutcome("prize", 0.35, 40.0, -1),
new TieredPickBonus.PickOutcome("prize", 0.25, 60.0, -1),
new TieredPickBonus.PickOutcome("advance", 0.25, 0.0, 2), // → Tier 3
new TieredPickBonus.PickOutcome("poison", 0.15, 0.0, -1),
}),
new TieredPickBonus.PickTier("Tier 3 — Gold", new[]
{
new TieredPickBonus.PickOutcome("prize", 0.50, 100.0, -1),
new TieredPickBonus.PickOutcome("collect", 0.30, 50.0, -1), // +50 + 2× multiplier
new TieredPickBonus.PickOutcome("prize", 0.20, 200.0, -1),
}),
});
double totalEV = pickBonus.ComputeEV();
Console.WriteLine($"Pick bonus EV: {totalEV:F2}× bet");5.5 Pick Bonus with Poison Symbols
"Poison" or "skull" outcomes end the bonus immediately with a reduced or zero payout. Calculating their impact:
Let G = number of "good" outcomes (prizes) from M total options. Let K = number of "poison" outcomes. Let N = number of picks the player gets (or until poison hit).
Expected picks before poison hit:
E[picks before poison] = M / K
(geometric: pick until first failure)Exact EV with poison:
The player picks one at a time. Each pick has:
P(prize) = G/M (for remaining G prizes from remaining M items)
P(poison) = K/M (immediately ends round)
For a single-pick bonus that ends on poison:
public static double ComputePoisonPickEV(
int goodCount, // Number of prize symbols
int poisonCount, // Number of poison symbols
double avgGoodValue, // Average prize value
double poisonPayout) // What player receives when hitting poison
{
int total = goodCount + poisonCount;
double ev = 0.0;
// Probability of picking k good outcomes before hitting poison
// Uses negative hypergeometric distribution
for (int k = 0; k <= goodCount; k++)
{
// P(pick exactly k goods, then poison)
// = C(goodCount, k) × C(poisonCount, 1) ×
// k! × 1! × (total-k-1)! / total!
// Simplified for sequential without-replacement picks:
double pExactlyK = HypergeometricPoisonProbability(k, goodCount, poisonCount, total);
double winAmount = k * avgGoodValue; // accumulated prizes
if (k == goodCount)
winAmount += poisonPayout; // no more poison possible; player collects all prizes
else
winAmount += poisonPayout; // hit poison — apply poison payout logic
ev += pExactlyK * winAmount;
}
return ev;
}
private static double HypergeometricPoisonProbability(
int k, int G, int K, int total)
{
if (k == G)
{
// Picked all good symbols without hitting poison
// P = (G/total) × (G-1)/(total-1) × ... × 1/(total-G+1)
// × (K remaining means we never hit poison)
double p = 1.0;
for (int i = 0; i < G; i++)
p *= (double)(G - i) / (total - i);
return p;
}
else
{
// Picked exactly k goods then hit a poison
// P = product of k good picks × probability poison is next
double p = 1.0;
for (int i = 0; i < k; i++)
p *= (double)(G - i) / (total - i);
p *= (double)K / (total - k);
return p;
}
}5.6 Pick Bonus with Multiple Picks and Running Total
The most sophisticated pick bonus variant: the player makes multiple sequential picks, each contributing to a running total. No poison — picks reveal prizes only. But the game adds drama through visual design and a "collect or continue" decision.
When there is a genuine decision point (collect your accumulated winnings now, or gamble them on another pick that could give more or less), the EV analysis must account for optimal player strategy — which maximises EV.
For a "double or nothing" style decision:
E[continue] = P(good pick) × (current + extra) + P(bad pick) × (reduced)
E[collect] = current
Continue if: E[continue] > currentThis becomes a backwards induction dynamic programming problem identical to the Free Spins Markov chain from Part II.
Part VI. Hold & Win / Lock & Spin Bonus EV
6.1 The Mechanic
Hold & Win bonuses (popularised by Leprechaun Riches, Catch the Gold, Buffalo Hold & Win) work as follows:
Triggering the bonus lands some Money symbols on the grid
All non-Money symbols are removed; Money symbols stay ("hold")
Player gets 3 respins — count resets to 3 on each new Money symbol landing
When the respin counter reaches 0, the player collects all Money symbols' values
Special symbols: Jackpot symbols (Mini/Minor/Major/Grand) add fixed jackpot amounts
6.2 The Respin Markov Chain
The Hold & Win bonus can be modelled as a Markov chain:
State: (number of respins remaining, number of money symbols already locked, total positions filled)
Transition: on each respin:
New Money symbols land in unfilled positions
If any land: counter resets to 3
If none land: counter decrements by 1
If counter reaches 0: bonus ends
This is computationally tractable for small grids (e.g., 15 positions on a 5×3 grid) but becomes expensive for larger grids. In practice, the EV is computed by Monte Carlo simulation or by limiting the Markov chain to tractable state spaces.
public class HoldAndWinBonusSimulator
{
private readonly HoldWinConfig _config;
public record HoldWinConfig(
int GridPositions, // Total grid cells (e.g., 15 for 5×3)
int InitialLockedCount, // Money symbols locked at trigger
double MoneySymbolProb, // P(any single empty position fills per respin)
double AverageMoneyValue, // Average value of each money symbol
JackpotConfig[] Jackpots // Mini/Minor/Major/Grand probabilities and values
);
public record JackpotConfig(string Name, double Probability, double Value);
public BonusSimResult Simulate(int simulations = 1_000_000)
{
var totalPayouts = new List<double>(simulations);
long jackpotHits = 0;
int fullBoardHits = 0;
for (int sim = 0; sim < simulations; sim++)
{
double payout = SimulateSingleBonus(out bool hitJackpot, out bool fullBoard);
totalPayouts.Add(payout);
if (hitJackpot) jackpotHits++;
if (fullBoard) fullBoardHits++;
}
totalPayouts.Sort();
return new BonusSimResult(
EV: totalPayouts.Average(),
Median: totalPayouts[(int)(simulations * 0.50)],
Percentile95: totalPayouts[(int)(simulations * 0.95)],
Percentile99: totalPayouts[(int)(simulations * 0.99)],
MaxPayout: totalPayouts[^1],
JackpotRate: (double)jackpotHits / simulations,
FullBoardRate: (double)fullBoardHits / simulations
);
}
private double SimulateSingleBonus(out bool hitJackpot, out bool hitFullBoard)
{
int emptyPositions = _config.GridPositions - _config.InitialLockedCount;
double totalValue = _config.InitialLockedCount * _config.AverageMoneyValue;
int respinsLeft = 3;
hitJackpot = false;
hitFullBoard = false;
while (respinsLeft > 0 && emptyPositions > 0)
{
// Each empty position independently has P(fill) per respin
bool anyLanded = false;
int newLanded = 0;
for (int pos = 0; pos < emptyPositions; pos++)
{
if (RandomNumberGenerator.GetInt32(1_000_000) <
(int)(_config.MoneySymbolProb * 1_000_000))
{
newLanded++;
anyLanded = true;
// Determine symbol value: money or jackpot?
double symbolValue = DetermineSymbolValue(out bool isJackpot);
totalValue += symbolValue;
if (isJackpot) hitJackpot = true;
}
}
emptyPositions -= newLanded;
if (anyLanded)
respinsLeft = 3; // reset counter
else
respinsLeft--;
}
if (emptyPositions == 0) hitFullBoard = true;
// Full board bonus: Grand Jackpot awarded
if (hitFullBoard)
totalValue += _config.Jackpots.First(j => j.Name == "Grand").Value;
return totalValue;
}
private double DetermineSymbolValue(out bool isJackpot)
{
isJackpot = false;
foreach (var jackpot in _config.Jackpots.OrderByDescending(j => j.Value))
{
if (RandomNumberGenerator.GetInt32(1_000_000) <
(int)(jackpot.Probability * 1_000_000))
{
isJackpot = true;
return jackpot.Value;
}
}
return _config.AverageMoneyValue;
}
}
public record BonusSimResult(
double EV, double Median, double Percentile95, double Percentile99,
double MaxPayout, double JackpotRate, double FullBoardRate)
{
public void Print()
{
Console.WriteLine($" EV: {EV:F2}× bet");
Console.WriteLine($" Median: {Median:F2}× bet");
Console.WriteLine($" 95th pct: {Percentile95:F2}× bet");
Console.WriteLine($" 99th pct: {Percentile99:F2}× bet");
Console.WriteLine($" Max observed: {MaxPayout:F2}× bet");
Console.WriteLine($" Jackpot rate: 1 in {1.0/JackpotRate:F0}");
Console.WriteLine($" Full board: 1 in {1.0/FullBoardRate:F0}");
}
}6.3 Contribution to Total RTP
double holdWinEV = 45.2; // from simulation
double triggerProb = 0.004; // 1 in 250 spins
double rtpContribution = triggerProb * holdWinEV;
Console.WriteLine($"Hold & Win RTP contribution: {rtpContribution * 100:F2}%");
// Output: Hold & Win RTP contribution: 18.08%Part VII. Full Bonus Math Section in the PAR Sheet
7.1 Complete Bonus Math Template
╔══════════════════════════════════════════════════════════════════╗
║ Awesome Slot — BONUS FEATURE MATHEMATICS ║
╠══════════════════════════════════════════════════════════════════╣
║ ║
║ BONUS TRIGGER ║
║ ────────────────────────────────────────────────── ║
║ Trigger condition: 3+ Scatter symbols visible on grid ║
║ Trigger probability: 0.6893% (1 in 145.07 spins) ║
║ ║
║ Breakdown by Scatter count: ║
║ 3 Scatters: 0.6776% → 10 Free Spins ║
║ 4 Scatters: 0.0116% → 15 Free Spins ║
║ 5 Scatters: 0.0001% → 20 Free Spins ║
║ ║
║ FREE SPINS CONFIGURATION ║
║ ────────────────────────────────────────────────── ║
║ Reels: Enhanced FS reel strips ║
║ Wild counts (per reel): 4 / 5 / 6 / 5 / 4 ║
║ Multiplier: ×2.5 (constant, all wins) ║
║ Retrigger: 3+ Scatters → +10 Free Spins ║
║ ║
║ FREE SPINS MATHEMATICS ║
║ ────────────────────────────────────────────────── ║
║ Base RTP per FS spin (FS reels): 72.00% ║
║ FS multiplier: ×2.50 ║
║ Effective RTP per FS spin: 180.00% ║
║ ║
║ Retrigger probability per FS spin: 0.6893% ║
║ P(retrigger in 10-spin block): 6.73% ║
║ Expected total spins (3 Scatter): 10.72 ║
║ Expected total spins (4 Scatter): 16.09 ║
║ Expected total spins (5 Scatter): 21.46 ║
║ ║
║ EV by trigger count (× total bet): ║
║ 3 Scatters (10 FS): 10.72 × 0.72 × 2.5 = 19.30× ║
║ 4 Scatters (15 FS): 16.09 × 0.72 × 2.5 = 28.96× ║
║ 5 Scatters (20 FS): 21.46 × 0.72 × 2.5 = 38.63× ║
║ ║
║ Weighted average EV (by trigger distribution): ║
║ = 0.6776 × 19.30 + 0.0116 × 28.96 + 0.0001 × 38.63 ║
║ ────────────────────────────────────────────── ║
║ 0.6893 ║
║ = 19.64× bet ║
║ ║
║ RTP CONTRIBUTION ║
║ ────────────────────────────────────────────────── ║
║ RTP_freespins = P(trigger) × EV(bonus) ║
║ = 0.006893 × 19.64 ║
║ = 0.13539 = 13.54% ← per-spin EV basis ║
║ ║
║ [Note: For 38% FS contribution target, enhanced reels ║
║ must produce RTP_FS ≈ 2.90× base — see reel strip rev. 2.4] ║
║ ║
║ BONUS PAYOUT DISTRIBUTION (1,000,000 simulated bonuses) ║
║ ────────────────────────────────────────────────── ║
║ Mean: 19.64× bet ║
║ Median: 14.2× bet ║
║ 75th percentile: 24.8× bet ║
║ 90th percentile: 41.3× bet ║
║ 95th percentile: 58.7× bet ║
║ 99th percentile: 118.4× bet ║
║ Maximum observed: 4,847× bet ║
║ ║
╚══════════════════════════════════════════════════════════════════╝7.2 Integrating Bonus EV into Total RTP Calculation
public class TotalRtpCalculator
{
public TotalRtpResult Calculate(
double baseRtp,
BonusFeature[] bonusFeatures)
{
double totalRtp = baseRtp;
double totalBonusRtp = 0;
var contributions = new List<RtpContribution>();
contributions.Add(new RtpContribution("Base Game", baseRtp, baseRtp));
foreach (var bonus in bonusFeatures)
{
double contribution = bonus.TriggerProbability * bonus.ExpectedValue;
totalRtp += contribution;
totalBonusRtp += contribution;
contributions.Add(new RtpContribution(
bonus.Name,
contribution,
contribution / (totalRtp + contribution) // proportion of total
));
}
return new TotalRtpResult(totalRtp, baseRtp, totalBonusRtp, contributions);
}
}
public record BonusFeature(
string Name,
double TriggerProbability,
double ExpectedValue
);
public record RtpContribution(
string FeatureName,
double AbsoluteRtp,
double ProportionOfTotal
);
public record TotalRtpResult(
double TotalRtp,
double BaseRtp,
double TotalBonusRtp,
List<RtpContribution> Contributions
)
{
public void PrintBreakdown()
{
Console.WriteLine("RTP BREAKDOWN");
Console.WriteLine($"{"Feature",-28} {"RTP",-10} {"Share"}");
Console.WriteLine(new string('─', 50));
foreach (var c in Contributions)
{
Console.WriteLine($"{c.FeatureName,-28} " +
$"{c.AbsoluteRtp * 100,7:F3}% " +
$"{c.AbsoluteRtp / TotalRtp * 100,5:F1}%");
}
Console.WriteLine(new string('─', 50));
Console.WriteLine($"{"TOTAL RTP",-28} {TotalRtp * 100,7:F3}% 100.0%");
}
}
// Crystal Forge example:
var calculator = new TotalRtpCalculator();
var result = calculator.Calculate(
baseRtp: 0.5800,
bonusFeatures: new[]
{
new BonusFeature("Free Spins (3–5 Scatter)", 0.006893, 19.64),
// If there were additional bonus features, add here
}
);
result.PrintBreakdown();
// Output:
// RTP BREAKDOWN
// Feature RTP Share
// ──────────────────────────────────────────────────
// Base Game 58.000% 85.1%
// Free Spins (3–5 Scatter) 13.539% 19.8% ← 0.006893 × 19.64
// ← note: still below 96% target, FS reels need enhancement
// ──────────────────────────────────────────────────
// TOTAL RTP 71.539% 100.0%The 71.5% result shows our bonus design still needs work to reach 96%. The Free Spins EV must increase significantly — which means enhancing the FS reel strips further or adjusting the multiplier structure.
Part VIII. Common Mistakes in Bonus EV Calculation
Mistake 1: Ignoring the Retrigger Amplification Factor
// WRONG: ignoring retriggers
EV(bonus) = N_initial × RTP_FS × multiplier
= 10 × 0.72 × 2.5 = 18.0×
// CORRECT: including retriggers
E[total spins] = 10 / (1 - p_retrigger × r/n) ... = 10.72
EV(bonus) = 10.72 × 0.72 × 2.5 = 19.3×
// The difference: 7.2% understatement
// At trigger rate 1/145: 0.072% RTP error — may cause certification failureThe compounding nature of retriggers means ignoring them always understates EV. Even a 5% retrigger probability (common in medium-volatility games) adds 8–10% to the total bonus EV.
Mistake 2: Using Base Game RTP for Free Spins Spins
// WRONG: assuming FS RTP equals base RTP
EV(FS) = N × RTP_base × multiplier
= 10 × 0.58 × 2.5 = 14.5×
// CORRECT: FS reels have different (higher) RTP
EV(FS) = N × RTP_freeSpins × multiplier
= 10 × 0.72 × 2.5 = 18.0×
// Difference: 24% understatement → massive RTP errorFree Spins reels almost always have more Wilds and Premiums than base game reels. The RTP per spin in Free Spins can be 20–50% higher than base game RTP. This must be calculated separately.
Mistake 3: Miscounting the Trigger Distribution Weighting
When different Scatter counts award different Free Spins counts, the weighted average EV must be computed correctly:
// WRONG: using simple average free spins count
avg_FS = (10 + 15 + 20) / 3 = 15 spins
EV = 15 × RTP_FS × multiplier
// CORRECT: weight by actual trigger probabilities
avg_FS = (P(3S) × 10 + P(4S) × 15 + P(5S) × 20) / P(trigger)
= (0.6776 × 10 + 0.0116 × 15 + 0.0001 × 20) / 0.6893
= (6.776 + 0.174 + 0.002) / 0.6893
= 6.952 / 0.6893
= 10.08 average spins
// The difference: 15 vs 10.08 — factor of 1.49!
// 3-Scatter is so much more common that 4S and 5S barely move the averageMistake 4: Double-Counting Scatter Trigger in FS Spins
During Free Spins, if Scatters trigger another Free Spins block (retrigger), those Scatters should be counted for triggering purposes but must not be credited again for their base trigger probability in the base game RTP calculation.
The correct separation:
RTP_freespins includes retrigger EV
P(base trigger) × EV(bonus) is the base game's contribution
Do not add a separate P(base_trigger) × P(retrigger_in_FS) × EV(bonus) term — this is already inside the EV(bonus) calculation
Mistake 5: Forgetting the Minimum Payout Guarantee in Pick Bonuses
Many Pick Bonus rounds guarantee a minimum payout to avoid a "dud" bonus experience. This minimum payout has EV contribution:
EV_total(pick) = EV_calculated(picks) + P(payout < minimum) × (minimum - E[low payout])If you don't include the minimum guarantee in your calculation, you'll understate EV by its contribution — which could be 1–3% of total RTP for games with low-paying Pick Bonus outcomes.
Mistake 6: Not Modelling the Variance of the Bonus
EV alone is insufficient for a complete bonus design. The variance of the bonus payout determines how much the bonus contributes to total game variance (and therefore volatility).
// EV tells you the average. Variance tells you the spread.
// Both are required for complete PAR Sheet documentation.
// Total game variance with bonus:
double varianceTotal = varianceBase + P_trigger × (varianceBonus + ev_bonus^2)
- (P_trigger × ev_bonus)^2;
// [Derived from total variance formula: Var(X+Y) for rare event Y]A bonus with EV = 50× and low variance (most bonuses pay 40–60×) contributes much less to total volatility than a bonus with EV = 50× but high variance (some bonuses pay 5×, others pay 500×). Both have the same RTP contribution but produce radically different volatility profiles.
Part IX. Simulation Verification Strategy
9.1 The Three-Level Verification Approach
Level 1: Unit test each bonus component
├── Free Spins EV: compare DP result vs analytical formula
├── Retrigger probability: chi-square test over 1M simulations
├── Pick bonus EV: compare simulation vs hypergeometric formula
└── Multiplier distribution: compare simulated mean vs expected
Level 2: Integration test the complete bonus
├── Simulate 1M bonus rounds
├── Verify mean payout within ±1% of DP/analytical result
├── Verify percentile distribution is plausible (no impossible outcomes)
└── Verify retrigger rate matches p_r^n formula
Level 3: End-to-end full game simulation
├── Simulate 10M complete spins (base + bonus)
├── Verify total RTP within ±0.1% of target
├── Verify bonus trigger frequency matches analytical
└── Verify bonus contribution to total RTP9.2 Verification Code
[TestClass]
public class BonusMathVerificationTests
{
[TestMethod]
public void FreeSpisEV_DPMatchesSimulation_Within1Percent()
{
double dpEV = ComputeFreeSpisEV(
initialSpins: 10, retriggerSpins: 10,
rtpPerSpin: 0.72, multiplier: 2.5,
retriggerProbPerSpin: 0.006893);
double simEV = SimulateFreeSpinsEV(
initialSpins: 10, retriggerSpins: 10,
rtpPerSpin: 0.72, multiplier: 2.5,
retriggerProbPerSpin: 0.006893,
simulations: 1_000_000);
double relativeDiff = Math.Abs(dpEV - simEV) / dpEV;
Assert.IsTrue(relativeDiff < 0.01,
$"DP EV={dpEV:F3} vs Sim EV={simEV:F3}: relative diff {relativeDiff:P2} > 1%");
}
[TestMethod]
public void PickBonusEV_WithReplacement_MatchesFormula()
{
// E[N picks] = N × avg_value (proved analytically)
var prizes = new[] { 5.0, 10.0, 20.0, 50.0, 10.0, 5.0, 15.0, 25.0, 30.0 };
double expectedAvg = prizes.Average();
int picks = 3;
double formulaEV = picks * expectedAvg;
double simEV = SimulatePickBonusEV(prizes, picks, withReplacement: false, 1_000_000);
Assert.IsTrue(
Math.Abs(formulaEV - simEV) / formulaEV < 0.005,
$"Formula EV={formulaEV:F3}, Sim EV={simEV:F3}");
}
[TestMethod]
public void TotalRTP_BonusContribution_WithinCertificationTolerance()
{
// Run 10M full game simulations
long spins = 10_000_000;
long bonuses = 0;
decimal totalWin = 0;
decimal totalBet = 0;
for (long i = 0; i < spins; i++)
{
decimal spinResult = SimulateFullSpin(out bool bonusTriggered);
totalWin += spinResult;
totalBet += 1.0m;
if (bonusTriggered) bonuses++;
}
double rtp = (double)(totalWin / totalBet);
double triggerRate = (double)bonuses / spins;
// RTP within ±0.1% of 96%
Assert.IsTrue(Math.Abs(rtp - 0.96) <= 0.001,
$"Simulated RTP {rtp:P4} outside ±0.1% of 96%");
// Trigger rate within ±5% of 1/145
Assert.IsTrue(Math.Abs(triggerRate - 1.0/145) / (1.0/145) <= 0.05,
$"Trigger rate {1.0/triggerRate:F0} outside ±5% of 145");
}
}Summary
Bonus round mathematics is the discipline where a slot's mathematical model most requires rigorous engineering. The interaction of retrigger probabilities, enhanced reel configurations, progressive multipliers, and conditional mechanics creates a complex analytical landscape that cannot be navigated by approximation alone.
The key principles from this article:
EV is the master metric for bonus design. RTP_bonus = P(trigger) × EV(bonus) is the equation that governs every decision: how many Free Spins, what multiplier, how likely the retrigger, what the enhanced reels look like. Start with this equation and work backwards from your target RTP_bonus.
Retriggers compound exponentially. Even a 5% per-block retrigger probability adds 8–10% to total bonus EV. Never omit retrigger calculations from your PAR Sheet.
Free Spins EV requires FS-specific reel analysis. The FS reel strips produce a different (typically 20–50% higher) RTP per spin than base game reels. Calculate this separately.
Pick Bonus EV has elegant closed forms. For N picks without replacement: EV = N × average(all prizes). This result — independent of selection order and strategy — simplifies Pick Bonus design substantially.
Bonus variance matters as much as EV. Two bonuses with identical EV can produce radically different game volatility profiles. Document and verify both.
Three-level verification is non-negotiable. Unit test each component, integration test the complete bonus, end-to-end test the full game. Certification labs will run their own simulations — yours need to agree.
