@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.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) {
@@ -1053,7 +1054,10 @@ var DataService = class extends AbstractService {
1053
1054
  * Calls `ctx.services.data.record()` with the provided data.
1054
1055
  */
1055
1056
  recordSymbolOccurrence(data) {
1056
- this.record(data);
1057
+ this.record({
1058
+ ...data,
1059
+ spinType: this.ctx().state.currentSpinType
1060
+ });
1057
1061
  }
1058
1062
  /**
1059
1063
  * Adds an event to the book.
@@ -3075,15 +3079,21 @@ function getLessBetHitrate(payoutWeights, cost) {
3075
3079
 
3076
3080
  // src/analysis/index.ts
3077
3081
  import { isMainThread as isMainThread3 } from "worker_threads";
3082
+ import chalk3 from "chalk";
3078
3083
  var Analysis = class {
3079
3084
  game;
3080
3085
  constructor(game) {
3081
3086
  this.game = game;
3082
3087
  }
3083
- async runAnalysis(gameModes) {
3088
+ async runAnalysis(opts) {
3089
+ const { gameModes, recordStats = [] } = opts;
3084
3090
  if (!isMainThread3) return;
3091
+ console.log(chalk3.gray("Starting analysis..."));
3085
3092
  this.getNumberStats(gameModes);
3086
3093
  this.getWinRanges(gameModes);
3094
+ if (recordStats.length > 0) {
3095
+ this.getRecordStats(gameModes, recordStats);
3096
+ }
3087
3097
  console.log("Analysis complete. Files written to build directory.");
3088
3098
  }
3089
3099
  getNumberStats(gameModes) {
@@ -3260,6 +3270,85 @@ var Analysis = class {
3260
3270
  }
3261
3271
  writeJsonFile(meta.paths.statsPayouts, payoutRanges);
3262
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
+ }
3263
3352
  getGameModeConfig(mode) {
3264
3353
  const config = this.game.getConfig().gameModes[mode];
3265
3354
  assert6(config, `Game mode "${mode}" not found in game config`);
@@ -3624,7 +3713,7 @@ var SlotGame = class _SlotGame {
3624
3713
  */
3625
3714
  runAnalysis(opts) {
3626
3715
  this.analyzer = new Analysis(this);
3627
- this.analyzer.runAnalysis(opts.gameModes);
3716
+ this.analyzer.runAnalysis(opts);
3628
3717
  }
3629
3718
  /**
3630
3719
  * Runs the configured tasks: simulation, optimization, and/or analysis.