@slot-engine/core 0.0.9 → 0.0.11

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
@@ -13,7 +13,7 @@ function createGameConfig(opts) {
13
13
  symbols.set(key, value);
14
14
  }
15
15
  const getAnticipationTrigger = (spinType) => {
16
- return Math.min(...Object.keys(opts.scatterToFreespins[spinType]).map(Number)) - 1;
16
+ return Math.min(...Object.keys(opts.scatterToFreespins[spinType] || {}).map(Number)) - 1;
17
17
  };
18
18
  return {
19
19
  padSymbols: opts.padSymbols || 1,
@@ -349,6 +349,16 @@ function createGameState(opts) {
349
349
  };
350
350
  }
351
351
 
352
+ // src/recorder/index.ts
353
+ var Recorder = class {
354
+ records;
355
+ pendingRecords;
356
+ constructor() {
357
+ this.records = [];
358
+ this.pendingRecords = [];
359
+ }
360
+ };
361
+
352
362
  // src/board/index.ts
353
363
  import assert3 from "assert";
354
364
 
@@ -384,6 +394,16 @@ var GameSymbol = class _GameSymbol {
384
394
  return true;
385
395
  }
386
396
  }
397
+ /**
398
+ * Creates a clone of this GameSymbol.
399
+ */
400
+ clone() {
401
+ return new _GameSymbol({
402
+ id: this.id,
403
+ pays: this.pays ? { ...this.pays } : void 0,
404
+ properties: Object.fromEntries(this.properties)
405
+ });
406
+ }
387
407
  };
388
408
 
389
409
  // src/board/index.ts
@@ -418,6 +438,13 @@ var Board = class {
418
438
  this.lastDrawnReelStops = [];
419
439
  this.lastUsedReels = [];
420
440
  }
441
+ getSymbol(reelIndex, rowIndex) {
442
+ return this.reels[reelIndex]?.[rowIndex];
443
+ }
444
+ setSymbol(reelIndex, rowIndex, symbol) {
445
+ this.reels[reelIndex] = this.reels[reelIndex] || [];
446
+ this.reels[reelIndex][rowIndex] = symbol;
447
+ }
421
448
  makeEmptyReels(opts) {
422
449
  const length = opts.reelsAmount ?? opts.ctx.services.game.getCurrentGameMode().reelsAmount;
423
450
  assert3(length, "Cannot make empty reels without context or reelsAmount.");
@@ -571,7 +598,11 @@ var Board = class {
571
598
  for (const [r, stopPos] of Object.entries(opts.forcedStops)) {
572
599
  const reelIdx = Number(r);
573
600
  const symCount = symbolsPerReel[reelIdx];
574
- finalReelStops[reelIdx] = stopPos - Math.round(opts.ctx.services.rng.randomFloat(0, symCount - 1));
601
+ if (opts.forcedStopsOffset !== false) {
602
+ finalReelStops[reelIdx] = stopPos - Math.round(opts.ctx.services.rng.randomFloat(0, symCount - 1));
603
+ } else {
604
+ finalReelStops[reelIdx] = stopPos;
605
+ }
575
606
  if (finalReelStops[reelIdx] < 0) {
576
607
  finalReelStops[reelIdx] = opts.reels[reelIdx].length + finalReelStops[reelIdx];
577
608
  }
@@ -614,7 +645,8 @@ var Board = class {
614
645
  );
615
646
  }
616
647
  const reels = this.lastUsedReels;
617
- opts.symbolsToDelete.forEach(({ reelIdx, rowIdx }) => {
648
+ const sortedDeletions = [...opts.symbolsToDelete].sort((a, b) => b.rowIdx - a.rowIdx);
649
+ sortedDeletions.forEach(({ reelIdx, rowIdx }) => {
618
650
  this.reels[reelIdx].splice(rowIdx, 1);
619
651
  });
620
652
  const newFirstSymbolPositions = {};
@@ -640,6 +672,7 @@ var Board = class {
640
672
  }
641
673
  for (let ridx = 0; ridx < reelsAmount; ridx++) {
642
674
  const firstSymbolPos = newFirstSymbolPositions[ridx];
675
+ if (firstSymbolPos === void 0) continue;
643
676
  for (let p = 1; p <= padSymbols; p++) {
644
677
  const topPos = (firstSymbolPos - p + reels[ridx].length) % reels[ridx].length;
645
678
  const padSymbol = reels[ridx][topPos];
@@ -680,6 +713,18 @@ var BoardService = class extends AbstractService {
680
713
  getAnticipation() {
681
714
  return this.board.anticipation;
682
715
  }
716
+ /**
717
+ * Gets the symbol at the specified reel and row index.
718
+ */
719
+ getSymbol(reelIndex, rowIndex) {
720
+ return this.board.getSymbol(reelIndex, rowIndex);
721
+ }
722
+ /**
723
+ * Sets the symbol at the specified reel and row index.
724
+ */
725
+ setSymbol(reelIndex, rowIndex, symbol) {
726
+ this.board.setSymbol(reelIndex, rowIndex, symbol);
727
+ }
683
728
  resetReels() {
684
729
  this.board.resetReels({
685
730
  ctx: this.ctx()
@@ -754,8 +799,8 @@ var BoardService = class extends AbstractService {
754
799
  /**
755
800
  * Draws a board using specified reel stops.
756
801
  */
757
- drawBoardWithForcedStops(reels, forcedStops) {
758
- this.drawBoardMixed(reels, forcedStops);
802
+ drawBoardWithForcedStops(opts) {
803
+ this.drawBoardMixed(opts.reels, opts.forcedStops, opts.randomOffset);
759
804
  }
760
805
  /**
761
806
  * Draws a board using random reel stops.
@@ -763,11 +808,12 @@ var BoardService = class extends AbstractService {
763
808
  drawBoardWithRandomStops(reels) {
764
809
  this.drawBoardMixed(reels);
765
810
  }
766
- drawBoardMixed(reels, forcedStops) {
811
+ drawBoardMixed(reels, forcedStops, forcedStopsOffset) {
767
812
  this.board.drawBoardMixed({
768
813
  ctx: this.ctx(),
769
814
  reels,
770
- forcedStops
815
+ forcedStops,
816
+ forcedStopsOffset
771
817
  });
772
818
  }
773
819
  /**
@@ -867,6 +913,25 @@ var GameService = class extends AbstractService {
867
913
  constructor(ctx) {
868
914
  super(ctx);
869
915
  }
916
+ /**
917
+ * Intended for internal use only.\
918
+ * Generates reels for all reel sets in the game configuration.
919
+ */
920
+ _generateReels() {
921
+ const config = this.ctx().config;
922
+ for (const mode of Object.values(config.gameModes)) {
923
+ if (mode.reelSets && mode.reelSets.length > 0) {
924
+ for (const reelSet of Object.values(mode.reelSets)) {
925
+ reelSet.associatedGameModeName = mode.name;
926
+ reelSet.generateReels(config);
927
+ }
928
+ } else {
929
+ throw new Error(
930
+ `Game mode "${mode.name}" has no reel sets defined. Cannot generate reelset files.`
931
+ );
932
+ }
933
+ }
934
+ }
870
935
  /**
871
936
  * Retrieves a reel set by its ID within a specific game mode.
872
937
  */
@@ -1105,16 +1170,6 @@ var Book = class _Book {
1105
1170
  }
1106
1171
  };
1107
1172
 
1108
- // src/recorder/index.ts
1109
- var Recorder = class {
1110
- records;
1111
- pendingRecords;
1112
- constructor() {
1113
- this.records = [];
1114
- this.pendingRecords = [];
1115
- }
1116
- };
1117
-
1118
1173
  // src/wallet/index.ts
1119
1174
  var Wallet = class {
1120
1175
  /**
@@ -1758,7 +1813,7 @@ Simulating game mode: ${mode}`);
1758
1813
  if (mode.reelSets && mode.reelSets.length > 0) {
1759
1814
  for (const reelSet of Object.values(mode.reelSets)) {
1760
1815
  reelSet.associatedGameModeName = mode.name;
1761
- reelSet.generateReels(this);
1816
+ reelSet.generateReels(this.gameConfig);
1762
1817
  }
1763
1818
  } else {
1764
1819
  throw new Error(
@@ -2527,9 +2582,20 @@ var WinType = class {
2527
2582
  isWild(symbol) {
2528
2583
  return !!this.wildSymbol && symbol.compare(this.wildSymbol);
2529
2584
  }
2585
+ getSymbolPayout(symbol, count) {
2586
+ if (!symbol.pays) return 0;
2587
+ let clusterSize = 0;
2588
+ const sizes = Object.keys(symbol.pays).map((s) => parseInt(s, 10)).filter((n) => Number.isFinite(n)).sort((a, b) => a - b);
2589
+ for (const size of sizes) {
2590
+ if (size > count) break;
2591
+ clusterSize = size;
2592
+ }
2593
+ return symbol.pays[clusterSize] || 0;
2594
+ }
2530
2595
  };
2531
2596
 
2532
2597
  // src/win-types/LinesWinType.ts
2598
+ import assert11 from "assert";
2533
2599
  var LinesWinType = class extends WinType {
2534
2600
  lines;
2535
2601
  constructor(opts) {
@@ -2570,106 +2636,335 @@ var LinesWinType = class extends WinType {
2570
2636
  evaluateWins(board) {
2571
2637
  this.validateConfig();
2572
2638
  const lineWins = [];
2573
- let payout = 0;
2574
2639
  const reels = board;
2575
- for (const [lineNumStr, lineDef] of Object.entries(this.lines)) {
2640
+ for (const [lineNumStr, line] of Object.entries(this.lines)) {
2576
2641
  const lineNum = Number(lineNumStr);
2577
- let baseSymbol = null;
2578
- let leadingWilds = 0;
2579
- const chain = [];
2580
- const details = [];
2581
- for (let ridx = 0; ridx < reels.length; ridx++) {
2582
- const rowIdx = lineDef[ridx];
2583
- const sym = reels[ridx][rowIdx];
2584
- if (!sym) throw new Error("Encountered an invalid symbol while evaluating wins.");
2585
- const wild = this.isWild(sym);
2586
- if (ridx === 0) {
2587
- chain.push(sym);
2588
- details.push({ reelIndex: ridx, posIndex: rowIdx, symbol: sym, isWild: wild });
2589
- if (wild) leadingWilds++;
2590
- else baseSymbol = sym;
2591
- continue;
2642
+ let baseSymbol;
2643
+ const potentialWinLine = [];
2644
+ const potentialWildLine = [];
2645
+ for (const [ridx, reel] of reels.entries()) {
2646
+ const sidx = line[ridx];
2647
+ const thisSymbol = reel[sidx];
2648
+ if (!baseSymbol) {
2649
+ baseSymbol = thisSymbol;
2592
2650
  }
2593
- if (wild) {
2594
- chain.push(sym);
2595
- details.push({
2596
- reelIndex: ridx,
2597
- posIndex: rowIdx,
2598
- symbol: sym,
2599
- isWild: true,
2600
- substitutedFor: baseSymbol || void 0
2601
- });
2651
+ assert11(baseSymbol, `No symbol found at line ${lineNum}, reel ${ridx}`);
2652
+ assert11(thisSymbol, `No symbol found at line ${lineNum}, reel ${ridx}`);
2653
+ if (potentialWinLine.length == 0) {
2654
+ if (this.isWild(thisSymbol)) {
2655
+ potentialWildLine.push({ reel: ridx, row: sidx, symbol: thisSymbol });
2656
+ }
2657
+ potentialWinLine.push({ reel: ridx, row: sidx, symbol: thisSymbol });
2602
2658
  continue;
2603
2659
  }
2604
- if (!baseSymbol) {
2605
- baseSymbol = sym;
2606
- chain.push(sym);
2607
- details.push({ reelIndex: ridx, posIndex: rowIdx, symbol: sym, isWild: false });
2660
+ if (this.isWild(baseSymbol)) {
2661
+ if (this.isWild(thisSymbol)) {
2662
+ potentialWildLine.push({ reel: ridx, row: sidx, symbol: thisSymbol });
2663
+ } else {
2664
+ baseSymbol = thisSymbol;
2665
+ }
2666
+ potentialWinLine.push({ reel: ridx, row: sidx, symbol: thisSymbol });
2608
2667
  continue;
2609
2668
  }
2610
- if (sym.id === baseSymbol.id) {
2611
- chain.push(sym);
2612
- details.push({ reelIndex: ridx, posIndex: rowIdx, symbol: sym, isWild: false });
2613
- continue;
2669
+ if (baseSymbol.compare(thisSymbol) || this.isWild(thisSymbol)) {
2670
+ potentialWinLine.push({ reel: ridx, row: sidx, symbol: thisSymbol });
2614
2671
  }
2615
- break;
2616
- }
2617
- if (chain.length === 0) continue;
2618
- const allWild = chain.every((s) => this.isWild(s));
2619
- const wildRepresentative = this.wildSymbol instanceof GameSymbol ? this.wildSymbol : null;
2620
- const len = chain.length;
2621
- let bestPayout = 0;
2622
- let bestType = null;
2623
- let payingSymbol = null;
2624
- if (baseSymbol?.pays && baseSymbol.pays[len]) {
2625
- bestPayout = baseSymbol.pays[len];
2626
- bestType = "substituted";
2627
- payingSymbol = baseSymbol;
2628
2672
  }
2629
- if (allWild && wildRepresentative?.pays && wildRepresentative.pays[len]) {
2630
- const wildPay = wildRepresentative.pays[len];
2631
- if (wildPay > bestPayout) {
2632
- bestPayout = wildPay;
2633
- bestType = "pure-wild";
2634
- payingSymbol = wildRepresentative;
2635
- }
2636
- }
2637
- if (!bestPayout || !bestType || !payingSymbol) continue;
2638
- const minLen = payingSymbol.pays ? Math.min(...Object.keys(payingSymbol.pays).map(Number)) : Infinity;
2639
- if (len < minLen) continue;
2640
- const wildCount = details.filter((d) => d.isWild).length;
2641
- const nonWildCount = len - wildCount;
2642
- lineWins.push({
2673
+ const minSymLine = Math.min(
2674
+ ...Object.keys(baseSymbol.pays || {}).map((k) => parseInt(k, 10))
2675
+ );
2676
+ if (potentialWinLine.length < minSymLine) continue;
2677
+ const linePayout = this.getLinePayout(potentialWinLine);
2678
+ const wildLinePayout = this.getLinePayout(potentialWildLine);
2679
+ let finalLine = {
2680
+ kind: potentialWinLine.length,
2681
+ baseSymbol,
2682
+ symbols: potentialWinLine.map((s) => ({
2683
+ symbol: s.symbol,
2684
+ isWild: this.isWild(s.symbol),
2685
+ reelIndex: s.reel,
2686
+ posIndex: s.row
2687
+ })),
2643
2688
  lineNumber: lineNum,
2644
- kind: len,
2645
- payout: bestPayout,
2646
- symbol: payingSymbol,
2647
- winType: bestType,
2648
- substitutedBaseSymbol: bestType === "pure-wild" ? null : baseSymbol,
2649
- symbols: details,
2650
- stats: { wildCount, nonWildCount, leadingWilds }
2651
- });
2652
- payout += bestPayout;
2689
+ payout: linePayout
2690
+ };
2691
+ if (wildLinePayout > linePayout) {
2692
+ baseSymbol = potentialWildLine[0]?.symbol;
2693
+ finalLine = {
2694
+ kind: potentialWildLine.length,
2695
+ baseSymbol,
2696
+ symbols: potentialWildLine.map((s) => ({
2697
+ symbol: s.symbol,
2698
+ isWild: this.isWild(s.symbol),
2699
+ reelIndex: s.reel,
2700
+ posIndex: s.row
2701
+ })),
2702
+ lineNumber: lineNum,
2703
+ payout: wildLinePayout
2704
+ };
2705
+ }
2706
+ lineWins.push(finalLine);
2653
2707
  }
2654
2708
  for (const win of lineWins) {
2655
2709
  this.ctx.services.data.recordSymbolOccurrence({
2656
2710
  kind: win.kind,
2657
- symbolId: win.symbol.id,
2711
+ symbolId: win.baseSymbol.id,
2658
2712
  spinType: this.ctx.state.currentSpinType
2659
2713
  });
2660
2714
  }
2661
- this.payout = payout;
2715
+ this.payout = lineWins.reduce((sum, l) => sum + l.payout, 0);
2662
2716
  this.winCombinations = lineWins;
2663
2717
  return this;
2664
2718
  }
2719
+ getLinePayout(line) {
2720
+ if (line.length === 0) return 0;
2721
+ let baseSymbol = line.find((s) => !this.isWild(s.symbol))?.symbol;
2722
+ if (!baseSymbol) baseSymbol = line[0].symbol;
2723
+ const kind = line.length;
2724
+ const payout = this.getSymbolPayout(baseSymbol, kind);
2725
+ return payout;
2726
+ }
2665
2727
  };
2666
2728
 
2667
2729
  // src/win-types/ClusterWinType.ts
2668
2730
  var ClusterWinType = class extends WinType {
2731
+ _checked = [];
2732
+ _checkedWilds = [];
2733
+ _currentBoard = [];
2734
+ constructor(opts) {
2735
+ super(opts);
2736
+ }
2737
+ validateConfig() {
2738
+ }
2739
+ /**
2740
+ * Calculates wins based on symbol cluster size and provided board state.\
2741
+ * Retrieve the results using `getWins()` after.
2742
+ */
2743
+ evaluateWins(board) {
2744
+ this.validateConfig();
2745
+ this._checked = [];
2746
+ this._currentBoard = board;
2747
+ const clusterWins = [];
2748
+ const potentialClusters = [];
2749
+ for (const [ridx, reel] of board.entries()) {
2750
+ for (const [sidx, symbol] of reel.entries()) {
2751
+ this._checkedWilds = [];
2752
+ if (this.isWild(symbol)) continue;
2753
+ if (this.isChecked(ridx, sidx)) {
2754
+ continue;
2755
+ }
2756
+ const thisSymbol = { reel: ridx, row: sidx, symbol };
2757
+ this._checked.push(thisSymbol);
2758
+ const neighbors = this.getNeighbors(ridx, sidx);
2759
+ const matchingSymbols = this.evaluateCluster(symbol, neighbors);
2760
+ if (matchingSymbols.size >= 1) {
2761
+ potentialClusters.push([thisSymbol, ...matchingSymbols.values()]);
2762
+ }
2763
+ }
2764
+ }
2765
+ for (const [ridx, reel] of board.entries()) {
2766
+ for (const [sidx, symbol] of reel.entries()) {
2767
+ this._checkedWilds = [];
2768
+ if (!this.isWild(symbol)) continue;
2769
+ if (this.isChecked(ridx, sidx)) {
2770
+ continue;
2771
+ }
2772
+ const thisSymbol = { reel: ridx, row: sidx, symbol };
2773
+ this._checked.push(thisSymbol);
2774
+ const neighbors = this.getNeighbors(ridx, sidx);
2775
+ const matchingSymbols = this.evaluateCluster(symbol, neighbors);
2776
+ if (matchingSymbols.size >= 1) {
2777
+ potentialClusters.push([thisSymbol, ...matchingSymbols.values()]);
2778
+ }
2779
+ }
2780
+ }
2781
+ potentialClusters.forEach((cluster) => {
2782
+ const kind = cluster.length;
2783
+ let baseSymbol = cluster.find((s) => !this.isWild(s.symbol))?.symbol;
2784
+ if (!baseSymbol) baseSymbol = cluster[0].symbol;
2785
+ const payout = this.getSymbolPayout(baseSymbol, kind);
2786
+ if (!baseSymbol.pays || Object.keys(baseSymbol.pays).length === 0) {
2787
+ return;
2788
+ }
2789
+ clusterWins.push({
2790
+ payout,
2791
+ kind,
2792
+ baseSymbol,
2793
+ symbols: cluster.map((s) => ({
2794
+ symbol: s.symbol,
2795
+ isWild: this.isWild(s.symbol),
2796
+ reelIndex: s.reel,
2797
+ posIndex: s.row
2798
+ }))
2799
+ });
2800
+ });
2801
+ for (const win of clusterWins) {
2802
+ this.ctx.services.data.recordSymbolOccurrence({
2803
+ kind: win.kind,
2804
+ symbolId: win.baseSymbol.id,
2805
+ spinType: this.ctx.state.currentSpinType
2806
+ });
2807
+ }
2808
+ this.payout = clusterWins.reduce((sum, c) => sum + c.payout, 0);
2809
+ this.winCombinations = clusterWins;
2810
+ return this;
2811
+ }
2812
+ getNeighbors(ridx, sidx) {
2813
+ const board = this._currentBoard;
2814
+ const neighbors = [];
2815
+ const potentialNeighbors = [
2816
+ [ridx - 1, sidx],
2817
+ [ridx + 1, sidx],
2818
+ [ridx, sidx - 1],
2819
+ [ridx, sidx + 1]
2820
+ ];
2821
+ potentialNeighbors.forEach(([nridx, nsidx]) => {
2822
+ if (board[nridx] && board[nridx][nsidx]) {
2823
+ neighbors.push({ reel: nridx, row: nsidx, symbol: board[nridx][nsidx] });
2824
+ }
2825
+ });
2826
+ return neighbors;
2827
+ }
2828
+ evaluateCluster(rootSymbol, neighbors) {
2829
+ const matchingSymbols = /* @__PURE__ */ new Map();
2830
+ neighbors.forEach((neighbor) => {
2831
+ const { reel, row, symbol } = neighbor;
2832
+ if (this.isChecked(reel, row)) return;
2833
+ if (this.isCheckedWild(reel, row)) return;
2834
+ if (this.isWild(symbol) || symbol.compare(rootSymbol)) {
2835
+ const key = `${reel}-${row}`;
2836
+ matchingSymbols.set(key, { reel, row, symbol });
2837
+ if (symbol.compare(rootSymbol)) {
2838
+ this._checked.push(neighbor);
2839
+ }
2840
+ if (this.isWild(symbol)) {
2841
+ this._checkedWilds.push(neighbor);
2842
+ }
2843
+ const neighbors2 = this.getNeighbors(reel, row);
2844
+ const nestedMatches = this.evaluateCluster(rootSymbol, neighbors2);
2845
+ nestedMatches.forEach((nsym) => {
2846
+ const nkey = `${nsym.reel}-${nsym.row}`;
2847
+ matchingSymbols.set(nkey, nsym);
2848
+ });
2849
+ }
2850
+ });
2851
+ return matchingSymbols;
2852
+ }
2853
+ isChecked(ridx, sidx) {
2854
+ return !!this._checked.find((c) => c.reel === ridx && c.row === sidx);
2855
+ }
2856
+ isCheckedWild(ridx, sidx) {
2857
+ return !!this._checkedWilds.find((c) => c.reel === ridx && c.row === sidx);
2858
+ }
2669
2859
  };
2670
2860
 
2671
2861
  // src/win-types/ManywaysWinType.ts
2672
2862
  var ManywaysWinType = class extends WinType {
2863
+ _checked = [];
2864
+ _checkedWilds = [];
2865
+ constructor(opts) {
2866
+ super(opts);
2867
+ }
2868
+ validateConfig() {
2869
+ }
2870
+ /**
2871
+ * Calculates wins based on the defined paylines and provided board state.\
2872
+ * Retrieve the results using `getWins()` after.
2873
+ */
2874
+ evaluateWins(board) {
2875
+ this.validateConfig();
2876
+ const waysWins = [];
2877
+ const reels = board;
2878
+ const possibleWaysWins = /* @__PURE__ */ new Map();
2879
+ const candidateSymbols = /* @__PURE__ */ new Map();
2880
+ let searchReelIdx = 0;
2881
+ let searchActive = true;
2882
+ while (searchActive && searchReelIdx < reels.length) {
2883
+ const reel = reels[searchReelIdx];
2884
+ let hasWild = false;
2885
+ for (const symbol of reel) {
2886
+ candidateSymbols.set(symbol.id, symbol);
2887
+ if (this.isWild(symbol)) {
2888
+ hasWild = true;
2889
+ }
2890
+ }
2891
+ if (!hasWild) {
2892
+ searchActive = false;
2893
+ }
2894
+ searchReelIdx++;
2895
+ }
2896
+ for (const baseSymbol of candidateSymbols.values()) {
2897
+ let symbolList = {};
2898
+ let isInterrupted = false;
2899
+ for (const [ridx, reel] of reels.entries()) {
2900
+ if (isInterrupted) break;
2901
+ for (const [sidx, symbol] of reel.entries()) {
2902
+ const isMatch = baseSymbol.compare(symbol) || this.isWild(symbol);
2903
+ if (isMatch) {
2904
+ if (!symbolList[ridx]) {
2905
+ symbolList[ridx] = [];
2906
+ }
2907
+ symbolList[ridx].push({ reel: ridx, row: sidx, symbol });
2908
+ }
2909
+ }
2910
+ if (!symbolList[ridx]) {
2911
+ isInterrupted = true;
2912
+ break;
2913
+ }
2914
+ }
2915
+ const minSymLine = Math.min(
2916
+ ...Object.keys(baseSymbol.pays || {}).map((k) => parseInt(k, 10))
2917
+ );
2918
+ const wayLength = this.getWayLength(symbolList);
2919
+ if (wayLength >= minSymLine) {
2920
+ possibleWaysWins.set(baseSymbol.id, symbolList);
2921
+ }
2922
+ }
2923
+ for (const [baseSymbolId, symbolList] of possibleWaysWins.entries()) {
2924
+ const wayLength = this.getWayLength(symbolList);
2925
+ let baseSymbol = Object.values(symbolList).flatMap((l) => l.map((s) => s)).find((s) => !this.isWild(s.symbol))?.symbol;
2926
+ if (!baseSymbol) baseSymbol = symbolList[0][0].symbol;
2927
+ const singleWayPayout = this.getSymbolPayout(baseSymbol, wayLength);
2928
+ const totalWays = Object.values(symbolList).reduce(
2929
+ (ways, syms) => ways * syms.length,
2930
+ 1
2931
+ );
2932
+ const totalPayout = singleWayPayout * totalWays;
2933
+ waysWins.push({
2934
+ kind: wayLength,
2935
+ baseSymbol,
2936
+ symbols: Object.values(symbolList).flatMap(
2937
+ (reel) => reel.map((s) => ({
2938
+ symbol: s.symbol,
2939
+ isWild: this.isWild(s.symbol),
2940
+ reelIndex: s.reel,
2941
+ posIndex: s.row
2942
+ }))
2943
+ ),
2944
+ ways: totalWays,
2945
+ payout: totalPayout
2946
+ });
2947
+ }
2948
+ for (const win of waysWins) {
2949
+ this.ctx.services.data.recordSymbolOccurrence({
2950
+ kind: win.kind,
2951
+ symbolId: win.baseSymbol.id,
2952
+ spinType: this.ctx.state.currentSpinType
2953
+ });
2954
+ }
2955
+ this.payout = waysWins.reduce((sum, l) => sum + l.payout, 0);
2956
+ this.winCombinations = waysWins;
2957
+ return this;
2958
+ }
2959
+ getWayLength(symbolList) {
2960
+ return Math.max(...Object.keys(symbolList).map((k) => parseInt(k, 10))) + 1;
2961
+ }
2962
+ isChecked(ridx, sidx) {
2963
+ return !!this._checked.find((c) => c.reel === ridx && c.row === sidx);
2964
+ }
2965
+ isCheckedWild(ridx, sidx) {
2966
+ return !!this._checkedWilds.find((c) => c.reel === ridx && c.row === sidx);
2967
+ }
2673
2968
  };
2674
2969
 
2675
2970
  // src/reel-set/GeneratedReelSet.ts
@@ -2692,7 +2987,7 @@ var ReelSet = class {
2692
2987
  this.rng = new RandomNumberGenerator();
2693
2988
  this.rng.setSeed(opts.seed ?? 0);
2694
2989
  }
2695
- generateReels(simulation) {
2990
+ generateReels(config) {
2696
2991
  throw new Error("Not implemented");
2697
2992
  }
2698
2993
  /**
@@ -2864,7 +3159,7 @@ var GeneratedReelSet = class extends ReelSet {
2864
3159
  }
2865
3160
  return false;
2866
3161
  }
2867
- generateReels({ gameConfig: config }) {
3162
+ generateReels(config) {
2868
3163
  this.validateConfig(config);
2869
3164
  const gameMode = config.gameModes[this.associatedGameModeName];
2870
3165
  if (!gameMode) {
@@ -2879,7 +3174,7 @@ var GeneratedReelSet = class extends ReelSet {
2879
3174
  const exists = fs5.existsSync(filePath);
2880
3175
  if (exists && !this.overrideExisting) {
2881
3176
  this.reels = this.parseReelsetCSV(filePath, config);
2882
- return;
3177
+ return this;
2883
3178
  }
2884
3179
  if (!exists && this.symbolWeights.size === 0) {
2885
3180
  throw new Error(
@@ -3026,11 +3321,12 @@ var GeneratedReelSet = class extends ReelSet {
3026
3321
  `Generated reelset ${this.id} for game mode ${this.associatedGameModeName}`
3027
3322
  );
3028
3323
  }
3324
+ return this;
3029
3325
  }
3030
3326
  };
3031
3327
 
3032
3328
  // src/reel-set/StaticReelSet.ts
3033
- import assert11 from "assert";
3329
+ import assert12 from "assert";
3034
3330
  var StaticReelSet = class extends ReelSet {
3035
3331
  reels;
3036
3332
  csvPath;
@@ -3040,7 +3336,7 @@ var StaticReelSet = class extends ReelSet {
3040
3336
  this.reels = [];
3041
3337
  this._strReels = opts.reels || [];
3042
3338
  this.csvPath = opts.csvPath || "";
3043
- assert11(
3339
+ assert12(
3044
3340
  opts.reels || opts.csvPath,
3045
3341
  `Either 'reels' or 'csvPath' must be provided for StaticReelSet ${this.id}`
3046
3342
  );
@@ -3061,7 +3357,7 @@ var StaticReelSet = class extends ReelSet {
3061
3357
  );
3062
3358
  }
3063
3359
  }
3064
- generateReels({ gameConfig: config }) {
3360
+ generateReels(config) {
3065
3361
  this.validateConfig(config);
3066
3362
  if (this._strReels.length > 0) {
3067
3363
  this.reels = this._strReels.map((reel) => {
@@ -3079,6 +3375,7 @@ var StaticReelSet = class extends ReelSet {
3079
3375
  if (this.csvPath) {
3080
3376
  this.reels = this.parseReelsetCSV(this.csvPath, config);
3081
3377
  }
3378
+ return this;
3082
3379
  }
3083
3380
  };
3084
3381
 
@@ -3116,6 +3413,18 @@ var StandaloneBoard = class {
3116
3413
  getPaddingBottom() {
3117
3414
  return this.board.paddingBottom;
3118
3415
  }
3416
+ /**
3417
+ * Gets the symbol at the specified reel and row index.
3418
+ */
3419
+ getSymbol(reelIndex, rowIndex) {
3420
+ return this.board.getSymbol(reelIndex, rowIndex);
3421
+ }
3422
+ /**
3423
+ * Sets the symbol at the specified reel and row index.
3424
+ */
3425
+ setSymbol(reelIndex, rowIndex, symbol) {
3426
+ this.board.setSymbol(reelIndex, rowIndex, symbol);
3427
+ }
3119
3428
  resetReels() {
3120
3429
  this.board.resetReels({
3121
3430
  ctx: this.ctx
@@ -3191,8 +3500,8 @@ var StandaloneBoard = class {
3191
3500
  /**
3192
3501
  * Draws a board using specified reel stops.
3193
3502
  */
3194
- drawBoardWithForcedStops(reels, forcedStops) {
3195
- this.drawBoardMixed(reels, forcedStops);
3503
+ drawBoardWithForcedStops(opts) {
3504
+ this.drawBoardMixed(opts.reels, opts.forcedStops, opts.randomOffset);
3196
3505
  }
3197
3506
  /**
3198
3507
  * Draws a board using random reel stops.
@@ -3200,11 +3509,12 @@ var StandaloneBoard = class {
3200
3509
  drawBoardWithRandomStops(reels) {
3201
3510
  this.drawBoardMixed(reels);
3202
3511
  }
3203
- drawBoardMixed(reels, forcedStops) {
3512
+ drawBoardMixed(reels, forcedStops, forcedStopsOffset) {
3204
3513
  this.board.drawBoardMixed({
3205
3514
  ctx: this.ctx,
3206
3515
  reels,
3207
3516
  forcedStops,
3517
+ forcedStopsOffset,
3208
3518
  reelsAmount: this.reelsAmount,
3209
3519
  symbolsPerReel: this.symbolsPerReel,
3210
3520
  padSymbols: this.padSymbols