@slot-engine/core 0.1.4 → 0.1.6

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 CHANGED
@@ -365,6 +365,70 @@ interface GameSymbolOpts {
365
365
  properties?: Record<string, any>;
366
366
  }
367
367
 
368
+ declare class WinType {
369
+ protected payout: number;
370
+ protected winCombinations: WinCombination[];
371
+ protected ctx: GameContext;
372
+ protected readonly wildSymbol?: WildSymbol;
373
+ constructor(opts: WinTypeOpts);
374
+ /**
375
+ * Implementation of win evaluation logic. Sets `this.payout` and `this.winCombinations`.
376
+ */
377
+ evaluateWins(board: Reels): this;
378
+ /**
379
+ * Custom post-processing of wins, e.g. for handling multipliers.
380
+ */
381
+ postProcess(func: PostProcessFn<typeof this.winCombinations>): this;
382
+ /**
383
+ * Returns the total payout and detailed win combinations.
384
+ */
385
+ getWins(): {
386
+ payout: number;
387
+ winCombinations: WinCombination[];
388
+ };
389
+ protected isWild(symbol: GameSymbol): boolean;
390
+ protected getSymbolPayout(symbol: GameSymbol, count: number): number;
391
+ }
392
+ interface WinTypeOpts {
393
+ /**
394
+ * A reference to the game context.
395
+ */
396
+ ctx: GameContext<any, any, any>;
397
+ /**
398
+ * Configuration used to identify wild symbols on the board.\
399
+ * You can either provide a specific `GameSymbol` instance or a set of properties to match against symbols on the board.
400
+ *
401
+ * @example
402
+ * If you have different wild symbols, each with a property `isWild: true`, you can define:
403
+ * ```ts
404
+ * wildSymbol: { isWild: true }
405
+ * ```
406
+ *
407
+ * @example
408
+ * If you have a single wild symbol instance, you can define:
409
+ * ```ts
410
+ * wildSymbol: myWildSymbol
411
+ * ```
412
+ */
413
+ wildSymbol?: WildSymbol;
414
+ }
415
+ type WinCombination = {
416
+ payout: number;
417
+ kind: number;
418
+ baseSymbol: GameSymbol;
419
+ symbols: Array<{
420
+ symbol: GameSymbol;
421
+ isWild: boolean;
422
+ substitutedFor?: GameSymbol;
423
+ reelIndex: number;
424
+ posIndex: number;
425
+ }>;
426
+ };
427
+ type PostProcessFn<TWinCombs extends WinCombination[]> = (wins: TWinCombs, ctx: GameContext) => {
428
+ winCombinations: TWinCombs;
429
+ };
430
+ type WildSymbol = GameSymbol | Record<string, any>;
431
+
368
432
  declare class BoardService<TGameModes extends AnyGameModes = AnyGameModes, TSymbols extends AnySymbols = AnySymbols, TUserState extends AnyUserData = AnyUserData> extends AbstractService {
369
433
  private board;
370
434
  constructor(ctx: () => GameContext<TGameModes, TSymbols, TUserState>);
@@ -454,6 +518,17 @@ declare class BoardService<TGameModes extends AnyGameModes = AnyGameModes, TSymb
454
518
  newBoardSymbols: Record<string, GameSymbol[]>;
455
519
  newPaddingTopSymbols: Record<string, GameSymbol[]>;
456
520
  };
521
+ /**
522
+ * Dedupes win symbols for tumble.\
523
+ * Returns a list of symbols to remove from the board based on the given win combinations.
524
+ *
525
+ * Since it may be possible that multiple win combinations include the same symbol (e.g. Wilds),\
526
+ * this method ensures that each symbol is only listed once for removal. Otherwise tumbling may break.
527
+ */
528
+ dedupeWinSymbolsForTumble(winCombinations: WinCombination[]): {
529
+ reelIdx: number;
530
+ rowIdx: number;
531
+ }[];
457
532
  }
458
533
 
459
534
  declare class Recorder {
@@ -736,6 +811,18 @@ declare class GameService<TGameModes extends AnyGameModes = AnyGameModes, TSymbo
736
811
  * Also sets `state.triggeredFreespins` to true.
737
812
  */
738
813
  awardFreespins(amount: number): void;
814
+ /**
815
+ * Dedupes win symbols.
816
+ *
817
+ * Since it may be possible that multiple win combinations include the same symbol (e.g. Wilds),\
818
+ * this method ensures that each symbol is only listed once.
819
+ *
820
+ * If you want to tumble based on winning symbols, run them through this method first.
821
+ */
822
+ dedupeWinSymbols(winCombinations: WinCombination[]): {
823
+ reelIdx: number;
824
+ rowIdx: number;
825
+ }[];
739
826
  }
740
827
 
741
828
  declare class Wallet {
@@ -1180,70 +1267,6 @@ declare const defineUserState: <TUserState extends AnyUserData>(data: TUserState
1180
1267
  declare const defineSymbols: <TSymbols extends AnySymbols>(symbols: TSymbols) => TSymbols;
1181
1268
  declare const defineGameModes: <TGameModes extends AnyGameModes>(gameModes: TGameModes) => TGameModes;
1182
1269
 
1183
- declare class WinType {
1184
- protected payout: number;
1185
- protected winCombinations: WinCombination[];
1186
- protected ctx: GameContext;
1187
- protected readonly wildSymbol?: WildSymbol;
1188
- constructor(opts: WinTypeOpts);
1189
- /**
1190
- * Implementation of win evaluation logic. Sets `this.payout` and `this.winCombinations`.
1191
- */
1192
- evaluateWins(board: Reels): this;
1193
- /**
1194
- * Custom post-processing of wins, e.g. for handling multipliers.
1195
- */
1196
- postProcess(func: PostProcessFn<typeof this.winCombinations>): this;
1197
- /**
1198
- * Returns the total payout and detailed win combinations.
1199
- */
1200
- getWins(): {
1201
- payout: number;
1202
- winCombinations: WinCombination[];
1203
- };
1204
- protected isWild(symbol: GameSymbol): boolean;
1205
- protected getSymbolPayout(symbol: GameSymbol, count: number): number;
1206
- }
1207
- interface WinTypeOpts {
1208
- /**
1209
- * A reference to the game context.
1210
- */
1211
- ctx: GameContext<any, any, any>;
1212
- /**
1213
- * Configuration used to identify wild symbols on the board.\
1214
- * You can either provide a specific `GameSymbol` instance or a set of properties to match against symbols on the board.
1215
- *
1216
- * @example
1217
- * If you have different wild symbols, each with a property `isWild: true`, you can define:
1218
- * ```ts
1219
- * wildSymbol: { isWild: true }
1220
- * ```
1221
- *
1222
- * @example
1223
- * If you have a single wild symbol instance, you can define:
1224
- * ```ts
1225
- * wildSymbol: myWildSymbol
1226
- * ```
1227
- */
1228
- wildSymbol?: WildSymbol;
1229
- }
1230
- type WinCombination = {
1231
- payout: number;
1232
- kind: number;
1233
- baseSymbol: GameSymbol;
1234
- symbols: Array<{
1235
- symbol: GameSymbol;
1236
- isWild: boolean;
1237
- substitutedFor?: GameSymbol;
1238
- reelIndex: number;
1239
- posIndex: number;
1240
- }>;
1241
- };
1242
- type PostProcessFn<TWinCombs extends WinCombination[]> = (wins: TWinCombs, ctx: GameContext) => {
1243
- winCombinations: TWinCombs;
1244
- };
1245
- type WildSymbol = GameSymbol | Record<string, any>;
1246
-
1247
1270
  declare class LinesWinType extends WinType {
1248
1271
  protected lines: Record<number, number[]>;
1249
1272
  protected winCombinations: LineWinCombination[];
@@ -1571,6 +1594,17 @@ declare class StandaloneBoard {
1571
1594
  newBoardSymbols: Record<string, GameSymbol[]>;
1572
1595
  newPaddingTopSymbols: Record<string, GameSymbol[]>;
1573
1596
  };
1597
+ /**
1598
+ * Dedupes win symbols for tumble.\
1599
+ * Returns a list of symbols to remove from the board based on the given win combinations.
1600
+ *
1601
+ * Since it may be possible that multiple win combinations include the same symbol (e.g. Wilds),\
1602
+ * this method ensures that each symbol is only listed once for removal. Otherwise tumbling may break.
1603
+ */
1604
+ dedupeWinSymbolsForTumble(winCombinations: WinCombination[]): {
1605
+ reelIdx: number;
1606
+ rowIdx: number;
1607
+ }[];
1574
1608
  }
1575
1609
  interface StandaloneBoardOptions {
1576
1610
  ctx: GameContext;
package/dist/index.d.ts CHANGED
@@ -365,6 +365,70 @@ interface GameSymbolOpts {
365
365
  properties?: Record<string, any>;
366
366
  }
367
367
 
368
+ declare class WinType {
369
+ protected payout: number;
370
+ protected winCombinations: WinCombination[];
371
+ protected ctx: GameContext;
372
+ protected readonly wildSymbol?: WildSymbol;
373
+ constructor(opts: WinTypeOpts);
374
+ /**
375
+ * Implementation of win evaluation logic. Sets `this.payout` and `this.winCombinations`.
376
+ */
377
+ evaluateWins(board: Reels): this;
378
+ /**
379
+ * Custom post-processing of wins, e.g. for handling multipliers.
380
+ */
381
+ postProcess(func: PostProcessFn<typeof this.winCombinations>): this;
382
+ /**
383
+ * Returns the total payout and detailed win combinations.
384
+ */
385
+ getWins(): {
386
+ payout: number;
387
+ winCombinations: WinCombination[];
388
+ };
389
+ protected isWild(symbol: GameSymbol): boolean;
390
+ protected getSymbolPayout(symbol: GameSymbol, count: number): number;
391
+ }
392
+ interface WinTypeOpts {
393
+ /**
394
+ * A reference to the game context.
395
+ */
396
+ ctx: GameContext<any, any, any>;
397
+ /**
398
+ * Configuration used to identify wild symbols on the board.\
399
+ * You can either provide a specific `GameSymbol` instance or a set of properties to match against symbols on the board.
400
+ *
401
+ * @example
402
+ * If you have different wild symbols, each with a property `isWild: true`, you can define:
403
+ * ```ts
404
+ * wildSymbol: { isWild: true }
405
+ * ```
406
+ *
407
+ * @example
408
+ * If you have a single wild symbol instance, you can define:
409
+ * ```ts
410
+ * wildSymbol: myWildSymbol
411
+ * ```
412
+ */
413
+ wildSymbol?: WildSymbol;
414
+ }
415
+ type WinCombination = {
416
+ payout: number;
417
+ kind: number;
418
+ baseSymbol: GameSymbol;
419
+ symbols: Array<{
420
+ symbol: GameSymbol;
421
+ isWild: boolean;
422
+ substitutedFor?: GameSymbol;
423
+ reelIndex: number;
424
+ posIndex: number;
425
+ }>;
426
+ };
427
+ type PostProcessFn<TWinCombs extends WinCombination[]> = (wins: TWinCombs, ctx: GameContext) => {
428
+ winCombinations: TWinCombs;
429
+ };
430
+ type WildSymbol = GameSymbol | Record<string, any>;
431
+
368
432
  declare class BoardService<TGameModes extends AnyGameModes = AnyGameModes, TSymbols extends AnySymbols = AnySymbols, TUserState extends AnyUserData = AnyUserData> extends AbstractService {
369
433
  private board;
370
434
  constructor(ctx: () => GameContext<TGameModes, TSymbols, TUserState>);
@@ -454,6 +518,17 @@ declare class BoardService<TGameModes extends AnyGameModes = AnyGameModes, TSymb
454
518
  newBoardSymbols: Record<string, GameSymbol[]>;
455
519
  newPaddingTopSymbols: Record<string, GameSymbol[]>;
456
520
  };
521
+ /**
522
+ * Dedupes win symbols for tumble.\
523
+ * Returns a list of symbols to remove from the board based on the given win combinations.
524
+ *
525
+ * Since it may be possible that multiple win combinations include the same symbol (e.g. Wilds),\
526
+ * this method ensures that each symbol is only listed once for removal. Otherwise tumbling may break.
527
+ */
528
+ dedupeWinSymbolsForTumble(winCombinations: WinCombination[]): {
529
+ reelIdx: number;
530
+ rowIdx: number;
531
+ }[];
457
532
  }
458
533
 
459
534
  declare class Recorder {
@@ -736,6 +811,18 @@ declare class GameService<TGameModes extends AnyGameModes = AnyGameModes, TSymbo
736
811
  * Also sets `state.triggeredFreespins` to true.
737
812
  */
738
813
  awardFreespins(amount: number): void;
814
+ /**
815
+ * Dedupes win symbols.
816
+ *
817
+ * Since it may be possible that multiple win combinations include the same symbol (e.g. Wilds),\
818
+ * this method ensures that each symbol is only listed once.
819
+ *
820
+ * If you want to tumble based on winning symbols, run them through this method first.
821
+ */
822
+ dedupeWinSymbols(winCombinations: WinCombination[]): {
823
+ reelIdx: number;
824
+ rowIdx: number;
825
+ }[];
739
826
  }
740
827
 
741
828
  declare class Wallet {
@@ -1180,70 +1267,6 @@ declare const defineUserState: <TUserState extends AnyUserData>(data: TUserState
1180
1267
  declare const defineSymbols: <TSymbols extends AnySymbols>(symbols: TSymbols) => TSymbols;
1181
1268
  declare const defineGameModes: <TGameModes extends AnyGameModes>(gameModes: TGameModes) => TGameModes;
1182
1269
 
1183
- declare class WinType {
1184
- protected payout: number;
1185
- protected winCombinations: WinCombination[];
1186
- protected ctx: GameContext;
1187
- protected readonly wildSymbol?: WildSymbol;
1188
- constructor(opts: WinTypeOpts);
1189
- /**
1190
- * Implementation of win evaluation logic. Sets `this.payout` and `this.winCombinations`.
1191
- */
1192
- evaluateWins(board: Reels): this;
1193
- /**
1194
- * Custom post-processing of wins, e.g. for handling multipliers.
1195
- */
1196
- postProcess(func: PostProcessFn<typeof this.winCombinations>): this;
1197
- /**
1198
- * Returns the total payout and detailed win combinations.
1199
- */
1200
- getWins(): {
1201
- payout: number;
1202
- winCombinations: WinCombination[];
1203
- };
1204
- protected isWild(symbol: GameSymbol): boolean;
1205
- protected getSymbolPayout(symbol: GameSymbol, count: number): number;
1206
- }
1207
- interface WinTypeOpts {
1208
- /**
1209
- * A reference to the game context.
1210
- */
1211
- ctx: GameContext<any, any, any>;
1212
- /**
1213
- * Configuration used to identify wild symbols on the board.\
1214
- * You can either provide a specific `GameSymbol` instance or a set of properties to match against symbols on the board.
1215
- *
1216
- * @example
1217
- * If you have different wild symbols, each with a property `isWild: true`, you can define:
1218
- * ```ts
1219
- * wildSymbol: { isWild: true }
1220
- * ```
1221
- *
1222
- * @example
1223
- * If you have a single wild symbol instance, you can define:
1224
- * ```ts
1225
- * wildSymbol: myWildSymbol
1226
- * ```
1227
- */
1228
- wildSymbol?: WildSymbol;
1229
- }
1230
- type WinCombination = {
1231
- payout: number;
1232
- kind: number;
1233
- baseSymbol: GameSymbol;
1234
- symbols: Array<{
1235
- symbol: GameSymbol;
1236
- isWild: boolean;
1237
- substitutedFor?: GameSymbol;
1238
- reelIndex: number;
1239
- posIndex: number;
1240
- }>;
1241
- };
1242
- type PostProcessFn<TWinCombs extends WinCombination[]> = (wins: TWinCombs, ctx: GameContext) => {
1243
- winCombinations: TWinCombs;
1244
- };
1245
- type WildSymbol = GameSymbol | Record<string, any>;
1246
-
1247
1270
  declare class LinesWinType extends WinType {
1248
1271
  protected lines: Record<number, number[]>;
1249
1272
  protected winCombinations: LineWinCombination[];
@@ -1571,6 +1594,17 @@ declare class StandaloneBoard {
1571
1594
  newBoardSymbols: Record<string, GameSymbol[]>;
1572
1595
  newPaddingTopSymbols: Record<string, GameSymbol[]>;
1573
1596
  };
1597
+ /**
1598
+ * Dedupes win symbols for tumble.\
1599
+ * Returns a list of symbols to remove from the board based on the given win combinations.
1600
+ *
1601
+ * Since it may be possible that multiple win combinations include the same symbol (e.g. Wilds),\
1602
+ * this method ensures that each symbol is only listed once for removal. Otherwise tumbling may break.
1603
+ */
1604
+ dedupeWinSymbolsForTumble(winCombinations: WinCombination[]): {
1605
+ reelIdx: number;
1606
+ rowIdx: number;
1607
+ }[];
1574
1608
  }
1575
1609
  interface StandaloneBoardOptions {
1576
1610
  ctx: GameContext;
package/dist/index.js CHANGED
@@ -697,14 +697,15 @@ var Board = class {
697
697
  this.lastUsedReels = opts.reels;
698
698
  for (let ridx = 0; ridx < reelsAmount; ridx++) {
699
699
  const reelPos = finalReelStops[ridx];
700
+ const reelLength = opts.reels[ridx].length;
700
701
  for (let p = padSymbols - 1; p >= 0; p--) {
701
- const topPos = (reelPos - (p + 1)) % opts.reels[ridx].length;
702
+ const topPos = ((reelPos - (p + 1)) % reelLength + reelLength) % reelLength;
702
703
  this.paddingTop[ridx].push(opts.reels[ridx][topPos]);
703
- const bottomPos = (reelPos + symbolsPerReel[ridx] + p) % opts.reels[ridx].length;
704
+ const bottomPos = (reelPos + symbolsPerReel[ridx] + p) % reelLength;
704
705
  this.paddingBottom[ridx].unshift(opts.reels[ridx][bottomPos]);
705
706
  }
706
707
  for (let row = 0; row < symbolsPerReel[ridx]; row++) {
707
- const symbol = opts.reels[ridx][(reelPos + row) % opts.reels[ridx].length];
708
+ const symbol = opts.reels[ridx][(reelPos + row) % reelLength];
708
709
  if (!symbol) {
709
710
  throw new Error(`Failed to get symbol at pos ${reelPos + row} on reel ${ridx}`);
710
711
  }
@@ -772,11 +773,27 @@ var Board = class {
772
773
  newPaddingTopSymbols[ridx].unshift(padSymbol);
773
774
  }
774
775
  }
776
+ this.lastDrawnReelStops = this.lastDrawnReelStops.map((stop, ridx) => {
777
+ return newFirstSymbolPositions[ridx] ?? stop;
778
+ });
775
779
  return {
776
780
  newBoardSymbols,
777
781
  newPaddingTopSymbols
778
782
  };
779
783
  }
784
+ dedupeWinSymbolsForTumble(winCombinations) {
785
+ const symbolsMap = /* @__PURE__ */ new Map();
786
+ winCombinations.forEach((wc) => {
787
+ wc.symbols.forEach((s) => {
788
+ symbolsMap.set(`${s.reelIndex},${s.posIndex}`, {
789
+ reelIdx: s.reelIndex,
790
+ rowIdx: s.posIndex
791
+ });
792
+ });
793
+ });
794
+ const symbolsToRemove = Array.from(symbolsMap.values());
795
+ return symbolsToRemove;
796
+ }
780
797
  };
781
798
 
782
799
  // src/service/board.ts
@@ -921,6 +938,16 @@ var BoardService = class extends AbstractService {
921
938
  symbolsToDelete
922
939
  });
923
940
  }
941
+ /**
942
+ * Dedupes win symbols for tumble.\
943
+ * Returns a list of symbols to remove from the board based on the given win combinations.
944
+ *
945
+ * Since it may be possible that multiple win combinations include the same symbol (e.g. Wilds),\
946
+ * this method ensures that each symbol is only listed once for removal. Otherwise tumbling may break.
947
+ */
948
+ dedupeWinSymbolsForTumble(winCombinations) {
949
+ return this.board.dedupeWinSymbolsForTumble(winCombinations);
950
+ }
924
951
  };
925
952
 
926
953
  // src/service/data.ts
@@ -1117,6 +1144,27 @@ var GameService = class extends AbstractService {
1117
1144
  this.ctx().state.totalFreespinAmount += amount;
1118
1145
  this.ctx().state.triggeredFreespins = true;
1119
1146
  }
1147
+ /**
1148
+ * Dedupes win symbols.
1149
+ *
1150
+ * Since it may be possible that multiple win combinations include the same symbol (e.g. Wilds),\
1151
+ * this method ensures that each symbol is only listed once.
1152
+ *
1153
+ * If you want to tumble based on winning symbols, run them through this method first.
1154
+ */
1155
+ dedupeWinSymbols(winCombinations) {
1156
+ const symbolsMap = /* @__PURE__ */ new Map();
1157
+ winCombinations.forEach((wc) => {
1158
+ wc.symbols.forEach((s) => {
1159
+ symbolsMap.set(`${s.reelIndex},${s.posIndex}`, {
1160
+ reelIdx: s.reelIndex,
1161
+ rowIdx: s.posIndex
1162
+ });
1163
+ });
1164
+ });
1165
+ const symbolsToRemove = Array.from(symbolsMap.values());
1166
+ return symbolsToRemove;
1167
+ }
1120
1168
  };
1121
1169
 
1122
1170
  // src/service/wallet.ts
@@ -1731,7 +1779,12 @@ Simulating game mode: ${mode}`);
1731
1779
  }
1732
1780
  });
1733
1781
  worker.on("error", (error) => {
1734
- console.error("Error:", error);
1782
+ process.stdout.write(`
1783
+ ${error.message}
1784
+ `);
1785
+ process.stdout.write(`
1786
+ ${error.stack}
1787
+ `);
1735
1788
  reject(error);
1736
1789
  });
1737
1790
  worker.on("exit", (code) => {
@@ -2293,28 +2346,12 @@ var Analysis = class {
2293
2346
  const payoutRanges = {};
2294
2347
  for (const modeStr of gameModes) {
2295
2348
  payoutRanges[modeStr] = { overall: {}, criteria: {} };
2296
- const lutOptimized = parseLookupTable(
2297
- import_fs3.default.readFileSync(this.filePaths[modeStr].lutOptimized, "utf-8")
2298
- );
2299
2349
  const lutSegmented = parseLookupTableSegmented(
2300
2350
  import_fs3.default.readFileSync(this.filePaths[modeStr].lutSegmented, "utf-8")
2301
2351
  );
2302
- lutOptimized.forEach(([, , p]) => {
2303
- const payout = p / 100;
2304
- for (const [min, max] of winRanges) {
2305
- if (payout >= min && payout <= max) {
2306
- const rangeKey = `${min}-${max}`;
2307
- if (!payoutRanges[modeStr].overall[rangeKey]) {
2308
- payoutRanges[modeStr].overall[rangeKey] = 0;
2309
- }
2310
- payoutRanges[modeStr].overall[rangeKey] += 1;
2311
- break;
2312
- }
2313
- }
2314
- });
2315
2352
  lutSegmented.forEach(([, criteria, bp, fsp]) => {
2316
- const basePayout = bp / 100;
2317
- const freeSpinPayout = fsp / 100;
2353
+ const basePayout = bp;
2354
+ const freeSpinPayout = fsp;
2318
2355
  const payout = basePayout + freeSpinPayout;
2319
2356
  for (const [min, max] of winRanges) {
2320
2357
  if (payout >= min && payout <= max) {
@@ -3771,6 +3808,16 @@ var StandaloneBoard = class {
3771
3808
  padSymbols: this.padSymbols
3772
3809
  });
3773
3810
  }
3811
+ /**
3812
+ * Dedupes win symbols for tumble.\
3813
+ * Returns a list of symbols to remove from the board based on the given win combinations.
3814
+ *
3815
+ * Since it may be possible that multiple win combinations include the same symbol (e.g. Wilds),\
3816
+ * this method ensures that each symbol is only listed once for removal. Otherwise tumbling may break.
3817
+ */
3818
+ dedupeWinSymbolsForTumble(winCombinations) {
3819
+ return this.board.dedupeWinSymbolsForTumble(winCombinations);
3820
+ }
3774
3821
  };
3775
3822
  // Annotate the CommonJS export names for ESM import in node:
3776
3823
  0 && (module.exports = {