@slot-engine/core 0.2.3 → 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 +34 -1
- package/dist/index.d.ts +34 -1
- package/dist/index.js +93 -4
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +93 -4
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -64,6 +64,7 @@ type PermanentFilePaths = {
|
|
|
64
64
|
simulationSummary: string;
|
|
65
65
|
statsPayouts: string;
|
|
66
66
|
statsSummary: string;
|
|
67
|
+
statsRecords: string;
|
|
67
68
|
};
|
|
68
69
|
type TemporaryFilePaths = {
|
|
69
70
|
tempBooks: (mode: string, i: number) => string;
|
|
@@ -785,7 +786,6 @@ declare class DataService<TGameModes extends AnyGameModes = AnyGameModes, TSymbo
|
|
|
785
786
|
recordSymbolOccurrence(data: {
|
|
786
787
|
kind: number;
|
|
787
788
|
symbolId: string;
|
|
788
|
-
spinType: SpinType;
|
|
789
789
|
[key: string]: any;
|
|
790
790
|
}): void;
|
|
791
791
|
/**
|
|
@@ -1246,7 +1246,40 @@ interface PayoutStatistics {
|
|
|
1246
1246
|
};
|
|
1247
1247
|
}
|
|
1248
1248
|
interface AnalysisOpts {
|
|
1249
|
+
/**
|
|
1250
|
+
* Which game modes to analyze.
|
|
1251
|
+
*/
|
|
1249
1252
|
gameModes: string[];
|
|
1253
|
+
/**
|
|
1254
|
+
* Configure which recorded properties to analyze.
|
|
1255
|
+
* This will provide you with hit rates for the specified groupings.
|
|
1256
|
+
* Each entry defines a grouping strategy for statistics.
|
|
1257
|
+
*
|
|
1258
|
+
* @example
|
|
1259
|
+
* ```ts
|
|
1260
|
+
* recordStats: [
|
|
1261
|
+
* { groupBy: ["symbolId", "kind", "spinType"] }, // All win combinations
|
|
1262
|
+
* { groupBy: ["symbolId", "kind"], filter: { spinType: "basegame" } }, // Base game win combinations only
|
|
1263
|
+
* { groupBy: ["criteria"] }, // Hit rate by result set criteria
|
|
1264
|
+
* ]
|
|
1265
|
+
* ```
|
|
1266
|
+
*/
|
|
1267
|
+
recordStats?: RecordStatsConfig[];
|
|
1268
|
+
}
|
|
1269
|
+
interface RecordStatsConfig {
|
|
1270
|
+
/**
|
|
1271
|
+
* Properties to group by from the recorded search entries.\
|
|
1272
|
+
* E.g. `["symbolId", "kind", "spinType"]` for symbol hit rates.
|
|
1273
|
+
*/
|
|
1274
|
+
groupBy: string[];
|
|
1275
|
+
/**
|
|
1276
|
+
* Optional filter to only include records matching these values.
|
|
1277
|
+
*/
|
|
1278
|
+
filter?: Record<string, string>;
|
|
1279
|
+
/**
|
|
1280
|
+
* Optional custom name for this stats group in the output.
|
|
1281
|
+
*/
|
|
1282
|
+
name?: string;
|
|
1250
1283
|
}
|
|
1251
1284
|
interface Statistics {
|
|
1252
1285
|
gameMode: string;
|
package/dist/index.d.ts
CHANGED
|
@@ -64,6 +64,7 @@ type PermanentFilePaths = {
|
|
|
64
64
|
simulationSummary: string;
|
|
65
65
|
statsPayouts: string;
|
|
66
66
|
statsSummary: string;
|
|
67
|
+
statsRecords: string;
|
|
67
68
|
};
|
|
68
69
|
type TemporaryFilePaths = {
|
|
69
70
|
tempBooks: (mode: string, i: number) => string;
|
|
@@ -785,7 +786,6 @@ declare class DataService<TGameModes extends AnyGameModes = AnyGameModes, TSymbo
|
|
|
785
786
|
recordSymbolOccurrence(data: {
|
|
786
787
|
kind: number;
|
|
787
788
|
symbolId: string;
|
|
788
|
-
spinType: SpinType;
|
|
789
789
|
[key: string]: any;
|
|
790
790
|
}): void;
|
|
791
791
|
/**
|
|
@@ -1246,7 +1246,40 @@ interface PayoutStatistics {
|
|
|
1246
1246
|
};
|
|
1247
1247
|
}
|
|
1248
1248
|
interface AnalysisOpts {
|
|
1249
|
+
/**
|
|
1250
|
+
* Which game modes to analyze.
|
|
1251
|
+
*/
|
|
1249
1252
|
gameModes: string[];
|
|
1253
|
+
/**
|
|
1254
|
+
* Configure which recorded properties to analyze.
|
|
1255
|
+
* This will provide you with hit rates for the specified groupings.
|
|
1256
|
+
* Each entry defines a grouping strategy for statistics.
|
|
1257
|
+
*
|
|
1258
|
+
* @example
|
|
1259
|
+
* ```ts
|
|
1260
|
+
* recordStats: [
|
|
1261
|
+
* { groupBy: ["symbolId", "kind", "spinType"] }, // All win combinations
|
|
1262
|
+
* { groupBy: ["symbolId", "kind"], filter: { spinType: "basegame" } }, // Base game win combinations only
|
|
1263
|
+
* { groupBy: ["criteria"] }, // Hit rate by result set criteria
|
|
1264
|
+
* ]
|
|
1265
|
+
* ```
|
|
1266
|
+
*/
|
|
1267
|
+
recordStats?: RecordStatsConfig[];
|
|
1268
|
+
}
|
|
1269
|
+
interface RecordStatsConfig {
|
|
1270
|
+
/**
|
|
1271
|
+
* Properties to group by from the recorded search entries.\
|
|
1272
|
+
* E.g. `["symbolId", "kind", "spinType"]` for symbol hit rates.
|
|
1273
|
+
*/
|
|
1274
|
+
groupBy: string[];
|
|
1275
|
+
/**
|
|
1276
|
+
* Optional filter to only include records matching these values.
|
|
1277
|
+
*/
|
|
1278
|
+
filter?: Record<string, string>;
|
|
1279
|
+
/**
|
|
1280
|
+
* Optional custom name for this stats group in the output.
|
|
1281
|
+
*/
|
|
1282
|
+
name?: string;
|
|
1250
1283
|
}
|
|
1251
1284
|
interface Statistics {
|
|
1252
1285
|
gameMode: string;
|
package/dist/index.js
CHANGED
|
@@ -92,7 +92,8 @@ function createPermanentFilePaths(basePath) {
|
|
|
92
92
|
publishFiles: import_path.default.join(basePath, "publish_files"),
|
|
93
93
|
simulationSummary: import_path.default.join(basePath, "simulation_summary.json"),
|
|
94
94
|
statsPayouts: import_path.default.join(basePath, "stats_payouts.json"),
|
|
95
|
-
statsSummary: import_path.default.join(basePath, "stats_summary.json")
|
|
95
|
+
statsSummary: import_path.default.join(basePath, "stats_summary.json"),
|
|
96
|
+
statsRecords: import_path.default.join(basePath, "stats_records.json")
|
|
96
97
|
};
|
|
97
98
|
}
|
|
98
99
|
function createTemporaryFilePaths(basePath, tempFolder) {
|
|
@@ -1101,7 +1102,10 @@ var DataService = class extends AbstractService {
|
|
|
1101
1102
|
* Calls `ctx.services.data.record()` with the provided data.
|
|
1102
1103
|
*/
|
|
1103
1104
|
recordSymbolOccurrence(data) {
|
|
1104
|
-
this.record(
|
|
1105
|
+
this.record({
|
|
1106
|
+
...data,
|
|
1107
|
+
spinType: this.ctx().state.currentSpinType
|
|
1108
|
+
});
|
|
1105
1109
|
}
|
|
1106
1110
|
/**
|
|
1107
1111
|
* Adds an event to the book.
|
|
@@ -3123,15 +3127,21 @@ function getLessBetHitrate(payoutWeights, cost) {
|
|
|
3123
3127
|
|
|
3124
3128
|
// src/analysis/index.ts
|
|
3125
3129
|
var import_worker_threads3 = require("worker_threads");
|
|
3130
|
+
var import_chalk3 = __toESM(require("chalk"));
|
|
3126
3131
|
var Analysis = class {
|
|
3127
3132
|
game;
|
|
3128
3133
|
constructor(game) {
|
|
3129
3134
|
this.game = game;
|
|
3130
3135
|
}
|
|
3131
|
-
async runAnalysis(
|
|
3136
|
+
async runAnalysis(opts) {
|
|
3137
|
+
const { gameModes, recordStats = [] } = opts;
|
|
3132
3138
|
if (!import_worker_threads3.isMainThread) return;
|
|
3139
|
+
console.log(import_chalk3.default.gray("Starting analysis..."));
|
|
3133
3140
|
this.getNumberStats(gameModes);
|
|
3134
3141
|
this.getWinRanges(gameModes);
|
|
3142
|
+
if (recordStats.length > 0) {
|
|
3143
|
+
this.getRecordStats(gameModes, recordStats);
|
|
3144
|
+
}
|
|
3135
3145
|
console.log("Analysis complete. Files written to build directory.");
|
|
3136
3146
|
}
|
|
3137
3147
|
getNumberStats(gameModes) {
|
|
@@ -3308,6 +3318,85 @@ var Analysis = class {
|
|
|
3308
3318
|
}
|
|
3309
3319
|
writeJsonFile(meta.paths.statsPayouts, payoutRanges);
|
|
3310
3320
|
}
|
|
3321
|
+
getRecordStats(gameModes, recordStatsConfig) {
|
|
3322
|
+
const meta = this.game.getMetadata();
|
|
3323
|
+
const allStats = [];
|
|
3324
|
+
for (const modeStr of gameModes) {
|
|
3325
|
+
const lutOptimized = parseLookupTable(
|
|
3326
|
+
import_fs4.default.readFileSync(meta.paths.lookupTablePublish(modeStr), "utf-8")
|
|
3327
|
+
);
|
|
3328
|
+
const totalWeight = getTotalLutWeight(lutOptimized);
|
|
3329
|
+
const weightMap = /* @__PURE__ */ new Map();
|
|
3330
|
+
lutOptimized.forEach(([bookId, weight]) => {
|
|
3331
|
+
weightMap.set(bookId, weight);
|
|
3332
|
+
});
|
|
3333
|
+
const forceRecordsPath = meta.paths.forceRecords(modeStr);
|
|
3334
|
+
if (!import_fs4.default.existsSync(forceRecordsPath)) continue;
|
|
3335
|
+
const forceRecords = JSON.parse(
|
|
3336
|
+
import_fs4.default.readFileSync(forceRecordsPath, "utf-8")
|
|
3337
|
+
);
|
|
3338
|
+
const modeStats = {
|
|
3339
|
+
gameMode: modeStr,
|
|
3340
|
+
groups: []
|
|
3341
|
+
};
|
|
3342
|
+
for (const config of recordStatsConfig) {
|
|
3343
|
+
const groupName = config.name || config.groupBy.join("_");
|
|
3344
|
+
const aggregated = /* @__PURE__ */ new Map();
|
|
3345
|
+
for (const record of forceRecords) {
|
|
3346
|
+
const searchMap = new Map(record.search.map((s) => [s.name, s.value]));
|
|
3347
|
+
if (config.filter) {
|
|
3348
|
+
let matches = true;
|
|
3349
|
+
for (const [key2, value] of Object.entries(config.filter)) {
|
|
3350
|
+
if (searchMap.get(key2) !== value) {
|
|
3351
|
+
matches = false;
|
|
3352
|
+
break;
|
|
3353
|
+
}
|
|
3354
|
+
}
|
|
3355
|
+
if (!matches) continue;
|
|
3356
|
+
}
|
|
3357
|
+
const hasAllProps = config.groupBy.every((prop) => searchMap.has(prop));
|
|
3358
|
+
if (!hasAllProps) continue;
|
|
3359
|
+
const key = config.groupBy.map((prop) => searchMap.get(prop)).join("|");
|
|
3360
|
+
const properties = Object.fromEntries(
|
|
3361
|
+
config.groupBy.map((prop) => [prop, searchMap.get(prop)])
|
|
3362
|
+
);
|
|
3363
|
+
let totalWeight2 = 0;
|
|
3364
|
+
for (const bookId of record.bookIds) {
|
|
3365
|
+
totalWeight2 += weightMap.get(bookId) ?? 0;
|
|
3366
|
+
}
|
|
3367
|
+
const existing = aggregated.get(key);
|
|
3368
|
+
if (existing) {
|
|
3369
|
+
existing.count += record.timesTriggered;
|
|
3370
|
+
existing.totalWeight += totalWeight2;
|
|
3371
|
+
} else {
|
|
3372
|
+
aggregated.set(key, {
|
|
3373
|
+
properties,
|
|
3374
|
+
count: record.timesTriggered,
|
|
3375
|
+
totalWeight: totalWeight2
|
|
3376
|
+
});
|
|
3377
|
+
}
|
|
3378
|
+
}
|
|
3379
|
+
const items = Array.from(aggregated.entries()).map(([key, data]) => {
|
|
3380
|
+
const hitRate = round(totalWeight / data.totalWeight, 4);
|
|
3381
|
+
return {
|
|
3382
|
+
key,
|
|
3383
|
+
properties: data.properties,
|
|
3384
|
+
count: data.count,
|
|
3385
|
+
hitRateString: `1 in ${Math.round(hitRate).toLocaleString()}`,
|
|
3386
|
+
hitRate
|
|
3387
|
+
};
|
|
3388
|
+
}).sort((a, b) => a.hitRate - b.hitRate);
|
|
3389
|
+
modeStats.groups.push({
|
|
3390
|
+
name: groupName,
|
|
3391
|
+
groupBy: config.groupBy,
|
|
3392
|
+
filter: config.filter,
|
|
3393
|
+
items
|
|
3394
|
+
});
|
|
3395
|
+
}
|
|
3396
|
+
allStats.push(modeStats);
|
|
3397
|
+
}
|
|
3398
|
+
writeJsonFile(meta.paths.statsRecords, allStats);
|
|
3399
|
+
}
|
|
3311
3400
|
getGameModeConfig(mode) {
|
|
3312
3401
|
const config = this.game.getConfig().gameModes[mode];
|
|
3313
3402
|
(0, import_assert6.default)(config, `Game mode "${mode}" not found in game config`);
|
|
@@ -3672,7 +3761,7 @@ var SlotGame = class _SlotGame {
|
|
|
3672
3761
|
*/
|
|
3673
3762
|
runAnalysis(opts) {
|
|
3674
3763
|
this.analyzer = new Analysis(this);
|
|
3675
|
-
this.analyzer.runAnalysis(opts
|
|
3764
|
+
this.analyzer.runAnalysis(opts);
|
|
3676
3765
|
}
|
|
3677
3766
|
/**
|
|
3678
3767
|
* Runs the configured tasks: simulation, optimization, and/or analysis.
|