@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.d.mts +86 -13
- package/dist/index.d.ts +86 -13
- package/dist/index.js +409 -99
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +409 -99
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
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
|
-
|
|
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.
|
|
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(
|
|
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,
|
|
2640
|
+
for (const [lineNumStr, line] of Object.entries(this.lines)) {
|
|
2576
2641
|
const lineNum = Number(lineNumStr);
|
|
2577
|
-
let baseSymbol
|
|
2578
|
-
|
|
2579
|
-
const
|
|
2580
|
-
const
|
|
2581
|
-
|
|
2582
|
-
const
|
|
2583
|
-
|
|
2584
|
-
|
|
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
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
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 (
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
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 (
|
|
2611
|
-
|
|
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
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
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
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
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.
|
|
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(
|
|
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(
|
|
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
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|