@slot-engine/core 0.2.3 → 0.2.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 +34 -1
- package/dist/index.d.ts +34 -1
- package/dist/index.js +117 -13
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +117 -13
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -44,7 +44,8 @@ function createPermanentFilePaths(basePath) {
|
|
|
44
44
|
publishFiles: path.join(basePath, "publish_files"),
|
|
45
45
|
simulationSummary: path.join(basePath, "simulation_summary.json"),
|
|
46
46
|
statsPayouts: path.join(basePath, "stats_payouts.json"),
|
|
47
|
-
statsSummary: path.join(basePath, "stats_summary.json")
|
|
47
|
+
statsSummary: path.join(basePath, "stats_summary.json"),
|
|
48
|
+
statsRecords: path.join(basePath, "stats_records.json")
|
|
48
49
|
};
|
|
49
50
|
}
|
|
50
51
|
function createTemporaryFilePaths(basePath, tempFolder) {
|
|
@@ -399,11 +400,11 @@ var GameSymbol = class _GameSymbol {
|
|
|
399
400
|
* Creates a clone of this GameSymbol.
|
|
400
401
|
*/
|
|
401
402
|
clone() {
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
403
|
+
const cloned = Object.create(_GameSymbol.prototype);
|
|
404
|
+
cloned.id = this.id;
|
|
405
|
+
cloned.pays = this.pays;
|
|
406
|
+
cloned.properties = this.properties.size > 0 ? new Map(this.properties) : /* @__PURE__ */ new Map();
|
|
407
|
+
return cloned;
|
|
407
408
|
}
|
|
408
409
|
};
|
|
409
410
|
|
|
@@ -456,6 +457,9 @@ var Board = class {
|
|
|
456
457
|
setSymbol(reelIndex, rowIndex, symbol) {
|
|
457
458
|
this.reels[reelIndex] = this.reels[reelIndex] || [];
|
|
458
459
|
this.reels[reelIndex][rowIndex] = symbol;
|
|
460
|
+
this.updateSymbol(reelIndex, rowIndex, {
|
|
461
|
+
position: [reelIndex, rowIndex]
|
|
462
|
+
});
|
|
459
463
|
}
|
|
460
464
|
removeSymbol(reelIndex, rowIndex) {
|
|
461
465
|
if (this.reels[reelIndex]) {
|
|
@@ -664,16 +668,19 @@ var Board = class {
|
|
|
664
668
|
const reelLength = opts.reels[ridx].length;
|
|
665
669
|
for (let p = padSymbols - 1; p >= 0; p--) {
|
|
666
670
|
const topPos = ((reelPos - (p + 1)) % reelLength + reelLength) % reelLength;
|
|
667
|
-
this.paddingTop[ridx].push(opts.reels[ridx][topPos]);
|
|
671
|
+
this.paddingTop[ridx].push(opts.reels[ridx][topPos].clone());
|
|
668
672
|
const bottomPos = (reelPos + symbolsPerReel[ridx] + p) % reelLength;
|
|
669
|
-
this.paddingBottom[ridx].unshift(opts.reels[ridx][bottomPos]);
|
|
673
|
+
this.paddingBottom[ridx].unshift(opts.reels[ridx][bottomPos].clone());
|
|
670
674
|
}
|
|
671
675
|
for (let row = 0; row < symbolsPerReel[ridx]; row++) {
|
|
672
676
|
const symbol = opts.reels[ridx][(reelPos + row) % reelLength];
|
|
673
677
|
if (!symbol) {
|
|
674
678
|
throw new Error(`Failed to get symbol at pos ${reelPos + row} on reel ${ridx}`);
|
|
675
679
|
}
|
|
676
|
-
this.reels[ridx][row] = symbol;
|
|
680
|
+
this.reels[ridx][row] = symbol.clone();
|
|
681
|
+
this.updateSymbol(ridx, row, {
|
|
682
|
+
position: [ridx, row]
|
|
683
|
+
});
|
|
677
684
|
}
|
|
678
685
|
}
|
|
679
686
|
return {
|
|
@@ -741,6 +748,7 @@ var Board = class {
|
|
|
741
748
|
newSymbol = forcedSym;
|
|
742
749
|
}
|
|
743
750
|
assert3(newSymbol, "Failed to get new symbol for tumbling.");
|
|
751
|
+
newSymbol = newSymbol.clone();
|
|
744
752
|
this.reels[ridx].unshift(newSymbol);
|
|
745
753
|
newFirstSymbolPositions[ridx] = symbolPos;
|
|
746
754
|
if (!newBoardSymbols[ridx]) {
|
|
@@ -754,7 +762,7 @@ var Board = class {
|
|
|
754
762
|
if (firstSymbolPos === void 0) continue;
|
|
755
763
|
for (let p = 1; p <= padSymbols; p++) {
|
|
756
764
|
const topPos = (firstSymbolPos - p + reels[ridx].length) % reels[ridx].length;
|
|
757
|
-
const padSymbol = reels[ridx][topPos];
|
|
765
|
+
const padSymbol = reels[ridx][topPos]?.clone();
|
|
758
766
|
assert3(padSymbol, "Failed to get new padding symbol for tumbling.");
|
|
759
767
|
this.paddingTop[ridx].unshift(padSymbol);
|
|
760
768
|
if (!newPaddingTopSymbols[ridx]) {
|
|
@@ -768,6 +776,14 @@ var Board = class {
|
|
|
768
776
|
return newFirstSymbolPositions[ridx] ?? stop;
|
|
769
777
|
});
|
|
770
778
|
}
|
|
779
|
+
for (let ridx = 0; ridx < reelsAmount; ridx++) {
|
|
780
|
+
const reel = this.reels[ridx];
|
|
781
|
+
for (let rowIdx = 0; rowIdx < reel.length; rowIdx++) {
|
|
782
|
+
this.updateSymbol(ridx, rowIdx, {
|
|
783
|
+
position: [ridx, rowIdx]
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
}
|
|
771
787
|
return {
|
|
772
788
|
newBoardSymbols,
|
|
773
789
|
newPaddingTopSymbols
|
|
@@ -1053,7 +1069,10 @@ var DataService = class extends AbstractService {
|
|
|
1053
1069
|
* Calls `ctx.services.data.record()` with the provided data.
|
|
1054
1070
|
*/
|
|
1055
1071
|
recordSymbolOccurrence(data) {
|
|
1056
|
-
this.record(
|
|
1072
|
+
this.record({
|
|
1073
|
+
...data,
|
|
1074
|
+
spinType: this.ctx().state.currentSpinType
|
|
1075
|
+
});
|
|
1057
1076
|
}
|
|
1058
1077
|
/**
|
|
1059
1078
|
* Adds an event to the book.
|
|
@@ -3075,15 +3094,21 @@ function getLessBetHitrate(payoutWeights, cost) {
|
|
|
3075
3094
|
|
|
3076
3095
|
// src/analysis/index.ts
|
|
3077
3096
|
import { isMainThread as isMainThread3 } from "worker_threads";
|
|
3097
|
+
import chalk3 from "chalk";
|
|
3078
3098
|
var Analysis = class {
|
|
3079
3099
|
game;
|
|
3080
3100
|
constructor(game) {
|
|
3081
3101
|
this.game = game;
|
|
3082
3102
|
}
|
|
3083
|
-
async runAnalysis(
|
|
3103
|
+
async runAnalysis(opts) {
|
|
3104
|
+
const { gameModes, recordStats = [] } = opts;
|
|
3084
3105
|
if (!isMainThread3) return;
|
|
3106
|
+
console.log(chalk3.gray("Starting analysis..."));
|
|
3085
3107
|
this.getNumberStats(gameModes);
|
|
3086
3108
|
this.getWinRanges(gameModes);
|
|
3109
|
+
if (recordStats.length > 0) {
|
|
3110
|
+
this.getRecordStats(gameModes, recordStats);
|
|
3111
|
+
}
|
|
3087
3112
|
console.log("Analysis complete. Files written to build directory.");
|
|
3088
3113
|
}
|
|
3089
3114
|
getNumberStats(gameModes) {
|
|
@@ -3260,6 +3285,85 @@ var Analysis = class {
|
|
|
3260
3285
|
}
|
|
3261
3286
|
writeJsonFile(meta.paths.statsPayouts, payoutRanges);
|
|
3262
3287
|
}
|
|
3288
|
+
getRecordStats(gameModes, recordStatsConfig) {
|
|
3289
|
+
const meta = this.game.getMetadata();
|
|
3290
|
+
const allStats = [];
|
|
3291
|
+
for (const modeStr of gameModes) {
|
|
3292
|
+
const lutOptimized = parseLookupTable(
|
|
3293
|
+
fs4.readFileSync(meta.paths.lookupTablePublish(modeStr), "utf-8")
|
|
3294
|
+
);
|
|
3295
|
+
const totalWeight = getTotalLutWeight(lutOptimized);
|
|
3296
|
+
const weightMap = /* @__PURE__ */ new Map();
|
|
3297
|
+
lutOptimized.forEach(([bookId, weight]) => {
|
|
3298
|
+
weightMap.set(bookId, weight);
|
|
3299
|
+
});
|
|
3300
|
+
const forceRecordsPath = meta.paths.forceRecords(modeStr);
|
|
3301
|
+
if (!fs4.existsSync(forceRecordsPath)) continue;
|
|
3302
|
+
const forceRecords = JSON.parse(
|
|
3303
|
+
fs4.readFileSync(forceRecordsPath, "utf-8")
|
|
3304
|
+
);
|
|
3305
|
+
const modeStats = {
|
|
3306
|
+
gameMode: modeStr,
|
|
3307
|
+
groups: []
|
|
3308
|
+
};
|
|
3309
|
+
for (const config of recordStatsConfig) {
|
|
3310
|
+
const groupName = config.name || config.groupBy.join("_");
|
|
3311
|
+
const aggregated = /* @__PURE__ */ new Map();
|
|
3312
|
+
for (const record of forceRecords) {
|
|
3313
|
+
const searchMap = new Map(record.search.map((s) => [s.name, s.value]));
|
|
3314
|
+
if (config.filter) {
|
|
3315
|
+
let matches = true;
|
|
3316
|
+
for (const [key2, value] of Object.entries(config.filter)) {
|
|
3317
|
+
if (searchMap.get(key2) !== value) {
|
|
3318
|
+
matches = false;
|
|
3319
|
+
break;
|
|
3320
|
+
}
|
|
3321
|
+
}
|
|
3322
|
+
if (!matches) continue;
|
|
3323
|
+
}
|
|
3324
|
+
const hasAllProps = config.groupBy.every((prop) => searchMap.has(prop));
|
|
3325
|
+
if (!hasAllProps) continue;
|
|
3326
|
+
const key = config.groupBy.map((prop) => searchMap.get(prop)).join("|");
|
|
3327
|
+
const properties = Object.fromEntries(
|
|
3328
|
+
config.groupBy.map((prop) => [prop, searchMap.get(prop)])
|
|
3329
|
+
);
|
|
3330
|
+
let totalWeight2 = 0;
|
|
3331
|
+
for (const bookId of record.bookIds) {
|
|
3332
|
+
totalWeight2 += weightMap.get(bookId) ?? 0;
|
|
3333
|
+
}
|
|
3334
|
+
const existing = aggregated.get(key);
|
|
3335
|
+
if (existing) {
|
|
3336
|
+
existing.count += record.timesTriggered;
|
|
3337
|
+
existing.totalWeight += totalWeight2;
|
|
3338
|
+
} else {
|
|
3339
|
+
aggregated.set(key, {
|
|
3340
|
+
properties,
|
|
3341
|
+
count: record.timesTriggered,
|
|
3342
|
+
totalWeight: totalWeight2
|
|
3343
|
+
});
|
|
3344
|
+
}
|
|
3345
|
+
}
|
|
3346
|
+
const items = Array.from(aggregated.entries()).map(([key, data]) => {
|
|
3347
|
+
const hitRate = round(totalWeight / data.totalWeight, 4);
|
|
3348
|
+
return {
|
|
3349
|
+
key,
|
|
3350
|
+
properties: data.properties,
|
|
3351
|
+
count: data.count,
|
|
3352
|
+
hitRateString: `1 in ${Math.round(hitRate).toLocaleString()}`,
|
|
3353
|
+
hitRate
|
|
3354
|
+
};
|
|
3355
|
+
}).sort((a, b) => a.hitRate - b.hitRate);
|
|
3356
|
+
modeStats.groups.push({
|
|
3357
|
+
name: groupName,
|
|
3358
|
+
groupBy: config.groupBy,
|
|
3359
|
+
filter: config.filter,
|
|
3360
|
+
items
|
|
3361
|
+
});
|
|
3362
|
+
}
|
|
3363
|
+
allStats.push(modeStats);
|
|
3364
|
+
}
|
|
3365
|
+
writeJsonFile(meta.paths.statsRecords, allStats);
|
|
3366
|
+
}
|
|
3263
3367
|
getGameModeConfig(mode) {
|
|
3264
3368
|
const config = this.game.getConfig().gameModes[mode];
|
|
3265
3369
|
assert6(config, `Game mode "${mode}" not found in game config`);
|
|
@@ -3624,7 +3728,7 @@ var SlotGame = class _SlotGame {
|
|
|
3624
3728
|
*/
|
|
3625
3729
|
runAnalysis(opts) {
|
|
3626
3730
|
this.analyzer = new Analysis(this);
|
|
3627
|
-
this.analyzer.runAnalysis(opts
|
|
3731
|
+
this.analyzer.runAnalysis(opts);
|
|
3628
3732
|
}
|
|
3629
3733
|
/**
|
|
3630
3734
|
* Runs the configured tasks: simulation, optimization, and/or analysis.
|