@omnitronix/happy-panda-game-engine 0.0.2 → 0.0.3
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 +584 -120
- package/dist/happy-panda-v1.game-engine.js +37 -39
- package/dist/happy-panda-v1.game-engine.js.map +1 -1
- package/dist/index.js +7 -1
- package/dist/index.js.map +1 -1
- package/dist/rng/dummy-rng-client.js +77 -0
- package/dist/rng/dummy-rng-client.js.map +1 -0
- package/dist/rng/index.js +5 -0
- package/dist/rng/index.js.map +1 -1
- package/dist/rng/rng-client.factory.js +57 -0
- package/dist/rng/rng-client.factory.js.map +1 -0
- package/dist/rng/rng-client.interface.js +9 -0
- package/dist/rng/rng-client.interface.js.map +1 -0
- package/dist/rng/rng-service.js +124 -0
- package/dist/rng/rng-service.js.map +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,13 +1,67 @@
|
|
|
1
1
|
# Happy Panda Game Engine
|
|
2
2
|
|
|
3
|
-

|
|
4
4
|

|
|
5
|
-

|
|
5
|
+

|
|
7
6
|
|
|
8
|
-
|
|
7
|
+
Pure business logic library for the Happy Panda (Cherry Master) slot game. A framework-agnostic, TypeScript-based game engine that handles all core game mechanics, bonus systems, and jackpot logic. Achieves C++ parity with the original `CherryMaster_A_2.cpp` implementation.
|
|
9
8
|
|
|
10
|
-
##
|
|
9
|
+
## What is Happy Panda Game Engine?
|
|
10
|
+
|
|
11
|
+
The Happy Panda Game Engine is an isolated, framework-agnostic library containing pure business logic for the Happy Panda slot game. It implements a command-based architecture that separates game logic from presentation and infrastructure concerns.
|
|
12
|
+
|
|
13
|
+
### Core Characteristics
|
|
14
|
+
|
|
15
|
+
- **Pure Business Logic**: Contains only game rules, calculations, and state transitions
|
|
16
|
+
- **Framework Agnostic**: No dependencies on specific UI frameworks or web servers
|
|
17
|
+
- **Command-Based Architecture**: All interactions through well-defined commands
|
|
18
|
+
- **State Separation**: Distinguishes between public state (visible to players) and private state (server-side only)
|
|
19
|
+
- **RNG Integration**: Supports pluggable RNG providers for testing and production
|
|
20
|
+
- **Audit Trail**: Complete RNG outcome tracking for compliance and debugging
|
|
21
|
+
- **C++ Parity**: Verified against original C++ implementation with 100M spin validation
|
|
22
|
+
|
|
23
|
+
### Game Specifications
|
|
24
|
+
|
|
25
|
+
- **RTP**: 96.05% (verified at 95.91% over 100M spins, within tolerance)
|
|
26
|
+
- **Game Type**: Classic 3x3 Slot
|
|
27
|
+
- **Version**: 1.0.0
|
|
28
|
+
- **Layout**: 3 reels x 3 rows
|
|
29
|
+
- **Win Evaluation**: 8/16 bidirectional paylines + wall wins + scatter wins
|
|
30
|
+
- **Modes**: 8-line (SINGLE) and 16-line (BOTH)
|
|
31
|
+
|
|
32
|
+
## RTP Validation Results
|
|
33
|
+
|
|
34
|
+
### 100 Million Spin Verification
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
================================================================
|
|
38
|
+
HAPPY PANDA RTP VERIFICATION - 100 MILLION SPINS
|
|
39
|
+
================================================================
|
|
40
|
+
|
|
41
|
+
Configuration:
|
|
42
|
+
- Game Direction: SINGLE (8 lines)
|
|
43
|
+
- Bet per spin: 8
|
|
44
|
+
- Seeds: 10 x 10M = 100M total paid spins
|
|
45
|
+
- C++ Target RTP: 96.05%
|
|
46
|
+
|
|
47
|
+
RTP Breakdown by Spin Type:
|
|
48
|
+
----------------------------------------------------------------
|
|
49
|
+
PAID_SPIN : 62.54% | C++: 63.51% | Diff: -0.97%
|
|
50
|
+
BONUS_JACKPOT : 16.24% | C++: 13.56% | Diff: +2.68%
|
|
51
|
+
BONUS_CHERRY : 3.96% | C++: 3.67% | Diff: +0.29%
|
|
52
|
+
BONUS_BELL : 5.06% | C++: 5.83% | Diff: -0.77%
|
|
53
|
+
BONUS_BAR1 : 5.47% | C++: 6.31% | Diff: -0.84%
|
|
54
|
+
RESPIN_CHERRY : 2.64% | C++: 3.18% | Diff: -0.54%
|
|
55
|
+
----------------------------------------------------------------
|
|
56
|
+
TOTAL : 95.91% | C++: 96.05%
|
|
57
|
+
|
|
58
|
+
Statistics:
|
|
59
|
+
- Standard Deviation: 0.32%
|
|
60
|
+
- Range: 95.54% - 96.40%
|
|
61
|
+
|
|
62
|
+
STATUS: PASS - RTP within 0.15% tolerance
|
|
63
|
+
================================================================
|
|
64
|
+
```
|
|
11
65
|
|
|
12
66
|
| Metric | TypeScript | C++ Target | Difference | Status |
|
|
13
67
|
|--------|------------|------------|------------|--------|
|
|
@@ -21,13 +75,243 @@ TypeScript implementation of the Happy Red Panda (Cherry Master) slot game math
|
|
|
21
75
|
|
|
22
76
|
**No configuration values from XLSX were modified** - only calculation logic was adjusted to match C++ behavior.
|
|
23
77
|
|
|
24
|
-
##
|
|
78
|
+
## Features
|
|
25
79
|
|
|
26
|
-
- **
|
|
27
|
-
- **
|
|
28
|
-
- **
|
|
29
|
-
- **
|
|
30
|
-
- **
|
|
80
|
+
- **Command-Based Architecture**: 3 different command types for all game operations
|
|
81
|
+
- **State Management**: Public/private state separation with type safety
|
|
82
|
+
- **RNG Integration**: Pluggable RNG providers with audit trail support
|
|
83
|
+
- **Bidirectional Paylines**: 8 or 16 paylines with left-to-right and right-to-left evaluation
|
|
84
|
+
- **Wall Wins**: Full 3x3 matrix wins with multiple types (pure, mixed, fruits, colors)
|
|
85
|
+
- **Scatter Wins**: Seven symbols pay anywhere on screen
|
|
86
|
+
- **Bonus System**: 5 distinct bonus types with counter-based triggers
|
|
87
|
+
- **Jackpot System**: Progressive and pool jackpots
|
|
88
|
+
- **TypeScript Support**: Complete type definitions for all interfaces
|
|
89
|
+
- **Comprehensive Testing**: Full test suite with Jest and 100M spin RTP validation
|
|
90
|
+
|
|
91
|
+
## Architecture Overview
|
|
92
|
+
|
|
93
|
+
The game engine follows a modular architecture with clear separation of concerns:
|
|
94
|
+
|
|
95
|
+
### Core Components
|
|
96
|
+
|
|
97
|
+
- **Game Engine** (`HappyPandaEngine`): Main entry point and command processor
|
|
98
|
+
- **V1 Wrapper** (`HappyPandaV1GameEngine`): Service integration wrapper with GameEngine interface
|
|
99
|
+
- **Spin Generator**: Grid generation with weighted random selection
|
|
100
|
+
- **Win Evaluator**: Line, wall, scatter, and special win detection
|
|
101
|
+
- **Counter Manager**: Bonus counter management and trigger logic
|
|
102
|
+
- **Jackpot Manager**: Progressive and pool jackpot handling
|
|
103
|
+
|
|
104
|
+
### Directory Structure
|
|
105
|
+
|
|
106
|
+
```
|
|
107
|
+
src/
|
|
108
|
+
├── happy-panda-v1.game-engine.ts # V1 service wrapper
|
|
109
|
+
├── engine/
|
|
110
|
+
│ └── happy-panda-engine.ts # Main engine class
|
|
111
|
+
├── config/
|
|
112
|
+
│ └── happy-panda.config.ts # All game configuration (XLSX values)
|
|
113
|
+
├── rng/
|
|
114
|
+
│ ├── spin-generator.ts # Grid generation with C++ parity
|
|
115
|
+
│ └── weighted-random.ts # Weighted random selection
|
|
116
|
+
├── logic/
|
|
117
|
+
│ ├── handlers/
|
|
118
|
+
│ │ └── spin-handler.ts # Spin orchestration
|
|
119
|
+
│ └── services/
|
|
120
|
+
│ ├── win-evaluator.ts # Win detection (line/wall/scatter)
|
|
121
|
+
│ ├── counter-manager.ts # Bonus counter management
|
|
122
|
+
│ └── jackpot-manager.ts # Jackpot handling
|
|
123
|
+
├── domain/
|
|
124
|
+
│ └── types.ts # Type definitions
|
|
125
|
+
└── __tests__/
|
|
126
|
+
├── rtp-simulation.test.ts # RTP validation
|
|
127
|
+
├── rtp-diagnostic.test.ts # RTP breakdown by spin type
|
|
128
|
+
├── cpp-parity.test.ts # C++ parity tests
|
|
129
|
+
└── win-evaluator.test.ts # Win evaluation tests
|
|
130
|
+
|
|
131
|
+
docs/
|
|
132
|
+
├── RTP-MATCHING.md # RTP implementation details
|
|
133
|
+
└── TEST-PROTOCOL-RTP-100M.md # 100M spin test protocol
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Installation
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
npm install @omnitronix/happy-panda-game-engine
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
> **Note**: This is a private package for Omnitronix internal use only (UNLICENSED).
|
|
143
|
+
|
|
144
|
+
## Quick Start
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
import { HappyPandaV1GameEngine } from '@omnitronix/happy-panda-game-engine';
|
|
148
|
+
|
|
149
|
+
// Initialize game engine
|
|
150
|
+
const gameEngine = new HappyPandaV1GameEngine();
|
|
151
|
+
|
|
152
|
+
// Get engine info
|
|
153
|
+
const info = gameEngine.getGameEngineInfo();
|
|
154
|
+
// { gameCode: 'happy-panda', version: '1.0.0', rtp: 96.05, gameType: 'slot', gameName: 'Happy Panda', provider: 'Omnitronix' }
|
|
155
|
+
|
|
156
|
+
// Initialize session
|
|
157
|
+
const initCommand = {
|
|
158
|
+
id: 'cmd-init-123',
|
|
159
|
+
type: 'INIT_SESSION_STATE',
|
|
160
|
+
payload: {
|
|
161
|
+
gameDirection: 0, // 0 = SINGLE (8 lines), 1 = BOTH (16 lines)
|
|
162
|
+
betStake: 1
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const initResult = await gameEngine.processCommand(null, null, initCommand);
|
|
167
|
+
// Returns: { success: true, publicState, privateState, outcome, rngOutcome }
|
|
168
|
+
|
|
169
|
+
// Process spin
|
|
170
|
+
const spinCommand = {
|
|
171
|
+
id: 'cmd-spin-456',
|
|
172
|
+
type: 'SPIN',
|
|
173
|
+
payload: {}
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const spinResult = await gameEngine.processCommand(
|
|
177
|
+
initResult.publicState,
|
|
178
|
+
initResult.privateState,
|
|
179
|
+
spinCommand
|
|
180
|
+
);
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## API Reference
|
|
184
|
+
|
|
185
|
+
### Main Class: `HappyPandaV1GameEngine`
|
|
186
|
+
|
|
187
|
+
Implements the standard `GameEngine` interface for integration with game-engine-service.
|
|
188
|
+
|
|
189
|
+
#### Constructor
|
|
190
|
+
|
|
191
|
+
```typescript
|
|
192
|
+
new HappyPandaV1GameEngine()
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
- Initializes game engine with tracked RNG provider
|
|
196
|
+
- Ready to process commands immediately
|
|
197
|
+
|
|
198
|
+
#### Methods
|
|
199
|
+
|
|
200
|
+
**`getGameEngineInfo(): GameEngineInfo`**
|
|
201
|
+
Returns game metadata including code, version, RTP, and provider.
|
|
202
|
+
|
|
203
|
+
```typescript
|
|
204
|
+
{
|
|
205
|
+
gameCode: 'happy-panda',
|
|
206
|
+
version: '1.0.0',
|
|
207
|
+
rtp: 96.05,
|
|
208
|
+
gameType: 'slot',
|
|
209
|
+
gameName: 'Happy Panda',
|
|
210
|
+
provider: 'Omnitronix'
|
|
211
|
+
}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
**`processCommand(publicState, privateState, command): Promise<CommandProcessingResult>`**
|
|
215
|
+
Main command processor that handles all game operations.
|
|
216
|
+
|
|
217
|
+
### Commands
|
|
218
|
+
|
|
219
|
+
The game engine supports 3 different command types:
|
|
220
|
+
|
|
221
|
+
#### 1. INIT_SESSION_STATE
|
|
222
|
+
|
|
223
|
+
**Purpose**: Initialize game session state
|
|
224
|
+
|
|
225
|
+
**Payload**:
|
|
226
|
+
```typescript
|
|
227
|
+
{
|
|
228
|
+
gameDirection?: number; // 0 = SINGLE (8 lines), 1 = BOTH (16 lines). Default: 0
|
|
229
|
+
betStake?: number; // Bet multiplier. Default: 1
|
|
230
|
+
}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
**Returns**: Initial public and private state with default values
|
|
234
|
+
|
|
235
|
+
#### 2. SPIN
|
|
236
|
+
|
|
237
|
+
**Purpose**: Execute a spin
|
|
238
|
+
|
|
239
|
+
**Payload**: None required (uses current state)
|
|
240
|
+
|
|
241
|
+
**Returns**: Spin result with grid, wins, and state updates
|
|
242
|
+
|
|
243
|
+
**Result Structure**:
|
|
244
|
+
```typescript
|
|
245
|
+
{
|
|
246
|
+
success: true,
|
|
247
|
+
publicState: PublicState,
|
|
248
|
+
privateState: PrivateState,
|
|
249
|
+
outcome: {
|
|
250
|
+
sessionId: string,
|
|
251
|
+
grid: Symbol[][], // 3x3 grid
|
|
252
|
+
wins: SpinWinResult, // All wins
|
|
253
|
+
state: PublicState,
|
|
254
|
+
jackpotWon: number, // Jackpot payout (if any)
|
|
255
|
+
poolJackpotWon: number, // Pool jackpot payout (if any)
|
|
256
|
+
bonusTriggered: SpinType | null,
|
|
257
|
+
isBonusComplete: boolean
|
|
258
|
+
},
|
|
259
|
+
rngOutcome: RngOutcome // Audit trail
|
|
260
|
+
}
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
#### 3. GET_SYMBOLS
|
|
264
|
+
|
|
265
|
+
**Purpose**: Retrieve symbol definitions
|
|
266
|
+
|
|
267
|
+
**Payload**: None
|
|
268
|
+
|
|
269
|
+
**Returns**: Array of symbol definitions with names and values
|
|
270
|
+
|
|
271
|
+
### State Types
|
|
272
|
+
|
|
273
|
+
#### PublicState (visible to player)
|
|
274
|
+
|
|
275
|
+
```typescript
|
|
276
|
+
{
|
|
277
|
+
gameDirection: GameDirection; // 0 = SINGLE, 1 = BOTH
|
|
278
|
+
betStake: number; // Bet multiplier
|
|
279
|
+
betGame: number; // Total bet (8 or 16 x betStake)
|
|
280
|
+
currentSpinType: SpinType; // Current spin type
|
|
281
|
+
spinsRemaining: number; // Remaining bonus spins
|
|
282
|
+
grid: Symbol[][]; // Current 3x3 grid
|
|
283
|
+
bonusJackpotValue: number; // Progressive jackpot
|
|
284
|
+
poolJackpotValue: number; // Pool jackpot
|
|
285
|
+
hasPendingBonus: boolean; // Indicates pending bonus
|
|
286
|
+
}
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
#### PrivateState (server-side only)
|
|
290
|
+
|
|
291
|
+
```typescript
|
|
292
|
+
{
|
|
293
|
+
counters: BonusCounters; // Bonus trigger counters
|
|
294
|
+
pendingBonuses: PendingBonuses; // Queued bonuses
|
|
295
|
+
nextSpinType: SpinType; // Next spin type
|
|
296
|
+
accumulatedBonusWins: number; // Bonus sequence wins
|
|
297
|
+
centerCherrySymbol: Symbol | null; // For respin feature
|
|
298
|
+
}
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
#### CommandProcessingResult
|
|
302
|
+
|
|
303
|
+
```typescript
|
|
304
|
+
{
|
|
305
|
+
success: boolean;
|
|
306
|
+
publicState: PublicState;
|
|
307
|
+
privateState: PrivateState;
|
|
308
|
+
outcome?: SpinResponse;
|
|
309
|
+
message?: string;
|
|
310
|
+
rngOutcome?: RngOutcome; // RNG audit trail
|
|
311
|
+
}
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
## Game Mechanics
|
|
31
315
|
|
|
32
316
|
### Symbol Set
|
|
33
317
|
|
|
@@ -46,148 +330,336 @@ TypeScript implementation of the Happy Red Panda (Cherry Master) slot game math
|
|
|
46
330
|
| 10 | CH_S | Cherry Special |
|
|
47
331
|
| 11 | BA_S | Super Bar |
|
|
48
332
|
|
|
49
|
-
|
|
333
|
+
### Win Types
|
|
334
|
+
|
|
335
|
+
#### 1. Line Wins
|
|
50
336
|
|
|
51
|
-
|
|
52
|
-
- 8 paylines (8-line mode) or 16 paylines (16-line mode)
|
|
337
|
+
- 8 paylines (SINGLE mode) or 16 paylines (BOTH mode)
|
|
53
338
|
- Evaluated left-to-right and right-to-left in 16-line mode
|
|
54
339
|
- Cherry pays on 1, 2, or 3 matches
|
|
340
|
+
- Line shapes follow classic 3x3 patterns
|
|
55
341
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
-
|
|
59
|
-
-
|
|
342
|
+
#### 2. Wall Wins (Matrix/Screen Wins)
|
|
343
|
+
|
|
344
|
+
- Full 3x3 of same symbol
|
|
345
|
+
- Special mixed types:
|
|
346
|
+
- **Cherry Mix**: CH + CH_S combinations
|
|
347
|
+
- **Any Bar Mix**: BA1 + BA2 + BA3 + BA_S
|
|
348
|
+
- **All Fruits**: ME + BE + PR + OR
|
|
349
|
+
- **All Colors**: Mixed symbol colors
|
|
350
|
+
- **Important**: Wall wins suppress line wins when active
|
|
351
|
+
|
|
352
|
+
#### 3. Scatter Wins (Seven)
|
|
60
353
|
|
|
61
|
-
### 3. Scatter Wins (Seven)
|
|
62
354
|
- 2-9 Seven symbols anywhere on screen
|
|
63
355
|
- Super Seven only = doubled payout
|
|
64
|
-
- **
|
|
356
|
+
- **Important**: Scatter wins suppress line wins when active
|
|
357
|
+
|
|
358
|
+
#### 4. Special Wins (Bonus modes only)
|
|
65
359
|
|
|
66
|
-
|
|
67
|
-
-
|
|
68
|
-
-
|
|
69
|
-
- Super Bar scatter (Bar1 Bonus)
|
|
360
|
+
- **Cherry Pieces**: Count of cherry pieces (Jackpot Bonus)
|
|
361
|
+
- **Bell Scatter**: Bell count anywhere (Bell Bonus)
|
|
362
|
+
- **Super Bar Scatter**: Super Bar count anywhere (Bar1 Bonus)
|
|
70
363
|
|
|
71
|
-
|
|
364
|
+
### Bonus System
|
|
72
365
|
|
|
73
|
-
|
|
366
|
+
Five distinct bonus types with counter-based triggers:
|
|
74
367
|
|
|
75
|
-
| Bonus | Trigger | Description |
|
|
76
|
-
|
|
77
|
-
| Jackpot | 3x Cherry on first 8 lines | Cherry pieces with jackpot chance |
|
|
78
|
-
| Cherry | Cherry pair counter
|
|
79
|
-
| Bell | Bell triple counter
|
|
80
|
-
| Bar1 | Bar1 triple counter
|
|
81
|
-
| Respin | Lone center cherry on losing spin | 1-2
|
|
368
|
+
| Bonus | Trigger | Spins | Description |
|
|
369
|
+
|-------|---------|-------|-------------|
|
|
370
|
+
| **Jackpot** | 3x Cherry on first 8 lines | 3 | Cherry pieces with progressive jackpot chance |
|
|
371
|
+
| **Cherry** | Cherry pair counter = 0 | Variable | Cherry respin feature |
|
|
372
|
+
| **Bell** | Bell triple counter = 0 | 5 | Bell scatter pays + pool jackpot |
|
|
373
|
+
| **Bar1** | Bar1 triple counter = 0 | 7 | Super Bar scatter pays |
|
|
374
|
+
| **Respin** | Lone center cherry on losing spin | 1-2 | Corner respin feature |
|
|
82
375
|
|
|
83
376
|
### Counter System
|
|
84
|
-
- Counters decrement on specific symbol combinations
|
|
85
|
-
- When counter reaches 0, bonus triggers
|
|
86
|
-
- Each bonus has different spin allocation
|
|
87
377
|
|
|
88
|
-
|
|
378
|
+
Each bonus has an associated counter that decrements on specific symbol combinations:
|
|
89
379
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
├── config/
|
|
95
|
-
│ └── happy-panda.config.ts # All game configuration (XLSX values)
|
|
96
|
-
├── rng/
|
|
97
|
-
│ ├── spin-generator.ts # Grid generation with C++ parity
|
|
98
|
-
│ └── weighted-random.ts # Weighted random selection
|
|
99
|
-
├── logic/
|
|
100
|
-
│ ├── handlers/
|
|
101
|
-
│ │ └── spin-handler.ts # Spin orchestration
|
|
102
|
-
│ └── services/
|
|
103
|
-
│ ├── win-evaluator.ts # Win detection
|
|
104
|
-
│ └── counter-manager.ts # Bonus counter management
|
|
105
|
-
├── domain/
|
|
106
|
-
│ └── types.ts # Type definitions
|
|
107
|
-
└── __tests__/
|
|
108
|
-
├── rtp-simulation.test.ts # RTP validation
|
|
109
|
-
├── rtp-diagnostic.test.ts # RTP breakdown by spin type
|
|
110
|
-
└── cpp-parity.test.ts # C++ parity tests
|
|
380
|
+
- **Jackpot Counter**: Decrements on specific patterns, triggers at 0
|
|
381
|
+
- **Cherry Counter**: Decrements on cherry pairs, resets to 6 or 9
|
|
382
|
+
- **Bell Counter**: Decrements on bell triples, resets to 3 or 5
|
|
383
|
+
- **Bar1 Counter**: Decrements on bar1 triples, resets to 1
|
|
111
384
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
385
|
+
### Jackpot System
|
|
386
|
+
|
|
387
|
+
Two jackpot types:
|
|
388
|
+
|
|
389
|
+
- **Progressive Jackpot** (`bonusJackpotValue`): Accumulates on losing paid spins, paid during Jackpot Bonus
|
|
390
|
+
- **Pool Jackpot** (`poolJackpotValue`): Accumulates separately, paid when Bell Bonus triggers
|
|
391
|
+
|
|
392
|
+
## Integration Examples
|
|
393
|
+
|
|
394
|
+
### Example 1: Basic RGS Integration
|
|
395
|
+
|
|
396
|
+
```typescript
|
|
397
|
+
import { HappyPandaV1GameEngine } from '@omnitronix/happy-panda-game-engine';
|
|
398
|
+
|
|
399
|
+
class GameSessionService {
|
|
400
|
+
private gameEngine: HappyPandaV1GameEngine;
|
|
401
|
+
|
|
402
|
+
constructor() {
|
|
403
|
+
this.gameEngine = new HappyPandaV1GameEngine();
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
async createSession(userId: string, gameDirection: number, betStake: number) {
|
|
407
|
+
const initCommand = {
|
|
408
|
+
id: `init-${userId}-${Date.now()}`,
|
|
409
|
+
type: 'INIT_SESSION_STATE',
|
|
410
|
+
payload: { gameDirection, betStake }
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
const result = await this.gameEngine.processCommand(null, null, initCommand);
|
|
414
|
+
|
|
415
|
+
// Store result.publicState in database (visible to player)
|
|
416
|
+
// Store result.privateState securely (server-side only)
|
|
417
|
+
// Store result.rngOutcome for audit trail
|
|
418
|
+
|
|
419
|
+
return {
|
|
420
|
+
sessionId: userId,
|
|
421
|
+
publicState: result.publicState,
|
|
422
|
+
// Never send privateState to client!
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
async processSpin(sessionId: string, publicState: any, privateState: any) {
|
|
427
|
+
const spinCommand = {
|
|
428
|
+
id: `spin-${sessionId}-${Date.now()}`,
|
|
429
|
+
type: 'SPIN',
|
|
430
|
+
payload: {}
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
const result = await this.gameEngine.processCommand(
|
|
434
|
+
publicState,
|
|
435
|
+
privateState,
|
|
436
|
+
spinCommand
|
|
437
|
+
);
|
|
438
|
+
|
|
439
|
+
// Update states in database
|
|
440
|
+
// Log RNG outcome for compliance
|
|
441
|
+
// Return outcome to client
|
|
442
|
+
|
|
443
|
+
return {
|
|
444
|
+
outcome: result.outcome,
|
|
445
|
+
publicState: result.publicState,
|
|
446
|
+
rngOutcome: result.rngOutcome, // For audit
|
|
447
|
+
// privateState stays on server
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
}
|
|
115
451
|
```
|
|
116
452
|
|
|
117
|
-
|
|
453
|
+
### Example 2: Handling Bonus Triggers
|
|
118
454
|
|
|
119
455
|
```typescript
|
|
120
|
-
|
|
121
|
-
|
|
456
|
+
async handleSpinResult(spinResult: any) {
|
|
457
|
+
const { outcome, publicState, privateState } = spinResult;
|
|
458
|
+
|
|
459
|
+
// Check win types
|
|
460
|
+
console.log(`Total Payout: ${outcome.wins.totalPayout}`);
|
|
461
|
+
console.log(`Line Wins: ${outcome.wins.lineWins.length}`);
|
|
462
|
+
console.log(`Wall Win: ${outcome.wins.wallWin ? 'Yes' : 'No'}`);
|
|
463
|
+
console.log(`Scatter Wins: ${outcome.wins.scatterWins.length}`);
|
|
464
|
+
|
|
465
|
+
// Check for jackpot
|
|
466
|
+
if (outcome.jackpotWon > 0) {
|
|
467
|
+
console.log(`JACKPOT WON: ${outcome.jackpotWon}`);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
if (outcome.poolJackpotWon > 0) {
|
|
471
|
+
console.log(`POOL JACKPOT WON: ${outcome.poolJackpotWon}`);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// Check if bonus was triggered
|
|
475
|
+
if (outcome.bonusTriggered) {
|
|
476
|
+
return {
|
|
477
|
+
type: 'BONUS_TRIGGERED',
|
|
478
|
+
bonusType: outcome.bonusTriggered,
|
|
479
|
+
spinsRemaining: publicState.spinsRemaining,
|
|
480
|
+
message: `${outcome.bonusTriggered} bonus triggered!`
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// Check if bonus sequence completed
|
|
485
|
+
if (outcome.isBonusComplete) {
|
|
486
|
+
return {
|
|
487
|
+
type: 'BONUS_COMPLETE',
|
|
488
|
+
message: 'Bonus sequence completed'
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
return {
|
|
493
|
+
type: 'REGULAR_SPIN',
|
|
494
|
+
totalPayout: outcome.wins.totalPayout
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
```
|
|
122
498
|
|
|
123
|
-
|
|
124
|
-
const engine = new HappyPandaEngine(rngProvider);
|
|
499
|
+
### Example 3: Game Direction Modes
|
|
125
500
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
betStake: 1
|
|
501
|
+
```typescript
|
|
502
|
+
// 8-line mode (SINGLE)
|
|
503
|
+
const single8Lines = await gameEngine.processCommand(null, null, {
|
|
504
|
+
id: 'init-single',
|
|
505
|
+
type: 'INIT_SESSION_STATE',
|
|
506
|
+
payload: { gameDirection: 0, betStake: 1 }
|
|
132
507
|
});
|
|
508
|
+
// Total bet = 8 x betStake = 8
|
|
133
509
|
|
|
134
|
-
//
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
console.log(response.grid);
|
|
510
|
+
// 16-line mode (BOTH)
|
|
511
|
+
const both16Lines = await gameEngine.processCommand(null, null, {
|
|
512
|
+
id: 'init-both',
|
|
513
|
+
type: 'INIT_SESSION_STATE',
|
|
514
|
+
payload: { gameDirection: 1, betStake: 1 }
|
|
515
|
+
});
|
|
516
|
+
// Total bet = 16 x betStake = 16
|
|
142
517
|
```
|
|
143
518
|
|
|
144
|
-
##
|
|
519
|
+
## Development
|
|
520
|
+
|
|
521
|
+
### Running Tests
|
|
145
522
|
|
|
146
523
|
```bash
|
|
147
524
|
# Run all tests
|
|
148
525
|
npm test
|
|
149
526
|
|
|
527
|
+
# Run tests in watch mode
|
|
528
|
+
npm run test:watch
|
|
529
|
+
|
|
530
|
+
# Run with coverage
|
|
531
|
+
npm run test:cov
|
|
532
|
+
|
|
150
533
|
# Run RTP diagnostic (100K spins)
|
|
151
534
|
npm test -- --testNamePattern="RTP Diagnostic"
|
|
152
535
|
|
|
153
|
-
# Run
|
|
154
|
-
npm test -- --testPathPattern="
|
|
536
|
+
# Run C++ parity tests
|
|
537
|
+
npm test -- --testPathPattern="cpp-parity"
|
|
155
538
|
```
|
|
156
539
|
|
|
157
|
-
|
|
540
|
+
### Building
|
|
158
541
|
|
|
159
|
-
|
|
542
|
+
```bash
|
|
543
|
+
# Clean build
|
|
544
|
+
npm run clean
|
|
545
|
+
npm run build
|
|
160
546
|
|
|
547
|
+
# Development mode
|
|
548
|
+
npm run build
|
|
161
549
|
```
|
|
162
|
-
================================================================
|
|
163
|
-
HAPPY PANDA RTP VERIFICATION - 100 MILLION SPINS
|
|
164
|
-
================================================================
|
|
165
550
|
|
|
166
|
-
|
|
167
|
-
- Game Direction: SINGLE (8 lines)
|
|
168
|
-
- Bet per spin: 8
|
|
169
|
-
- Seeds: 10 x 10M = 100M total paid spins
|
|
170
|
-
- C++ Target RTP: 96.05%
|
|
551
|
+
### Linting
|
|
171
552
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
BONUS_JACKPOT : 16.24% | C++: 13.56% | Diff: +2.68%
|
|
176
|
-
BONUS_CHERRY : 3.96% | C++: 3.67% | Diff: +0.29%
|
|
177
|
-
BONUS_BELL : 5.06% | C++: 5.83% | Diff: -0.77%
|
|
178
|
-
BONUS_BAR1 : 5.47% | C++: 6.31% | Diff: -0.84%
|
|
179
|
-
RESPIN_CHERRY : 2.64% | C++: 3.18% | Diff: -0.54%
|
|
180
|
-
----------------------------------------------------------------
|
|
181
|
-
TOTAL : 95.91% | C++: 96.05%
|
|
553
|
+
```bash
|
|
554
|
+
# Check for issues
|
|
555
|
+
npm run lint
|
|
182
556
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
557
|
+
# Auto-fix issues
|
|
558
|
+
npm run lint -- --fix
|
|
559
|
+
```
|
|
186
560
|
|
|
187
|
-
|
|
188
|
-
|
|
561
|
+
## Configuration
|
|
562
|
+
|
|
563
|
+
Game configuration located in `src/config/happy-panda.config.ts`:
|
|
564
|
+
|
|
565
|
+
- Symbol definitions and IDs
|
|
566
|
+
- Paytable values for all win types
|
|
567
|
+
- Line shapes for 8 and 16 line modes
|
|
568
|
+
- Reel strip weights by spin type
|
|
569
|
+
- Bonus counter initial values
|
|
570
|
+
- Jackpot accumulation rates
|
|
571
|
+
|
|
572
|
+
All values match the original Excel specification (`CherryMaster_A_2_26.05.2025.xlsx`).
|
|
573
|
+
|
|
574
|
+
## Type Exports
|
|
575
|
+
|
|
576
|
+
The package exports all necessary types for TypeScript integration:
|
|
577
|
+
|
|
578
|
+
```typescript
|
|
579
|
+
import {
|
|
580
|
+
// V1 Game Engine
|
|
581
|
+
HappyPandaV1GameEngine,
|
|
582
|
+
GameEngine,
|
|
583
|
+
GameEngineInfo,
|
|
584
|
+
GameActionCommand,
|
|
585
|
+
CommandProcessingResult,
|
|
586
|
+
RngOutcome,
|
|
587
|
+
RngOutcomeRecord,
|
|
588
|
+
|
|
589
|
+
// Core Engine
|
|
590
|
+
HappyPandaEngine,
|
|
591
|
+
|
|
592
|
+
// Configuration
|
|
593
|
+
Symbol,
|
|
594
|
+
SpinType,
|
|
595
|
+
ScreenWinType,
|
|
596
|
+
GRID,
|
|
597
|
+
LINE_SHAPES,
|
|
598
|
+
LINES_PER_DIRECTION,
|
|
599
|
+
|
|
600
|
+
// Domain Types
|
|
601
|
+
Grid,
|
|
602
|
+
Position,
|
|
603
|
+
LineWin,
|
|
604
|
+
WallWin,
|
|
605
|
+
ScatterWin,
|
|
606
|
+
SpecialWin,
|
|
607
|
+
SpinWinResult,
|
|
608
|
+
BonusCounters,
|
|
609
|
+
PendingBonuses,
|
|
610
|
+
JackpotState,
|
|
611
|
+
GameDirection,
|
|
612
|
+
GameState,
|
|
613
|
+
PublicState,
|
|
614
|
+
PrivateState,
|
|
615
|
+
SpinRequest,
|
|
616
|
+
SpinResponse,
|
|
617
|
+
SessionState,
|
|
618
|
+
RngProvider,
|
|
619
|
+
CommandType,
|
|
620
|
+
GameCommand,
|
|
621
|
+
|
|
622
|
+
// Logic Services
|
|
623
|
+
evaluateSpin,
|
|
624
|
+
evaluateLineWins,
|
|
625
|
+
evaluateWallWin,
|
|
626
|
+
evaluateScatterWins,
|
|
627
|
+
generateGrid,
|
|
628
|
+
} from '@omnitronix/happy-panda-game-engine';
|
|
189
629
|
```
|
|
190
630
|
|
|
631
|
+
## Key Differences from Traditional Slots
|
|
632
|
+
|
|
633
|
+
### Classic 3x3 Layout
|
|
634
|
+
|
|
635
|
+
- **Traditional Modern**: 5+ reels, multiple rows
|
|
636
|
+
- **Happy Panda**: Classic 3x3 grid with bidirectional paylines
|
|
637
|
+
- **Advantage**: Nostalgic gameplay, simpler visual presentation
|
|
638
|
+
|
|
639
|
+
### Bidirectional Paylines
|
|
640
|
+
|
|
641
|
+
- **8-Line Mode**: Left-to-right only
|
|
642
|
+
- **16-Line Mode**: Both directions (LTR + RTL)
|
|
643
|
+
- **Strategic Choice**: Player chooses mode based on bet preference
|
|
644
|
+
|
|
645
|
+
### Wall Wins (Matrix Wins)
|
|
646
|
+
|
|
647
|
+
- **Screen-filling wins**: Full 3x3 of same/related symbols
|
|
648
|
+
- **Multiple types**: Pure, cherry mix, bar mix, fruits, colors
|
|
649
|
+
- **Important**: Wall wins suppress line wins (no double-counting)
|
|
650
|
+
|
|
651
|
+
### Counter-Based Bonuses
|
|
652
|
+
|
|
653
|
+
- **Not scatter-triggered**: Bonuses trigger via counter depletion
|
|
654
|
+
- **Progressive build-up**: Counters decrement on specific symbol combos
|
|
655
|
+
- **Multiple bonus types**: 5 distinct bonus modes with different mechanics
|
|
656
|
+
|
|
657
|
+
### Dual Jackpot System
|
|
658
|
+
|
|
659
|
+
- **Progressive Jackpot**: Accumulates on losses, paid in Jackpot Bonus
|
|
660
|
+
- **Pool Jackpot**: Separate accumulator, paid with Bell Bonus
|
|
661
|
+
- **Dual opportunity**: Two paths to jackpot wins
|
|
662
|
+
|
|
191
663
|
## Documentation
|
|
192
664
|
|
|
193
665
|
- **Test Protocol**: `docs/TEST-PROTOCOL-RTP-100M.md` - Complete 100M spin test results
|
|
@@ -195,18 +667,10 @@ Statistics:
|
|
|
195
667
|
- **C++ Source**: `math/Happy Red Panda/CherryMaster_A_2.cpp`
|
|
196
668
|
- **Excel Spec**: `math/Happy Red Panda/CherryMaster_A_2_26.05.2025.xlsx`
|
|
197
669
|
|
|
198
|
-
## Installation
|
|
199
|
-
|
|
200
|
-
```bash
|
|
201
|
-
npm install @omnitronix/happy-panda-game-engine
|
|
202
|
-
```
|
|
203
|
-
|
|
204
|
-
## Build
|
|
205
|
-
|
|
206
|
-
```bash
|
|
207
|
-
npm run build
|
|
208
|
-
```
|
|
209
|
-
|
|
210
670
|
## License
|
|
211
671
|
|
|
212
672
|
UNLICENSED - Internal use only for Omnitronix
|
|
673
|
+
|
|
674
|
+
## Support
|
|
675
|
+
|
|
676
|
+
For questions or issues, contact the Omnitronix development team.
|
|
@@ -4,36 +4,35 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Wrapper class that adapts HappyPandaEngine to the standard GameEngine interface
|
|
6
6
|
* expected by the game-engine-service infrastructure.
|
|
7
|
+
*
|
|
8
|
+
* Uses the proper RNG infrastructure matching Heroes of Aether and Bonny's Fortune:
|
|
9
|
+
* - RngClientFactory for environment-based client selection
|
|
10
|
+
* - RngService for audit trail tracking
|
|
11
|
+
* - Full RNG outcome reporting for compliance
|
|
7
12
|
*/
|
|
8
13
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
14
|
exports.HappyPandaV1GameEngine = void 0;
|
|
10
15
|
const happy_panda_engine_1 = require("./engine/happy-panda-engine");
|
|
11
16
|
const types_1 = require("./domain/types");
|
|
17
|
+
const rng_client_factory_1 = require("./rng/rng-client.factory");
|
|
18
|
+
const rng_service_1 = require("./rng/rng-service");
|
|
12
19
|
/**
|
|
13
|
-
* RNG adapter that
|
|
20
|
+
* RNG Provider adapter that bridges RngService to the engine's RngProvider interface
|
|
14
21
|
*/
|
|
15
|
-
class
|
|
16
|
-
constructor() {
|
|
17
|
-
this.
|
|
22
|
+
class RngServiceAdapter {
|
|
23
|
+
constructor(rngService) {
|
|
24
|
+
this.rngService = rngService;
|
|
18
25
|
this.currentCommandId = '';
|
|
19
|
-
this.
|
|
26
|
+
this.actionCounter = 0;
|
|
20
27
|
}
|
|
21
28
|
setCommandId(commandId) {
|
|
22
29
|
this.currentCommandId = commandId;
|
|
23
|
-
|
|
24
|
-
this.outcomes.set(commandId, []);
|
|
25
|
-
}
|
|
30
|
+
this.actionCounter = 0;
|
|
26
31
|
}
|
|
27
32
|
async random(max) {
|
|
28
|
-
const
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
seed: this.seedCounter++,
|
|
32
|
-
min: 0,
|
|
33
|
-
max: max - 1,
|
|
34
|
-
};
|
|
35
|
-
this.outcomes.get(this.currentCommandId)?.push(record);
|
|
36
|
-
return result;
|
|
33
|
+
const actionId = `rng_${this.actionCounter++}`;
|
|
34
|
+
const { value } = await this.rngService.getSingleNumber(0, max - 1, this.currentCommandId, actionId);
|
|
35
|
+
return value;
|
|
37
36
|
}
|
|
38
37
|
async randomBatch(count, max) {
|
|
39
38
|
const results = [];
|
|
@@ -42,33 +41,29 @@ class TrackedRngProvider {
|
|
|
42
41
|
}
|
|
43
42
|
return results;
|
|
44
43
|
}
|
|
45
|
-
getOutcomes(commandId) {
|
|
46
|
-
const records = this.outcomes.get(commandId) || [];
|
|
47
|
-
const outcome = {};
|
|
48
|
-
records.forEach((record, index) => {
|
|
49
|
-
outcome[`rng_${index}`] = record;
|
|
50
|
-
});
|
|
51
|
-
return outcome;
|
|
52
|
-
}
|
|
53
|
-
clearOutcomes(commandId) {
|
|
54
|
-
this.outcomes.delete(commandId);
|
|
55
|
-
}
|
|
56
44
|
}
|
|
57
45
|
/**
|
|
58
46
|
* Happy Panda V1 Game Engine
|
|
59
47
|
*
|
|
60
48
|
* Implements the standard GameEngine interface for use with game-engine-service.
|
|
49
|
+
* Uses proper RNG infrastructure with full audit trail support.
|
|
61
50
|
*/
|
|
62
51
|
class HappyPandaV1GameEngine {
|
|
63
|
-
constructor() {
|
|
52
|
+
constructor(config = {}) {
|
|
64
53
|
this.gameCode = 'happy-panda';
|
|
65
54
|
this.version = '1.0.0';
|
|
66
55
|
this.rtp = 96.05;
|
|
67
56
|
this.gameType = 'slot';
|
|
68
57
|
this.gameName = 'Happy Panda';
|
|
69
58
|
this.provider = 'Omnitronix';
|
|
70
|
-
|
|
71
|
-
|
|
59
|
+
// Create RNG client based on environment (local for dev, external for prod)
|
|
60
|
+
const rngClient = rng_client_factory_1.RngClientFactory.create({ metrics: config.metrics });
|
|
61
|
+
// Wrap in service for audit trail tracking
|
|
62
|
+
this.rngService = new rng_service_1.RngService(rngClient);
|
|
63
|
+
// Create adapter to bridge to engine's RngProvider interface
|
|
64
|
+
this.rngAdapter = new RngServiceAdapter(this.rngService);
|
|
65
|
+
// Initialize engine with RNG adapter
|
|
66
|
+
this.engine = new happy_panda_engine_1.HappyPandaEngine(this.rngAdapter);
|
|
72
67
|
}
|
|
73
68
|
getGameEngineInfo() {
|
|
74
69
|
return {
|
|
@@ -81,7 +76,9 @@ class HappyPandaV1GameEngine {
|
|
|
81
76
|
};
|
|
82
77
|
}
|
|
83
78
|
async processCommand(publicState, privateState, command) {
|
|
84
|
-
|
|
79
|
+
// Register command for RNG audit trail
|
|
80
|
+
this.rngService.registerCommand(command.id);
|
|
81
|
+
this.rngAdapter.setCommandId(command.id);
|
|
85
82
|
try {
|
|
86
83
|
switch (command.type) {
|
|
87
84
|
case 'INIT_SESSION_STATE': {
|
|
@@ -94,8 +91,8 @@ class HappyPandaV1GameEngine {
|
|
|
94
91
|
gameDirection,
|
|
95
92
|
betStake,
|
|
96
93
|
});
|
|
97
|
-
const rngOutcome = this.
|
|
98
|
-
this.
|
|
94
|
+
const rngOutcome = this.rngService.getRngOutcomeForCommand(command.id);
|
|
95
|
+
this.rngService.removeCommand(command.id);
|
|
99
96
|
return {
|
|
100
97
|
success: true,
|
|
101
98
|
publicState: result.state.publicState,
|
|
@@ -107,6 +104,7 @@ class HappyPandaV1GameEngine {
|
|
|
107
104
|
}
|
|
108
105
|
case 'SPIN': {
|
|
109
106
|
if (!publicState || !privateState) {
|
|
107
|
+
this.rngService.removeCommand(command.id);
|
|
110
108
|
return {
|
|
111
109
|
success: false,
|
|
112
110
|
publicState: publicState,
|
|
@@ -126,8 +124,8 @@ class HappyPandaV1GameEngine {
|
|
|
126
124
|
command: types_1.CommandType.SPIN,
|
|
127
125
|
sessionId: command.id,
|
|
128
126
|
}, sessionState);
|
|
129
|
-
const rngOutcome = this.
|
|
130
|
-
this.
|
|
127
|
+
const rngOutcome = this.rngService.getRngOutcomeForCommand(command.id);
|
|
128
|
+
this.rngService.removeCommand(command.id);
|
|
131
129
|
return {
|
|
132
130
|
success: true,
|
|
133
131
|
publicState: result.state.publicState,
|
|
@@ -142,7 +140,7 @@ class HappyPandaV1GameEngine {
|
|
|
142
140
|
command: types_1.CommandType.GET_SYMBOLS,
|
|
143
141
|
sessionId: command.id,
|
|
144
142
|
});
|
|
145
|
-
this.
|
|
143
|
+
this.rngService.removeCommand(command.id);
|
|
146
144
|
return {
|
|
147
145
|
success: true,
|
|
148
146
|
publicState: publicState,
|
|
@@ -152,7 +150,7 @@ class HappyPandaV1GameEngine {
|
|
|
152
150
|
};
|
|
153
151
|
}
|
|
154
152
|
default: {
|
|
155
|
-
this.
|
|
153
|
+
this.rngService.removeCommand(command.id);
|
|
156
154
|
return {
|
|
157
155
|
success: false,
|
|
158
156
|
publicState: publicState,
|
|
@@ -163,7 +161,7 @@ class HappyPandaV1GameEngine {
|
|
|
163
161
|
}
|
|
164
162
|
}
|
|
165
163
|
catch (error) {
|
|
166
|
-
this.
|
|
164
|
+
this.rngService.removeCommand(command.id);
|
|
167
165
|
return {
|
|
168
166
|
success: false,
|
|
169
167
|
publicState: publicState,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"happy-panda-v1.game-engine.js","sourceRoot":"","sources":["../src/happy-panda-v1.game-engine.ts"],"names":[],"mappings":";AAAA
|
|
1
|
+
{"version":3,"file":"happy-panda-v1.game-engine.js","sourceRoot":"","sources":["../src/happy-panda-v1.game-engine.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* @packageDocumentation
|
|
8
8
|
*/
|
|
9
9
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
-
exports.generateGrid = exports.evaluateScatterWins = exports.evaluateWallWin = exports.evaluateLineWins = exports.evaluateSpin = exports.CommandType = exports.GameDirection = exports.LINES_PER_DIRECTION = exports.LINE_SHAPES = exports.GRID = exports.ScreenWinType = exports.SpinType = exports.Symbol = exports.HappyPandaEngine = exports.HappyPandaV1GameEngine = void 0;
|
|
10
|
+
exports.RngService = exports.RngClientFactory = exports.DummyRngClient = exports.generateGrid = exports.evaluateScatterWins = exports.evaluateWallWin = exports.evaluateLineWins = exports.evaluateSpin = exports.CommandType = exports.GameDirection = exports.LINES_PER_DIRECTION = exports.LINE_SHAPES = exports.GRID = exports.ScreenWinType = exports.SpinType = exports.Symbol = exports.HappyPandaEngine = exports.HappyPandaV1GameEngine = void 0;
|
|
11
11
|
// V1 Game Engine (for game-engine-service integration)
|
|
12
12
|
var happy_panda_v1_game_engine_1 = require("./happy-panda-v1.game-engine");
|
|
13
13
|
Object.defineProperty(exports, "HappyPandaV1GameEngine", { enumerable: true, get: function () { return happy_panda_v1_game_engine_1.HappyPandaV1GameEngine; } });
|
|
@@ -34,4 +34,10 @@ Object.defineProperty(exports, "evaluateWallWin", { enumerable: true, get: funct
|
|
|
34
34
|
Object.defineProperty(exports, "evaluateScatterWins", { enumerable: true, get: function () { return services_1.evaluateScatterWins; } });
|
|
35
35
|
var rng_1 = require("./rng");
|
|
36
36
|
Object.defineProperty(exports, "generateGrid", { enumerable: true, get: function () { return rng_1.generateGrid; } });
|
|
37
|
+
var dummy_rng_client_1 = require("./rng/dummy-rng-client");
|
|
38
|
+
Object.defineProperty(exports, "DummyRngClient", { enumerable: true, get: function () { return dummy_rng_client_1.DummyRngClient; } });
|
|
39
|
+
var rng_client_factory_1 = require("./rng/rng-client.factory");
|
|
40
|
+
Object.defineProperty(exports, "RngClientFactory", { enumerable: true, get: function () { return rng_client_factory_1.RngClientFactory; } });
|
|
41
|
+
var rng_service_1 = require("./rng/rng-service");
|
|
42
|
+
Object.defineProperty(exports, "RngService", { enumerable: true, get: function () { return rng_service_1.RngService; } });
|
|
37
43
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA;;;;;;GAMG;;;AAEH,uDAAuD;AACvD,2EAAsE;AAA7D,oIAAA,sBAAsB,OAAA;AAU/B,cAAc;AACd,mCAA4C;AAAnC,0GAAA,gBAAgB,OAAA;AAEzB,gBAAgB;AAChB,mCAOkB;AANhB,gGAAA,MAAM,OAAA;AACN,kGAAA,QAAQ,OAAA;AACR,uGAAA,aAAa,OAAA;AACb,8FAAA,IAAI,OAAA;AACJ,qGAAA,WAAW,OAAA;AACX,6GAAA,mBAAmB,OAAA;AAGrB,eAAe;AACf,mCAwBkB;AAbhB,uGAAA,aAAa,OAAA;AAQb,qGAAA,WAAW,OAAA;AAOb,yCAAyC;AACzC,6CAAwG;AAA/F,wGAAA,YAAY,OAAA;AAAE,4GAAA,gBAAgB,OAAA;AAAE,2GAAA,eAAe,OAAA;AAAE,+GAAA,mBAAmB,OAAA;AAC7E,6BAAqC;AAA5B,mGAAA,YAAY,OAAA"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA;;;;;;GAMG;;;AAEH,uDAAuD;AACvD,2EAAsE;AAA7D,oIAAA,sBAAsB,OAAA;AAU/B,cAAc;AACd,mCAA4C;AAAnC,0GAAA,gBAAgB,OAAA;AAEzB,gBAAgB;AAChB,mCAOkB;AANhB,gGAAA,MAAM,OAAA;AACN,kGAAA,QAAQ,OAAA;AACR,uGAAA,aAAa,OAAA;AACb,8FAAA,IAAI,OAAA;AACJ,qGAAA,WAAW,OAAA;AACX,6GAAA,mBAAmB,OAAA;AAGrB,eAAe;AACf,mCAwBkB;AAbhB,uGAAA,aAAa,OAAA;AAQb,qGAAA,WAAW,OAAA;AAOb,yCAAyC;AACzC,6CAAwG;AAA/F,wGAAA,YAAY,OAAA;AAAE,4GAAA,gBAAgB,OAAA;AAAE,2GAAA,eAAe,OAAA;AAAE,+GAAA,mBAAmB,OAAA;AAC7E,6BAAqC;AAA5B,mGAAA,YAAY,OAAA;AAQrB,2DAAwD;AAA/C,kHAAA,cAAc,OAAA;AACvB,+DAA6E;AAApE,sHAAA,gBAAgB,OAAA;AACzB,iDAA+C;AAAtC,yGAAA,UAAU,OAAA"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Dummy RNG Client
|
|
4
|
+
*
|
|
5
|
+
* Local RNG client for development and testing.
|
|
6
|
+
* Uses Math.random() for all operations.
|
|
7
|
+
* Returns seed value of 0 (non-cryptographic, for testing only).
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.DummyRngClient = void 0;
|
|
11
|
+
class DummyRngClient {
|
|
12
|
+
/**
|
|
13
|
+
* Generate a single random integer in range [min, max]
|
|
14
|
+
*/
|
|
15
|
+
async getSingleNumber(min, max, _seed) {
|
|
16
|
+
const randomValue = Math.random();
|
|
17
|
+
const result = Math.floor(randomValue * (max - min + 1)) + min;
|
|
18
|
+
return { value: result, seed: 0 };
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Generate multiple random integers in range [min, max]
|
|
22
|
+
*/
|
|
23
|
+
async getBatchNumbers(min, max, count, _seed) {
|
|
24
|
+
const results = [];
|
|
25
|
+
for (let i = 0; i < count; i++) {
|
|
26
|
+
const randomValue = Math.random();
|
|
27
|
+
results.push(Math.floor(randomValue * (max - min + 1)) + min);
|
|
28
|
+
}
|
|
29
|
+
return results;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Generate multiple random integers with individual seeds
|
|
33
|
+
*/
|
|
34
|
+
async getBatchNumbersWithSeeds(min, max, count, _seed) {
|
|
35
|
+
const results = [];
|
|
36
|
+
for (let i = 0; i < count; i++) {
|
|
37
|
+
const randomValue = Math.random();
|
|
38
|
+
results.push({
|
|
39
|
+
value: Math.floor(randomValue * (max - min + 1)) + min,
|
|
40
|
+
seed: 0,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
return results;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Generate a single random float in range [0, 1)
|
|
47
|
+
*/
|
|
48
|
+
async getSingleFloat(_seed) {
|
|
49
|
+
return { value: Math.random(), seed: 0 };
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Generate multiple random floats in range [0, 1)
|
|
53
|
+
*/
|
|
54
|
+
async getBatchFloats(count, _seed) {
|
|
55
|
+
const values = [];
|
|
56
|
+
const seeds = [];
|
|
57
|
+
for (let i = 0; i < count; i++) {
|
|
58
|
+
values.push(Math.random());
|
|
59
|
+
seeds.push(0);
|
|
60
|
+
}
|
|
61
|
+
return { values, seeds };
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Alternative method for single random number
|
|
65
|
+
*/
|
|
66
|
+
async getRandomNumber(min, max, _seed) {
|
|
67
|
+
return this.getSingleNumber(min, max, _seed);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Alternative method for single random float
|
|
71
|
+
*/
|
|
72
|
+
async getRandomFloat(_seed) {
|
|
73
|
+
return this.getSingleFloat(_seed);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
exports.DummyRngClient = DummyRngClient;
|
|
77
|
+
//# sourceMappingURL=dummy-rng-client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dummy-rng-client.js","sourceRoot":"","sources":["../../src/rng/dummy-rng-client.ts"],"names":[],"mappings":";AAAA;;;;;;GAMG;;;AAQH,MAAa,cAAc;IACzB;;OAEG;IACH,KAAK,CAAC,eAAe,CACnB,GAAW,EACX,GAAW,EACX,KAAc;QAEd,MAAM,WAAW,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC;QAClC,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,WAAW,GAAG,CAAC,GAAG,GAAG,GAAG,GAAG,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC;QAC/D,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;IACpC,CAAC;IAED;;OAEG;IACH,KAAK,CAAC,eAAe,CACnB,GAAW,EACX,GAAW,EACX,KAAa,EACb,KAAc;QAEd,MAAM,OAAO,GAAa,EAAE,CAAC;QAC7B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,EAAE,CAAC,EAAE,EAAE,CAAC;YAC/B,MAAM,WAAW,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC;YAClC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,WAAW,GAAG,CAAC,GAAG,GAAG,GAAG,GAAG,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC;QAChE,CAAC;QACD,OAAO,OAAO,CAAC;IACjB,CAAC;IAED;;OAEG;IACH,KAAK,CAAC,wBAAwB,CAC5B,GAAW,EACX,GAAW,EACX,KAAa,EACb,KAAc;QAEd,MAAM,OAAO,GAAwB,EAAE,CAAC;QACxC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,EAAE,CAAC,EAAE,EAAE,CAAC;YAC/B,MAAM,WAAW,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC;YAClC,OAAO,CAAC,IAAI,CAAC;gBACX,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,WAAW,GAAG,CAAC,GAAG,GAAG,GAAG,GAAG,CAAC,CAAC,CAAC,GAAG,GAAG;gBACtD,IAAI,EAAE,CAAC;aACR,CAAC,CAAC;QACL,CAAC;QACD,OAAO,OAAO,CAAC;IACjB,CAAC;IAED;;OAEG;IACH,KAAK,CAAC,cAAc,CAAC,KAAc;QACjC,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC,MAAM,EAAE,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;IAC3C,CAAC;IAED;;OAEG;IACH,KAAK,CAAC,cAAc,CAClB,KAAa,EACb,KAAc;QAEd,MAAM,MAAM,GAAa,EAAE,CAAC;QAC5B,MAAM,KAAK,GAAa,EAAE,CAAC;QAC3B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,EAAE,CAAC,EAAE,EAAE,CAAC;YAC/B,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC;YAC3B,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAChB,CAAC;QACD,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC;IAC3B,CAAC;IAED;;OAEG;IACH,KAAK,CAAC,eAAe,CACnB,GAAW,EACX,GAAW,EACX,KAAc;QAEd,OAAO,IAAI,CAAC,eAAe,CAAC,GAAG,EAAE,GAAG,EAAE,KAAK,CAAC,CAAC;IAC/C,CAAC;IAED;;OAEG;IACH,KAAK,CAAC,cAAc,CAAC,KAAc;QACjC,OAAO,IAAI,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC;IACpC,CAAC;CACF;AA3FD,wCA2FC"}
|
package/dist/rng/index.js
CHANGED
|
@@ -19,4 +19,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
19
19
|
*/
|
|
20
20
|
__exportStar(require("./weighted-random"), exports);
|
|
21
21
|
__exportStar(require("./spin-generator"), exports);
|
|
22
|
+
// RNG Service and Client Infrastructure
|
|
23
|
+
__exportStar(require("./rng-client.interface"), exports);
|
|
24
|
+
__exportStar(require("./dummy-rng-client"), exports);
|
|
25
|
+
__exportStar(require("./rng-client.factory"), exports);
|
|
26
|
+
__exportStar(require("./rng-service"), exports);
|
|
22
27
|
//# sourceMappingURL=index.js.map
|
package/dist/rng/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/rng/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA;;GAEG;AACH,oDAAkC;AAClC,mDAAiC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/rng/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA;;GAEG;AACH,oDAAkC;AAClC,mDAAiC;AAEjC,wCAAwC;AACxC,yDAAuC;AACvC,qDAAmC;AACnC,uDAAqC;AACrC,gDAA8B"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* RNG Client Factory
|
|
4
|
+
*
|
|
5
|
+
* Factory pattern for creating RNG clients based on environment configuration.
|
|
6
|
+
* Matches the pattern used in Heroes of Aether and Bonny's Fortune.
|
|
7
|
+
*
|
|
8
|
+
* Environment Variables:
|
|
9
|
+
* - RNG_CLIENT_MODE: 'local' (DummyRngClient) or 'external' (production RngClient)
|
|
10
|
+
* - RNG_CLIENT_URL: HTTP endpoint for external RNG service
|
|
11
|
+
* - RNG_CLIENT_GRPC_URL: gRPC endpoint for external RNG service
|
|
12
|
+
* - RNG_CLIENT_METHOD: 'grpc' or 'rest'
|
|
13
|
+
* - RNG_CLIENT_POOL_SIZE: Ring buffer pool size (default: 2000)
|
|
14
|
+
*/
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.RngClientFactory = void 0;
|
|
17
|
+
const dummy_rng_client_1 = require("./dummy-rng-client");
|
|
18
|
+
class RngClientFactory {
|
|
19
|
+
/**
|
|
20
|
+
* Create an RNG client based on environment configuration.
|
|
21
|
+
*
|
|
22
|
+
* @param config Optional configuration overrides
|
|
23
|
+
* @returns IRngClient instance (DummyRngClient for local, external client for production)
|
|
24
|
+
*/
|
|
25
|
+
static create(config = {}) {
|
|
26
|
+
const mode = process.env.RNG_CLIENT_MODE || 'external';
|
|
27
|
+
if (mode === 'local') {
|
|
28
|
+
console.log('[RngClientFactory] Creating DummyRngClient (local mode)');
|
|
29
|
+
return new dummy_rng_client_1.DummyRngClient();
|
|
30
|
+
}
|
|
31
|
+
// For external mode, we would use @omnitronix/rng-client-core
|
|
32
|
+
// For now, fall back to DummyRngClient if external client not available
|
|
33
|
+
console.log('[RngClientFactory] External RNG mode requested');
|
|
34
|
+
const rngClientConfig = {
|
|
35
|
+
serverUrl: process.env.RNG_CLIENT_URL,
|
|
36
|
+
grpcUrl: process.env.RNG_CLIENT_GRPC_URL,
|
|
37
|
+
clientMethod: process.env.RNG_CLIENT_METHOD,
|
|
38
|
+
poolingSize: parseInt(process.env.RNG_CLIENT_POOL_SIZE || '2000'),
|
|
39
|
+
metrics: config.metrics,
|
|
40
|
+
};
|
|
41
|
+
// Try to load external RNG client
|
|
42
|
+
try {
|
|
43
|
+
// Dynamic import to avoid hard dependency
|
|
44
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
45
|
+
const { RngClient } = require('@omnitronix/rng-client-core');
|
|
46
|
+
console.log('[RngClientFactory] Creating external RngClient');
|
|
47
|
+
return new RngClient(rngClientConfig);
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
console.warn('[RngClientFactory] @omnitronix/rng-client-core not available, falling back to DummyRngClient');
|
|
51
|
+
console.warn('[RngClientFactory] Install @omnitronix/rng-client-core for production use');
|
|
52
|
+
return new dummy_rng_client_1.DummyRngClient();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
exports.RngClientFactory = RngClientFactory;
|
|
57
|
+
//# sourceMappingURL=rng-client.factory.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rng-client.factory.js","sourceRoot":"","sources":["../../src/rng/rng-client.factory.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;GAYG;;;AAGH,yDAAoD;AAUpD,MAAa,gBAAgB;IAC3B;;;;;OAKG;IACH,MAAM,CAAC,MAAM,CAAC,SAAgC,EAAE;QAC9C,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,eAAe,IAAI,UAAU,CAAC;QAEvD,IAAI,IAAI,KAAK,OAAO,EAAE,CAAC;YACrB,OAAO,CAAC,GAAG,CAAC,yDAAyD,CAAC,CAAC;YACvE,OAAO,IAAI,iCAAc,EAAE,CAAC;QAC9B,CAAC;QAED,8DAA8D;QAC9D,wEAAwE;QACxE,OAAO,CAAC,GAAG,CAAC,gDAAgD,CAAC,CAAC;QAE9D,MAAM,eAAe,GAAoB;YACvC,SAAS,EAAE,OAAO,CAAC,GAAG,CAAC,cAAc;YACrC,OAAO,EAAE,OAAO,CAAC,GAAG,CAAC,mBAAmB;YACxC,YAAY,EAAE,OAAO,CAAC,GAAG,CAAC,iBAAoC;YAC9D,WAAW,EAAE,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,oBAAoB,IAAI,MAAM,CAAC;YACjE,OAAO,EAAE,MAAM,CAAC,OAAO;SACxB,CAAC;QAEF,kCAAkC;QAClC,IAAI,CAAC;YACH,0CAA0C;YAC1C,8DAA8D;YAC9D,MAAM,EAAE,SAAS,EAAE,GAAG,OAAO,CAAC,6BAA6B,CAAC,CAAC;YAC7D,OAAO,CAAC,GAAG,CAAC,gDAAgD,CAAC,CAAC;YAC9D,OAAO,IAAI,SAAS,CAAC,eAAe,CAAC,CAAC;QACxC,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,CAAC,IAAI,CACV,8FAA8F,CAC/F,CAAC;YACF,OAAO,CAAC,IAAI,CACV,2EAA2E,CAC5E,CAAC;YACF,OAAO,IAAI,iCAAc,EAAE,CAAC;QAC9B,CAAC;IACH,CAAC;CACF;AA5CD,4CA4CC"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* RNG Client Interface
|
|
4
|
+
*
|
|
5
|
+
* Contract for all RNG clients (local and external).
|
|
6
|
+
* Matches the pattern used in Heroes of Aether and Bonny's Fortune.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
//# sourceMappingURL=rng-client.interface.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rng-client.interface.js","sourceRoot":"","sources":["../../src/rng/rng-client.interface.ts"],"names":[],"mappings":";AAAA;;;;;GAKG"}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* RNG Service
|
|
4
|
+
*
|
|
5
|
+
* Core RNG service with audit trail tracking for compliance.
|
|
6
|
+
* Wraps an IRngClient and tracks all RNG outcomes per command.
|
|
7
|
+
* Matches the pattern used in Heroes of Aether and Bonny's Fortune.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.RngService = void 0;
|
|
11
|
+
/**
|
|
12
|
+
* RNG Service
|
|
13
|
+
*
|
|
14
|
+
* Provides RNG operations with full audit trail tracking.
|
|
15
|
+
* Each command ID can have multiple action IDs, allowing fine-grained tracking
|
|
16
|
+
* of all RNG calls within a single game command.
|
|
17
|
+
*/
|
|
18
|
+
class RngService {
|
|
19
|
+
constructor(rngClient) {
|
|
20
|
+
this.rngClient = rngClient;
|
|
21
|
+
this.rngOutcomes = {};
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Register a command for RNG tracking.
|
|
25
|
+
* Call this at the start of command processing.
|
|
26
|
+
*/
|
|
27
|
+
registerCommand(commandId) {
|
|
28
|
+
if (!this.rngOutcomes[commandId]) {
|
|
29
|
+
this.rngOutcomes[commandId] = {};
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Remove a command's RNG outcomes.
|
|
34
|
+
* Call this after command processing to clean up.
|
|
35
|
+
*/
|
|
36
|
+
removeCommand(commandId) {
|
|
37
|
+
delete this.rngOutcomes[commandId];
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Check if a command has RNG outcomes
|
|
41
|
+
*/
|
|
42
|
+
hasCommandRngOutcome(commandId) {
|
|
43
|
+
return !!this.rngOutcomes[commandId];
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Get the count of RNG outcomes for a command
|
|
47
|
+
*/
|
|
48
|
+
getRngOutcomeCountForCommand(commandId) {
|
|
49
|
+
const outcomes = this.rngOutcomes[commandId];
|
|
50
|
+
return outcomes ? Object.keys(outcomes).length : 0;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Store an RNG outcome for audit trail
|
|
54
|
+
*/
|
|
55
|
+
storeSeedForCommand(commandId, actionId, result, seed, min, max) {
|
|
56
|
+
this.registerCommand(commandId);
|
|
57
|
+
this.rngOutcomes[commandId][actionId] = { result, seed, min, max };
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Get cached seed for a command/action (for replay scenarios)
|
|
61
|
+
*/
|
|
62
|
+
getSeedForCommand(commandId, actionId) {
|
|
63
|
+
return this.rngOutcomes[commandId]?.[actionId]?.seed;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Get all RNG outcomes for a command
|
|
67
|
+
*/
|
|
68
|
+
getRngOutcomeForCommand(commandId) {
|
|
69
|
+
return this.rngOutcomes[commandId] || {};
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Get all RNG outcomes across all commands
|
|
73
|
+
*/
|
|
74
|
+
getAllRngOutcome() {
|
|
75
|
+
return { ...this.rngOutcomes };
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Generate a single random integer in range [min, max]
|
|
79
|
+
* Tracks the outcome for audit trail
|
|
80
|
+
*/
|
|
81
|
+
async getSingleNumber(min, max, commandId, actionId) {
|
|
82
|
+
// Check if we already have a cached result (for replay)
|
|
83
|
+
const existingSeed = this.getSeedForCommand(commandId, actionId);
|
|
84
|
+
if (existingSeed !== undefined) {
|
|
85
|
+
const existingResult = this.rngOutcomes[commandId][actionId];
|
|
86
|
+
return { value: existingResult.result, seed: existingResult.seed };
|
|
87
|
+
}
|
|
88
|
+
// Generate new random number
|
|
89
|
+
const response = await this.rngClient.getSingleNumber(min, max);
|
|
90
|
+
// Store for audit trail
|
|
91
|
+
this.storeSeedForCommand(commandId, actionId, response.value, response.seed, min, max);
|
|
92
|
+
return response;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Generate multiple random integers in range [min, max]
|
|
96
|
+
* Each number is tracked individually with incrementing action IDs
|
|
97
|
+
*/
|
|
98
|
+
async getBatchNumbers(min, max, count, commandId, actionId) {
|
|
99
|
+
const results = [];
|
|
100
|
+
for (let i = 0; i < count; i++) {
|
|
101
|
+
const result = await this.getSingleNumber(min, max, commandId, `${actionId}_${i}`);
|
|
102
|
+
results.push(result);
|
|
103
|
+
}
|
|
104
|
+
return results;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Generate a weighted random selection
|
|
108
|
+
* Returns the index selected based on weights
|
|
109
|
+
*/
|
|
110
|
+
async getWeightedRandomIndex(weights, commandId, actionId) {
|
|
111
|
+
const total = weights.reduce((acc, w) => acc + w, 0);
|
|
112
|
+
const { value: rand } = await this.getSingleNumber(0, total - 1, commandId, actionId);
|
|
113
|
+
let cumulative = 0;
|
|
114
|
+
for (let i = 0; i < weights.length; i++) {
|
|
115
|
+
cumulative += weights[i];
|
|
116
|
+
if (rand < cumulative) {
|
|
117
|
+
return i;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return weights.length - 1;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
exports.RngService = RngService;
|
|
124
|
+
//# sourceMappingURL=rng-service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rng-service.js","sourceRoot":"","sources":["../../src/rng/rng-service.ts"],"names":[],"mappings":"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"}
|