@slot-engine/core 0.2.0 → 0.2.2

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.d.mts CHANGED
@@ -50,6 +50,7 @@ type PermanentFilePaths = {
50
50
  booksIndexMeta: (mode: string) => string;
51
51
  booksChunk: (mode: string, worker: number, chunk: number) => string;
52
52
  booksCompressed: (mode: string) => string;
53
+ booksUncompressed: (mode: string) => string;
53
54
  lookupTable: (mode: string) => string;
54
55
  lookupTableIndex: (mode: string) => string;
55
56
  lookupTableSegmented: (mode: string) => string;
@@ -175,6 +176,7 @@ declare class Simulation {
175
176
  readonly gameConfig: GameConfig & GameMetadata;
176
177
  readonly simRunsAmount: Partial<Record<string, number>>;
177
178
  readonly concurrency: number;
179
+ readonly makeUncompressedBooks: boolean;
178
180
  private debug;
179
181
  private actualSims;
180
182
  private wallet;
@@ -265,6 +267,7 @@ declare class Simulation {
265
267
  */
266
268
  confirmRecords(ctx: GameContext): void;
267
269
  printSimulationSummary(): Promise<void>;
270
+ private sendSimulationStatus;
268
271
  }
269
272
  type SimulationOptions = {
270
273
  /**
@@ -293,6 +296,10 @@ type SimulationOptions = {
293
296
  * Default: 50
294
297
  */
295
298
  maxDiskBuffer?: number;
299
+ /**
300
+ * Whether to generate uncompressed book files alongside compressed ones.
301
+ */
302
+ makeUncompressedBooks?: boolean;
296
303
  };
297
304
  type SimulationConfigOptions = {
298
305
  debug?: boolean;
@@ -538,6 +545,7 @@ declare class BoardService<TGameModes extends AnyGameModes = AnyGameModes, TSymb
538
545
  getPaddingTop(): Reels;
539
546
  getPaddingBottom(): Reels;
540
547
  getAnticipation(): boolean[];
548
+ getLockedReels(): boolean[];
541
549
  /**
542
550
  * Gets the symbol at the specified reel and row index.
543
551
  */
@@ -555,6 +563,10 @@ declare class BoardService<TGameModes extends AnyGameModes = AnyGameModes, TSymb
555
563
  * Sets the anticipation value for a specific reel.
556
564
  */
557
565
  setAnticipationForReel(reelIndex: number, value: boolean): void;
566
+ /**
567
+ * Sets the locked state for a specific reel.
568
+ */
569
+ setReelLocked(reelIndex: number, value: boolean): void;
558
570
  /**
559
571
  * Counts how many symbols matching the criteria are on a specific reel.
560
572
  */
@@ -1722,6 +1734,8 @@ declare class StandaloneBoard {
1722
1734
  getBoardReels(): Reels;
1723
1735
  getPaddingTop(): Reels;
1724
1736
  getPaddingBottom(): Reels;
1737
+ getAnticipation(): boolean[];
1738
+ getLockedReels(): boolean[];
1725
1739
  /**
1726
1740
  * Gets the symbol at the specified reel and row index.
1727
1741
  */
@@ -1739,6 +1753,10 @@ declare class StandaloneBoard {
1739
1753
  * Sets the anticipation value for a specific reel.
1740
1754
  */
1741
1755
  setAnticipationForReel(reelIndex: number, value: boolean): void;
1756
+ /**
1757
+ * Sets the locked state for a specific reel.
1758
+ */
1759
+ setReelLocked(reelIndex: number, value: boolean): void;
1742
1760
  /**
1743
1761
  * Counts how many symbols matching the criteria are on a specific reel.
1744
1762
  */
package/dist/index.d.ts CHANGED
@@ -50,6 +50,7 @@ type PermanentFilePaths = {
50
50
  booksIndexMeta: (mode: string) => string;
51
51
  booksChunk: (mode: string, worker: number, chunk: number) => string;
52
52
  booksCompressed: (mode: string) => string;
53
+ booksUncompressed: (mode: string) => string;
53
54
  lookupTable: (mode: string) => string;
54
55
  lookupTableIndex: (mode: string) => string;
55
56
  lookupTableSegmented: (mode: string) => string;
@@ -175,6 +176,7 @@ declare class Simulation {
175
176
  readonly gameConfig: GameConfig & GameMetadata;
176
177
  readonly simRunsAmount: Partial<Record<string, number>>;
177
178
  readonly concurrency: number;
179
+ readonly makeUncompressedBooks: boolean;
178
180
  private debug;
179
181
  private actualSims;
180
182
  private wallet;
@@ -265,6 +267,7 @@ declare class Simulation {
265
267
  */
266
268
  confirmRecords(ctx: GameContext): void;
267
269
  printSimulationSummary(): Promise<void>;
270
+ private sendSimulationStatus;
268
271
  }
269
272
  type SimulationOptions = {
270
273
  /**
@@ -293,6 +296,10 @@ type SimulationOptions = {
293
296
  * Default: 50
294
297
  */
295
298
  maxDiskBuffer?: number;
299
+ /**
300
+ * Whether to generate uncompressed book files alongside compressed ones.
301
+ */
302
+ makeUncompressedBooks?: boolean;
296
303
  };
297
304
  type SimulationConfigOptions = {
298
305
  debug?: boolean;
@@ -538,6 +545,7 @@ declare class BoardService<TGameModes extends AnyGameModes = AnyGameModes, TSymb
538
545
  getPaddingTop(): Reels;
539
546
  getPaddingBottom(): Reels;
540
547
  getAnticipation(): boolean[];
548
+ getLockedReels(): boolean[];
541
549
  /**
542
550
  * Gets the symbol at the specified reel and row index.
543
551
  */
@@ -555,6 +563,10 @@ declare class BoardService<TGameModes extends AnyGameModes = AnyGameModes, TSymb
555
563
  * Sets the anticipation value for a specific reel.
556
564
  */
557
565
  setAnticipationForReel(reelIndex: number, value: boolean): void;
566
+ /**
567
+ * Sets the locked state for a specific reel.
568
+ */
569
+ setReelLocked(reelIndex: number, value: boolean): void;
558
570
  /**
559
571
  * Counts how many symbols matching the criteria are on a specific reel.
560
572
  */
@@ -1722,6 +1734,8 @@ declare class StandaloneBoard {
1722
1734
  getBoardReels(): Reels;
1723
1735
  getPaddingTop(): Reels;
1724
1736
  getPaddingBottom(): Reels;
1737
+ getAnticipation(): boolean[];
1738
+ getLockedReels(): boolean[];
1725
1739
  /**
1726
1740
  * Gets the symbol at the specified reel and row index.
1727
1741
  */
@@ -1739,6 +1753,10 @@ declare class StandaloneBoard {
1739
1753
  * Sets the anticipation value for a specific reel.
1740
1754
  */
1741
1755
  setAnticipationForReel(reelIndex: number, value: boolean): void;
1756
+ /**
1757
+ * Sets the locked state for a specific reel.
1758
+ */
1759
+ setReelLocked(reelIndex: number, value: boolean): void;
1742
1760
  /**
1743
1761
  * Counts how many symbols matching the criteria are on a specific reel.
1744
1762
  */
package/dist/index.js CHANGED
@@ -79,6 +79,7 @@ function createPermanentFilePaths(basePath) {
79
79
  `books_${mode}_chunk_${worker}-${chunk}.jsonl.zst`
80
80
  ),
81
81
  booksCompressed: (mode) => import_path.default.join(basePath, "publish_files", `books_${mode}.jsonl.zst`),
82
+ booksUncompressed: (mode) => import_path.default.join(basePath, `books_${mode}.jsonl`),
82
83
  lookupTable: (mode) => import_path.default.join(basePath, `lookUpTable_${mode}.csv`),
83
84
  lookupTableIndex: (mode) => import_path.default.join(basePath, `lookUpTable_${mode}.index`),
84
85
  lookupTableSegmented: (mode) => import_path.default.join(basePath, `lookUpTableSegmented_${mode}.csv`),
@@ -476,8 +477,18 @@ var Board = class {
476
477
  * Used for triggering anticipation effects.
477
478
  */
478
479
  anticipation;
480
+ /**
481
+ * The most recent stop positions for the reels.
482
+ */
479
483
  lastDrawnReelStops;
484
+ /**
485
+ * The reel set used in the most recent draw.
486
+ */
480
487
  lastUsedReels;
488
+ /**
489
+ * Indicates whether each reel is locked or not.
490
+ */
491
+ reelsLocked;
481
492
  constructor() {
482
493
  this.reels = [];
483
494
  this.paddingTop = [];
@@ -485,6 +496,7 @@ var Board = class {
485
496
  this.anticipation = [];
486
497
  this.lastDrawnReelStops = [];
487
498
  this.lastUsedReels = [];
499
+ this.reelsLocked = [];
488
500
  }
489
501
  getSymbol(reelIndex, rowIndex) {
490
502
  return this.reels[reelIndex]?.[rowIndex];
@@ -632,14 +644,19 @@ var Board = class {
632
644
  return reelSet;
633
645
  }
634
646
  resetReels(opts) {
635
- const length = opts.reelsAmount ?? opts.ctx.services.game.getCurrentGameMode().reelsAmount;
647
+ const { ctx, reelsAmount, reelsLocked } = opts;
648
+ const length = reelsAmount ?? ctx.services.game.getCurrentGameMode().reelsAmount;
636
649
  this.reels = this.makeEmptyReels(opts);
637
650
  this.anticipation = Array.from({ length }, () => false);
651
+ this.reelsLocked = reelsLocked ?? Array.from({ length }, () => false);
638
652
  this.paddingTop = this.makeEmptyReels(opts);
639
653
  this.paddingBottom = this.makeEmptyReels(opts);
640
654
  }
641
655
  drawBoardMixed(opts) {
642
- this.resetReels(opts);
656
+ this.resetReels({
657
+ ...opts,
658
+ ...this.reelsLocked.length && { reelsLocked: this.reelsLocked }
659
+ });
643
660
  const reelsAmount = opts.reelsAmount ?? opts.ctx.services.game.getCurrentGameMode().reelsAmount;
644
661
  const symbolsPerReel = opts.symbolsPerReel ?? opts.ctx.services.game.getCurrentGameMode().symbolsPerReel;
645
662
  const padSymbols = opts.padSymbols ?? opts.ctx.config.padSymbols;
@@ -668,6 +685,18 @@ var Board = class {
668
685
  );
669
686
  }
670
687
  }
688
+ if (this.reelsLocked.some((locked) => locked) && this.lastDrawnReelStops.length == 0) {
689
+ throw new Error(
690
+ "Cannot draw board with locked reels before drawing it at least once."
691
+ );
692
+ }
693
+ if (this.reelsLocked.some((locked) => locked)) {
694
+ for (let ridx = 0; ridx < reelsAmount; ridx++) {
695
+ if (this.reelsLocked[ridx]) {
696
+ finalReelStops[ridx] = this.lastDrawnReelStops[ridx];
697
+ }
698
+ }
699
+ }
671
700
  this.lastDrawnReelStops = finalReelStops.map((pos) => pos);
672
701
  this.lastUsedReels = opts.reels;
673
702
  for (let ridx = 0; ridx < reelsAmount; ridx++) {
@@ -829,6 +858,9 @@ var BoardService = class extends AbstractService {
829
858
  getAnticipation() {
830
859
  return this.board.anticipation;
831
860
  }
861
+ getLockedReels() {
862
+ return this.board.reelsLocked;
863
+ }
832
864
  /**
833
865
  * Gets the symbol at the specified reel and row index.
834
866
  */
@@ -858,6 +890,12 @@ var BoardService = class extends AbstractService {
858
890
  setAnticipationForReel(reelIndex, value) {
859
891
  this.board.anticipation[reelIndex] = value;
860
892
  }
893
+ /**
894
+ * Sets the locked state for a specific reel.
895
+ */
896
+ setReelLocked(reelIndex, value) {
897
+ this.board.reelsLocked[reelIndex] = value;
898
+ }
861
899
  /**
862
900
  * Counts how many symbols matching the criteria are on a specific reel.
863
901
  */
@@ -1799,6 +1837,8 @@ var TerminalUi = class {
1799
1837
  logs = [];
1800
1838
  logScrollOffset = 0;
1801
1839
  isScrolled = false;
1840
+ maxLogs = 500;
1841
+ totalLogs = 0;
1802
1842
  minWidth = 50;
1803
1843
  minHeight = 12;
1804
1844
  isRendering = false;
@@ -1824,12 +1864,13 @@ var TerminalUi = class {
1824
1864
  this.scrollDown();
1825
1865
  } else if (key === "l") {
1826
1866
  this.scrollToBottom();
1827
- } else if (key === "") {
1867
+ } else if (key === "q" || key === "") {
1828
1868
  this.stop();
1829
1869
  process.exit(0);
1830
1870
  }
1831
1871
  };
1832
1872
  process.stdout.on("resize", this.resizeHandler);
1873
+ process.on("SIGINT", this.sigintHandler);
1833
1874
  }
1834
1875
  get terminalWidth() {
1835
1876
  return process.stdout.columns || 80;
@@ -1851,7 +1892,6 @@ var TerminalUi = class {
1851
1892
  }
1852
1893
  this.render();
1853
1894
  this.renderInterval = setInterval(() => this.render(), 100);
1854
- process.on("SIGINT", this.sigintHandler);
1855
1895
  }
1856
1896
  stop() {
1857
1897
  if (this.renderInterval) {
@@ -1879,7 +1919,16 @@ var TerminalUi = class {
1879
1919
  this.totalSims = opts.totalSims;
1880
1920
  }
1881
1921
  log(message) {
1882
- this.logs.push({ i: this.logs.length, m: message });
1922
+ this.logs.push({ i: this.totalLogs, m: message });
1923
+ this.totalLogs++;
1924
+ if (this.logs.length > this.maxLogs) {
1925
+ const excess = this.logs.length - this.maxLogs;
1926
+ this.logs.splice(0, excess);
1927
+ this.logScrollOffset = Math.min(
1928
+ this.logScrollOffset,
1929
+ Math.max(0, this.logs.length - this.getLogAreaHeight())
1930
+ );
1931
+ }
1883
1932
  if (!this.isScrolled) this.scrollToBottom();
1884
1933
  }
1885
1934
  scrollUp(lines = 1) {
@@ -2044,6 +2093,7 @@ var Simulation = class {
2044
2093
  gameConfig;
2045
2094
  simRunsAmount;
2046
2095
  concurrency;
2096
+ makeUncompressedBooks;
2047
2097
  debug = false;
2048
2098
  actualSims = 0;
2049
2099
  wallet = new Wallet();
@@ -2072,6 +2122,7 @@ var Simulation = class {
2072
2122
  const { config, metadata } = createGameConfig(gameConfigOpts);
2073
2123
  this.gameConfig = { ...config, ...metadata };
2074
2124
  this.gameConfigOpts = gameConfigOpts;
2125
+ this.makeUncompressedBooks = opts.makeUncompressedBooks || false;
2075
2126
  this.simRunsAmount = opts.simRunsAmount || {};
2076
2127
  this.concurrency = (opts.concurrency || 6) >= 2 ? opts.concurrency || 6 : 2;
2077
2128
  this.maxPendingSims = opts.maxPendingSims ?? 25;
@@ -2149,9 +2200,7 @@ var Simulation = class {
2149
2200
  const startTime = Date.now();
2150
2201
  statusMessage = `Simulating mode "${mode}" with ${this.simRunsAmount[mode]} runs.`;
2151
2202
  this.tui?.log(statusMessage);
2152
- if (this.socket && this.panelActive) {
2153
- this.socket.emit("simulationStatus", statusMessage);
2154
- }
2203
+ this.sendSimulationStatus(statusMessage);
2155
2204
  const runs = this.simRunsAmount[mode] || 0;
2156
2205
  if (runs <= 0) continue;
2157
2206
  if (!configuredGameModes.includes(mode)) {
@@ -2188,9 +2237,7 @@ var Simulation = class {
2188
2237
  createDirIfNotExists(this.PATHS.publishFiles);
2189
2238
  statusMessage = `Writing final files for game mode "${mode}". This may take a while...`;
2190
2239
  this.tui?.log(statusMessage);
2191
- if (this.socket && this.panelActive) {
2192
- this.socket.emit("simulationStatus", statusMessage);
2193
- }
2240
+ this.sendSimulationStatus(statusMessage);
2194
2241
  writeFile(
2195
2242
  this.PATHS.booksIndexMeta(mode),
2196
2243
  JSON.stringify(
@@ -2245,13 +2292,43 @@ var Simulation = class {
2245
2292
  }
2246
2293
  await this.writeRecords(mode);
2247
2294
  this.writeIndexJson();
2295
+ if (this.makeUncompressedBooks) {
2296
+ statusMessage = `Creating decompressed book file for mode "${mode}". This may take a while...`;
2297
+ this.tui?.log(statusMessage);
2298
+ this.sendSimulationStatus(statusMessage);
2299
+ const uncompressedBooksPath = this.PATHS.booksUncompressed(mode);
2300
+ const outputStream = import_fs3.default.createWriteStream(uncompressedBooksPath, {
2301
+ highWaterMark: this.streamHighWaterMark
2302
+ });
2303
+ try {
2304
+ for (const { worker, chunks: chunks2 } of this.bookIndexMetas) {
2305
+ for (let chunk = 0; chunk < chunks2; chunk++) {
2306
+ const bookChunkPath = this.PATHS.booksChunk(mode, worker, chunk);
2307
+ if (!import_fs3.default.existsSync(bookChunkPath)) continue;
2308
+ const inputStream = import_fs3.default.createReadStream(bookChunkPath);
2309
+ const compress = import_zlib.default.createZstdDecompress();
2310
+ for await (const decompChunk of inputStream.pipe(compress)) {
2311
+ if (!outputStream.write(decompChunk)) {
2312
+ await new Promise((r) => outputStream.once("drain", () => r()));
2313
+ }
2314
+ }
2315
+ }
2316
+ }
2317
+ outputStream.end();
2318
+ await new Promise((r) => outputStream.on("finish", () => r()));
2319
+ } catch (error) {
2320
+ statusMessage = import_chalk2.default.yellow(
2321
+ `Error creating uncompressed book file: ${error.message}`
2322
+ );
2323
+ this.tui?.log(statusMessage);
2324
+ this.sendSimulationStatus(statusMessage);
2325
+ }
2326
+ }
2248
2327
  const endTime = Date.now();
2249
2328
  const prettyTime = new Date(endTime - startTime).toISOString().slice(11, -1);
2250
2329
  statusMessage = `Mode ${mode} done! Time taken: ${prettyTime}`;
2251
2330
  this.tui?.log(statusMessage);
2252
- if (this.socket && this.panelActive) {
2253
- this.socket.emit("simulationStatus", statusMessage);
2254
- }
2331
+ this.sendSimulationStatus(statusMessage);
2255
2332
  }
2256
2333
  this.tui?.stop();
2257
2334
  await this.printSimulationSummary();
@@ -2453,7 +2530,7 @@ var Simulation = class {
2453
2530
  `
2454
2531
  )
2455
2532
  ]);
2456
- if (this.bookBufferSizes.get(index) >= 12 * 1024 * 1024) {
2533
+ if (this.bookBufferSizes.get(index) >= 10 * 1024 * 1024) {
2457
2534
  await flushBookChunk();
2458
2535
  }
2459
2536
  if (this.recordsWriteStream) {
@@ -2899,6 +2976,11 @@ var Simulation = class {
2899
2976
  });
2900
2977
  }
2901
2978
  }
2979
+ sendSimulationStatus(message) {
2980
+ if (this.socket && this.panelActive) {
2981
+ this.socket.emit("simulationStatus", message);
2982
+ }
2983
+ }
2902
2984
  };
2903
2985
 
2904
2986
  // src/analysis/index.ts
@@ -4546,6 +4628,12 @@ var StandaloneBoard = class {
4546
4628
  getPaddingBottom() {
4547
4629
  return this.board.paddingBottom;
4548
4630
  }
4631
+ getAnticipation() {
4632
+ return this.board.anticipation;
4633
+ }
4634
+ getLockedReels() {
4635
+ return this.board.reelsLocked;
4636
+ }
4549
4637
  /**
4550
4638
  * Gets the symbol at the specified reel and row index.
4551
4639
  */
@@ -4575,6 +4663,12 @@ var StandaloneBoard = class {
4575
4663
  setAnticipationForReel(reelIndex, value) {
4576
4664
  this.board.anticipation[reelIndex] = value;
4577
4665
  }
4666
+ /**
4667
+ * Sets the locked state for a specific reel.
4668
+ */
4669
+ setReelLocked(reelIndex, value) {
4670
+ this.board.reelsLocked[reelIndex] = value;
4671
+ }
4578
4672
  /**
4579
4673
  * Counts how many symbols matching the criteria are on a specific reel.
4580
4674
  */