@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.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(data);
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(gameModes) {
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.gameModes);
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
- let searchReelIdx = 0;
4066
- let searchActive = true;
4067
- while (searchActive && searchReelIdx < reels.length) {
4068
- const reel = reels[searchReelIdx];
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
- if (!hasWild) {
4077
- searchActive = false;
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 Math.max(...Object.keys(symbolList).map((k) => parseInt(k, 10))) + 1;
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