Optimizing win distributions with iterative weighted sampling
A discussion of how the provided optimization algorithm operates can be viewed by downloading this paper.
The aforementioned algorithm is implemented in the Rust programming language, this program compiles down to a binary executable. If the program is being run for the first time, or if there are modifications made to the main.rs file, the binary should be rebuilt using:
Configuring the optimization class
If the provided optimization algorithm is being used, each mode must be independently configured within the games/game_optimization.py file. This file utilizes the OptimizationSetup() class which handles the setup and verification of initial inputs. After the simulations are completed, the Rust binary is executed using the OptimizationExecution() class.
Within the runfile, the setup would look something like:
...
#Assign and verify optimization parameters within the game configuration
if run_conditions["run_optimization"] or run_conditions["run_analysis"]:
optimization_setup_class = OptimizationSetup(config)
if run_conditions["run_sims"]:
create_books(
...
)
generate_configs(gamestate)
#Setup and execute optimization process using simulation results
if run_conditions["run_optimization"]:
OptimizationExecution().run_all_modes(config, target_modes, rust_threads)
...
Game mode optimization parameters
The most crucial and game-specific components are defined within the OptimizationSetup.game_config.opt_params object.
For each mode there are three optimization inputs which can be modified: conditions, scaling (optional), parameters (statistical constraints and simulation size).
Conditions
This component defines which simulation numbers correspond to specific events where you want to control hit-rates or RTP contributions.
!!! IMPORTANT:
The order in which conditions are defined matter! Initially all simulation numbers are available for each entry within the condition section. Each entry will identify which simulations satisfy the specified search condition, which are then removed from the available simulation pool.
For example, if the wincap condition is applied and there are 100 total simulations for a game with a 1000x maximum payout, we can use the search_conditions tag to identify the simulation numbers where the final payout is equal to 1000x. This may, for example be simulations 55 and 96 only. After the application of this condition, the available simulation pool is now simulations [0 -> 99], excluding [55 and 96].
The search condition can be a specific win value [float], a win range tuple[float,float] or a tag recorded in the force-files dict{'search_key': 'search_value' ,..}. Another commonly used search condition is the freespin trigger. We often would like to control the hit-rate of free-spins (from the basegame). So assuming we have recorded all freespin entries within the gamestate using self.record({'symbol': 'scatter'}), we could apply this as a search condition.
Typically the final input into conditions will not have a search condition, which has the affect of assigning all remaining simulations to this condition.
When constructing each condition we have the option of specifying the RTP contribution from this condition, the hit-rate of simulations satisfying this condition, and the average win. The optimization algorithm must know two of these three inputs to solve for the unknown variable within the equation RTP = av-win * hit-rate. If we do not want to specify the hit-rate for a particular condition input, one field may have hr='x' specified. Since any bet within this bet-mode must return *some result, the total hit-rate of all conditions must be exactly 1. If 'x' is provided as the hit-rate this condition is set to be 1 - [sum of all other condition hit-rates].
Putting this together we will take as example a typical base-game with a bonus and super-bonus trigger. We would like to allocation a specific RTP contribution to the max-win, control the bonus and super hit-rates, and dictate the hit-rate of winning base-game results.
"conditions": {
"wincap": ConstructConditions(rtp=0.005, av_win=1000, search_conditions=1000).return_dict(),
"freegame": ConstructConditions(
rtp=0.20, hr=300, search_conditions={"symbol": "scatter"}
).return_dict(),
"superfreegame": ConstructConditions(
rtp=0.18, hr=1000, search_conditions={"symbol": "superScatter"}
).return_dict(),
"0": ConstructConditions(rtp=0, av_win=0, search_conditions=0).return_dict(),
"basegame": ConstructConditions(hr=3.5, rtp=0.97 - (0.005 + 0.20 + 0.18)).return_dict(),
},
Note that in this example it is important that we place the "0" condition above the "basegame". Otherwise, 0-wins will be removed from the simulation pool before getting to this condition. Similarly "wincap" is placed before "freegame" because it is a subset of the latter condition and so should be removed first.
Scaling
Scaling takes as input a criteria (usually the basegame and freegame types), a scale-factor, win-range in which to apply the scaling factor to, and a probability of this scaling being applied. This scaling will bias simulations within the specified criteria and win_range by the provided scale_factor. This can be < 1 to reduce the weighting assigned to wins within this range, or > 1 to increase the probability of selecting simulations with a final payout multiplier within the range provided. Finally probability is the chance of applying this scaling factor at all to a trial distribution and who's value must be between 0 and 1. Fundamentally the optimization algorithm trials many randomly generated trial distributions, most of which are rejected. This value is used to bias a certain proportion of distributions by the given scale factor.
While the outputs are not deterministic and the scaling of the given ranges is not exact, if while studying the hit-rate tables you desire for a given payout multiplier range to occur more or less frequently than generated, this factor can help guide the final output to bias wins within a given range. Note that biasing particular ranges by a significant amount can be lead to a lower likelihood of a randomly assigned distribution being accepted, so its effect should be used carefully.
Parameters
There are several parameters passed to the optimization tool which determines the minimum number of trial distribution to compare, along with statistical testing properties. Generally speaking these are left constant for a given game mode.
-
num_show: Used to set the number of initial trial distributions to generate. The larger this number the larger range of possible solutions are generated at the expense of computation time.
-
num_per_fence: Used to set the number combined distributions to generate. The optimization requires having 2 distributions under and over the target RTP. The square-root of this value determines the equal number of under and over distributions which are then iteratively combined.
-
min_m2m,max_m2m: These two values set the minimum and maximum bounds for the distribution mean-to-median ratio. This factor roughly determines the volatility of a distribution and is used to reject or accept a combined distribution (which will always be the correct RTP).
-
pmb_rtp: Of all accepted, balanced distributions 10 are output to the user to choose from. These are ranked by simulating many spins from many ficticious players and choosing which of the balanced distributions maximize the players chance of exceeding some ratio of their starting balance. So when this value is 1.0, we are asking which distributions will give the player the greatest chance of finishing some set number of spins with >= 1.0x their starting balance.
-
sim_trials: This is linked to *pmb_rtp and determines the number of virtual players to test.
-
test_spins: We want to optimize a players return by simulating a set number of spins. This value is linked to test_weights:, which weights the outcome from *pmb_rtp based on the spin number. So for a base-game we may know that many players stop of 30 spins, most stop at around 100 spins, and there is a reasonable number of players who play 150 spins or more. We are most interested in maximizing a players return at the average number of spins. So we could set the spin array and weightings to be [30 ,100, 150] and [0.3, 0.5, 0.2].
-
score_type: This string is used to determine which type of statistical test we want to use to rank possible distributions. Currently "rtp" is the only valid method, with more to be added in the future.