@slot-engine/core 0.0.3 → 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.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 {
@@ -116,7 +115,7 @@ var GameConfig = class _GameConfig {
116
115
  `Reel set with id "${id}" not found in game mode "${gameMode}". Available reel sets: ${this.config.gameModes[gameMode].reelSets.map((rs) => rs.id).join(", ")}`
117
116
  );
118
117
  }
119
- return reelSet;
118
+ return reelSet.reels;
120
119
  }
121
120
  /**
122
121
  * Retrieves the number of free spins awarded for a given spin type and scatter count.
@@ -133,7 +132,7 @@ var GameConfig = class _GameConfig {
133
132
  /**
134
133
  * Retrieves a result set by its criteria within a specific game mode.
135
134
  */
136
- getGameModeCriteria(mode, criteria) {
135
+ getResultSetByCriteria(mode, criteria) {
137
136
  const gameMode = this.config.gameModes[mode];
138
137
  if (!gameMode) {
139
138
  throw new Error(`Game mode "${mode}" not found in game config.`);
@@ -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,
@@ -1103,13 +1095,6 @@ var GameState = class extends GameConfig {
1103
1095
  this.clearPendingRecords();
1104
1096
  this.state.userData = this.config.userState || {};
1105
1097
  }
1106
- /**
1107
- * Checks if a max win is reached by comparing `wallet.currentWin` to `config.maxWin`.
1108
- *
1109
- * Should be called after `wallet.confirmSpinWin()`.
1110
- */
1111
- isMaxWinTriggered() {
1112
- }
1113
1098
  /**
1114
1099
  * Empties the list of pending records in the recorder.
1115
1100
  */
@@ -1555,7 +1540,7 @@ var Board = class extends GameState {
1555
1540
  return stopPositionsForReels;
1556
1541
  }
1557
1542
  /**
1558
- * Selects a random reelset based on the configured weights for the current game mode.\
1543
+ * Selects a random reel set based on the configured weights of the current result set.\
1559
1544
  * Returns the reels as arrays of GameSymbols.
1560
1545
  */
1561
1546
  getRandomReelset() {
@@ -1570,7 +1555,7 @@ var Board = class extends GameState {
1570
1555
  reelSetId = weightedRandom(weights[this.state.currentSpinType], this.state.rng);
1571
1556
  }
1572
1557
  const reelSet = this.getReelsetById(this.state.currentGameMode, reelSetId);
1573
- return reelSet.reels;
1558
+ return reelSet;
1574
1559
  }
1575
1560
  /**
1576
1561
  * Draws a board using specified reel stops.
@@ -1826,7 +1811,7 @@ var ManywaysWinType = class extends WinType {
1826
1811
  };
1827
1812
 
1828
1813
  // src/optimizer/OptimizationConditions.ts
1829
- var import_assert2 = __toESM(require("assert"));
1814
+ var import_assert4 = __toESM(require("assert"));
1830
1815
  var OptimizationConditions = class {
1831
1816
  rtp;
1832
1817
  avgWin;
@@ -1837,14 +1822,14 @@ var OptimizationConditions = class {
1837
1822
  constructor(opts) {
1838
1823
  let { rtp, avgWin, hitRate, searchConditions, priority } = opts;
1839
1824
  if (rtp == void 0 || rtp === "x") {
1840
- (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.");
1841
1826
  rtp = Math.round(avgWin / Number(hitRate) * 1e5) / 1e5;
1842
1827
  }
1843
1828
  let noneCount = 0;
1844
1829
  for (const val of [rtp, avgWin, hitRate]) {
1845
1830
  if (val === void 0) noneCount++;
1846
1831
  }
1847
- (0, import_assert2.default)(noneCount <= 1, "Invalid combination of optimization conditions.");
1832
+ (0, import_assert4.default)(noneCount <= 1, "Invalid combination of optimization conditions.");
1848
1833
  this.searchRange = [-1, -1];
1849
1834
  this.forceSearch = {};
1850
1835
  if (typeof searchConditions === "number") {
@@ -1925,7 +1910,7 @@ var OptimizationParameters = class _OptimizationParameters {
1925
1910
  // src/Simulation.ts
1926
1911
  var import_fs3 = __toESM(require("fs"));
1927
1912
  var import_path2 = __toESM(require("path"));
1928
- var import_assert3 = __toESM(require("assert"));
1913
+ var import_assert5 = __toESM(require("assert"));
1929
1914
  var import_zlib = __toESM(require("zlib"));
1930
1915
  var import_esbuild = require("esbuild");
1931
1916
  var import_worker_threads2 = require("worker_threads");
@@ -1949,7 +1934,7 @@ var Simulation = class _Simulation {
1949
1934
  this.library = /* @__PURE__ */ new Map();
1950
1935
  this.records = [];
1951
1936
  const gameModeKeys = Object.keys(this.gameConfig.config.gameModes);
1952
- (0, import_assert3.default)(
1937
+ (0, import_assert5.default)(
1953
1938
  Object.values(this.gameConfig.config.gameModes).map((m) => gameModeKeys.includes(m.name)).every((v) => v === true),
1954
1939
  "Game mode name must match its key in the gameModes object."
1955
1940
  );
@@ -2300,22 +2285,25 @@ var SimulationContext = class extends Board {
2300
2285
  this.state.currentGameMode = mode;
2301
2286
  this.state.currentSimulationId = simId;
2302
2287
  this.state.isCriteriaMet = false;
2288
+ const resultSet = this.getResultSetByCriteria(this.state.currentGameMode, criteria);
2303
2289
  while (!this.state.isCriteriaMet) {
2304
2290
  this.actualSims++;
2305
2291
  this.resetSimulation();
2306
- const resultSet = this.getGameModeCriteria(this.state.currentGameMode, criteria);
2307
2292
  this.state.currentResultSet = resultSet;
2308
2293
  this.state.book.criteria = resultSet.criteria;
2309
2294
  this.handleGameFlow();
2310
2295
  if (resultSet.meetsCriteria(this)) {
2311
2296
  this.state.isCriteriaMet = true;
2312
- this.config.hooks.onSimulationAccepted?.(this);
2313
- this.record({
2314
- criteria: resultSet.criteria
2315
- });
2316
2297
  }
2317
2298
  }
2318
2299
  this.wallet.confirmWins(this);
2300
+ if (this.state.book.getPayout() >= this.config.maxWinX) {
2301
+ this.state.triggeredMaxWin = true;
2302
+ }
2303
+ this.record({
2304
+ criteria: resultSet.criteria
2305
+ });
2306
+ this.config.hooks.onSimulationAccepted?.(this);
2319
2307
  this.confirmRecords();
2320
2308
  import_worker_threads2.parentPort?.postMessage({
2321
2309
  type: "complete",
@@ -2352,7 +2340,7 @@ var SimulationContext = class extends Board {
2352
2340
  // src/analysis/index.ts
2353
2341
  var import_fs4 = __toESM(require("fs"));
2354
2342
  var import_path3 = __toESM(require("path"));
2355
- var import_assert4 = __toESM(require("assert"));
2343
+ var import_assert6 = __toESM(require("assert"));
2356
2344
 
2357
2345
  // src/analysis/utils.ts
2358
2346
  function parseLookupTable(content) {
@@ -2517,7 +2505,7 @@ var Analysis = class {
2517
2505
  booksJsonlCompressed
2518
2506
  };
2519
2507
  for (const p of Object.values(paths[modeStr])) {
2520
- (0, import_assert4.default)(
2508
+ (0, import_assert6.default)(
2521
2509
  import_fs4.default.existsSync(p),
2522
2510
  `File "${p}" does not exist. Run optimization to auto-create it.`
2523
2511
  );
@@ -2589,7 +2577,7 @@ var Analysis = class {
2589
2577
  }
2590
2578
  getGameModeConfig(mode) {
2591
2579
  const config = this.gameConfig.gameModes[mode];
2592
- (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`);
2593
2581
  return config;
2594
2582
  }
2595
2583
  };
@@ -2693,7 +2681,7 @@ function makeSetupFile(optimizer, gameMode) {
2693
2681
  // src/optimizer/index.ts
2694
2682
  var import_child_process = require("child_process");
2695
2683
  var import_path6 = __toESM(require("path"));
2696
- var import_assert5 = __toESM(require("assert"));
2684
+ var import_assert7 = __toESM(require("assert"));
2697
2685
  var import_worker_threads4 = require("worker_threads");
2698
2686
  var Optimizer = class {
2699
2687
  gameConfig;
@@ -2737,7 +2725,7 @@ var Optimizer = class {
2737
2725
  }
2738
2726
  }
2739
2727
  const criteria = configMode.resultSets.map((r) => r.criteria);
2740
- (0, import_assert5.default)(
2728
+ (0, import_assert7.default)(
2741
2729
  conditions.every((c) => criteria.includes(c)),
2742
2730
  `Not all ResultSet criteria in game mode "${k}" are defined as optimization conditions.`
2743
2731
  );
@@ -2749,7 +2737,7 @@ var Optimizer = class {
2749
2737
  }
2750
2738
  gameModeRtp = Math.round(gameModeRtp * 1e3) / 1e3;
2751
2739
  paramRtp = Math.round(paramRtp * 1e3) / 1e3;
2752
- (0, import_assert5.default)(
2740
+ (0, import_assert7.default)(
2753
2741
  gameModeRtp === paramRtp,
2754
2742
  `Sum of all RTP conditions (${paramRtp}) does not match the game mode RTP (${gameModeRtp}) in game mode "${k}".`
2755
2743
  );