@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.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
- return new _GameSymbol({
403
- id: this.id,
404
- pays: this.pays ? { ...this.pays } : void 0,
405
- properties: Object.fromEntries(this.properties)
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(data);
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(gameModes) {
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.gameModes);
3731
+ this.analyzer.runAnalysis(opts);
3628
3732
  }
3629
3733
  /**
3630
3734
  * Runs the configured tasks: simulation, optimization, and/or analysis.