@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.js CHANGED
@@ -65,7 +65,7 @@ function createGameConfig(opts) {
65
65
  symbols.set(key, value);
66
66
  }
67
67
  const getAnticipationTrigger = (spinType) => {
68
- return Math.min(...Object.keys(opts.scatterToFreespins[spinType]).map(Number)) - 1;
68
+ return Math.min(...Object.keys(opts.scatterToFreespins[spinType] || {}).map(Number)) - 1;
69
69
  };
70
70
  return {
71
71
  padSymbols: opts.padSymbols || 1,
@@ -401,6 +401,16 @@ function createGameState(opts) {
401
401
  };
402
402
  }
403
403
 
404
+ // src/recorder/index.ts
405
+ var Recorder = class {
406
+ records;
407
+ pendingRecords;
408
+ constructor() {
409
+ this.records = [];
410
+ this.pendingRecords = [];
411
+ }
412
+ };
413
+
404
414
  // src/board/index.ts
405
415
  var import_assert3 = __toESM(require("assert"));
406
416
 
@@ -436,6 +446,16 @@ var GameSymbol = class _GameSymbol {
436
446
  return true;
437
447
  }
438
448
  }
449
+ /**
450
+ * Creates a clone of this GameSymbol.
451
+ */
452
+ clone() {
453
+ return new _GameSymbol({
454
+ id: this.id,
455
+ pays: this.pays ? { ...this.pays } : void 0,
456
+ properties: Object.fromEntries(this.properties)
457
+ });
458
+ }
439
459
  };
440
460
 
441
461
  // src/board/index.ts
@@ -470,6 +490,13 @@ var Board = class {
470
490
  this.lastDrawnReelStops = [];
471
491
  this.lastUsedReels = [];
472
492
  }
493
+ getSymbol(reelIndex, rowIndex) {
494
+ return this.reels[reelIndex]?.[rowIndex];
495
+ }
496
+ setSymbol(reelIndex, rowIndex, symbol) {
497
+ this.reels[reelIndex] = this.reels[reelIndex] || [];
498
+ this.reels[reelIndex][rowIndex] = symbol;
499
+ }
473
500
  makeEmptyReels(opts) {
474
501
  const length = opts.reelsAmount ?? opts.ctx.services.game.getCurrentGameMode().reelsAmount;
475
502
  (0, import_assert3.default)(length, "Cannot make empty reels without context or reelsAmount.");
@@ -623,7 +650,11 @@ var Board = class {
623
650
  for (const [r, stopPos] of Object.entries(opts.forcedStops)) {
624
651
  const reelIdx = Number(r);
625
652
  const symCount = symbolsPerReel[reelIdx];
626
- finalReelStops[reelIdx] = stopPos - Math.round(opts.ctx.services.rng.randomFloat(0, symCount - 1));
653
+ if (opts.forcedStopsOffset !== false) {
654
+ finalReelStops[reelIdx] = stopPos - Math.round(opts.ctx.services.rng.randomFloat(0, symCount - 1));
655
+ } else {
656
+ finalReelStops[reelIdx] = stopPos;
657
+ }
627
658
  if (finalReelStops[reelIdx] < 0) {
628
659
  finalReelStops[reelIdx] = opts.reels[reelIdx].length + finalReelStops[reelIdx];
629
660
  }
@@ -666,7 +697,8 @@ var Board = class {
666
697
  );
667
698
  }
668
699
  const reels = this.lastUsedReels;
669
- opts.symbolsToDelete.forEach(({ reelIdx, rowIdx }) => {
700
+ const sortedDeletions = [...opts.symbolsToDelete].sort((a, b) => b.rowIdx - a.rowIdx);
701
+ sortedDeletions.forEach(({ reelIdx, rowIdx }) => {
670
702
  this.reels[reelIdx].splice(rowIdx, 1);
671
703
  });
672
704
  const newFirstSymbolPositions = {};
@@ -692,6 +724,7 @@ var Board = class {
692
724
  }
693
725
  for (let ridx = 0; ridx < reelsAmount; ridx++) {
694
726
  const firstSymbolPos = newFirstSymbolPositions[ridx];
727
+ if (firstSymbolPos === void 0) continue;
695
728
  for (let p = 1; p <= padSymbols; p++) {
696
729
  const topPos = (firstSymbolPos - p + reels[ridx].length) % reels[ridx].length;
697
730
  const padSymbol = reels[ridx][topPos];
@@ -732,6 +765,18 @@ var BoardService = class extends AbstractService {
732
765
  getAnticipation() {
733
766
  return this.board.anticipation;
734
767
  }
768
+ /**
769
+ * Gets the symbol at the specified reel and row index.
770
+ */
771
+ getSymbol(reelIndex, rowIndex) {
772
+ return this.board.getSymbol(reelIndex, rowIndex);
773
+ }
774
+ /**
775
+ * Sets the symbol at the specified reel and row index.
776
+ */
777
+ setSymbol(reelIndex, rowIndex, symbol) {
778
+ this.board.setSymbol(reelIndex, rowIndex, symbol);
779
+ }
735
780
  resetReels() {
736
781
  this.board.resetReels({
737
782
  ctx: this.ctx()
@@ -806,8 +851,8 @@ var BoardService = class extends AbstractService {
806
851
  /**
807
852
  * Draws a board using specified reel stops.
808
853
  */
809
- drawBoardWithForcedStops(reels, forcedStops) {
810
- this.drawBoardMixed(reels, forcedStops);
854
+ drawBoardWithForcedStops(opts) {
855
+ this.drawBoardMixed(opts.reels, opts.forcedStops, opts.randomOffset);
811
856
  }
812
857
  /**
813
858
  * Draws a board using random reel stops.
@@ -815,11 +860,12 @@ var BoardService = class extends AbstractService {
815
860
  drawBoardWithRandomStops(reels) {
816
861
  this.drawBoardMixed(reels);
817
862
  }
818
- drawBoardMixed(reels, forcedStops) {
863
+ drawBoardMixed(reels, forcedStops, forcedStopsOffset) {
819
864
  this.board.drawBoardMixed({
820
865
  ctx: this.ctx(),
821
866
  reels,
822
- forcedStops
867
+ forcedStops,
868
+ forcedStopsOffset
823
869
  });
824
870
  }
825
871
  /**
@@ -919,6 +965,25 @@ var GameService = class extends AbstractService {
919
965
  constructor(ctx) {
920
966
  super(ctx);
921
967
  }
968
+ /**
969
+ * Intended for internal use only.\
970
+ * Generates reels for all reel sets in the game configuration.
971
+ */
972
+ _generateReels() {
973
+ const config = this.ctx().config;
974
+ for (const mode of Object.values(config.gameModes)) {
975
+ if (mode.reelSets && mode.reelSets.length > 0) {
976
+ for (const reelSet of Object.values(mode.reelSets)) {
977
+ reelSet.associatedGameModeName = mode.name;
978
+ reelSet.generateReels(config);
979
+ }
980
+ } else {
981
+ throw new Error(
982
+ `Game mode "${mode.name}" has no reel sets defined. Cannot generate reelset files.`
983
+ );
984
+ }
985
+ }
986
+ }
922
987
  /**
923
988
  * Retrieves a reel set by its ID within a specific game mode.
924
989
  */
@@ -1157,16 +1222,6 @@ var Book = class _Book {
1157
1222
  }
1158
1223
  };
1159
1224
 
1160
- // src/recorder/index.ts
1161
- var Recorder = class {
1162
- records;
1163
- pendingRecords;
1164
- constructor() {
1165
- this.records = [];
1166
- this.pendingRecords = [];
1167
- }
1168
- };
1169
-
1170
1225
  // src/wallet/index.ts
1171
1226
  var Wallet = class {
1172
1227
  /**
@@ -1810,7 +1865,7 @@ Simulating game mode: ${mode}`);
1810
1865
  if (mode.reelSets && mode.reelSets.length > 0) {
1811
1866
  for (const reelSet of Object.values(mode.reelSets)) {
1812
1867
  reelSet.associatedGameModeName = mode.name;
1813
- reelSet.generateReels(this);
1868
+ reelSet.generateReels(this.gameConfig);
1814
1869
  }
1815
1870
  } else {
1816
1871
  throw new Error(
@@ -2579,9 +2634,20 @@ var WinType = class {
2579
2634
  isWild(symbol) {
2580
2635
  return !!this.wildSymbol && symbol.compare(this.wildSymbol);
2581
2636
  }
2637
+ getSymbolPayout(symbol, count) {
2638
+ if (!symbol.pays) return 0;
2639
+ let clusterSize = 0;
2640
+ const sizes = Object.keys(symbol.pays).map((s) => parseInt(s, 10)).filter((n) => Number.isFinite(n)).sort((a, b) => a - b);
2641
+ for (const size of sizes) {
2642
+ if (size > count) break;
2643
+ clusterSize = size;
2644
+ }
2645
+ return symbol.pays[clusterSize] || 0;
2646
+ }
2582
2647
  };
2583
2648
 
2584
2649
  // src/win-types/LinesWinType.ts
2650
+ var import_assert11 = __toESM(require("assert"));
2585
2651
  var LinesWinType = class extends WinType {
2586
2652
  lines;
2587
2653
  constructor(opts) {
@@ -2622,106 +2688,335 @@ var LinesWinType = class extends WinType {
2622
2688
  evaluateWins(board) {
2623
2689
  this.validateConfig();
2624
2690
  const lineWins = [];
2625
- let payout = 0;
2626
2691
  const reels = board;
2627
- for (const [lineNumStr, lineDef] of Object.entries(this.lines)) {
2692
+ for (const [lineNumStr, line] of Object.entries(this.lines)) {
2628
2693
  const lineNum = Number(lineNumStr);
2629
- let baseSymbol = null;
2630
- let leadingWilds = 0;
2631
- const chain = [];
2632
- const details = [];
2633
- for (let ridx = 0; ridx < reels.length; ridx++) {
2634
- const rowIdx = lineDef[ridx];
2635
- const sym = reels[ridx][rowIdx];
2636
- if (!sym) throw new Error("Encountered an invalid symbol while evaluating wins.");
2637
- const wild = this.isWild(sym);
2638
- if (ridx === 0) {
2639
- chain.push(sym);
2640
- details.push({ reelIndex: ridx, posIndex: rowIdx, symbol: sym, isWild: wild });
2641
- if (wild) leadingWilds++;
2642
- else baseSymbol = sym;
2643
- continue;
2694
+ let baseSymbol;
2695
+ const potentialWinLine = [];
2696
+ const potentialWildLine = [];
2697
+ for (const [ridx, reel] of reels.entries()) {
2698
+ const sidx = line[ridx];
2699
+ const thisSymbol = reel[sidx];
2700
+ if (!baseSymbol) {
2701
+ baseSymbol = thisSymbol;
2644
2702
  }
2645
- if (wild) {
2646
- chain.push(sym);
2647
- details.push({
2648
- reelIndex: ridx,
2649
- posIndex: rowIdx,
2650
- symbol: sym,
2651
- isWild: true,
2652
- substitutedFor: baseSymbol || void 0
2653
- });
2703
+ (0, import_assert11.default)(baseSymbol, `No symbol found at line ${lineNum}, reel ${ridx}`);
2704
+ (0, import_assert11.default)(thisSymbol, `No symbol found at line ${lineNum}, reel ${ridx}`);
2705
+ if (potentialWinLine.length == 0) {
2706
+ if (this.isWild(thisSymbol)) {
2707
+ potentialWildLine.push({ reel: ridx, row: sidx, symbol: thisSymbol });
2708
+ }
2709
+ potentialWinLine.push({ reel: ridx, row: sidx, symbol: thisSymbol });
2654
2710
  continue;
2655
2711
  }
2656
- if (!baseSymbol) {
2657
- baseSymbol = sym;
2658
- chain.push(sym);
2659
- details.push({ reelIndex: ridx, posIndex: rowIdx, symbol: sym, isWild: false });
2712
+ if (this.isWild(baseSymbol)) {
2713
+ if (this.isWild(thisSymbol)) {
2714
+ potentialWildLine.push({ reel: ridx, row: sidx, symbol: thisSymbol });
2715
+ } else {
2716
+ baseSymbol = thisSymbol;
2717
+ }
2718
+ potentialWinLine.push({ reel: ridx, row: sidx, symbol: thisSymbol });
2660
2719
  continue;
2661
2720
  }
2662
- if (sym.id === baseSymbol.id) {
2663
- chain.push(sym);
2664
- details.push({ reelIndex: ridx, posIndex: rowIdx, symbol: sym, isWild: false });
2665
- continue;
2721
+ if (baseSymbol.compare(thisSymbol) || this.isWild(thisSymbol)) {
2722
+ potentialWinLine.push({ reel: ridx, row: sidx, symbol: thisSymbol });
2666
2723
  }
2667
- break;
2668
- }
2669
- if (chain.length === 0) continue;
2670
- const allWild = chain.every((s) => this.isWild(s));
2671
- const wildRepresentative = this.wildSymbol instanceof GameSymbol ? this.wildSymbol : null;
2672
- const len = chain.length;
2673
- let bestPayout = 0;
2674
- let bestType = null;
2675
- let payingSymbol = null;
2676
- if (baseSymbol?.pays && baseSymbol.pays[len]) {
2677
- bestPayout = baseSymbol.pays[len];
2678
- bestType = "substituted";
2679
- payingSymbol = baseSymbol;
2680
2724
  }
2681
- if (allWild && wildRepresentative?.pays && wildRepresentative.pays[len]) {
2682
- const wildPay = wildRepresentative.pays[len];
2683
- if (wildPay > bestPayout) {
2684
- bestPayout = wildPay;
2685
- bestType = "pure-wild";
2686
- payingSymbol = wildRepresentative;
2687
- }
2688
- }
2689
- if (!bestPayout || !bestType || !payingSymbol) continue;
2690
- const minLen = payingSymbol.pays ? Math.min(...Object.keys(payingSymbol.pays).map(Number)) : Infinity;
2691
- if (len < minLen) continue;
2692
- const wildCount = details.filter((d) => d.isWild).length;
2693
- const nonWildCount = len - wildCount;
2694
- lineWins.push({
2725
+ const minSymLine = Math.min(
2726
+ ...Object.keys(baseSymbol.pays || {}).map((k) => parseInt(k, 10))
2727
+ );
2728
+ if (potentialWinLine.length < minSymLine) continue;
2729
+ const linePayout = this.getLinePayout(potentialWinLine);
2730
+ const wildLinePayout = this.getLinePayout(potentialWildLine);
2731
+ let finalLine = {
2732
+ kind: potentialWinLine.length,
2733
+ baseSymbol,
2734
+ symbols: potentialWinLine.map((s) => ({
2735
+ symbol: s.symbol,
2736
+ isWild: this.isWild(s.symbol),
2737
+ reelIndex: s.reel,
2738
+ posIndex: s.row
2739
+ })),
2695
2740
  lineNumber: lineNum,
2696
- kind: len,
2697
- payout: bestPayout,
2698
- symbol: payingSymbol,
2699
- winType: bestType,
2700
- substitutedBaseSymbol: bestType === "pure-wild" ? null : baseSymbol,
2701
- symbols: details,
2702
- stats: { wildCount, nonWildCount, leadingWilds }
2703
- });
2704
- payout += bestPayout;
2741
+ payout: linePayout
2742
+ };
2743
+ if (wildLinePayout > linePayout) {
2744
+ baseSymbol = potentialWildLine[0]?.symbol;
2745
+ finalLine = {
2746
+ kind: potentialWildLine.length,
2747
+ baseSymbol,
2748
+ symbols: potentialWildLine.map((s) => ({
2749
+ symbol: s.symbol,
2750
+ isWild: this.isWild(s.symbol),
2751
+ reelIndex: s.reel,
2752
+ posIndex: s.row
2753
+ })),
2754
+ lineNumber: lineNum,
2755
+ payout: wildLinePayout
2756
+ };
2757
+ }
2758
+ lineWins.push(finalLine);
2705
2759
  }
2706
2760
  for (const win of lineWins) {
2707
2761
  this.ctx.services.data.recordSymbolOccurrence({
2708
2762
  kind: win.kind,
2709
- symbolId: win.symbol.id,
2763
+ symbolId: win.baseSymbol.id,
2710
2764
  spinType: this.ctx.state.currentSpinType
2711
2765
  });
2712
2766
  }
2713
- this.payout = payout;
2767
+ this.payout = lineWins.reduce((sum, l) => sum + l.payout, 0);
2714
2768
  this.winCombinations = lineWins;
2715
2769
  return this;
2716
2770
  }
2771
+ getLinePayout(line) {
2772
+ if (line.length === 0) return 0;
2773
+ let baseSymbol = line.find((s) => !this.isWild(s.symbol))?.symbol;
2774
+ if (!baseSymbol) baseSymbol = line[0].symbol;
2775
+ const kind = line.length;
2776
+ const payout = this.getSymbolPayout(baseSymbol, kind);
2777
+ return payout;
2778
+ }
2717
2779
  };
2718
2780
 
2719
2781
  // src/win-types/ClusterWinType.ts
2720
2782
  var ClusterWinType = class extends WinType {
2783
+ _checked = [];
2784
+ _checkedWilds = [];
2785
+ _currentBoard = [];
2786
+ constructor(opts) {
2787
+ super(opts);
2788
+ }
2789
+ validateConfig() {
2790
+ }
2791
+ /**
2792
+ * Calculates wins based on symbol cluster size and provided board state.\
2793
+ * Retrieve the results using `getWins()` after.
2794
+ */
2795
+ evaluateWins(board) {
2796
+ this.validateConfig();
2797
+ this._checked = [];
2798
+ this._currentBoard = board;
2799
+ const clusterWins = [];
2800
+ const potentialClusters = [];
2801
+ for (const [ridx, reel] of board.entries()) {
2802
+ for (const [sidx, symbol] of reel.entries()) {
2803
+ this._checkedWilds = [];
2804
+ if (this.isWild(symbol)) continue;
2805
+ if (this.isChecked(ridx, sidx)) {
2806
+ continue;
2807
+ }
2808
+ const thisSymbol = { reel: ridx, row: sidx, symbol };
2809
+ this._checked.push(thisSymbol);
2810
+ const neighbors = this.getNeighbors(ridx, sidx);
2811
+ const matchingSymbols = this.evaluateCluster(symbol, neighbors);
2812
+ if (matchingSymbols.size >= 1) {
2813
+ potentialClusters.push([thisSymbol, ...matchingSymbols.values()]);
2814
+ }
2815
+ }
2816
+ }
2817
+ for (const [ridx, reel] of board.entries()) {
2818
+ for (const [sidx, symbol] of reel.entries()) {
2819
+ this._checkedWilds = [];
2820
+ if (!this.isWild(symbol)) continue;
2821
+ if (this.isChecked(ridx, sidx)) {
2822
+ continue;
2823
+ }
2824
+ const thisSymbol = { reel: ridx, row: sidx, symbol };
2825
+ this._checked.push(thisSymbol);
2826
+ const neighbors = this.getNeighbors(ridx, sidx);
2827
+ const matchingSymbols = this.evaluateCluster(symbol, neighbors);
2828
+ if (matchingSymbols.size >= 1) {
2829
+ potentialClusters.push([thisSymbol, ...matchingSymbols.values()]);
2830
+ }
2831
+ }
2832
+ }
2833
+ potentialClusters.forEach((cluster) => {
2834
+ const kind = cluster.length;
2835
+ let baseSymbol = cluster.find((s) => !this.isWild(s.symbol))?.symbol;
2836
+ if (!baseSymbol) baseSymbol = cluster[0].symbol;
2837
+ const payout = this.getSymbolPayout(baseSymbol, kind);
2838
+ if (!baseSymbol.pays || Object.keys(baseSymbol.pays).length === 0) {
2839
+ return;
2840
+ }
2841
+ clusterWins.push({
2842
+ payout,
2843
+ kind,
2844
+ baseSymbol,
2845
+ symbols: cluster.map((s) => ({
2846
+ symbol: s.symbol,
2847
+ isWild: this.isWild(s.symbol),
2848
+ reelIndex: s.reel,
2849
+ posIndex: s.row
2850
+ }))
2851
+ });
2852
+ });
2853
+ for (const win of clusterWins) {
2854
+ this.ctx.services.data.recordSymbolOccurrence({
2855
+ kind: win.kind,
2856
+ symbolId: win.baseSymbol.id,
2857
+ spinType: this.ctx.state.currentSpinType
2858
+ });
2859
+ }
2860
+ this.payout = clusterWins.reduce((sum, c) => sum + c.payout, 0);
2861
+ this.winCombinations = clusterWins;
2862
+ return this;
2863
+ }
2864
+ getNeighbors(ridx, sidx) {
2865
+ const board = this._currentBoard;
2866
+ const neighbors = [];
2867
+ const potentialNeighbors = [
2868
+ [ridx - 1, sidx],
2869
+ [ridx + 1, sidx],
2870
+ [ridx, sidx - 1],
2871
+ [ridx, sidx + 1]
2872
+ ];
2873
+ potentialNeighbors.forEach(([nridx, nsidx]) => {
2874
+ if (board[nridx] && board[nridx][nsidx]) {
2875
+ neighbors.push({ reel: nridx, row: nsidx, symbol: board[nridx][nsidx] });
2876
+ }
2877
+ });
2878
+ return neighbors;
2879
+ }
2880
+ evaluateCluster(rootSymbol, neighbors) {
2881
+ const matchingSymbols = /* @__PURE__ */ new Map();
2882
+ neighbors.forEach((neighbor) => {
2883
+ const { reel, row, symbol } = neighbor;
2884
+ if (this.isChecked(reel, row)) return;
2885
+ if (this.isCheckedWild(reel, row)) return;
2886
+ if (this.isWild(symbol) || symbol.compare(rootSymbol)) {
2887
+ const key = `${reel}-${row}`;
2888
+ matchingSymbols.set(key, { reel, row, symbol });
2889
+ if (symbol.compare(rootSymbol)) {
2890
+ this._checked.push(neighbor);
2891
+ }
2892
+ if (this.isWild(symbol)) {
2893
+ this._checkedWilds.push(neighbor);
2894
+ }
2895
+ const neighbors2 = this.getNeighbors(reel, row);
2896
+ const nestedMatches = this.evaluateCluster(rootSymbol, neighbors2);
2897
+ nestedMatches.forEach((nsym) => {
2898
+ const nkey = `${nsym.reel}-${nsym.row}`;
2899
+ matchingSymbols.set(nkey, nsym);
2900
+ });
2901
+ }
2902
+ });
2903
+ return matchingSymbols;
2904
+ }
2905
+ isChecked(ridx, sidx) {
2906
+ return !!this._checked.find((c) => c.reel === ridx && c.row === sidx);
2907
+ }
2908
+ isCheckedWild(ridx, sidx) {
2909
+ return !!this._checkedWilds.find((c) => c.reel === ridx && c.row === sidx);
2910
+ }
2721
2911
  };
2722
2912
 
2723
2913
  // src/win-types/ManywaysWinType.ts
2724
2914
  var ManywaysWinType = class extends WinType {
2915
+ _checked = [];
2916
+ _checkedWilds = [];
2917
+ constructor(opts) {
2918
+ super(opts);
2919
+ }
2920
+ validateConfig() {
2921
+ }
2922
+ /**
2923
+ * Calculates wins based on the defined paylines and provided board state.\
2924
+ * Retrieve the results using `getWins()` after.
2925
+ */
2926
+ evaluateWins(board) {
2927
+ this.validateConfig();
2928
+ const waysWins = [];
2929
+ const reels = board;
2930
+ const possibleWaysWins = /* @__PURE__ */ new Map();
2931
+ const candidateSymbols = /* @__PURE__ */ new Map();
2932
+ let searchReelIdx = 0;
2933
+ let searchActive = true;
2934
+ while (searchActive && searchReelIdx < reels.length) {
2935
+ const reel = reels[searchReelIdx];
2936
+ let hasWild = false;
2937
+ for (const symbol of reel) {
2938
+ candidateSymbols.set(symbol.id, symbol);
2939
+ if (this.isWild(symbol)) {
2940
+ hasWild = true;
2941
+ }
2942
+ }
2943
+ if (!hasWild) {
2944
+ searchActive = false;
2945
+ }
2946
+ searchReelIdx++;
2947
+ }
2948
+ for (const baseSymbol of candidateSymbols.values()) {
2949
+ let symbolList = {};
2950
+ let isInterrupted = false;
2951
+ for (const [ridx, reel] of reels.entries()) {
2952
+ if (isInterrupted) break;
2953
+ for (const [sidx, symbol] of reel.entries()) {
2954
+ const isMatch = baseSymbol.compare(symbol) || this.isWild(symbol);
2955
+ if (isMatch) {
2956
+ if (!symbolList[ridx]) {
2957
+ symbolList[ridx] = [];
2958
+ }
2959
+ symbolList[ridx].push({ reel: ridx, row: sidx, symbol });
2960
+ }
2961
+ }
2962
+ if (!symbolList[ridx]) {
2963
+ isInterrupted = true;
2964
+ break;
2965
+ }
2966
+ }
2967
+ const minSymLine = Math.min(
2968
+ ...Object.keys(baseSymbol.pays || {}).map((k) => parseInt(k, 10))
2969
+ );
2970
+ const wayLength = this.getWayLength(symbolList);
2971
+ if (wayLength >= minSymLine) {
2972
+ possibleWaysWins.set(baseSymbol.id, symbolList);
2973
+ }
2974
+ }
2975
+ for (const [baseSymbolId, symbolList] of possibleWaysWins.entries()) {
2976
+ const wayLength = this.getWayLength(symbolList);
2977
+ let baseSymbol = Object.values(symbolList).flatMap((l) => l.map((s) => s)).find((s) => !this.isWild(s.symbol))?.symbol;
2978
+ if (!baseSymbol) baseSymbol = symbolList[0][0].symbol;
2979
+ const singleWayPayout = this.getSymbolPayout(baseSymbol, wayLength);
2980
+ const totalWays = Object.values(symbolList).reduce(
2981
+ (ways, syms) => ways * syms.length,
2982
+ 1
2983
+ );
2984
+ const totalPayout = singleWayPayout * totalWays;
2985
+ waysWins.push({
2986
+ kind: wayLength,
2987
+ baseSymbol,
2988
+ symbols: Object.values(symbolList).flatMap(
2989
+ (reel) => reel.map((s) => ({
2990
+ symbol: s.symbol,
2991
+ isWild: this.isWild(s.symbol),
2992
+ reelIndex: s.reel,
2993
+ posIndex: s.row
2994
+ }))
2995
+ ),
2996
+ ways: totalWays,
2997
+ payout: totalPayout
2998
+ });
2999
+ }
3000
+ for (const win of waysWins) {
3001
+ this.ctx.services.data.recordSymbolOccurrence({
3002
+ kind: win.kind,
3003
+ symbolId: win.baseSymbol.id,
3004
+ spinType: this.ctx.state.currentSpinType
3005
+ });
3006
+ }
3007
+ this.payout = waysWins.reduce((sum, l) => sum + l.payout, 0);
3008
+ this.winCombinations = waysWins;
3009
+ return this;
3010
+ }
3011
+ getWayLength(symbolList) {
3012
+ return Math.max(...Object.keys(symbolList).map((k) => parseInt(k, 10))) + 1;
3013
+ }
3014
+ isChecked(ridx, sidx) {
3015
+ return !!this._checked.find((c) => c.reel === ridx && c.row === sidx);
3016
+ }
3017
+ isCheckedWild(ridx, sidx) {
3018
+ return !!this._checkedWilds.find((c) => c.reel === ridx && c.row === sidx);
3019
+ }
2725
3020
  };
2726
3021
 
2727
3022
  // src/reel-set/GeneratedReelSet.ts
@@ -2744,7 +3039,7 @@ var ReelSet = class {
2744
3039
  this.rng = new RandomNumberGenerator();
2745
3040
  this.rng.setSeed(opts.seed ?? 0);
2746
3041
  }
2747
- generateReels(simulation) {
3042
+ generateReels(config) {
2748
3043
  throw new Error("Not implemented");
2749
3044
  }
2750
3045
  /**
@@ -2916,7 +3211,7 @@ var GeneratedReelSet = class extends ReelSet {
2916
3211
  }
2917
3212
  return false;
2918
3213
  }
2919
- generateReels({ gameConfig: config }) {
3214
+ generateReels(config) {
2920
3215
  this.validateConfig(config);
2921
3216
  const gameMode = config.gameModes[this.associatedGameModeName];
2922
3217
  if (!gameMode) {
@@ -2931,7 +3226,7 @@ var GeneratedReelSet = class extends ReelSet {
2931
3226
  const exists = import_fs5.default.existsSync(filePath);
2932
3227
  if (exists && !this.overrideExisting) {
2933
3228
  this.reels = this.parseReelsetCSV(filePath, config);
2934
- return;
3229
+ return this;
2935
3230
  }
2936
3231
  if (!exists && this.symbolWeights.size === 0) {
2937
3232
  throw new Error(
@@ -3078,11 +3373,12 @@ var GeneratedReelSet = class extends ReelSet {
3078
3373
  `Generated reelset ${this.id} for game mode ${this.associatedGameModeName}`
3079
3374
  );
3080
3375
  }
3376
+ return this;
3081
3377
  }
3082
3378
  };
3083
3379
 
3084
3380
  // src/reel-set/StaticReelSet.ts
3085
- var import_assert11 = __toESM(require("assert"));
3381
+ var import_assert12 = __toESM(require("assert"));
3086
3382
  var StaticReelSet = class extends ReelSet {
3087
3383
  reels;
3088
3384
  csvPath;
@@ -3092,7 +3388,7 @@ var StaticReelSet = class extends ReelSet {
3092
3388
  this.reels = [];
3093
3389
  this._strReels = opts.reels || [];
3094
3390
  this.csvPath = opts.csvPath || "";
3095
- (0, import_assert11.default)(
3391
+ (0, import_assert12.default)(
3096
3392
  opts.reels || opts.csvPath,
3097
3393
  `Either 'reels' or 'csvPath' must be provided for StaticReelSet ${this.id}`
3098
3394
  );
@@ -3113,7 +3409,7 @@ var StaticReelSet = class extends ReelSet {
3113
3409
  );
3114
3410
  }
3115
3411
  }
3116
- generateReels({ gameConfig: config }) {
3412
+ generateReels(config) {
3117
3413
  this.validateConfig(config);
3118
3414
  if (this._strReels.length > 0) {
3119
3415
  this.reels = this._strReels.map((reel) => {
@@ -3131,6 +3427,7 @@ var StaticReelSet = class extends ReelSet {
3131
3427
  if (this.csvPath) {
3132
3428
  this.reels = this.parseReelsetCSV(this.csvPath, config);
3133
3429
  }
3430
+ return this;
3134
3431
  }
3135
3432
  };
3136
3433
 
@@ -3168,6 +3465,18 @@ var StandaloneBoard = class {
3168
3465
  getPaddingBottom() {
3169
3466
  return this.board.paddingBottom;
3170
3467
  }
3468
+ /**
3469
+ * Gets the symbol at the specified reel and row index.
3470
+ */
3471
+ getSymbol(reelIndex, rowIndex) {
3472
+ return this.board.getSymbol(reelIndex, rowIndex);
3473
+ }
3474
+ /**
3475
+ * Sets the symbol at the specified reel and row index.
3476
+ */
3477
+ setSymbol(reelIndex, rowIndex, symbol) {
3478
+ this.board.setSymbol(reelIndex, rowIndex, symbol);
3479
+ }
3171
3480
  resetReels() {
3172
3481
  this.board.resetReels({
3173
3482
  ctx: this.ctx
@@ -3243,8 +3552,8 @@ var StandaloneBoard = class {
3243
3552
  /**
3244
3553
  * Draws a board using specified reel stops.
3245
3554
  */
3246
- drawBoardWithForcedStops(reels, forcedStops) {
3247
- this.drawBoardMixed(reels, forcedStops);
3555
+ drawBoardWithForcedStops(opts) {
3556
+ this.drawBoardMixed(opts.reels, opts.forcedStops, opts.randomOffset);
3248
3557
  }
3249
3558
  /**
3250
3559
  * Draws a board using random reel stops.
@@ -3252,11 +3561,12 @@ var StandaloneBoard = class {
3252
3561
  drawBoardWithRandomStops(reels) {
3253
3562
  this.drawBoardMixed(reels);
3254
3563
  }
3255
- drawBoardMixed(reels, forcedStops) {
3564
+ drawBoardMixed(reels, forcedStops, forcedStopsOffset) {
3256
3565
  this.board.drawBoardMixed({
3257
3566
  ctx: this.ctx,
3258
3567
  reels,
3259
3568
  forcedStops,
3569
+ forcedStopsOffset,
3260
3570
  reelsAmount: this.reelsAmount,
3261
3571
  symbolsPerReel: this.symbolsPerReel,
3262
3572
  padSymbols: this.padSymbols