@slot-engine/core 0.0.4 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.mts CHANGED
@@ -82,7 +82,6 @@ declare class ReelGenerator {
82
82
  max?: number | Record<string, number>;
83
83
  }>;
84
84
  protected readonly symbolQuotas?: Record<string, number | Record<string, number>>;
85
- outputDir: string;
86
85
  csvPath: string;
87
86
  overrideExisting: boolean;
88
87
  rng: RandomNumberGenerator;
@@ -117,11 +116,6 @@ interface ReelGeneratorOpts {
117
116
  * Default is 250, but can be adjusted as needed.
118
117
  */
119
118
  rowsAmount?: number;
120
- /**
121
- * The directory where the generated reelset files will be saved.\
122
- * **It's recommended to just use `__dirname`**!
123
- */
124
- outputDir: string;
125
119
  /**
126
120
  * Prevent the same symbol from appearing directly above or below itself.\
127
121
  * This can be a single number for all symbols, or a mapping of symbol IDs to
package/dist/index.d.ts CHANGED
@@ -82,7 +82,6 @@ declare class ReelGenerator {
82
82
  max?: number | Record<string, number>;
83
83
  }>;
84
84
  protected readonly symbolQuotas?: Record<string, number | Record<string, number>>;
85
- outputDir: string;
86
85
  csvPath: string;
87
86
  overrideExisting: boolean;
88
87
  rng: RandomNumberGenerator;
@@ -117,11 +116,6 @@ interface ReelGeneratorOpts {
117
116
  * Default is 250, but can be adjusted as needed.
118
117
  */
119
118
  rowsAmount?: number;
120
- /**
121
- * The directory where the generated reelset files will be saved.\
122
- * **It's recommended to just use `__dirname`**!
123
- */
124
- outputDir: string;
125
119
  /**
126
120
  * Prevent the same symbol from appearing directly above or below itself.\
127
121
  * This can be a single number for all symbols, or a mapping of symbol IDs to
package/dist/index.js CHANGED
@@ -96,7 +96,6 @@ var GameConfig = class _GameConfig {
96
96
  if (mode.reelSets && mode.reelSets.length > 0) {
97
97
  for (const reelGenerator of Object.values(mode.reelSets)) {
98
98
  reelGenerator.associatedGameModeName = mode.name;
99
- reelGenerator.outputDir = this.config.outputDir;
100
99
  reelGenerator.generateReels(this);
101
100
  }
102
101
  } else {
@@ -159,6 +158,7 @@ var GameConfig = class _GameConfig {
159
158
  };
160
159
 
161
160
  // src/GameMode.ts
161
+ var import_assert2 = __toESM(require("assert"));
162
162
  var GameMode = class {
163
163
  name;
164
164
  reelsAmount;
@@ -177,14 +177,12 @@ var GameMode = class {
177
177
  this.reelSets = opts.reelSets;
178
178
  this.resultSets = opts.resultSets;
179
179
  this.isBonusBuy = opts.isBonusBuy;
180
- if (this.symbolsPerReel.length !== this.reelsAmount) {
181
- throw new Error(
182
- `symbolsPerReel length (${this.symbolsPerReel.length}) must match reelsAmount (${this.reelsAmount}).`
183
- );
184
- }
185
- if (this.resultSets.length == 0) {
186
- throw new Error("GameMode must have at least one ResultSet defined.");
187
- }
180
+ (0, import_assert2.default)(this.rtp >= 0.9 && this.rtp <= 0.99, "RTP must be between 0.9 and 0.99");
181
+ (0, import_assert2.default)(
182
+ this.symbolsPerReel.length === this.reelsAmount,
183
+ "symbolsPerReel length must match reelsAmount."
184
+ );
185
+ (0, import_assert2.default)(this.reelSets.length > 0, "GameMode must have at least one ReelSet defined.");
188
186
  }
189
187
  };
190
188
 
@@ -395,7 +393,6 @@ var ReelGenerator = class {
395
393
  preferStackedSymbols;
396
394
  symbolStacks;
397
395
  symbolQuotas;
398
- outputDir = "";
399
396
  csvPath = "";
400
397
  overrideExisting;
401
398
  rng;
@@ -403,7 +400,6 @@ var ReelGenerator = class {
403
400
  this.id = opts.id;
404
401
  this.symbolWeights = new Map(Object.entries(opts.symbolWeights));
405
402
  this.rowsAmount = opts.rowsAmount || 250;
406
- this.outputDir = opts.outputDir;
407
403
  if (opts.limitSymbolsToReels) this.limitSymbolsToReels = opts.limitSymbolsToReels;
408
404
  this.overrideExisting = opts.overrideExisting || false;
409
405
  this.spaceBetweenSameSymbols = opts.spaceBetweenSameSymbols;
@@ -432,34 +428,16 @@ var ReelGenerator = class {
432
428
  this.rng.setSeed(opts.seed ?? 0);
433
429
  }
434
430
  validateConfig({ config }) {
435
- config.symbols.forEach((symbol) => {
436
- if (!this.symbolWeights.has(symbol.id)) {
431
+ this.symbolWeights.forEach((_, symbol) => {
432
+ if (!config.symbols.has(symbol)) {
437
433
  throw new Error(
438
- [
439
- `Symbol "${symbol.id}" is not defined in the symbol weights of the reel generator ${this.id} for mode ${this.associatedGameModeName}.`,
440
- `Please ensure all symbols have weights defined.
441
- `
442
- ].join(" ")
434
+ `Symbol "${symbol}" of the reel generator ${this.id} for mode ${this.associatedGameModeName} is not defined in the game config`
443
435
  );
444
436
  }
445
437
  });
446
- for (const [symbolId, weight] of this.symbolWeights.entries()) {
447
- if (!config.symbols.has(symbolId)) {
448
- throw new Error(
449
- [
450
- `Symbol "${symbolId}" is defined in the reel generator's symbol weights, but does not exist in the game config.`,
451
- `Please ensure all symbols in the reel generator are defined in the game config.
452
- `
453
- ].join(" ")
454
- );
455
- }
456
- }
457
438
  if (this.limitSymbolsToReels && Object.keys(this.limitSymbolsToReels).length == 0) {
458
439
  this.limitSymbolsToReels = void 0;
459
440
  }
460
- if (this.outputDir === "") {
461
- throw new Error("Output directory must be specified for the ReelGenerator.");
462
- }
463
441
  }
464
442
  isSymbolAllowedOnReel(symbolId, reelIdx) {
465
443
  if (!this.limitSymbolsToReels) return true;
@@ -539,6 +517,15 @@ var ReelGenerator = class {
539
517
  );
540
518
  this.csvPath = filePath;
541
519
  const exists = import_fs2.default.existsSync(filePath);
520
+ if (exists && !this.overrideExisting) {
521
+ this.reels = this.parseReelsetCSV(filePath, gameConf);
522
+ return;
523
+ }
524
+ if (!exists && this.symbolWeights.size === 0) {
525
+ throw new Error(
526
+ `Cannot generate reels for generator "${this.id}" of mode "${this.associatedGameModeName}" because the symbol weights are empty.`
527
+ );
528
+ }
542
529
  const reelsAmount = gameMode.reelsAmount;
543
530
  const weightsObj = Object.fromEntries(this.symbolWeights);
544
531
  for (let ridx = 0; ridx < reelsAmount; ridx++) {
@@ -673,16 +660,12 @@ var ReelGenerator = class {
673
660
  }
674
661
  const csvString = csvRows.map((row) => row.join(",")).join("\n");
675
662
  if (import_worker_threads.isMainThread) {
676
- createDirIfNotExists(this.outputDir);
677
663
  import_fs2.default.writeFileSync(filePath, csvString);
678
664
  this.reels = this.parseReelsetCSV(filePath, gameConf);
679
665
  console.log(
680
666
  `Generated reelset ${this.id} for game mode ${this.associatedGameModeName}`
681
667
  );
682
668
  }
683
- if (exists) {
684
- this.reels = this.parseReelsetCSV(filePath, gameConf);
685
- }
686
669
  }
687
670
  /**
688
671
  * Reads a reelset CSV file and returns the reels as arrays of GameSymbols.
@@ -706,11 +689,21 @@ var ReelGenerator = class {
706
689
  reels[ridx].push(symbol);
707
690
  });
708
691
  });
692
+ const reelLengths = reels.map((r) => r.length);
693
+ const uniqueLengths = new Set(reelLengths);
694
+ if (uniqueLengths.size > 1) {
695
+ throw new Error(
696
+ `Inconsistent reel lengths in reelset CSV at ${reelSetPath}: ${[
697
+ ...uniqueLengths
698
+ ].join(", ")}`
699
+ );
700
+ }
709
701
  return reels;
710
702
  }
711
703
  };
712
704
 
713
705
  // src/ResultSet.ts
706
+ var import_assert3 = __toESM(require("assert"));
714
707
  var ResultSet = class {
715
708
  criteria;
716
709
  quota;
@@ -729,16 +722,11 @@ var ResultSet = class {
729
722
  this.forceMaxWin = opts.forceMaxWin;
730
723
  this.forceFreespins = opts.forceFreespins;
731
724
  this.evaluate = opts.evaluate;
732
- if (this.quota < 0 || this.quota > 1) {
733
- throw new Error(`Quota must be a float between 0 and 1, got ${this.quota}.`);
734
- }
735
725
  }
736
726
  static assignCriteriaToSimulations(ctx, gameModeName) {
737
727
  const rng = new RandomNumberGenerator();
738
728
  rng.setSeed(0);
739
- if (!ctx.simRunsAmount) {
740
- throw new Error("Simulation configuration is not set.");
741
- }
729
+ (0, import_assert3.default)(ctx.simRunsAmount, "Simulation configuration is not set.");
742
730
  const simNums = ctx.simRunsAmount[gameModeName];
743
731
  const resultSets = ctx.gameConfig.config.gameModes[gameModeName]?.resultSets;
744
732
  if (!resultSets || resultSets.length === 0) {
@@ -747,8 +735,12 @@ var ResultSet = class {
747
735
  if (simNums === void 0 || simNums <= 0) {
748
736
  throw new Error(`No simulations configured for game mode "${gameModeName}".`);
749
737
  }
738
+ const totalQuota = resultSets.reduce((sum, rs) => sum + rs.quota, 0);
750
739
  const numberOfSimsForCriteria = Object.fromEntries(
751
- resultSets.map((rs) => [rs.criteria, Math.max(Math.floor(rs.quota * simNums), 1)])
740
+ resultSets.map((rs) => {
741
+ const normalizedQuota = totalQuota > 0 ? rs.quota / totalQuota : 0;
742
+ return [rs.criteria, Math.max(Math.floor(normalizedQuota * simNums), 1)];
743
+ })
752
744
  );
753
745
  let totalSims = Object.values(numberOfSimsForCriteria).reduce(
754
746
  (sum, num) => sum + num,
@@ -1819,7 +1811,7 @@ var ManywaysWinType = class extends WinType {
1819
1811
  };
1820
1812
 
1821
1813
  // src/optimizer/OptimizationConditions.ts
1822
- var import_assert2 = __toESM(require("assert"));
1814
+ var import_assert4 = __toESM(require("assert"));
1823
1815
  var OptimizationConditions = class {
1824
1816
  rtp;
1825
1817
  avgWin;
@@ -1830,14 +1822,14 @@ var OptimizationConditions = class {
1830
1822
  constructor(opts) {
1831
1823
  let { rtp, avgWin, hitRate, searchConditions, priority } = opts;
1832
1824
  if (rtp == void 0 || rtp === "x") {
1833
- (0, import_assert2.default)(avgWin !== void 0 && hitRate !== void 0, "If RTP is not specified, hit-rate (hr) and average win amount (av_win) must be given.");
1825
+ (0, import_assert4.default)(avgWin !== void 0 && hitRate !== void 0, "If RTP is not specified, hit-rate (hr) and average win amount (av_win) must be given.");
1834
1826
  rtp = Math.round(avgWin / Number(hitRate) * 1e5) / 1e5;
1835
1827
  }
1836
1828
  let noneCount = 0;
1837
1829
  for (const val of [rtp, avgWin, hitRate]) {
1838
1830
  if (val === void 0) noneCount++;
1839
1831
  }
1840
- (0, import_assert2.default)(noneCount <= 1, "Invalid combination of optimization conditions.");
1832
+ (0, import_assert4.default)(noneCount <= 1, "Invalid combination of optimization conditions.");
1841
1833
  this.searchRange = [-1, -1];
1842
1834
  this.forceSearch = {};
1843
1835
  if (typeof searchConditions === "number") {
@@ -1918,7 +1910,7 @@ var OptimizationParameters = class _OptimizationParameters {
1918
1910
  // src/Simulation.ts
1919
1911
  var import_fs3 = __toESM(require("fs"));
1920
1912
  var import_path2 = __toESM(require("path"));
1921
- var import_assert3 = __toESM(require("assert"));
1913
+ var import_assert5 = __toESM(require("assert"));
1922
1914
  var import_zlib = __toESM(require("zlib"));
1923
1915
  var import_esbuild = require("esbuild");
1924
1916
  var import_worker_threads2 = require("worker_threads");
@@ -1942,7 +1934,7 @@ var Simulation = class _Simulation {
1942
1934
  this.library = /* @__PURE__ */ new Map();
1943
1935
  this.records = [];
1944
1936
  const gameModeKeys = Object.keys(this.gameConfig.config.gameModes);
1945
- (0, import_assert3.default)(
1937
+ (0, import_assert5.default)(
1946
1938
  Object.values(this.gameConfig.config.gameModes).map((m) => gameModeKeys.includes(m.name)).every((v) => v === true),
1947
1939
  "Game mode name must match its key in the gameModes object."
1948
1940
  );
@@ -2348,7 +2340,7 @@ var SimulationContext = class extends Board {
2348
2340
  // src/analysis/index.ts
2349
2341
  var import_fs4 = __toESM(require("fs"));
2350
2342
  var import_path3 = __toESM(require("path"));
2351
- var import_assert4 = __toESM(require("assert"));
2343
+ var import_assert6 = __toESM(require("assert"));
2352
2344
 
2353
2345
  // src/analysis/utils.ts
2354
2346
  function parseLookupTable(content) {
@@ -2513,7 +2505,7 @@ var Analysis = class {
2513
2505
  booksJsonlCompressed
2514
2506
  };
2515
2507
  for (const p of Object.values(paths[modeStr])) {
2516
- (0, import_assert4.default)(
2508
+ (0, import_assert6.default)(
2517
2509
  import_fs4.default.existsSync(p),
2518
2510
  `File "${p}" does not exist. Run optimization to auto-create it.`
2519
2511
  );
@@ -2585,7 +2577,7 @@ var Analysis = class {
2585
2577
  }
2586
2578
  getGameModeConfig(mode) {
2587
2579
  const config = this.gameConfig.gameModes[mode];
2588
- (0, import_assert4.default)(config, `Game mode "${mode}" not found in game config`);
2580
+ (0, import_assert6.default)(config, `Game mode "${mode}" not found in game config`);
2589
2581
  return config;
2590
2582
  }
2591
2583
  };
@@ -2689,7 +2681,7 @@ function makeSetupFile(optimizer, gameMode) {
2689
2681
  // src/optimizer/index.ts
2690
2682
  var import_child_process = require("child_process");
2691
2683
  var import_path6 = __toESM(require("path"));
2692
- var import_assert5 = __toESM(require("assert"));
2684
+ var import_assert7 = __toESM(require("assert"));
2693
2685
  var import_worker_threads4 = require("worker_threads");
2694
2686
  var Optimizer = class {
2695
2687
  gameConfig;
@@ -2733,7 +2725,7 @@ var Optimizer = class {
2733
2725
  }
2734
2726
  }
2735
2727
  const criteria = configMode.resultSets.map((r) => r.criteria);
2736
- (0, import_assert5.default)(
2728
+ (0, import_assert7.default)(
2737
2729
  conditions.every((c) => criteria.includes(c)),
2738
2730
  `Not all ResultSet criteria in game mode "${k}" are defined as optimization conditions.`
2739
2731
  );
@@ -2745,7 +2737,7 @@ var Optimizer = class {
2745
2737
  }
2746
2738
  gameModeRtp = Math.round(gameModeRtp * 1e3) / 1e3;
2747
2739
  paramRtp = Math.round(paramRtp * 1e3) / 1e3;
2748
- (0, import_assert5.default)(
2740
+ (0, import_assert7.default)(
2749
2741
  gameModeRtp === paramRtp,
2750
2742
  `Sum of all RTP conditions (${paramRtp}) does not match the game mode RTP (${gameModeRtp}) in game mode "${k}".`
2751
2743
  );