@slot-engine/core 0.0.3 → 0.0.5
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 +51 -65
- package/dist/index.d.ts +51 -65
- package/dist/index.js +58 -70
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +58 -70
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -42,7 +42,6 @@ var GameConfig = class _GameConfig {
|
|
|
42
42
|
if (mode.reelSets && mode.reelSets.length > 0) {
|
|
43
43
|
for (const reelGenerator of Object.values(mode.reelSets)) {
|
|
44
44
|
reelGenerator.associatedGameModeName = mode.name;
|
|
45
|
-
reelGenerator.outputDir = this.config.outputDir;
|
|
46
45
|
reelGenerator.generateReels(this);
|
|
47
46
|
}
|
|
48
47
|
} else {
|
|
@@ -62,7 +61,7 @@ var GameConfig = class _GameConfig {
|
|
|
62
61
|
`Reel set with id "${id}" not found in game mode "${gameMode}". Available reel sets: ${this.config.gameModes[gameMode].reelSets.map((rs) => rs.id).join(", ")}`
|
|
63
62
|
);
|
|
64
63
|
}
|
|
65
|
-
return reelSet;
|
|
64
|
+
return reelSet.reels;
|
|
66
65
|
}
|
|
67
66
|
/**
|
|
68
67
|
* Retrieves the number of free spins awarded for a given spin type and scatter count.
|
|
@@ -79,7 +78,7 @@ var GameConfig = class _GameConfig {
|
|
|
79
78
|
/**
|
|
80
79
|
* Retrieves a result set by its criteria within a specific game mode.
|
|
81
80
|
*/
|
|
82
|
-
|
|
81
|
+
getResultSetByCriteria(mode, criteria) {
|
|
83
82
|
const gameMode = this.config.gameModes[mode];
|
|
84
83
|
if (!gameMode) {
|
|
85
84
|
throw new Error(`Game mode "${mode}" not found in game config.`);
|
|
@@ -105,6 +104,7 @@ var GameConfig = class _GameConfig {
|
|
|
105
104
|
};
|
|
106
105
|
|
|
107
106
|
// src/GameMode.ts
|
|
107
|
+
import assert2 from "assert";
|
|
108
108
|
var GameMode = class {
|
|
109
109
|
name;
|
|
110
110
|
reelsAmount;
|
|
@@ -123,14 +123,12 @@ var GameMode = class {
|
|
|
123
123
|
this.reelSets = opts.reelSets;
|
|
124
124
|
this.resultSets = opts.resultSets;
|
|
125
125
|
this.isBonusBuy = opts.isBonusBuy;
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
throw new Error("GameMode must have at least one ResultSet defined.");
|
|
133
|
-
}
|
|
126
|
+
assert2(this.rtp >= 0.9 && this.rtp <= 0.99, "RTP must be between 0.9 and 0.99");
|
|
127
|
+
assert2(
|
|
128
|
+
this.symbolsPerReel.length === this.reelsAmount,
|
|
129
|
+
"symbolsPerReel length must match reelsAmount."
|
|
130
|
+
);
|
|
131
|
+
assert2(this.reelSets.length > 0, "GameMode must have at least one ReelSet defined.");
|
|
134
132
|
}
|
|
135
133
|
};
|
|
136
134
|
|
|
@@ -341,7 +339,6 @@ var ReelGenerator = class {
|
|
|
341
339
|
preferStackedSymbols;
|
|
342
340
|
symbolStacks;
|
|
343
341
|
symbolQuotas;
|
|
344
|
-
outputDir = "";
|
|
345
342
|
csvPath = "";
|
|
346
343
|
overrideExisting;
|
|
347
344
|
rng;
|
|
@@ -349,7 +346,6 @@ var ReelGenerator = class {
|
|
|
349
346
|
this.id = opts.id;
|
|
350
347
|
this.symbolWeights = new Map(Object.entries(opts.symbolWeights));
|
|
351
348
|
this.rowsAmount = opts.rowsAmount || 250;
|
|
352
|
-
this.outputDir = opts.outputDir;
|
|
353
349
|
if (opts.limitSymbolsToReels) this.limitSymbolsToReels = opts.limitSymbolsToReels;
|
|
354
350
|
this.overrideExisting = opts.overrideExisting || false;
|
|
355
351
|
this.spaceBetweenSameSymbols = opts.spaceBetweenSameSymbols;
|
|
@@ -378,34 +374,16 @@ var ReelGenerator = class {
|
|
|
378
374
|
this.rng.setSeed(opts.seed ?? 0);
|
|
379
375
|
}
|
|
380
376
|
validateConfig({ config }) {
|
|
381
|
-
|
|
382
|
-
if (!
|
|
377
|
+
this.symbolWeights.forEach((_, symbol) => {
|
|
378
|
+
if (!config.symbols.has(symbol)) {
|
|
383
379
|
throw new Error(
|
|
384
|
-
|
|
385
|
-
`Symbol "${symbol.id}" is not defined in the symbol weights of the reel generator ${this.id} for mode ${this.associatedGameModeName}.`,
|
|
386
|
-
`Please ensure all symbols have weights defined.
|
|
387
|
-
`
|
|
388
|
-
].join(" ")
|
|
380
|
+
`Symbol "${symbol}" of the reel generator ${this.id} for mode ${this.associatedGameModeName} is not defined in the game config`
|
|
389
381
|
);
|
|
390
382
|
}
|
|
391
383
|
});
|
|
392
|
-
for (const [symbolId, weight] of this.symbolWeights.entries()) {
|
|
393
|
-
if (!config.symbols.has(symbolId)) {
|
|
394
|
-
throw new Error(
|
|
395
|
-
[
|
|
396
|
-
`Symbol "${symbolId}" is defined in the reel generator's symbol weights, but does not exist in the game config.`,
|
|
397
|
-
`Please ensure all symbols in the reel generator are defined in the game config.
|
|
398
|
-
`
|
|
399
|
-
].join(" ")
|
|
400
|
-
);
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
384
|
if (this.limitSymbolsToReels && Object.keys(this.limitSymbolsToReels).length == 0) {
|
|
404
385
|
this.limitSymbolsToReels = void 0;
|
|
405
386
|
}
|
|
406
|
-
if (this.outputDir === "") {
|
|
407
|
-
throw new Error("Output directory must be specified for the ReelGenerator.");
|
|
408
|
-
}
|
|
409
387
|
}
|
|
410
388
|
isSymbolAllowedOnReel(symbolId, reelIdx) {
|
|
411
389
|
if (!this.limitSymbolsToReels) return true;
|
|
@@ -485,6 +463,15 @@ var ReelGenerator = class {
|
|
|
485
463
|
);
|
|
486
464
|
this.csvPath = filePath;
|
|
487
465
|
const exists = fs2.existsSync(filePath);
|
|
466
|
+
if (exists && !this.overrideExisting) {
|
|
467
|
+
this.reels = this.parseReelsetCSV(filePath, gameConf);
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
if (!exists && this.symbolWeights.size === 0) {
|
|
471
|
+
throw new Error(
|
|
472
|
+
`Cannot generate reels for generator "${this.id}" of mode "${this.associatedGameModeName}" because the symbol weights are empty.`
|
|
473
|
+
);
|
|
474
|
+
}
|
|
488
475
|
const reelsAmount = gameMode.reelsAmount;
|
|
489
476
|
const weightsObj = Object.fromEntries(this.symbolWeights);
|
|
490
477
|
for (let ridx = 0; ridx < reelsAmount; ridx++) {
|
|
@@ -619,16 +606,12 @@ var ReelGenerator = class {
|
|
|
619
606
|
}
|
|
620
607
|
const csvString = csvRows.map((row) => row.join(",")).join("\n");
|
|
621
608
|
if (isMainThread) {
|
|
622
|
-
createDirIfNotExists(this.outputDir);
|
|
623
609
|
fs2.writeFileSync(filePath, csvString);
|
|
624
610
|
this.reels = this.parseReelsetCSV(filePath, gameConf);
|
|
625
611
|
console.log(
|
|
626
612
|
`Generated reelset ${this.id} for game mode ${this.associatedGameModeName}`
|
|
627
613
|
);
|
|
628
614
|
}
|
|
629
|
-
if (exists) {
|
|
630
|
-
this.reels = this.parseReelsetCSV(filePath, gameConf);
|
|
631
|
-
}
|
|
632
615
|
}
|
|
633
616
|
/**
|
|
634
617
|
* Reads a reelset CSV file and returns the reels as arrays of GameSymbols.
|
|
@@ -652,11 +635,21 @@ var ReelGenerator = class {
|
|
|
652
635
|
reels[ridx].push(symbol);
|
|
653
636
|
});
|
|
654
637
|
});
|
|
638
|
+
const reelLengths = reels.map((r) => r.length);
|
|
639
|
+
const uniqueLengths = new Set(reelLengths);
|
|
640
|
+
if (uniqueLengths.size > 1) {
|
|
641
|
+
throw new Error(
|
|
642
|
+
`Inconsistent reel lengths in reelset CSV at ${reelSetPath}: ${[
|
|
643
|
+
...uniqueLengths
|
|
644
|
+
].join(", ")}`
|
|
645
|
+
);
|
|
646
|
+
}
|
|
655
647
|
return reels;
|
|
656
648
|
}
|
|
657
649
|
};
|
|
658
650
|
|
|
659
651
|
// src/ResultSet.ts
|
|
652
|
+
import assert3 from "assert";
|
|
660
653
|
var ResultSet = class {
|
|
661
654
|
criteria;
|
|
662
655
|
quota;
|
|
@@ -675,16 +668,11 @@ var ResultSet = class {
|
|
|
675
668
|
this.forceMaxWin = opts.forceMaxWin;
|
|
676
669
|
this.forceFreespins = opts.forceFreespins;
|
|
677
670
|
this.evaluate = opts.evaluate;
|
|
678
|
-
if (this.quota < 0 || this.quota > 1) {
|
|
679
|
-
throw new Error(`Quota must be a float between 0 and 1, got ${this.quota}.`);
|
|
680
|
-
}
|
|
681
671
|
}
|
|
682
672
|
static assignCriteriaToSimulations(ctx, gameModeName) {
|
|
683
673
|
const rng = new RandomNumberGenerator();
|
|
684
674
|
rng.setSeed(0);
|
|
685
|
-
|
|
686
|
-
throw new Error("Simulation configuration is not set.");
|
|
687
|
-
}
|
|
675
|
+
assert3(ctx.simRunsAmount, "Simulation configuration is not set.");
|
|
688
676
|
const simNums = ctx.simRunsAmount[gameModeName];
|
|
689
677
|
const resultSets = ctx.gameConfig.config.gameModes[gameModeName]?.resultSets;
|
|
690
678
|
if (!resultSets || resultSets.length === 0) {
|
|
@@ -693,8 +681,12 @@ var ResultSet = class {
|
|
|
693
681
|
if (simNums === void 0 || simNums <= 0) {
|
|
694
682
|
throw new Error(`No simulations configured for game mode "${gameModeName}".`);
|
|
695
683
|
}
|
|
684
|
+
const totalQuota = resultSets.reduce((sum, rs) => sum + rs.quota, 0);
|
|
696
685
|
const numberOfSimsForCriteria = Object.fromEntries(
|
|
697
|
-
resultSets.map((rs) =>
|
|
686
|
+
resultSets.map((rs) => {
|
|
687
|
+
const normalizedQuota = totalQuota > 0 ? rs.quota / totalQuota : 0;
|
|
688
|
+
return [rs.criteria, Math.max(Math.floor(normalizedQuota * simNums), 1)];
|
|
689
|
+
})
|
|
698
690
|
);
|
|
699
691
|
let totalSims = Object.values(numberOfSimsForCriteria).reduce(
|
|
700
692
|
(sum, num) => sum + num,
|
|
@@ -1049,13 +1041,6 @@ var GameState = class extends GameConfig {
|
|
|
1049
1041
|
this.clearPendingRecords();
|
|
1050
1042
|
this.state.userData = this.config.userState || {};
|
|
1051
1043
|
}
|
|
1052
|
-
/**
|
|
1053
|
-
* Checks if a max win is reached by comparing `wallet.currentWin` to `config.maxWin`.
|
|
1054
|
-
*
|
|
1055
|
-
* Should be called after `wallet.confirmSpinWin()`.
|
|
1056
|
-
*/
|
|
1057
|
-
isMaxWinTriggered() {
|
|
1058
|
-
}
|
|
1059
1044
|
/**
|
|
1060
1045
|
* Empties the list of pending records in the recorder.
|
|
1061
1046
|
*/
|
|
@@ -1501,7 +1486,7 @@ var Board = class extends GameState {
|
|
|
1501
1486
|
return stopPositionsForReels;
|
|
1502
1487
|
}
|
|
1503
1488
|
/**
|
|
1504
|
-
* Selects a random
|
|
1489
|
+
* Selects a random reel set based on the configured weights of the current result set.\
|
|
1505
1490
|
* Returns the reels as arrays of GameSymbols.
|
|
1506
1491
|
*/
|
|
1507
1492
|
getRandomReelset() {
|
|
@@ -1516,7 +1501,7 @@ var Board = class extends GameState {
|
|
|
1516
1501
|
reelSetId = weightedRandom(weights[this.state.currentSpinType], this.state.rng);
|
|
1517
1502
|
}
|
|
1518
1503
|
const reelSet = this.getReelsetById(this.state.currentGameMode, reelSetId);
|
|
1519
|
-
return reelSet
|
|
1504
|
+
return reelSet;
|
|
1520
1505
|
}
|
|
1521
1506
|
/**
|
|
1522
1507
|
* Draws a board using specified reel stops.
|
|
@@ -1772,7 +1757,7 @@ var ManywaysWinType = class extends WinType {
|
|
|
1772
1757
|
};
|
|
1773
1758
|
|
|
1774
1759
|
// src/optimizer/OptimizationConditions.ts
|
|
1775
|
-
import
|
|
1760
|
+
import assert4 from "assert";
|
|
1776
1761
|
var OptimizationConditions = class {
|
|
1777
1762
|
rtp;
|
|
1778
1763
|
avgWin;
|
|
@@ -1783,14 +1768,14 @@ var OptimizationConditions = class {
|
|
|
1783
1768
|
constructor(opts) {
|
|
1784
1769
|
let { rtp, avgWin, hitRate, searchConditions, priority } = opts;
|
|
1785
1770
|
if (rtp == void 0 || rtp === "x") {
|
|
1786
|
-
|
|
1771
|
+
assert4(avgWin !== void 0 && hitRate !== void 0, "If RTP is not specified, hit-rate (hr) and average win amount (av_win) must be given.");
|
|
1787
1772
|
rtp = Math.round(avgWin / Number(hitRate) * 1e5) / 1e5;
|
|
1788
1773
|
}
|
|
1789
1774
|
let noneCount = 0;
|
|
1790
1775
|
for (const val of [rtp, avgWin, hitRate]) {
|
|
1791
1776
|
if (val === void 0) noneCount++;
|
|
1792
1777
|
}
|
|
1793
|
-
|
|
1778
|
+
assert4(noneCount <= 1, "Invalid combination of optimization conditions.");
|
|
1794
1779
|
this.searchRange = [-1, -1];
|
|
1795
1780
|
this.forceSearch = {};
|
|
1796
1781
|
if (typeof searchConditions === "number") {
|
|
@@ -1871,7 +1856,7 @@ var OptimizationParameters = class _OptimizationParameters {
|
|
|
1871
1856
|
// src/Simulation.ts
|
|
1872
1857
|
import fs3 from "fs";
|
|
1873
1858
|
import path2 from "path";
|
|
1874
|
-
import
|
|
1859
|
+
import assert5 from "assert";
|
|
1875
1860
|
import zlib from "zlib";
|
|
1876
1861
|
import { buildSync } from "esbuild";
|
|
1877
1862
|
import { Worker, isMainThread as isMainThread2, parentPort, workerData } from "worker_threads";
|
|
@@ -1895,7 +1880,7 @@ var Simulation = class _Simulation {
|
|
|
1895
1880
|
this.library = /* @__PURE__ */ new Map();
|
|
1896
1881
|
this.records = [];
|
|
1897
1882
|
const gameModeKeys = Object.keys(this.gameConfig.config.gameModes);
|
|
1898
|
-
|
|
1883
|
+
assert5(
|
|
1899
1884
|
Object.values(this.gameConfig.config.gameModes).map((m) => gameModeKeys.includes(m.name)).every((v) => v === true),
|
|
1900
1885
|
"Game mode name must match its key in the gameModes object."
|
|
1901
1886
|
);
|
|
@@ -2246,22 +2231,25 @@ var SimulationContext = class extends Board {
|
|
|
2246
2231
|
this.state.currentGameMode = mode;
|
|
2247
2232
|
this.state.currentSimulationId = simId;
|
|
2248
2233
|
this.state.isCriteriaMet = false;
|
|
2234
|
+
const resultSet = this.getResultSetByCriteria(this.state.currentGameMode, criteria);
|
|
2249
2235
|
while (!this.state.isCriteriaMet) {
|
|
2250
2236
|
this.actualSims++;
|
|
2251
2237
|
this.resetSimulation();
|
|
2252
|
-
const resultSet = this.getGameModeCriteria(this.state.currentGameMode, criteria);
|
|
2253
2238
|
this.state.currentResultSet = resultSet;
|
|
2254
2239
|
this.state.book.criteria = resultSet.criteria;
|
|
2255
2240
|
this.handleGameFlow();
|
|
2256
2241
|
if (resultSet.meetsCriteria(this)) {
|
|
2257
2242
|
this.state.isCriteriaMet = true;
|
|
2258
|
-
this.config.hooks.onSimulationAccepted?.(this);
|
|
2259
|
-
this.record({
|
|
2260
|
-
criteria: resultSet.criteria
|
|
2261
|
-
});
|
|
2262
2243
|
}
|
|
2263
2244
|
}
|
|
2264
2245
|
this.wallet.confirmWins(this);
|
|
2246
|
+
if (this.state.book.getPayout() >= this.config.maxWinX) {
|
|
2247
|
+
this.state.triggeredMaxWin = true;
|
|
2248
|
+
}
|
|
2249
|
+
this.record({
|
|
2250
|
+
criteria: resultSet.criteria
|
|
2251
|
+
});
|
|
2252
|
+
this.config.hooks.onSimulationAccepted?.(this);
|
|
2265
2253
|
this.confirmRecords();
|
|
2266
2254
|
parentPort?.postMessage({
|
|
2267
2255
|
type: "complete",
|
|
@@ -2298,7 +2286,7 @@ var SimulationContext = class extends Board {
|
|
|
2298
2286
|
// src/analysis/index.ts
|
|
2299
2287
|
import fs4 from "fs";
|
|
2300
2288
|
import path3 from "path";
|
|
2301
|
-
import
|
|
2289
|
+
import assert6 from "assert";
|
|
2302
2290
|
|
|
2303
2291
|
// src/analysis/utils.ts
|
|
2304
2292
|
function parseLookupTable(content) {
|
|
@@ -2463,7 +2451,7 @@ var Analysis = class {
|
|
|
2463
2451
|
booksJsonlCompressed
|
|
2464
2452
|
};
|
|
2465
2453
|
for (const p of Object.values(paths[modeStr])) {
|
|
2466
|
-
|
|
2454
|
+
assert6(
|
|
2467
2455
|
fs4.existsSync(p),
|
|
2468
2456
|
`File "${p}" does not exist. Run optimization to auto-create it.`
|
|
2469
2457
|
);
|
|
@@ -2535,7 +2523,7 @@ var Analysis = class {
|
|
|
2535
2523
|
}
|
|
2536
2524
|
getGameModeConfig(mode) {
|
|
2537
2525
|
const config = this.gameConfig.gameModes[mode];
|
|
2538
|
-
|
|
2526
|
+
assert6(config, `Game mode "${mode}" not found in game config`);
|
|
2539
2527
|
return config;
|
|
2540
2528
|
}
|
|
2541
2529
|
};
|
|
@@ -2639,7 +2627,7 @@ function makeSetupFile(optimizer, gameMode) {
|
|
|
2639
2627
|
// src/optimizer/index.ts
|
|
2640
2628
|
import { spawn } from "child_process";
|
|
2641
2629
|
import path6 from "path";
|
|
2642
|
-
import
|
|
2630
|
+
import assert7 from "assert";
|
|
2643
2631
|
import { isMainThread as isMainThread4 } from "worker_threads";
|
|
2644
2632
|
var Optimizer = class {
|
|
2645
2633
|
gameConfig;
|
|
@@ -2683,7 +2671,7 @@ var Optimizer = class {
|
|
|
2683
2671
|
}
|
|
2684
2672
|
}
|
|
2685
2673
|
const criteria = configMode.resultSets.map((r) => r.criteria);
|
|
2686
|
-
|
|
2674
|
+
assert7(
|
|
2687
2675
|
conditions.every((c) => criteria.includes(c)),
|
|
2688
2676
|
`Not all ResultSet criteria in game mode "${k}" are defined as optimization conditions.`
|
|
2689
2677
|
);
|
|
@@ -2695,7 +2683,7 @@ var Optimizer = class {
|
|
|
2695
2683
|
}
|
|
2696
2684
|
gameModeRtp = Math.round(gameModeRtp * 1e3) / 1e3;
|
|
2697
2685
|
paramRtp = Math.round(paramRtp * 1e3) / 1e3;
|
|
2698
|
-
|
|
2686
|
+
assert7(
|
|
2699
2687
|
gameModeRtp === paramRtp,
|
|
2700
2688
|
`Sum of all RTP conditions (${paramRtp}) does not match the game mode RTP (${gameModeRtp}) in game mode "${k}".`
|
|
2701
2689
|
);
|