@omnitronix/lucky-sharky-game-engine 1.1.0 → 1.2.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 +10 -0
- package/dist/__tests__/base-game.test.d.ts +2 -0
- package/dist/__tests__/base-game.test.d.ts.map +1 -0
- package/dist/__tests__/base-game.test.js +262 -0
- package/dist/__tests__/base-game.test.js.map +1 -0
- package/dist/__tests__/bonus-handlers.test.d.ts +2 -0
- package/dist/__tests__/bonus-handlers.test.d.ts.map +1 -0
- package/dist/__tests__/bonus-handlers.test.js +243 -0
- package/dist/__tests__/bonus-handlers.test.js.map +1 -0
- package/dist/__tests__/error-paths.test.d.ts +2 -0
- package/dist/__tests__/error-paths.test.d.ts.map +1 -0
- package/dist/__tests__/error-paths.test.js +328 -0
- package/dist/__tests__/error-paths.test.js.map +1 -0
- package/dist/__tests__/free-spins-handler.test.d.ts +2 -0
- package/dist/__tests__/free-spins-handler.test.d.ts.map +1 -0
- package/dist/__tests__/free-spins-handler.test.js +410 -0
- package/dist/__tests__/free-spins-handler.test.js.map +1 -0
- package/dist/__tests__/helpers/test-engine-factory.d.ts +98 -0
- package/dist/__tests__/helpers/test-engine-factory.d.ts.map +1 -0
- package/dist/__tests__/helpers/test-engine-factory.js +236 -0
- package/dist/__tests__/helpers/test-engine-factory.js.map +1 -0
- package/dist/__tests__/hold-win-handler.test.d.ts +2 -0
- package/dist/__tests__/hold-win-handler.test.d.ts.map +1 -0
- package/dist/__tests__/hold-win-handler.test.js +481 -0
- package/dist/__tests__/hold-win-handler.test.js.map +1 -0
- package/dist/__tests__/rng-gli19-compliance.test.d.ts +0 -4
- package/dist/__tests__/rng-gli19-compliance.test.d.ts.map +1 -1
- package/dist/__tests__/rng-gli19-compliance.test.js +232 -198
- package/dist/__tests__/rng-gli19-compliance.test.js.map +1 -1
- package/dist/__tests__/rng-security.test.d.ts +2 -0
- package/dist/__tests__/rng-security.test.d.ts.map +1 -0
- package/dist/__tests__/rng-security.test.js +379 -0
- package/dist/__tests__/rng-security.test.js.map +1 -0
- package/dist/__tests__/rtp-diagnostic.test.d.ts +6 -0
- package/dist/__tests__/rtp-diagnostic.test.d.ts.map +1 -0
- package/dist/__tests__/rtp-diagnostic.test.js +216 -0
- package/dist/__tests__/rtp-diagnostic.test.js.map +1 -0
- package/dist/__tests__/rtp-simulation.test.d.ts +0 -4
- package/dist/__tests__/rtp-simulation.test.d.ts.map +1 -1
- package/dist/__tests__/rtp-simulation.test.js +228 -153
- package/dist/__tests__/rtp-simulation.test.js.map +1 -1
- package/dist/__tests__/spin-generator.test.d.ts +2 -0
- package/dist/__tests__/spin-generator.test.d.ts.map +1 -0
- package/dist/__tests__/spin-generator.test.js +375 -0
- package/dist/__tests__/spin-generator.test.js.map +1 -0
- package/dist/__tests__/state-transitions.test.d.ts +2 -0
- package/dist/__tests__/state-transitions.test.d.ts.map +1 -0
- package/dist/__tests__/state-transitions.test.js +249 -0
- package/dist/__tests__/state-transitions.test.js.map +1 -0
- package/dist/__tests__/symbol-distribution.test.d.ts +2 -0
- package/dist/__tests__/symbol-distribution.test.d.ts.map +1 -0
- package/dist/__tests__/symbol-distribution.test.js +210 -0
- package/dist/__tests__/symbol-distribution.test.js.map +1 -0
- package/dist/__tests__/win-calculator.test.d.ts +2 -0
- package/dist/__tests__/win-calculator.test.d.ts.map +1 -0
- package/dist/__tests__/win-calculator.test.js +516 -0
- package/dist/__tests__/win-calculator.test.js.map +1 -0
- package/dist/config/game-logic-config/game-logic-config.d.ts.map +1 -1
- package/dist/config/game-logic-config/game-logic-config.js +27 -17
- package/dist/config/game-logic-config/game-logic-config.js.map +1 -1
- package/dist/logic/handlers/free-spins.handler.d.ts +2 -0
- package/dist/logic/handlers/free-spins.handler.d.ts.map +1 -1
- package/dist/logic/handlers/free-spins.handler.js +74 -6
- package/dist/logic/handlers/free-spins.handler.js.map +1 -1
- package/package.json +4 -2
- package/dist/__tests__/rng-seed-security.test.d.ts +0 -6
- package/dist/__tests__/rng-seed-security.test.d.ts.map +0 -1
- package/dist/__tests__/rng-seed-security.test.js +0 -263
- package/dist/__tests__/rng-seed-security.test.js.map +0 -1
- package/dist/__tests__/rng-seed-type.test.d.ts +0 -16
- package/dist/__tests__/rng-seed-type.test.d.ts.map +0 -1
- package/dist/__tests__/rng-seed-type.test.js +0 -288
- package/dist/__tests__/rng-seed-type.test.js.map +0 -1
- package/dist/__tests__/rng-stress-boundary.test.d.ts +0 -5
- package/dist/__tests__/rng-stress-boundary.test.d.ts.map +0 -1
- package/dist/__tests__/rng-stress-boundary.test.js +0 -395
- package/dist/__tests__/rng-stress-boundary.test.js.map +0 -1
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
/**
|
|
4
|
+
* RNG Security Tests — Lucky Sharky
|
|
5
|
+
*
|
|
6
|
+
* Tests attack surface hardening for the RNG system:
|
|
7
|
+
* - Seed injection attacks
|
|
8
|
+
* - Type confusion
|
|
9
|
+
* - Integer overflow/underflow
|
|
10
|
+
* - Replay attack prevention
|
|
11
|
+
* - RNG isolation between commands
|
|
12
|
+
*/
|
|
13
|
+
const test_engine_factory_1 = require("./helpers/test-engine-factory");
|
|
14
|
+
describe('RNG Security — Lucky Sharky', () => {
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
(0, test_engine_factory_1.resetCommandIdCounter)();
|
|
17
|
+
});
|
|
18
|
+
describe('Seed Injection Attacks', () => {
|
|
19
|
+
it('should handle undefined seed gracefully', async () => {
|
|
20
|
+
const engine = (0, test_engine_factory_1.createEngine)();
|
|
21
|
+
const session = await (0, test_engine_factory_1.initSession)(engine);
|
|
22
|
+
const result = await (0, test_engine_factory_1.executeSpin)(engine, session.publicState, session.privateState);
|
|
23
|
+
expect(result.success).toBe(true);
|
|
24
|
+
});
|
|
25
|
+
it('should handle null state gracefully for INIT', async () => {
|
|
26
|
+
const engine = (0, test_engine_factory_1.createEngine)();
|
|
27
|
+
await (0, test_engine_factory_1.waitForEngineReady)(engine);
|
|
28
|
+
const result = await engine.processCommand(null, null, {
|
|
29
|
+
id: (0, test_engine_factory_1.nextCommandId)('init'),
|
|
30
|
+
type: 'INIT_SESSION_STATE',
|
|
31
|
+
payload: { betAmountThresholds: [1.0], defaultBetAmount: 1.0 },
|
|
32
|
+
});
|
|
33
|
+
expect(result.success).toBe(true);
|
|
34
|
+
expect(result.publicState).toBeDefined();
|
|
35
|
+
expect(result.privateState).toBeDefined();
|
|
36
|
+
// Validate structure: publicState and privateState should be objects
|
|
37
|
+
expect(typeof result.publicState).toBe('object');
|
|
38
|
+
expect(typeof result.privateState).toBe('object');
|
|
39
|
+
});
|
|
40
|
+
it('should reject spin with missing public state', async () => {
|
|
41
|
+
const engine = (0, test_engine_factory_1.createEngine)();
|
|
42
|
+
const session = await (0, test_engine_factory_1.initSession)(engine);
|
|
43
|
+
try {
|
|
44
|
+
await (0, test_engine_factory_1.executeSpin)(engine, null, session.privateState);
|
|
45
|
+
// If it doesn't throw, that's also acceptable
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
expect(error).toBeInstanceOf(Error);
|
|
49
|
+
expect(error.message).toBeDefined();
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
it('should reject spin with missing private state', async () => {
|
|
53
|
+
const engine = (0, test_engine_factory_1.createEngine)();
|
|
54
|
+
const session = await (0, test_engine_factory_1.initSession)(engine);
|
|
55
|
+
try {
|
|
56
|
+
await (0, test_engine_factory_1.executeSpin)(engine, session.publicState, null);
|
|
57
|
+
// If it doesn't throw, that's also acceptable
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
expect(error).toBeInstanceOf(Error);
|
|
61
|
+
expect(error.message).toBeDefined();
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
describe('Type Confusion', () => {
|
|
66
|
+
it('should handle string bet amount', async () => {
|
|
67
|
+
const engine = (0, test_engine_factory_1.createEngine)();
|
|
68
|
+
const session = await (0, test_engine_factory_1.initSession)(engine);
|
|
69
|
+
const result = await engine.processCommand(session.publicState, session.privateState, {
|
|
70
|
+
id: (0, test_engine_factory_1.nextCommandId)('spin'),
|
|
71
|
+
type: 'SPIN',
|
|
72
|
+
payload: {
|
|
73
|
+
sessionId: 'test',
|
|
74
|
+
betAmount: '1.0',
|
|
75
|
+
creditsPerLine: 1,
|
|
76
|
+
gameId: 'lucky-sharky',
|
|
77
|
+
gameVersion: '1.0.0',
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
// Should either succeed with coercion or fail gracefully
|
|
81
|
+
expect(result).toBeDefined();
|
|
82
|
+
});
|
|
83
|
+
it('should handle negative bet amount', async () => {
|
|
84
|
+
const engine = (0, test_engine_factory_1.createEngine)();
|
|
85
|
+
const session = await (0, test_engine_factory_1.initSession)(engine);
|
|
86
|
+
try {
|
|
87
|
+
const result = await engine.processCommand(session.publicState, session.privateState, {
|
|
88
|
+
id: (0, test_engine_factory_1.nextCommandId)('spin'),
|
|
89
|
+
type: 'SPIN',
|
|
90
|
+
payload: {
|
|
91
|
+
sessionId: 'test',
|
|
92
|
+
betAmount: -1.0,
|
|
93
|
+
creditsPerLine: 1,
|
|
94
|
+
gameId: 'lucky-sharky',
|
|
95
|
+
gameVersion: '1.0.0',
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
// If no exception, verify it doesn't produce negative wins
|
|
99
|
+
if (result.success && result.outcome) {
|
|
100
|
+
expect(result.outcome.totalWin).toBeGreaterThanOrEqual(0);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
expect(error).toBeInstanceOf(Error);
|
|
105
|
+
expect(error.message).toBeDefined();
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
it('should handle zero bet amount', async () => {
|
|
109
|
+
const engine = (0, test_engine_factory_1.createEngine)();
|
|
110
|
+
const session = await (0, test_engine_factory_1.initSession)(engine);
|
|
111
|
+
try {
|
|
112
|
+
const result = await engine.processCommand(session.publicState, session.privateState, {
|
|
113
|
+
id: (0, test_engine_factory_1.nextCommandId)('spin'),
|
|
114
|
+
type: 'SPIN',
|
|
115
|
+
payload: {
|
|
116
|
+
sessionId: 'test',
|
|
117
|
+
betAmount: 0,
|
|
118
|
+
creditsPerLine: 1,
|
|
119
|
+
gameId: 'lucky-sharky',
|
|
120
|
+
gameVersion: '1.0.0',
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
// If no exception, result should have valid structure
|
|
124
|
+
expect(typeof result).toBe('object');
|
|
125
|
+
expect(typeof result.success).toBe('boolean');
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
expect(error).toBeInstanceOf(Error);
|
|
129
|
+
expect(error.message).toBeDefined();
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
it('should handle NaN bet amount', async () => {
|
|
133
|
+
const engine = (0, test_engine_factory_1.createEngine)();
|
|
134
|
+
const session = await (0, test_engine_factory_1.initSession)(engine);
|
|
135
|
+
try {
|
|
136
|
+
const result = await engine.processCommand(session.publicState, session.privateState, {
|
|
137
|
+
id: (0, test_engine_factory_1.nextCommandId)('spin'),
|
|
138
|
+
type: 'SPIN',
|
|
139
|
+
payload: {
|
|
140
|
+
sessionId: 'test',
|
|
141
|
+
betAmount: NaN,
|
|
142
|
+
creditsPerLine: 1,
|
|
143
|
+
gameId: 'lucky-sharky',
|
|
144
|
+
gameVersion: '1.0.0',
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
// If no exception, result should have valid structure
|
|
148
|
+
expect(typeof result).toBe('object');
|
|
149
|
+
expect(typeof result.success).toBe('boolean');
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
expect(error).toBeInstanceOf(Error);
|
|
153
|
+
expect(error.message).toBeDefined();
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
it('should handle Infinity bet amount', async () => {
|
|
157
|
+
const engine = (0, test_engine_factory_1.createEngine)();
|
|
158
|
+
const session = await (0, test_engine_factory_1.initSession)(engine);
|
|
159
|
+
try {
|
|
160
|
+
const result = await engine.processCommand(session.publicState, session.privateState, {
|
|
161
|
+
id: (0, test_engine_factory_1.nextCommandId)('spin'),
|
|
162
|
+
type: 'SPIN',
|
|
163
|
+
payload: {
|
|
164
|
+
sessionId: 'test',
|
|
165
|
+
betAmount: Infinity,
|
|
166
|
+
creditsPerLine: 1,
|
|
167
|
+
gameId: 'lucky-sharky',
|
|
168
|
+
gameVersion: '1.0.0',
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
// If no exception, result should have valid structure
|
|
172
|
+
expect(typeof result).toBe('object');
|
|
173
|
+
expect(typeof result.success).toBe('boolean');
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
expect(error).toBeInstanceOf(Error);
|
|
177
|
+
expect(error.message).toBeDefined();
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
describe('Integer Overflow/Underflow', () => {
|
|
182
|
+
it('should handle MAX_SAFE_INTEGER in RNG sequence', async () => {
|
|
183
|
+
const { engine } = (0, test_engine_factory_1.createEngineWithMockRng)([Number.MAX_SAFE_INTEGER]);
|
|
184
|
+
const session = await (0, test_engine_factory_1.initSession)(engine);
|
|
185
|
+
const result = await (0, test_engine_factory_1.executeSpin)(engine, session.publicState, session.privateState);
|
|
186
|
+
// Should handle gracefully - values clamped to range
|
|
187
|
+
expect(result.success).toBe(true);
|
|
188
|
+
const validation = (0, test_engine_factory_1.validateRngOutcome)(result.rngOutcome);
|
|
189
|
+
expect(validation.valid).toBe(true);
|
|
190
|
+
});
|
|
191
|
+
it('should handle negative values in RNG sequence', async () => {
|
|
192
|
+
const { engine } = (0, test_engine_factory_1.createEngineWithMockRng)([-1, -100, -999]);
|
|
193
|
+
const session = await (0, test_engine_factory_1.initSession)(engine);
|
|
194
|
+
const result = await (0, test_engine_factory_1.executeSpin)(engine, session.publicState, session.privateState);
|
|
195
|
+
expect(result.success).toBe(true);
|
|
196
|
+
// Results should be within valid bounds
|
|
197
|
+
for (const [, record] of Object.entries(result.rngOutcome || {})) {
|
|
198
|
+
const rec = record;
|
|
199
|
+
expect(rec.result).toBeGreaterThanOrEqual(rec.min);
|
|
200
|
+
expect(rec.result).toBeLessThanOrEqual(rec.max);
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
it('should handle zero values in RNG sequence', async () => {
|
|
204
|
+
const { engine } = (0, test_engine_factory_1.createEngineWithMockRng)([0, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
|
|
205
|
+
const session = await (0, test_engine_factory_1.initSession)(engine);
|
|
206
|
+
const result = await (0, test_engine_factory_1.executeSpin)(engine, session.publicState, session.privateState);
|
|
207
|
+
expect(result.success).toBe(true);
|
|
208
|
+
});
|
|
209
|
+
it('should handle large values in RNG sequence', async () => {
|
|
210
|
+
const { engine } = (0, test_engine_factory_1.createEngineWithMockRng)([999999, 888888, 777777]);
|
|
211
|
+
const session = await (0, test_engine_factory_1.initSession)(engine);
|
|
212
|
+
const result = await (0, test_engine_factory_1.executeSpin)(engine, session.publicState, session.privateState);
|
|
213
|
+
expect(result.success).toBe(true);
|
|
214
|
+
for (const [, record] of Object.entries(result.rngOutcome || {})) {
|
|
215
|
+
const rec = record;
|
|
216
|
+
expect(rec.result).toBeGreaterThanOrEqual(rec.min);
|
|
217
|
+
expect(rec.result).toBeLessThanOrEqual(rec.max);
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
describe('Prototype Pollution Defense', () => {
|
|
222
|
+
it('should not be affected by Object.prototype pollution', async () => {
|
|
223
|
+
const engine = (0, test_engine_factory_1.createEngine)();
|
|
224
|
+
const session = await (0, test_engine_factory_1.initSession)(engine);
|
|
225
|
+
// Attempt prototype pollution on state
|
|
226
|
+
const pollutedState = (0, test_engine_factory_1.cloneState)(session.privateState);
|
|
227
|
+
pollutedState.__proto__ = { malicious: true };
|
|
228
|
+
const result = await (0, test_engine_factory_1.executeSpin)(engine, session.publicState, pollutedState);
|
|
229
|
+
expect(result.success).toBe(true);
|
|
230
|
+
});
|
|
231
|
+
it('should handle extra properties on state without issues', async () => {
|
|
232
|
+
const engine = (0, test_engine_factory_1.createEngine)();
|
|
233
|
+
const session = await (0, test_engine_factory_1.initSession)(engine);
|
|
234
|
+
const modifiedState = { ...session.privateState, extraProp: 'malicious', hack: true };
|
|
235
|
+
const result = await (0, test_engine_factory_1.executeSpin)(engine, session.publicState, modifiedState);
|
|
236
|
+
expect(result.success).toBe(true);
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
describe('Replay Attack Prevention', () => {
|
|
240
|
+
it('should produce different RNG outcomes for different command IDs', async () => {
|
|
241
|
+
const engine = (0, test_engine_factory_1.createEngine)();
|
|
242
|
+
const session = await (0, test_engine_factory_1.initSession)(engine);
|
|
243
|
+
const result1 = await (0, test_engine_factory_1.executeSpin)(engine, session.publicState, session.privateState);
|
|
244
|
+
const result2 = await (0, test_engine_factory_1.executeSpin)(engine, result1.publicState, result1.privateState);
|
|
245
|
+
// Different commands should have different RNG outcomes
|
|
246
|
+
expect(result1.rngOutcome).not.toEqual(result2.rngOutcome);
|
|
247
|
+
});
|
|
248
|
+
it('should not allow state replay to get same outcome', async () => {
|
|
249
|
+
const engine = (0, test_engine_factory_1.createEngine)();
|
|
250
|
+
const session = await (0, test_engine_factory_1.initSession)(engine);
|
|
251
|
+
// Execute spin and save state before
|
|
252
|
+
const stateBefore = (0, test_engine_factory_1.cloneState)(session);
|
|
253
|
+
const result1 = await (0, test_engine_factory_1.executeSpin)(engine, session.publicState, session.privateState);
|
|
254
|
+
// Try to replay with same state (DummyRngClient uses Math.random so results will differ)
|
|
255
|
+
const result2 = await (0, test_engine_factory_1.executeSpin)(engine, stateBefore.publicState, stateBefore.privateState);
|
|
256
|
+
// With non-deterministic RNG, replaying same state should give different outcomes
|
|
257
|
+
// (unless mock is used, which is intentional for testing)
|
|
258
|
+
expect(result1).toBeDefined();
|
|
259
|
+
expect(result2).toBeDefined();
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
describe('RNG Isolation Between Commands', () => {
|
|
263
|
+
it('should not leak RNG state between init and spin', async () => {
|
|
264
|
+
const engine = (0, test_engine_factory_1.createEngine)();
|
|
265
|
+
const session = await (0, test_engine_factory_1.initSession)(engine);
|
|
266
|
+
// The spin's RNG should be independent of init's RNG
|
|
267
|
+
const result = await (0, test_engine_factory_1.executeSpin)(engine, session.publicState, session.privateState);
|
|
268
|
+
expect(result.success).toBe(true);
|
|
269
|
+
// RNG outcome should only contain actions from this spin
|
|
270
|
+
const actionIds = Object.keys(result.rngOutcome || {});
|
|
271
|
+
for (const id of actionIds) {
|
|
272
|
+
expect(id).not.toContain('init');
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
it('should isolate RNG between consecutive spins', async () => {
|
|
276
|
+
const engine = (0, test_engine_factory_1.createEngine)();
|
|
277
|
+
const session = await (0, test_engine_factory_1.initSession)(engine);
|
|
278
|
+
const result1 = await (0, test_engine_factory_1.executeSpin)(engine, session.publicState, session.privateState);
|
|
279
|
+
const result2 = await (0, test_engine_factory_1.executeSpin)(engine, result1.publicState, result1.privateState);
|
|
280
|
+
// Each spin should produce its own independent RNG outcome
|
|
281
|
+
expect(result1.rngOutcome).toBeDefined();
|
|
282
|
+
expect(result2.rngOutcome).toBeDefined();
|
|
283
|
+
// Validate structure: both must have at least 3 RNG calls
|
|
284
|
+
expect(Object.keys(result1.rngOutcome || {}).length).toBeGreaterThanOrEqual(3);
|
|
285
|
+
expect(Object.keys(result2.rngOutcome || {}).length).toBeGreaterThanOrEqual(3);
|
|
286
|
+
// Outcomes should differ (different random values even if action IDs repeat)
|
|
287
|
+
expect(result1.rngOutcome).not.toEqual(result2.rngOutcome);
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
describe('Command Type Validation', () => {
|
|
291
|
+
it('should reject unknown command types', async () => {
|
|
292
|
+
const engine = (0, test_engine_factory_1.createEngine)();
|
|
293
|
+
const session = await (0, test_engine_factory_1.initSession)(engine);
|
|
294
|
+
try {
|
|
295
|
+
const result = await engine.processCommand(session.publicState, session.privateState, {
|
|
296
|
+
id: (0, test_engine_factory_1.nextCommandId)('unknown'),
|
|
297
|
+
type: 'UNKNOWN_COMMAND',
|
|
298
|
+
payload: {},
|
|
299
|
+
});
|
|
300
|
+
// Should either fail or return error
|
|
301
|
+
expect(result.success).toBe(false);
|
|
302
|
+
}
|
|
303
|
+
catch (error) {
|
|
304
|
+
expect(error).toBeInstanceOf(Error);
|
|
305
|
+
expect(error.message).toBeDefined();
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
it('should reject empty command type', async () => {
|
|
309
|
+
const engine = (0, test_engine_factory_1.createEngine)();
|
|
310
|
+
const session = await (0, test_engine_factory_1.initSession)(engine);
|
|
311
|
+
try {
|
|
312
|
+
const result = await engine.processCommand(session.publicState, session.privateState, {
|
|
313
|
+
id: (0, test_engine_factory_1.nextCommandId)('empty'),
|
|
314
|
+
type: '',
|
|
315
|
+
payload: {},
|
|
316
|
+
});
|
|
317
|
+
expect(result.success).toBe(false);
|
|
318
|
+
}
|
|
319
|
+
catch (error) {
|
|
320
|
+
expect(error).toBeInstanceOf(Error);
|
|
321
|
+
expect(error.message).toBeDefined();
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
it('should handle missing command ID gracefully', async () => {
|
|
325
|
+
const engine = (0, test_engine_factory_1.createEngine)();
|
|
326
|
+
const session = await (0, test_engine_factory_1.initSession)(engine);
|
|
327
|
+
try {
|
|
328
|
+
const result = await engine.processCommand(session.publicState, session.privateState, {
|
|
329
|
+
id: '',
|
|
330
|
+
type: 'SPIN',
|
|
331
|
+
payload: {
|
|
332
|
+
sessionId: 'test',
|
|
333
|
+
betAmount: 1.0,
|
|
334
|
+
creditsPerLine: 1,
|
|
335
|
+
gameId: 'lucky-sharky',
|
|
336
|
+
gameVersion: '1.0.0',
|
|
337
|
+
},
|
|
338
|
+
});
|
|
339
|
+
// If no exception, result should have valid structure
|
|
340
|
+
expect(typeof result).toBe('object');
|
|
341
|
+
expect(typeof result.success).toBe('boolean');
|
|
342
|
+
}
|
|
343
|
+
catch (error) {
|
|
344
|
+
expect(error).toBeInstanceOf(Error);
|
|
345
|
+
expect(error.message).toBeDefined();
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
});
|
|
349
|
+
describe('State Integrity', () => {
|
|
350
|
+
it('should not mutate input public state', async () => {
|
|
351
|
+
const engine = (0, test_engine_factory_1.createEngine)();
|
|
352
|
+
const session = await (0, test_engine_factory_1.initSession)(engine);
|
|
353
|
+
const originalPublic = (0, test_engine_factory_1.cloneState)(session.publicState);
|
|
354
|
+
await (0, test_engine_factory_1.executeSpin)(engine, session.publicState, session.privateState);
|
|
355
|
+
expect((0, test_engine_factory_1.statesEqual)(session.publicState, originalPublic)).toBe(true);
|
|
356
|
+
});
|
|
357
|
+
it('should not mutate input private state', async () => {
|
|
358
|
+
const engine = (0, test_engine_factory_1.createEngine)();
|
|
359
|
+
const session = await (0, test_engine_factory_1.initSession)(engine);
|
|
360
|
+
const originalPrivate = (0, test_engine_factory_1.cloneState)(session.privateState);
|
|
361
|
+
await (0, test_engine_factory_1.executeSpin)(engine, session.publicState, session.privateState);
|
|
362
|
+
expect((0, test_engine_factory_1.statesEqual)(session.privateState, originalPrivate)).toBe(true);
|
|
363
|
+
});
|
|
364
|
+
it('should return valid state after each spin', async () => {
|
|
365
|
+
const engine = (0, test_engine_factory_1.createEngine)();
|
|
366
|
+
const session = await (0, test_engine_factory_1.initSession)(engine);
|
|
367
|
+
const results = await (0, test_engine_factory_1.executeSpins)(engine, session.publicState, session.privateState, 5);
|
|
368
|
+
for (const result of results) {
|
|
369
|
+
expect(result.success).toBe(true);
|
|
370
|
+
expect(result.publicState).toBeDefined();
|
|
371
|
+
expect(result.privateState).toBeDefined();
|
|
372
|
+
// Validate structure: states should be objects
|
|
373
|
+
expect(typeof result.publicState).toBe('object');
|
|
374
|
+
expect(typeof result.privateState).toBe('object');
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
});
|
|
378
|
+
});
|
|
379
|
+
//# sourceMappingURL=rng-security.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rng-security.test.js","sourceRoot":"","sources":["../../src/__tests__/rng-security.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rtp-diagnostic.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/rtp-diagnostic.test.ts"],"names":[],"mappings":"AAAA;;;GAGG"}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* RTP Diagnostic Test - Detailed breakdown for Lucky Sharky
|
|
4
|
+
* This test provides granular data to identify RTP discrepancies
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
const lucky_sharky_v1_game_engine_1 = require("../lucky-sharky-v1.game-engine");
|
|
8
|
+
const waitForPreload = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
|
9
|
+
describe('Lucky Sharky RTP Diagnostics', () => {
|
|
10
|
+
const NUM_SPINS = 100000; // 100k spins (increase for more accuracy)
|
|
11
|
+
const BET_AMOUNT = 20;
|
|
12
|
+
const BET_STAKE = 1; // betAmount / 20
|
|
13
|
+
let engine;
|
|
14
|
+
beforeAll(async () => {
|
|
15
|
+
engine = new lucky_sharky_v1_game_engine_1.LuckySharkyV1GameEngine();
|
|
16
|
+
await waitForPreload(500);
|
|
17
|
+
});
|
|
18
|
+
async function spin(publicState, privateState) {
|
|
19
|
+
return engine.processCommand(publicState, privateState, {
|
|
20
|
+
id: `spin-${Math.random().toString(36).slice(2)}`,
|
|
21
|
+
type: 'SPIN',
|
|
22
|
+
payload: {
|
|
23
|
+
sessionId: 'rtp-diag',
|
|
24
|
+
betAmount: BET_AMOUNT,
|
|
25
|
+
creditsPerLine: 20,
|
|
26
|
+
gameId: 'lucky-sharky',
|
|
27
|
+
gameVersion: '1.0.0',
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
async function startBonusRound(publicState, privateState) {
|
|
32
|
+
const pendingBonus = privateState.pendingBonuses?.[0];
|
|
33
|
+
if (!pendingBonus)
|
|
34
|
+
throw new Error('No pending bonus');
|
|
35
|
+
return engine.processCommand(publicState, privateState, {
|
|
36
|
+
id: `bonus-${Math.random().toString(36).slice(2)}`,
|
|
37
|
+
type: 'START_BONUS_ROUND',
|
|
38
|
+
payload: {
|
|
39
|
+
sessionId: 'rtp-diag',
|
|
40
|
+
betAmount: pendingBonus.betAmount || BET_AMOUNT,
|
|
41
|
+
bonusId: pendingBonus.bonusId,
|
|
42
|
+
bonusType: pendingBonus.bonusType,
|
|
43
|
+
gameCode: 'lucky-sharky',
|
|
44
|
+
gameVersion: '1.0.0',
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
it('should provide detailed RTP breakdown', async () => {
|
|
49
|
+
const initResult = await engine.processCommand(null, null, {
|
|
50
|
+
id: 'init',
|
|
51
|
+
type: 'INIT_SESSION_STATE',
|
|
52
|
+
payload: {
|
|
53
|
+
betAmountThresholds: [0.2, 1.0, 5.0, 10.0, 20.0],
|
|
54
|
+
defaultBetAmount: BET_AMOUNT,
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
let publicState = initResult.publicState;
|
|
58
|
+
let privateState = initResult.privateState;
|
|
59
|
+
// Detailed tracking
|
|
60
|
+
const stats = {
|
|
61
|
+
totalWagered: 0,
|
|
62
|
+
totalWon: 0,
|
|
63
|
+
// Paid spins
|
|
64
|
+
paidSpins: 0,
|
|
65
|
+
paidLineWins: 0,
|
|
66
|
+
paidCoinWins: 0,
|
|
67
|
+
paidScatterWins: 0,
|
|
68
|
+
// Free spins
|
|
69
|
+
freeSpinsTriggers: 0,
|
|
70
|
+
freeSpinsTotal: 0,
|
|
71
|
+
freeSpinLineWins: 0,
|
|
72
|
+
freeSpinCoinWins: 0,
|
|
73
|
+
freeSpinRetriggers: 0,
|
|
74
|
+
freeSpinRetriggerSpins: 0,
|
|
75
|
+
avgBoosterAtTrigger: 0,
|
|
76
|
+
boosterAtTriggerSum: 0,
|
|
77
|
+
// Hold-Win
|
|
78
|
+
holdWinTriggers: 0,
|
|
79
|
+
holdWinWins: 0,
|
|
80
|
+
};
|
|
81
|
+
console.log(`Running RTP diagnostics with ${NUM_SPINS.toLocaleString()} spins...`);
|
|
82
|
+
const startTime = Date.now();
|
|
83
|
+
for (let i = 0; i < NUM_SPINS; i++) {
|
|
84
|
+
stats.totalWagered += BET_AMOUNT;
|
|
85
|
+
stats.paidSpins++;
|
|
86
|
+
const spinResult = await spin(publicState, privateState);
|
|
87
|
+
if (!spinResult.success)
|
|
88
|
+
throw new Error(`Spin failed: ${spinResult.message}`);
|
|
89
|
+
const outcome = spinResult.outcome;
|
|
90
|
+
// Track paid spin wins
|
|
91
|
+
if (outcome?.result) {
|
|
92
|
+
const result = outcome.result;
|
|
93
|
+
// Line wins
|
|
94
|
+
if (result.winResult?.totalWin) {
|
|
95
|
+
stats.paidLineWins += result.winResult.totalWin;
|
|
96
|
+
stats.totalWon += result.winResult.totalWin;
|
|
97
|
+
}
|
|
98
|
+
// Coin wins
|
|
99
|
+
if (result.coinCollection?.finalWin) {
|
|
100
|
+
stats.paidCoinWins += result.coinCollection.finalWin;
|
|
101
|
+
stats.totalWon += result.coinCollection.finalWin;
|
|
102
|
+
}
|
|
103
|
+
// Scatter wins
|
|
104
|
+
if (result.scatterWin) {
|
|
105
|
+
stats.paidScatterWins += result.scatterWin;
|
|
106
|
+
stats.totalWon += result.scatterWin;
|
|
107
|
+
}
|
|
108
|
+
// Check for free spins trigger
|
|
109
|
+
if (result.triggeredBonus?.bonusType === 'FREE_SPINS') {
|
|
110
|
+
stats.freeSpinsTriggers++;
|
|
111
|
+
stats.boosterAtTriggerSum += publicState.boosterLevel || 0;
|
|
112
|
+
}
|
|
113
|
+
// Check for hold-win trigger
|
|
114
|
+
if (result.triggeredBonus?.bonusType === 'HOLD_WIN_BONUS') {
|
|
115
|
+
stats.holdWinTriggers++;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
publicState = spinResult.publicState;
|
|
119
|
+
privateState = spinResult.privateState;
|
|
120
|
+
// Handle bonuses
|
|
121
|
+
while (privateState.pendingBonuses && privateState.pendingBonuses.length > 0) {
|
|
122
|
+
const bonusResult = await startBonusRound(publicState, privateState);
|
|
123
|
+
if (!bonusResult.success)
|
|
124
|
+
throw new Error(`Bonus failed`);
|
|
125
|
+
const bonusOutcome = bonusResult.outcome;
|
|
126
|
+
if (Array.isArray(bonusOutcome?.results)) {
|
|
127
|
+
const results = bonusOutcome.results;
|
|
128
|
+
const firstResult = results[0]?.result;
|
|
129
|
+
if (firstResult?.spinType === 'FREE_SPIN') {
|
|
130
|
+
stats.freeSpinsTotal += results.length;
|
|
131
|
+
for (const r of results) {
|
|
132
|
+
const res = r.result;
|
|
133
|
+
if (res?.winResult?.totalWin) {
|
|
134
|
+
stats.freeSpinLineWins += res.winResult.totalWin;
|
|
135
|
+
stats.totalWon += res.winResult.totalWin;
|
|
136
|
+
}
|
|
137
|
+
if (res?.coinCollection?.finalWin) {
|
|
138
|
+
stats.freeSpinCoinWins += res.coinCollection.finalWin;
|
|
139
|
+
stats.totalWon += res.coinCollection.finalWin;
|
|
140
|
+
}
|
|
141
|
+
if (res?.retrigger?.triggered) {
|
|
142
|
+
stats.freeSpinRetriggers++;
|
|
143
|
+
stats.freeSpinRetriggerSpins += res.retrigger.additionalSpins || 0;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
else if (firstResult?.spinType === 'HOLD_WIN_BONUS_SPIN') {
|
|
148
|
+
for (const r of results) {
|
|
149
|
+
const res = r.result;
|
|
150
|
+
if (res?.playerWinning) {
|
|
151
|
+
stats.holdWinWins += res.playerWinning;
|
|
152
|
+
stats.totalWon += res.playerWinning;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
publicState = bonusResult.publicState;
|
|
158
|
+
privateState = bonusResult.privateState;
|
|
159
|
+
}
|
|
160
|
+
if ((i + 1) % 10000 === 0) {
|
|
161
|
+
const rtp = (stats.totalWon / stats.totalWagered) * 100;
|
|
162
|
+
console.log(` Progress: ${((i + 1) / NUM_SPINS * 100).toFixed(0)}% - RTP: ${rtp.toFixed(2)}%`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
const elapsed = (Date.now() - startTime) / 1000;
|
|
166
|
+
// Calculate percentages
|
|
167
|
+
const paidLineRtp = (stats.paidLineWins / stats.totalWagered) * 100;
|
|
168
|
+
const paidCoinRtp = (stats.paidCoinWins / stats.totalWagered) * 100;
|
|
169
|
+
const paidScatterRtp = (stats.paidScatterWins / stats.totalWagered) * 100;
|
|
170
|
+
const paidTotalRtp = paidLineRtp + paidCoinRtp + paidScatterRtp;
|
|
171
|
+
const freeLineRtp = (stats.freeSpinLineWins / stats.totalWagered) * 100;
|
|
172
|
+
const freeCoinRtp = (stats.freeSpinCoinWins / stats.totalWagered) * 100;
|
|
173
|
+
const freeTotalRtp = freeLineRtp + freeCoinRtp;
|
|
174
|
+
const holdWinRtp = (stats.holdWinWins / stats.totalWagered) * 100;
|
|
175
|
+
const totalRtp = (stats.totalWon / stats.totalWagered) * 100;
|
|
176
|
+
const avgSpinsPerSeries = stats.freeSpinsTriggers > 0
|
|
177
|
+
? stats.freeSpinsTotal / stats.freeSpinsTriggers
|
|
178
|
+
: 0;
|
|
179
|
+
const avgBooster = stats.freeSpinsTriggers > 0
|
|
180
|
+
? stats.boosterAtTriggerSum / stats.freeSpinsTriggers
|
|
181
|
+
: 0;
|
|
182
|
+
console.log('\n========================================');
|
|
183
|
+
console.log(' RTP DIAGNOSTIC RESULTS');
|
|
184
|
+
console.log('========================================');
|
|
185
|
+
console.log(`\nTotal wagered: ${stats.totalWagered.toLocaleString()}`);
|
|
186
|
+
console.log(`Total won: ${stats.totalWon.toFixed(2)}`);
|
|
187
|
+
console.log(`Total RTP: ${totalRtp.toFixed(2)}% (target: 96.17%)`);
|
|
188
|
+
console.log('\n--- PAID SPINS (target: 63.64%) ---');
|
|
189
|
+
console.log(` Line wins: ${paidLineRtp.toFixed(2)}% (target: 37.29%)`);
|
|
190
|
+
console.log(` Coin wins: ${paidCoinRtp.toFixed(2)}% (target: 26.35%)`);
|
|
191
|
+
console.log(` Scatter wins: ${paidScatterRtp.toFixed(2)}%`);
|
|
192
|
+
console.log(` TOTAL: ${paidTotalRtp.toFixed(2)}%`);
|
|
193
|
+
console.log('\n--- FREE SPINS (target: 16.13%) ---');
|
|
194
|
+
console.log(` Triggers: ${stats.freeSpinsTriggers} (${(stats.freeSpinsTriggers / stats.paidSpins * 100).toFixed(3)}%)`);
|
|
195
|
+
console.log(` Total spins: ${stats.freeSpinsTotal}`);
|
|
196
|
+
console.log(` Avg per series: ${avgSpinsPerSeries.toFixed(1)}`);
|
|
197
|
+
console.log(` Retriggers: ${stats.freeSpinRetriggers} (+${stats.freeSpinRetriggerSpins} spins)`);
|
|
198
|
+
console.log(` Avg booster at trigger: ${avgBooster.toFixed(2)}`);
|
|
199
|
+
console.log(` Line wins: ${freeLineRtp.toFixed(2)}% (target: 11.76%)`);
|
|
200
|
+
console.log(` Coin wins: ${freeCoinRtp.toFixed(2)}% (target: 4.37%)`);
|
|
201
|
+
console.log(` TOTAL: ${freeTotalRtp.toFixed(2)}%`);
|
|
202
|
+
console.log('\n--- HOLD-WIN BONUS (target: 16.40%) ---');
|
|
203
|
+
console.log(` Triggers: ${stats.holdWinTriggers}`);
|
|
204
|
+
console.log(` TOTAL: ${holdWinRtp.toFixed(2)}%`);
|
|
205
|
+
console.log('\n--- DISCREPANCY ANALYSIS ---');
|
|
206
|
+
console.log(` Paid spins: ${(paidTotalRtp - 63.64).toFixed(2)}% (${paidTotalRtp > 63.64 ? 'HIGH' : 'LOW'})`);
|
|
207
|
+
console.log(` Free spins: ${(freeTotalRtp - 16.13).toFixed(2)}% (${freeTotalRtp > 16.13 ? 'HIGH' : 'LOW'})`);
|
|
208
|
+
console.log(` Hold-Win: ${(holdWinRtp - 16.40).toFixed(2)}% (${holdWinRtp > 16.40 ? 'HIGH' : 'LOW'})`);
|
|
209
|
+
console.log(` TOTAL GAP: ${(totalRtp - 96.17).toFixed(2)}%`);
|
|
210
|
+
console.log(`\nTime: ${elapsed.toFixed(1)}s`);
|
|
211
|
+
console.log('========================================\n');
|
|
212
|
+
// Pass if we got meaningful data
|
|
213
|
+
expect(stats.totalWon).toBeGreaterThan(0);
|
|
214
|
+
}, 600000);
|
|
215
|
+
});
|
|
216
|
+
//# sourceMappingURL=rtp-diagnostic.test.js.map
|