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/README.md ADDED
@@ -0,0 +1,949 @@
1
+ # Slot Server
2
+
3
+ A professional server-side companion library for the [Slot Engine client](https://github.com/bakarilevy/slot-engine).
4
+ It provides comprehensive types, game logic managers, and utilities to handle all server-side operations for modern slot games including spins, bonuses, jackpots, gamble features, and regulatory compliance.
5
+
6
+ ## Features
7
+
8
+ ### Core Features
9
+ - **Full game state management** - Balance, bet, free spins, bonus tracking
10
+ - **Configurable game data** - Symbols, paytables, reel strips, paylines
11
+ - **Spin evaluation** - Win calculation with payline and ways-to-win support
12
+ - **Bonus system** - Free spins, pick'em games, and extensible bonus framework
13
+ - **History tracking** - Complete spin history with audit trail
14
+
15
+ ### Advanced Features
16
+ - **Progressive Jackpots** - Multi-tier jackpot system with contribution tracking
17
+ - **Ways-to-Win** - Support for 243/1024/4096 ways mechanics
18
+ - **Gamble Feature** - Card guess, ladder climb, and wheel mini-games
19
+ - **Buy Bonus** - Direct bonus purchase with configurable limits and cooldowns
20
+
21
+ ### Enhanced Gameplay Features (NEW)
22
+ - **Cascading Reels** - Tumbling mechanics with symbol removal, gravity drops, chain reactions, and progressive multipliers
23
+ - **Expanding Wilds** - Wild expansion to cover entire reels with configurable triggers and multiplier stacking
24
+ - **Sticky Wilds** - Persistent wilds that stay in place for multiple spins
25
+ - **Scatter Enhancements** - Advanced scatter mechanics including cluster triggers, collection systems, progressive scatters, transformation mechanics, and multiplier zones
26
+ - **Respins / Hold & Win** - Locking symbols for respin rounds, money symbol collection, reset symbols, jackpot symbols (Mini/Minor/Major/Grand), full screen bonuses, and weighted symbol generation
27
+
28
+ ### Compliance & Operations (NEW)
29
+ - **Audit System** - Complete spin audit records for regulatory compliance with export capabilities
30
+ - **Debug Mode** - Comprehensive debugging tools with logging, forced outcomes, and state inspection
31
+ - **Responsible Gaming** - Session limits, reality checks, self-exclusion support
32
+ - **Tournament Mode** - Leaderboards, scoring, and time-limited competitions
33
+
34
+ ## Installation
35
+
36
+ ```bash
37
+ npm install slot-server
38
+ ```
39
+
40
+ ## Quick Start
41
+
42
+ ```ts
43
+ import {
44
+ ConfigBuilder,
45
+ GameSession,
46
+ sampleConfig,
47
+ JackpotManager,
48
+ WaysEvaluator,
49
+ GambleManager,
50
+ BuyBonusManager,
51
+ CascadingReelsManager,
52
+ ExpandingWildsManager,
53
+ ScatterEnhancementsManager,
54
+ RespinsManager,
55
+ AuditManager,
56
+ DebugManager
57
+ } from 'slot-server';
58
+
59
+ // 1. Build your game configuration
60
+ const config = new ConfigBuilder()
61
+ .setGameId('my-game')
62
+ .setReelCount(5)
63
+ .setRowCount(3)
64
+ .setSymbolMap({ 0: 'symbol0', 1: 'symbol1', ... })
65
+ .setPaytableData({ 0: { x3: 10, x4: 20, x5: 50 }, ... })
66
+ .setReelStrips([[...], ...])
67
+ .setLinePatterns([...])
68
+ .setBetRange(0.1, 100, 0.1, 1)
69
+ .build();
70
+
71
+ // Or use the sample config:
72
+ // const config = sampleConfig;
73
+
74
+ // 2. Implement a balance provider (e.g., using a database)
75
+ const balanceProvider = {
76
+ getBalance: (playerId) => getFromDB(playerId),
77
+ setBalance: (playerId, amount) => updateDB(playerId, amount),
78
+ };
79
+
80
+ // 3. Create a game session for a player
81
+ const session = new GameSession(config, 'player-123', balanceProvider);
82
+
83
+ // 4. Send INIT response to client
84
+ const initResponse = session.getInitResponse();
85
+ // Send via WebSocket: ws.send(JSON.stringify({ type: 'INIT', data: initResponse }))
86
+
87
+ // 5. Process a spin request
88
+ const spinRequest = {
89
+ action: 'spin',
90
+ stakePerLine: 1,
91
+ selectedLines: 5,
92
+ totalStake: 5,
93
+ gameMode: 0, // 0 = main game, 1 = bonus/free spins
94
+ };
95
+ const spinResult = session.spin(spinRequest);
96
+ // Send result to client: ws.send(JSON.stringify({ type: 'SPIN', data: spinResult }))
97
+
98
+ // 6. Handle bonus actions (e.g., pick'em games)
99
+ const bonusResult = session.bonusAction('pick', { itemId: 'item-123' });
100
+ ```
101
+
102
+ ## Advanced Usage
103
+
104
+ ### Progressive Jackpots
105
+
106
+ ```ts
107
+ import { Random, JackpotManager } from 'slot-server';
108
+
109
+ const rng = new Random();
110
+ const jackpotConfig = {
111
+ enabled: true,
112
+ type: 'standalone' as const,
113
+ tiers: [
114
+ {
115
+ id: 'mini',
116
+ name: 'Mini Jackpot',
117
+ seedAmount: 100,
118
+ currentAmount: 100,
119
+ triggerProbability: 0.001,
120
+ contributionRate: 0.5, // 0.5% of each bet
121
+ },
122
+ {
123
+ id: 'major',
124
+ name: 'Major Jackpot',
125
+ seedAmount: 1000,
126
+ currentAmount: 1000,
127
+ triggerProbability: 0.0001,
128
+ contributionRate: 0.3,
129
+ },
130
+ ],
131
+ };
132
+
133
+ const jackpotManager = new JackpotManager(jackpotConfig, rng);
134
+
135
+ // On each spin, calculate contributions
136
+ const contributions = jackpotManager.calculateContribution(totalStake);
137
+
138
+ // Check for jackpot trigger
139
+ const jackpotWin = jackpotManager.checkTrigger();
140
+ if (jackpotWin) {
141
+ console.log(`Jackpot won: ${jackpotWin.tierName} - $${jackpotWin.winAmount}`);
142
+ }
143
+ ```
144
+
145
+ ### Ways-to-Win Evaluation
146
+
147
+ ```ts
148
+ import { WaysEvaluator } from 'slot-server';
149
+
150
+ const waysConfig = {
151
+ enabled: true,
152
+ waysCount: 243,
153
+ minMatches: 3,
154
+ direction: 'leftToRight' as const,
155
+ };
156
+
157
+ const evaluator = new WaysEvaluator(
158
+ waysConfig,
159
+ paytableData,
160
+ reelCount,
161
+ rowCount
162
+ );
163
+
164
+ const result = evaluator.evaluate(spinResult, stakePerLine);
165
+ console.log(`Total ways: ${result.ways}, Total win: ${result.totalWin}`);
166
+ ```
167
+
168
+ ### Gamble Feature
169
+
170
+ ```ts
171
+ import { Random, GambleManager } from 'slot-server';
172
+
173
+ const rng = new Random();
174
+ const gambleConfig = {
175
+ enabled: true,
176
+ maxGambleAmount: 500,
177
+ maxConsecutiveGambles: 5,
178
+ gameTypes: ['card', 'ladder', 'wheel'] as const,
179
+ autoCollectAt: 1000,
180
+ };
181
+
182
+ const gambleManager = new GambleManager(gambleConfig, rng);
183
+
184
+ // Player wants to gamble their win
185
+ if (gambleManager.canGamble(currentWin)) {
186
+ const gambleRequest = {
187
+ action: 'gamble',
188
+ gambleType: 'card',
189
+ currentWin,
190
+ choice: 'red', // or 'black'
191
+ };
192
+ const gambleResult = gambleManager.processGamble(gambleRequest);
193
+
194
+ if (gambleResult.result === 'win') {
195
+ console.log(`Won $${gambleResult.winAmount}!`);
196
+ }
197
+ }
198
+ ```
199
+
200
+ ### Buy Bonus Feature
201
+
202
+ ```ts
203
+ import { BuyBonusManager } from 'slot-server';
204
+
205
+ const buyBonusConfig = {
206
+ enabled: true,
207
+ costMultiplier: 100, // 100x bet
208
+ minBetRequired: 1,
209
+ maxPurchasesPerSession: 3,
210
+ cooldownMs: 5000, // 5 seconds between purchases
211
+ };
212
+
213
+ const buyBonusManager = new BuyBonusManager(buyBonusConfig);
214
+
215
+ // Check if player can buy bonus
216
+ const canBuy = buyBonusManager.canBuyBonus(currentBet);
217
+ if (canBuy.canBuy) {
218
+ console.log(`Cost: $${canBuy.cost}`);
219
+ buyBonusManager.recordPurchase();
220
+ // Trigger bonus round
221
+ }
222
+ ```
223
+
224
+ ### Cascading Reels
225
+
226
+ ```ts
227
+ import { CascadingReelsManager } from 'slot-server';
228
+
229
+ const cascadeConfig = {
230
+ enabled: true,
231
+ maxCascades: 10,
232
+ initialMultiplier: 1,
233
+ multiplierIncrement: 1, // +1 per cascade
234
+ symbolPool: [...], // Your symbol pool for filling empty positions
235
+ stopOnNoWinIncrease: true,
236
+ };
237
+
238
+ const cascadeManager = new CascadingReelsManager(cascadeConfig);
239
+
240
+ // After initial spin evaluation with wins
241
+ const cascadeResult = cascadeManager.executeCascade(gameState, initialWins);
242
+
243
+ console.log(`Total cascades: ${cascadeResult.totalCascades}`);
244
+ console.log(`Final multiplier: ${cascadeResult.finalMultiplier}`);
245
+ console.log(`Total wins from cascades: ${cascadeResult.totalWinsFromCascades}`);
246
+
247
+ // Send cascade steps to client for animation
248
+ cascadeResult.steps.forEach(step => {
249
+ ws.send(JSON.stringify({
250
+ type: 'CASCADE_STEP',
251
+ data: {
252
+ cascadeNumber: step.cascadeNumber,
253
+ removedPositions: step.removedPositions,
254
+ droppedSymbols: step.droppedSymbols,
255
+ newSymbols: step.newSymbols,
256
+ winsInStep: step.winsInStep,
257
+ multiplier: step.multiplier,
258
+ }
259
+ }));
260
+ });
261
+ ```
262
+
263
+ ### Expanding & Sticky Wilds
264
+
265
+ ```ts
266
+ import { ExpandingWildsManager } from 'slot-server';
267
+
268
+ const expandConfig = {
269
+ enabled: true,
270
+ triggerCondition: 'any_wild' as const,
271
+ wildMultiplier: 2,
272
+ multiplierStacking: 'multiply' as const,
273
+ };
274
+
275
+ const stickyConfig = {
276
+ enabled: true,
277
+ persistSpins: 3,
278
+ maxStickyWilds: 5,
279
+ };
280
+
281
+ const wildsManager = new ExpandingWildsManager(expandConfig, stickyConfig);
282
+
283
+ // Process expanding wilds after spin
284
+ const expandResult = wildsManager.processExpandingWilds(gameState);
285
+
286
+ if (expandResult.success) {
287
+ console.log(`Expanded ${expandResult.totalExpanded} positions`);
288
+ console.log(`Applied multiplier: ${expandResult.appliedMultiplier}`);
289
+
290
+ // Send expansion data to client
291
+ ws.send(JSON.stringify({
292
+ type: 'WILDS_EXPANDED',
293
+ data: {
294
+ expandedReels: expandResult.expandedReels,
295
+ expandedPositions: expandResult.expandedPositions,
296
+ modifiedGrid: expandResult.modifiedGrid,
297
+ }
298
+ }));
299
+ }
300
+
301
+ // Manage sticky wilds across spins
302
+ let activeStickyWilds = []; // Persist this between spins
303
+
304
+ const stickyResult = wildsManager.processStickyWilds(gameState, activeStickyWilds);
305
+ activeStickyWilds = stickyResult.activeStickyWilds;
306
+
307
+ // Add new sticky wilds (e.g., from bonus feature)
308
+ const newStickyResult = wildsManager.addStickyWilds(
309
+ gameState,
310
+ [{ reel: 2, row: 1 }, { reel: 3, row: 1 }],
311
+ 5 // persist for 5 spins
312
+ );
313
+ ```
314
+
315
+ ### Scatter Enhancements
316
+
317
+ ```ts
318
+ import { ScatterEnhancementsManager } from 'slot-server';
319
+
320
+ const scatterConfig = {
321
+ enabled: true,
322
+ minScattersForTrigger: 3,
323
+ triggeredFeature: 'FREE_SPINS' as const,
324
+ spinAwardTable: { 3: 10, 4: 15, 5: 20 },
325
+ enableScatterPays: true,
326
+ scatterPayTable: { 3: 5, 4: 20, 5: 100 },
327
+ enableClusterTriggers: true,
328
+ clusterSizeForTrigger: 4,
329
+ enableCollection: true,
330
+ collectionTarget: 10,
331
+ collectionBonusSpins: 15,
332
+ enableTransformation: true,
333
+ transformationType: 'to_wild' as const,
334
+ enableMultiplierZones: true,
335
+ multiplierZones: [
336
+ { reelStart: 1, reelEnd: 3, rowStart: 1, rowEnd: 1, multiplier: 2 }
337
+ ],
338
+ };
339
+
340
+ const scatterManager = new ScatterEnhancementsManager(scatterConfig);
341
+
342
+ // Evaluate scatters on current grid
343
+ const scatterResult = scatterManager.evaluateScatters(gameState);
344
+
345
+ if (scatterResult.success) {
346
+ console.log(`Found ${scatterResult.scatterCount} scatters`);
347
+ console.log(`Scatter pays: ${scatterResult.scatterPays}`);
348
+
349
+ // Process triggers
350
+ scatterResult.triggers.forEach(trigger => {
351
+ if (trigger.triggered) {
352
+ console.log(`Triggered: ${trigger.type} - ${trigger.featureType}`);
353
+
354
+ if (trigger.awardedSpins) {
355
+ console.log(`Awarded ${trigger.awardedSpins} free spins`);
356
+ }
357
+ }
358
+ });
359
+
360
+ // Send scatter data to client
361
+ ws.send(JSON.stringify({
362
+ type: 'SCATTER_EVALUATED',
363
+ data: {
364
+ positions: scatterResult.positions,
365
+ triggers: scatterResult.triggers,
366
+ scatterPays: scatterResult.scatterPays,
367
+ }
368
+ }));
369
+ }
370
+
371
+ // Manage scatter collection system
372
+ let collectedData = { count: 0, values: [], spinsRemaining: 20 };
373
+ const { updated, collectionTrigger } = scatterManager.manageScatterCollection(
374
+ scatterResult,
375
+ collectedData
376
+ );
377
+ collectedData = updated;
378
+
379
+ if (collectionTrigger) {
380
+ console.log('Collection bonus triggered!');
381
+ // Award collection bonus
382
+ }
383
+ ```
384
+
385
+ ### Respins / Hold & Win
386
+
387
+ ```ts
388
+ import { RespinsManager } from 'slot-server';
389
+
390
+ const respinsConfig = {
391
+ enabled: true,
392
+ initialRespins: 3,
393
+ triggerType: 'symbol_count' as const,
394
+ minTriggerSymbols: 6,
395
+ symbolWeights: [
396
+ { symbolId: 'money_1', probability: 0.4, baseValue: 1, isMoneySymbol: true },
397
+ { symbolId: 'money_5', probability: 0.25, baseValue: 5, isMoneySymbol: true },
398
+ { symbolId: 'money_10', probability: 0.15, baseValue: 10, isMoneySymbol: true },
399
+ { symbolId: 'reset', probability: 0.08, isResetSymbol: true },
400
+ { symbolId: 'jackpot_mini', probability: 0.05, isJackpotSymbol: true, jackpotType: 'mini' as const, baseValue: 25 },
401
+ { symbolId: 'jackpot_major', probability: 0.02, isJackpotSymbol: true, jackpotType: 'major' as const, baseValue: 100 },
402
+ ],
403
+ endOnNoSpecial: true,
404
+ fullScreenBonus: { multiplier: 3 },
405
+ };
406
+
407
+ const respinsManager = new RespinsManager(respinsConfig);
408
+
409
+ // Check if respins should trigger
410
+ const shouldTrigger = respinsManager.checkTrigger(gameState);
411
+
412
+ if (shouldTrigger) {
413
+ console.log('Hold & Win feature triggered!');
414
+
415
+ // Execute the respins feature
416
+ const respinsResult = respinsManager.execute(gameState);
417
+
418
+ console.log(`Final value: ${respinsResult.finalValue}`);
419
+ console.log(`Total respins used: ${respinsResult.totalRespinsUsed}`);
420
+ console.log(`Jackpots collected: ${respinsResult.jackpotsCollected.join(', ')}`);
421
+
422
+ // Send step-by-step updates to client for animation
423
+ respinsResult.steps.forEach((step, index) => {
424
+ ws.send(JSON.stringify({
425
+ type: 'RESPINS_STEP',
426
+ data: {
427
+ stepNumber: index + 1,
428
+ respinsRemaining: step.respinsRemaining,
429
+ lockedPositions: step.lockedPositions,
430
+ newSymbols: step.newSymbols,
431
+ valuesAdded: step.valuesAdded,
432
+ jackpotsAwarded: step.jackpotsAwarded,
433
+ reason: step.reason,
434
+ }
435
+ }));
436
+ });
437
+
438
+ // Send final result
439
+ ws.send(JSON.stringify({
440
+ type: 'RESPINS_COMPLETE',
441
+ data: {
442
+ totalValue: respinsResult.finalValue,
443
+ jackpotsCollected: respinsResult.jackpotsCollected,
444
+ totalRespinsUsed: respinsResult.totalRespinsUsed,
445
+ }
446
+ }));
447
+ }
448
+
449
+ // Calculate potential payout for RTP verification
450
+ const potentialPayout = respinsManager.calculatePotentialPayout(betAmount);
451
+ console.log(`Expected respins payout: ${potentialPayout}`);
452
+ ```
453
+
454
+ ### Audit System
455
+
456
+ ```ts
457
+ import { AuditManager, InMemoryAuditStorage } from 'slot-server';
458
+
459
+ // Create audit manager with in-memory storage (use database in production)
460
+ const auditStorage = new InMemoryAuditStorage();
461
+ const auditManager = new AuditManager('my-game', auditStorage, 'operator-123');
462
+
463
+ // Record each spin for compliance
464
+ await auditManager.recordSpin({
465
+ spinId: 'spin-uuid-here',
466
+ playerId: 'player-123',
467
+ sessionId: 'session-456',
468
+ stake: 5.00,
469
+ win: 10.50,
470
+ balanceBefore: 100.00,
471
+ balanceAfter: 105.50,
472
+ rngSeed: 'abc123xyz',
473
+ result: [[...]], // 2D symbol array
474
+ gameMode: 0,
475
+ bonusTriggered: false,
476
+ jackpotContributions: [],
477
+ jackpotWins: [],
478
+ ipAddress: '192.168.1.1',
479
+ deviceInfo: 'Chrome on Windows',
480
+ location: 'US-NJ',
481
+ platform: 'web',
482
+ });
483
+
484
+ // Retrieve player history for dispute resolution
485
+ const records = await auditManager.getPlayerRecords(
486
+ 'player-123',
487
+ Date.now() - 24 * 60 * 60 * 1000, // Last 24 hours
488
+ Date.now()
489
+ );
490
+
491
+ // Get activity summary for reporting
492
+ const summary = await auditManager.getPlayerSummary(
493
+ 'player-123',
494
+ startTime,
495
+ endTime
496
+ );
497
+
498
+ // Export records for regulatory submission
499
+ const jsonExport = await auditManager.exportRecords(
500
+ 'player-123',
501
+ startTime,
502
+ endTime,
503
+ 'json' // or 'csv'
504
+ );
505
+
506
+ // Verify RNG integrity
507
+ const verification = await auditManager.verifyRngChain(spinIds);
508
+ if (!verification.valid) {
509
+ console.error('RNG chain verification failed:', verification.errors);
510
+ }
511
+ ```
512
+
513
+ ### Debug Mode
514
+
515
+ ```ts
516
+ import { DebugManager } from 'slot-server';
517
+
518
+ // Initialize debug manager
519
+ const debugManager = new DebugManager({
520
+ enabled: true,
521
+ logSpins: true,
522
+ logRngSeeds: true,
523
+ skipBalanceChecks: false,
524
+ verboseLevel: 'debug', // 'none' | 'errors' | 'warnings' | 'info' | 'debug'
525
+ outputFormat: 'console', // 'console' | 'json' | 'file'
526
+ });
527
+
528
+ // Force specific outcomes for testing
529
+ debugManager.configure({
530
+ forcedOutcomes: [
531
+ {
532
+ condition: { type: 'spinNumber', value: 10 },
533
+ result: {
534
+ symbols: [[1, 2, 3], [4, 5, 6], ...],
535
+ winAmount: 100,
536
+ bonusTriggered: true,
537
+ },
538
+ },
539
+ {
540
+ condition: { type: 'betAmount', value: 50 },
541
+ result: {
542
+ winAmount: 500,
543
+ jackpotWon: 'mini',
544
+ },
545
+ },
546
+ ],
547
+ });
548
+
549
+ // Check for forced outcome before spin
550
+ const forcedResult = debugManager.checkForcedOutcome({
551
+ spinNumber: 10,
552
+ rngSeed: 'abc123',
553
+ betAmount: 50,
554
+ playerId: 'player-123',
555
+ });
556
+
557
+ if (forcedResult) {
558
+ console.log('Using forced outcome for testing');
559
+ return forcedResult;
560
+ }
561
+
562
+ // Log spin results
563
+ debugManager.logSpin('spin-uuid', spinResult, 'rng-seed-here');
564
+
565
+ // Get debug state and statistics
566
+ const state = debugManager.getState();
567
+ const stats = debugManager.getStatistics();
568
+
569
+ console.log(`Spins tracked: ${stats.spinsTracked}`);
570
+ console.log(`Forced outcomes applied: ${stats.forcedOutcomesApplied}`);
571
+ console.log(`Errors logged: ${stats.errorCount}`);
572
+
573
+ // Export logs for analysis
574
+ const logs = debugManager.exportLogs();
575
+
576
+ // Reset debug state
577
+ debugManager.reset();
578
+ ```
579
+
580
+ ## Architecture
581
+
582
+ The library is designed to work seamlessly with the Slot Engine client:
583
+
584
+ ```
585
+ ┌─────────────────┐ WebSocket ┌──────────────────┐
586
+ │ Slot Server │ ◄──────────────────────► │ Slot Engine │
587
+ │ (This Library) │ │ (Client) │
588
+ ├─────────────────┤ ├──────────────────┤
589
+ │ • GameSession │ │ • ReelRenderer │
590
+ │ • SpinEvaluator │ │ • UIManager │
591
+ │ • BonusManager │◄──── Game State ───────► │ • PaylineRender │
592
+ │ • JackpotMgr │ │ • ParticleSys │
593
+ │ • HistoryMgr │ │ • SoundManager │
594
+ │ • CascadeMgr │ │ • CascadeAnim │
595
+ │ • WildsMgr │ │ • WildEffects │
596
+ │ • ScatterMgr │ │ • ScatterFX │
597
+ └─────────────────┘ └──────────────────┘
598
+ ```
599
+
600
+ ## Message Protocol
601
+
602
+ All communication follows a standard WebSocket message format:
603
+
604
+ ```ts
605
+ // Client → Server
606
+ {
607
+ type: 'SPIN',
608
+ data: {
609
+ action: 'spin',
610
+ stakePerLine: 1,
611
+ selectedLines: 5,
612
+ totalStake: 5,
613
+ gameMode: 0
614
+ },
615
+ messageId: 'uuid-here',
616
+ timestamp: 1234567890
617
+ }
618
+
619
+ // Server → Client
620
+ {
621
+ type: 'SPIN',
622
+ data: {
623
+ result: [[...]],
624
+ totalWin: 10.50,
625
+ winLines: [...],
626
+ balanceAfter: 95.50,
627
+ bonusTriggered: false
628
+ },
629
+ messageId: 'uuid-here',
630
+ timestamp: 1234567890
631
+ }
632
+ ```
633
+
634
+ ## Responsible Gaming
635
+
636
+ The library includes comprehensive responsible gaming features through the `ResponsibleGamingManager`:
637
+
638
+ - **Session Time Limits** - Track and limit play duration with warnings at 80% threshold
639
+ - **Loss Limits** - Stop play when loss threshold reached with automatic blocking
640
+ - **Win Goals** - Notify players when they reach their win targets
641
+ - **Reality Checks** - Periodic reminders showing time played, stakes, wins, and net result
642
+ - **Self-Exclusion** - Block access during exclusion period with date validation
643
+ - **Bet Limits** - Enforce maximum bet amounts
644
+ - **Activity Monitoring** - Track player statistics and generate alerts
645
+ - **RG Reports** - Export player activity reports for transparency
646
+
647
+ ### Usage Example
648
+
649
+ ```ts
650
+ import { ResponsibleGamingManager } from 'slot-server';
651
+
652
+ // Initialize with limits and player state
653
+ const limits = {
654
+ sessionTimeLimit: 60, // minutes
655
+ lossLimit: 100,
656
+ winGoal: 500,
657
+ maxBetLimit: 10,
658
+ realityCheckInterval: 15,
659
+ };
660
+
661
+ const playerState = {
662
+ balance: 1000,
663
+ bet: 1,
664
+ gameMode: 0 as const,
665
+ freeSpinsRemaining: 0,
666
+ bonusActive: false,
667
+ history: [],
668
+ };
669
+
670
+ const rgManager = new ResponsibleGamingManager(limits, playerState);
671
+
672
+ // Check if player can place a bet before spinning
673
+ const canBet = rgManager.canPlaceBet(5);
674
+ if (!canBet.success) {
675
+ console.error(`Bet rejected: ${canBet.error}`);
676
+ return;
677
+ }
678
+
679
+ // Apply spin results to tracking
680
+ rgManager.applyResult(stake, win);
681
+
682
+ // Update player state after each spin
683
+ rgManager.updatePlayerState({
684
+ sessionLoss: updatedLoss,
685
+ sessionWin: updatedWin,
686
+ });
687
+
688
+ // Check for reality check
689
+ if (rgManager.shouldShowRealityCheck()) {
690
+ const realityCheck = rgManager.generateRealityCheck();
691
+ // Send to client for display
692
+ ws.send(JSON.stringify({ type: 'REALITY_CHECK', data: realityCheck }));
693
+ }
694
+
695
+ // Get active alerts
696
+ const alerts = rgManager.getAlerts();
697
+ alerts.forEach(alert => {
698
+ if (alert.actionRequired) {
699
+ // Critical alert requiring immediate action
700
+ console.warn(`${alert.type}: ${alert.message}`);
701
+ }
702
+ });
703
+
704
+ // Get player activity statistics
705
+ const stats = rgManager.getPlayerActivityStats();
706
+ console.log('Player Stats:', stats);
707
+
708
+ // Export RG report for player download
709
+ const report = rgManager.exportRGReport();
710
+ ```
711
+
712
+ ### Alert Types
713
+
714
+ The manager generates alerts with different severity levels:
715
+
716
+ - **info** - Win goals reached, general notifications
717
+ - **warning** - Approaching limits (80% threshold)
718
+ - **critical** - Limits reached, self-exclusion active (requires action)
719
+
720
+ ### Self-Exclusion
721
+
722
+ ```ts
723
+ // Set self-exclusion until a specific date
724
+ rgManager.updateLimits({
725
+ selfExclusionUntil: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(), // 30 days
726
+ });
727
+
728
+ // Check exclusion status
729
+ const status = rgManager.getSelfExclusionStatus();
730
+ if (status.isExcluded) {
731
+ console.log(`Excluded until ${status.exclusionUntil}, ${status.remainingDays} days remaining`);
732
+ }
733
+ ```
734
+
735
+ ### Session Management
736
+
737
+ ```ts
738
+ // Reset session tracking when player starts new session
739
+ rgManager.resetSession();
740
+
741
+ // Get session time info
742
+ const timeInfo = rgManager.getSessionTimeInfo();
743
+ console.log(`Playing for ${timeInfo.elapsedMinutes} minutes`);
744
+ console.log(`Limit: ${timeInfo.limitMinutes} minutes`);
745
+ console.log(`Warning reached: ${timeInfo.warningThresholdReached}`);
746
+ ```
747
+
748
+ ## Audit & Compliance
749
+
750
+ The `AuditManager` provides complete regulatory compliance:
751
+
752
+ ```ts
753
+ import { AuditManager, InMemoryAuditStorage } from 'slot-server';
754
+
755
+ const auditStorage = new InMemoryAuditStorage();
756
+ const auditManager = new AuditManager(auditStorage);
757
+
758
+ // Record every spin
759
+ await auditManager.recordSpin({
760
+ spinId: 'spin-123',
761
+ playerId: 'player-456',
762
+ gameId: 'my-slot',
763
+ stake: 5,
764
+ win: 0,
765
+ balanceBefore: 1000,
766
+ balanceAfter: 995,
767
+ rngSeed: 'abc123',
768
+ result: [[...], ...],
769
+ jackpotContributions: [],
770
+ jackpotWins: [],
771
+ });
772
+
773
+ // Retrieve player history for disputes
774
+ const history = await auditManager.getPlayerHistory('player-456', startTime, endTime);
775
+
776
+ // Generate activity summary for reporting
777
+ const summary = await auditManager.getActivitySummary('player-456', lastMonthStart, lastMonthEnd);
778
+
779
+ // Export records for regulators
780
+ const jsonExport = await auditManager.exportRecords('json');
781
+ const csvExport = await auditManager.exportRecords('csv');
782
+ ```
783
+
784
+ ## Debug Mode
785
+
786
+ The `DebugManager` provides comprehensive testing tools:
787
+
788
+ ```ts
789
+ import { DebugManager } from 'slot-server';
790
+
791
+ const debugManager = new DebugManager({
792
+ enabled: true,
793
+ logSpins: true,
794
+ logRngSeeds: true,
795
+ skipBalanceChecks: true, // For testing only
796
+ verboseLevel: 'debug',
797
+ outputFormat: 'console',
798
+ });
799
+
800
+ // Force specific outcomes for testing
801
+ debugManager.setForcedOutcome({
802
+ condition: { type: 'spinNumber', value: 10 },
803
+ result: { bonusTriggered: true, winAmount: 500 },
804
+ });
805
+
806
+ // Log spin results
807
+ debugManager.logSpin(spinRequest, spinResult, 'info');
808
+
809
+ // Get debug statistics
810
+ const stats = debugManager.getStatistics();
811
+ console.log(`Total spins: ${stats.spinCount}`);
812
+
813
+ // Export logs for analysis
814
+ const logs = debugManager.exportLogs('json');
815
+ ```
816
+
817
+ ## Wallet & Transaction Management
818
+ The `WalletManager` provides a database-agnostic, atomic transaction system designed for real-money gaming. It ensures financial integrity through double-entry ledger logic (recording `balanceBefore` and `balanceAfter` for every operation) and optimistic locking to prevent race conditions.
819
+
820
+ ### Key Features
821
+ - Atomic Transactions: All balance updates are wrapped in transaction blocks with rollback support.
822
+ - Optimistic Locking: Prevents concurrent modification issues using version checking.
823
+ - Double-Entry Ledger: Immutable history of every credit and debit.
824
+ - Pluggable Storage: Implements the `IWalletStorage` interface to work with any database (PostgreSQL, MongoDB, Redis, etc.).
825
+ - Precision Handling: Configurable decimal precision for different currencies.
826
+
827
+ ### Basic Usage
828
+ ```ts
829
+ import { WalletManager, InMemoryWalletStorage } from 'slot-server';
830
+
831
+ // 1. Initialize Storage (Use InMemoryWalletStorage for testing/dev)
832
+ const storage = new InMemoryWalletStorage();
833
+
834
+ // 2. Initialize Manager
835
+ const walletManager = new WalletManager(storage, {
836
+ currency: 'USD',
837
+ precision: 2,
838
+ allowNegativeBalance: false
839
+ });
840
+
841
+ // 3. Create a wallet for a player
842
+ await walletManager.createWallet('player-123', '100.00');
843
+
844
+ // 4. Place a Bet (Debit)
845
+ const betResult = await walletManager.placeBet('player-123', '10.00', {
846
+ referenceId: 'spin-uuid-999',
847
+ gameId: 'slot-game-01'
848
+ });
849
+
850
+ if (!betResult.success) {
851
+ console.error('Bet failed:', betResult.error);
852
+ return;
853
+ }
854
+
855
+ console.log(`New Balance: ${betResult.balance}`);
856
+
857
+ // 5. Process a Payout (Credit)
858
+ const winAmount = '25.00';
859
+ const payoutResult = await walletManager.credit('player-123', winAmount, 'payout', {
860
+ referenceId: 'spin-uuid-999',
861
+ description: 'Line hit payout'
862
+ });
863
+ ```
864
+
865
+ ### Implementing Custom Storage
866
+ To integrate with your production database, implement the `IWalletStorage` interface:
867
+ ```ts
868
+ import { IWalletStorage, IWalletBalance, ITransaction } from 'slot-server';
869
+ import { db, userBalances, transactions } from './my-database-schema'; // Your DB schema
870
+
871
+ class PostgresWalletStorage implements IWalletStorage {
872
+ async getBalance(userId: string): Promise<IWalletBalance | null> {
873
+ const result = await db.select().from(userBalances).where({ userId });
874
+ return result[0] || null;
875
+ }
876
+
877
+ async saveTransaction(tx: ITransaction): Promise<void> {
878
+ await db.insert(transactions).values({
879
+ id: tx.id,
880
+ userId: tx.userId,
881
+ type: tx.type,
882
+ amount: tx.amount,
883
+ balanceBefore: tx.balanceBefore,
884
+ balanceAfter: tx.balanceAfter,
885
+ referenceId: tx.referenceId,
886
+ createdAt: new Date(tx.createdAt)
887
+ });
888
+ }
889
+
890
+ async updateBalance(userId: string, newBalance: string, version: number): Promise<boolean> {
891
+ // Implement optimistic locking here
892
+ const result = await db
893
+ .update(userBalances)
894
+ .set({ balance: newBalance, updatedAt: new Date() })
895
+ .where({ userId, version }) // 'version' column required for locking
896
+ .returning();
897
+
898
+ return result.length > 0;
899
+ }
900
+
901
+ // ... implement other required methods
902
+ }
903
+ ```
904
+
905
+ ### Integration with Game Session
906
+ A common pattern is to wrap the game spin cycle with wallet operations:
907
+ ```ts
908
+ async function handleSpin(sessionId: string, playerId: string, betAmount: string) {
909
+ // 1. Debit Bet
910
+ const betTx = await walletManager.placeBet(playerId, betAmount, { referenceId: sessionId });
911
+ if (!betTx.success) throw new Error('Insufficient funds');
912
+
913
+ try {
914
+ // 2. Evaluate Game Logic
915
+ const gameResult = slotEngine.spin(betAmount);
916
+
917
+ // 3. Credit Wins (if any)
918
+ if (gameResult.totalWin > 0) {
919
+ await walletManager.credit(playerId, gameResult.totalWin.toString(), 'payout', {
920
+ referenceId: sessionId
921
+ });
922
+ }
923
+
924
+ return gameResult;
925
+ } catch (error) {
926
+ // 4. Rollback on Critical Error (Optional depending on requirements)
927
+ await walletManager.refund(playerId, betAmount, 'error_refund', { referenceId: sessionId });
928
+ throw error;
929
+ }
930
+ }
931
+ ```
932
+ ### Transaction Types
933
+ The system supports the following standard transaction types:
934
+ - `bet`: Wager placement
935
+ - `payout`: Standard win payment
936
+ - `jackpot_win`: Progressive jackpot award
937
+ - `bonus_payout`: Feature round winnings
938
+ - `deposit`: External funds added
939
+ - `withdrawal`: External funds added
940
+ - `fee`: House fee or commission
941
+ - `refund`: Reversal of a previous transaction
942
+
943
+ ## API Reference
944
+
945
+ See the generated TypeScript definitions (`dist/index.d.ts`) for complete API documentation.
946
+
947
+ ## License
948
+
949
+ MIT