@slot-engine/core 0.1.1 → 0.1.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.
package/dist/index.mjs CHANGED
@@ -24,7 +24,8 @@ function createGameConfig(opts) {
24
24
  [SPIN_TYPE.BASE_GAME]: getAnticipationTrigger(SPIN_TYPE.BASE_GAME),
25
25
  [SPIN_TYPE.FREE_SPINS]: getAnticipationTrigger(SPIN_TYPE.FREE_SPINS)
26
26
  },
27
- outputDir: "__build__"
27
+ outputDir: "__build__",
28
+ rootDir: opts.rootDir || process.cwd()
28
29
  };
29
30
  }
30
31
 
@@ -1148,6 +1149,7 @@ var Book = class _Book {
1148
1149
  freespinsWins = 0;
1149
1150
  constructor(opts) {
1150
1151
  this.id = opts.id;
1152
+ this.criteria = opts.criteria;
1151
1153
  }
1152
1154
  /**
1153
1155
  * Intended for internal use only.
@@ -1469,10 +1471,14 @@ Simulating game mode: ${mode}`);
1469
1471
  const simNumsToCriteria = ResultSet.assignCriteriaToSimulations(this, mode);
1470
1472
  await this.spawnWorkersForGameMode({ mode, simNumsToCriteria });
1471
1473
  createDirIfNotExists(
1472
- path.join(process.cwd(), this.gameConfig.outputDir, "optimization_files")
1474
+ path.join(
1475
+ this.gameConfig.rootDir,
1476
+ this.gameConfig.outputDir,
1477
+ "optimization_files"
1478
+ )
1473
1479
  );
1474
1480
  createDirIfNotExists(
1475
- path.join(process.cwd(), this.gameConfig.outputDir, "publish_files")
1481
+ path.join(this.gameConfig.rootDir, this.gameConfig.outputDir, "publish_files")
1476
1482
  );
1477
1483
  this.writeLookupTableCSV(mode);
1478
1484
  this.writeLookupTableSegmentedCSV(mode);
@@ -1525,7 +1531,7 @@ Simulating game mode: ${mode}`);
1525
1531
  await Promise.all(
1526
1532
  simRangesPerChunk.map(([simStart, simEnd], index) => {
1527
1533
  return this.callWorker({
1528
- basePath: this.gameConfig.outputDir,
1534
+ basePath: path.join(this.gameConfig.rootDir, this.gameConfig.outputDir),
1529
1535
  mode,
1530
1536
  simStart,
1531
1537
  simEnd,
@@ -1548,7 +1554,7 @@ Simulating game mode: ${mode}`);
1548
1554
  }
1549
1555
  }
1550
1556
  return new Promise((resolve, reject) => {
1551
- const scriptPath = path.join(process.cwd(), basePath, TEMP_FILENAME);
1557
+ const scriptPath = path.join(basePath, TEMP_FILENAME);
1552
1558
  const worker = new Worker(scriptPath, {
1553
1559
  workerData: {
1554
1560
  mode,
@@ -1598,10 +1604,10 @@ Simulating game mode: ${mode}`);
1598
1604
  ctx.state.currentGameMode,
1599
1605
  criteria
1600
1606
  );
1607
+ ctx.state.currentResultSet = resultSet;
1601
1608
  while (!ctx.state.isCriteriaMet) {
1602
1609
  this.actualSims++;
1603
1610
  this.resetSimulation(ctx);
1604
- ctx.state.currentResultSet = resultSet;
1605
1611
  this.handleGameFlow(ctx);
1606
1612
  if (resultSet.meetsCriteria(ctx)) {
1607
1613
  ctx.state.isCriteriaMet = true;
@@ -1676,10 +1682,19 @@ Simulating game mode: ${mode}`);
1676
1682
  }
1677
1683
  rows.sort((a, b) => Number(a.split(",")[0]) - Number(b.split(",")[0]));
1678
1684
  let outputFileName = `lookUpTable_${gameMode}.csv`;
1679
- let outputFilePath = path.join(this.gameConfig.outputDir, outputFileName);
1685
+ let outputFilePath = path.join(
1686
+ this.gameConfig.rootDir,
1687
+ this.gameConfig.outputDir,
1688
+ outputFileName
1689
+ );
1680
1690
  writeFile(outputFilePath, rows.join("\n"));
1681
1691
  outputFileName = `lookUpTable_${gameMode}_0.csv`;
1682
- outputFilePath = path.join(this.gameConfig.outputDir, "publish_files", outputFileName);
1692
+ outputFilePath = path.join(
1693
+ this.gameConfig.rootDir,
1694
+ this.gameConfig.outputDir,
1695
+ "publish_files",
1696
+ outputFileName
1697
+ );
1683
1698
  writeFile(outputFilePath, rows.join("\n"));
1684
1699
  return outputFilePath;
1685
1700
  }
@@ -1693,20 +1708,28 @@ Simulating game mode: ${mode}`);
1693
1708
  }
1694
1709
  rows.sort((a, b) => Number(a.split(",")[0]) - Number(b.split(",")[0]));
1695
1710
  const outputFileName = `lookUpTableSegmented_${gameMode}.csv`;
1696
- const outputFilePath = path.join(this.gameConfig.outputDir, outputFileName);
1711
+ const outputFilePath = path.join(
1712
+ this.gameConfig.rootDir,
1713
+ this.gameConfig.outputDir,
1714
+ outputFileName
1715
+ );
1697
1716
  writeFile(outputFilePath, rows.join("\n"));
1698
1717
  return outputFilePath;
1699
1718
  }
1700
1719
  writeRecords(gameMode) {
1701
1720
  const outputFileName = `force_record_${gameMode}.json`;
1702
- const outputFilePath = path.join(this.gameConfig.outputDir, outputFileName);
1721
+ const outputFilePath = path.join(
1722
+ this.gameConfig.rootDir,
1723
+ this.gameConfig.outputDir,
1724
+ outputFileName
1725
+ );
1703
1726
  writeFile(outputFilePath, JSON.stringify(this.recorder.records, null, 2));
1704
1727
  if (this.debug) this.logSymbolOccurrences();
1705
1728
  return outputFilePath;
1706
1729
  }
1707
1730
  writeIndexJson() {
1708
1731
  const outputFilePath = path.join(
1709
- process.cwd(),
1732
+ this.gameConfig.rootDir,
1710
1733
  this.gameConfig.outputDir,
1711
1734
  "publish_files",
1712
1735
  "index.json"
@@ -1725,7 +1748,11 @@ Simulating game mode: ${mode}`);
1725
1748
  }
1726
1749
  async writeBooksJson(gameMode) {
1727
1750
  const outputFileName = `books_${gameMode}.jsonl`;
1728
- const outputFilePath = path.join(this.gameConfig.outputDir, outputFileName);
1751
+ const outputFilePath = path.join(
1752
+ this.gameConfig.rootDir,
1753
+ this.gameConfig.outputDir,
1754
+ outputFileName
1755
+ );
1729
1756
  const books = Array.from(this.library.values()).map((b) => b.serialize()).map((b) => ({
1730
1757
  id: b.id,
1731
1758
  payoutMultiplier: b.payout,
@@ -1735,7 +1762,7 @@ Simulating game mode: ${mode}`);
1735
1762
  writeFile(outputFilePath, contents);
1736
1763
  const compressedFileName = `books_${gameMode}.jsonl.zst`;
1737
1764
  const compressedFilePath = path.join(
1738
- process.cwd(),
1765
+ this.gameConfig.rootDir,
1739
1766
  this.gameConfig.outputDir,
1740
1767
  "publish_files",
1741
1768
  compressedFileName
@@ -1773,13 +1800,21 @@ Simulating game mode: ${mode}`);
1773
1800
  * Compiles user configured game to JS for use in different Node processes
1774
1801
  */
1775
1802
  preprocessFiles() {
1776
- const builtFilePath = path.join(this.gameConfig.outputDir, TEMP_FILENAME);
1803
+ const builtFilePath = path.join(
1804
+ this.gameConfig.rootDir,
1805
+ this.gameConfig.outputDir,
1806
+ TEMP_FILENAME
1807
+ );
1777
1808
  fs2.rmSync(builtFilePath, { force: true });
1778
1809
  buildSync({
1779
- entryPoints: [process.cwd()],
1810
+ entryPoints: [this.gameConfig.rootDir],
1780
1811
  bundle: true,
1781
1812
  platform: "node",
1782
- outfile: path.join(this.gameConfig.outputDir, TEMP_FILENAME),
1813
+ outfile: path.join(
1814
+ this.gameConfig.rootDir,
1815
+ this.gameConfig.outputDir,
1816
+ TEMP_FILENAME
1817
+ ),
1783
1818
  external: ["esbuild"]
1784
1819
  });
1785
1820
  }
@@ -1890,6 +1925,18 @@ function parseLookupTable(content) {
1890
1925
  }
1891
1926
  return lut;
1892
1927
  }
1928
+ function parseLookupTableSegmented(content) {
1929
+ const lines = content.trim().split("\n");
1930
+ const lut = [];
1931
+ for (const line of lines) {
1932
+ const [indexStr, criteria, weightStr, payoutStr] = line.split(",");
1933
+ const index = parseInt(indexStr.trim());
1934
+ const weight = parseInt(weightStr.trim());
1935
+ const payout = parseFloat(payoutStr.trim());
1936
+ lut.push([index, criteria, weight, payout]);
1937
+ }
1938
+ return lut;
1939
+ }
1893
1940
  function getTotalLutWeight(lut) {
1894
1941
  return lut.reduce((sum, [, weight]) => sum + weight, 0);
1895
1942
  }
@@ -2002,7 +2049,7 @@ var Analysis = class {
2002
2049
  console.log("Analysis complete. Files written to build directory.");
2003
2050
  }
2004
2051
  getPathsForModes(gameModes) {
2005
- const rootPath = process.cwd();
2052
+ const rootPath = this.gameConfig.rootDir;
2006
2053
  const paths = {};
2007
2054
  for (const modeStr of gameModes) {
2008
2055
  const lut = path2.join(
@@ -2074,7 +2121,7 @@ var Analysis = class {
2074
2121
  });
2075
2122
  }
2076
2123
  writeJsonFile(
2077
- path2.join(process.cwd(), this.gameConfig.outputDir, "stats_summary.json"),
2124
+ path2.join(this.gameConfig.rootDir, this.gameConfig.outputDir, "stats_summary.json"),
2078
2125
  stats
2079
2126
  );
2080
2127
  }
@@ -2103,35 +2150,76 @@ var Analysis = class {
2103
2150
  ];
2104
2151
  const payoutRanges = {};
2105
2152
  for (const modeStr of gameModes) {
2106
- payoutRanges[modeStr] = {};
2153
+ payoutRanges[modeStr] = { overall: {}, criteria: {} };
2107
2154
  const lutOptimized = parseLookupTable(
2108
2155
  fs3.readFileSync(this.filePaths[modeStr].lutOptimized, "utf-8")
2109
2156
  );
2157
+ const lutSegmented = parseLookupTableSegmented(
2158
+ fs3.readFileSync(this.filePaths[modeStr].lutSegmented, "utf-8")
2159
+ );
2110
2160
  lutOptimized.forEach(([, , p]) => {
2111
2161
  const payout = p / 100;
2112
2162
  for (const [min, max] of winRanges) {
2113
2163
  if (payout >= min && payout <= max) {
2114
2164
  const rangeKey = `${min}-${max}`;
2115
- if (!payoutRanges[modeStr][rangeKey]) {
2116
- payoutRanges[modeStr][rangeKey] = 0;
2165
+ if (!payoutRanges[modeStr].overall[rangeKey]) {
2166
+ payoutRanges[modeStr].overall[rangeKey] = 0;
2167
+ }
2168
+ payoutRanges[modeStr].overall[rangeKey] += 1;
2169
+ break;
2170
+ }
2171
+ }
2172
+ });
2173
+ lutSegmented.forEach(([, criteria, bp, fsp]) => {
2174
+ const basePayout = bp / 100;
2175
+ const freeSpinPayout = fsp / 100;
2176
+ const payout = basePayout + freeSpinPayout;
2177
+ for (const [min, max] of winRanges) {
2178
+ if (payout >= min && payout <= max) {
2179
+ const rangeKey = `${min}-${max}`;
2180
+ if (!payoutRanges[modeStr].overall[rangeKey]) {
2181
+ payoutRanges[modeStr].overall[rangeKey] = 0;
2182
+ }
2183
+ payoutRanges[modeStr].overall[rangeKey] += 1;
2184
+ if (!payoutRanges[modeStr].criteria[criteria]) {
2185
+ payoutRanges[modeStr].criteria[criteria] = {};
2117
2186
  }
2118
- payoutRanges[modeStr][rangeKey] += 1;
2187
+ if (!payoutRanges[modeStr].criteria[criteria][rangeKey]) {
2188
+ payoutRanges[modeStr].criteria[criteria][rangeKey] = 0;
2189
+ }
2190
+ payoutRanges[modeStr].criteria[criteria][rangeKey] += 1;
2119
2191
  break;
2120
2192
  }
2121
2193
  }
2122
2194
  });
2123
- const orderedRanges = {};
2124
- Object.keys(payoutRanges[modeStr]).sort((a, b) => {
2195
+ const orderedOverall = {};
2196
+ Object.keys(payoutRanges[modeStr].overall).sort((a, b) => {
2125
2197
  const [aMin] = a.split("-").map(Number);
2126
2198
  const [bMin] = b.split("-").map(Number);
2127
2199
  return aMin - bMin;
2128
2200
  }).forEach((key) => {
2129
- orderedRanges[key] = payoutRanges[modeStr][key];
2201
+ orderedOverall[key] = payoutRanges[modeStr].overall[key];
2202
+ });
2203
+ const orderedCriteria = {};
2204
+ Object.keys(payoutRanges[modeStr].criteria).forEach((crit) => {
2205
+ const critMap = payoutRanges[modeStr].criteria[crit];
2206
+ const orderedCritMap = {};
2207
+ Object.keys(critMap).sort((a, b) => {
2208
+ const [aMin] = a.split("-").map(Number);
2209
+ const [bMin] = b.split("-").map(Number);
2210
+ return aMin - bMin;
2211
+ }).forEach((key) => {
2212
+ orderedCritMap[key] = critMap[key];
2213
+ });
2214
+ orderedCriteria[crit] = orderedCritMap;
2130
2215
  });
2131
- payoutRanges[modeStr] = orderedRanges;
2216
+ payoutRanges[modeStr] = {
2217
+ overall: orderedOverall,
2218
+ criteria: {}
2219
+ };
2132
2220
  }
2133
2221
  writeJsonFile(
2134
- path2.join(process.cwd(), this.gameConfig.outputDir, "stats_payouts.json"),
2222
+ path2.join(this.gameConfig.rootDir, this.gameConfig.outputDir, "stats_payouts.json"),
2135
2223
  payoutRanges
2136
2224
  );
2137
2225
  }
@@ -2193,7 +2281,7 @@ function makeMathConfig(optimizer, opts = {}) {
2193
2281
  }))
2194
2282
  };
2195
2283
  if (writeToFile) {
2196
- const outPath = path3.join(process.cwd(), game.outputDir, "math_config.json");
2284
+ const outPath = path3.join(game.rootDir, game.outputDir, "math_config.json");
2197
2285
  writeJsonFile(outPath, config);
2198
2286
  }
2199
2287
  return config;
@@ -2236,7 +2324,7 @@ function makeSetupFile(optimizer, gameMode) {
2236
2324
  `;
2237
2325
  content += `simulation_trials;${params.simulationTrials}
2238
2326
  `;
2239
- content += `user_game_build_path;${path4.join(process.cwd(), gameConfig.outputDir)}
2327
+ content += `user_game_build_path;${path4.join(gameConfig.rootDir, gameConfig.outputDir)}
2240
2328
  `;
2241
2329
  content += `pmb_rtp;${params.pmbRtp}
2242
2330
  `;
@@ -2376,10 +2464,10 @@ var Optimizer = class {
2376
2464
  const conditions = Object.keys(mode.conditions);
2377
2465
  const scalings = Object.keys(mode.scaling);
2378
2466
  const parameters = Object.keys(mode.parameters);
2379
- for (const condition of conditions) {
2380
- if (!configMode.resultSets.find((r) => r.criteria === condition)) {
2467
+ for (const rs of configMode.resultSets) {
2468
+ if (!conditions.includes(rs.criteria)) {
2381
2469
  throw new Error(
2382
- `Condition "${condition}" defined in optimizer config for game mode "${k}" does not exist as criteria in any ResultSet of the same game mode.`
2470
+ `ResultSet criteria "${rs.criteria}" in game mode "${k}" does not have a corresponding optimization condition defined.`
2383
2471
  );
2384
2472
  }
2385
2473
  }
@@ -2410,9 +2498,9 @@ var Optimizer = class {
2410
2498
  }
2411
2499
  };
2412
2500
  async function rustProgram(...args) {
2501
+ console.log("Starting Rust optimizer. This may take a while...");
2413
2502
  return new Promise((resolve, reject) => {
2414
- const task = spawn("cargo", ["run", "--release", ...args], {
2415
- shell: true,
2503
+ const task = spawn("cargo", ["run", "-q", "--release", ...args], {
2416
2504
  cwd: path5.join(__dirname, "./optimizer-rust"),
2417
2505
  stdio: "pipe"
2418
2506
  });
@@ -2878,8 +2966,6 @@ var ClusterWinType = class extends WinType {
2878
2966
 
2879
2967
  // src/win-types/ManywaysWinType.ts
2880
2968
  var ManywaysWinType = class extends WinType {
2881
- _checked = [];
2882
- _checkedWilds = [];
2883
2969
  constructor(opts) {
2884
2970
  super(opts);
2885
2971
  }
@@ -2977,12 +3063,6 @@ var ManywaysWinType = class extends WinType {
2977
3063
  getWayLength(symbolList) {
2978
3064
  return Math.max(...Object.keys(symbolList).map((k) => parseInt(k, 10))) + 1;
2979
3065
  }
2980
- isChecked(ridx, sidx) {
2981
- return !!this._checked.find((c) => c.reel === ridx && c.row === sidx);
2982
- }
2983
- isCheckedWild(ridx, sidx) {
2984
- return !!this._checkedWilds.find((c) => c.reel === ridx && c.row === sidx);
2985
- }
2986
3066
  };
2987
3067
 
2988
3068
  // src/reel-set/GeneratedReelSet.ts
@@ -3186,6 +3266,7 @@ var GeneratedReelSet = class extends ReelSet {
3186
3266
  );
3187
3267
  }
3188
3268
  const filePath = path7.join(
3269
+ config.rootDir,
3189
3270
  config.outputDir,
3190
3271
  `reels_${this.associatedGameModeName}-${this.id}.csv`
3191
3272
  );