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