@slot-engine/core 0.1.2 → 0.1.4

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
@@ -34,6 +34,7 @@ import fs2 from "fs";
34
34
  import path from "path";
35
35
  import assert6 from "assert";
36
36
  import zlib from "zlib";
37
+ import readline2 from "readline";
37
38
  import { buildSync } from "esbuild";
38
39
  import { Worker, isMainThread, parentPort, workerData } from "worker_threads";
39
40
 
@@ -201,6 +202,7 @@ var RandomNumberGenerator = class {
201
202
 
202
203
  // utils.ts
203
204
  import fs from "fs";
205
+ import readline from "readline";
204
206
  function createDirIfNotExists(dirPath) {
205
207
  if (!fs.existsSync(dirPath)) {
206
208
  fs.mkdirSync(dirPath, { recursive: true });
@@ -232,6 +234,29 @@ var JSONL = class {
232
234
  static parse(jsonl) {
233
235
  return jsonl.split("\n").filter((s) => s !== "").map((str) => JSON.parse(str));
234
236
  }
237
+ static async convertToJson(inputPath, outputPath) {
238
+ const writeStream = fs.createWriteStream(outputPath, { encoding: "utf-8" });
239
+ writeStream.write("[\n");
240
+ const rl = readline.createInterface({
241
+ input: fs.createReadStream(inputPath),
242
+ crlfDelay: Infinity
243
+ });
244
+ let isFirst = true;
245
+ for await (const line of rl) {
246
+ if (line.trim() === "") continue;
247
+ if (!isFirst) {
248
+ writeStream.write(",\n");
249
+ }
250
+ writeStream.write(line);
251
+ isFirst = false;
252
+ }
253
+ writeStream.write("\n]");
254
+ writeStream.end();
255
+ return new Promise((resolve, reject) => {
256
+ writeStream.on("finish", () => resolve());
257
+ writeStream.on("error", reject);
258
+ });
259
+ }
235
260
  };
236
261
 
237
262
  // src/result-set/index.ts
@@ -1412,8 +1437,10 @@ var Wallet = class {
1412
1437
  };
1413
1438
 
1414
1439
  // src/simulation/index.ts
1440
+ import { pipeline } from "stream/promises";
1415
1441
  var completedSimulations = 0;
1416
1442
  var TEMP_FILENAME = "__temp_compiled_src_IGNORE.js";
1443
+ var TEMP_FOLDER = "temp_files";
1417
1444
  var Simulation = class {
1418
1445
  gameConfigOpts;
1419
1446
  gameConfig;
@@ -1422,15 +1449,15 @@ var Simulation = class {
1422
1449
  debug = false;
1423
1450
  actualSims = 0;
1424
1451
  library;
1425
- recorder;
1426
1452
  wallet;
1453
+ recordsWriteStream;
1454
+ hasWrittenRecord = false;
1427
1455
  constructor(opts, gameConfigOpts) {
1428
1456
  this.gameConfig = createGameConfig(gameConfigOpts);
1429
1457
  this.gameConfigOpts = gameConfigOpts;
1430
1458
  this.simRunsAmount = opts.simRunsAmount || {};
1431
1459
  this.concurrency = (opts.concurrency || 6) >= 2 ? opts.concurrency || 6 : 2;
1432
1460
  this.library = /* @__PURE__ */ new Map();
1433
- this.recorder = new Recorder();
1434
1461
  this.wallet = new Wallet();
1435
1462
  const gameModeKeys = Object.keys(this.gameConfig.gameModes);
1436
1463
  assert6(
@@ -1456,7 +1483,7 @@ var Simulation = class {
1456
1483
  completedSimulations = 0;
1457
1484
  this.wallet = new Wallet();
1458
1485
  this.library = /* @__PURE__ */ new Map();
1459
- this.recorder = new Recorder();
1486
+ this.hasWrittenRecord = false;
1460
1487
  debugDetails[mode] = {};
1461
1488
  console.log(`
1462
1489
  Simulating game mode: ${mode}`);
@@ -1468,8 +1495,59 @@ Simulating game mode: ${mode}`);
1468
1495
  `Tried to simulate game mode "${mode}", but it's not configured in the game config.`
1469
1496
  );
1470
1497
  }
1498
+ const booksPath = path.join(
1499
+ this.gameConfig.rootDir,
1500
+ this.gameConfig.outputDir,
1501
+ `books_${mode}.jsonl`
1502
+ );
1503
+ const tempRecordsPath = path.join(
1504
+ this.gameConfig.rootDir,
1505
+ this.gameConfig.outputDir,
1506
+ TEMP_FOLDER,
1507
+ `temp_records_${mode}.jsonl`
1508
+ );
1509
+ createDirIfNotExists(
1510
+ path.join(this.gameConfig.rootDir, this.gameConfig.outputDir)
1511
+ );
1512
+ createDirIfNotExists(
1513
+ path.join(this.gameConfig.rootDir, this.gameConfig.outputDir, TEMP_FOLDER)
1514
+ );
1515
+ this.recordsWriteStream = fs2.createWriteStream(tempRecordsPath);
1471
1516
  const simNumsToCriteria = ResultSet.assignCriteriaToSimulations(this, mode);
1472
1517
  await this.spawnWorkersForGameMode({ mode, simNumsToCriteria });
1518
+ const finalBookStream = fs2.createWriteStream(booksPath);
1519
+ const numSims = Object.keys(simNumsToCriteria).length;
1520
+ const chunks = this.getSimRangesForChunks(numSims, this.concurrency);
1521
+ let isFirstChunk = true;
1522
+ for (let i = 0; i < chunks.length; i++) {
1523
+ const tempBookPath = path.join(
1524
+ this.gameConfig.rootDir,
1525
+ this.gameConfig.outputDir,
1526
+ TEMP_FOLDER,
1527
+ `temp_books_${mode}_${i}.jsonl`
1528
+ );
1529
+ if (fs2.existsSync(tempBookPath)) {
1530
+ if (!isFirstChunk) {
1531
+ finalBookStream.write("\n");
1532
+ }
1533
+ const content = fs2.createReadStream(tempBookPath);
1534
+ for await (const chunk of content) {
1535
+ finalBookStream.write(chunk);
1536
+ }
1537
+ fs2.rmSync(tempBookPath);
1538
+ isFirstChunk = false;
1539
+ }
1540
+ }
1541
+ finalBookStream.end();
1542
+ await new Promise((resolve) => finalBookStream.on("finish", resolve));
1543
+ if (this.recordsWriteStream) {
1544
+ await new Promise((resolve) => {
1545
+ this.recordsWriteStream.end(() => {
1546
+ resolve();
1547
+ });
1548
+ });
1549
+ this.recordsWriteStream = void 0;
1550
+ }
1473
1551
  createDirIfNotExists(
1474
1552
  path.join(
1475
1553
  this.gameConfig.rootDir,
@@ -1480,11 +1558,13 @@ Simulating game mode: ${mode}`);
1480
1558
  createDirIfNotExists(
1481
1559
  path.join(this.gameConfig.rootDir, this.gameConfig.outputDir, "publish_files")
1482
1560
  );
1561
+ console.log(`Writing final files for game mode: ${mode} ...`);
1483
1562
  this.writeLookupTableCSV(mode);
1484
1563
  this.writeLookupTableSegmentedCSV(mode);
1485
1564
  this.writeRecords(mode);
1486
1565
  await this.writeBooksJson(mode);
1487
1566
  this.writeIndexJson();
1567
+ console.log(`Mode ${mode} done!`);
1488
1568
  debugDetails[mode].rtp = this.wallet.getCumulativeWins() / (runs * this.gameConfig.gameModes[mode].cost);
1489
1569
  debugDetails[mode].wins = this.wallet.getCumulativeWins();
1490
1570
  debugDetails[mode].winsPerSpinType = this.wallet.getCumulativeWinsPerSpinType();
@@ -1563,6 +1643,12 @@ Simulating game mode: ${mode}`);
1563
1643
  index
1564
1644
  }
1565
1645
  });
1646
+ const tempBookPath = path.join(
1647
+ basePath,
1648
+ TEMP_FOLDER,
1649
+ `temp_books_${mode}_${index}.jsonl`
1650
+ );
1651
+ const bookStream = fs2.createWriteStream(tempBookPath);
1566
1652
  worker.on("message", (msg) => {
1567
1653
  if (msg.type === "log") {
1568
1654
  } else if (msg.type === "complete") {
@@ -1571,9 +1657,23 @@ Simulating game mode: ${mode}`);
1571
1657
  logArrowProgress(completedSimulations, totalSims);
1572
1658
  }
1573
1659
  const book = Book.fromSerialized(msg.book);
1660
+ const bookData = {
1661
+ id: book.id,
1662
+ payoutMultiplier: book.payout,
1663
+ events: book.events
1664
+ };
1665
+ const prefix = book.id === simStart ? "" : "\n";
1666
+ bookStream.write(prefix + JSONL.stringify([bookData]));
1667
+ book.events = [];
1574
1668
  this.library.set(book.id, book);
1669
+ if (this.recordsWriteStream) {
1670
+ for (const record of msg.records) {
1671
+ const recordPrefix = this.hasWrittenRecord ? "\n" : "";
1672
+ this.recordsWriteStream.write(recordPrefix + JSONL.stringify([record]));
1673
+ this.hasWrittenRecord = true;
1674
+ }
1675
+ }
1575
1676
  this.wallet.mergeSerialized(msg.wallet);
1576
- this.mergeRecords(msg.records);
1577
1677
  } else if (msg.type === "done") {
1578
1678
  resolve(true);
1579
1679
  }
@@ -1716,16 +1816,61 @@ Simulating game mode: ${mode}`);
1716
1816
  writeFile(outputFilePath, rows.join("\n"));
1717
1817
  return outputFilePath;
1718
1818
  }
1719
- writeRecords(gameMode) {
1720
- const outputFileName = `force_record_${gameMode}.json`;
1721
- const outputFilePath = path.join(
1819
+ async writeRecords(mode) {
1820
+ const tempRecordsPath = path.join(
1722
1821
  this.gameConfig.rootDir,
1723
1822
  this.gameConfig.outputDir,
1724
- outputFileName
1823
+ TEMP_FOLDER,
1824
+ `temp_records_${mode}.jsonl`
1725
1825
  );
1726
- writeFile(outputFilePath, JSON.stringify(this.recorder.records, null, 2));
1727
- if (this.debug) this.logSymbolOccurrences();
1728
- return outputFilePath;
1826
+ const forceRecordsPath = path.join(
1827
+ this.gameConfig.rootDir,
1828
+ this.gameConfig.outputDir,
1829
+ `force_record_${mode}.json`
1830
+ );
1831
+ const aggregatedRecords = /* @__PURE__ */ new Map();
1832
+ if (fs2.existsSync(tempRecordsPath)) {
1833
+ const fileStream = fs2.createReadStream(tempRecordsPath);
1834
+ const rl = readline2.createInterface({
1835
+ input: fileStream,
1836
+ crlfDelay: Infinity
1837
+ });
1838
+ for await (const line of rl) {
1839
+ if (line.trim() === "") continue;
1840
+ const record = JSON.parse(line);
1841
+ const key = JSON.stringify(record.search);
1842
+ let existing = aggregatedRecords.get(key);
1843
+ if (!existing) {
1844
+ existing = {
1845
+ search: record.search,
1846
+ timesTriggered: 0,
1847
+ bookIds: []
1848
+ };
1849
+ aggregatedRecords.set(key, existing);
1850
+ }
1851
+ existing.timesTriggered += record.timesTriggered;
1852
+ for (const bookId of record.bookIds) {
1853
+ existing.bookIds.push(bookId);
1854
+ }
1855
+ }
1856
+ }
1857
+ fs2.rmSync(forceRecordsPath, { force: true });
1858
+ const writeStream = fs2.createWriteStream(forceRecordsPath, { encoding: "utf-8" });
1859
+ writeStream.write("[\n");
1860
+ let isFirst = true;
1861
+ for (const record of aggregatedRecords.values()) {
1862
+ if (!isFirst) {
1863
+ writeStream.write(",\n");
1864
+ }
1865
+ writeStream.write(JSON.stringify(record));
1866
+ isFirst = false;
1867
+ }
1868
+ writeStream.write("\n]");
1869
+ writeStream.end();
1870
+ await new Promise((resolve) => {
1871
+ writeStream.on("finish", () => resolve());
1872
+ });
1873
+ fs2.rmSync(tempRecordsPath, { force: true });
1729
1874
  }
1730
1875
  writeIndexJson() {
1731
1876
  const outputFilePath = path.join(
@@ -1747,54 +1892,25 @@ Simulating game mode: ${mode}`);
1747
1892
  writeFile(outputFilePath, JSON.stringify({ modes }, null, 2));
1748
1893
  }
1749
1894
  async writeBooksJson(gameMode) {
1750
- const outputFileName = `books_${gameMode}.jsonl`;
1751
1895
  const outputFilePath = path.join(
1752
1896
  this.gameConfig.rootDir,
1753
1897
  this.gameConfig.outputDir,
1754
- outputFileName
1898
+ `books_${gameMode}.jsonl`
1755
1899
  );
1756
- const books = Array.from(this.library.values()).map((b) => b.serialize()).map((b) => ({
1757
- id: b.id,
1758
- payoutMultiplier: b.payout,
1759
- events: b.events
1760
- })).sort((a, b) => a.id - b.id);
1761
- const contents = JSONL.stringify(books);
1762
- writeFile(outputFilePath, contents);
1763
- const compressedFileName = `books_${gameMode}.jsonl.zst`;
1764
1900
  const compressedFilePath = path.join(
1765
1901
  this.gameConfig.rootDir,
1766
1902
  this.gameConfig.outputDir,
1767
1903
  "publish_files",
1768
- compressedFileName
1904
+ `books_${gameMode}.jsonl.zst`
1769
1905
  );
1770
1906
  fs2.rmSync(compressedFilePath, { force: true });
1771
- const compressed = zlib.zstdCompressSync(Buffer.from(contents));
1772
- fs2.writeFileSync(compressedFilePath, compressed);
1773
- }
1774
- logSymbolOccurrences() {
1775
- const validRecords = this.recorder.records.filter(
1776
- (r) => r.search.some((s) => s.name === "symbolId") && r.search.some((s) => s.name === "kind")
1777
- );
1778
- const structuredRecords = validRecords.map((r) => {
1779
- const symbolEntry = r.search.find((s) => s.name === "symbolId");
1780
- const kindEntry = r.search.find((s) => s.name === "kind");
1781
- const spinTypeEntry = r.search.find((s) => s.name === "spinType");
1782
- return {
1783
- symbol: symbolEntry ? symbolEntry.value : "unknown",
1784
- kind: kindEntry ? kindEntry.value : "unknown",
1785
- spinType: spinTypeEntry ? spinTypeEntry.value : "unknown",
1786
- timesTriggered: r.timesTriggered
1787
- };
1788
- }).sort((a, b) => {
1789
- if (a.symbol < b.symbol) return -1;
1790
- if (a.symbol > b.symbol) return 1;
1791
- if (a.kind < b.kind) return -1;
1792
- if (a.kind > b.kind) return 1;
1793
- if (a.spinType < b.spinType) return -1;
1794
- if (a.spinType > b.spinType) return 1;
1795
- return 0;
1796
- });
1797
- console.table(structuredRecords);
1907
+ if (fs2.existsSync(outputFilePath)) {
1908
+ await pipeline(
1909
+ fs2.createReadStream(outputFilePath),
1910
+ zlib.createZstdCompress(),
1911
+ fs2.createWriteStream(compressedFilePath)
1912
+ );
1913
+ }
1798
1914
  }
1799
1915
  /**
1800
1916
  * Compiles user configured game to JS for use in different Node processes
@@ -1832,32 +1948,6 @@ Simulating game mode: ${mode}`);
1832
1948
  }
1833
1949
  return result;
1834
1950
  }
1835
- mergeRecords(otherRecords) {
1836
- for (const otherRecord of otherRecords) {
1837
- let record = this.recorder.records.find((r) => {
1838
- if (r.search.length !== otherRecord.search.length) return false;
1839
- for (let i = 0; i < r.search.length; i++) {
1840
- if (r.search[i].name !== otherRecord.search[i].name) return false;
1841
- if (r.search[i].value !== otherRecord.search[i].value) return false;
1842
- }
1843
- return true;
1844
- });
1845
- if (!record) {
1846
- record = {
1847
- search: otherRecord.search,
1848
- timesTriggered: 0,
1849
- bookIds: []
1850
- };
1851
- this.recorder.records.push(record);
1852
- }
1853
- record.timesTriggered += otherRecord.timesTriggered;
1854
- for (const bookId of otherRecord.bookIds) {
1855
- if (!record.bookIds.includes(bookId)) {
1856
- record.bookIds.push(bookId);
1857
- }
1858
- }
1859
- }
1860
- }
1861
1951
  /**
1862
1952
  * Generates reelset CSV files for all game modes.
1863
1953
  */
@@ -2464,18 +2554,13 @@ var Optimizer = class {
2464
2554
  const conditions = Object.keys(mode.conditions);
2465
2555
  const scalings = Object.keys(mode.scaling);
2466
2556
  const parameters = Object.keys(mode.parameters);
2467
- for (const condition of conditions) {
2468
- if (!configMode.resultSets.find((r) => r.criteria === condition)) {
2557
+ for (const rs of configMode.resultSets) {
2558
+ if (!conditions.includes(rs.criteria)) {
2469
2559
  throw new Error(
2470
- `Condition "${condition}" defined in optimizer config for game mode "${k}" does not exist as criteria in any ResultSet of the same game mode.`
2560
+ `ResultSet criteria "${rs.criteria}" in game mode "${k}" does not have a corresponding optimization condition defined.`
2471
2561
  );
2472
2562
  }
2473
2563
  }
2474
- const criteria = configMode.resultSets.map((r) => r.criteria);
2475
- assert9(
2476
- conditions.every((c) => criteria.includes(c)),
2477
- `Not all ResultSet criteria in game mode "${k}" are defined as optimization conditions.`
2478
- );
2479
2564
  let gameModeRtp = configMode.rtp;
2480
2565
  let paramRtp = 0;
2481
2566
  for (const cond of conditions) {
@@ -2528,6 +2613,7 @@ async function rustProgram(...args) {
2528
2613
  }
2529
2614
 
2530
2615
  // src/slot-game/index.ts
2616
+ import { isMainThread as isMainThread4 } from "worker_threads";
2531
2617
  var SlotGame = class {
2532
2618
  configOpts;
2533
2619
  simulation;
@@ -2603,6 +2689,7 @@ var SlotGame = class {
2603
2689
  if (opts.doAnalysis) {
2604
2690
  await this.runAnalysis(opts.analysisOpts || { gameModes: [] });
2605
2691
  }
2692
+ if (isMainThread4) console.log("Finishing up...");
2606
2693
  }
2607
2694
  /**
2608
2695
  * Gets the game configuration.
@@ -2884,13 +2971,14 @@ var ClusterWinType = class extends WinType {
2884
2971
  }
2885
2972
  }
2886
2973
  }
2887
- potentialClusters.forEach((cluster) => {
2974
+ for (const cluster of potentialClusters) {
2888
2975
  const kind = cluster.length;
2889
2976
  let baseSymbol = cluster.find((s) => !this.isWild(s.symbol))?.symbol;
2890
2977
  if (!baseSymbol) baseSymbol = cluster[0].symbol;
2891
2978
  const payout = this.getSymbolPayout(baseSymbol, kind);
2979
+ if (payout === 0) continue;
2892
2980
  if (!baseSymbol.pays || Object.keys(baseSymbol.pays).length === 0) {
2893
- return;
2981
+ continue;
2894
2982
  }
2895
2983
  clusterWins.push({
2896
2984
  payout,
@@ -2903,7 +2991,7 @@ var ClusterWinType = class extends WinType {
2903
2991
  posIndex: s.row
2904
2992
  }))
2905
2993
  });
2906
- });
2994
+ }
2907
2995
  for (const win of clusterWins) {
2908
2996
  this.ctx.services.data.recordSymbolOccurrence({
2909
2997
  kind: win.kind,
@@ -3068,7 +3156,7 @@ var ManywaysWinType = class extends WinType {
3068
3156
  // src/reel-set/GeneratedReelSet.ts
3069
3157
  import fs5 from "fs";
3070
3158
  import path7 from "path";
3071
- import { isMainThread as isMainThread4 } from "worker_threads";
3159
+ import { isMainThread as isMainThread5 } from "worker_threads";
3072
3160
 
3073
3161
  // src/reel-set/index.ts
3074
3162
  import fs4 from "fs";
@@ -3413,7 +3501,7 @@ var GeneratedReelSet = class extends ReelSet {
3413
3501
  }
3414
3502
  }
3415
3503
  const csvString = csvRows.map((row) => row.join(",")).join("\n");
3416
- if (isMainThread4) {
3504
+ if (isMainThread5) {
3417
3505
  fs5.writeFileSync(filePath, csvString);
3418
3506
  this.reels = this.parseReelsetCSV(filePath, config);
3419
3507
  console.log(