@slot-engine/core 0.2.2 → 0.2.4
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 +45 -2
- package/dist/index.d.ts +45 -2
- package/dist/index.js +138 -20
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +138 -20
- 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) {
|
|
@@ -462,6 +463,14 @@ var Board = class {
|
|
|
462
463
|
this.reels[reelIndex].splice(rowIndex, 1);
|
|
463
464
|
}
|
|
464
465
|
}
|
|
466
|
+
updateSymbol(reelIndex, rowIndex, properties) {
|
|
467
|
+
const symbol = this.getSymbol(reelIndex, rowIndex);
|
|
468
|
+
if (symbol) {
|
|
469
|
+
for (const [key, value] of Object.entries(properties)) {
|
|
470
|
+
symbol.properties.set(key, value);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
465
474
|
makeEmptyReels(opts) {
|
|
466
475
|
const length = opts.reelsAmount ?? opts.ctx.services.game.getCurrentGameMode().reelsAmount;
|
|
467
476
|
assert3(length, "Cannot make empty reels without context or reelsAmount.");
|
|
@@ -831,6 +840,12 @@ var BoardService = class extends AbstractService {
|
|
|
831
840
|
removeSymbol(reelIndex, rowIndex) {
|
|
832
841
|
this.board.removeSymbol(reelIndex, rowIndex);
|
|
833
842
|
}
|
|
843
|
+
/**
|
|
844
|
+
* Updates properties of the symbol at the specified reel and row index.
|
|
845
|
+
*/
|
|
846
|
+
updateSymbol(reelIndex, rowIndex, properties) {
|
|
847
|
+
this.board.updateSymbol(reelIndex, rowIndex, properties);
|
|
848
|
+
}
|
|
834
849
|
resetReels() {
|
|
835
850
|
this.board.resetReels({
|
|
836
851
|
ctx: this.ctx()
|
|
@@ -1039,7 +1054,10 @@ var DataService = class extends AbstractService {
|
|
|
1039
1054
|
* Calls `ctx.services.data.record()` with the provided data.
|
|
1040
1055
|
*/
|
|
1041
1056
|
recordSymbolOccurrence(data) {
|
|
1042
|
-
this.record(
|
|
1057
|
+
this.record({
|
|
1058
|
+
...data,
|
|
1059
|
+
spinType: this.ctx().state.currentSpinType
|
|
1060
|
+
});
|
|
1043
1061
|
}
|
|
1044
1062
|
/**
|
|
1045
1063
|
* Adds an event to the book.
|
|
@@ -3061,15 +3079,21 @@ function getLessBetHitrate(payoutWeights, cost) {
|
|
|
3061
3079
|
|
|
3062
3080
|
// src/analysis/index.ts
|
|
3063
3081
|
import { isMainThread as isMainThread3 } from "worker_threads";
|
|
3082
|
+
import chalk3 from "chalk";
|
|
3064
3083
|
var Analysis = class {
|
|
3065
3084
|
game;
|
|
3066
3085
|
constructor(game) {
|
|
3067
3086
|
this.game = game;
|
|
3068
3087
|
}
|
|
3069
|
-
async runAnalysis(
|
|
3088
|
+
async runAnalysis(opts) {
|
|
3089
|
+
const { gameModes, recordStats = [] } = opts;
|
|
3070
3090
|
if (!isMainThread3) return;
|
|
3091
|
+
console.log(chalk3.gray("Starting analysis..."));
|
|
3071
3092
|
this.getNumberStats(gameModes);
|
|
3072
3093
|
this.getWinRanges(gameModes);
|
|
3094
|
+
if (recordStats.length > 0) {
|
|
3095
|
+
this.getRecordStats(gameModes, recordStats);
|
|
3096
|
+
}
|
|
3073
3097
|
console.log("Analysis complete. Files written to build directory.");
|
|
3074
3098
|
}
|
|
3075
3099
|
getNumberStats(gameModes) {
|
|
@@ -3246,6 +3270,85 @@ var Analysis = class {
|
|
|
3246
3270
|
}
|
|
3247
3271
|
writeJsonFile(meta.paths.statsPayouts, payoutRanges);
|
|
3248
3272
|
}
|
|
3273
|
+
getRecordStats(gameModes, recordStatsConfig) {
|
|
3274
|
+
const meta = this.game.getMetadata();
|
|
3275
|
+
const allStats = [];
|
|
3276
|
+
for (const modeStr of gameModes) {
|
|
3277
|
+
const lutOptimized = parseLookupTable(
|
|
3278
|
+
fs4.readFileSync(meta.paths.lookupTablePublish(modeStr), "utf-8")
|
|
3279
|
+
);
|
|
3280
|
+
const totalWeight = getTotalLutWeight(lutOptimized);
|
|
3281
|
+
const weightMap = /* @__PURE__ */ new Map();
|
|
3282
|
+
lutOptimized.forEach(([bookId, weight]) => {
|
|
3283
|
+
weightMap.set(bookId, weight);
|
|
3284
|
+
});
|
|
3285
|
+
const forceRecordsPath = meta.paths.forceRecords(modeStr);
|
|
3286
|
+
if (!fs4.existsSync(forceRecordsPath)) continue;
|
|
3287
|
+
const forceRecords = JSON.parse(
|
|
3288
|
+
fs4.readFileSync(forceRecordsPath, "utf-8")
|
|
3289
|
+
);
|
|
3290
|
+
const modeStats = {
|
|
3291
|
+
gameMode: modeStr,
|
|
3292
|
+
groups: []
|
|
3293
|
+
};
|
|
3294
|
+
for (const config of recordStatsConfig) {
|
|
3295
|
+
const groupName = config.name || config.groupBy.join("_");
|
|
3296
|
+
const aggregated = /* @__PURE__ */ new Map();
|
|
3297
|
+
for (const record of forceRecords) {
|
|
3298
|
+
const searchMap = new Map(record.search.map((s) => [s.name, s.value]));
|
|
3299
|
+
if (config.filter) {
|
|
3300
|
+
let matches = true;
|
|
3301
|
+
for (const [key2, value] of Object.entries(config.filter)) {
|
|
3302
|
+
if (searchMap.get(key2) !== value) {
|
|
3303
|
+
matches = false;
|
|
3304
|
+
break;
|
|
3305
|
+
}
|
|
3306
|
+
}
|
|
3307
|
+
if (!matches) continue;
|
|
3308
|
+
}
|
|
3309
|
+
const hasAllProps = config.groupBy.every((prop) => searchMap.has(prop));
|
|
3310
|
+
if (!hasAllProps) continue;
|
|
3311
|
+
const key = config.groupBy.map((prop) => searchMap.get(prop)).join("|");
|
|
3312
|
+
const properties = Object.fromEntries(
|
|
3313
|
+
config.groupBy.map((prop) => [prop, searchMap.get(prop)])
|
|
3314
|
+
);
|
|
3315
|
+
let totalWeight2 = 0;
|
|
3316
|
+
for (const bookId of record.bookIds) {
|
|
3317
|
+
totalWeight2 += weightMap.get(bookId) ?? 0;
|
|
3318
|
+
}
|
|
3319
|
+
const existing = aggregated.get(key);
|
|
3320
|
+
if (existing) {
|
|
3321
|
+
existing.count += record.timesTriggered;
|
|
3322
|
+
existing.totalWeight += totalWeight2;
|
|
3323
|
+
} else {
|
|
3324
|
+
aggregated.set(key, {
|
|
3325
|
+
properties,
|
|
3326
|
+
count: record.timesTriggered,
|
|
3327
|
+
totalWeight: totalWeight2
|
|
3328
|
+
});
|
|
3329
|
+
}
|
|
3330
|
+
}
|
|
3331
|
+
const items = Array.from(aggregated.entries()).map(([key, data]) => {
|
|
3332
|
+
const hitRate = round(totalWeight / data.totalWeight, 4);
|
|
3333
|
+
return {
|
|
3334
|
+
key,
|
|
3335
|
+
properties: data.properties,
|
|
3336
|
+
count: data.count,
|
|
3337
|
+
hitRateString: `1 in ${Math.round(hitRate).toLocaleString()}`,
|
|
3338
|
+
hitRate
|
|
3339
|
+
};
|
|
3340
|
+
}).sort((a, b) => a.hitRate - b.hitRate);
|
|
3341
|
+
modeStats.groups.push({
|
|
3342
|
+
name: groupName,
|
|
3343
|
+
groupBy: config.groupBy,
|
|
3344
|
+
filter: config.filter,
|
|
3345
|
+
items
|
|
3346
|
+
});
|
|
3347
|
+
}
|
|
3348
|
+
allStats.push(modeStats);
|
|
3349
|
+
}
|
|
3350
|
+
writeJsonFile(meta.paths.statsRecords, allStats);
|
|
3351
|
+
}
|
|
3249
3352
|
getGameModeConfig(mode) {
|
|
3250
3353
|
const config = this.game.getConfig().gameModes[mode];
|
|
3251
3354
|
assert6(config, `Game mode "${mode}" not found in game config`);
|
|
@@ -3610,7 +3713,7 @@ var SlotGame = class _SlotGame {
|
|
|
3610
3713
|
*/
|
|
3611
3714
|
runAnalysis(opts) {
|
|
3612
3715
|
this.analyzer = new Analysis(this);
|
|
3613
|
-
this.analyzer.runAnalysis(opts
|
|
3716
|
+
this.analyzer.runAnalysis(opts);
|
|
3614
3717
|
}
|
|
3615
3718
|
/**
|
|
3616
3719
|
* Runs the configured tasks: simulation, optimization, and/or analysis.
|
|
@@ -4056,27 +4159,36 @@ var ManywaysWinType = class extends WinType {
|
|
|
4056
4159
|
* Calculates wins based on the defined paylines and provided board state.\
|
|
4057
4160
|
* Retrieve the results using `getWins()` after.
|
|
4058
4161
|
*/
|
|
4059
|
-
evaluateWins(board) {
|
|
4162
|
+
evaluateWins(board, opts = {}) {
|
|
4060
4163
|
this.validateConfig();
|
|
4164
|
+
const { jumpGaps = false } = opts;
|
|
4061
4165
|
const waysWins = [];
|
|
4062
4166
|
const reels = board;
|
|
4063
4167
|
const possibleWaysWins = /* @__PURE__ */ new Map();
|
|
4064
4168
|
const candidateSymbols = /* @__PURE__ */ new Map();
|
|
4065
|
-
|
|
4066
|
-
|
|
4067
|
-
|
|
4068
|
-
|
|
4069
|
-
let hasWild = false;
|
|
4070
|
-
for (const symbol of reel) {
|
|
4071
|
-
candidateSymbols.set(symbol.id, symbol);
|
|
4072
|
-
if (this.isWild(symbol)) {
|
|
4073
|
-
hasWild = true;
|
|
4169
|
+
if (jumpGaps) {
|
|
4170
|
+
for (const reel of reels) {
|
|
4171
|
+
for (const symbol of reel) {
|
|
4172
|
+
candidateSymbols.set(symbol.id, symbol);
|
|
4074
4173
|
}
|
|
4075
4174
|
}
|
|
4076
|
-
|
|
4077
|
-
|
|
4175
|
+
} else {
|
|
4176
|
+
let searchReelIdx = 0;
|
|
4177
|
+
let searchActive = true;
|
|
4178
|
+
while (searchActive && searchReelIdx < reels.length) {
|
|
4179
|
+
const reel = reels[searchReelIdx];
|
|
4180
|
+
let hasWild = false;
|
|
4181
|
+
for (const symbol of reel) {
|
|
4182
|
+
candidateSymbols.set(symbol.id, symbol);
|
|
4183
|
+
if (this.isWild(symbol)) {
|
|
4184
|
+
hasWild = true;
|
|
4185
|
+
}
|
|
4186
|
+
}
|
|
4187
|
+
if (!hasWild) {
|
|
4188
|
+
searchActive = false;
|
|
4189
|
+
}
|
|
4190
|
+
searchReelIdx++;
|
|
4078
4191
|
}
|
|
4079
|
-
searchReelIdx++;
|
|
4080
4192
|
}
|
|
4081
4193
|
for (const baseSymbol of candidateSymbols.values()) {
|
|
4082
4194
|
let symbolList = {};
|
|
@@ -4092,7 +4204,7 @@ var ManywaysWinType = class extends WinType {
|
|
|
4092
4204
|
symbolList[ridx].push({ reel: ridx, row: sidx, symbol });
|
|
4093
4205
|
}
|
|
4094
4206
|
}
|
|
4095
|
-
if (!symbolList[ridx]) {
|
|
4207
|
+
if (!symbolList[ridx] && !jumpGaps) {
|
|
4096
4208
|
isInterrupted = true;
|
|
4097
4209
|
break;
|
|
4098
4210
|
}
|
|
@@ -4108,7 +4220,7 @@ var ManywaysWinType = class extends WinType {
|
|
|
4108
4220
|
for (const [baseSymbolId, symbolList] of possibleWaysWins.entries()) {
|
|
4109
4221
|
const wayLength = this.getWayLength(symbolList);
|
|
4110
4222
|
let baseSymbol = Object.values(symbolList).flatMap((l) => l.map((s) => s)).find((s) => !this.isWild(s.symbol))?.symbol;
|
|
4111
|
-
if (!baseSymbol) baseSymbol = symbolList[0][0].symbol;
|
|
4223
|
+
if (!baseSymbol) baseSymbol = symbolList[Object.keys(symbolList)[0]][0].symbol;
|
|
4112
4224
|
const singleWayPayout = this.getSymbolPayout(baseSymbol, wayLength);
|
|
4113
4225
|
const totalWays = Object.values(symbolList).reduce(
|
|
4114
4226
|
(ways, syms) => ways * syms.length,
|
|
@@ -4142,7 +4254,7 @@ var ManywaysWinType = class extends WinType {
|
|
|
4142
4254
|
return this;
|
|
4143
4255
|
}
|
|
4144
4256
|
getWayLength(symbolList) {
|
|
4145
|
-
return
|
|
4257
|
+
return Object.keys(symbolList).length;
|
|
4146
4258
|
}
|
|
4147
4259
|
};
|
|
4148
4260
|
|
|
@@ -4604,6 +4716,12 @@ var StandaloneBoard = class {
|
|
|
4604
4716
|
removeSymbol(reelIndex, rowIndex) {
|
|
4605
4717
|
this.board.removeSymbol(reelIndex, rowIndex);
|
|
4606
4718
|
}
|
|
4719
|
+
/**
|
|
4720
|
+
* Updates properties of the symbol at the specified reel and row index.
|
|
4721
|
+
*/
|
|
4722
|
+
updateSymbol(reelIndex, rowIndex, properties) {
|
|
4723
|
+
this.board.updateSymbol(reelIndex, rowIndex, properties);
|
|
4724
|
+
}
|
|
4607
4725
|
resetReels() {
|
|
4608
4726
|
this.board.resetReels({
|
|
4609
4727
|
ctx: this.ctx
|