slot-server 1.0.0

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.cjs ADDED
@@ -0,0 +1,2944 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ AuditManager: () => AuditManager,
24
+ BonusManager: () => BonusManager,
25
+ BuyBonusManager: () => BuyBonusManager,
26
+ CascadingReelsManager: () => CascadingReelsManager,
27
+ ConfigBuilder: () => ConfigBuilder,
28
+ DebugManager: () => DebugManager,
29
+ ExpandingWildsManager: () => ExpandingWildsManager,
30
+ GambleManager: () => GambleManager,
31
+ GameSession: () => GameSession,
32
+ HistoryManager: () => HistoryManager,
33
+ InMemoryAuditStorage: () => InMemoryAuditStorage,
34
+ InMemoryWalletStorage: () => InMemoryWalletStorage,
35
+ JackpotManager: () => JackpotManager,
36
+ Random: () => Random,
37
+ RespinsManager: () => RespinsManager,
38
+ ResponsibleGamingManager: () => ResponsibleGamingManager,
39
+ ScatterEnhancementsManager: () => ScatterEnhancementsManager,
40
+ SpinEvaluator: () => SpinEvaluator,
41
+ WalletManager: () => WalletManager,
42
+ WaysEvaluator: () => WaysEvaluator,
43
+ sampleConfig: () => sampleConfig
44
+ });
45
+ module.exports = __toCommonJS(index_exports);
46
+
47
+ // src/ConfigBuilder.ts
48
+ var ConfigBuilder = class {
49
+ config = {};
50
+ setGameId(id) {
51
+ this.config.gameId = id;
52
+ return this;
53
+ }
54
+ setReelCount(count) {
55
+ this.config.reelCount = count;
56
+ return this;
57
+ }
58
+ setRowCount(count) {
59
+ this.config.rowCount = count;
60
+ return this;
61
+ }
62
+ setSymbolMap(map) {
63
+ this.config.symbolMap = map;
64
+ return this;
65
+ }
66
+ setPaytableData(data) {
67
+ this.config.paytableData = data;
68
+ return this;
69
+ }
70
+ setReelStrips(strips) {
71
+ this.config.reelStrips = strips;
72
+ return this;
73
+ }
74
+ setLinePatterns(patterns) {
75
+ this.config.linePatterns = patterns;
76
+ return this;
77
+ }
78
+ setCurrency(code, symbol, decimalSeparator = ".", thousandSeparator = ",", minDecimalPlaces = 2) {
79
+ this.config.currency = { code, symbol, decimalSeparator, thousandSeparator, minDecimalPlaces };
80
+ return this;
81
+ }
82
+ setFeatures(features) {
83
+ this.config.features = features;
84
+ return this;
85
+ }
86
+ setBetRange(min, max, step, defaultBet) {
87
+ this.config.minBet = min;
88
+ this.config.maxBet = max;
89
+ this.config.betStep = step;
90
+ this.config.defaultBet = defaultBet ?? min;
91
+ return this;
92
+ }
93
+ build() {
94
+ const required = [
95
+ "gameId",
96
+ "reelCount",
97
+ "rowCount",
98
+ "symbolMap",
99
+ "paytableData",
100
+ "reelStrips",
101
+ "linePatterns"
102
+ ];
103
+ for (const key of required) {
104
+ if (this.config[key] === void 0) {
105
+ throw new Error(`ConfigBuilder: Missing required field "${key}"`);
106
+ }
107
+ }
108
+ return {
109
+ ...this.config,
110
+ gameId: this.config.gameId,
111
+ // Assert that gameId is definitely present after validation
112
+ reelCount: this.config.reelCount,
113
+ rowCount: this.config.rowCount,
114
+ symbolMap: this.config.symbolMap,
115
+ paytableData: this.config.paytableData,
116
+ reelStrips: this.config.reelStrips,
117
+ linePatterns: this.config.linePatterns,
118
+ currency: this.config.currency || { code: "USD", symbol: "$", decimalSeparator: ".", thousandSeparator: ",", minDecimalPlaces: 2 },
119
+ features: this.config.features || { autoplay: true, fastSpin: true, realityCheck: true, fullscreen: true },
120
+ defaultBet: this.config.defaultBet ?? 1,
121
+ minBet: this.config.minBet ?? 0.1,
122
+ maxBet: this.config.maxBet ?? 100,
123
+ betStep: this.config.betStep ?? 0.1,
124
+ defaultLines: this.config.defaultLines ?? this.config.linePatterns.length,
125
+ rtp: this.config.rtp ?? 96.5,
126
+ volatility: this.config.volatility ?? "medium"
127
+ };
128
+ }
129
+ };
130
+
131
+ // src/Random.ts
132
+ var Random = class {
133
+ seed;
134
+ constructor(seed) {
135
+ this.seed = seed;
136
+ }
137
+ next() {
138
+ if (this.seed !== void 0) {
139
+ this.seed = (this.seed * 9301 + 49297) % 233280;
140
+ return this.seed / 233280;
141
+ }
142
+ return Math.random();
143
+ }
144
+ nextInt(min, max) {
145
+ return Math.floor(this.next() * (max - min)) + min;
146
+ }
147
+ pick(arr) {
148
+ return arr[this.nextInt(0, arr.length)];
149
+ }
150
+ shuffle(arr) {
151
+ for (let i = arr.length - 1; i > 0; i--) {
152
+ const j = this.nextInt(0, i + 1);
153
+ [arr[i], arr[j]] = [arr[j], arr[i]];
154
+ }
155
+ return arr;
156
+ }
157
+ };
158
+
159
+ // src/SpinEvaluator.ts
160
+ var SpinEvaluator = class {
161
+ config;
162
+ rng;
163
+ constructor(config, rng) {
164
+ this.config = config;
165
+ this.rng = rng;
166
+ }
167
+ /**
168
+ * Generate a random spin result (visible symbols for each reel).
169
+ * Returns a 2D array: [reelIndex][rowIndex]
170
+ */
171
+ generateResult() {
172
+ const { reelCount, rowCount, reelStrips } = this.config;
173
+ const result = [];
174
+ for (let r = 0; r < reelCount; r++) {
175
+ const strip = reelStrips[r];
176
+ const offset = this.rng.nextInt(0, strip.length - rowCount);
177
+ const row = [];
178
+ for (let i = 0; i < rowCount; i++) {
179
+ row.push(strip[offset + i]);
180
+ }
181
+ result.push(row);
182
+ }
183
+ return result;
184
+ }
185
+ /**
186
+ * Evaluate the result against all paylines and the paytable.
187
+ * Returns win lines and total win amount.
188
+ */
189
+ // FIXED: Added stakePerLine as a parameter since it belongs to the request payload, not the class instance
190
+ evaluateResult(result, stakePerLine) {
191
+ const { linePatterns, paytableData } = this.config;
192
+ const winLines = [];
193
+ let totalWin = 0;
194
+ for (let li = 0; li < linePatterns.length; li++) {
195
+ const pattern = linePatterns[li];
196
+ const symbols = pattern.map((pos) => result[pos.reel]?.[pos.row]);
197
+ if (symbols.some((s) => s === void 0)) continue;
198
+ const firstSymbol = symbols[0];
199
+ let count = 1;
200
+ for (let i = 1; i < symbols.length; i++) {
201
+ if (symbols[i] === firstSymbol) {
202
+ count++;
203
+ } else {
204
+ break;
205
+ }
206
+ }
207
+ const symbolPaytable = paytableData[firstSymbol];
208
+ if (!symbolPaytable) continue;
209
+ const multiplierKey = `x${count}`;
210
+ const multiplier = symbolPaytable[multiplierKey];
211
+ if (!multiplier || multiplier === 0) continue;
212
+ const winAmount = (stakePerLine || 1) * multiplier;
213
+ totalWin += winAmount;
214
+ winLines.push({
215
+ lineIndex: li,
216
+ symbolId: firstSymbol,
217
+ multiplier,
218
+ winAmount,
219
+ positions: pattern.slice(0, count)
220
+ });
221
+ }
222
+ return { winLines, totalWin };
223
+ }
224
+ /**
225
+ * Full spin processing: generate result, evaluate, build response.
226
+ */
227
+ // FIXED: Swapped the messy inline type for the clean, updated ISpinRequestData interface
228
+ spin(request) {
229
+ const result = this.generateResult();
230
+ const stakePerLine = request.stakePerLine;
231
+ const { winLines, totalWin } = this.evaluateResult(result, stakePerLine);
232
+ return {
233
+ result,
234
+ totalWin,
235
+ stakePerLine: request.stakePerLine,
236
+ totalStake: request.totalStake,
237
+ gameMode: request.gameMode,
238
+ winLines: winLines.length > 0 ? winLines : void 0
239
+ };
240
+ }
241
+ };
242
+
243
+ // src/BonusManager.ts
244
+ var BonusManager = class {
245
+ rng;
246
+ bonusState = null;
247
+ constructor(rng) {
248
+ this.rng = rng;
249
+ }
250
+ /**
251
+ * Check if a spin result should trigger a bonus.
252
+ * This is a simple example: trigger on 3+ scatter symbols (e.g., symbol ID 9).
253
+ */
254
+ checkTrigger(result) {
255
+ const scatterId = 9;
256
+ let count = 0;
257
+ for (const row of result) {
258
+ for (const symbol of row) {
259
+ if (symbol === scatterId) count++;
260
+ }
261
+ }
262
+ if (count >= 3) {
263
+ return {
264
+ triggered: true,
265
+ data: { scatterCount: count, freeSpins: count * 2 }
266
+ };
267
+ }
268
+ return { triggered: false };
269
+ }
270
+ /**
271
+ * Start a free spin bonus.
272
+ */
273
+ startFreeSpins(spins, multiplier = 1, bet) {
274
+ this.bonusState = {
275
+ type: "freespin",
276
+ spinsRemaining: spins,
277
+ multiplier,
278
+ totalWin: 0,
279
+ bet
280
+ };
281
+ return this.bonusState;
282
+ }
283
+ /**
284
+ * Start a pick bonus (e.g., pick items for prizes).
285
+ */
286
+ startPickBonus(items) {
287
+ this.bonusState = {
288
+ type: "pick",
289
+ items,
290
+ pickedIds: [],
291
+ totalWin: 0
292
+ };
293
+ return this.bonusState;
294
+ }
295
+ /**
296
+ * Process a pick action.
297
+ */
298
+ pickItem(itemId) {
299
+ const state = this.bonusState;
300
+ if (!state || state.type !== "pick") {
301
+ throw new Error("No active pick bonus");
302
+ }
303
+ const item = state.items.find((i) => i.id === itemId);
304
+ if (!item) throw new Error("Invalid item");
305
+ if (state.pickedIds.includes(itemId)) throw new Error("Item already picked");
306
+ state.pickedIds.push(itemId);
307
+ state.totalWin += item.value;
308
+ const remaining = state.items.filter((i) => !state.pickedIds.includes(i.id));
309
+ return { win: item.value, remaining };
310
+ }
311
+ /**
312
+ * Process a free spin: deduct one spin, return the spin result.
313
+ * The spin result is generated by the SpinEvaluator, but we need to apply the multiplier.
314
+ */
315
+ processFreeSpin(spinResult) {
316
+ const state = this.bonusState;
317
+ if (!state || state.type !== "freespin") {
318
+ throw new Error("No active free spin bonus");
319
+ }
320
+ if (state.spinsRemaining <= 0) {
321
+ throw new Error("No free spins remaining");
322
+ }
323
+ state.spinsRemaining--;
324
+ const multiplier = state.multiplier || 1;
325
+ const totalWin = (spinResult.totalWin || 0) * multiplier;
326
+ state.totalWin += totalWin;
327
+ return {
328
+ ...spinResult,
329
+ totalWin,
330
+ spinsRemaining: state.spinsRemaining
331
+ };
332
+ }
333
+ /**
334
+ * Check if a bonus is active.
335
+ */
336
+ isActive() {
337
+ return this.bonusState !== null && (this.bonusState.type === "freespin" && this.bonusState.spinsRemaining > 0 || this.bonusState.type === "pick" && this.bonusState.items.length > this.bonusState.pickedIds.length);
338
+ }
339
+ /**
340
+ * Get the current bonus state.
341
+ */
342
+ getState() {
343
+ return this.bonusState;
344
+ }
345
+ /**
346
+ * End the current bonus.
347
+ */
348
+ endBonus() {
349
+ this.bonusState = null;
350
+ }
351
+ };
352
+
353
+ // src/HistoryManager.ts
354
+ var HistoryManager = class {
355
+ history = [];
356
+ maxEntries;
357
+ constructor(maxEntries = 100) {
358
+ this.maxEntries = maxEntries;
359
+ }
360
+ /**
361
+ * Add a spin result to history.
362
+ */
363
+ addEntry(result, bet, win, balance, gameMode) {
364
+ const entry = {
365
+ time: Date.now(),
366
+ bet,
367
+ win,
368
+ balance,
369
+ result: result.map((row) => [...row]),
370
+ // deep copy
371
+ gameMode
372
+ };
373
+ this.history.push(entry);
374
+ if (this.history.length > this.maxEntries) {
375
+ this.history.shift();
376
+ }
377
+ }
378
+ /**
379
+ * Get all history entries.
380
+ */
381
+ getEntries() {
382
+ return this.history.slice();
383
+ }
384
+ /**
385
+ * Get the last N entries.
386
+ */
387
+ getLast(n) {
388
+ return this.history.slice(-n);
389
+ }
390
+ /**
391
+ * Clear history.
392
+ */
393
+ clear() {
394
+ this.history = [];
395
+ }
396
+ };
397
+
398
+ // src/GameSession.ts
399
+ var GameSession = class {
400
+ config;
401
+ playerId;
402
+ balanceProvider;
403
+ state;
404
+ spinEvaluator;
405
+ bonusManager;
406
+ historyManager;
407
+ rng;
408
+ constructor(config, playerId, balanceProvider, seed) {
409
+ this.config = config;
410
+ this.playerId = playerId;
411
+ this.balanceProvider = balanceProvider;
412
+ this.rng = new Random(seed);
413
+ this.spinEvaluator = new SpinEvaluator(config, this.rng);
414
+ this.bonusManager = new BonusManager(this.rng);
415
+ this.historyManager = new HistoryManager(100);
416
+ const initialBalance = balanceProvider.getBalance(playerId);
417
+ this.state = {
418
+ balance: initialBalance,
419
+ bet: config.defaultBet || 1,
420
+ gameMode: 0,
421
+ freeSpinsRemaining: 0,
422
+ bonusActive: false,
423
+ history: []
424
+ };
425
+ }
426
+ /**
427
+ * Get the current full player state.
428
+ */
429
+ getState() {
430
+ return { ...this.state };
431
+ }
432
+ /**
433
+ * Build the INIT response for the client.
434
+ */
435
+ getInitResponse() {
436
+ return {
437
+ balance: this.state.balance,
438
+ bet: this.state.bet,
439
+ config: this.config,
440
+ bonusOffers: null,
441
+ // you can add offers here
442
+ state: {
443
+ gameMode: this.state.gameMode,
444
+ freeSpinsRemaining: this.state.freeSpinsRemaining,
445
+ bonusActive: this.state.bonusActive
446
+ }
447
+ };
448
+ }
449
+ /**
450
+ * Set the player's bet.
451
+ */
452
+ setBet(bet) {
453
+ if (bet < this.config.minBet || bet > this.config.maxBet) {
454
+ throw new Error(`Bet must be between ${this.config.minBet} and ${this.config.maxBet}`);
455
+ }
456
+ this.state.bet = bet;
457
+ }
458
+ /**
459
+ * Process a spin request.
460
+ */
461
+ spin(request) {
462
+ const totalStake = request.totalStake;
463
+ const stakePerLine = request.stakePerLine;
464
+ if (this.state.balance < totalStake) {
465
+ return { ...this.createErrorResponse("Insufficient balance"), balanceAfter: this.state.balance };
466
+ }
467
+ this.state.balance -= totalStake;
468
+ this.balanceProvider.setBalance(this.playerId, this.state.balance);
469
+ let spinResult;
470
+ let isBonusSpin = false;
471
+ if (this.bonusManager.isActive() && this.bonusManager.getState()?.type === "freespin") {
472
+ const rawResult = this.spinEvaluator.generateResult();
473
+ const evaluated = this.spinEvaluator.spin({
474
+ action: request.action,
475
+ stakePerLine: request.stakePerLine,
476
+ selectedLines: request.selectedLines,
477
+ totalStake: request.totalStake,
478
+ gameMode: 1
479
+ });
480
+ const bonusSpinResult = this.bonusManager.processFreeSpin(evaluated);
481
+ spinResult = bonusSpinResult;
482
+ isBonusSpin = true;
483
+ } else {
484
+ spinResult = this.spinEvaluator.spin(request);
485
+ }
486
+ let bonusTriggered = false;
487
+ let bonusData = null;
488
+ if (!isBonusSpin) {
489
+ const trigger = this.bonusManager.checkTrigger(spinResult.result);
490
+ if (trigger.triggered) {
491
+ bonusTriggered = true;
492
+ bonusData = trigger.data;
493
+ const freeSpins = bonusData.freeSpins || 5;
494
+ const multiplier = bonusData.multiplier || 1;
495
+ this.bonusManager.startFreeSpins(freeSpins, multiplier, request.stakePerLine);
496
+ this.state.bonusActive = true;
497
+ this.state.freeSpinsRemaining = freeSpins;
498
+ spinResult.freeSpinsRemaining = freeSpins;
499
+ spinResult.bonusTriggered = true;
500
+ spinResult.bonusData = bonusData;
501
+ }
502
+ }
503
+ const winAmount = spinResult.totalWin || 0;
504
+ if (winAmount > 0) {
505
+ this.state.balance += winAmount;
506
+ this.balanceProvider.setBalance(this.playerId, this.state.balance);
507
+ }
508
+ if (spinResult.freeSpinsRemaining !== void 0) {
509
+ this.state.freeSpinsRemaining = spinResult.freeSpinsRemaining;
510
+ if (this.state.freeSpinsRemaining <= 0) {
511
+ this.state.bonusActive = false;
512
+ this.bonusManager.endBonus();
513
+ }
514
+ }
515
+ this.historyManager.addEntry(
516
+ spinResult.result,
517
+ request.totalStake,
518
+ winAmount,
519
+ this.state.balance,
520
+ request.gameMode
521
+ );
522
+ const response = {
523
+ ...spinResult,
524
+ balanceAfter: this.state.balance
525
+ };
526
+ if (spinResult.winLines) {
527
+ response.winLines = spinResult.winLines;
528
+ }
529
+ return response;
530
+ }
531
+ /**
532
+ * Get history.
533
+ */
534
+ getHistory(limit = 20) {
535
+ const entries = this.historyManager.getLast(limit);
536
+ return {
537
+ entries,
538
+ total: this.historyManager.getEntries().length
539
+ };
540
+ }
541
+ /**
542
+ * Process a bonus action (e.g., pick).
543
+ */
544
+ bonusAction(action, payload) {
545
+ if (action === "pick" && payload?.itemId) {
546
+ const result = this.bonusManager.pickItem(payload.itemId);
547
+ const win = result.win;
548
+ if (win > 0) {
549
+ this.state.balance += win;
550
+ this.balanceProvider.setBalance(this.playerId, this.state.balance);
551
+ }
552
+ const remaining = result.remaining;
553
+ if (remaining.length === 0) {
554
+ this.bonusManager.endBonus();
555
+ this.state.bonusActive = false;
556
+ return {
557
+ state: "COMPLETE",
558
+ data: { totalWin: this.bonusManager.getState()?.totalWin || 0 }
559
+ };
560
+ }
561
+ return {
562
+ state: "ACTIVE",
563
+ data: { remaining, lastWin: win }
564
+ };
565
+ }
566
+ throw new Error(`Unknown bonus action: ${action}`);
567
+ }
568
+ /**
569
+ * Helper to create an error response.
570
+ */
571
+ createErrorResponse(error) {
572
+ return {
573
+ result: [],
574
+ totalWin: 0,
575
+ stakePerLine: 0,
576
+ totalStake: 0,
577
+ gameMode: 0,
578
+ error
579
+ };
580
+ }
581
+ };
582
+
583
+ // src/JackpotManager.ts
584
+ var JackpotManager = class {
585
+ config;
586
+ rng;
587
+ tiers;
588
+ constructor(config, rng) {
589
+ this.config = config;
590
+ this.rng = rng;
591
+ this.tiers = /* @__PURE__ */ new Map();
592
+ if (config.enabled && config.tiers) {
593
+ for (const tier of config.tiers) {
594
+ this.tiers.set(tier.id, { ...tier });
595
+ }
596
+ }
597
+ }
598
+ /**
599
+ * Calculate jackpot contribution from a bet.
600
+ * Returns contributions for each tier.
601
+ */
602
+ calculateContribution(totalStake) {
603
+ const contributions = [];
604
+ if (!this.config.enabled) {
605
+ return contributions;
606
+ }
607
+ for (const [tierId, tier] of this.tiers.entries()) {
608
+ const contributedAmount = totalStake * (tier.contributionRate / 100);
609
+ tier.currentAmount += contributedAmount;
610
+ contributions.push({
611
+ tierId,
612
+ contributedAmount,
613
+ newJackpotAmount: tier.currentAmount
614
+ });
615
+ }
616
+ return contributions;
617
+ }
618
+ /**
619
+ * Check if a jackpot was triggered.
620
+ * Uses probability-based triggering.
621
+ */
622
+ checkTrigger() {
623
+ if (!this.config.enabled) {
624
+ return null;
625
+ }
626
+ for (const [tierId, tier] of this.tiers.entries()) {
627
+ const roll = this.rng.next();
628
+ if (roll < tier.triggerProbability) {
629
+ const winAmount = tier.currentAmount;
630
+ tier.currentAmount = tier.seedAmount;
631
+ return {
632
+ tierId,
633
+ tierName: tier.name,
634
+ winAmount,
635
+ timestamp: Date.now()
636
+ };
637
+ }
638
+ }
639
+ return null;
640
+ }
641
+ /**
642
+ * Get current jackpot amounts for all tiers.
643
+ */
644
+ getCurrentAmounts() {
645
+ const amounts = {};
646
+ for (const [tierId, tier] of this.tiers.entries()) {
647
+ amounts[tierId] = tier.currentAmount;
648
+ }
649
+ return amounts;
650
+ }
651
+ /**
652
+ * Get a specific tier's current amount.
653
+ */
654
+ getTierAmount(tierId) {
655
+ const tier = this.tiers.get(tierId);
656
+ return tier?.currentAmount;
657
+ }
658
+ /**
659
+ * Update a tier's current amount (for external sync).
660
+ */
661
+ updateTierAmount(tierId, amount) {
662
+ const tier = this.tiers.get(tierId);
663
+ if (tier) {
664
+ tier.currentAmount = amount;
665
+ }
666
+ }
667
+ /**
668
+ * Check if a bet is eligible for jackpot (based on minBetRequired).
669
+ */
670
+ isBetEligible(totalStake) {
671
+ if (!this.config.enabled) {
672
+ return false;
673
+ }
674
+ for (const tier of this.tiers.values()) {
675
+ if (tier.minBetRequired !== void 0 && totalStake < tier.minBetRequired) {
676
+ return false;
677
+ }
678
+ }
679
+ return true;
680
+ }
681
+ /**
682
+ * Get configuration for a specific tier.
683
+ */
684
+ getTierConfig(tierId) {
685
+ return this.tiers.get(tierId);
686
+ }
687
+ /**
688
+ * Get all tier configurations.
689
+ */
690
+ getAllTiers() {
691
+ return Array.from(this.tiers.values());
692
+ }
693
+ /**
694
+ * Reset all tiers to seed amounts.
695
+ */
696
+ resetAll() {
697
+ for (const tier of this.tiers.values()) {
698
+ tier.currentAmount = tier.seedAmount;
699
+ }
700
+ }
701
+ };
702
+
703
+ // src/WaysEvaluator.ts
704
+ var WaysEvaluator = class {
705
+ config;
706
+ paytableData;
707
+ reelCount;
708
+ rowCount;
709
+ constructor(config, paytableData, reelCount, rowCount) {
710
+ this.config = config;
711
+ this.paytableData = paytableData;
712
+ this.reelCount = reelCount;
713
+ this.rowCount = rowCount;
714
+ }
715
+ /**
716
+ * Evaluate a spin result for ways-to-win.
717
+ * Returns win lines and total win amount.
718
+ */
719
+ evaluate(result, stakePerLine) {
720
+ const { minMatches, direction } = this.config;
721
+ const winLines = [];
722
+ let totalWin = 0;
723
+ let totalWays = 0;
724
+ const symbolCountsPerReel = /* @__PURE__ */ new Map();
725
+ for (let r = 0; r < this.reelCount; r++) {
726
+ const reelSymbols = result[r] || [];
727
+ for (const symbolId of reelSymbols) {
728
+ if (!symbolCountsPerReel.has(symbolId)) {
729
+ symbolCountsPerReel.set(symbolId, new Array(this.reelCount).fill(0));
730
+ }
731
+ const counts = symbolCountsPerReel.get(symbolId);
732
+ counts[r]++;
733
+ }
734
+ }
735
+ for (const [symbolId, counts] of symbolCountsPerReel.entries()) {
736
+ const symbolPaytable = this.paytableData[symbolId];
737
+ if (!symbolPaytable) continue;
738
+ let consecutiveReels = 0;
739
+ const positions = [];
740
+ for (let r = 0; r < this.reelCount; r++) {
741
+ if (counts[r] > 0) {
742
+ consecutiveReels++;
743
+ const reelSymbols = result[r] || [];
744
+ for (let row = 0; row < reelSymbols.length; row++) {
745
+ if (reelSymbols[row] === symbolId) {
746
+ positions.push({ reel: r, row });
747
+ }
748
+ }
749
+ } else {
750
+ break;
751
+ }
752
+ }
753
+ if (consecutiveReels >= minMatches) {
754
+ const multiplierKey = `x${consecutiveReels}`;
755
+ const multiplier = symbolPaytable[multiplierKey];
756
+ if (multiplier && multiplier > 0) {
757
+ let ways = 1;
758
+ for (let r = 0; r < consecutiveReels; r++) {
759
+ ways *= counts[r];
760
+ }
761
+ if (direction === "bothDirections") {
762
+ let consecutiveFromRight = 0;
763
+ for (let r = this.reelCount - 1; r >= 0; r--) {
764
+ if (counts[r] > 0) {
765
+ consecutiveFromRight++;
766
+ } else {
767
+ break;
768
+ }
769
+ }
770
+ if (consecutiveFromRight >= minMatches && consecutiveFromRight > consecutiveReels) {
771
+ ways = 1;
772
+ for (let r = this.reelCount - consecutiveFromRight; r < this.reelCount; r++) {
773
+ ways *= counts[r];
774
+ }
775
+ consecutiveReels = consecutiveFromRight;
776
+ }
777
+ }
778
+ const winAmount = stakePerLine * multiplier * ways;
779
+ totalWin += winAmount;
780
+ totalWays += ways;
781
+ winLines.push({
782
+ lineIndex: symbolId,
783
+ // Use symbol ID as line index for ways
784
+ symbolId,
785
+ multiplier,
786
+ winAmount,
787
+ positions: positions.slice(0, consecutiveReels * this.rowCount),
788
+ wayIndex: totalWays
789
+ });
790
+ }
791
+ }
792
+ }
793
+ return { winLines, totalWin, ways: totalWays };
794
+ }
795
+ /**
796
+ * Get the maximum possible ways for this configuration.
797
+ */
798
+ getMaxWays() {
799
+ return Math.pow(this.rowCount, this.reelCount);
800
+ }
801
+ /**
802
+ * Check if ways-to-win is enabled.
803
+ */
804
+ isEnabled() {
805
+ return this.config.enabled;
806
+ }
807
+ };
808
+
809
+ // src/GambleManager.ts
810
+ var GambleManager = class {
811
+ config;
812
+ rng;
813
+ consecutiveGambles = 0;
814
+ currentGambleWin = 0;
815
+ constructor(config, rng) {
816
+ this.config = config;
817
+ this.rng = rng;
818
+ }
819
+ /**
820
+ * Check if gamble feature is enabled and available.
821
+ */
822
+ canGamble(currentWin) {
823
+ if (!this.config.enabled) {
824
+ return false;
825
+ }
826
+ if (currentWin <= 0) {
827
+ return false;
828
+ }
829
+ if (this.config.maxGambleAmount !== void 0 && currentWin > this.config.maxGambleAmount) {
830
+ return false;
831
+ }
832
+ if (this.consecutiveGambles >= this.config.maxConsecutiveGambles) {
833
+ return false;
834
+ }
835
+ if (this.config.autoCollectAt !== void 0 && currentWin >= this.config.autoCollectAt) {
836
+ return false;
837
+ }
838
+ return true;
839
+ }
840
+ /**
841
+ * Process a gamble request.
842
+ */
843
+ processGamble(request) {
844
+ const { gambleType, currentWin, choice } = request;
845
+ if (!this.canGamble(currentWin)) {
846
+ return {
847
+ result: "lose",
848
+ winAmount: 0,
849
+ canGambleAgain: false,
850
+ consecutiveGambles: this.consecutiveGambles
851
+ };
852
+ }
853
+ let result;
854
+ let winAmount = 0;
855
+ switch (gambleType) {
856
+ case "card":
857
+ result = this.playCardGame(choice);
858
+ winAmount = result === "win" ? currentWin * 2 : 0;
859
+ break;
860
+ case "ladder":
861
+ result = this.playLadderGame(choice);
862
+ winAmount = result === "win" ? currentWin * 1.5 : 0;
863
+ break;
864
+ case "wheel":
865
+ result = this.playWheelGame(choice);
866
+ winAmount = result === "win" ? currentWin * this.rng.nextInt(2, 5) : 0;
867
+ break;
868
+ default:
869
+ result = "lose";
870
+ winAmount = 0;
871
+ }
872
+ if (result === "win") {
873
+ this.consecutiveGambles++;
874
+ this.currentGambleWin = winAmount;
875
+ } else if (result === "lose") {
876
+ this.consecutiveGambles = 0;
877
+ this.currentGambleWin = 0;
878
+ }
879
+ return {
880
+ result,
881
+ winAmount,
882
+ canGambleAgain: this.canGamble(winAmount),
883
+ consecutiveGambles: this.consecutiveGambles
884
+ };
885
+ }
886
+ /**
887
+ * Card game: guess if card is red or black (50/50 chance).
888
+ */
889
+ playCardGame(choice) {
890
+ const isRed = this.rng.next() < 0.5;
891
+ const playerChoosesRed = choice === "red" || choice === 0 || choice === void 0;
892
+ if (playerChoosesRed && isRed || !playerChoosesRed && !isRed) {
893
+ return "win";
894
+ }
895
+ return "lose";
896
+ }
897
+ /**
898
+ * Ladder game: 60% chance to win, 40% to lose.
899
+ */
900
+ playLadderGame(choice) {
901
+ const winChance = 0.6;
902
+ return this.rng.next() < winChance ? "win" : "lose";
903
+ }
904
+ /**
905
+ * Wheel game: multiple outcomes based on wheel segments.
906
+ */
907
+ playWheelGame(choice) {
908
+ const segments = [
909
+ { value: 0, weight: 20 },
910
+ // Lose
911
+ { value: 1, weight: 30 },
912
+ // Push (get bet back)
913
+ { value: 2, weight: 30 },
914
+ // 2x win
915
+ { value: 3, weight: 15 },
916
+ // 3x win
917
+ { value: 5, weight: 5 }
918
+ // 5x win (jackpot)
919
+ ];
920
+ const totalWeight = segments.reduce((sum, s) => sum + s.weight, 0);
921
+ let roll = this.rng.next() * totalWeight;
922
+ for (const segment of segments) {
923
+ roll -= segment.weight;
924
+ if (roll <= 0) {
925
+ if (segment.value === 0) return "lose";
926
+ if (segment.value === 1) return "push";
927
+ return "win";
928
+ }
929
+ }
930
+ return "lose";
931
+ }
932
+ /**
933
+ * Get current consecutive gamble count.
934
+ */
935
+ getConsecutiveGambles() {
936
+ return this.consecutiveGambles;
937
+ }
938
+ /**
939
+ * Reset gamble state (e.g., after collecting winnings).
940
+ */
941
+ reset() {
942
+ this.consecutiveGambles = 0;
943
+ this.currentGambleWin = 0;
944
+ }
945
+ /**
946
+ * Get the current gamble win amount.
947
+ */
948
+ getCurrentGambleWin() {
949
+ return this.currentGambleWin;
950
+ }
951
+ /**
952
+ * Get gamble configuration.
953
+ */
954
+ getConfig() {
955
+ return this.config;
956
+ }
957
+ };
958
+
959
+ // src/BuyBonusManager.ts
960
+ var BuyBonusManager = class {
961
+ config;
962
+ purchasesThisSession = 0;
963
+ lastPurchaseTime = 0;
964
+ constructor(config) {
965
+ this.config = config;
966
+ }
967
+ /**
968
+ * Check if buying bonus is enabled and available.
969
+ */
970
+ canBuyBonus(currentBet) {
971
+ if (!this.config.enabled) {
972
+ return { canBuy: false, reason: "Buy bonus feature is disabled" };
973
+ }
974
+ if (this.config.minBetRequired !== void 0 && currentBet < this.config.minBetRequired) {
975
+ return {
976
+ canBuy: false,
977
+ reason: `Minimum bet of ${this.config.minBetRequired} required`
978
+ };
979
+ }
980
+ if (this.config.maxPurchasesPerSession !== void 0 && this.purchasesThisSession >= this.config.maxPurchasesPerSession) {
981
+ return {
982
+ canBuy: false,
983
+ reason: `Maximum purchases (${this.config.maxPurchasesPerSession}) reached for this session`
984
+ };
985
+ }
986
+ if (this.config.cooldownMs !== void 0) {
987
+ const timeSinceLastPurchase = Date.now() - this.lastPurchaseTime;
988
+ if (timeSinceLastPurchase < this.config.cooldownMs) {
989
+ const remainingCooldown = this.config.cooldownMs - timeSinceLastPurchase;
990
+ return {
991
+ canBuy: false,
992
+ reason: `Please wait ${Math.ceil(remainingCooldown / 1e3)} seconds before purchasing again`
993
+ };
994
+ }
995
+ }
996
+ const cost = currentBet * this.config.costMultiplier;
997
+ return { canBuy: true, cost };
998
+ }
999
+ /**
1000
+ * Calculate the cost to buy bonus.
1001
+ */
1002
+ calculateCost(currentBet) {
1003
+ return currentBet * this.config.costMultiplier;
1004
+ }
1005
+ /**
1006
+ * Record a bonus purchase.
1007
+ */
1008
+ recordPurchase() {
1009
+ this.purchasesThisSession++;
1010
+ this.lastPurchaseTime = Date.now();
1011
+ }
1012
+ /**
1013
+ * Get the number of purchases made this session.
1014
+ */
1015
+ getPurchasesThisSession() {
1016
+ return this.purchasesThisSession;
1017
+ }
1018
+ /**
1019
+ * Reset session tracking.
1020
+ */
1021
+ resetSession() {
1022
+ this.purchasesThisSession = 0;
1023
+ this.lastPurchaseTime = 0;
1024
+ }
1025
+ /**
1026
+ * Get configuration.
1027
+ */
1028
+ getConfig() {
1029
+ return this.config;
1030
+ }
1031
+ /**
1032
+ * Create a spin response that triggers bonus (for buy bonus feature).
1033
+ * This marks the spin as having triggered the bonus.
1034
+ */
1035
+ createBonusTriggerSpin(baseSpinResult, bonusData) {
1036
+ return {
1037
+ ...baseSpinResult,
1038
+ bonusTriggered: true,
1039
+ bonusData,
1040
+ freeSpinsRemaining: bonusData?.freeSpins || 0
1041
+ };
1042
+ }
1043
+ };
1044
+
1045
+ // src/CascadingReelsManager.ts
1046
+ var CascadingReelsManager = class {
1047
+ config;
1048
+ constructor(config) {
1049
+ this.config = config;
1050
+ }
1051
+ /**
1052
+ * Execute a full cascade sequence until no more wins occur
1053
+ */
1054
+ executeCascade(gameState, initialWins) {
1055
+ const cascadeResults = [];
1056
+ let currentGameState = { ...gameState };
1057
+ let remainingWins = [...initialWins];
1058
+ let totalMultiplier = this.config.initialMultiplier || 1;
1059
+ let cascadeCount = 0;
1060
+ const maxCascades = this.config.maxCascades || Infinity;
1061
+ while (remainingWins.length > 0 && cascadeCount < maxCascades) {
1062
+ cascadeCount++;
1063
+ const removedPositions = this.removeWinningSymbols(
1064
+ currentGameState.grid,
1065
+ remainingWins
1066
+ );
1067
+ const dropResult = this.applyGravity(currentGameState.grid, removedPositions);
1068
+ const newSymbols = this.fillEmptyPositions(dropResult.grid);
1069
+ if (this.config.multiplierIncrement) {
1070
+ totalMultiplier += this.config.multiplierIncrement;
1071
+ }
1072
+ const newWins = this.evaluateWins(newSymbols.grid);
1073
+ cascadeResults.push({
1074
+ cascadeNumber: cascadeCount,
1075
+ removedPositions,
1076
+ droppedSymbols: dropResult.droppedSymbols,
1077
+ newSymbols: newSymbols.addedSymbols,
1078
+ resultingGrid: newSymbols.grid,
1079
+ winsInStep: newWins,
1080
+ multiplier: totalMultiplier
1081
+ });
1082
+ currentGameState.grid = newSymbols.grid;
1083
+ remainingWins = newWins;
1084
+ if (this.config.stopOnNoWinIncrease && newWins.length === 0) {
1085
+ break;
1086
+ }
1087
+ }
1088
+ return {
1089
+ success: cascadeCount > 0,
1090
+ totalCascades: cascadeCount,
1091
+ steps: cascadeResults,
1092
+ finalGrid: currentGameState.grid,
1093
+ finalMultiplier: totalMultiplier,
1094
+ totalWinsFromCascades: cascadeResults.reduce(
1095
+ (sum, step) => sum + step.winsInStep.length,
1096
+ 0
1097
+ )
1098
+ };
1099
+ }
1100
+ /**
1101
+ * Remove symbols that are part of winning combinations
1102
+ */
1103
+ removeWinningSymbols(grid, wins) {
1104
+ const removedPositions = [];
1105
+ const positionsToRemove = /* @__PURE__ */ new Set();
1106
+ wins.forEach((win) => {
1107
+ win.positions.forEach((pos) => {
1108
+ const key = `${pos.reel}-${pos.row}`;
1109
+ if (!positionsToRemove.has(key)) {
1110
+ positionsToRemove.add(key);
1111
+ removedPositions.push({
1112
+ reel: pos.reel,
1113
+ row: pos.row,
1114
+ symbol: grid[pos.reel][pos.row]
1115
+ });
1116
+ }
1117
+ });
1118
+ });
1119
+ positionsToRemove.forEach((key) => {
1120
+ const [reelStr, rowStr] = key.split("-");
1121
+ const reel = parseInt(reelStr, 10);
1122
+ const row = parseInt(rowStr, 10);
1123
+ grid[reel][row] = {
1124
+ id: "EMPTY",
1125
+ value: 0,
1126
+ isWild: false,
1127
+ isScatter: false
1128
+ };
1129
+ });
1130
+ return removedPositions;
1131
+ }
1132
+ /**
1133
+ * Apply gravity to make symbols fall down into empty positions
1134
+ */
1135
+ applyGravity(grid, removedPositions) {
1136
+ const droppedSymbols = [];
1137
+ const numRows = grid.length > 0 ? grid[0].length : 0;
1138
+ for (let reel = 0; reel < grid.length; reel++) {
1139
+ const column = [];
1140
+ for (let row = numRows - 1; row >= 0; row--) {
1141
+ if (grid[reel][row].id !== "EMPTY") {
1142
+ column.push(grid[reel][row]);
1143
+ }
1144
+ }
1145
+ while (column.length < numRows) {
1146
+ column.push({
1147
+ id: "EMPTY",
1148
+ value: 0,
1149
+ isWild: false,
1150
+ isScatter: false
1151
+ });
1152
+ }
1153
+ column.reverse();
1154
+ for (let row = 0; row < numRows; row++) {
1155
+ const originalSymbol = grid[reel][row];
1156
+ const newSymbol = column[row];
1157
+ if (originalSymbol.id !== "EMPTY" && newSymbol.id !== "EMPTY" && originalSymbol.id !== newSymbol.id) {
1158
+ for (let origRow = row + 1; origRow < numRows; origRow++) {
1159
+ if (grid[reel][origRow].id === newSymbol.id) {
1160
+ droppedSymbols.push({
1161
+ symbol: newSymbol,
1162
+ fromRow: origRow,
1163
+ toRow: row,
1164
+ reel
1165
+ });
1166
+ break;
1167
+ }
1168
+ }
1169
+ }
1170
+ }
1171
+ grid[reel] = column;
1172
+ }
1173
+ return { grid, droppedSymbols };
1174
+ }
1175
+ /**
1176
+ * Fill empty positions at the top with new random symbols
1177
+ */
1178
+ fillEmptyPositions(grid) {
1179
+ const addedSymbols = [];
1180
+ const symbolPool = this.config.symbolPool || [];
1181
+ if (symbolPool.length === 0) {
1182
+ throw new Error("Symbol pool must be provided for cascade filling");
1183
+ }
1184
+ for (let reel = 0; reel < grid.length; reel++) {
1185
+ for (let row = 0; row < grid[reel].length; row++) {
1186
+ if (grid[reel][row].id === "EMPTY") {
1187
+ const newSymbol = this.generateWeightedSymbol(symbolPool);
1188
+ grid[reel][row] = newSymbol;
1189
+ addedSymbols.push({
1190
+ symbol: newSymbol,
1191
+ position: { reel, row }
1192
+ });
1193
+ }
1194
+ }
1195
+ }
1196
+ return { grid, addedSymbols };
1197
+ }
1198
+ /**
1199
+ * Generate a symbol based on weighted probabilities
1200
+ */
1201
+ generateWeightedSymbol(symbolPool) {
1202
+ const totalWeight = symbolPool.reduce((sum, sym) => sum + (sym.weight || 1), 0);
1203
+ let random = Math.random() * totalWeight;
1204
+ for (const symbol of symbolPool) {
1205
+ random -= symbol.weight || 1;
1206
+ if (random <= 0) {
1207
+ return { ...symbol };
1208
+ }
1209
+ }
1210
+ return { ...symbolPool[symbolPool.length - 1] };
1211
+ }
1212
+ /**
1213
+ * Evaluate wins on the current grid state
1214
+ * This is a simplified version - in production you'd integrate with your WinEvaluator
1215
+ */
1216
+ evaluateWins(grid) {
1217
+ return [];
1218
+ }
1219
+ /**
1220
+ * Check if a cascade sequence should trigger special features
1221
+ */
1222
+ checkCascadeTriggers(cascadeResult) {
1223
+ const triggers = [];
1224
+ if (cascadeResult.totalCascades >= (this.config.triggerOnCascadeCount || Infinity)) {
1225
+ triggers.push("CASCADE_COUNT_ACHIEVED");
1226
+ }
1227
+ if (cascadeResult.finalMultiplier >= (this.config.triggerOnMultiplier || Infinity)) {
1228
+ triggers.push("MULTIPLIER_THRESHOLD");
1229
+ }
1230
+ const consecutiveWins = cascadeResult.steps.filter((step) => step.winsInStep.length > 0).length;
1231
+ if (consecutiveWins >= (this.config.triggerOnConsecutiveWins || Infinity)) {
1232
+ triggers.push("CONSECUTIVE_WINS");
1233
+ }
1234
+ return { triggers };
1235
+ }
1236
+ };
1237
+
1238
+ // src/ExpandingWildsManager.ts
1239
+ var ExpandingWildsManager = class {
1240
+ expandConfig;
1241
+ stickyConfig;
1242
+ constructor(expandConfig, stickyConfig) {
1243
+ this.expandConfig = expandConfig;
1244
+ this.stickyConfig = stickyConfig || {
1245
+ enabled: false,
1246
+ persistSpins: 0,
1247
+ maxStickyWilds: Infinity
1248
+ };
1249
+ }
1250
+ /**
1251
+ * Process expanding wilds on the current grid
1252
+ */
1253
+ processExpandingWilds(gameState) {
1254
+ const expandedReels = [];
1255
+ const expandedPositions = [];
1256
+ let modifiedGrid = gameState.grid.map((reel) => [...reel]);
1257
+ for (let reel = 0; reel < modifiedGrid.length; reel++) {
1258
+ const shouldExpand = this.shouldReelExpand(modifiedGrid[reel], reel);
1259
+ if (shouldExpand) {
1260
+ expandedReels.push(reel);
1261
+ const rowsToExpand = this.expandConfig.expandToRows || modifiedGrid[reel].length;
1262
+ for (let row = 0; row < rowsToExpand; row++) {
1263
+ const originalSymbol = modifiedGrid[reel][row];
1264
+ if (!originalSymbol.isWild) {
1265
+ expandedPositions.push({ reel, row });
1266
+ modifiedGrid[reel][row] = {
1267
+ ...originalSymbol,
1268
+ isWild: true,
1269
+ wildMultiplier: this.expandConfig.wildMultiplier || 1,
1270
+ expandedFrom: originalSymbol.id
1271
+ };
1272
+ }
1273
+ }
1274
+ }
1275
+ }
1276
+ return {
1277
+ success: expandedReels.length > 0,
1278
+ expandedReels,
1279
+ expandedPositions,
1280
+ modifiedGrid,
1281
+ totalExpanded: expandedPositions.length,
1282
+ appliedMultiplier: this.expandConfig.wildMultiplier || 1
1283
+ };
1284
+ }
1285
+ /**
1286
+ * Determine if a reel should expand based on configuration
1287
+ */
1288
+ shouldReelExpand(reel, reelIndex) {
1289
+ const wildCount = reel.filter((sym) => sym.isWild).length;
1290
+ if (wildCount === 0) {
1291
+ return false;
1292
+ }
1293
+ switch (this.expandConfig.triggerCondition) {
1294
+ case "any_wild":
1295
+ return true;
1296
+ case "multiple_wilds":
1297
+ return wildCount >= (this.expandConfig.minWildsForExpand || 2);
1298
+ case "specific_positions":
1299
+ return this.hasWildInSpecificPositions(reel);
1300
+ case "random":
1301
+ const probability = this.expandConfig.expansionProbability || 0.5;
1302
+ return Math.random() < probability;
1303
+ default:
1304
+ return wildCount > 0;
1305
+ }
1306
+ }
1307
+ /**
1308
+ * Check if wilds exist in specific configured positions
1309
+ */
1310
+ hasWildInSpecificPositions(reel) {
1311
+ const positions = this.expandConfig.specificPositions || [];
1312
+ return positions.some((pos) => reel[pos] && reel[pos].isWild);
1313
+ }
1314
+ /**
1315
+ * Apply sticky wilds logic for persistent wilds across spins
1316
+ */
1317
+ processStickyWilds(gameState, existingStickyWilds) {
1318
+ const updatedStickyWilds = [];
1319
+ const removedStickyWilds = [];
1320
+ let modifiedGrid = gameState.grid.map((reel) => [...reel]);
1321
+ existingStickyWilds.forEach((sticky) => {
1322
+ const remainingSpins = sticky.remainingSpins - 1;
1323
+ if (remainingSpins <= 0) {
1324
+ removedStickyWilds.push({ reel: sticky.reel, row: sticky.row });
1325
+ modifiedGrid[sticky.reel][sticky.row] = {
1326
+ ...modifiedGrid[sticky.reel][sticky.row],
1327
+ isSticky: false,
1328
+ isWild: false
1329
+ };
1330
+ } else {
1331
+ updatedStickyWilds.push({
1332
+ reel: sticky.reel,
1333
+ row: sticky.row,
1334
+ remainingSpins
1335
+ });
1336
+ }
1337
+ });
1338
+ const currentStickyCount = modifiedGrid.reduce(
1339
+ (sum, reel) => sum + reel.filter((sym) => sym.isSticky).length,
1340
+ 0
1341
+ );
1342
+ return {
1343
+ success: updatedStickyWilds.length > 0 || removedStickyWilds.length > 0,
1344
+ activeStickyWilds: updatedStickyWilds,
1345
+ removedStickyWilds,
1346
+ modifiedGrid,
1347
+ totalActive: updatedStickyWilds.length,
1348
+ totalRemoved: removedStickyWilds.length,
1349
+ canAddMore: currentStickyCount < (this.stickyConfig.maxStickyWilds || Infinity)
1350
+ };
1351
+ }
1352
+ /**
1353
+ * Add new sticky wilds to the grid
1354
+ */
1355
+ addStickyWilds(gameState, positions, persistSpins) {
1356
+ const newStickyWilds = [];
1357
+ let modifiedGrid = gameState.grid.map((reel) => [...reel]);
1358
+ const spinsToPersist = persistSpins || this.stickyConfig.persistSpins || 3;
1359
+ positions.forEach((pos) => {
1360
+ if (pos.reel >= 0 && pos.reel < modifiedGrid.length && pos.row >= 0 && pos.row < modifiedGrid[pos.reel].length) {
1361
+ const currentSymbol = modifiedGrid[pos.reel][pos.row];
1362
+ modifiedGrid[pos.reel][pos.row] = {
1363
+ ...currentSymbol,
1364
+ isWild: true,
1365
+ isSticky: true,
1366
+ remainingSpins: spinsToPersist,
1367
+ stickyId: `sticky_${pos.reel}_${pos.row}_${Date.now()}`
1368
+ };
1369
+ newStickyWilds.push({
1370
+ reel: pos.reel,
1371
+ row: pos.row,
1372
+ remainingSpins: spinsToPersist
1373
+ });
1374
+ }
1375
+ });
1376
+ return {
1377
+ success: newStickyWilds.length > 0,
1378
+ activeStickyWilds: newStickyWilds,
1379
+ removedStickyWilds: [],
1380
+ modifiedGrid,
1381
+ totalActive: newStickyWilds.length,
1382
+ totalRemoved: 0,
1383
+ canAddMore: newStickyWilds.length < (this.stickyConfig.maxStickyWilds || Infinity)
1384
+ };
1385
+ }
1386
+ /**
1387
+ * Calculate multiplier effect from wilds in a win line
1388
+ */
1389
+ calculateWildMultiplier(winPositions, grid) {
1390
+ let totalMultiplier = 1;
1391
+ winPositions.forEach((pos) => {
1392
+ const symbol = grid[pos.reel][pos.row];
1393
+ if (symbol.isWild) {
1394
+ const wildMult = symbol.wildMultiplier || this.expandConfig.wildMultiplier || 1;
1395
+ if (this.expandConfig.multiplierStacking === "multiply") {
1396
+ totalMultiplier *= wildMult;
1397
+ } else if (this.expandConfig.multiplierStacking === "add") {
1398
+ totalMultiplier += wildMult - 1;
1399
+ } else {
1400
+ totalMultiplier = Math.max(totalMultiplier, wildMult);
1401
+ }
1402
+ }
1403
+ });
1404
+ return totalMultiplier;
1405
+ }
1406
+ /**
1407
+ * Check for special wild interactions (e.g., expanding wild meeting sticky wild)
1408
+ */
1409
+ checkWildInteractions(grid) {
1410
+ const interactions = [];
1411
+ const expandedReels = [];
1412
+ const stickyPositions = [];
1413
+ grid.forEach((reel, reelIndex) => {
1414
+ if (reel.some((sym) => sym.expandedFrom)) {
1415
+ expandedReels.push(reelIndex);
1416
+ }
1417
+ });
1418
+ grid.forEach((reel, reelIndex) => {
1419
+ reel.forEach((sym, rowIndex) => {
1420
+ if (sym.isSticky) {
1421
+ stickyPositions.push({ reel: reelIndex, row: rowIndex });
1422
+ }
1423
+ });
1424
+ });
1425
+ if (expandedReels.length > 0 && stickyPositions.length > 0) {
1426
+ const overlappingReels = expandedReels.filter(
1427
+ (reel) => stickyPositions.some((pos) => pos.reel === reel)
1428
+ );
1429
+ if (overlappingReels.length > 0) {
1430
+ interactions.push("EXPANDING_STICKY_OVERLAP");
1431
+ }
1432
+ }
1433
+ const fullScreenWild = grid.every((reel) => reel.every((sym) => sym.isWild));
1434
+ if (fullScreenWild) {
1435
+ interactions.push("FULL_SCREEN_WILDS");
1436
+ }
1437
+ return { interactions };
1438
+ }
1439
+ };
1440
+
1441
+ // src/ScatterEnhancementsManager.ts
1442
+ var ScatterEnhancementsManager = class {
1443
+ config;
1444
+ constructor(config) {
1445
+ this.config = config;
1446
+ }
1447
+ /**
1448
+ * Evaluate scatter symbols on the current grid
1449
+ */
1450
+ evaluateScatters(gameState) {
1451
+ const scatterPositions = [];
1452
+ let totalScatterCount = 0;
1453
+ let totalScatterValue = 0;
1454
+ gameState.grid.forEach((reel, reelIndex) => {
1455
+ reel.forEach((symbol, rowIndex) => {
1456
+ if (symbol.isScatter) {
1457
+ scatterPositions.push({
1458
+ reel: reelIndex,
1459
+ row: rowIndex,
1460
+ symbol
1461
+ });
1462
+ totalScatterCount++;
1463
+ totalScatterValue += symbol.value || 0;
1464
+ }
1465
+ });
1466
+ });
1467
+ const triggers = this.checkTriggerConditions(totalScatterCount, scatterPositions);
1468
+ const scatterPays = this.calculateScatterPays(totalScatterCount, totalScatterValue);
1469
+ return {
1470
+ success: scatterPositions.length > 0,
1471
+ scatterCount: totalScatterCount,
1472
+ scatterValue: totalScatterValue,
1473
+ positions: scatterPositions,
1474
+ triggers,
1475
+ scatterPays,
1476
+ collectedScatters: 0
1477
+ // Updated by collection system
1478
+ };
1479
+ }
1480
+ /**
1481
+ * Check if scatter conditions trigger bonus features
1482
+ */
1483
+ checkTriggerConditions(scatterCount, positions) {
1484
+ const triggers = [];
1485
+ if (scatterCount >= (this.config.minScattersForTrigger || 3)) {
1486
+ triggers.push({
1487
+ type: "BONUS_TRIGGER",
1488
+ triggered: true,
1489
+ scatterCount,
1490
+ featureType: this.config.triggeredFeature || "FREE_SPINS",
1491
+ awardedSpins: this.calculateAwardedSpins(scatterCount)
1492
+ });
1493
+ }
1494
+ if (this.config.requireSpecificReels) {
1495
+ const reelSet = new Set(positions.map((p) => p.reel));
1496
+ const requiredReels = this.config.requiredReels || [];
1497
+ const hasAllRequired = requiredReels.every((reel) => reelSet.has(reel));
1498
+ if (hasAllRequired) {
1499
+ triggers.push({
1500
+ type: "REEL_COMBINATION_TRIGGER",
1501
+ triggered: true,
1502
+ scatterCount,
1503
+ featureType: "ENHANCED_BONUS",
1504
+ awardedSpins: this.calculateAwardedSpins(scatterCount) * 2
1505
+ });
1506
+ }
1507
+ }
1508
+ if (this.config.enableClusterTriggers) {
1509
+ const clusters = this.findScatterClusters(positions);
1510
+ clusters.forEach((cluster) => {
1511
+ if (cluster.size >= (this.config.clusterSizeForTrigger || 4)) {
1512
+ triggers.push({
1513
+ type: "CLUSTER_TRIGGER",
1514
+ triggered: true,
1515
+ scatterCount: cluster.size,
1516
+ featureType: "CLUSTER_BONUS",
1517
+ clusterPositions: cluster.positions
1518
+ });
1519
+ }
1520
+ });
1521
+ }
1522
+ if (this.config.enableProgressiveTrigger) {
1523
+ }
1524
+ return triggers;
1525
+ }
1526
+ /**
1527
+ * Calculate number of spins awarded based on scatter count
1528
+ */
1529
+ calculateAwardedSpins(scatterCount) {
1530
+ const spinTable = this.config.spinAwardTable || {
1531
+ 3: 10,
1532
+ 4: 15,
1533
+ 5: 20,
1534
+ 6: 25
1535
+ };
1536
+ const matchingKey = Object.keys(spinTable).map(Number).filter((key) => scatterCount >= key).sort((a, b) => b - a)[0];
1537
+ return matchingKey ? spinTable[matchingKey] : 0;
1538
+ }
1539
+ /**
1540
+ * Calculate immediate scatter payouts
1541
+ */
1542
+ calculateScatterPays(count, totalValue) {
1543
+ if (!this.config.enableScatterPays) {
1544
+ return 0;
1545
+ }
1546
+ const payTable = this.config.scatterPayTable || {};
1547
+ const basePay = payTable[count] || 0;
1548
+ const multiplier = this.config.scatterMultiplier || 1;
1549
+ return (basePay + totalValue) * multiplier;
1550
+ }
1551
+ /**
1552
+ * Find clusters of adjacent scatter symbols
1553
+ */
1554
+ findScatterClusters(positions) {
1555
+ const clusters = [];
1556
+ const visited = /* @__PURE__ */ new Set();
1557
+ positions.forEach((pos) => {
1558
+ const key = `${pos.reel}-${pos.row}`;
1559
+ if (!visited.has(key)) {
1560
+ const cluster = this.floodFillCluster(pos, positions, visited);
1561
+ if (cluster.length > 1) {
1562
+ clusters.push({
1563
+ size: cluster.length,
1564
+ positions: cluster
1565
+ });
1566
+ }
1567
+ }
1568
+ });
1569
+ return clusters;
1570
+ }
1571
+ /**
1572
+ * Flood fill algorithm to find connected scatter positions
1573
+ */
1574
+ floodFillCluster(start, allPositions, visited) {
1575
+ const cluster = [];
1576
+ const queue = [start];
1577
+ const positionSet = new Set(allPositions.map((p) => `${p.reel}-${p.row}`));
1578
+ while (queue.length > 0) {
1579
+ const current = queue.shift();
1580
+ const key = `${current.reel}-${current.row}`;
1581
+ if (visited.has(key)) {
1582
+ continue;
1583
+ }
1584
+ visited.add(key);
1585
+ cluster.push(current);
1586
+ const adjacentOffsets = [
1587
+ [-1, -1],
1588
+ [-1, 0],
1589
+ [-1, 1],
1590
+ [0, -1],
1591
+ [0, 1],
1592
+ [1, -1],
1593
+ [1, 0],
1594
+ [1, 1]
1595
+ ];
1596
+ adjacentOffsets.forEach(([dReel, dRow]) => {
1597
+ const neighbor = {
1598
+ reel: current.reel + dReel,
1599
+ row: current.row + dRow
1600
+ };
1601
+ const neighborKey = `${neighbor.reel}-${neighbor.row}`;
1602
+ if (positionSet.has(neighborKey) && !visited.has(neighborKey)) {
1603
+ queue.push(neighbor);
1604
+ }
1605
+ });
1606
+ }
1607
+ return cluster;
1608
+ }
1609
+ /**
1610
+ * Manage scatter collection system (collect scatters over multiple spins)
1611
+ */
1612
+ manageScatterCollection(currentScatters, collectedData) {
1613
+ const updated = { ...collectedData };
1614
+ updated.count += currentScatters.scatterCount;
1615
+ currentScatters.positions.forEach((pos) => {
1616
+ updated.values.push(pos.symbol.value || 0);
1617
+ });
1618
+ let collectionTrigger;
1619
+ const targetCount = this.config.collectionTarget || 10;
1620
+ if (updated.count >= targetCount) {
1621
+ collectionTrigger = {
1622
+ type: "COLLECTION_COMPLETE",
1623
+ triggered: true,
1624
+ scatterCount: updated.count,
1625
+ featureType: "COLLECTION_BONUS",
1626
+ awardedSpins: this.config.collectionBonusSpins || 10,
1627
+ collectedValue: updated.values.reduce((sum, val) => sum + val, 0)
1628
+ };
1629
+ updated.count = 0;
1630
+ updated.values = [];
1631
+ }
1632
+ updated.spinsRemaining--;
1633
+ if (updated.spinsRemaining <= 0 && updated.count > 0 && this.config.autoTriggerOnExpiry) {
1634
+ collectionTrigger = {
1635
+ type: "COLLECTION_EXPIRY_TRIGGER",
1636
+ triggered: true,
1637
+ scatterCount: updated.count,
1638
+ featureType: "MINI_BONUS",
1639
+ awardedSpins: Math.floor(updated.count / 2)
1640
+ };
1641
+ updated.count = 0;
1642
+ updated.values = [];
1643
+ }
1644
+ return { updated, collectionTrigger };
1645
+ }
1646
+ /**
1647
+ * Apply scatter transformation mechanics (e.g., scatters becoming wilds)
1648
+ */
1649
+ transformScatters(gameState, scatterPositions) {
1650
+ const transformed = [];
1651
+ const modifiedGrid = gameState.grid.map((reel) => [...reel]);
1652
+ if (!this.config.enableTransformation) {
1653
+ return { modifiedGrid, transformed };
1654
+ }
1655
+ scatterPositions.forEach((pos) => {
1656
+ if (pos.reel >= 0 && pos.reel < modifiedGrid.length && pos.row >= 0 && pos.row < modifiedGrid[pos.reel].length) {
1657
+ const transformType = this.config.transformationType || "to_wild";
1658
+ if (transformType === "to_wild") {
1659
+ modifiedGrid[pos.reel][pos.row] = {
1660
+ ...modifiedGrid[pos.reel][pos.row],
1661
+ isWild: true,
1662
+ isScatter: false,
1663
+ wildMultiplier: this.config.transformedWildMultiplier || 1
1664
+ };
1665
+ transformed.push(pos);
1666
+ } else if (transformType === "to_multiplier") {
1667
+ modifiedGrid[pos.reel][pos.row] = {
1668
+ ...modifiedGrid[pos.reel][pos.row],
1669
+ isScatter: false,
1670
+ value: (modifiedGrid[pos.reel][pos.row].value || 1) * 2
1671
+ };
1672
+ transformed.push(pos);
1673
+ }
1674
+ }
1675
+ });
1676
+ return { modifiedGrid, transformed };
1677
+ }
1678
+ /**
1679
+ * Check for scatter in multiplier zones (special grid areas)
1680
+ */
1681
+ checkMultiplierZones(scatterPositions) {
1682
+ const zoneHits = [];
1683
+ let totalMultiplier = 1;
1684
+ const zones = this.config.multiplierZones || [];
1685
+ scatterPositions.forEach((pos) => {
1686
+ zones.forEach((zone) => {
1687
+ if (pos.reel >= zone.reelStart && pos.reel <= zone.reelEnd && pos.row >= zone.rowStart && pos.row <= zone.rowEnd) {
1688
+ zoneHits.push({
1689
+ reel: pos.reel,
1690
+ row: pos.row,
1691
+ multiplier: zone.multiplier
1692
+ });
1693
+ if (this.config.zoneMultiplierStacking === "multiply") {
1694
+ totalMultiplier *= zone.multiplier;
1695
+ } else {
1696
+ totalMultiplier += zone.multiplier - 1;
1697
+ }
1698
+ }
1699
+ });
1700
+ });
1701
+ return { totalMultiplier, zoneHits };
1702
+ }
1703
+ /**
1704
+ * Implement progressive scatter trigger (accumulates across game session)
1705
+ */
1706
+ updateProgressiveScatter(currentCount, progressiveState) {
1707
+ const updated = { ...progressiveState };
1708
+ updated.accumulated += currentCount;
1709
+ let triggered = false;
1710
+ let reward;
1711
+ if (updated.accumulated >= updated.threshold) {
1712
+ triggered = true;
1713
+ reward = {
1714
+ type: "PROGRESSIVE_SCATTER_BONUS",
1715
+ level: updated.level,
1716
+ accumulatedCount: updated.accumulated,
1717
+ awardedSpins: this.config.progressiveBonusSpins || updated.level * 10,
1718
+ multiplier: this.config.progressiveMultiplier || updated.level
1719
+ };
1720
+ if (this.config.progressiveResetOnTrigger) {
1721
+ updated.accumulated = 0;
1722
+ updated.level = 1;
1723
+ updated.threshold = this.config.baseProgressiveThreshold || 20;
1724
+ } else {
1725
+ updated.level++;
1726
+ updated.threshold = Math.floor(updated.threshold * 1.5);
1727
+ }
1728
+ }
1729
+ return { updated, triggered, reward };
1730
+ }
1731
+ };
1732
+
1733
+ // src/RespinsManager.ts
1734
+ var RespinsManager = class {
1735
+ config;
1736
+ constructor(config) {
1737
+ this.config = config;
1738
+ }
1739
+ /**
1740
+ * Check if respin feature should be triggered
1741
+ */
1742
+ checkTrigger(gameState) {
1743
+ const { triggerType, minTriggerSymbols } = this.config;
1744
+ if (triggerType === "symbol_count") {
1745
+ const specialSymbols = this.countSpecialSymbols(gameState);
1746
+ return specialSymbols >= minTriggerSymbols;
1747
+ }
1748
+ if (triggerType === "specific_positions") {
1749
+ return this.checkSpecificPositions(gameState);
1750
+ }
1751
+ return false;
1752
+ }
1753
+ /**
1754
+ * Count special symbols (money, reset, jackpot) on the grid
1755
+ */
1756
+ countSpecialSymbols(gameState) {
1757
+ let count = 0;
1758
+ const rows = gameState.grid.length;
1759
+ const cols = rows > 0 ? gameState.grid[0].length : 0;
1760
+ for (let row = 0; row < rows; row++) {
1761
+ for (let col = 0; col < cols; col++) {
1762
+ const symbol = gameState.grid[row][col];
1763
+ if (this.isSpecialSymbol(symbol)) {
1764
+ count++;
1765
+ }
1766
+ }
1767
+ }
1768
+ return count;
1769
+ }
1770
+ /**
1771
+ * Check if special symbols are in required positions
1772
+ */
1773
+ checkSpecificPositions(gameState) {
1774
+ const { triggerPositions } = this.config;
1775
+ if (!triggerPositions) return false;
1776
+ for (const pos of triggerPositions) {
1777
+ const { row, col, symbolId } = pos;
1778
+ if (row >= gameState.grid.length || col >= gameState.grid[0].length) {
1779
+ continue;
1780
+ }
1781
+ const symbol = gameState.grid[row][col];
1782
+ if (symbol.id === symbolId || this.isSpecialSymbol(symbol)) {
1783
+ continue;
1784
+ }
1785
+ return false;
1786
+ }
1787
+ return true;
1788
+ }
1789
+ /**
1790
+ * Check if a symbol is a special respin symbol
1791
+ */
1792
+ isSpecialSymbol(symbol) {
1793
+ const moneySymbol = symbol;
1794
+ return moneySymbol.isMoneySymbol === true || moneySymbol.isResetSymbol === true || moneySymbol.isJackpotSymbol === true;
1795
+ }
1796
+ /**
1797
+ * Execute the respins feature
1798
+ */
1799
+ execute(gameState) {
1800
+ const steps = [];
1801
+ let remainingRespins = this.config.initialRespins || 3;
1802
+ const lockedPositions = [];
1803
+ let totalValue = 0;
1804
+ let jackpotsCollected = [];
1805
+ const rows = gameState.grid.length;
1806
+ const cols = rows > 0 ? gameState.grid[0].length : 0;
1807
+ for (let row = 0; row < rows; row++) {
1808
+ for (let col = 0; col < cols; col++) {
1809
+ const symbol = gameState.grid[row][col];
1810
+ if (this.isSpecialSymbol(symbol)) {
1811
+ lockedPositions.push({ row, col });
1812
+ const moneySymbol = symbol;
1813
+ if (moneySymbol.value) {
1814
+ totalValue += moneySymbol.value;
1815
+ }
1816
+ if (moneySymbol.jackpotType) {
1817
+ jackpotsCollected.push(moneySymbol.jackpotType);
1818
+ }
1819
+ }
1820
+ }
1821
+ }
1822
+ steps.push({
1823
+ respinsRemaining: remainingRespins,
1824
+ lockedPositions: [...lockedPositions],
1825
+ newSymbols: [],
1826
+ valuesAdded: totalValue,
1827
+ jackpotsAwarded: [...jackpotsCollected],
1828
+ reason: "initial_trigger"
1829
+ });
1830
+ while (remainingRespins > 0 && lockedPositions.length < rows * cols) {
1831
+ remainingRespins--;
1832
+ const newSymbols = [];
1833
+ let stepValueAdded = 0;
1834
+ let stepJackpots = [];
1835
+ for (let row = 0; row < rows; row++) {
1836
+ for (let col = 0; col < cols; col++) {
1837
+ const isLocked = lockedPositions.some(
1838
+ (pos) => pos.row === row && pos.col === col
1839
+ );
1840
+ if (!isLocked) {
1841
+ const newSymbol = this.generateRespinsSymbol();
1842
+ newSymbols.push({ row, col, symbol: newSymbol });
1843
+ if (this.isSpecialSymbol(newSymbol)) {
1844
+ lockedPositions.push({ row, col });
1845
+ const moneySymbol = newSymbol;
1846
+ if (moneySymbol.value) {
1847
+ stepValueAdded += moneySymbol.value;
1848
+ totalValue += moneySymbol.value;
1849
+ }
1850
+ if (moneySymbol.jackpotType) {
1851
+ stepJackpots.push(moneySymbol.jackpotType);
1852
+ jackpotsCollected.push(moneySymbol.jackpotType);
1853
+ }
1854
+ if (moneySymbol.isResetSymbol === true) {
1855
+ remainingRespins = this.config.initialRespins || 3;
1856
+ }
1857
+ }
1858
+ }
1859
+ }
1860
+ }
1861
+ steps.push({
1862
+ respinsRemaining: remainingRespins,
1863
+ lockedPositions: [...lockedPositions],
1864
+ newSymbols: newSymbols.map((ns) => ({
1865
+ row: ns.row,
1866
+ col: ns.col,
1867
+ symbolId: ns.symbol.id
1868
+ })),
1869
+ valuesAdded: stepValueAdded,
1870
+ jackpotsAwarded: stepJackpots,
1871
+ reason: stepJackpots.length > 0 ? "jackpot_collected" : newSymbols.length > 0 ? "respins" : "no_new_symbols"
1872
+ });
1873
+ if (newSymbols.every((ns) => !this.isSpecialSymbol(ns.symbol))) {
1874
+ if (this.config.endOnNoSpecial) {
1875
+ break;
1876
+ }
1877
+ }
1878
+ if (lockedPositions.length === rows * cols && this.config.fullScreenBonus) {
1879
+ totalValue *= this.config.fullScreenBonus.multiplier;
1880
+ steps[steps.length - 1].reason = "full_screen_bonus";
1881
+ steps[steps.length - 1].valuesAdded = totalValue;
1882
+ break;
1883
+ }
1884
+ }
1885
+ return {
1886
+ triggered: true,
1887
+ steps,
1888
+ finalValue: totalValue,
1889
+ jackpotsCollected,
1890
+ lockedPositions,
1891
+ totalRespinsUsed: (this.config.initialRespins || 3) - remainingRespins
1892
+ };
1893
+ }
1894
+ /**
1895
+ * Generate a special symbol for respins (weighted random)
1896
+ * In production, this should use a certified RNG with configurable weights
1897
+ */
1898
+ generateRespinsSymbol() {
1899
+ const { symbolWeights } = this.config;
1900
+ const rand = Math.random();
1901
+ let cumulative = 0;
1902
+ if (symbolWeights) {
1903
+ for (const weight of symbolWeights) {
1904
+ cumulative += weight.probability;
1905
+ if (rand <= cumulative) {
1906
+ const symbol = {
1907
+ id: weight.symbolId,
1908
+ value: weight.baseValue || 0,
1909
+ isWild: false,
1910
+ isScatter: false,
1911
+ isMoneySymbol: weight.isMoneySymbol,
1912
+ isResetSymbol: weight.isResetSymbol,
1913
+ isJackpotSymbol: weight.isJackpotSymbol,
1914
+ jackpotType: weight.jackpotType
1915
+ };
1916
+ return symbol;
1917
+ }
1918
+ }
1919
+ }
1920
+ return {
1921
+ id: "regular",
1922
+ value: 0,
1923
+ isWild: false,
1924
+ isScatter: false
1925
+ };
1926
+ }
1927
+ /**
1928
+ * Calculate potential payout for respins feature
1929
+ */
1930
+ calculatePotentialPayout(betAmount) {
1931
+ const { initialRespins, symbolWeights, fullScreenBonus } = this.config;
1932
+ if (!symbolWeights) return 0;
1933
+ let expectedValue = 0;
1934
+ const totalProbability = symbolWeights.reduce((sum, w) => sum + w.probability, 0);
1935
+ for (const weight of symbolWeights) {
1936
+ const symbolValue = weight.baseValue || 0;
1937
+ const probability = weight.probability / totalProbability;
1938
+ expectedValue += symbolValue * probability;
1939
+ }
1940
+ let expectedTotal = expectedValue * (initialRespins || 3);
1941
+ if (fullScreenBonus) {
1942
+ expectedTotal *= 1 + (fullScreenBonus.multiplier - 1) * 0.01;
1943
+ }
1944
+ return expectedTotal * betAmount;
1945
+ }
1946
+ /**
1947
+ * Get configuration for client-side preview
1948
+ */
1949
+ getConfig() {
1950
+ return { ...this.config };
1951
+ }
1952
+ };
1953
+
1954
+ // src/WalletManager.ts
1955
+ var InMemoryWalletStorage = class {
1956
+ balances = /* @__PURE__ */ new Map();
1957
+ transactions = /* @__PURE__ */ new Map();
1958
+ async getBalance(userId) {
1959
+ return this.balances.get(userId) || null;
1960
+ }
1961
+ async updateBalance(userId, amount, version) {
1962
+ const current = this.balances.get(userId);
1963
+ if (current && current.version !== version) {
1964
+ return null;
1965
+ }
1966
+ const newBalance = current ? parseFloat(current.amount) + parseFloat(amount) : parseFloat(amount);
1967
+ const updatedBalance = {
1968
+ userId,
1969
+ amount: newBalance.toFixed(4),
1970
+ // Adjust precision as needed
1971
+ version: (current?.version || 0) + 1,
1972
+ updatedAt: /* @__PURE__ */ new Date()
1973
+ };
1974
+ this.balances.set(userId, updatedBalance);
1975
+ return updatedBalance;
1976
+ }
1977
+ async recordTransaction(transaction) {
1978
+ const userTxns = this.transactions.get(transaction.userId) || [];
1979
+ userTxns.push(transaction);
1980
+ this.transactions.set(transaction.userId, userTxns);
1981
+ }
1982
+ async getTransactionHistory(userId, limit = 50) {
1983
+ const userTxns = this.transactions.get(userId) || [];
1984
+ return userTxns.slice(-limit);
1985
+ }
1986
+ };
1987
+ var WalletManager = class {
1988
+ storage;
1989
+ config;
1990
+ constructor(storage, config) {
1991
+ this.storage = storage;
1992
+ this.config = config;
1993
+ }
1994
+ /**
1995
+ * Place a bet (deduct funds)
1996
+ */
1997
+ async placeBet(userId, amount, gameId, roundId) {
1998
+ return this.executeTransaction(userId, -amount, "bet", { gameId, roundId });
1999
+ }
2000
+ /**
2001
+ * Credit winnings
2002
+ */
2003
+ async creditWin(userId, amount, gameId, roundId, winType = "normal") {
2004
+ let type = "payout";
2005
+ if (winType === "bonus") type = "bonus_payout";
2006
+ if (winType === "jackpot") type = "jackpot_win";
2007
+ return this.executeTransaction(userId, amount, type, { gameId, roundId });
2008
+ }
2009
+ /**
2010
+ * Core transaction logic with optimistic locking
2011
+ */
2012
+ async executeTransaction(userId, amount, type, metadata) {
2013
+ const maxRetries = 3;
2014
+ let attempt = 0;
2015
+ while (attempt < maxRetries) {
2016
+ try {
2017
+ const currentBalance = await this.storage.getBalance(userId);
2018
+ const currentAmount = currentBalance ? parseFloat(currentBalance.amount) : 0;
2019
+ const version = currentBalance?.version || 0;
2020
+ if (amount < 0 && currentAmount + amount < 0) {
2021
+ return { success: false, error: "Insufficient funds", code: "INSUFFICIENT_FUNDS" };
2022
+ }
2023
+ const newAmount = currentAmount + amount;
2024
+ const updatedBalance = await this.storage.updateBalance(userId, amount.toString(), version);
2025
+ if (!updatedBalance) {
2026
+ attempt++;
2027
+ continue;
2028
+ }
2029
+ const transaction = {
2030
+ id: crypto.randomUUID(),
2031
+ userId,
2032
+ type,
2033
+ amount: amount.toString(),
2034
+ balanceBefore: currentAmount.toString(),
2035
+ balanceAfter: newAmount.toString(),
2036
+ referenceId: metadata.roundId,
2037
+ metadata: { gameId: metadata.gameId },
2038
+ createdAt: /* @__PURE__ */ new Date()
2039
+ };
2040
+ await this.storage.recordTransaction(transaction);
2041
+ return {
2042
+ success: true,
2043
+ balance: updatedBalance.amount,
2044
+ transactionId: transaction.id
2045
+ };
2046
+ } catch (error) {
2047
+ return { success: false, error: error.message, code: "INTERNAL_ERROR" };
2048
+ }
2049
+ }
2050
+ return { success: false, error: "Transaction failed after retries", code: "CONFLICT" };
2051
+ }
2052
+ /**
2053
+ * Transfer funds between users (e.g., P2P features)
2054
+ */
2055
+ async transfer(fromId, toId, amount) {
2056
+ const debit = await this.placeBet(fromId, amount, "transfer", `tx-${Date.now()}`);
2057
+ if (!debit.success) return debit;
2058
+ const credit = await this.creditWin(toId, amount, "transfer", `tx-${Date.now()}`, "normal");
2059
+ if (!credit.success) {
2060
+ return { success: false, error: "Transfer partial failure", code: "ROLLBACK_REQUIRED" };
2061
+ }
2062
+ return { success: true, message: "Transfer complete" };
2063
+ }
2064
+ };
2065
+
2066
+ // src/AuditManager.ts
2067
+ var InMemoryAuditStorage = class {
2068
+ records = /* @__PURE__ */ new Map();
2069
+ playerIndex = /* @__PURE__ */ new Map();
2070
+ async save(record) {
2071
+ this.records.set(record.spinId, record);
2072
+ if (!this.playerIndex.has(record.playerId)) {
2073
+ this.playerIndex.set(record.playerId, /* @__PURE__ */ new Set());
2074
+ }
2075
+ this.playerIndex.get(record.playerId).add(record.spinId);
2076
+ }
2077
+ async findByPlayer(playerId, startTime, endTime) {
2078
+ const spinIds = this.playerIndex.get(playerId) || /* @__PURE__ */ new Set();
2079
+ const results = [];
2080
+ for (const spinId of spinIds) {
2081
+ const record = this.records.get(spinId);
2082
+ if (record && record.timestamp >= startTime && record.timestamp <= endTime) {
2083
+ results.push(record);
2084
+ }
2085
+ }
2086
+ return results.sort((a, b) => a.timestamp - b.timestamp);
2087
+ }
2088
+ async findById(spinId) {
2089
+ return this.records.get(spinId) || null;
2090
+ }
2091
+ async getSummary(playerId, startTime, endTime) {
2092
+ const records = await this.findByPlayer(playerId, startTime, endTime);
2093
+ const summary = {
2094
+ playerId,
2095
+ periodStart: startTime,
2096
+ periodEnd: endTime,
2097
+ totalSpins: records.length,
2098
+ totalStaked: 0,
2099
+ totalWon: 0,
2100
+ netResult: 0,
2101
+ biggestWin: 0,
2102
+ bonusTriggers: 0,
2103
+ jackpotWins: 0,
2104
+ sessionCount: 0,
2105
+ averageSessionDuration: 0
2106
+ };
2107
+ const sessions = /* @__PURE__ */ new Set();
2108
+ for (const record of records) {
2109
+ summary.totalStaked += record.stake;
2110
+ summary.totalWon += record.win;
2111
+ if (record.win > summary.biggestWin) {
2112
+ summary.biggestWin = record.win;
2113
+ }
2114
+ if (record.bonusTriggered) {
2115
+ summary.bonusTriggers++;
2116
+ }
2117
+ if (record.jackpotWins.length > 0) {
2118
+ summary.jackpotWins += record.jackpotWins.length;
2119
+ }
2120
+ sessions.add(record.sessionId);
2121
+ }
2122
+ summary.netResult = summary.totalWon - summary.totalStaked;
2123
+ summary.sessionCount = sessions.size;
2124
+ if (summary.sessionCount > 0) {
2125
+ summary.averageSessionDuration = (endTime - startTime) / summary.sessionCount / 6e4;
2126
+ }
2127
+ return summary;
2128
+ }
2129
+ /**
2130
+ * Clear all records (for testing).
2131
+ */
2132
+ clear() {
2133
+ this.records.clear();
2134
+ this.playerIndex.clear();
2135
+ }
2136
+ /**
2137
+ * Get total record count.
2138
+ */
2139
+ getCount() {
2140
+ return this.records.size;
2141
+ }
2142
+ };
2143
+ var AuditManager = class {
2144
+ storage;
2145
+ gameId;
2146
+ operatorId;
2147
+ constructor(gameId, storage, operatorId) {
2148
+ this.gameId = gameId;
2149
+ this.storage = storage || new InMemoryAuditStorage();
2150
+ this.operatorId = operatorId;
2151
+ }
2152
+ /**
2153
+ * Record a spin for audit purposes.
2154
+ */
2155
+ async recordSpin(params) {
2156
+ const record = {
2157
+ spinId: params.spinId,
2158
+ playerId: params.playerId,
2159
+ gameId: this.gameId,
2160
+ timestamp: Date.now(),
2161
+ stake: params.stake,
2162
+ win: params.win,
2163
+ balanceBefore: params.balanceBefore,
2164
+ balanceAfter: params.balanceAfter,
2165
+ rngSeed: params.rngSeed,
2166
+ result: params.result,
2167
+ gameMode: params.gameMode,
2168
+ bonusTriggered: params.bonusTriggered,
2169
+ jackpotContributions: params.jackpotContributions,
2170
+ jackpotWins: params.jackpotWins,
2171
+ ipAddress: params.ipAddress,
2172
+ sessionId: params.sessionId,
2173
+ deviceInfo: params.deviceInfo,
2174
+ location: params.location,
2175
+ operatorId: this.operatorId,
2176
+ platform: params.platform
2177
+ };
2178
+ await this.storage.save(record);
2179
+ return record;
2180
+ }
2181
+ /**
2182
+ * Get audit records for a player within a time range.
2183
+ */
2184
+ async getPlayerRecords(playerId, startTime, endTime) {
2185
+ return this.storage.findByPlayer(playerId, startTime, endTime);
2186
+ }
2187
+ /**
2188
+ * Get a specific spin record by ID.
2189
+ */
2190
+ async getSpinRecord(spinId) {
2191
+ return this.storage.findById(spinId);
2192
+ }
2193
+ /**
2194
+ * Get player activity summary for a period.
2195
+ */
2196
+ async getPlayerSummary(playerId, startTime, endTime) {
2197
+ return this.storage.getSummary(playerId, startTime, endTime);
2198
+ }
2199
+ /**
2200
+ * Export records for regulatory reporting.
2201
+ */
2202
+ async exportRecords(playerId, startTime, endTime, format = "json") {
2203
+ const records = await this.getPlayerRecords(playerId, startTime, endTime);
2204
+ if (format === "json") {
2205
+ return JSON.stringify(records, null, 2);
2206
+ }
2207
+ const headers = [
2208
+ "spinId",
2209
+ "playerId",
2210
+ "timestamp",
2211
+ "stake",
2212
+ "win",
2213
+ "balanceBefore",
2214
+ "balanceAfter",
2215
+ "gameMode",
2216
+ "bonusTriggered"
2217
+ ].join(",");
2218
+ const rows = records.map((r) => [
2219
+ r.spinId,
2220
+ r.playerId,
2221
+ r.timestamp,
2222
+ r.stake,
2223
+ r.win,
2224
+ r.balanceBefore,
2225
+ r.balanceAfter,
2226
+ r.gameMode,
2227
+ r.bonusTriggered
2228
+ ].join(","));
2229
+ return [headers, ...rows].join("\n");
2230
+ }
2231
+ /**
2232
+ * Verify game integrity by checking RNG seed chain.
2233
+ */
2234
+ async verifyRngChain(spinIds) {
2235
+ const errors = [];
2236
+ for (let i = 1; i < spinIds.length; i++) {
2237
+ const prevRecord = await this.getSpinRecord(spinIds[i - 1]);
2238
+ const currRecord = await this.getSpinRecord(spinIds[i]);
2239
+ if (!prevRecord || !currRecord) {
2240
+ errors.push(`Missing record for spin ${spinIds[i]}`);
2241
+ continue;
2242
+ }
2243
+ if (currRecord.timestamp <= prevRecord.timestamp) {
2244
+ errors.push(`Timestamp order violation: ${spinIds[i]} <= ${spinIds[i - 1]}`);
2245
+ }
2246
+ }
2247
+ return {
2248
+ valid: errors.length === 0,
2249
+ errors
2250
+ };
2251
+ }
2252
+ };
2253
+
2254
+ // src/DebugManager.ts
2255
+ var DebugManager = class {
2256
+ config;
2257
+ spinCount = 0;
2258
+ lastRngSeed = "";
2259
+ forcedOutcomesApplied = 0;
2260
+ logs = {
2261
+ errors: [],
2262
+ warnings: [],
2263
+ infoLogs: [],
2264
+ debugLogs: []
2265
+ };
2266
+ constructor(config) {
2267
+ this.config = {
2268
+ enabled: config?.enabled ?? false,
2269
+ logSpins: config?.logSpins ?? false,
2270
+ logRngSeeds: config?.logRngSeeds ?? false,
2271
+ forcedOutcomes: config?.forcedOutcomes ?? [],
2272
+ skipBalanceChecks: config?.skipBalanceChecks ?? false,
2273
+ verboseLevel: config?.verboseLevel ?? "warnings",
2274
+ outputFormat: config?.outputFormat ?? "console",
2275
+ logFilePath: config?.logFilePath
2276
+ };
2277
+ }
2278
+ /**
2279
+ * Update debug configuration.
2280
+ */
2281
+ configure(config) {
2282
+ this.config = { ...this.config, ...config };
2283
+ this.log("info", "config", "Debug configuration updated", { config });
2284
+ }
2285
+ /**
2286
+ * Check if debug mode is enabled.
2287
+ */
2288
+ isEnabled() {
2289
+ return this.config.enabled;
2290
+ }
2291
+ /**
2292
+ * Get current debug state.
2293
+ */
2294
+ getState() {
2295
+ return {
2296
+ enabled: this.config.enabled,
2297
+ spinCount: this.spinCount,
2298
+ lastRngSeed: this.lastRngSeed,
2299
+ forcedOutcomesApplied: this.forcedOutcomesApplied,
2300
+ errors: this.logs.errors,
2301
+ warnings: this.logs.warnings,
2302
+ infoLogs: this.logs.infoLogs
2303
+ };
2304
+ }
2305
+ /**
2306
+ * Reset debug state and logs.
2307
+ */
2308
+ reset() {
2309
+ this.spinCount = 0;
2310
+ this.lastRngSeed = "";
2311
+ this.forcedOutcomesApplied = 0;
2312
+ this.logs = {
2313
+ errors: [],
2314
+ warnings: [],
2315
+ infoLogs: [],
2316
+ debugLogs: []
2317
+ };
2318
+ this.log("info", "system", "Debug state reset");
2319
+ }
2320
+ /**
2321
+ * Log a message at specified level.
2322
+ */
2323
+ log(level, category, message, data) {
2324
+ if (!this.config.enabled) return;
2325
+ const shouldLog = this.shouldLogLevel(level);
2326
+ if (!shouldLog) return;
2327
+ const entry = {
2328
+ timestamp: Date.now(),
2329
+ level,
2330
+ category,
2331
+ message,
2332
+ data
2333
+ };
2334
+ switch (level) {
2335
+ case "error":
2336
+ this.logs.errors.push(entry);
2337
+ break;
2338
+ case "warning":
2339
+ this.logs.warnings.push(entry);
2340
+ break;
2341
+ case "info":
2342
+ this.logs.infoLogs.push(entry);
2343
+ break;
2344
+ case "debug":
2345
+ this.logs.debugLogs.push(entry);
2346
+ break;
2347
+ }
2348
+ this.outputLog(entry);
2349
+ }
2350
+ /**
2351
+ * Log a spin result.
2352
+ */
2353
+ logSpin(spinId, result, rngSeed) {
2354
+ if (!this.config.enabled || !this.config.logSpins) return;
2355
+ this.spinCount++;
2356
+ this.lastRngSeed = rngSeed;
2357
+ this.log("debug", "spin", `Spin #${this.spinCount}`, {
2358
+ spinId,
2359
+ totalWin: result.totalWin,
2360
+ bonusTriggered: result.bonusTriggered,
2361
+ cascadeLevel: result.cascadeLevel,
2362
+ rngSeed
2363
+ });
2364
+ if (this.config.logRngSeeds) {
2365
+ this.log("debug", "rng", `RNG Seed for spin #${this.spinCount}`, { seed: rngSeed });
2366
+ }
2367
+ }
2368
+ /**
2369
+ * Check and apply forced outcome if conditions match.
2370
+ */
2371
+ checkForcedOutcome(params) {
2372
+ if (!this.config.enabled || !this.config.forcedOutcomes) return null;
2373
+ for (const forced of this.config.forcedOutcomes) {
2374
+ let matches = false;
2375
+ switch (forced.condition.type) {
2376
+ case "spinNumber":
2377
+ matches = forced.condition.value === params.spinNumber;
2378
+ break;
2379
+ case "randomSeed":
2380
+ matches = forced.condition.value === params.rngSeed;
2381
+ break;
2382
+ case "betAmount":
2383
+ matches = forced.condition.value === params.betAmount;
2384
+ break;
2385
+ case "playerId":
2386
+ matches = forced.condition.value === params.playerId;
2387
+ break;
2388
+ }
2389
+ if (matches && forced.result) {
2390
+ this.forcedOutcomesApplied++;
2391
+ this.log("warning", "forced", `Forced outcome applied for spin #${params.spinNumber}`, {
2392
+ condition: forced.condition,
2393
+ result: forced.result
2394
+ });
2395
+ return {
2396
+ result: forced.result.symbols || [],
2397
+ totalWin: forced.result.winAmount || 0,
2398
+ stakePerLine: 0,
2399
+ totalStake: 0,
2400
+ gameMode: 0,
2401
+ balanceAfter: 0,
2402
+ bonusTriggered: forced.result.bonusTriggered ?? false
2403
+ };
2404
+ }
2405
+ }
2406
+ return null;
2407
+ }
2408
+ /**
2409
+ * Check if balance checks should be skipped.
2410
+ */
2411
+ shouldSkipBalanceCheck() {
2412
+ return this.config.enabled && this.config.skipBalanceChecks;
2413
+ }
2414
+ /**
2415
+ * Get all logs (optionally filtered by level).
2416
+ */
2417
+ getLogs(level) {
2418
+ if (!level) {
2419
+ return [
2420
+ ...this.logs.errors,
2421
+ ...this.logs.warnings,
2422
+ ...this.logs.infoLogs,
2423
+ ...this.logs.debugLogs
2424
+ ].sort((a, b) => a.timestamp - b.timestamp);
2425
+ }
2426
+ switch (level) {
2427
+ case "error":
2428
+ return this.logs.errors;
2429
+ case "warning":
2430
+ return this.logs.warnings;
2431
+ case "info":
2432
+ return this.logs.infoLogs;
2433
+ case "debug":
2434
+ return this.logs.debugLogs;
2435
+ }
2436
+ }
2437
+ /**
2438
+ * Export logs as JSON.
2439
+ */
2440
+ exportLogs() {
2441
+ return JSON.stringify(this.getLogs(), null, 2);
2442
+ }
2443
+ /**
2444
+ * Clear all logs.
2445
+ */
2446
+ clearLogs() {
2447
+ this.logs = {
2448
+ errors: [],
2449
+ warnings: [],
2450
+ infoLogs: [],
2451
+ debugLogs: []
2452
+ };
2453
+ }
2454
+ /**
2455
+ * Determine if a log level should be output based on verboseLevel config.
2456
+ */
2457
+ shouldLogLevel(level) {
2458
+ const levels = ["none", "errors", "warnings", "info", "debug"];
2459
+ const currentIndex = levels.indexOf(this.config.verboseLevel);
2460
+ const levelIndex = levels.indexOf(level);
2461
+ return levelIndex <= currentIndex && levelIndex > 0;
2462
+ }
2463
+ /**
2464
+ * Output log entry based on outputFormat config.
2465
+ */
2466
+ outputLog(entry) {
2467
+ if (this.config.outputFormat === "console") {
2468
+ const prefix = `[${new Date(entry.timestamp).toISOString()}] [${entry.level.toUpperCase()}] ${entry.category}:`;
2469
+ switch (entry.level) {
2470
+ case "error":
2471
+ console.error(prefix, entry.message, entry.data || "");
2472
+ break;
2473
+ case "warning":
2474
+ console.warn(prefix, entry.message, entry.data || "");
2475
+ break;
2476
+ case "info":
2477
+ console.info(prefix, entry.message, entry.data || "");
2478
+ break;
2479
+ case "debug":
2480
+ console.log(prefix, entry.message, entry.data || "");
2481
+ break;
2482
+ }
2483
+ } else if (this.config.outputFormat === "json") {
2484
+ console.log(JSON.stringify(entry));
2485
+ }
2486
+ }
2487
+ /**
2488
+ * Get statistics about logged events.
2489
+ */
2490
+ getStatistics() {
2491
+ return {
2492
+ totalLogs: this.logs.errors.length + this.logs.warnings.length + this.logs.infoLogs.length + this.logs.debugLogs.length,
2493
+ errorCount: this.logs.errors.length,
2494
+ warningCount: this.logs.warnings.length,
2495
+ infoCount: this.logs.infoLogs.length,
2496
+ debugCount: this.logs.debugLogs.length,
2497
+ spinsTracked: this.spinCount,
2498
+ forcedOutcomesApplied: this.forcedOutcomesApplied
2499
+ };
2500
+ }
2501
+ };
2502
+
2503
+ // src/ResponsibleGamingManager.ts
2504
+ var ResponsibleGamingManager = class {
2505
+ limits;
2506
+ playerState;
2507
+ alerts = [];
2508
+ realityCheckCounter = 0;
2509
+ lastRealityCheckTime = 0;
2510
+ constructor(limits, playerState) {
2511
+ this.limits = limits;
2512
+ this.playerState = playerState;
2513
+ if (!playerState.sessionStartTime && limits.sessionTimeLimit) {
2514
+ playerState.sessionStartTime = Date.now();
2515
+ }
2516
+ }
2517
+ /**
2518
+ * Update player state (call after each spin).
2519
+ */
2520
+ updatePlayerState(state) {
2521
+ this.playerState = { ...this.playerState, ...state };
2522
+ this.checkAllLimits();
2523
+ }
2524
+ /**
2525
+ * Update responsible gaming limits (e.g., player changed their limits).
2526
+ */
2527
+ updateLimits(newLimits) {
2528
+ this.limits = { ...this.limits, ...newLimits };
2529
+ this.validateLimits();
2530
+ this.checkAllLimits();
2531
+ }
2532
+ /**
2533
+ * Check if player can place a bet of the given amount.
2534
+ */
2535
+ canPlaceBet(betAmount) {
2536
+ const selfExclusion = this.getSelfExclusionStatus();
2537
+ if (selfExclusion.isExcluded) {
2538
+ return {
2539
+ success: false,
2540
+ error: `Player is self-excluded until ${selfExclusion.exclusionUntil?.toISOString()}`
2541
+ };
2542
+ }
2543
+ if (this.limits.maxBetLimit && betAmount > this.limits.maxBetLimit) {
2544
+ return {
2545
+ success: false,
2546
+ error: `Bet amount ${betAmount} exceeds maximum allowed bet of ${this.limits.maxBetLimit}`
2547
+ };
2548
+ }
2549
+ const lossInfo = this.getLossTrackingInfo();
2550
+ if (lossInfo.isLimitReached) {
2551
+ return {
2552
+ success: false,
2553
+ error: `Session loss limit of ${this.limits.lossLimit} has been reached`
2554
+ };
2555
+ }
2556
+ const timeInfo = this.getSessionTimeInfo();
2557
+ if (timeInfo.isLimitReached) {
2558
+ return {
2559
+ success: false,
2560
+ error: `Session time limit of ${this.limits.sessionTimeLimit} minutes has been reached`
2561
+ };
2562
+ }
2563
+ return { success: true, data: true };
2564
+ }
2565
+ /**
2566
+ * Check if player can continue playing (general eligibility).
2567
+ */
2568
+ canContinuePlaying() {
2569
+ return this.canPlaceBet(0);
2570
+ }
2571
+ /**
2572
+ * Get session time tracking information.
2573
+ */
2574
+ getSessionTimeInfo() {
2575
+ const startTime = this.playerState.sessionStartTime || Date.now();
2576
+ const elapsedMs = Date.now() - startTime;
2577
+ const elapsedMinutes = Math.floor(elapsedMs / 6e4);
2578
+ const limitMinutes = this.limits.sessionTimeLimit;
2579
+ const isLimitReached = limitMinutes ? elapsedMinutes >= limitMinutes : false;
2580
+ const warningThreshold = limitMinutes ? limitMinutes * 0.8 : 0;
2581
+ const warningThresholdReached = limitMinutes ? elapsedMinutes >= warningThreshold : false;
2582
+ return {
2583
+ sessionStartTime: startTime,
2584
+ elapsedMinutes,
2585
+ limitMinutes,
2586
+ isLimitReached,
2587
+ warningThresholdReached
2588
+ };
2589
+ }
2590
+ /**
2591
+ * Get loss tracking information.
2592
+ */
2593
+ getLossTrackingInfo() {
2594
+ const sessionLoss = this.playerState.sessionLoss || 0;
2595
+ const lossLimit = this.limits.lossLimit;
2596
+ const isLimitReached = lossLimit ? sessionLoss >= lossLimit : false;
2597
+ const warningThreshold = lossLimit ? lossLimit * 0.8 : 0;
2598
+ const warningThresholdReached = lossLimit ? sessionLoss >= warningThreshold : false;
2599
+ return {
2600
+ sessionLoss,
2601
+ lossLimit,
2602
+ isLimitReached,
2603
+ warningThresholdReached
2604
+ };
2605
+ }
2606
+ /**
2607
+ * Get win tracking information.
2608
+ */
2609
+ getWinTrackingInfo() {
2610
+ const sessionWin = this.playerState.sessionWin || 0;
2611
+ const winGoal = this.limits.winGoal;
2612
+ const isGoalReached = winGoal ? sessionWin >= winGoal : false;
2613
+ const notificationSent = isGoalReached && this.alerts.some(
2614
+ (a) => a.type === "win_goal" && !a.actionRequired
2615
+ );
2616
+ return {
2617
+ sessionWin,
2618
+ winGoal,
2619
+ isGoalReached,
2620
+ notificationSent
2621
+ };
2622
+ }
2623
+ /**
2624
+ * Get self-exclusion status.
2625
+ */
2626
+ getSelfExclusionStatus() {
2627
+ const exclusionUntil = this.limits.selfExclusionUntil;
2628
+ if (!exclusionUntil) {
2629
+ return { isExcluded: false };
2630
+ }
2631
+ const exclusionDate = new Date(exclusionUntil);
2632
+ const now = /* @__PURE__ */ new Date();
2633
+ const isExcluded = now < exclusionDate;
2634
+ if (!isExcluded) {
2635
+ return { isExcluded: false };
2636
+ }
2637
+ const remainingDays = Math.ceil((exclusionDate.getTime() - now.getTime()) / (1e3 * 60 * 60 * 24));
2638
+ return {
2639
+ isExcluded: true,
2640
+ exclusionUntil: exclusionDate,
2641
+ remainingDays
2642
+ };
2643
+ }
2644
+ /**
2645
+ * Check if reality check is due.
2646
+ */
2647
+ shouldShowRealityCheck() {
2648
+ const interval = this.limits.realityCheckInterval;
2649
+ if (!interval || !this.playerState.sessionStartTime) {
2650
+ return false;
2651
+ }
2652
+ const elapsedMinutes = this.getSessionTimeInfo().elapsedMinutes;
2653
+ const checksDue = Math.floor(elapsedMinutes / interval);
2654
+ return checksDue > this.realityCheckCounter;
2655
+ }
2656
+ /**
2657
+ * Generate reality check event.
2658
+ */
2659
+ generateRealityCheck() {
2660
+ if (!this.shouldShowRealityCheck()) {
2661
+ return null;
2662
+ }
2663
+ this.realityCheckCounter++;
2664
+ this.lastRealityCheckTime = Date.now();
2665
+ const elapsedMinutes = this.getSessionTimeInfo().elapsedMinutes;
2666
+ const totalSpins = this.playerState.history?.length || 0;
2667
+ const totalStaked = this.playerState.history?.reduce((sum, entry) => sum + entry.bet, 0) || 0;
2668
+ const totalWon = this.playerState.history?.reduce((sum, entry) => sum + entry.win, 0) || 0;
2669
+ const netResult = totalWon - totalStaked;
2670
+ const event = {
2671
+ timestamp: Date.now(),
2672
+ elapsedMinutes,
2673
+ totalSpins,
2674
+ totalStaked,
2675
+ totalWon,
2676
+ netResult,
2677
+ message: `Reality Check: You've been playing for ${elapsedMinutes} minutes. Total staked: ${totalStaked}, Total won: ${totalWon}, Net: ${netResult}`
2678
+ };
2679
+ this.addAlert({
2680
+ type: "reality_check",
2681
+ severity: "info",
2682
+ message: event.message,
2683
+ timestamp: event.timestamp
2684
+ });
2685
+ return event;
2686
+ }
2687
+ /**
2688
+ * Check all limits and generate alerts.
2689
+ */
2690
+ checkAllLimits() {
2691
+ const timeInfo = this.getSessionTimeInfo();
2692
+ if (timeInfo.isLimitReached) {
2693
+ this.addAlert({
2694
+ type: "session_time",
2695
+ severity: "critical",
2696
+ message: `Session time limit of ${this.limits.sessionTimeLimit} minutes reached. Play suspended.`,
2697
+ timestamp: Date.now(),
2698
+ actionRequired: true,
2699
+ suggestedAction: "Please take a break or end your session."
2700
+ });
2701
+ } else if (timeInfo.warningThresholdReached) {
2702
+ this.addAlert({
2703
+ type: "session_time",
2704
+ severity: "warning",
2705
+ message: `You've been playing for ${timeInfo.elapsedMinutes} minutes. Limit is ${this.limits.sessionTimeLimit} minutes.`,
2706
+ timestamp: Date.now(),
2707
+ actionRequired: false
2708
+ });
2709
+ }
2710
+ const lossInfo = this.getLossTrackingInfo();
2711
+ if (lossInfo.isLimitReached) {
2712
+ this.addAlert({
2713
+ type: "loss_limit",
2714
+ severity: "critical",
2715
+ message: `Session loss limit of ${this.limits.lossLimit} reached. Play suspended.`,
2716
+ timestamp: Date.now(),
2717
+ actionRequired: true,
2718
+ suggestedAction: "Please end your session or wait until tomorrow."
2719
+ });
2720
+ } else if (lossInfo.warningThresholdReached) {
2721
+ this.addAlert({
2722
+ type: "loss_limit",
2723
+ severity: "warning",
2724
+ message: `Your session losses are ${lossInfo.sessionLoss}. Limit is ${this.limits.lossLimit}.`,
2725
+ timestamp: Date.now(),
2726
+ actionRequired: false
2727
+ });
2728
+ }
2729
+ const winInfo = this.getWinTrackingInfo();
2730
+ if (winInfo.isGoalReached && !winInfo.notificationSent) {
2731
+ this.addAlert({
2732
+ type: "win_goal",
2733
+ severity: "info",
2734
+ message: `Congratulations! You've reached your win goal of ${this.limits.winGoal}.`,
2735
+ timestamp: Date.now(),
2736
+ actionRequired: false,
2737
+ suggestedAction: "Consider cashing out your winnings."
2738
+ });
2739
+ }
2740
+ }
2741
+ /**
2742
+ * Add an alert to the list.
2743
+ */
2744
+ addAlert(alert) {
2745
+ this.alerts.push(alert);
2746
+ if (this.alerts.length > 100) {
2747
+ this.alerts = this.alerts.slice(-100);
2748
+ }
2749
+ }
2750
+ /**
2751
+ * Get all active alerts.
2752
+ */
2753
+ getAlerts() {
2754
+ return [...this.alerts];
2755
+ }
2756
+ /**
2757
+ * Clear alerts (call after player acknowledges them).
2758
+ */
2759
+ clearAlerts() {
2760
+ this.alerts = [];
2761
+ }
2762
+ /**
2763
+ * Get player activity statistics.
2764
+ */
2765
+ getPlayerActivityStats() {
2766
+ const history = this.playerState.history || [];
2767
+ const totalStaked = history.reduce((sum, entry) => sum + entry.bet, 0);
2768
+ const totalWon = history.reduce((sum, entry) => sum + entry.win, 0);
2769
+ let totalTimePlayed = 0;
2770
+ if (this.playerState.sessionStartTime) {
2771
+ totalTimePlayed = this.getSessionTimeInfo().elapsedMinutes;
2772
+ }
2773
+ return {
2774
+ totalSessions: 1,
2775
+ // In production, track across multiple sessions
2776
+ totalTimePlayed,
2777
+ totalStaked,
2778
+ totalWon,
2779
+ netResult: totalWon - totalStaked,
2780
+ averageSessionDuration: totalTimePlayed,
2781
+ longestSession: totalTimePlayed,
2782
+ biggestLoss: Math.min(0, totalWon - totalStaked),
2783
+ biggestWin: Math.max(0, ...history.map((h) => h.win)),
2784
+ lastSessionDate: this.playerState.sessionStartTime
2785
+ };
2786
+ }
2787
+ /**
2788
+ * Validate limits configuration.
2789
+ */
2790
+ validateLimits() {
2791
+ const errors = [];
2792
+ if (this.limits.sessionTimeLimit && this.limits.sessionTimeLimit <= 0) {
2793
+ errors.push("Session time limit must be positive");
2794
+ }
2795
+ if (this.limits.lossLimit && this.limits.lossLimit <= 0) {
2796
+ errors.push("Loss limit must be positive");
2797
+ }
2798
+ if (this.limits.winGoal && this.limits.winGoal <= 0) {
2799
+ errors.push("Win goal must be positive");
2800
+ }
2801
+ if (this.limits.maxBetLimit && this.limits.maxBetLimit <= 0) {
2802
+ errors.push("Max bet limit must be positive");
2803
+ }
2804
+ if (this.limits.realityCheckInterval && this.limits.realityCheckInterval <= 0) {
2805
+ errors.push("Reality check interval must be positive");
2806
+ }
2807
+ if (this.limits.selfExclusionUntil) {
2808
+ const exclusionDate = new Date(this.limits.selfExclusionUntil);
2809
+ if (isNaN(exclusionDate.getTime())) {
2810
+ errors.push("Invalid self-exclusion date format");
2811
+ }
2812
+ }
2813
+ if (errors.length > 0) {
2814
+ throw new Error(`Invalid RG limits configuration: ${errors.join(", ")}`);
2815
+ }
2816
+ }
2817
+ /**
2818
+ * Reset session tracking (call when player starts new session).
2819
+ */
2820
+ resetSession() {
2821
+ this.playerState.sessionStartTime = Date.now();
2822
+ this.playerState.sessionLoss = 0;
2823
+ this.playerState.sessionWin = 0;
2824
+ this.realityCheckCounter = 0;
2825
+ this.clearAlerts();
2826
+ }
2827
+ /**
2828
+ * Apply a win/loss result to session tracking.
2829
+ */
2830
+ applyResult(stake, win) {
2831
+ const netResult = win - stake;
2832
+ if (netResult < 0) {
2833
+ this.playerState.sessionLoss = (this.playerState.sessionLoss || 0) + Math.abs(netResult);
2834
+ }
2835
+ if (netResult > 0) {
2836
+ this.playerState.sessionWin = (this.playerState.sessionWin || 0) + netResult;
2837
+ }
2838
+ this.checkAllLimits();
2839
+ }
2840
+ /**
2841
+ * Export responsible gaming report for player download.
2842
+ */
2843
+ exportRGReport() {
2844
+ const stats = this.getPlayerActivityStats();
2845
+ const selfExclusion = this.getSelfExclusionStatus();
2846
+ const report = {
2847
+ reportGenerated: (/* @__PURE__ */ new Date()).toISOString(),
2848
+ playerLimits: this.limits,
2849
+ selfExclusionStatus: selfExclusion,
2850
+ activityStatistics: stats,
2851
+ recentAlerts: this.alerts.slice(-20)
2852
+ };
2853
+ return JSON.stringify(report, null, 2);
2854
+ }
2855
+ };
2856
+
2857
+ // src/sampleData.ts
2858
+ var sampleConfig = {
2859
+ gameId: "fruit-slot",
2860
+ reelCount: 5,
2861
+ rowCount: 3,
2862
+ symbolMap: {
2863
+ 0: "cherry",
2864
+ 1: "lemon",
2865
+ 2: "orange",
2866
+ 3: "plum",
2867
+ 4: "grape",
2868
+ 5: "watermelon",
2869
+ 6: "star",
2870
+ 7: "bell",
2871
+ 8: "seven",
2872
+ 9: "diamond"
2873
+ },
2874
+ paytableData: {
2875
+ 0: { x3: 10, x4: 20, x5: 50 },
2876
+ 1: { x3: 5, x4: 15, x5: 30 },
2877
+ 2: { x3: 4, x4: 10, x5: 25 },
2878
+ 3: { x3: 3, x4: 8, x5: 20 },
2879
+ 4: { x3: 2, x4: 5, x5: 15 },
2880
+ 5: { x3: 1, x4: 3, x5: 10 },
2881
+ 6: { x3: 0, x4: 2, x5: 8 },
2882
+ 7: { x3: 0, x4: 1, x5: 5 },
2883
+ 8: { x3: 0, x4: 0, x5: 2 },
2884
+ 9: { x3: 0, x4: 0, x5: 1 }
2885
+ },
2886
+ reelStrips: [
2887
+ [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
2888
+ [1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0],
2889
+ [2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1],
2890
+ [3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2],
2891
+ [4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3]
2892
+ ],
2893
+ linePatterns: [
2894
+ [{ reel: 0, row: 1 }, { reel: 1, row: 1 }, { reel: 2, row: 1 }, { reel: 3, row: 1 }, { reel: 4, row: 1 }],
2895
+ [{ reel: 0, row: 0 }, { reel: 1, row: 0 }, { reel: 2, row: 0 }, { reel: 3, row: 0 }, { reel: 4, row: 0 }],
2896
+ [{ reel: 0, row: 2 }, { reel: 1, row: 2 }, { reel: 2, row: 2 }, { reel: 3, row: 2 }, { reel: 4, row: 2 }],
2897
+ [{ reel: 0, row: 0 }, { reel: 1, row: 1 }, { reel: 2, row: 2 }, { reel: 3, row: 1 }, { reel: 4, row: 0 }],
2898
+ [{ reel: 0, row: 2 }, { reel: 1, row: 1 }, { reel: 2, row: 0 }, { reel: 3, row: 1 }, { reel: 4, row: 2 }]
2899
+ ],
2900
+ currency: {
2901
+ code: "USD",
2902
+ symbol: "$",
2903
+ decimalSeparator: ".",
2904
+ thousandSeparator: ",",
2905
+ minDecimalPlaces: 2
2906
+ },
2907
+ features: {
2908
+ autoplay: true,
2909
+ fastSpin: true,
2910
+ realityCheck: true,
2911
+ fullscreen: true
2912
+ },
2913
+ defaultBet: 1,
2914
+ minBet: 0.1,
2915
+ maxBet: 100,
2916
+ betStep: 0.1,
2917
+ defaultLines: 5,
2918
+ rtp: 96.5,
2919
+ volatility: "medium"
2920
+ };
2921
+ // Annotate the CommonJS export names for ESM import in node:
2922
+ 0 && (module.exports = {
2923
+ AuditManager,
2924
+ BonusManager,
2925
+ BuyBonusManager,
2926
+ CascadingReelsManager,
2927
+ ConfigBuilder,
2928
+ DebugManager,
2929
+ ExpandingWildsManager,
2930
+ GambleManager,
2931
+ GameSession,
2932
+ HistoryManager,
2933
+ InMemoryAuditStorage,
2934
+ InMemoryWalletStorage,
2935
+ JackpotManager,
2936
+ Random,
2937
+ RespinsManager,
2938
+ ResponsibleGamingManager,
2939
+ ScatterEnhancementsManager,
2940
+ SpinEvaluator,
2941
+ WalletManager,
2942
+ WaysEvaluator,
2943
+ sampleConfig
2944
+ });