@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.js
CHANGED
|
@@ -96,7 +96,6 @@ var GameConfig = class _GameConfig {
|
|
|
96
96
|
if (mode.reelSets && mode.reelSets.length > 0) {
|
|
97
97
|
for (const reelGenerator of Object.values(mode.reelSets)) {
|
|
98
98
|
reelGenerator.associatedGameModeName = mode.name;
|
|
99
|
-
reelGenerator.outputDir = this.config.outputDir;
|
|
100
99
|
reelGenerator.generateReels(this);
|
|
101
100
|
}
|
|
102
101
|
} else {
|
|
@@ -116,7 +115,7 @@ var GameConfig = class _GameConfig {
|
|
|
116
115
|
`Reel set with id "${id}" not found in game mode "${gameMode}". Available reel sets: ${this.config.gameModes[gameMode].reelSets.map((rs) => rs.id).join(", ")}`
|
|
117
116
|
);
|
|
118
117
|
}
|
|
119
|
-
return reelSet;
|
|
118
|
+
return reelSet.reels;
|
|
120
119
|
}
|
|
121
120
|
/**
|
|
122
121
|
* Retrieves the number of free spins awarded for a given spin type and scatter count.
|
|
@@ -133,7 +132,7 @@ var GameConfig = class _GameConfig {
|
|
|
133
132
|
/**
|
|
134
133
|
* Retrieves a result set by its criteria within a specific game mode.
|
|
135
134
|
*/
|
|
136
|
-
|
|
135
|
+
getResultSetByCriteria(mode, criteria) {
|
|
137
136
|
const gameMode = this.config.gameModes[mode];
|
|
138
137
|
if (!gameMode) {
|
|
139
138
|
throw new Error(`Game mode "${mode}" not found in game config.`);
|
|
@@ -159,6 +158,7 @@ var GameConfig = class _GameConfig {
|
|
|
159
158
|
};
|
|
160
159
|
|
|
161
160
|
// src/GameMode.ts
|
|
161
|
+
var import_assert2 = __toESM(require("assert"));
|
|
162
162
|
var GameMode = class {
|
|
163
163
|
name;
|
|
164
164
|
reelsAmount;
|
|
@@ -177,14 +177,12 @@ var GameMode = class {
|
|
|
177
177
|
this.reelSets = opts.reelSets;
|
|
178
178
|
this.resultSets = opts.resultSets;
|
|
179
179
|
this.isBonusBuy = opts.isBonusBuy;
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
throw new Error("GameMode must have at least one ResultSet defined.");
|
|
187
|
-
}
|
|
180
|
+
(0, import_assert2.default)(this.rtp >= 0.9 && this.rtp <= 0.99, "RTP must be between 0.9 and 0.99");
|
|
181
|
+
(0, import_assert2.default)(
|
|
182
|
+
this.symbolsPerReel.length === this.reelsAmount,
|
|
183
|
+
"symbolsPerReel length must match reelsAmount."
|
|
184
|
+
);
|
|
185
|
+
(0, import_assert2.default)(this.reelSets.length > 0, "GameMode must have at least one ReelSet defined.");
|
|
188
186
|
}
|
|
189
187
|
};
|
|
190
188
|
|
|
@@ -395,7 +393,6 @@ var ReelGenerator = class {
|
|
|
395
393
|
preferStackedSymbols;
|
|
396
394
|
symbolStacks;
|
|
397
395
|
symbolQuotas;
|
|
398
|
-
outputDir = "";
|
|
399
396
|
csvPath = "";
|
|
400
397
|
overrideExisting;
|
|
401
398
|
rng;
|
|
@@ -403,7 +400,6 @@ var ReelGenerator = class {
|
|
|
403
400
|
this.id = opts.id;
|
|
404
401
|
this.symbolWeights = new Map(Object.entries(opts.symbolWeights));
|
|
405
402
|
this.rowsAmount = opts.rowsAmount || 250;
|
|
406
|
-
this.outputDir = opts.outputDir;
|
|
407
403
|
if (opts.limitSymbolsToReels) this.limitSymbolsToReels = opts.limitSymbolsToReels;
|
|
408
404
|
this.overrideExisting = opts.overrideExisting || false;
|
|
409
405
|
this.spaceBetweenSameSymbols = opts.spaceBetweenSameSymbols;
|
|
@@ -432,34 +428,16 @@ var ReelGenerator = class {
|
|
|
432
428
|
this.rng.setSeed(opts.seed ?? 0);
|
|
433
429
|
}
|
|
434
430
|
validateConfig({ config }) {
|
|
435
|
-
|
|
436
|
-
if (!
|
|
431
|
+
this.symbolWeights.forEach((_, symbol) => {
|
|
432
|
+
if (!config.symbols.has(symbol)) {
|
|
437
433
|
throw new Error(
|
|
438
|
-
|
|
439
|
-
`Symbol "${symbol.id}" is not defined in the symbol weights of the reel generator ${this.id} for mode ${this.associatedGameModeName}.`,
|
|
440
|
-
`Please ensure all symbols have weights defined.
|
|
441
|
-
`
|
|
442
|
-
].join(" ")
|
|
434
|
+
`Symbol "${symbol}" of the reel generator ${this.id} for mode ${this.associatedGameModeName} is not defined in the game config`
|
|
443
435
|
);
|
|
444
436
|
}
|
|
445
437
|
});
|
|
446
|
-
for (const [symbolId, weight] of this.symbolWeights.entries()) {
|
|
447
|
-
if (!config.symbols.has(symbolId)) {
|
|
448
|
-
throw new Error(
|
|
449
|
-
[
|
|
450
|
-
`Symbol "${symbolId}" is defined in the reel generator's symbol weights, but does not exist in the game config.`,
|
|
451
|
-
`Please ensure all symbols in the reel generator are defined in the game config.
|
|
452
|
-
`
|
|
453
|
-
].join(" ")
|
|
454
|
-
);
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
438
|
if (this.limitSymbolsToReels && Object.keys(this.limitSymbolsToReels).length == 0) {
|
|
458
439
|
this.limitSymbolsToReels = void 0;
|
|
459
440
|
}
|
|
460
|
-
if (this.outputDir === "") {
|
|
461
|
-
throw new Error("Output directory must be specified for the ReelGenerator.");
|
|
462
|
-
}
|
|
463
441
|
}
|
|
464
442
|
isSymbolAllowedOnReel(symbolId, reelIdx) {
|
|
465
443
|
if (!this.limitSymbolsToReels) return true;
|
|
@@ -539,6 +517,15 @@ var ReelGenerator = class {
|
|
|
539
517
|
);
|
|
540
518
|
this.csvPath = filePath;
|
|
541
519
|
const exists = import_fs2.default.existsSync(filePath);
|
|
520
|
+
if (exists && !this.overrideExisting) {
|
|
521
|
+
this.reels = this.parseReelsetCSV(filePath, gameConf);
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
if (!exists && this.symbolWeights.size === 0) {
|
|
525
|
+
throw new Error(
|
|
526
|
+
`Cannot generate reels for generator "${this.id}" of mode "${this.associatedGameModeName}" because the symbol weights are empty.`
|
|
527
|
+
);
|
|
528
|
+
}
|
|
542
529
|
const reelsAmount = gameMode.reelsAmount;
|
|
543
530
|
const weightsObj = Object.fromEntries(this.symbolWeights);
|
|
544
531
|
for (let ridx = 0; ridx < reelsAmount; ridx++) {
|
|
@@ -673,16 +660,12 @@ var ReelGenerator = class {
|
|
|
673
660
|
}
|
|
674
661
|
const csvString = csvRows.map((row) => row.join(",")).join("\n");
|
|
675
662
|
if (import_worker_threads.isMainThread) {
|
|
676
|
-
createDirIfNotExists(this.outputDir);
|
|
677
663
|
import_fs2.default.writeFileSync(filePath, csvString);
|
|
678
664
|
this.reels = this.parseReelsetCSV(filePath, gameConf);
|
|
679
665
|
console.log(
|
|
680
666
|
`Generated reelset ${this.id} for game mode ${this.associatedGameModeName}`
|
|
681
667
|
);
|
|
682
668
|
}
|
|
683
|
-
if (exists) {
|
|
684
|
-
this.reels = this.parseReelsetCSV(filePath, gameConf);
|
|
685
|
-
}
|
|
686
669
|
}
|
|
687
670
|
/**
|
|
688
671
|
* Reads a reelset CSV file and returns the reels as arrays of GameSymbols.
|
|
@@ -706,11 +689,21 @@ var ReelGenerator = class {
|
|
|
706
689
|
reels[ridx].push(symbol);
|
|
707
690
|
});
|
|
708
691
|
});
|
|
692
|
+
const reelLengths = reels.map((r) => r.length);
|
|
693
|
+
const uniqueLengths = new Set(reelLengths);
|
|
694
|
+
if (uniqueLengths.size > 1) {
|
|
695
|
+
throw new Error(
|
|
696
|
+
`Inconsistent reel lengths in reelset CSV at ${reelSetPath}: ${[
|
|
697
|
+
...uniqueLengths
|
|
698
|
+
].join(", ")}`
|
|
699
|
+
);
|
|
700
|
+
}
|
|
709
701
|
return reels;
|
|
710
702
|
}
|
|
711
703
|
};
|
|
712
704
|
|
|
713
705
|
// src/ResultSet.ts
|
|
706
|
+
var import_assert3 = __toESM(require("assert"));
|
|
714
707
|
var ResultSet = class {
|
|
715
708
|
criteria;
|
|
716
709
|
quota;
|
|
@@ -729,16 +722,11 @@ var ResultSet = class {
|
|
|
729
722
|
this.forceMaxWin = opts.forceMaxWin;
|
|
730
723
|
this.forceFreespins = opts.forceFreespins;
|
|
731
724
|
this.evaluate = opts.evaluate;
|
|
732
|
-
if (this.quota < 0 || this.quota > 1) {
|
|
733
|
-
throw new Error(`Quota must be a float between 0 and 1, got ${this.quota}.`);
|
|
734
|
-
}
|
|
735
725
|
}
|
|
736
726
|
static assignCriteriaToSimulations(ctx, gameModeName) {
|
|
737
727
|
const rng = new RandomNumberGenerator();
|
|
738
728
|
rng.setSeed(0);
|
|
739
|
-
|
|
740
|
-
throw new Error("Simulation configuration is not set.");
|
|
741
|
-
}
|
|
729
|
+
(0, import_assert3.default)(ctx.simRunsAmount, "Simulation configuration is not set.");
|
|
742
730
|
const simNums = ctx.simRunsAmount[gameModeName];
|
|
743
731
|
const resultSets = ctx.gameConfig.config.gameModes[gameModeName]?.resultSets;
|
|
744
732
|
if (!resultSets || resultSets.length === 0) {
|
|
@@ -747,8 +735,12 @@ var ResultSet = class {
|
|
|
747
735
|
if (simNums === void 0 || simNums <= 0) {
|
|
748
736
|
throw new Error(`No simulations configured for game mode "${gameModeName}".`);
|
|
749
737
|
}
|
|
738
|
+
const totalQuota = resultSets.reduce((sum, rs) => sum + rs.quota, 0);
|
|
750
739
|
const numberOfSimsForCriteria = Object.fromEntries(
|
|
751
|
-
resultSets.map((rs) =>
|
|
740
|
+
resultSets.map((rs) => {
|
|
741
|
+
const normalizedQuota = totalQuota > 0 ? rs.quota / totalQuota : 0;
|
|
742
|
+
return [rs.criteria, Math.max(Math.floor(normalizedQuota * simNums), 1)];
|
|
743
|
+
})
|
|
752
744
|
);
|
|
753
745
|
let totalSims = Object.values(numberOfSimsForCriteria).reduce(
|
|
754
746
|
(sum, num) => sum + num,
|
|
@@ -1103,13 +1095,6 @@ var GameState = class extends GameConfig {
|
|
|
1103
1095
|
this.clearPendingRecords();
|
|
1104
1096
|
this.state.userData = this.config.userState || {};
|
|
1105
1097
|
}
|
|
1106
|
-
/**
|
|
1107
|
-
* Checks if a max win is reached by comparing `wallet.currentWin` to `config.maxWin`.
|
|
1108
|
-
*
|
|
1109
|
-
* Should be called after `wallet.confirmSpinWin()`.
|
|
1110
|
-
*/
|
|
1111
|
-
isMaxWinTriggered() {
|
|
1112
|
-
}
|
|
1113
1098
|
/**
|
|
1114
1099
|
* Empties the list of pending records in the recorder.
|
|
1115
1100
|
*/
|
|
@@ -1555,7 +1540,7 @@ var Board = class extends GameState {
|
|
|
1555
1540
|
return stopPositionsForReels;
|
|
1556
1541
|
}
|
|
1557
1542
|
/**
|
|
1558
|
-
* Selects a random
|
|
1543
|
+
* Selects a random reel set based on the configured weights of the current result set.\
|
|
1559
1544
|
* Returns the reels as arrays of GameSymbols.
|
|
1560
1545
|
*/
|
|
1561
1546
|
getRandomReelset() {
|
|
@@ -1570,7 +1555,7 @@ var Board = class extends GameState {
|
|
|
1570
1555
|
reelSetId = weightedRandom(weights[this.state.currentSpinType], this.state.rng);
|
|
1571
1556
|
}
|
|
1572
1557
|
const reelSet = this.getReelsetById(this.state.currentGameMode, reelSetId);
|
|
1573
|
-
return reelSet
|
|
1558
|
+
return reelSet;
|
|
1574
1559
|
}
|
|
1575
1560
|
/**
|
|
1576
1561
|
* Draws a board using specified reel stops.
|
|
@@ -1826,7 +1811,7 @@ var ManywaysWinType = class extends WinType {
|
|
|
1826
1811
|
};
|
|
1827
1812
|
|
|
1828
1813
|
// src/optimizer/OptimizationConditions.ts
|
|
1829
|
-
var
|
|
1814
|
+
var import_assert4 = __toESM(require("assert"));
|
|
1830
1815
|
var OptimizationConditions = class {
|
|
1831
1816
|
rtp;
|
|
1832
1817
|
avgWin;
|
|
@@ -1837,14 +1822,14 @@ var OptimizationConditions = class {
|
|
|
1837
1822
|
constructor(opts) {
|
|
1838
1823
|
let { rtp, avgWin, hitRate, searchConditions, priority } = opts;
|
|
1839
1824
|
if (rtp == void 0 || rtp === "x") {
|
|
1840
|
-
(0,
|
|
1825
|
+
(0, import_assert4.default)(avgWin !== void 0 && hitRate !== void 0, "If RTP is not specified, hit-rate (hr) and average win amount (av_win) must be given.");
|
|
1841
1826
|
rtp = Math.round(avgWin / Number(hitRate) * 1e5) / 1e5;
|
|
1842
1827
|
}
|
|
1843
1828
|
let noneCount = 0;
|
|
1844
1829
|
for (const val of [rtp, avgWin, hitRate]) {
|
|
1845
1830
|
if (val === void 0) noneCount++;
|
|
1846
1831
|
}
|
|
1847
|
-
(0,
|
|
1832
|
+
(0, import_assert4.default)(noneCount <= 1, "Invalid combination of optimization conditions.");
|
|
1848
1833
|
this.searchRange = [-1, -1];
|
|
1849
1834
|
this.forceSearch = {};
|
|
1850
1835
|
if (typeof searchConditions === "number") {
|
|
@@ -1925,7 +1910,7 @@ var OptimizationParameters = class _OptimizationParameters {
|
|
|
1925
1910
|
// src/Simulation.ts
|
|
1926
1911
|
var import_fs3 = __toESM(require("fs"));
|
|
1927
1912
|
var import_path2 = __toESM(require("path"));
|
|
1928
|
-
var
|
|
1913
|
+
var import_assert5 = __toESM(require("assert"));
|
|
1929
1914
|
var import_zlib = __toESM(require("zlib"));
|
|
1930
1915
|
var import_esbuild = require("esbuild");
|
|
1931
1916
|
var import_worker_threads2 = require("worker_threads");
|
|
@@ -1949,7 +1934,7 @@ var Simulation = class _Simulation {
|
|
|
1949
1934
|
this.library = /* @__PURE__ */ new Map();
|
|
1950
1935
|
this.records = [];
|
|
1951
1936
|
const gameModeKeys = Object.keys(this.gameConfig.config.gameModes);
|
|
1952
|
-
(0,
|
|
1937
|
+
(0, import_assert5.default)(
|
|
1953
1938
|
Object.values(this.gameConfig.config.gameModes).map((m) => gameModeKeys.includes(m.name)).every((v) => v === true),
|
|
1954
1939
|
"Game mode name must match its key in the gameModes object."
|
|
1955
1940
|
);
|
|
@@ -2300,22 +2285,25 @@ var SimulationContext = class extends Board {
|
|
|
2300
2285
|
this.state.currentGameMode = mode;
|
|
2301
2286
|
this.state.currentSimulationId = simId;
|
|
2302
2287
|
this.state.isCriteriaMet = false;
|
|
2288
|
+
const resultSet = this.getResultSetByCriteria(this.state.currentGameMode, criteria);
|
|
2303
2289
|
while (!this.state.isCriteriaMet) {
|
|
2304
2290
|
this.actualSims++;
|
|
2305
2291
|
this.resetSimulation();
|
|
2306
|
-
const resultSet = this.getGameModeCriteria(this.state.currentGameMode, criteria);
|
|
2307
2292
|
this.state.currentResultSet = resultSet;
|
|
2308
2293
|
this.state.book.criteria = resultSet.criteria;
|
|
2309
2294
|
this.handleGameFlow();
|
|
2310
2295
|
if (resultSet.meetsCriteria(this)) {
|
|
2311
2296
|
this.state.isCriteriaMet = true;
|
|
2312
|
-
this.config.hooks.onSimulationAccepted?.(this);
|
|
2313
|
-
this.record({
|
|
2314
|
-
criteria: resultSet.criteria
|
|
2315
|
-
});
|
|
2316
2297
|
}
|
|
2317
2298
|
}
|
|
2318
2299
|
this.wallet.confirmWins(this);
|
|
2300
|
+
if (this.state.book.getPayout() >= this.config.maxWinX) {
|
|
2301
|
+
this.state.triggeredMaxWin = true;
|
|
2302
|
+
}
|
|
2303
|
+
this.record({
|
|
2304
|
+
criteria: resultSet.criteria
|
|
2305
|
+
});
|
|
2306
|
+
this.config.hooks.onSimulationAccepted?.(this);
|
|
2319
2307
|
this.confirmRecords();
|
|
2320
2308
|
import_worker_threads2.parentPort?.postMessage({
|
|
2321
2309
|
type: "complete",
|
|
@@ -2352,7 +2340,7 @@ var SimulationContext = class extends Board {
|
|
|
2352
2340
|
// src/analysis/index.ts
|
|
2353
2341
|
var import_fs4 = __toESM(require("fs"));
|
|
2354
2342
|
var import_path3 = __toESM(require("path"));
|
|
2355
|
-
var
|
|
2343
|
+
var import_assert6 = __toESM(require("assert"));
|
|
2356
2344
|
|
|
2357
2345
|
// src/analysis/utils.ts
|
|
2358
2346
|
function parseLookupTable(content) {
|
|
@@ -2517,7 +2505,7 @@ var Analysis = class {
|
|
|
2517
2505
|
booksJsonlCompressed
|
|
2518
2506
|
};
|
|
2519
2507
|
for (const p of Object.values(paths[modeStr])) {
|
|
2520
|
-
(0,
|
|
2508
|
+
(0, import_assert6.default)(
|
|
2521
2509
|
import_fs4.default.existsSync(p),
|
|
2522
2510
|
`File "${p}" does not exist. Run optimization to auto-create it.`
|
|
2523
2511
|
);
|
|
@@ -2589,7 +2577,7 @@ var Analysis = class {
|
|
|
2589
2577
|
}
|
|
2590
2578
|
getGameModeConfig(mode) {
|
|
2591
2579
|
const config = this.gameConfig.gameModes[mode];
|
|
2592
|
-
(0,
|
|
2580
|
+
(0, import_assert6.default)(config, `Game mode "${mode}" not found in game config`);
|
|
2593
2581
|
return config;
|
|
2594
2582
|
}
|
|
2595
2583
|
};
|
|
@@ -2693,7 +2681,7 @@ function makeSetupFile(optimizer, gameMode) {
|
|
|
2693
2681
|
// src/optimizer/index.ts
|
|
2694
2682
|
var import_child_process = require("child_process");
|
|
2695
2683
|
var import_path6 = __toESM(require("path"));
|
|
2696
|
-
var
|
|
2684
|
+
var import_assert7 = __toESM(require("assert"));
|
|
2697
2685
|
var import_worker_threads4 = require("worker_threads");
|
|
2698
2686
|
var Optimizer = class {
|
|
2699
2687
|
gameConfig;
|
|
@@ -2737,7 +2725,7 @@ var Optimizer = class {
|
|
|
2737
2725
|
}
|
|
2738
2726
|
}
|
|
2739
2727
|
const criteria = configMode.resultSets.map((r) => r.criteria);
|
|
2740
|
-
(0,
|
|
2728
|
+
(0, import_assert7.default)(
|
|
2741
2729
|
conditions.every((c) => criteria.includes(c)),
|
|
2742
2730
|
`Not all ResultSet criteria in game mode "${k}" are defined as optimization conditions.`
|
|
2743
2731
|
);
|
|
@@ -2749,7 +2737,7 @@ var Optimizer = class {
|
|
|
2749
2737
|
}
|
|
2750
2738
|
gameModeRtp = Math.round(gameModeRtp * 1e3) / 1e3;
|
|
2751
2739
|
paramRtp = Math.round(paramRtp * 1e3) / 1e3;
|
|
2752
|
-
(0,
|
|
2740
|
+
(0, import_assert7.default)(
|
|
2753
2741
|
gameModeRtp === paramRtp,
|
|
2754
2742
|
`Sum of all RTP conditions (${paramRtp}) does not match the game mode RTP (${gameModeRtp}) in game mode "${k}".`
|
|
2755
2743
|
);
|