@slot-engine/core 0.0.1 → 0.0.3

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.
Files changed (43) hide show
  1. package/README.md +1 -1
  2. package/dist/index.d.mts +7 -14
  3. package/dist/index.d.ts +7 -14
  4. package/dist/index.js +68 -90
  5. package/dist/index.js.map +1 -1
  6. package/dist/index.mjs +68 -90
  7. package/dist/index.mjs.map +1 -1
  8. package/package.json +5 -1
  9. package/.turbo/turbo-build.log +0 -33
  10. package/.turbo/turbo-typecheck.log +0 -4
  11. package/CHANGELOG.md +0 -7
  12. package/dist/lib/zstd.exe +0 -0
  13. package/index.ts +0 -205
  14. package/lib/zstd.exe +0 -0
  15. package/optimizer-rust/Cargo.toml +0 -19
  16. package/optimizer-rust/src/exes.rs +0 -154
  17. package/optimizer-rust/src/main.rs +0 -1659
  18. package/src/Board.ts +0 -527
  19. package/src/Book.ts +0 -83
  20. package/src/GameConfig.ts +0 -148
  21. package/src/GameMode.ts +0 -86
  22. package/src/GameState.ts +0 -272
  23. package/src/GameSymbol.ts +0 -61
  24. package/src/ReelGenerator.ts +0 -589
  25. package/src/ResultSet.ts +0 -207
  26. package/src/Simulation.ts +0 -625
  27. package/src/SlotGame.ts +0 -117
  28. package/src/Wallet.ts +0 -203
  29. package/src/WinType.ts +0 -102
  30. package/src/analysis/index.ts +0 -198
  31. package/src/analysis/utils.ts +0 -128
  32. package/src/optimizer/OptimizationConditions.ts +0 -99
  33. package/src/optimizer/OptimizationParameters.ts +0 -46
  34. package/src/optimizer/OptimizationScaling.ts +0 -18
  35. package/src/optimizer/index.ts +0 -142
  36. package/src/utils/math-config.ts +0 -109
  37. package/src/utils/setup-file.ts +0 -36
  38. package/src/utils/zstd.ts +0 -28
  39. package/src/winTypes/ClusterWinType.ts +0 -3
  40. package/src/winTypes/LinesWinType.ts +0 -208
  41. package/src/winTypes/ManywaysWinType.ts +0 -3
  42. package/tsconfig.json +0 -19
  43. package/utils.ts +0 -270
@@ -1,589 +0,0 @@
1
- import fs from "fs"
2
- import path from "path"
3
- import { AnyGameConfig, GameConfig } from "./GameConfig"
4
- import { GameSymbol } from "./GameSymbol"
5
- import { createDirIfNotExists, RandomNumberGenerator, weightedRandom } from "../utils"
6
- import { isMainThread } from "worker_threads"
7
-
8
- /**
9
- * This class is responsible for generating reel sets for slot games based on specified configurations.
10
- *
11
- * **While it offers a high degree of customization, some configurations may lead to unsolvable scenarios.**
12
- *
13
- * If the reel generator is unable to fulfill niche constraints,\
14
- * you might need to adjust your configuration, or edit the generated reels manually.\
15
- * Setting a different seed may also help.
16
- */
17
- export class ReelGenerator {
18
- id: string
19
- associatedGameModeName: string = ""
20
- protected readonly symbolWeights: Map<string, number> = new Map()
21
- protected readonly rowsAmount: number
22
- reels: Reels = []
23
- protected limitSymbolsToReels?: Record<string, number[]>
24
- protected readonly spaceBetweenSameSymbols?: number | Record<string, number>
25
- protected readonly spaceBetweenSymbols?: Record<string, Record<string, number>>
26
- protected readonly preferStackedSymbols?: number
27
- protected readonly symbolStacks?: Record<
28
- string,
29
- {
30
- chance: number | Record<string, number>
31
- min?: number | Record<string, number>
32
- max?: number | Record<string, number>
33
- }
34
- >
35
- protected readonly symbolQuotas?: Record<string, number | Record<string, number>>
36
- outputDir: string = ""
37
- csvPath: string = ""
38
- overrideExisting: boolean
39
- rng: RandomNumberGenerator
40
-
41
- constructor(opts: ReelGeneratorOpts) {
42
- this.id = opts.id
43
- this.symbolWeights = new Map(Object.entries(opts.symbolWeights))
44
- this.rowsAmount = opts.rowsAmount || 250
45
- this.outputDir = opts.outputDir
46
-
47
- if (opts.limitSymbolsToReels) this.limitSymbolsToReels = opts.limitSymbolsToReels
48
-
49
- this.overrideExisting = opts.overrideExisting || false
50
- this.spaceBetweenSameSymbols = opts.spaceBetweenSameSymbols
51
- this.spaceBetweenSymbols = opts.spaceBetweenSymbols
52
- this.preferStackedSymbols = opts.preferStackedSymbols
53
- this.symbolStacks = opts.symbolStacks
54
- this.symbolQuotas = opts.symbolQuotas
55
-
56
- if (
57
- (typeof this.spaceBetweenSameSymbols == "number" &&
58
- (this.spaceBetweenSameSymbols < 1 || this.spaceBetweenSameSymbols > 8)) ||
59
- (typeof this.spaceBetweenSameSymbols == "object" &&
60
- Object.values(this.spaceBetweenSameSymbols).some((v) => v < 1 || v > 8))
61
- ) {
62
- throw new Error(
63
- `spaceBetweenSameSymbols must be between 1 and 8, got ${this.spaceBetweenSameSymbols}.`,
64
- )
65
- }
66
-
67
- if (
68
- Object.values(this.spaceBetweenSymbols || {}).some((o) =>
69
- Object.values(o).some((v) => v < 1 || v > 8),
70
- )
71
- ) {
72
- throw new Error(
73
- `spaceBetweenSymbols must be between 1 and 8, got ${this.spaceBetweenSymbols}.`,
74
- )
75
- }
76
-
77
- if (
78
- this.preferStackedSymbols &&
79
- (this.preferStackedSymbols < 0 || this.preferStackedSymbols > 100)
80
- ) {
81
- throw new Error(
82
- `preferStackedSymbols must be between 0 and 100, got ${this.preferStackedSymbols}.`,
83
- )
84
- }
85
-
86
- this.rng = new RandomNumberGenerator()
87
- this.rng.setSeed(opts.seed ?? 0)
88
- }
89
-
90
- private validateConfig({ config }: GameConfig) {
91
- config.symbols.forEach((symbol) => {
92
- if (!this.symbolWeights.has(symbol.id)) {
93
- throw new Error(
94
- [
95
- `Symbol "${symbol.id}" is not defined in the symbol weights of the reel generator ${this.id} for mode ${this.associatedGameModeName}.`,
96
- `Please ensure all symbols have weights defined.\n`,
97
- ].join(" "),
98
- )
99
- }
100
- })
101
-
102
- for (const [symbolId, weight] of this.symbolWeights.entries()) {
103
- if (!config.symbols.has(symbolId)) {
104
- throw new Error(
105
- [
106
- `Symbol "${symbolId}" is defined in the reel generator's symbol weights, but does not exist in the game config.`,
107
- `Please ensure all symbols in the reel generator are defined in the game config.\n`,
108
- ].join(" "),
109
- )
110
- }
111
- }
112
-
113
- if (this.limitSymbolsToReels && Object.keys(this.limitSymbolsToReels).length == 0) {
114
- this.limitSymbolsToReels = undefined
115
- }
116
-
117
- if (this.outputDir === "") {
118
- throw new Error("Output directory must be specified for the ReelGenerator.")
119
- }
120
- }
121
-
122
- private isSymbolAllowedOnReel(symbolId: string, reelIdx: number) {
123
- if (!this.limitSymbolsToReels) return true
124
- const allowedReels = this.limitSymbolsToReels[symbolId]
125
- if (!allowedReels || allowedReels.length === 0) return true
126
- return allowedReels.includes(reelIdx)
127
- }
128
-
129
- private resolveStacking(symbolId: string, reelIdx: number) {
130
- const cfg = this.symbolStacks?.[symbolId]
131
- if (!cfg) return null
132
-
133
- const STACKING_MIN = 1
134
- const STACKING_MAX = 4
135
-
136
- const chance =
137
- typeof cfg.chance === "number" ? cfg.chance : (cfg.chance?.[reelIdx] ?? 0)
138
- if (chance <= 0) return null
139
-
140
- let min = typeof cfg.min === "number" ? cfg.min : (cfg.min?.[reelIdx] ?? STACKING_MIN)
141
- let max = typeof cfg.max === "number" ? cfg.max : (cfg.max?.[reelIdx] ?? STACKING_MAX)
142
-
143
- return { chance, min, max }
144
- }
145
-
146
- private tryPlaceStack(
147
- reel: Array<GameSymbol | null>,
148
- gameConf: GameConfig,
149
- reelIdx: number,
150
- symbolId: string,
151
- startIndex: number,
152
- maxStack: number,
153
- ) {
154
- if (!this.isSymbolAllowedOnReel(symbolId, reelIdx)) return 0
155
-
156
- let canPlace = 0
157
- for (let j = 0; j < maxStack; j++) {
158
- const idx = (startIndex + j) % this.rowsAmount
159
- if (reel[idx] !== null) break
160
- canPlace++
161
- }
162
- if (canPlace === 0) return 0
163
-
164
- const symObj = gameConf.config.symbols.get(symbolId)
165
- if (!symObj) {
166
- throw new Error(
167
- `Symbol with id "${symbolId}" not found in the game config symbols map.`,
168
- )
169
- }
170
-
171
- for (let j = 0; j < canPlace; j++) {
172
- const idx = (startIndex + j) % reel.length
173
- reel[idx] = symObj
174
- }
175
- return canPlace
176
- }
177
-
178
- /**
179
- * Checks if a symbol can be placed at the target index without violating spacing rules.
180
- */
181
- private violatesSpacing(
182
- reel: Array<GameSymbol | null>,
183
- symbolId: string,
184
- targetIndex: number,
185
- ) {
186
- const circDist = (a: number, b: number) => {
187
- const diff = Math.abs(a - b)
188
- return Math.min(diff, this.rowsAmount - diff)
189
- }
190
-
191
- const spacingType = this.spaceBetweenSameSymbols ?? undefined
192
- const sameSpacing =
193
- typeof spacingType === "number" ? spacingType : (spacingType?.[symbolId] ?? 0)
194
-
195
- for (let i = 0; i <= reel.length; i++) {
196
- const placed = reel[i]
197
- if (!placed) continue
198
-
199
- const dist = circDist(targetIndex, i)
200
-
201
- // Same symbol spacing
202
- if (sameSpacing >= 1 && placed.id === symbolId) {
203
- if (dist <= sameSpacing) return true
204
- }
205
-
206
- // Cross-symbol spacing
207
- if (this.spaceBetweenSymbols) {
208
- const forward = this.spaceBetweenSymbols[symbolId]?.[placed.id] ?? 0
209
- if (forward >= 1 && dist <= forward) return true
210
-
211
- const reverse = this.spaceBetweenSymbols[placed.id]?.[symbolId] ?? 0
212
- if (reverse >= 1 && dist <= reverse) return true
213
- }
214
- }
215
-
216
- return false
217
- }
218
-
219
- generateReels(gameConf: AnyGameConfig) {
220
- this.validateConfig(gameConf)
221
-
222
- const gameMode = gameConf.config.gameModes[this.associatedGameModeName]
223
-
224
- if (!gameMode) {
225
- throw new Error(
226
- `Error generating reels for game mode "${this.associatedGameModeName}". It's not defined in the game config.`,
227
- )
228
- }
229
-
230
- const filePath = path.join(
231
- gameConf.config.outputDir,
232
- `reels_${this.associatedGameModeName}-${this.id}.csv`,
233
- )
234
- this.csvPath = filePath
235
-
236
- const exists = fs.existsSync(filePath)
237
-
238
- const reelsAmount = gameMode.reelsAmount
239
- const weightsObj = Object.fromEntries(this.symbolWeights)
240
-
241
- // Generate initial reels with random symbols
242
- for (let ridx = 0; ridx < reelsAmount; ridx++) {
243
- const reel: Array<GameSymbol | null> = new Array(this.rowsAmount).fill(null)
244
-
245
- const reelQuotas: Record<string, number> = {}
246
- const quotaCounts: Record<string, number> = {}
247
- let totalReelsQuota = 0
248
-
249
- // Get quotas for this reel, across all symbols
250
- for (const [sym, quotaConf] of Object.entries(this.symbolQuotas || {})) {
251
- const q = typeof quotaConf === "number" ? quotaConf : quotaConf[ridx]
252
- if (!q) continue
253
- reelQuotas[sym] = q
254
- totalReelsQuota += q
255
- }
256
-
257
- if (totalReelsQuota > 100) {
258
- throw new Error(
259
- `Total symbol quotas for reel ${ridx} exceed 100%. Adjust your configuration on ReelGenerator "${this.id}".`,
260
- )
261
- }
262
-
263
- if (totalReelsQuota > 0) {
264
- for (const [sym, quota] of Object.entries(reelQuotas)) {
265
- const quotaCount = Math.max(1, Math.floor((this.rowsAmount * quota) / 100))
266
- quotaCounts[sym] = quotaCount
267
- }
268
- }
269
-
270
- // Place required quotas first (use stacking over spacing, if configured)
271
- for (const [sym, targetCount] of Object.entries(quotaCounts)) {
272
- let remaining = targetCount
273
- let attempts = 0
274
-
275
- while (remaining > 0) {
276
- if (attempts++ > this.rowsAmount * 10) {
277
- throw new Error(
278
- `Failed to place ${targetCount} of symbol ${sym} on reel ${ridx} (likely spacing/stacking too strict).`,
279
- )
280
- }
281
-
282
- const pos = Math.round(this.rng.randomFloat(0, this.rowsAmount - 1))
283
- const stackCfg = this.resolveStacking(sym, ridx)
284
- let placed = 0
285
-
286
- // Try to place a symbol stack first, if configured
287
- if (stackCfg && Math.round(this.rng.randomFloat(1, 100)) <= stackCfg.chance) {
288
- const stackSize = Math.max(
289
- 0,
290
- Math.round(this.rng.randomFloat(stackCfg.min, stackCfg.max)),
291
- )
292
- const toPlace = Math.min(stackSize, remaining)
293
- placed = this.tryPlaceStack(reel, gameConf, ridx, sym, pos, toPlace)
294
- }
295
-
296
- // Not enough space, fall back to placing single symbols
297
- if (
298
- placed === 0 &&
299
- reel[pos] === null &&
300
- this.isSymbolAllowedOnReel(sym, ridx) &&
301
- !this.violatesSpacing(reel, sym, pos)
302
- ) {
303
- reel[pos] = gameConf.config.symbols.get(sym)!
304
- placed = 1
305
- }
306
-
307
- remaining -= placed
308
- }
309
- }
310
-
311
- // Fill the rest of the reel randomly
312
- for (let r = 0; r < this.rowsAmount; r++) {
313
- if (reel[r] !== null) continue // already placed quota
314
-
315
- let chosenSymbolId = weightedRandom(weightsObj, this.rng)
316
-
317
- // If symbolStacks is NOT configured for the next choice, allow "preferStackedSymbols" fallback
318
- const nextHasStackCfg = !!this.resolveStacking(chosenSymbolId, ridx)
319
- if (!nextHasStackCfg && this.preferStackedSymbols && reel.length > 0) {
320
- const prevSymbol = r - 1 >= 0 ? reel[r - 1] : reel[reel.length - 1]
321
- if (
322
- prevSymbol &&
323
- Math.round(this.rng.randomFloat(1, 100)) <= this.preferStackedSymbols &&
324
- (!this.spaceBetweenSameSymbols ||
325
- !this.violatesSpacing(reel, prevSymbol.id, r))
326
- ) {
327
- chosenSymbolId = prevSymbol.id
328
- }
329
- }
330
-
331
- // Check for stacking preference
332
- if (this.preferStackedSymbols && reel.length > 0) {
333
- const prevSymbol = r - 1 >= 0 ? reel[r - 1] : reel[reel.length - 1]
334
-
335
- if (
336
- Math.round(this.rng.randomFloat(1, 100)) <= this.preferStackedSymbols &&
337
- (!this.spaceBetweenSameSymbols ||
338
- !this.violatesSpacing(reel, prevSymbol!.id, r))
339
- ) {
340
- chosenSymbolId = prevSymbol!.id
341
- }
342
- }
343
-
344
- // If symbol has stack config, try to place a stack (ignore spacing)
345
- const stackCfg = this.resolveStacking(chosenSymbolId, ridx)
346
- if (stackCfg && this.isSymbolAllowedOnReel(chosenSymbolId, ridx)) {
347
- const roll = Math.round(this.rng.randomFloat(1, 100))
348
- if (roll <= stackCfg.chance) {
349
- const desiredSize = Math.max(
350
- 1,
351
- Math.round(this.rng.randomFloat(stackCfg.min, stackCfg.max)),
352
- )
353
- const placed = this.tryPlaceStack(
354
- reel,
355
- gameConf,
356
- ridx,
357
- chosenSymbolId,
358
- r,
359
- desiredSize,
360
- )
361
- if (placed > 0) {
362
- // advance loop to skip the cells we just filled on this side of the boundary
363
- // (wrapped cells at the start are already filled and will be skipped when encountered)
364
- r += placed - 1
365
- continue
366
- }
367
- }
368
- }
369
-
370
- let tries = 0
371
- const maxTries = 2500
372
-
373
- while (
374
- !this.isSymbolAllowedOnReel(chosenSymbolId, ridx) ||
375
- this.violatesSpacing(reel, chosenSymbolId, r)
376
- ) {
377
- if (++tries > maxTries) {
378
- throw new Error(
379
- [
380
- `Failed to place a symbol on reel ${ridx} at position ${r} after ${maxTries} attempts.\n`,
381
- "Try to change the seed or adjust your configuration.\n",
382
- ].join(" "),
383
- )
384
- }
385
- chosenSymbolId = weightedRandom(weightsObj, this.rng)
386
-
387
- const hasStackCfg = !!this.resolveStacking(chosenSymbolId, ridx)
388
- if (!hasStackCfg && this.preferStackedSymbols && reel.length > 0) {
389
- const prevSymbol = r - 1 >= 0 ? reel[r - 1] : reel[reel.length - 1]
390
- if (
391
- prevSymbol &&
392
- Math.round(this.rng.randomFloat(1, 100)) <= this.preferStackedSymbols &&
393
- (!this.spaceBetweenSameSymbols ||
394
- !this.violatesSpacing(reel, prevSymbol.id, r))
395
- ) {
396
- chosenSymbolId = prevSymbol.id
397
- }
398
- }
399
- }
400
-
401
- const symbol = gameConf.config.symbols.get(chosenSymbolId)
402
-
403
- if (!symbol) {
404
- throw new Error(
405
- `Symbol with id "${chosenSymbolId}" not found in the game config symbols map.`,
406
- )
407
- }
408
-
409
- reel[r] = symbol
410
- }
411
-
412
- if (reel.some((s) => s === null)) {
413
- throw new Error(`Reel ${ridx} has unfilled positions after generation.`)
414
- }
415
-
416
- this.reels.push(reel as GameSymbol[])
417
- }
418
-
419
- // Write the CSV
420
- const csvRows: string[][] = Array.from({ length: this.rowsAmount }, () =>
421
- Array.from({ length: reelsAmount }, () => ""),
422
- )
423
-
424
- for (let ridx = 0; ridx < reelsAmount; ridx++) {
425
- for (let r = 0; r < this.rowsAmount; r++) {
426
- csvRows[r]![ridx] = this.reels[ridx]![r]!.id
427
- }
428
- }
429
-
430
- const csvString = csvRows.map((row) => row.join(",")).join("\n")
431
-
432
- if (isMainThread) {
433
- createDirIfNotExists(this.outputDir)
434
- fs.writeFileSync(filePath, csvString)
435
-
436
- this.reels = this.parseReelsetCSV(filePath, gameConf)
437
- console.log(
438
- `Generated reelset ${this.id} for game mode ${this.associatedGameModeName}`,
439
- )
440
- }
441
-
442
- if (exists) {
443
- this.reels = this.parseReelsetCSV(filePath, gameConf)
444
- }
445
- }
446
-
447
- /**
448
- * Reads a reelset CSV file and returns the reels as arrays of GameSymbols.
449
- */
450
- parseReelsetCSV(reelSetPath: string, { config }: GameConfig) {
451
- const csvData = fs.readFileSync(reelSetPath, "utf8")
452
- const rows = csvData.split("\n").filter((line) => line.trim() !== "")
453
- const reels: Reels = Array.from(
454
- { length: config.gameModes[this.associatedGameModeName]!.reelsAmount },
455
- () => [],
456
- )
457
- rows.forEach((row) => {
458
- const symsInRow = row.split(",").map((symbolId) => {
459
- const symbol = config.symbols.get(symbolId.trim())
460
- if (!symbol) {
461
- throw new Error(`Symbol with id "${symbolId}" not found in game config.`)
462
- }
463
- return symbol
464
- })
465
- symsInRow.forEach((symbol, ridx) => {
466
- reels[ridx]!.push(symbol)
467
- })
468
- })
469
- return reels
470
- }
471
- }
472
-
473
- interface ReelGeneratorOpts {
474
- /**
475
- * The unique identifier of the reel generator.\
476
- * Must be unique per game mode.
477
- */
478
- id: string
479
- /**
480
- * The weights of the symbols in the reelset.\
481
- * This is a mapping of symbol IDs to their respective weights.
482
- */
483
- symbolWeights: Record<string, number>
484
- /**
485
- * The number of rows in the reelset.\
486
- * Default is 250, but can be adjusted as needed.
487
- */
488
- rowsAmount?: number
489
- /**
490
- * The directory where the generated reelset files will be saved.\
491
- * **It's recommended to just use `__dirname`**!
492
- */
493
- outputDir: string
494
- /**
495
- * Prevent the same symbol from appearing directly above or below itself.\
496
- * This can be a single number for all symbols, or a mapping of symbol IDs to
497
- * their respective spacing values.
498
- *
499
- * Must be 1 or higher, if set.
500
- *
501
- * **This is overridden by `symbolStacks`**
502
- */
503
- spaceBetweenSameSymbols?: number | Record<string, number>
504
- /**
505
- * Prevents specific symbols from appearing within a certain distance of each other.
506
- *
507
- * Useful for preventing scatter and super scatter symbols from appearing too close to each other.
508
- *
509
- * **This is overridden by `symbolStacks`**
510
- */
511
- spaceBetweenSymbols?: Record<string, Record<string, number>>
512
- /**
513
- * A percentage value 0-100 that indicates the likelihood of a symbol being stacked.\
514
- * A value of 0 means no stacked symbols, while 100 means all symbols are stacked.
515
- *
516
- * This is only a preference. Symbols may still not be stacked if\
517
- * other restrictions (like `spaceBetweenSameSymbols`) prevent it.
518
- *
519
- * **This is overridden by `symbolStacks`**
520
- */
521
- preferStackedSymbols?: number
522
- /**
523
- * A mapping of symbols to their respective advanced stacking configuration.
524
- *
525
- * @example
526
- * ```ts
527
- * symbolStacks: {
528
- * "W": {
529
- * chance: { "1": 20, "2": 20, "3": 20, "4": 20 }, // 20% chance to be stacked on reels 2-5
530
- * min: 2, // At least 2 wilds in a stack
531
- * max: 4, // At most 4 wilds in a stack
532
- * }
533
- * }
534
- * ```
535
- */
536
- symbolStacks?: Record<
537
- string,
538
- {
539
- chance: number | Record<string, number>
540
- min?: number | Record<string, number>
541
- max?: number | Record<string, number>
542
- }
543
- >
544
- /**
545
- * Configures symbols to be limited to specific reels.\
546
- * For example, you could configure Scatters to appear only on reels 1, 3 and 5.
547
- *
548
- * @example
549
- * ```ts
550
- * limitSymbolsToReels: {
551
- * "S": [0, 2, 4], // Remember that reels are 0-indexed.
552
- * }
553
- * ```
554
- */
555
- limitSymbolsToReels?: Record<string, number[]>
556
- /**
557
- * Defines optional quotas for symbols on the reels.\
558
- * The quota (1-100%) defines how often a symbol should appear in the reelset, or in a specific reel.
559
- *
560
- * This is particularly useful for controlling the frequency of special symbols like scatters or wilds.
561
- *
562
- * Reels not provided for a symbol will use the weights from `symbolWeights`.
563
- *
564
- * _Any_ small quota will ensure that the symbol appears at least once on the reel.
565
- *
566
- * @example
567
- * ```ts
568
- * symbolQuotas: {
569
- * "S": 3, // 3% of symbols on each reel will be scatters
570
- * "W": { "1": 10, "2": 5, "3": 3, "4": 1 }, // Wilds will appear with different quotas on selected reels
571
- * }
572
- * ```
573
- */
574
- symbolQuotas?: Record<string, number | Record<string, number>>
575
- /**
576
- * If true, existing reels CSV files will be overwritten.
577
- */
578
- overrideExisting?: boolean
579
- /**
580
- * Optional seed for the RNG to ensure reproducible results.
581
- *
582
- * Default seed is `0`.
583
- *
584
- * Note: Seeds 0 and 1 produce the same results.
585
- */
586
- seed?: number
587
- }
588
-
589
- export type Reels = GameSymbol[][]