@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.mjs CHANGED
@@ -42,7 +42,6 @@ var GameConfig = class _GameConfig {
42
42
  if (mode.reelSets && mode.reelSets.length > 0) {
43
43
  for (const reelGenerator of Object.values(mode.reelSets)) {
44
44
  reelGenerator.associatedGameModeName = mode.name;
45
- reelGenerator.outputDir = this.config.outputDir;
46
45
  reelGenerator.generateReels(this);
47
46
  }
48
47
  } else {
@@ -62,7 +61,7 @@ var GameConfig = class _GameConfig {
62
61
  `Reel set with id "${id}" not found in game mode "${gameMode}". Available reel sets: ${this.config.gameModes[gameMode].reelSets.map((rs) => rs.id).join(", ")}`
63
62
  );
64
63
  }
65
- return reelSet;
64
+ return reelSet.reels;
66
65
  }
67
66
  /**
68
67
  * Retrieves the number of free spins awarded for a given spin type and scatter count.
@@ -79,7 +78,7 @@ var GameConfig = class _GameConfig {
79
78
  /**
80
79
  * Retrieves a result set by its criteria within a specific game mode.
81
80
  */
82
- getGameModeCriteria(mode, criteria) {
81
+ getResultSetByCriteria(mode, criteria) {
83
82
  const gameMode = this.config.gameModes[mode];
84
83
  if (!gameMode) {
85
84
  throw new Error(`Game mode "${mode}" not found in game config.`);
@@ -105,6 +104,7 @@ var GameConfig = class _GameConfig {
105
104
  };
106
105
 
107
106
  // src/GameMode.ts
107
+ import assert2 from "assert";
108
108
  var GameMode = class {
109
109
  name;
110
110
  reelsAmount;
@@ -123,14 +123,12 @@ var GameMode = class {
123
123
  this.reelSets = opts.reelSets;
124
124
  this.resultSets = opts.resultSets;
125
125
  this.isBonusBuy = opts.isBonusBuy;
126
- if (this.symbolsPerReel.length !== this.reelsAmount) {
127
- throw new Error(
128
- `symbolsPerReel length (${this.symbolsPerReel.length}) must match reelsAmount (${this.reelsAmount}).`
129
- );
130
- }
131
- if (this.resultSets.length == 0) {
132
- throw new Error("GameMode must have at least one ResultSet defined.");
133
- }
126
+ assert2(this.rtp >= 0.9 && this.rtp <= 0.99, "RTP must be between 0.9 and 0.99");
127
+ assert2(
128
+ this.symbolsPerReel.length === this.reelsAmount,
129
+ "symbolsPerReel length must match reelsAmount."
130
+ );
131
+ assert2(this.reelSets.length > 0, "GameMode must have at least one ReelSet defined.");
134
132
  }
135
133
  };
136
134
 
@@ -341,7 +339,6 @@ var ReelGenerator = class {
341
339
  preferStackedSymbols;
342
340
  symbolStacks;
343
341
  symbolQuotas;
344
- outputDir = "";
345
342
  csvPath = "";
346
343
  overrideExisting;
347
344
  rng;
@@ -349,7 +346,6 @@ var ReelGenerator = class {
349
346
  this.id = opts.id;
350
347
  this.symbolWeights = new Map(Object.entries(opts.symbolWeights));
351
348
  this.rowsAmount = opts.rowsAmount || 250;
352
- this.outputDir = opts.outputDir;
353
349
  if (opts.limitSymbolsToReels) this.limitSymbolsToReels = opts.limitSymbolsToReels;
354
350
  this.overrideExisting = opts.overrideExisting || false;
355
351
  this.spaceBetweenSameSymbols = opts.spaceBetweenSameSymbols;
@@ -378,34 +374,16 @@ var ReelGenerator = class {
378
374
  this.rng.setSeed(opts.seed ?? 0);
379
375
  }
380
376
  validateConfig({ config }) {
381
- config.symbols.forEach((symbol) => {
382
- if (!this.symbolWeights.has(symbol.id)) {
377
+ this.symbolWeights.forEach((_, symbol) => {
378
+ if (!config.symbols.has(symbol)) {
383
379
  throw new Error(
384
- [
385
- `Symbol "${symbol.id}" is not defined in the symbol weights of the reel generator ${this.id} for mode ${this.associatedGameModeName}.`,
386
- `Please ensure all symbols have weights defined.
387
- `
388
- ].join(" ")
380
+ `Symbol "${symbol}" of the reel generator ${this.id} for mode ${this.associatedGameModeName} is not defined in the game config`
389
381
  );
390
382
  }
391
383
  });
392
- for (const [symbolId, weight] of this.symbolWeights.entries()) {
393
- if (!config.symbols.has(symbolId)) {
394
- throw new Error(
395
- [
396
- `Symbol "${symbolId}" is defined in the reel generator's symbol weights, but does not exist in the game config.`,
397
- `Please ensure all symbols in the reel generator are defined in the game config.
398
- `
399
- ].join(" ")
400
- );
401
- }
402
- }
403
384
  if (this.limitSymbolsToReels && Object.keys(this.limitSymbolsToReels).length == 0) {
404
385
  this.limitSymbolsToReels = void 0;
405
386
  }
406
- if (this.outputDir === "") {
407
- throw new Error("Output directory must be specified for the ReelGenerator.");
408
- }
409
387
  }
410
388
  isSymbolAllowedOnReel(symbolId, reelIdx) {
411
389
  if (!this.limitSymbolsToReels) return true;
@@ -485,6 +463,15 @@ var ReelGenerator = class {
485
463
  );
486
464
  this.csvPath = filePath;
487
465
  const exists = fs2.existsSync(filePath);
466
+ if (exists && !this.overrideExisting) {
467
+ this.reels = this.parseReelsetCSV(filePath, gameConf);
468
+ return;
469
+ }
470
+ if (!exists && this.symbolWeights.size === 0) {
471
+ throw new Error(
472
+ `Cannot generate reels for generator "${this.id}" of mode "${this.associatedGameModeName}" because the symbol weights are empty.`
473
+ );
474
+ }
488
475
  const reelsAmount = gameMode.reelsAmount;
489
476
  const weightsObj = Object.fromEntries(this.symbolWeights);
490
477
  for (let ridx = 0; ridx < reelsAmount; ridx++) {
@@ -619,16 +606,12 @@ var ReelGenerator = class {
619
606
  }
620
607
  const csvString = csvRows.map((row) => row.join(",")).join("\n");
621
608
  if (isMainThread) {
622
- createDirIfNotExists(this.outputDir);
623
609
  fs2.writeFileSync(filePath, csvString);
624
610
  this.reels = this.parseReelsetCSV(filePath, gameConf);
625
611
  console.log(
626
612
  `Generated reelset ${this.id} for game mode ${this.associatedGameModeName}`
627
613
  );
628
614
  }
629
- if (exists) {
630
- this.reels = this.parseReelsetCSV(filePath, gameConf);
631
- }
632
615
  }
633
616
  /**
634
617
  * Reads a reelset CSV file and returns the reels as arrays of GameSymbols.
@@ -652,11 +635,21 @@ var ReelGenerator = class {
652
635
  reels[ridx].push(symbol);
653
636
  });
654
637
  });
638
+ const reelLengths = reels.map((r) => r.length);
639
+ const uniqueLengths = new Set(reelLengths);
640
+ if (uniqueLengths.size > 1) {
641
+ throw new Error(
642
+ `Inconsistent reel lengths in reelset CSV at ${reelSetPath}: ${[
643
+ ...uniqueLengths
644
+ ].join(", ")}`
645
+ );
646
+ }
655
647
  return reels;
656
648
  }
657
649
  };
658
650
 
659
651
  // src/ResultSet.ts
652
+ import assert3 from "assert";
660
653
  var ResultSet = class {
661
654
  criteria;
662
655
  quota;
@@ -675,16 +668,11 @@ var ResultSet = class {
675
668
  this.forceMaxWin = opts.forceMaxWin;
676
669
  this.forceFreespins = opts.forceFreespins;
677
670
  this.evaluate = opts.evaluate;
678
- if (this.quota < 0 || this.quota > 1) {
679
- throw new Error(`Quota must be a float between 0 and 1, got ${this.quota}.`);
680
- }
681
671
  }
682
672
  static assignCriteriaToSimulations(ctx, gameModeName) {
683
673
  const rng = new RandomNumberGenerator();
684
674
  rng.setSeed(0);
685
- if (!ctx.simRunsAmount) {
686
- throw new Error("Simulation configuration is not set.");
687
- }
675
+ assert3(ctx.simRunsAmount, "Simulation configuration is not set.");
688
676
  const simNums = ctx.simRunsAmount[gameModeName];
689
677
  const resultSets = ctx.gameConfig.config.gameModes[gameModeName]?.resultSets;
690
678
  if (!resultSets || resultSets.length === 0) {
@@ -693,8 +681,12 @@ var ResultSet = class {
693
681
  if (simNums === void 0 || simNums <= 0) {
694
682
  throw new Error(`No simulations configured for game mode "${gameModeName}".`);
695
683
  }
684
+ const totalQuota = resultSets.reduce((sum, rs) => sum + rs.quota, 0);
696
685
  const numberOfSimsForCriteria = Object.fromEntries(
697
- resultSets.map((rs) => [rs.criteria, Math.max(Math.floor(rs.quota * simNums), 1)])
686
+ resultSets.map((rs) => {
687
+ const normalizedQuota = totalQuota > 0 ? rs.quota / totalQuota : 0;
688
+ return [rs.criteria, Math.max(Math.floor(normalizedQuota * simNums), 1)];
689
+ })
698
690
  );
699
691
  let totalSims = Object.values(numberOfSimsForCriteria).reduce(
700
692
  (sum, num) => sum + num,
@@ -1049,13 +1041,6 @@ var GameState = class extends GameConfig {
1049
1041
  this.clearPendingRecords();
1050
1042
  this.state.userData = this.config.userState || {};
1051
1043
  }
1052
- /**
1053
- * Checks if a max win is reached by comparing `wallet.currentWin` to `config.maxWin`.
1054
- *
1055
- * Should be called after `wallet.confirmSpinWin()`.
1056
- */
1057
- isMaxWinTriggered() {
1058
- }
1059
1044
  /**
1060
1045
  * Empties the list of pending records in the recorder.
1061
1046
  */
@@ -1501,7 +1486,7 @@ var Board = class extends GameState {
1501
1486
  return stopPositionsForReels;
1502
1487
  }
1503
1488
  /**
1504
- * Selects a random reelset based on the configured weights for the current game mode.\
1489
+ * Selects a random reel set based on the configured weights of the current result set.\
1505
1490
  * Returns the reels as arrays of GameSymbols.
1506
1491
  */
1507
1492
  getRandomReelset() {
@@ -1516,7 +1501,7 @@ var Board = class extends GameState {
1516
1501
  reelSetId = weightedRandom(weights[this.state.currentSpinType], this.state.rng);
1517
1502
  }
1518
1503
  const reelSet = this.getReelsetById(this.state.currentGameMode, reelSetId);
1519
- return reelSet.reels;
1504
+ return reelSet;
1520
1505
  }
1521
1506
  /**
1522
1507
  * Draws a board using specified reel stops.
@@ -1772,7 +1757,7 @@ var ManywaysWinType = class extends WinType {
1772
1757
  };
1773
1758
 
1774
1759
  // src/optimizer/OptimizationConditions.ts
1775
- import assert2 from "assert";
1760
+ import assert4 from "assert";
1776
1761
  var OptimizationConditions = class {
1777
1762
  rtp;
1778
1763
  avgWin;
@@ -1783,14 +1768,14 @@ var OptimizationConditions = class {
1783
1768
  constructor(opts) {
1784
1769
  let { rtp, avgWin, hitRate, searchConditions, priority } = opts;
1785
1770
  if (rtp == void 0 || rtp === "x") {
1786
- assert2(avgWin !== void 0 && hitRate !== void 0, "If RTP is not specified, hit-rate (hr) and average win amount (av_win) must be given.");
1771
+ assert4(avgWin !== void 0 && hitRate !== void 0, "If RTP is not specified, hit-rate (hr) and average win amount (av_win) must be given.");
1787
1772
  rtp = Math.round(avgWin / Number(hitRate) * 1e5) / 1e5;
1788
1773
  }
1789
1774
  let noneCount = 0;
1790
1775
  for (const val of [rtp, avgWin, hitRate]) {
1791
1776
  if (val === void 0) noneCount++;
1792
1777
  }
1793
- assert2(noneCount <= 1, "Invalid combination of optimization conditions.");
1778
+ assert4(noneCount <= 1, "Invalid combination of optimization conditions.");
1794
1779
  this.searchRange = [-1, -1];
1795
1780
  this.forceSearch = {};
1796
1781
  if (typeof searchConditions === "number") {
@@ -1871,7 +1856,7 @@ var OptimizationParameters = class _OptimizationParameters {
1871
1856
  // src/Simulation.ts
1872
1857
  import fs3 from "fs";
1873
1858
  import path2 from "path";
1874
- import assert3 from "assert";
1859
+ import assert5 from "assert";
1875
1860
  import zlib from "zlib";
1876
1861
  import { buildSync } from "esbuild";
1877
1862
  import { Worker, isMainThread as isMainThread2, parentPort, workerData } from "worker_threads";
@@ -1895,7 +1880,7 @@ var Simulation = class _Simulation {
1895
1880
  this.library = /* @__PURE__ */ new Map();
1896
1881
  this.records = [];
1897
1882
  const gameModeKeys = Object.keys(this.gameConfig.config.gameModes);
1898
- assert3(
1883
+ assert5(
1899
1884
  Object.values(this.gameConfig.config.gameModes).map((m) => gameModeKeys.includes(m.name)).every((v) => v === true),
1900
1885
  "Game mode name must match its key in the gameModes object."
1901
1886
  );
@@ -2246,22 +2231,25 @@ var SimulationContext = class extends Board {
2246
2231
  this.state.currentGameMode = mode;
2247
2232
  this.state.currentSimulationId = simId;
2248
2233
  this.state.isCriteriaMet = false;
2234
+ const resultSet = this.getResultSetByCriteria(this.state.currentGameMode, criteria);
2249
2235
  while (!this.state.isCriteriaMet) {
2250
2236
  this.actualSims++;
2251
2237
  this.resetSimulation();
2252
- const resultSet = this.getGameModeCriteria(this.state.currentGameMode, criteria);
2253
2238
  this.state.currentResultSet = resultSet;
2254
2239
  this.state.book.criteria = resultSet.criteria;
2255
2240
  this.handleGameFlow();
2256
2241
  if (resultSet.meetsCriteria(this)) {
2257
2242
  this.state.isCriteriaMet = true;
2258
- this.config.hooks.onSimulationAccepted?.(this);
2259
- this.record({
2260
- criteria: resultSet.criteria
2261
- });
2262
2243
  }
2263
2244
  }
2264
2245
  this.wallet.confirmWins(this);
2246
+ if (this.state.book.getPayout() >= this.config.maxWinX) {
2247
+ this.state.triggeredMaxWin = true;
2248
+ }
2249
+ this.record({
2250
+ criteria: resultSet.criteria
2251
+ });
2252
+ this.config.hooks.onSimulationAccepted?.(this);
2265
2253
  this.confirmRecords();
2266
2254
  parentPort?.postMessage({
2267
2255
  type: "complete",
@@ -2298,7 +2286,7 @@ var SimulationContext = class extends Board {
2298
2286
  // src/analysis/index.ts
2299
2287
  import fs4 from "fs";
2300
2288
  import path3 from "path";
2301
- import assert4 from "assert";
2289
+ import assert6 from "assert";
2302
2290
 
2303
2291
  // src/analysis/utils.ts
2304
2292
  function parseLookupTable(content) {
@@ -2463,7 +2451,7 @@ var Analysis = class {
2463
2451
  booksJsonlCompressed
2464
2452
  };
2465
2453
  for (const p of Object.values(paths[modeStr])) {
2466
- assert4(
2454
+ assert6(
2467
2455
  fs4.existsSync(p),
2468
2456
  `File "${p}" does not exist. Run optimization to auto-create it.`
2469
2457
  );
@@ -2535,7 +2523,7 @@ var Analysis = class {
2535
2523
  }
2536
2524
  getGameModeConfig(mode) {
2537
2525
  const config = this.gameConfig.gameModes[mode];
2538
- assert4(config, `Game mode "${mode}" not found in game config`);
2526
+ assert6(config, `Game mode "${mode}" not found in game config`);
2539
2527
  return config;
2540
2528
  }
2541
2529
  };
@@ -2639,7 +2627,7 @@ function makeSetupFile(optimizer, gameMode) {
2639
2627
  // src/optimizer/index.ts
2640
2628
  import { spawn } from "child_process";
2641
2629
  import path6 from "path";
2642
- import assert5 from "assert";
2630
+ import assert7 from "assert";
2643
2631
  import { isMainThread as isMainThread4 } from "worker_threads";
2644
2632
  var Optimizer = class {
2645
2633
  gameConfig;
@@ -2683,7 +2671,7 @@ var Optimizer = class {
2683
2671
  }
2684
2672
  }
2685
2673
  const criteria = configMode.resultSets.map((r) => r.criteria);
2686
- assert5(
2674
+ assert7(
2687
2675
  conditions.every((c) => criteria.includes(c)),
2688
2676
  `Not all ResultSet criteria in game mode "${k}" are defined as optimization conditions.`
2689
2677
  );
@@ -2695,7 +2683,7 @@ var Optimizer = class {
2695
2683
  }
2696
2684
  gameModeRtp = Math.round(gameModeRtp * 1e3) / 1e3;
2697
2685
  paramRtp = Math.round(paramRtp * 1e3) / 1e3;
2698
- assert5(
2686
+ assert7(
2699
2687
  gameModeRtp === paramRtp,
2700
2688
  `Sum of all RTP conditions (${paramRtp}) does not match the game mode RTP (${gameModeRtp}) in game mode "${k}".`
2701
2689
  );