@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.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 {
@@ -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,
@@ -1765,7 +1757,7 @@ var ManywaysWinType = class extends WinType {
1765
1757
  };
1766
1758
 
1767
1759
  // src/optimizer/OptimizationConditions.ts
1768
- import assert2 from "assert";
1760
+ import assert4 from "assert";
1769
1761
  var OptimizationConditions = class {
1770
1762
  rtp;
1771
1763
  avgWin;
@@ -1776,14 +1768,14 @@ var OptimizationConditions = class {
1776
1768
  constructor(opts) {
1777
1769
  let { rtp, avgWin, hitRate, searchConditions, priority } = opts;
1778
1770
  if (rtp == void 0 || rtp === "x") {
1779
- 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.");
1780
1772
  rtp = Math.round(avgWin / Number(hitRate) * 1e5) / 1e5;
1781
1773
  }
1782
1774
  let noneCount = 0;
1783
1775
  for (const val of [rtp, avgWin, hitRate]) {
1784
1776
  if (val === void 0) noneCount++;
1785
1777
  }
1786
- assert2(noneCount <= 1, "Invalid combination of optimization conditions.");
1778
+ assert4(noneCount <= 1, "Invalid combination of optimization conditions.");
1787
1779
  this.searchRange = [-1, -1];
1788
1780
  this.forceSearch = {};
1789
1781
  if (typeof searchConditions === "number") {
@@ -1864,7 +1856,7 @@ var OptimizationParameters = class _OptimizationParameters {
1864
1856
  // src/Simulation.ts
1865
1857
  import fs3 from "fs";
1866
1858
  import path2 from "path";
1867
- import assert3 from "assert";
1859
+ import assert5 from "assert";
1868
1860
  import zlib from "zlib";
1869
1861
  import { buildSync } from "esbuild";
1870
1862
  import { Worker, isMainThread as isMainThread2, parentPort, workerData } from "worker_threads";
@@ -1888,7 +1880,7 @@ var Simulation = class _Simulation {
1888
1880
  this.library = /* @__PURE__ */ new Map();
1889
1881
  this.records = [];
1890
1882
  const gameModeKeys = Object.keys(this.gameConfig.config.gameModes);
1891
- assert3(
1883
+ assert5(
1892
1884
  Object.values(this.gameConfig.config.gameModes).map((m) => gameModeKeys.includes(m.name)).every((v) => v === true),
1893
1885
  "Game mode name must match its key in the gameModes object."
1894
1886
  );
@@ -2294,7 +2286,7 @@ var SimulationContext = class extends Board {
2294
2286
  // src/analysis/index.ts
2295
2287
  import fs4 from "fs";
2296
2288
  import path3 from "path";
2297
- import assert4 from "assert";
2289
+ import assert6 from "assert";
2298
2290
 
2299
2291
  // src/analysis/utils.ts
2300
2292
  function parseLookupTable(content) {
@@ -2459,7 +2451,7 @@ var Analysis = class {
2459
2451
  booksJsonlCompressed
2460
2452
  };
2461
2453
  for (const p of Object.values(paths[modeStr])) {
2462
- assert4(
2454
+ assert6(
2463
2455
  fs4.existsSync(p),
2464
2456
  `File "${p}" does not exist. Run optimization to auto-create it.`
2465
2457
  );
@@ -2531,7 +2523,7 @@ var Analysis = class {
2531
2523
  }
2532
2524
  getGameModeConfig(mode) {
2533
2525
  const config = this.gameConfig.gameModes[mode];
2534
- assert4(config, `Game mode "${mode}" not found in game config`);
2526
+ assert6(config, `Game mode "${mode}" not found in game config`);
2535
2527
  return config;
2536
2528
  }
2537
2529
  };
@@ -2635,7 +2627,7 @@ function makeSetupFile(optimizer, gameMode) {
2635
2627
  // src/optimizer/index.ts
2636
2628
  import { spawn } from "child_process";
2637
2629
  import path6 from "path";
2638
- import assert5 from "assert";
2630
+ import assert7 from "assert";
2639
2631
  import { isMainThread as isMainThread4 } from "worker_threads";
2640
2632
  var Optimizer = class {
2641
2633
  gameConfig;
@@ -2679,7 +2671,7 @@ var Optimizer = class {
2679
2671
  }
2680
2672
  }
2681
2673
  const criteria = configMode.resultSets.map((r) => r.criteria);
2682
- assert5(
2674
+ assert7(
2683
2675
  conditions.every((c) => criteria.includes(c)),
2684
2676
  `Not all ResultSet criteria in game mode "${k}" are defined as optimization conditions.`
2685
2677
  );
@@ -2691,7 +2683,7 @@ var Optimizer = class {
2691
2683
  }
2692
2684
  gameModeRtp = Math.round(gameModeRtp * 1e3) / 1e3;
2693
2685
  paramRtp = Math.round(paramRtp * 1e3) / 1e3;
2694
- assert5(
2686
+ assert7(
2695
2687
  gameModeRtp === paramRtp,
2696
2688
  `Sum of all RTP conditions (${paramRtp}) does not match the game mode RTP (${gameModeRtp}) in game mode "${k}".`
2697
2689
  );