@slot-engine/core 0.0.4 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.mts CHANGED
@@ -1,520 +1,473 @@
1
- declare class GameSymbol {
1
+ declare const SPIN_TYPE: {
2
+ readonly BASE_GAME: "basegame";
3
+ readonly FREE_SPINS: "freespins";
4
+ };
5
+
6
+ interface GameConfigOptions<TGameModes extends AnyGameModes = AnyGameModes, TSymbols extends AnySymbols = AnySymbols, TUserState extends AnyUserData = AnyUserData> {
7
+ /**
8
+ * The unique identifier of the game, used for configuration and identification.
9
+ */
2
10
  id: string;
3
- pays?: Record<number, number>;
4
- properties: Map<string, any>;
5
- constructor(opts: GameSymbolOpts);
6
11
  /**
7
- * Compares this symbol to another symbol or a set of properties.
12
+ * The name of the game, used for display purposes.
8
13
  */
9
- compare(symbolOrProperties?: GameSymbol | Record<string, any>): boolean;
10
- }
11
- interface GameSymbolOpts {
14
+ name: string;
12
15
  /**
13
- * Unique identifier for the symbol, e.g. "W", "H1", "L5", etc.
16
+ * A GameMode is the core structure of a slot, defining the board,\
17
+ * bet cost, win type, and other properties.
14
18
  */
15
- id: string;
19
+ gameModes: TGameModes;
16
20
  /**
17
- * Paytable for the symbol, where the key is the number of symbols and the value is the payout multiplier.
21
+ * A list of all symbols that will appear on the reels.
18
22
  */
19
- pays?: Record<number, number>;
23
+ symbols: TSymbols;
20
24
  /**
21
- * Additional properties for the symbol, e.g. `multiplier` or `isWild`.
22
- *
23
- * Properties can help identify special symbols.
25
+ * A mapping from spin type to scatter counts to the number of free spins awarded.
24
26
  *
25
27
  * @example
26
- * If your game has a "normal" scatter and a "super" scatter, you can define them like this:
27
- *
28
28
  * ```ts
29
- * properties: {
30
- * isScatter: true,
31
- * }
29
+ * scatterToFreespins: {
30
+ * [SPIN_TYPE.BASE_GAME]: {
31
+ * 3: 10,
32
+ * 4: 12,
33
+ * 5: 15,
34
+ * },
35
+ * [SPIN_TYPE.FREE_SPINS]: {
36
+ * 3: 6,
37
+ * 4: 8,
38
+ * 5: 10,
39
+ * },
40
+ * },
32
41
  * ```
33
42
  */
34
- properties?: Record<string, any>;
35
- }
36
-
37
- declare function weightedRandom<T extends Record<string, number>>(weights: T, rng: RandomNumberGenerator): string;
38
- declare class RandomNumberGenerator {
39
- mIdum: number;
40
- mIy: number;
41
- mIv: Array<number>;
42
- NTAB: number;
43
- IA: number;
44
- IM: number;
45
- IQ: number;
46
- IR: number;
47
- NDIV: number;
48
- AM: number;
49
- RNMX: number;
50
- protected _currentSeed: number;
51
- constructor();
52
- getCurrentSeed(): number;
53
- protected setCurrentSeed(seed: number): void;
54
- setSeed(seed: number): void;
55
- setSeedIfDifferent(seed: number): void;
56
- generateRandomNumber(): number;
57
- randomFloat(low: number, high: number): number;
58
- }
59
-
60
- /**
61
- * This class is responsible for generating reel sets for slot games based on specified configurations.
62
- *
63
- * **While it offers a high degree of customization, some configurations may lead to unsolvable scenarios.**
64
- *
65
- * If the reel generator is unable to fulfill niche constraints,\
66
- * you might need to adjust your configuration, or edit the generated reels manually.\
67
- * Setting a different seed may also help.
68
- */
69
- declare class ReelGenerator {
70
- id: string;
71
- associatedGameModeName: string;
72
- protected readonly symbolWeights: Map<string, number>;
73
- protected readonly rowsAmount: number;
74
- reels: Reels;
75
- protected limitSymbolsToReels?: Record<string, number[]>;
76
- protected readonly spaceBetweenSameSymbols?: number | Record<string, number>;
77
- protected readonly spaceBetweenSymbols?: Record<string, Record<string, number>>;
78
- protected readonly preferStackedSymbols?: number;
79
- protected readonly symbolStacks?: Record<string, {
80
- chance: number | Record<string, number>;
81
- min?: number | Record<string, number>;
82
- max?: number | Record<string, number>;
83
- }>;
84
- protected readonly symbolQuotas?: Record<string, number | Record<string, number>>;
85
- outputDir: string;
86
- csvPath: string;
87
- overrideExisting: boolean;
88
- rng: RandomNumberGenerator;
89
- constructor(opts: ReelGeneratorOpts);
90
- private validateConfig;
91
- private isSymbolAllowedOnReel;
92
- private resolveStacking;
93
- private tryPlaceStack;
43
+ scatterToFreespins: Record<string, Record<number, number>>;
94
44
  /**
95
- * Checks if a symbol can be placed at the target index without violating spacing rules.
45
+ * If set, this will pad the board with symbols on the top and bottom of the reels.\
46
+ * Useful for teasing symbols right above or below the active board.
47
+ *
48
+ * Default: 1
96
49
  */
97
- private violatesSpacing;
98
- generateReels(gameConf: AnyGameConfig): void;
50
+ padSymbols?: number;
99
51
  /**
100
- * Reads a reelset CSV file and returns the reels as arrays of GameSymbols.
52
+ * The maximum win multiplier of the game, e.g. 5000 for a 5000x max win.
53
+ */
54
+ maxWinX: number;
55
+ /**
56
+ * Custom additional state that can be used in game flow logic.
57
+ */
58
+ userState?: TUserState;
59
+ /**
60
+ * Hooks are used to inject custom logic at specific points in the game flow.\
61
+ * Some required hooks must be implemented for certain features to work.
101
62
  */
102
- parseReelsetCSV(reelSetPath: string, { config }: GameConfig): Reels;
63
+ hooks: GameHooks<TGameModes, TSymbols, TUserState>;
103
64
  }
104
- interface ReelGeneratorOpts {
65
+ type GameConfig<TGameModes extends AnyGameModes = AnyGameModes, TSymbols extends AnySymbols = AnySymbols, TUserState extends AnyUserData = AnyUserData> = Required<Omit<GameConfigOptions<TGameModes, TSymbols, TUserState>, "symbols">> & {
105
66
  /**
106
- * The unique identifier of the reel generator.\
107
- * Must be unique per game mode.
67
+ * A map of all symbols.
108
68
  */
109
- id: string;
69
+ symbols: Map<keyof TSymbols & string, TSymbols[keyof TSymbols]>;
70
+ outputDir: string;
110
71
  /**
111
- * The weights of the symbols in the reelset.\
112
- * This is a mapping of symbol IDs to their respective weights.
72
+ * A mapping of spin types to the number of scatter symbols required to trigger anticipation.
113
73
  */
114
- symbolWeights: Record<string, number>;
74
+ anticipationTriggers: Record<(typeof SPIN_TYPE)[keyof typeof SPIN_TYPE], number>;
75
+ };
76
+
77
+ declare class Simulation {
78
+ readonly gameConfigOpts: GameConfigOptions;
79
+ readonly gameConfig: GameConfig;
80
+ readonly simRunsAmount: Partial<Record<string, number>>;
81
+ readonly concurrency: number;
82
+ private debug;
83
+ private actualSims;
84
+ private library;
85
+ private recorder;
86
+ private wallet;
87
+ constructor(opts: SimulationOptions, gameConfigOpts: GameConfigOptions);
88
+ runSimulation(opts: SimulationConfigOptions): Promise<void>;
115
89
  /**
116
- * The number of rows in the reelset.\
117
- * Default is 250, but can be adjusted as needed.
90
+ * Runs all simulations for a specific game mode.
118
91
  */
119
- rowsAmount?: number;
92
+ spawnWorkersForGameMode(opts: {
93
+ mode: string;
94
+ simNumsToCriteria: Record<number, string>;
95
+ }): Promise<void>;
96
+ callWorker(opts: {
97
+ basePath: string;
98
+ mode: string;
99
+ simStart: number;
100
+ simEnd: number;
101
+ index: number;
102
+ totalSims: number;
103
+ }): Promise<unknown>;
120
104
  /**
121
- * The directory where the generated reelset files will be saved.\
122
- * **It's recommended to just use `__dirname`**!
105
+ * Will run a single simulation until the specified criteria is met.
123
106
  */
124
- outputDir: string;
107
+ runSingleSimulation(opts: {
108
+ simId: number;
109
+ mode: string;
110
+ criteria: string;
111
+ index: number;
112
+ }): void;
125
113
  /**
126
- * Prevent the same symbol from appearing directly above or below itself.\
127
- * This can be a single number for all symbols, or a mapping of symbol IDs to
128
- * their respective spacing values.
129
- *
130
- * Must be 1 or higher, if set.
114
+ * If a simulation does not meet the required criteria, reset the state to run it again.
131
115
  *
132
- * **This is overridden by `symbolStacks`**
116
+ * This also runs once before each simulation to ensure a clean state.
133
117
  */
134
- spaceBetweenSameSymbols?: number | Record<string, number>;
118
+ protected resetSimulation(ctx: GameContext): void;
119
+ protected resetState(ctx: GameContext): void;
135
120
  /**
136
- * Prevents specific symbols from appearing within a certain distance of each other.
137
- *
138
- * Useful for preventing scatter and super scatter symbols from appearing too close to each other.
121
+ * Contains and executes the entire game logic:
122
+ * - Drawing the board
123
+ * - Evaluating wins
124
+ * - Updating wallet
125
+ * - Handling free spins
126
+ * - Recording events
139
127
  *
140
- * **This is overridden by `symbolStacks`**
128
+ * You can customize the game flow by implementing the `onHandleGameFlow` hook in the game configuration.
141
129
  */
142
- spaceBetweenSymbols?: Record<string, Record<string, number>>;
130
+ protected handleGameFlow(ctx: GameContext): void;
143
131
  /**
144
- * A percentage value 0-100 that indicates the likelihood of a symbol being stacked.\
145
- * A value of 0 means no stacked symbols, while 100 means all symbols are stacked.
146
- *
147
- * This is only a preference. Symbols may still not be stacked if\
148
- * other restrictions (like `spaceBetweenSameSymbols`) prevent it.
132
+ * Creates a CSV file in the format "simulationId,weight,payout".
149
133
  *
150
- * **This is overridden by `symbolStacks`**
134
+ * `weight` defaults to 1.
151
135
  */
152
- preferStackedSymbols?: number;
136
+ private writeLookupTableCSV;
153
137
  /**
154
- * A mapping of symbols to their respective advanced stacking configuration.
155
- *
156
- * @example
157
- * ```ts
158
- * symbolStacks: {
159
- * "W": {
160
- * chance: { "1": 20, "2": 20, "3": 20, "4": 20 }, // 20% chance to be stacked on reels 2-5
161
- * min: 2, // At least 2 wilds in a stack
162
- * max: 4, // At most 4 wilds in a stack
163
- * }
164
- * }
165
- * ```
138
+ * Creates a CSV file in the format "simulationId,criteria,payoutBase,payoutFreespins".
166
139
  */
167
- symbolStacks?: Record<string, {
168
- chance: number | Record<string, number>;
169
- min?: number | Record<string, number>;
170
- max?: number | Record<string, number>;
171
- }>;
140
+ private writeLookupTableSegmentedCSV;
141
+ private writeRecords;
142
+ private writeIndexJson;
143
+ private writeBooksJson;
144
+ private logSymbolOccurrences;
172
145
  /**
173
- * Configures symbols to be limited to specific reels.\
174
- * For example, you could configure Scatters to appear only on reels 1, 3 and 5.
175
- *
176
- * @example
177
- * ```ts
178
- * limitSymbolsToReels: {
179
- * "S": [0, 2, 4], // Remember that reels are 0-indexed.
180
- * }
181
- * ```
146
+ * Compiles user configured game to JS for use in different Node processes
182
147
  */
183
- limitSymbolsToReels?: Record<string, number[]>;
148
+ private preprocessFiles;
149
+ private getSimRangesForChunks;
150
+ private mergeRecords;
184
151
  /**
185
- * Defines optional quotas for symbols on the reels.\
186
- * The quota (1-100%) defines how often a symbol should appear in the reelset, or in a specific reel.
187
- *
188
- * This is particularly useful for controlling the frequency of special symbols like scatters or wilds.
189
- *
190
- * Reels not provided for a symbol will use the weights from `symbolWeights`.
191
- *
192
- * _Any_ small quota will ensure that the symbol appears at least once on the reel.
193
- *
194
- * @example
195
- * ```ts
196
- * symbolQuotas: {
197
- * "S": 3, // 3% of symbols on each reel will be scatters
198
- * "W": { "1": 10, "2": 5, "3": 3, "4": 1 }, // Wilds will appear with different quotas on selected reels
199
- * }
200
- * ```
152
+ * Generates reelset CSV files for all game modes.
201
153
  */
202
- symbolQuotas?: Record<string, number | Record<string, number>>;
154
+ private generateReelsetFiles;
203
155
  /**
204
- * If true, existing reels CSV files will be overwritten.
156
+ * Confirms all pending records and adds them to the main records list.
205
157
  */
206
- overrideExisting?: boolean;
158
+ confirmRecords(ctx: GameContext): void;
159
+ }
160
+ type SimulationOptions = {
207
161
  /**
208
- * Optional seed for the RNG to ensure reproducible results.
209
- *
210
- * Default seed is `0`.
162
+ * Object containing the game modes and their respective simulation runs amount.
163
+ */
164
+ simRunsAmount: Partial<Record<string, number>>;
165
+ /**
166
+ * Number of concurrent processes to use for simulations.
211
167
  *
212
- * Note: Seeds 0 and 1 produce the same results.
168
+ * Default: 6
213
169
  */
214
- seed?: number;
215
- }
216
- type Reels = GameSymbol[][];
170
+ concurrency?: number;
171
+ };
172
+ type SimulationConfigOptions = {
173
+ debug?: boolean;
174
+ };
217
175
 
218
- declare class GameMode {
219
- name: GameModeName;
220
- reelsAmount: number;
221
- symbolsPerReel: number[];
222
- cost: number;
223
- rtp: number;
224
- reelSets: ReelGenerator[];
225
- resultSets: ResultSet<any>[];
226
- isBonusBuy: boolean;
227
- constructor(opts: GameModeOpts);
176
+ declare class ResultSet<TUserState extends AnyUserData> {
177
+ criteria: string;
178
+ quota: number;
179
+ multiplier?: number;
180
+ reelWeights: ReelWeights<TUserState>;
181
+ userData?: Record<string, any>;
182
+ forceMaxWin?: boolean;
183
+ forceFreespins?: boolean;
184
+ evaluate?: (ctx: GameContext<AnyGameModes, AnySymbols, TUserState>) => boolean;
185
+ constructor(opts: ResultSetOpts<TUserState>);
186
+ static assignCriteriaToSimulations(ctx: Simulation, gameModeName: string): Record<number, string>;
187
+ /**
188
+ * Checks if core criteria is met, e.g. target multiplier or max win.
189
+ */
190
+ meetsCriteria(ctx: GameContext): boolean;
228
191
  }
229
- interface GameModeOpts {
192
+ interface ResultSetOpts<TUserState extends AnyUserData> {
230
193
  /**
231
- * Name of the game mode.
194
+ * A short string to describe the criteria for this ResultSet.
232
195
  */
233
- name: GameModeName;
196
+ criteria: string;
234
197
  /**
235
- * Number of reels the board has.
198
+ * The quota of spins, out of the total simulations, that must be forced to meet the specified criteria.\
199
+ * **Float from 0 to 1. Total quota of all ResultSets in a GameMode must be 1.**
236
200
  */
237
- reelsAmount: number;
201
+ quota: number;
238
202
  /**
239
- * How many symbols each reel has. Array length must match `reelsAmount`.\
240
- * The number at an array index represents the number of symbols on that reel.
203
+ * The required multiplier for a simulated spin to be accepted.
241
204
  */
242
- symbolsPerReel: number[];
205
+ multiplier?: number;
243
206
  /**
244
- * Cost of the game mode, multiplied by the base bet.
207
+ * Configure the weights of the reels in this ResultSet.
208
+ *
209
+ * If you need to support dynamic / special reel weights based on the simulation context,\
210
+ * you can provide an `evaluate` function that returns the desired weights.
211
+ *
212
+ * If the `evaluate` function returns a falsy value, the usual spin type based weights will be used.
213
+ *
214
+ * @example
215
+ * ```ts
216
+ * new ResultSet({
217
+ * criteria: "superFreespins",
218
+ * quota: 0.05,
219
+ * forceFreespins: true,
220
+ * reelWeights: {
221
+ * [SPIN_TYPE.BASE_GAME]: { base1: 1 },
222
+ * [SPIN_TYPE.FREE_SPINS]: { bonus1: 1, bonus2: 2 },
223
+ * evaluate: (ctx) => {
224
+ * if (ctx.state.userData.triggeredSuperFreespins) {
225
+ * return { superbonus: 1 }
226
+ * }
227
+ * }
228
+ * },
229
+ * userData: { forceSuperFreespins: true },
230
+ * }),
231
+ * ```
245
232
  */
246
- cost: number;
233
+ reelWeights: ReelWeights<TUserState>;
247
234
  /**
248
- * The target RTP of the game.
235
+ * Optional data to use when evaluating the criteria.\
236
+ * This can be used to pass additional context or parameters needed for the evaluation.
249
237
  */
250
- rtp: number;
238
+ userData?: Record<string, any>;
251
239
  /**
252
- * Defines and generates all reels for the game.\
253
- * Which reels are used in a spin is determined by the ResultSet of the current game mode.
254
- *
255
- * It is common to have one reel set for the base game and another for free spins.\
256
- * Each `ResultSet` can then set the weights of these reel sets to control which\
257
- * reel set is used for a specific criteria.
258
- *
259
- * The generator can be adjusted to match the reels to your games needs.
240
+ * If set, this will force the game to always trigger a max win.
260
241
  */
261
- reelSets: ReelGenerator[];
242
+ forceMaxWin?: boolean;
262
243
  /**
263
- * A ResultSet defines how often a specific outcome should be generated.\
264
- * For example, a ResultSet can be used to force a specific ratio of max wins\
265
- * in the simulations to ensure there are different frontend representations.
244
+ * If set, this will force the game to always trigger free spins.
266
245
  */
267
- resultSets: ResultSet<any>[];
246
+ forceFreespins?: boolean;
268
247
  /**
269
- * Whether this game mode is a bonus buy.
248
+ * Custom function to evaluate if the criteria is met.
249
+ *
250
+ * E.g. use this to check for free spins that upgraded to super free spins\
251
+ * or other arbitrary simulation criteria.
270
252
  */
271
- isBonusBuy: boolean;
253
+ evaluate?: (ctx: GameContext<AnyGameModes, AnySymbols, TUserState>) => boolean;
254
+ }
255
+ interface ReelWeights<TUserState extends AnyUserData> {
256
+ [SPIN_TYPE.BASE_GAME]: Record<string, number>;
257
+ [SPIN_TYPE.FREE_SPINS]: Record<string, number>;
258
+ evaluate?: (ctx: GameContext<AnyGameModes, AnySymbols, TUserState>) => Record<string, number> | undefined | null | false;
272
259
  }
273
- type GameModeName = "base" | "base-extra-chance-2x" | "base-extra-chance-10x" | "bonus" | string;
274
260
 
275
- declare class Simulation {
276
- protected readonly gameConfigOpts: CommonGameOptions;
277
- readonly gameConfig: GameConfig;
278
- readonly simRunsAmount: Partial<Record<GameModeName, number>>;
279
- private readonly concurrency;
280
- private wallet;
281
- private library;
282
- readonly records: RecordItem[];
283
- protected debug: boolean;
284
- constructor(opts: SimulationConfigOpts, gameConfigOpts: CommonGameOptions);
285
- runSimulation(opts: SimulationOpts): Promise<void>;
261
+ interface GameStateOptions<TUserState extends AnyUserData> {
262
+ currentSimulationId: number;
286
263
  /**
287
- * Runs all simulations for a specific game mode.
264
+ * e.g. "base", "freespins", etc. (depending on the game config)
288
265
  */
289
- spawnWorkersForGameMode(opts: {
290
- mode: string;
291
- simNumsToCriteria: Record<number, string>;
292
- }): Promise<void>;
293
- callWorker(opts: {
294
- basePath: string;
295
- mode: string;
296
- simStart: number;
297
- simEnd: number;
298
- index: number;
299
- totalSims: number;
300
- }): Promise<unknown>;
266
+ currentGameMode: string;
301
267
  /**
302
- * Creates a CSV file in the format "simulationId,weight,payout".
303
- *
304
- * `weight` defaults to 1.
268
+ * Spin type constant as defined in `SPIN_TYPE`
305
269
  */
306
- private static writeLookupTableCSV;
270
+ currentSpinType: SpinType;
307
271
  /**
308
- * Creates a CSV file in the format "simulationId,criteria,payoutBase,payoutFreespins".
272
+ * The current ResultSet for the active simulation run.
309
273
  */
310
- private static writeLookupTableSegmentedCSV;
311
- private static writeRecords;
312
- private static writeIndexJson;
313
- private static writeBooksJson;
314
- private static logSymbolOccurrences;
274
+ currentResultSet: ResultSet<any>;
315
275
  /**
316
- * Compiles user configured game to JS for use in different Node processes
276
+ * Whether the criteria in the ResultSet for the current simulation has been met.
317
277
  */
318
- private preprocessFiles;
319
- private getSimRangesForChunks;
320
- private mergeRecords;
321
- }
322
- type SimulationConfigOpts = {
278
+ isCriteriaMet: boolean;
323
279
  /**
324
- * Object containing the game modes and their respective simulation runs amount.
280
+ * Number of freespins remaining in the current freespin round.
325
281
  */
326
- simRunsAmount: Partial<Record<GameModeName, number>>;
282
+ currentFreespinAmount: number;
327
283
  /**
328
- * Number of concurrent processes to use for simulations.
329
- *
330
- * Default: 6
284
+ * Total amount of freespins awarded during the active simulation.
331
285
  */
332
- concurrency?: number;
333
- };
334
- /**
335
- * @internal
336
- */
337
- type AnySimulationContext<TGameModes extends AnyGameModes = AnyGameModes, TSymbols extends AnySymbols = AnySymbols, TUserState extends AnyUserData = AnyUserData> = SimulationContext<TGameModes, TSymbols, TUserState>;
338
- /**
339
- * @internal
340
- */
341
- declare class SimulationContext<TGameModes extends AnyGameModes, TSymbols extends AnySymbols, TUserState extends AnyUserData> extends Board<TGameModes, TSymbols, TUserState> {
342
- constructor(opts: CommonGameOptions<any, any, TUserState>);
343
- actualSims: number;
286
+ totalFreespinAmount: number;
344
287
  /**
345
- * Will run a single simulation until the specified criteria is met.
288
+ * Custom user data that can be used in game flow logic.
346
289
  */
347
- runSingleSimulation(opts: {
348
- simId: number;
349
- mode: string;
350
- criteria: string;
351
- index: number;
352
- }): void;
290
+ userData: TUserState;
353
291
  /**
354
- * If a simulation does not meet the required criteria, reset the state to run it again.
355
- *
356
- * This also runs once before each simulation to ensure a clean state.
292
+ * Whether a max win has been triggered during the active simulation.
357
293
  */
358
- protected resetSimulation(): void;
294
+ triggeredMaxWin: boolean;
359
295
  /**
360
- * Contains and executes the entire game logic:
361
- * - Drawing the board
362
- * - Evaluating wins
363
- * - Updating wallet
364
- * - Handling free spins
365
- * - Recording events
366
- *
367
- * You can customize the game flow by implementing the `onHandleGameFlow` hook in the game configuration.
296
+ * Whether freespins have been triggered during the active simulation.
368
297
  */
369
- protected handleGameFlow(): void;
298
+ triggeredFreespins: boolean;
370
299
  }
371
- interface SimulationOpts {
372
- debug?: boolean;
300
+ declare function createGameState<TUserState extends AnyUserData = AnyUserData>(opts?: Partial<GameStateOptions<TUserState>>): {
301
+ currentSimulationId: number;
302
+ currentGameMode: string;
303
+ currentSpinType: SpinType;
304
+ currentResultSet: ResultSet<any>;
305
+ isCriteriaMet: boolean;
306
+ currentFreespinAmount: number;
307
+ totalFreespinAmount: number;
308
+ userData: TUserState;
309
+ triggeredMaxWin: boolean;
310
+ triggeredFreespins: boolean;
311
+ };
312
+ type GameState<TUserState extends AnyUserData = AnyUserData> = ReturnType<typeof createGameState<TUserState>>;
313
+
314
+ declare class AbstractService {
315
+ /**
316
+ * Function that returns the current game context.
317
+ */
318
+ protected ctx: () => GameContext;
319
+ constructor(ctx: () => GameContext);
373
320
  }
374
321
 
375
- /**
376
- * Stores win amounts for simulations.
377
- */
378
- declare class Wallet {
322
+ declare class GameSymbol {
323
+ readonly id: string;
324
+ readonly pays?: Record<number, number>;
325
+ readonly properties: Map<string, any>;
326
+ constructor(opts: GameSymbolOpts);
379
327
  /**
380
- * Total win amount (as the bet multiplier) from all simulations.
328
+ * Compares this symbol to another symbol or a set of properties.
381
329
  */
382
- protected cumulativeWins: number;
330
+ compare(symbolOrProperties?: GameSymbol | Record<string, any>): boolean;
331
+ }
332
+ interface GameSymbolOpts {
383
333
  /**
384
- * Total win amount (as the bet multiplier) per spin type.
385
- *
386
- * @example
387
- * ```ts
388
- * {
389
- * basegame: 50,
390
- * freespins: 100,
391
- * superfreespins: 200,
392
- * }
393
- * ```
334
+ * Unique identifier for the symbol, e.g. "W", "H1", "L5", etc.
394
335
  */
395
- protected cumulativeWinsPerSpinType: {
396
- basegame: number;
397
- freespins: number;
398
- };
336
+ id: string;
399
337
  /**
400
- * Current win amount (as the bet multiplier) for the ongoing simulation.
338
+ * Paytable for the symbol, where the key is the number of symbols and the value is the payout multiplier.
401
339
  */
402
- protected currentWin: number;
340
+ pays?: Record<number, number>;
403
341
  /**
404
- * Current win amount (as the bet multiplier) for the ongoing simulation per spin type.
342
+ * Additional properties for the symbol, e.g. `multiplier` or `isWild`.
343
+ *
344
+ * Properties can help identify special symbols.
405
345
  *
406
346
  * @example
347
+ * If your game has a "normal" scatter and a "super" scatter, you can define them like this:
348
+ *
407
349
  * ```ts
408
- * {
409
- * basegame: 50,
410
- * freespins: 100,
411
- * superfreespins: 200,
350
+ * properties: {
351
+ * isScatter: true,
412
352
  * }
413
353
  * ```
414
354
  */
415
- protected currentWinPerSpinType: {
416
- basegame: number;
417
- freespins: number;
418
- };
355
+ properties?: Record<string, any>;
356
+ }
357
+
358
+ declare class BoardService<TGameModes extends AnyGameModes = AnyGameModes, TSymbols extends AnySymbols = AnySymbols, TUserState extends AnyUserData = AnyUserData> extends AbstractService {
359
+ private board;
360
+ constructor(ctx: () => GameContext<TGameModes, TSymbols, TUserState>);
419
361
  /**
420
- * Holds the current win amount for a single (free) spin.\
421
- * After each spin, this amount is added to `currentWinPerSpinType` and then reset to zero.
362
+ * Resets the board to an empty state.\
363
+ * This is called before drawing a new board.
422
364
  */
423
- protected currentSpinWin: number;
365
+ resetBoard(): void;
424
366
  /**
425
- * Current win amount (as the bet multiplier) for the ongoing tumble sequence.
367
+ * Gets the current reels and symbols on the board.
426
368
  */
427
- protected currentTumbleWin: number;
428
- constructor();
369
+ getBoardReels(): Reels;
370
+ getPaddingTop(): Reels;
371
+ getPaddingBottom(): Reels;
372
+ getAnticipation(): boolean[];
373
+ private resetReels;
429
374
  /**
430
- * Updates the win for the current spin.
431
- *
432
- * Should be called after each tumble event, if applicable.\
433
- * Or generally call this to add wins during a spin.
434
- *
435
- * After each (free) spin, this amount should be added to `currentWinPerSpinType` via `confirmSpinWin()`
375
+ * Sets the anticipation value for a specific reel.
436
376
  */
437
- addSpinWin(amount: number): void;
377
+ setAnticipationForReel(reelIndex: number, value: boolean): void;
438
378
  /**
439
- * Assigns a win amount to the given spin type.
440
- *
441
- * Should be called after `addSpinWin()`, and after your tumble events are played out,\
442
- * and after a (free) spin is played out to finalize the win.
379
+ * Counts how many symbols matching the criteria are on a specific reel.
443
380
  */
444
- confirmSpinWin(spinType: SpinType): void;
381
+ countSymbolsOnReel(symbolOrProperties: GameSymbol | Record<string, any>, reelIndex: number): number;
445
382
  /**
446
- * Returns the accumulated win amount (as the bet multiplier) from all simulations.
383
+ * Counts how many symbols matching the criteria are on the board.
384
+ *
385
+ * Passing a GameSymbol will compare by ID, passing a properties object will compare by properties.
386
+ *
387
+ * Returns a tuple where the first element is the total count, and the second element is a record of counts per reel index.
447
388
  */
448
- getCumulativeWins(): number;
389
+ countSymbolsOnBoard(symbolOrProperties: GameSymbol | Record<string, any>): [number, Record<number, number>];
449
390
  /**
450
- * Returns the accumulated win amount (as the bet multiplier) per spin type from all simulations.
391
+ * Checks if a symbol appears more than once on any reel in the current reel set.
392
+ *
393
+ * Useful to check for "forbidden" generations, e.g. 2 scatters on one reel.
451
394
  */
452
- getCumulativeWinsPerSpinType(): {
453
- basegame: number;
454
- freespins: number;
455
- };
395
+ isSymbolOnAnyReelMultipleTimes(symbol: GameSymbol): boolean;
456
396
  /**
457
- * Returns the current win amount (as the bet multiplier) for the ongoing simulation.
397
+ * Gets all reel stops (positions) where the specified symbol appears in the current reel set.\
398
+ * Returns an array of arrays, where each inner array contains the positions for the corresponding reel.
458
399
  */
459
- getCurrentWin(): number;
400
+ getReelStopsForSymbol(reels: Reels, symbol: GameSymbol): number[][];
460
401
  /**
461
- * Returns the current win amount (as the bet multiplier) per spin type for the ongoing simulation.
402
+ * Combines multiple arrays of reel stops into a single array of reel stops.\
462
403
  */
463
- getCurrentWinPerSpinType(): {
464
- basegame: number;
465
- freespins: number;
466
- };
404
+ combineReelStops(...reelStops: number[][][]): number[][];
467
405
  /**
468
- * Adds a win to `currentSpinWin` and `currentTumbleWin`.
406
+ * From a list of reel stops on reels, selects a random stop for a speficied number of random symbols.
469
407
  *
470
- * After each (free) spin, this amount should be added to `currentWinPerSpinType` via `confirmSpinWin()`
408
+ * Mostly useful for placing scatter symbols on the board.
471
409
  */
472
- addTumbleWin(amount: number): void;
410
+ getRandomReelStops(reels: Reels, reelStops: number[][], amount: number): Record<number, number>;
473
411
  /**
474
- * Resets the current win amounts to zero.
412
+ * Selects a random reel set based on the configured weights of the current result set.\
413
+ * Returns the reels as arrays of GameSymbols.
475
414
  */
476
- resetCurrentWin(): void;
415
+ getRandomReelset(): Reels;
477
416
  /**
478
- * Adds current wins to cumulative wins and resets current wins to zero.
417
+ * Draws a board using specified reel stops.
479
418
  */
480
- confirmWins(ctx: AnySimulationContext): void;
481
- serialize(): {
482
- cumulativeWins: number;
483
- cumulativeWinsPerSpinType: {
484
- basegame: number;
485
- freespins: number;
486
- };
487
- currentWin: number;
488
- currentWinPerSpinType: {
489
- basegame: number;
490
- freespins: number;
491
- };
492
- currentSpinWin: number;
493
- currentTumbleWin: number;
494
- };
495
- merge(wallet: Wallet): void;
496
- mergeSerialized(data: ReturnType<Wallet["serialize"]>): void;
419
+ drawBoardWithForcedStops(reels: Reels, forcedStops: Record<string, number>): void;
420
+ /**
421
+ * Draws a board using random reel stops.
422
+ */
423
+ drawBoardWithRandomStops(reels: Reels): void;
424
+ private drawBoardMixed;
425
+ /**
426
+ * Tumbles the board. All given symbols will be deleted and new symbols will fall from the top.
427
+ */
428
+ tumbleBoard(symbolsToDelete: Array<{
429
+ reelIdx: number;
430
+ rowIdx: number;
431
+ }>): void;
432
+ }
433
+
434
+ declare class Recorder {
435
+ records: RecordItem[];
436
+ pendingRecords: PendingRecord[];
437
+ constructor();
438
+ }
439
+ interface PendingRecord {
440
+ bookId: number;
441
+ properties: Record<string, string>;
442
+ }
443
+ interface RecordItem {
444
+ search: Array<{
445
+ name: string;
446
+ value: string;
447
+ }>;
448
+ timesTriggered: number;
449
+ bookIds: number[];
497
450
  }
498
451
 
499
452
  declare class Book {
500
- id: number;
453
+ readonly id: number;
501
454
  criteria: string;
502
- protected events: BookEvent[];
503
- protected payout: number;
504
- protected basegameWins: number;
505
- protected freespinsWins: number;
455
+ events: BookEvent[];
456
+ payout: number;
457
+ basegameWins: number;
458
+ freespinsWins: number;
506
459
  constructor(opts: BookOpts);
460
+ /**
461
+ * Intended for internal use only.
462
+ */
463
+ setCriteria(criteria: string): void;
507
464
  /**
508
465
  * Adds an event to the book.
509
466
  */
510
467
  addEvent(event: Omit<BookEvent, "index">): void;
511
468
  /**
512
- * Transfers the win data from the wallet to the book.
469
+ * Intended for internal use only.
513
470
  */
514
- writePayout(ctx: AnySimulationContext): void;
515
- getPayout(): number;
516
- getBasegameWins(): number;
517
- getFreespinsWins(): number;
518
471
  serialize(): {
519
472
  id: number;
520
473
  criteria: string;
@@ -523,6 +476,9 @@ declare class Book {
523
476
  basegameWins: number;
524
477
  freespinsWins: number;
525
478
  };
479
+ /**
480
+ * Intended for internal use only.
481
+ */
526
482
  static fromSerialized(data: ReturnType<Book["serialize"]>): Book;
527
483
  }
528
484
  interface BookEvent {
@@ -532,85 +488,35 @@ interface BookEvent {
532
488
  }
533
489
  interface BookOpts {
534
490
  id: number;
491
+ criteria: string;
535
492
  }
536
493
 
537
- /**
538
- * The GameState manages the current state of the game.
539
- */
540
- declare class GameState<TGameModes extends AnyGameModes, TSymbols extends AnySymbols, TUserState extends AnyUserData> extends GameConfig<TGameModes, TSymbols, TUserState> {
541
- state: {
542
- currentSimulationId: number;
543
- /**
544
- * e.g. "base", "freespins", etc. (depending on the game config)
545
- */
546
- currentGameMode: string;
547
- /**
548
- * Spin type constant as defined in `GameConfig.SPIN_TYPE`
549
- */
550
- currentSpinType: SpinType;
551
- /**
552
- * The current ResultSet for the active simulation run.
553
- */
554
- currentResultSet: ResultSet<any>;
555
- /**
556
- * Whether the criteria in the ResultSet for the current simulation has been met.
557
- */
558
- isCriteriaMet: boolean;
559
- /**
560
- * Number of freespins remaining in the current freespin round.
561
- */
562
- currentFreespinAmount: number;
563
- /**
564
- * Total amount of freespins awarded during the active simulation.
565
- */
566
- totalFreespinAmount: number;
567
- /**
568
- * A library of all completed books, indexed by their ID.
569
- */
570
- library: Map<string, Book>;
571
- /**
572
- * The current book being recorded.
573
- */
574
- book: Book;
575
- /**
576
- * Seeded random number generator instance for the current simulation.
577
- */
578
- rng: RandomNumberGenerator;
579
- /**
580
- * Custom user data that can be used in game flow logic.
581
- */
582
- userData: TUserState;
583
- /**
584
- * Whether a max win has been triggered during the active simulation.
585
- */
586
- triggeredMaxWin: boolean;
587
- /**
588
- * Whether freespins have been triggered during the active simulation.
589
- */
590
- triggeredFreespins: boolean;
591
- };
494
+ declare class DataService<TGameModes extends AnyGameModes = AnyGameModes, TSymbols extends AnySymbols = AnySymbols, TUserState extends AnyUserData = AnyUserData> extends AbstractService {
495
+ private recorder;
496
+ private book;
497
+ constructor(ctx: () => GameContext<TGameModes, TSymbols, TUserState>);
498
+ private ensureRecorder;
499
+ private ensureBook;
592
500
  /**
593
- * The wallet stores win data for the current and all simulations, respectively.
501
+ * Intended for internal use only.
594
502
  */
595
- wallet: Wallet;
503
+ _setRecorder(recorder: Recorder): void;
596
504
  /**
597
- * Recorder for statistical analysis (e.g. symbol occurrences, etc.).
505
+ * Intended for internal use only.
598
506
  */
599
- private recorder;
600
- constructor(opts: CommonGameOptions<TGameModes, TSymbols, TUserState>);
507
+ _getBook(): Book;
601
508
  /**
602
- * Gets the configuration for the current game mode.
509
+ * Intended for internal use only.
603
510
  */
604
- getCurrentGameMode(): GameMode;
605
- resetState(): void;
511
+ _setBook(book: Book): void;
606
512
  /**
607
- * Empties the list of pending records in the recorder.
513
+ * Intended for internal use only.
608
514
  */
609
- clearPendingRecords(): void;
515
+ _getRecorder(): Recorder;
610
516
  /**
611
- * Confirms all pending records and adds them to the main records list.
517
+ * Intended for internal use only.
612
518
  */
613
- confirmRecords(): void;
519
+ _getRecords(): RecordItem[];
614
520
  /**
615
521
  * Record data for statistical analysis.
616
522
  */
@@ -618,7 +524,7 @@ declare class GameState<TGameModes extends AnyGameModes, TSymbols extends AnySym
618
524
  /**
619
525
  * Records a symbol occurrence for statistical analysis.
620
526
  *
621
- * Calls `this.record()` with the provided data.
527
+ * Calls `ctx.services.data.record()` with the provided data.
622
528
  */
623
529
  recordSymbolOccurrence(data: {
624
530
  kind: number;
@@ -627,458 +533,411 @@ declare class GameState<TGameModes extends AnyGameModes, TSymbols extends AnySym
627
533
  [key: string]: any;
628
534
  }): void;
629
535
  /**
630
- * Gets all confirmed records.
536
+ * Adds an event to the book.
631
537
  */
632
- getRecords(): RecordItem[];
538
+ addBookEvent(event: Omit<BookEvent, "index">): void;
633
539
  /**
634
- * Moves the current book to the library and resets the current book.
540
+ * Intended for internal use only.
635
541
  */
636
- moveBookToLibrary(): void;
542
+ _clearPendingRecords(): void;
543
+ }
544
+
545
+ declare class RngService<TGameModes extends AnyGameModes = AnyGameModes, TSymbols extends AnySymbols = AnySymbols, TUserState extends AnyUserData = AnyUserData> extends AbstractService {
546
+ protected rng: RandomNumberGenerator;
547
+ constructor(ctx: () => GameContext<TGameModes, TSymbols, TUserState>);
637
548
  /**
638
- * Increases the freespin count by the specified amount.
639
- *
640
- * Also sets `state.triggeredFreespins` to true.
549
+ * Random weighted selection from a set of items.
641
550
  */
642
- awardFreespins(amount: number): void;
551
+ weightedRandom: <T extends Record<string, number>>(weights: T) => string;
643
552
  /**
644
- * Ensures the requested number of scatters is valid based on the game configuration.\
645
- * Returns a valid number of scatters.
553
+ * Selects a random item from an array.
646
554
  */
647
- verifyScatterCount(numScatters: number): number;
648
- }
649
- interface RecordItem {
650
- search: Array<{
651
- name: string;
652
- value: string;
653
- }>;
654
- timesTriggered: number;
655
- bookIds: number[];
656
- }
657
-
658
- /**
659
- * A version of the Board class that is disconnected from the actual game state\
660
- * and operates on a copy of the game context.
661
- *
662
- * Can be used in custom game logic where you need to evaluate an additional board or reels independent of the main board,\
663
- * similar to the top and bottom reels of the game "San Quentin".
664
- */
665
- declare class StandaloneBoard {
666
- protected reels: Reels;
667
- protected paddingTop: Reels;
668
- protected paddingBottom: Reels;
669
- protected anticipation: number[];
670
- protected ctx: AnySimulationContext;
671
- constructor(opts: StandaloneBoardOpts);
555
+ randomItem: <T>(array: T[]) => NonNullable<T>;
672
556
  /**
673
- * Updates the context used by this board instance.
557
+ * Shuffles an array.
674
558
  */
675
- context(ctx: AnySimulationContext): void;
559
+ shuffle: <T>(array: T[]) => T[];
676
560
  /**
677
- * Resets the board to an empty state.\
678
- * This is called before drawing a new board.
561
+ * Generates a random float between two values.
679
562
  */
680
- resetBoard(): void;
681
- private makeEmptyReels;
682
- private resetReels;
563
+ randomFloat: (low: number, high: number) => number;
683
564
  /**
684
- * Counts how many symbols matching the criteria are on a specific reel.
565
+ * Sets the seed for the RNG.
685
566
  */
686
- countSymbolsOnReel(symbolOrProperties: GameSymbol | Record<string, any>, reelIndex: number): number;
567
+ setSeedIfDifferent: (seed: number) => void;
568
+ }
569
+ declare class RandomNumberGenerator {
570
+ mIdum: number;
571
+ mIy: number;
572
+ mIv: Array<number>;
573
+ NTAB: number;
574
+ IA: number;
575
+ IM: number;
576
+ IQ: number;
577
+ IR: number;
578
+ NDIV: number;
579
+ AM: number;
580
+ RNMX: number;
581
+ protected _currentSeed: number;
582
+ constructor();
583
+ getCurrentSeed(): number;
584
+ protected setCurrentSeed(seed: number): void;
585
+ setSeed(seed: number): void;
586
+ setSeedIfDifferent(seed: number): void;
587
+ generateRandomNumber(): number;
588
+ randomFloat(low: number, high: number): number;
589
+ weightedRandom<T extends Record<string, number>>(weights: T): string;
590
+ randomItem<T>(array: T[]): NonNullable<T>;
591
+ shuffle<T>(array: T[]): T[];
592
+ }
593
+
594
+ declare class ReelSet {
595
+ id: string;
596
+ associatedGameModeName: string;
597
+ reels: Reels;
598
+ protected rng: RandomNumberGenerator;
599
+ constructor(opts: ReelSetOptions);
600
+ generateReels(simulation: Simulation): void;
687
601
  /**
688
- * Counts how many symbols matching the criteria are on the board.
689
- *
690
- * Passing a GameSymbol will compare by ID, passing a properties object will compare by properties.
691
- *
692
- * Returns a tuple where the first element is the total count, and the second element is a record of counts per reel index.
602
+ * Reads a reelset CSV file and returns the reels as arrays of GameSymbols.
693
603
  */
694
- countSymbolsOnBoard(symbolOrProperties: GameSymbol | Record<string, any>): [number, Record<number, number>];
604
+ parseReelsetCSV(reelSetPath: string, config: GameConfig): Reels;
605
+ }
606
+ interface ReelSetOptions {
695
607
  /**
696
- * Checks if a symbol appears more than once on any reel in the current reel set.
608
+ * The unique identifier of the reel generator.\
609
+ * Must be unique per game mode.
610
+ */
611
+ id: string;
612
+ /**
613
+ * Optional seed for the RNG to ensure reproducible results.
697
614
  *
698
- * Useful to check for "forbidden" generations, e.g. 2 scatters on one reel.
615
+ * Default seed is `0`.
616
+ *
617
+ * Note: Seeds 0 and 1 produce the same results.
699
618
  */
700
- isSymbolOnAnyReelMultipleTimes(symbol: GameSymbol): boolean;
619
+ seed?: number;
620
+ }
621
+
622
+ declare class GameMode {
623
+ readonly name: string;
624
+ readonly reelsAmount: number;
625
+ readonly symbolsPerReel: number[];
626
+ readonly cost: number;
627
+ readonly rtp: number;
628
+ readonly reelSets: ReelSet[];
629
+ readonly resultSets: ResultSet<any>[];
630
+ readonly isBonusBuy: boolean;
631
+ constructor(opts: GameModeOpts);
632
+ }
633
+ interface GameModeOpts {
701
634
  /**
702
- * Draws a board using specified reel stops.
635
+ * Name of the game mode.
703
636
  */
704
- drawBoardWithForcedStops(reels: Reels, forcedStops: Record<string, number>): void;
637
+ name: string;
705
638
  /**
706
- * Draws a board using random reel stops.
639
+ * Number of reels the board has.
707
640
  */
708
- drawBoardWithRandomStops(reels: Reels): void;
709
- private drawBoardMixed;
710
- }
711
- interface StandaloneBoardOpts {
712
- ctx: AnySimulationContext;
713
- }
714
- /**
715
- * Extends GameState. Provides board-related functionality.
716
- */
717
- declare class Board<TGameModes extends AnyGameModes, TSymbols extends AnySymbols, TUserState extends AnyUserData> extends GameState<TGameModes, TSymbols, TUserState> {
718
- board: {
719
- reels: Reels;
720
- paddingTop: Reels;
721
- paddingBottom: Reels;
722
- anticipation: number[];
723
- };
724
- constructor(opts: CommonGameOptions<TGameModes, TSymbols, TUserState>);
641
+ reelsAmount: number;
725
642
  /**
726
- * Resets the board to an empty state.\
727
- * This is called before drawing a new board.
643
+ * How many symbols each reel has. Array length must match `reelsAmount`.\
644
+ * The number at an array index represents the number of symbols on that reel.
728
645
  */
729
- resetBoard(): void;
730
- private makeEmptyReels;
731
- private resetReels;
646
+ symbolsPerReel: number[];
732
647
  /**
733
- * Counts how many symbols matching the criteria are on a specific reel.
648
+ * Cost of the game mode, multiplied by the base bet.
734
649
  */
735
- countSymbolsOnReel(symbolOrProperties: GameSymbol | Record<string, any>, reelIndex: number): number;
650
+ cost: number;
736
651
  /**
737
- * Counts how many symbols matching the criteria are on the board.
738
- *
739
- * Passing a GameSymbol will compare by ID, passing a properties object will compare by properties.
740
- *
741
- * Returns a tuple where the first element is the total count, and the second element is a record of counts per reel index.
652
+ * The target RTP of the game.
742
653
  */
743
- countSymbolsOnBoard(symbolOrProperties: GameSymbol | Record<string, any>): [number, Record<number, number>];
654
+ rtp: number;
744
655
  /**
745
- * Checks if a symbol appears more than once on any reel in the current reel set.
656
+ * Defines (and generates) all reels for the game.\
657
+ * Which reels are used in a spin is determined by the ResultSet of the current game mode.
746
658
  *
747
- * Useful to check for "forbidden" generations, e.g. 2 scatters on one reel.
659
+ * It is common to have one reel set for the base game and another for free spins.\
660
+ * Each `ResultSet` can then set the weights of these reel sets to control which\
661
+ * reel set is used for a specific criteria.
748
662
  */
749
- isSymbolOnAnyReelMultipleTimes(symbol: GameSymbol): boolean;
663
+ reelSets: ReelSet[];
750
664
  /**
751
- * Gets all reel stops (positions) where the specified symbol appears in the current reel set.\
752
- * Returns an array of arrays, where each inner array contains the positions for the corresponding reel.
665
+ * A ResultSet defines how often a specific outcome should be generated.\
666
+ * For example, a ResultSet can be used to force a specific ratio of max wins\
667
+ * in the simulations to ensure there are different frontend representations.
753
668
  */
754
- getReelStopsForSymbol(reels: Reels, symbol: GameSymbol): number[][];
669
+ resultSets: ResultSet<any>[];
755
670
  /**
756
- * Combines multiple arrays of reel stops into a single array of reel stops.\
671
+ * Whether this game mode is a bonus buy.
757
672
  */
758
- combineReelStops(...reelStops: number[][][]): number[][];
673
+ isBonusBuy: boolean;
674
+ }
675
+
676
+ declare class GameService<TGameModes extends AnyGameModes = AnyGameModes, TSymbols extends AnySymbols = AnySymbols, TUserState extends AnyUserData = AnyUserData> extends AbstractService {
677
+ constructor(ctx: () => GameContext<TGameModes, TSymbols, TUserState>);
759
678
  /**
760
- * From a list of reel stops on reels, selects a random stop for a speficied number of random symbols.
761
- *
762
- * Mostly useful for placing scatter symbols on the board.
679
+ * Retrieves a reel set by its ID within a specific game mode.
763
680
  */
764
- getRandomReelStops(reels: Reels, reelStops: number[][], amount: number): Record<string, number>;
681
+ getReelsetById(gameMode: string, id: string): Reels;
765
682
  /**
766
- * Selects a random reel set based on the configured weights of the current result set.\
767
- * Returns the reels as arrays of GameSymbols.
683
+ * Retrieves the number of free spins awarded for a given spin type and scatter count.
768
684
  */
769
- getRandomReelset(): Reels;
685
+ getFreeSpinsForScatters(spinType: SpinType, scatterCount: number): number;
770
686
  /**
771
- * Draws a board using specified reel stops.
687
+ * Retrieves a result set by its criteria within a specific game mode.
772
688
  */
773
- drawBoardWithForcedStops(reels: Reels, forcedStops: Record<string, number>): void;
689
+ getResultSetByCriteria(mode: string, criteria: string): ResultSet<any>;
774
690
  /**
775
- * Draws a board using random reel stops.
691
+ * Returns all configured symbols as an array.
776
692
  */
777
- drawBoardWithRandomStops(reels: Reels): void;
778
- private drawBoardMixed;
779
- }
780
-
781
- declare class ResultSet<TUserState extends AnyUserData> {
782
- criteria: string;
783
- quota: number;
784
- multiplier?: number;
785
- reelWeights: ReelWeights<TUserState>;
786
- userData?: Record<string, any>;
787
- forceMaxWin?: boolean;
788
- forceFreespins?: boolean;
789
- evaluate?: (ctx: AnySimulationContext<any, any, TUserState>) => boolean;
790
- constructor(opts: ResultSetOpts<TUserState>);
791
- static assignCriteriaToSimulations(ctx: Simulation, gameModeName: string): Record<number, string>;
693
+ getSymbolArray(): GameSymbol[];
792
694
  /**
793
- * Checks if core criteria is met, e.g. target multiplier or max win.
695
+ * Gets the configuration for the current game mode.
794
696
  */
795
- meetsCriteria(ctx: AnySimulationContext<any, any, TUserState>): boolean;
796
- }
797
- interface ResultSetOpts<TUserState extends AnyUserData> {
697
+ getCurrentGameMode(): GameMode;
798
698
  /**
799
- * A short string to describe the criteria for this ResultSet.
699
+ * Ensures the requested number of scatters is valid based on the game configuration.\
700
+ * Returns a valid number of scatters.
800
701
  */
801
- criteria: string;
702
+ verifyScatterCount(numScatters: number): number;
802
703
  /**
803
- * The quota of spins, out of the total simulations, that must be forced to meet the specified criteria.\
804
- * **Float from 0 to 1. Total quota of all ResultSets in a GameMode must be 1.**
704
+ * Increases the freespin count by the specified amount.
705
+ *
706
+ * Also sets `state.triggeredFreespins` to true.
805
707
  */
806
- quota: number;
708
+ awardFreespins(amount: number): void;
709
+ }
710
+
711
+ declare class Wallet {
807
712
  /**
808
- * The required multiplier for a simulated spin to be accepted.
713
+ * Total win amount (as the bet multiplier) from all simulations.
809
714
  */
810
- multiplier?: number;
715
+ protected cumulativeWins: number;
811
716
  /**
812
- * Configure the weights of the reels in this ResultSet.
813
- *
814
- * If you need to support dynamic / special reel weights based on the simulation context,\
815
- * you can provide an `evaluate` function that returns the desired weights.
816
- *
817
- * If the `evaluate` function returns a falsy value, the usual spin type based weights will be used.
717
+ * Total win amount (as the bet multiplier) per spin type.
818
718
  *
819
719
  * @example
820
720
  * ```ts
821
- * new ResultSet({
822
- * criteria: "superFreespins",
823
- * quota: 0.05,
824
- * forceFreespins: true,
825
- * reelWeights: {
826
- * [GameConfig.SPIN_TYPE.BASE_GAME]: { base1: 1 },
827
- * [GameConfig.SPIN_TYPE.FREE_SPINS]: { bonus1: 1, bonus2: 2 },
828
- * evaluate: (ctx) => {
829
- * if (ctx.state.userData.triggeredSuperFreespins) {
830
- * return { superbonus: 1 }
831
- * }
832
- * }
833
- * },
834
- * userData: { forceSuperFreespins: true },
835
- * }),
721
+ * {
722
+ * basegame: 50,
723
+ * freespins: 100,
724
+ * superfreespins: 200,
725
+ * }
836
726
  * ```
837
727
  */
838
- reelWeights: ReelWeights<TUserState>;
839
- /**
840
- * Optional data to use when evaluating the criteria.\
841
- * This can be used to pass additional context or parameters needed for the evaluation.
842
- */
843
- userData?: Record<string, any>;
844
- /**
845
- * If set, this will force the game to always trigger a max win.
846
- */
847
- forceMaxWin?: boolean;
728
+ protected cumulativeWinsPerSpinType: {
729
+ basegame: number;
730
+ freespins: number;
731
+ };
848
732
  /**
849
- * If set, this will force the game to always trigger free spins.
733
+ * Current win amount (as the bet multiplier) for the ongoing simulation.
850
734
  */
851
- forceFreespins?: boolean;
735
+ protected currentWin: number;
852
736
  /**
853
- * Custom function to evaluate if the criteria is met.
737
+ * Current win amount (as the bet multiplier) for the ongoing simulation per spin type.
854
738
  *
855
- * E.g. use this to check for free spins that upgraded to super free spins\
856
- * or other arbitrary simulation criteria.
739
+ * @example
740
+ * ```ts
741
+ * {
742
+ * basegame: 50,
743
+ * freespins: 100,
744
+ * superfreespins: 200,
745
+ * }
746
+ * ```
857
747
  */
858
- evaluate?: (ctx: EvaluationContext<TUserState>) => boolean;
859
- }
860
- interface ReelWeights<TUserState extends AnyUserData> {
861
- [GameConfig.SPIN_TYPE.BASE_GAME]: Record<string, number>;
862
- [GameConfig.SPIN_TYPE.FREE_SPINS]: Record<string, number>;
863
- evaluate?: (ctx: EvaluationContext<TUserState>) => Record<string, number> | undefined | null | false;
864
- }
865
- type EvaluationContext<TUserState extends AnyUserData> = Board<any, any, TUserState>;
866
-
867
- /**
868
- * Static configuration for a slot game.\
869
- * This shouldn't change during gameplay.
870
- */
871
- declare class GameConfig<TGameModes extends AnyGameModes = AnyGameModes, TSymbols extends AnySymbols = AnySymbols, TUserState extends AnyUserData = AnyUserData> {
872
- readonly config: {
873
- readonly id: string;
874
- readonly name: string;
875
- readonly gameModes: Record<GameModeName, GameMode>;
876
- readonly symbols: Map<keyof TSymbols & string, TSymbols[keyof TSymbols]>;
877
- readonly padSymbols?: number;
878
- readonly scatterToFreespins: Record<string, Record<number, number>>;
879
- readonly anticipationTriggers: Record<SpinType, number>;
880
- readonly maxWinX: number;
881
- readonly outputDir: string;
882
- readonly hooks: GameHooks<TGameModes, TSymbols, TUserState>;
883
- readonly userState?: TUserState;
748
+ protected currentWinPerSpinType: {
749
+ basegame: number;
750
+ freespins: number;
884
751
  };
885
- constructor(opts: CommonGameOptions<TGameModes, TSymbols, TUserState>);
886
752
  /**
887
- * Generates reelset CSV files for all game modes.
753
+ * Holds the current win amount for a single (free) spin.\
754
+ * After each spin, this amount is added to `currentWinPerSpinType` and then reset to zero.
888
755
  */
889
- generateReelsetFiles(): void;
756
+ protected currentSpinWin: number;
890
757
  /**
891
- * Retrieves a reel set by its ID within a specific game mode.
758
+ * Current win amount (as the bet multiplier) for the ongoing tumble sequence.
892
759
  */
893
- getReelsetById(gameMode: string, id: string): Reels;
760
+ protected currentTumbleWin: number;
761
+ constructor();
894
762
  /**
895
- * Retrieves the number of free spins awarded for a given spin type and scatter count.
763
+ * Updates the win for the current spin.
764
+ *
765
+ * Should be called after each tumble event, if applicable.\
766
+ * Or generally call this to add wins during a spin.
767
+ *
768
+ * After each (free) spin, this amount should be added to `currentWinPerSpinType` via `confirmSpinWin()`
896
769
  */
897
- getFreeSpinsForScatters(spinType: SpinType, scatterCount: number): number;
770
+ addSpinWin(amount: number): void;
898
771
  /**
899
- * Retrieves a result set by its criteria within a specific game mode.
772
+ * Confirms the wins of the current spin.
773
+ *
774
+ * Should be called after `addSpinWin()`, and after your tumble events are played out,\
775
+ * and after a (free) spin is played out to finalize the win.
900
776
  */
901
- getResultSetByCriteria(mode: string, criteria: string): ResultSet<any>;
777
+ confirmSpinWin(spinType: SpinType): void;
902
778
  /**
903
- * Returns all configured symbols as an array.
779
+ * Returns the accumulated win amount (as the bet multiplier) from all simulations.
780
+ */
781
+ getCumulativeWins(): number;
782
+ /**
783
+ * Returns the accumulated win amount (as the bet multiplier) per spin type from all simulations.
904
784
  */
905
- getSymbolArray(): TSymbols[keyof TSymbols][];
906
- static SPIN_TYPE: {
907
- readonly BASE_GAME: "basegame";
908
- readonly FREE_SPINS: "freespins";
785
+ getCumulativeWinsPerSpinType(): {
786
+ basegame: number;
787
+ freespins: number;
909
788
  };
910
- }
911
- type SpinType = (typeof GameConfig.SPIN_TYPE)[keyof typeof GameConfig.SPIN_TYPE];
912
- type AnyGameConfig = GameConfig<any, any, any>;
913
-
914
- declare class WinType {
915
- protected payout: number;
916
- protected winCombinations: WinCombination[];
917
- protected ctx: AnySimulationContext;
918
- protected readonly wildSymbol?: WildSymbol;
919
- constructor(opts?: WinTypeOpts);
920
789
  /**
921
- * Sets the simulation context for this WinType instance.
922
- *
923
- * This gives the WinType access to the current board.
790
+ * Returns the current win amount (as the bet multiplier) for the ongoing simulation.
924
791
  */
925
- context(ctx: SimulationContext<any, any, any>): WinType;
926
- protected ensureContext(): void;
792
+ getCurrentWin(): number;
927
793
  /**
928
- * Implementation of win evaluation logic. Sets `this.payout` and `this.winCombinations`.
794
+ * Returns the current spin win amount (as the bet multiplier) for the ongoing simulation.
929
795
  */
930
- evaluateWins(): this;
796
+ getCurrentSpinWin(): number;
931
797
  /**
932
- * Custom post-processing of wins, e.g. for handling multipliers.
798
+ * Returns the current tumble win amount (as the bet multiplier) for the ongoing simulation.
933
799
  */
934
- postProcess(func: PostProcessFn<typeof this.winCombinations>): this;
800
+ getCurrentTumbleWin(): number;
935
801
  /**
936
- * Returns the total payout and detailed win combinations.
802
+ * Returns the current win amount (as the bet multiplier) per spin type for the ongoing simulation.
937
803
  */
938
- getWins(): {
939
- payout: number;
940
- winCombinations: WinCombination[];
804
+ getCurrentWinPerSpinType(): {
805
+ basegame: number;
806
+ freespins: number;
941
807
  };
942
- }
943
- interface WinTypeOpts {
944
808
  /**
945
- * Configuration used to identify wild symbols on the board.\
946
- * You can either provide a specific `GameSymbol` instance or a set of properties to match against symbols on the board.
809
+ * Adds a win to `currentSpinWin` and `currentTumbleWin`.
947
810
  *
948
- * @example
949
- * If you have different wild symbols, each with a property `isWild: true`, you can define:
950
- * ```ts
951
- * wildSymbol: { isWild: true }
952
- * ```
811
+ * After each (free) spin, this amount should be added to `currentWinPerSpinType` via `confirmSpinWin()`
812
+ */
813
+ addTumbleWin(amount: number): void;
814
+ /**
815
+ * Intended for internal use only.
953
816
  *
954
- * @example
955
- * If you have a single wild symbol instance, you can define:
956
- * ```ts
957
- * wildSymbol: myWildSymbol
958
- * ```
817
+ * Resets the current win amounts to zero.
959
818
  */
960
- wildSymbol?: WildSymbol;
961
- }
962
- type WinCombination = {
963
- payout: number;
964
- kind: number;
965
- symbols: Array<{
966
- symbol: GameSymbol;
967
- isWild: boolean;
968
- substitutedFor?: GameSymbol;
969
- reelIndex: number;
970
- posIndex: number;
971
- }>;
972
- };
973
- type PostProcessFn<TWinCombs extends WinCombination[]> = (winType: WinType, ctx: AnySimulationContext) => {
974
- payout: number;
975
- winCombinations: TWinCombs;
976
- };
977
- type WildSymbol = GameSymbol | Record<string, any>;
978
-
979
- declare class LinesWinType extends WinType {
980
- protected lines: Record<number, number[]>;
981
- protected winCombinations: LineWinCombination[];
982
- context: (ctx: SimulationContext<any, any, any>) => LinesWinType;
983
- getWins: () => {
984
- payout: number;
985
- winCombinations: LineWinCombination[];
986
- };
987
- constructor(opts: LinesWinTypeOpts);
988
- private validateConfig;
989
- private isWild;
990
- evaluateWins(): this;
991
- }
992
- interface LinesWinTypeOpts extends WinTypeOpts {
819
+ resetCurrentWin(): void;
993
820
  /**
994
- * Defines the paylines for the slot game.
821
+ * Intended for internal use only.
995
822
  *
996
- * @example
997
- * ```ts
998
- * lines: {
999
- * 1: [0, 0, 0, 0, 0],
1000
- * 2: [1, 1, 1, 1, 1],
1001
- * 3: [2, 2, 2, 2, 2],
1002
- * }
1003
- * ```
823
+ * Adds current wins to cumulative wins and resets current wins to zero.
1004
824
  */
1005
- lines: Record<number, number[]>;
1006
- }
1007
- interface LineWinCombination extends WinCombination {
1008
- lineNumber: number;
1009
- symbol: GameSymbol;
1010
- winType: "pure-wild" | "substituted";
1011
- substitutedBaseSymbol: GameSymbol | null;
1012
- stats: {
1013
- wildCount: number;
1014
- nonWildCount: number;
1015
- leadingWilds: number;
825
+ confirmWins(ctx: GameContext): void;
826
+ /**
827
+ * Intended for internal use only.
828
+ *
829
+ * Transfers the win data from the given wallet to the calling book.
830
+ */
831
+ writePayoutToBook(ctx: GameContext): void;
832
+ /**
833
+ * Intended for internal use only.
834
+ */
835
+ serialize(): {
836
+ cumulativeWins: number;
837
+ cumulativeWinsPerSpinType: {
838
+ basegame: number;
839
+ freespins: number;
840
+ };
841
+ currentWin: number;
842
+ currentWinPerSpinType: {
843
+ basegame: number;
844
+ freespins: number;
845
+ };
846
+ currentSpinWin: number;
847
+ currentTumbleWin: number;
1016
848
  };
849
+ /**
850
+ * Intended for internal use only.
851
+ */
852
+ merge(wallet: Wallet): void;
853
+ /**
854
+ * Intended for internal use only.
855
+ */
856
+ mergeSerialized(data: ReturnType<Wallet["serialize"]>): void;
1017
857
  }
1018
858
 
1019
- declare class ClusterWinType extends WinType {
1020
- }
1021
-
1022
- declare class ManywaysWinType extends WinType {
1023
- }
1024
-
1025
- declare class OptimizationConditions {
1026
- protected rtp?: number | "x";
1027
- protected avgWin?: number;
1028
- protected hitRate?: number | "x";
1029
- protected searchRange: number[];
1030
- protected forceSearch: Record<string, string>;
1031
- priority: number;
1032
- constructor(opts: OptimizationConditionsOpts);
1033
- getRtp(): number | "x" | undefined;
1034
- getAvgWin(): number | undefined;
1035
- getHitRate(): number | "x" | undefined;
1036
- getSearchRange(): number[];
1037
- getForceSearch(): Record<string, string>;
1038
- }
1039
- interface OptimizationConditionsOpts {
859
+ declare class WalletService<TGameModes extends AnyGameModes = AnyGameModes, TSymbols extends AnySymbols = AnySymbols, TUserState extends AnyUserData = AnyUserData> extends AbstractService {
860
+ private wallet;
861
+ constructor(ctx: () => GameContext<TGameModes, TSymbols, TUserState>);
862
+ private ensureWallet;
1040
863
  /**
1041
- * The desired RTP (0-1)
864
+ * Intended for internal use only.
1042
865
  */
1043
- rtp?: number | "x";
866
+ _getWallet(): Wallet;
1044
867
  /**
1045
- * The desired average win (per spin).
868
+ * Intended for internal use only.
1046
869
  */
1047
- avgWin?: number;
870
+ _setWallet(wallet: Wallet): void;
1048
871
  /**
1049
- * The desired hit rate (e.g. `200` to hit 1 in 200 spins).
872
+ * Adds the given amount to the wallet state.
873
+ *
874
+ * After calculating the win for a board, call this method to update the wallet state.\
875
+ * If your game has tumbling mechanics, you should call this method again after every new tumble and win calculation.
1050
876
  */
1051
- hitRate?: number | "x";
877
+ addSpinWin(amount: number): void;
1052
878
  /**
1053
- * A way of filtering results by
879
+ * Helps to add tumble wins to the wallet state.
1054
880
  *
1055
- * - A number (payout multiplier), e.g. `5000`
1056
- * - Force record value, e.g. `{ "symbolId": "scatter" }`
1057
- * - A range of numbers, e.g. `[0, 100]` (payout multiplier range)
881
+ * This also calls `addSpinWin()` internally, to add the tumble win to the overall spin win.
1058
882
  */
1059
- searchConditions?: number | Record<string, string> | [number, number];
883
+ addTumbleWin(amount: number): void;
1060
884
  /**
1061
- * **Priority matters!**\
1062
- * Higher priority conditions will be evaluated first.\
1063
- * After a book matching this condition is found, the book will be removed from the pool\
1064
- * and can't be used to satisfy other conditions with lower priority.
885
+ * Confirms the wins of the current spin.
1065
886
  *
1066
- * TODO add better explanation
887
+ * Should be called after `addSpinWin()`, and after your tumble events are played out,\
888
+ * and after a (free) spin is played out to finalize the win.
1067
889
  */
1068
- priority: number;
890
+ confirmSpinWin(): void;
891
+ /**
892
+ * Gets the total win amount of the current simulation.
893
+ */
894
+ getCurrentWin(): number;
895
+ /**
896
+ * Gets the current spin win amount of the ongoing spin.
897
+ */
898
+ getCurrentSpinWin(): number;
899
+ /**
900
+ * Gets the current tumble win amount of the ongoing spin.
901
+ */
902
+ getCurrentTumbleWin(): number;
1069
903
  }
1070
904
 
1071
- declare class OptimizationScaling {
1072
- protected config: OptimizationScalingOpts;
1073
- constructor(opts: OptimizationScalingOpts);
1074
- getConfig(): OptimizationScalingOpts;
905
+ type GameContext<TGameModes extends AnyGameModes = AnyGameModes, TSymbols extends AnySymbols = AnySymbols, TUserState extends AnyUserData = AnyUserData> = {
906
+ /**
907
+ * The static configuration of the game.
908
+ */
909
+ config: GameConfig<TGameModes, TSymbols, TUserState>;
910
+ /**
911
+ * Game state holding information about the current simulation.
912
+ */
913
+ state: GameState<TUserState>;
914
+ /**
915
+ * Services providing game functionality.
916
+ */
917
+ services: GameContextServices;
918
+ };
919
+ interface GameContextServices {
920
+ /**
921
+ * Service providing common utility functions.
922
+ */
923
+ game: GameService;
924
+ /**
925
+ * Service for interacting with the book data or recorder.
926
+ */
927
+ data: DataService;
928
+ /**
929
+ * Service managing the game board and reels.
930
+ */
931
+ board: BoardService;
932
+ /**
933
+ * Service providing win related functionality.
934
+ */
935
+ wallet: WalletService;
936
+ /**
937
+ * Service for seeded random number generation.
938
+ */
939
+ rng: RngService;
1075
940
  }
1076
- type OptimizationScalingOpts = Array<{
1077
- criteria: string;
1078
- scaleFactor: number;
1079
- winRange: [number, number];
1080
- probability: number;
1081
- }>;
1082
941
 
1083
942
  declare class OptimizationParameters {
1084
943
  protected parameters: OptimizationParametersOpts;
@@ -1102,6 +961,64 @@ interface OptimizationParametersOpts {
1102
961
  readonly scoreType: "rtp";
1103
962
  }
1104
963
 
964
+ declare class OptimizationConditions {
965
+ protected rtp?: number | "x";
966
+ protected avgWin?: number;
967
+ protected hitRate?: number | "x";
968
+ protected searchRange: number[];
969
+ protected forceSearch: Record<string, string>;
970
+ priority: number;
971
+ constructor(opts: OptimizationConditionsOpts);
972
+ getRtp(): number | "x" | undefined;
973
+ getAvgWin(): number | undefined;
974
+ getHitRate(): number | "x" | undefined;
975
+ getSearchRange(): number[];
976
+ getForceSearch(): Record<string, string>;
977
+ }
978
+ interface OptimizationConditionsOpts {
979
+ /**
980
+ * The desired RTP (0-1)
981
+ */
982
+ rtp?: number | "x";
983
+ /**
984
+ * The desired average win (per spin).
985
+ */
986
+ avgWin?: number;
987
+ /**
988
+ * The desired hit rate (e.g. `200` to hit 1 in 200 spins).
989
+ */
990
+ hitRate?: number | "x";
991
+ /**
992
+ * A way of filtering results by
993
+ *
994
+ * - A number (payout multiplier), e.g. `5000`
995
+ * - Force record value, e.g. `{ "symbolId": "scatter" }`
996
+ * - A range of numbers, e.g. `[0, 100]` (payout multiplier range)
997
+ */
998
+ searchConditions?: number | Record<string, string> | [number, number];
999
+ /**
1000
+ * **Priority matters!**\
1001
+ * Higher priority conditions will be evaluated first.\
1002
+ * After a book matching this condition is found, the book will be removed from the pool\
1003
+ * and can't be used to satisfy other conditions with lower priority.
1004
+ *
1005
+ * TODO add better explanation
1006
+ */
1007
+ priority: number;
1008
+ }
1009
+
1010
+ declare class OptimizationScaling {
1011
+ protected config: OptimizationScalingOpts;
1012
+ constructor(opts: OptimizationScalingOpts);
1013
+ getConfig(): OptimizationScalingOpts;
1014
+ }
1015
+ type OptimizationScalingOpts = Array<{
1016
+ criteria: string;
1017
+ scaleFactor: number;
1018
+ winRange: [number, number];
1019
+ probability: number;
1020
+ }>;
1021
+
1105
1022
  interface OptimizationOpts {
1106
1023
  gameModes: string[];
1107
1024
  }
@@ -1109,7 +1026,7 @@ interface OptimizerOpts {
1109
1026
  game: SlotGame<any, any, any>;
1110
1027
  gameModes: OptimzierGameModeConfig;
1111
1028
  }
1112
- type OptimzierGameModeConfig = Record<GameModeName, {
1029
+ type OptimzierGameModeConfig = Record<string, {
1113
1030
  conditions: Record<string, OptimizationConditions>;
1114
1031
  scaling: OptimizationScaling;
1115
1032
  parameters: OptimizationParameters;
@@ -1124,16 +1041,16 @@ interface AnalysisOpts {
1124
1041
  * Main entry point for the slot game.
1125
1042
  */
1126
1043
  declare class SlotGame<TGameModes extends AnyGameModes = AnyGameModes, TSymbols extends AnySymbols = AnySymbols, TUserState extends AnyUserData = AnyUserData> {
1127
- private readonly gameConfigOpts;
1044
+ private readonly configOpts;
1128
1045
  private simulation?;
1129
1046
  private optimizer?;
1130
1047
  private analyzer?;
1131
- constructor(config: CommonGameOptions<TGameModes, TSymbols, TUserState>);
1048
+ constructor(config: GameConfigOptions<TGameModes, TSymbols, TUserState>);
1132
1049
  /**
1133
1050
  * Sets up the simulation configuration.\
1134
1051
  * Must be called before `runTasks()`.
1135
1052
  */
1136
- configureSimulation(opts: SimulationConfigOpts): void;
1053
+ configureSimulation(opts: SimulationOptions): void;
1137
1054
  /**
1138
1055
  * Sets up the optimization configuration.\
1139
1056
  * Must be called before `runTasks()`.
@@ -1151,34 +1068,39 @@ declare class SlotGame<TGameModes extends AnyGameModes = AnyGameModes, TSymbols
1151
1068
  * Runs the analysis based on the configured settings.
1152
1069
  */
1153
1070
  private runAnalysis;
1071
+ /**
1072
+ * Runs the configured tasks: simulation, optimization, and/or analysis.
1073
+ */
1154
1074
  runTasks(opts?: {
1155
1075
  doSimulation?: boolean;
1156
1076
  doOptimization?: boolean;
1157
1077
  doAnalysis?: boolean;
1158
- simulationOpts?: SimulationOpts;
1078
+ simulationOpts?: SimulationConfigOptions;
1159
1079
  optimizationOpts?: OptimizationOpts;
1160
1080
  analysisOpts?: AnalysisOpts;
1161
1081
  }): Promise<void>;
1162
1082
  /**
1163
1083
  * Gets the game configuration.
1164
1084
  */
1165
- getConfig(): GameConfig<TGameModes, TSymbols, TUserState>;
1085
+ getConfig(): {
1086
+ symbols: Map<keyof TSymbols & string, TSymbols[keyof TSymbols]>;
1087
+ anticipationTriggers: {
1088
+ basegame: number;
1089
+ freespins: number;
1090
+ };
1091
+ outputDir: string;
1092
+ id: string;
1093
+ name: string;
1094
+ gameModes: TGameModes;
1095
+ scatterToFreespins: Record<string, Record<number, number>>;
1096
+ padSymbols: number;
1097
+ maxWinX: number;
1098
+ userState: TUserState;
1099
+ hooks: GameHooks<TGameModes, TSymbols, TUserState>;
1100
+ };
1166
1101
  }
1167
1102
 
1168
- /**
1169
- * @internal
1170
- */
1171
- interface CommonGameOptions<TGameModes extends AnyGameModes = AnyGameModes, TSymbols extends AnySymbols = AnySymbols, TUserState extends AnyUserData = AnyUserData> {
1172
- id: string;
1173
- name: string;
1174
- gameModes: Record<GameModeName, GameMode>;
1175
- symbols: TSymbols;
1176
- scatterToFreespins: Record<string, Record<number, number>>;
1177
- padSymbols?: number;
1178
- maxWinX: number;
1179
- hooks: GameHooks<TGameModes, TSymbols, TUserState>;
1180
- userState?: TUserState;
1181
- }
1103
+ type InferGameType<TGameModes extends AnyGameModes, TSymbols extends AnySymbols, TUserState extends AnyUserData> = SlotGame<TGameModes, TSymbols, TUserState>;
1182
1104
  /**
1183
1105
  * @internal
1184
1106
  */
@@ -1213,79 +1135,367 @@ interface GameHooks<TGameModes extends AnyGameModes = AnyGameModes, TSymbols ext
1213
1135
  * The game flow is not built into the core, because it can vary greatly between different games.\
1214
1136
  * This hook provides the flexibility to implement any game flow you need.
1215
1137
  */
1216
- onHandleGameFlow: (ctx: SimulationContext<TGameModes, TSymbols, TUserState>) => void;
1138
+ onHandleGameFlow: (ctx: GameContext<TGameModes, TSymbols, TUserState>) => void;
1217
1139
  /**
1218
1140
  * This hook is called whenever a simulation is accepted, i.e. when the criteria of the current ResultSet is met.
1219
1141
  */
1220
- onSimulationAccepted?: (ctx: SimulationContext<TGameModes, TSymbols, TUserState>) => void;
1142
+ onSimulationAccepted?: (ctx: GameContext<TGameModes, TSymbols, TUserState>) => void;
1221
1143
  }
1222
- type InferUserState<T> = T extends SlotGame<infer U> ? U : never;
1223
- type HookContext<T> = T extends SlotGame<infer G, infer S, infer U> ? SimulationContext<G, S, U> : never;
1144
+ type SpinType = (typeof SPIN_TYPE)[keyof typeof SPIN_TYPE];
1145
+ type Reels = GameSymbol[][];
1224
1146
 
1225
- type InferGameType<TGameModes extends AnyGameModes, TSymbols extends AnySymbols, TUserState extends AnyUserData> = SlotGame<TGameModes, TSymbols, TUserState>;
1226
- interface CreateSlotGameOpts<TGameModes extends AnyGameModes = AnyGameModes, TSymbols extends AnySymbols = AnySymbols, TUserState extends AnyUserData = AnyUserData> {
1147
+ declare function createSlotGame<TGame>(opts: TGame extends InferGameType<infer G, infer S, infer U> ? GameConfigOptions<G, S, U> : never): TGame;
1148
+ declare const defineUserState: <TUserState extends AnyUserData>(data: TUserState) => TUserState;
1149
+ declare const defineSymbols: <TSymbols extends AnySymbols>(symbols: TSymbols) => TSymbols;
1150
+ declare const defineGameModes: <TGameModes extends AnyGameModes>(gameModes: TGameModes) => TGameModes;
1151
+
1152
+ declare class WinType {
1153
+ protected payout: number;
1154
+ protected winCombinations: WinCombination[];
1155
+ protected ctx: GameContext;
1156
+ protected readonly wildSymbol?: WildSymbol;
1157
+ constructor(opts: WinTypeOpts);
1227
1158
  /**
1228
- * The unique identifier of the game, used for configuration and identification.
1159
+ * Implementation of win evaluation logic. Sets `this.payout` and `this.winCombinations`.
1229
1160
  */
1230
- id: CommonGameOptions["id"];
1161
+ evaluateWins(board: Reels): this;
1231
1162
  /**
1232
- * The name of the game, used for display purposes.
1163
+ * Custom post-processing of wins, e.g. for handling multipliers.
1233
1164
  */
1234
- name: CommonGameOptions["name"];
1165
+ postProcess(func: PostProcessFn<typeof this.winCombinations>): this;
1235
1166
  /**
1236
- * A GameMode is the core structure of a slot, defining the board,\
1237
- * bet cost, win type, and other properties.
1167
+ * Returns the total payout and detailed win combinations.
1238
1168
  */
1239
- gameModes: TGameModes;
1169
+ getWins(): {
1170
+ payout: number;
1171
+ winCombinations: WinCombination[];
1172
+ };
1173
+ protected isWild(symbol: GameSymbol): boolean;
1174
+ }
1175
+ interface WinTypeOpts {
1240
1176
  /**
1241
- * A list of all symbols that will appear on the reels.
1177
+ * A reference to the game context.
1242
1178
  */
1243
- symbols: TSymbols;
1179
+ ctx: GameContext<any, any, any>;
1244
1180
  /**
1245
- * A mapping from spin type to scatter counts to the number of free spins awarded.
1181
+ * Configuration used to identify wild symbols on the board.\
1182
+ * You can either provide a specific `GameSymbol` instance or a set of properties to match against symbols on the board.
1246
1183
  *
1247
1184
  * @example
1185
+ * If you have different wild symbols, each with a property `isWild: true`, you can define:
1248
1186
  * ```ts
1249
- * scatterToFreespins: {
1250
- * [GameConfig.SPIN_TYPE.BASE_GAME]: {
1251
- * 3: 10,
1252
- * 4: 12,
1253
- * 5: 15,
1254
- * },
1255
- * [GameConfig.SPIN_TYPE.FREE_SPINS]: {
1256
- * 3: 6,
1257
- * 4: 8,
1258
- * 5: 10,
1259
- * },
1260
- * },
1187
+ * wildSymbol: { isWild: true }
1188
+ * ```
1189
+ *
1190
+ * @example
1191
+ * If you have a single wild symbol instance, you can define:
1192
+ * ```ts
1193
+ * wildSymbol: myWildSymbol
1261
1194
  * ```
1262
1195
  */
1263
- scatterToFreespins: CommonGameOptions["scatterToFreespins"];
1196
+ wildSymbol?: WildSymbol;
1197
+ }
1198
+ type WinCombination = {
1199
+ payout: number;
1200
+ kind: number;
1201
+ symbols: Array<{
1202
+ symbol: GameSymbol;
1203
+ isWild: boolean;
1204
+ substitutedFor?: GameSymbol;
1205
+ reelIndex: number;
1206
+ posIndex: number;
1207
+ }>;
1208
+ };
1209
+ type PostProcessFn<TWinCombs extends WinCombination[]> = (winType: WinType, ctx: GameContext) => {
1210
+ payout: number;
1211
+ winCombinations: TWinCombs;
1212
+ };
1213
+ type WildSymbol = GameSymbol | Record<string, any>;
1214
+
1215
+ declare class LinesWinType extends WinType {
1216
+ protected lines: Record<number, number[]>;
1217
+ protected winCombinations: LineWinCombination[];
1218
+ getWins: () => {
1219
+ payout: number;
1220
+ winCombinations: LineWinCombination[];
1221
+ };
1222
+ constructor(opts: LinesWinTypeOpts);
1223
+ private validateConfig;
1264
1224
  /**
1265
- * If set, this will pad the board with symbols on the top and bottom of the reels.\
1266
- * Useful for teasing symbols right above or below the active board.
1225
+ * Calculates wins based on the defined paylines and provided board state.\
1226
+ * Retrieve the results using `getWins()` after.
1227
+ */
1228
+ evaluateWins(board: Reels): this;
1229
+ }
1230
+ interface LinesWinTypeOpts extends WinTypeOpts {
1231
+ /**
1232
+ * Defines the paylines for the slot game.
1267
1233
  *
1268
- * Default: 1
1234
+ * @example
1235
+ * ```ts
1236
+ * lines: {
1237
+ * 1: [0, 0, 0, 0, 0],
1238
+ * 2: [1, 1, 1, 1, 1],
1239
+ * 3: [2, 2, 2, 2, 2],
1240
+ * }
1241
+ * ```
1242
+ */
1243
+ lines: Record<number, number[]>;
1244
+ }
1245
+ interface LineWinCombination extends WinCombination {
1246
+ lineNumber: number;
1247
+ symbol: GameSymbol;
1248
+ winType: "pure-wild" | "substituted";
1249
+ substitutedBaseSymbol: GameSymbol | null;
1250
+ stats: {
1251
+ wildCount: number;
1252
+ nonWildCount: number;
1253
+ leadingWilds: number;
1254
+ };
1255
+ }
1256
+
1257
+ declare class ClusterWinType extends WinType {
1258
+ }
1259
+
1260
+ declare class ManywaysWinType extends WinType {
1261
+ }
1262
+
1263
+ /**
1264
+ * This class is responsible for generating reel sets for slot games based on specified configurations.
1265
+ *
1266
+ * **While it offers a high degree of customization, some configurations may lead to unsolvable scenarios.**
1267
+ *
1268
+ * If the reel generator is unable to fulfill niche constraints,\
1269
+ * you might need to adjust your configuration, or edit the generated reels manually.\
1270
+ * Setting a different seed may also help.
1271
+ */
1272
+ declare class GeneratedReelSet extends ReelSet {
1273
+ protected readonly symbolWeights: Map<string, number>;
1274
+ protected readonly rowsAmount: number;
1275
+ protected limitSymbolsToReels?: Record<string, number[]>;
1276
+ protected readonly spaceBetweenSameSymbols?: number | Record<string, number>;
1277
+ protected readonly spaceBetweenSymbols?: Record<string, Record<string, number>>;
1278
+ protected readonly preferStackedSymbols?: number;
1279
+ protected readonly symbolStacks?: Record<string, {
1280
+ chance: number | Record<string, number>;
1281
+ min?: number | Record<string, number>;
1282
+ max?: number | Record<string, number>;
1283
+ }>;
1284
+ protected readonly symbolQuotas?: Record<string, number | Record<string, number>>;
1285
+ private overrideExisting;
1286
+ constructor(opts: GeneratedReelSetOptions);
1287
+ private validateConfig;
1288
+ private isSymbolAllowedOnReel;
1289
+ private resolveStacking;
1290
+ private tryPlaceStack;
1291
+ /**
1292
+ * Checks if a symbol can be placed at the target index without violating spacing rules.
1269
1293
  */
1270
- padSymbols?: CommonGameOptions["padSymbols"];
1294
+ private violatesSpacing;
1295
+ generateReels({ gameConfig: config }: Simulation): void;
1296
+ }
1297
+ interface GeneratedReelSetOptions extends ReelSetOptions {
1271
1298
  /**
1272
- * The maximum win multiplier of the game, e.g. 5000 for a 5000x max win.
1299
+ * The weights of the symbols in the reelset.\
1300
+ * This is a mapping of symbol IDs to their respective weights.
1273
1301
  */
1274
- maxWinX: CommonGameOptions["maxWinX"];
1302
+ symbolWeights: Record<string, number>;
1275
1303
  /**
1276
- * Custom additional state that can be used in game flow logic.
1304
+ * The number of rows in the reelset.\
1305
+ * Default is 250, but can be adjusted as needed.
1277
1306
  */
1278
- userState?: TUserState;
1307
+ rowsAmount?: number;
1279
1308
  /**
1280
- * Hooks are used to inject custom logic at specific points in the game flow.\
1281
- * Some required hooks must be implemented for certain features to work.
1309
+ * Prevent the same symbol from appearing directly above or below itself.\
1310
+ * This can be a single number for all symbols, or a mapping of symbol IDs to
1311
+ * their respective spacing values.
1312
+ *
1313
+ * Must be 1 or higher, if set.
1314
+ *
1315
+ * **This is overridden by `symbolStacks`**
1316
+ */
1317
+ spaceBetweenSameSymbols?: number | Record<string, number>;
1318
+ /**
1319
+ * Prevents specific symbols from appearing within a certain distance of each other.
1320
+ *
1321
+ * Useful for preventing scatter and super scatter symbols from appearing too close to each other.
1322
+ *
1323
+ * **This is overridden by `symbolStacks`**
1324
+ */
1325
+ spaceBetweenSymbols?: Record<string, Record<string, number>>;
1326
+ /**
1327
+ * A percentage value 0-100 that indicates the likelihood of a symbol being stacked.\
1328
+ * A value of 0 means no stacked symbols, while 100 means all symbols are stacked.
1329
+ *
1330
+ * This is only a preference. Symbols may still not be stacked if\
1331
+ * other restrictions (like `spaceBetweenSameSymbols`) prevent it.
1332
+ *
1333
+ * **This is overridden by `symbolStacks`**
1334
+ */
1335
+ preferStackedSymbols?: number;
1336
+ /**
1337
+ * A mapping of symbols to their respective advanced stacking configuration.
1338
+ *
1339
+ * @example
1340
+ * ```ts
1341
+ * symbolStacks: {
1342
+ * "W": {
1343
+ * chance: { "1": 20, "2": 20, "3": 20, "4": 20 }, // 20% chance to be stacked on reels 2-5
1344
+ * min: 2, // At least 2 wilds in a stack
1345
+ * max: 4, // At most 4 wilds in a stack
1346
+ * }
1347
+ * }
1348
+ * ```
1349
+ */
1350
+ symbolStacks?: Record<string, {
1351
+ chance: number | Record<string, number>;
1352
+ min?: number | Record<string, number>;
1353
+ max?: number | Record<string, number>;
1354
+ }>;
1355
+ /**
1356
+ * Configures symbols to be limited to specific reels.\
1357
+ * For example, you could configure Scatters to appear only on reels 1, 3 and 5.
1358
+ *
1359
+ * @example
1360
+ * ```ts
1361
+ * limitSymbolsToReels: {
1362
+ * "S": [0, 2, 4], // Remember that reels are 0-indexed.
1363
+ * }
1364
+ * ```
1365
+ */
1366
+ limitSymbolsToReels?: Record<string, number[]>;
1367
+ /**
1368
+ * Defines optional quotas for symbols on the reels.\
1369
+ * The quota (1-100%) defines how often a symbol should appear in the reelset, or in a specific reel.
1370
+ *
1371
+ * This is particularly useful for controlling the frequency of special symbols like scatters or wilds.
1372
+ *
1373
+ * Reels not provided for a symbol will use the weights from `symbolWeights`.
1374
+ *
1375
+ * _Any_ small quota will ensure that the symbol appears at least once on the reel.
1376
+ *
1377
+ * @example
1378
+ * ```ts
1379
+ * symbolQuotas: {
1380
+ * "S": 3, // 3% of symbols on each reel will be scatters
1381
+ * "W": { "1": 10, "2": 5, "3": 3, "4": 1 }, // Wilds will appear with different quotas on selected reels
1382
+ * }
1383
+ * ```
1384
+ */
1385
+ symbolQuotas?: Record<string, number | Record<string, number>>;
1386
+ /**
1387
+ * If true, existing reels CSV files will be overwritten.
1282
1388
  */
1283
- hooks: CommonGameOptions<TGameModes, TSymbols, TUserState>["hooks"];
1389
+ overrideExisting?: boolean;
1390
+ /**
1391
+ * Optional seed for the RNG to ensure reproducible results.
1392
+ *
1393
+ * Default seed is `0`.
1394
+ *
1395
+ * Note: Seeds 0 and 1 produce the same results.
1396
+ */
1397
+ seed?: number;
1398
+ }
1399
+
1400
+ /**
1401
+ * This class is responsible for providing reel sets for slot games based on a static configuration or file.
1402
+ */
1403
+ declare class StaticReelSet extends ReelSet {
1404
+ reels: Reels;
1405
+ csvPath: string;
1406
+ private _strReels;
1407
+ constructor(opts: StaticReelSetOptions);
1408
+ private validateConfig;
1409
+ generateReels({ gameConfig: config }: Simulation): void;
1410
+ }
1411
+ interface StaticReelSetOptions extends ReelSetOptions {
1412
+ reels?: string[][];
1413
+ csvPath?: string;
1414
+ }
1415
+
1416
+ declare class StandaloneBoard {
1417
+ private board;
1418
+ private ctx;
1419
+ private reelsAmount;
1420
+ private symbolsPerReel;
1421
+ private padSymbols;
1422
+ constructor(opts: StandaloneBoardOptions);
1423
+ /**
1424
+ * Resets the board to an empty state.\
1425
+ * This is called before drawing a new board.
1426
+ */
1427
+ resetBoard(): void;
1428
+ /**
1429
+ * Gets the current reels and symbols on the board.
1430
+ */
1431
+ getBoardReels(): Reels;
1432
+ getPaddingTop(): Reels;
1433
+ getPaddingBottom(): Reels;
1434
+ private resetReels;
1435
+ /**
1436
+ * Sets the anticipation value for a specific reel.
1437
+ */
1438
+ setAnticipationForReel(reelIndex: number, value: boolean): void;
1439
+ /**
1440
+ * Counts how many symbols matching the criteria are on a specific reel.
1441
+ */
1442
+ countSymbolsOnReel(symbolOrProperties: GameSymbol | Record<string, any>, reelIndex: number): number;
1443
+ /**
1444
+ * Counts how many symbols matching the criteria are on the board.
1445
+ *
1446
+ * Passing a GameSymbol will compare by ID, passing a properties object will compare by properties.
1447
+ *
1448
+ * Returns a tuple where the first element is the total count, and the second element is a record of counts per reel index.
1449
+ */
1450
+ countSymbolsOnBoard(symbolOrProperties: GameSymbol | Record<string, any>): [number, Record<number, number>];
1451
+ /**
1452
+ * Checks if a symbol appears more than once on any reel in the current reel set.
1453
+ *
1454
+ * Useful to check for "forbidden" generations, e.g. 2 scatters on one reel.
1455
+ */
1456
+ isSymbolOnAnyReelMultipleTimes(symbol: GameSymbol): boolean;
1457
+ /**
1458
+ * Gets all reel stops (positions) where the specified symbol appears in the current reel set.\
1459
+ * Returns an array of arrays, where each inner array contains the positions for the corresponding reel.
1460
+ */
1461
+ getReelStopsForSymbol(reels: Reels, symbol: GameSymbol): number[][];
1462
+ /**
1463
+ * Combines multiple arrays of reel stops into a single array of reel stops.\
1464
+ */
1465
+ combineReelStops(...reelStops: number[][][]): number[][];
1466
+ /**
1467
+ * From a list of reel stops on reels, selects a random stop for a speficied number of random symbols.
1468
+ *
1469
+ * Mostly useful for placing scatter symbols on the board.
1470
+ */
1471
+ getRandomReelStops(reels: Reels, reelStops: number[][], amount: number): Record<number, number>;
1472
+ /**
1473
+ * Selects a random reel set based on the configured weights of the current result set.\
1474
+ * Returns the reels as arrays of GameSymbols.
1475
+ */
1476
+ getRandomReelset(): Reels;
1477
+ /**
1478
+ * Draws a board using specified reel stops.
1479
+ */
1480
+ drawBoardWithForcedStops(reels: Reels, forcedStops: Record<string, number>): void;
1481
+ /**
1482
+ * Draws a board using random reel stops.
1483
+ */
1484
+ drawBoardWithRandomStops(reels: Reels): void;
1485
+ private drawBoardMixed;
1486
+ /**
1487
+ * Tumbles the board. All given symbols will be deleted and new symbols will fall from the top.
1488
+ */
1489
+ tumbleBoard(symbolsToDelete: Array<{
1490
+ reelIdx: number;
1491
+ rowIdx: number;
1492
+ }>): void;
1493
+ }
1494
+ interface StandaloneBoardOptions {
1495
+ ctx: GameContext;
1496
+ reelsAmount: number;
1497
+ symbolsPerReel: number[];
1498
+ padSymbols: number;
1284
1499
  }
1285
- declare function createSlotGame<TGame>(opts: TGame extends InferGameType<infer G, infer S, infer U> ? CreateSlotGameOpts<G, S, U> : never): TGame;
1286
- declare const defineUserState: <TUserState extends AnyUserData>(data: TUserState) => TUserState;
1287
- declare const defineSymbols: <TSymbols extends AnySymbols>(symbols: TSymbols) => TSymbols;
1288
- declare const defineGameModes: <TGameModes extends AnyGameModes>(gameModes: TGameModes) => TGameModes;
1289
- declare const defineReelSets: <TSymbols extends AnySymbols>(reelSets: ReelGenerator[]) => ReelGenerator[];
1290
1500
 
1291
- export { type AnyGameModes, type AnySymbols, type AnyUserData, ClusterWinType, type CommonGameOptions, type CreateSlotGameOpts, type EvaluationContext, GameConfig, type GameHooks, GameMode, GameSymbol, type HookContext, type InferGameType, type InferUserState, LinesWinType, ManywaysWinType, OptimizationConditions, OptimizationParameters, OptimizationScaling, ReelGenerator, type Reels, ResultSet, StandaloneBoard, WinType, createSlotGame, defineGameModes, defineReelSets, defineSymbols, defineUserState, weightedRandom };
1501
+ export { type AnyGameModes, type AnySymbols, type AnyUserData, ClusterWinType, type GameContext, type GameHooks, GameMode, GameSymbol, GeneratedReelSet, type InferGameType, LinesWinType, ManywaysWinType, OptimizationConditions, OptimizationParameters, OptimizationScaling, type Reels, ResultSet, SPIN_TYPE, type SpinType, StandaloneBoard, StaticReelSet, createSlotGame, defineGameModes, defineSymbols, defineUserState };