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