@omnitronix/bonnys-fortune-game-engine 1.7.2 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/__tests__/base-game.test.js +17 -10
- package/dist/__tests__/base-game.test.js.map +1 -1
- package/dist/__tests__/bonus-handlers.test.js +15 -21
- package/dist/__tests__/bonus-handlers.test.js.map +1 -1
- package/dist/__tests__/bonus-meters.test.js.map +1 -1
- package/dist/__tests__/collect-feature-handler.test.js +30 -24
- package/dist/__tests__/collect-feature-handler.test.js.map +1 -1
- package/dist/__tests__/comprehensive.test.js.map +1 -1
- package/dist/__tests__/error-paths.test.js +11 -7
- package/dist/__tests__/error-paths.test.js.map +1 -1
- package/dist/__tests__/helpers/test-engine-factory.d.ts +72 -18
- package/dist/__tests__/helpers/test-engine-factory.js +32 -7
- package/dist/__tests__/helpers/test-engine-factory.js.map +1 -1
- package/dist/__tests__/integration-1000-spin.test.js +33 -37
- package/dist/__tests__/integration-1000-spin.test.js.map +1 -1
- package/dist/__tests__/rng-gli19-compliance.test.js.map +1 -1
- package/dist/__tests__/rng-security.test.js +0 -1
- package/dist/__tests__/rng-security.test.js.map +1 -1
- package/dist/__tests__/rtp-simulation.test.js +11 -10
- package/dist/__tests__/rtp-simulation.test.js.map +1 -1
- package/dist/__tests__/state-transitions.test.js +0 -1
- package/dist/__tests__/state-transitions.test.js.map +1 -1
- package/dist/__tests__/steering-fortune-handler.test.js +18 -14
- package/dist/__tests__/steering-fortune-handler.test.js.map +1 -1
- package/dist/__tests__/symbol-distribution.test.js +13 -16
- package/dist/__tests__/symbol-distribution.test.js.map +1 -1
- package/dist/__tests__/treasure-hunt-handler.test.js +23 -16
- package/dist/__tests__/treasure-hunt-handler.test.js.map +1 -1
- package/dist/__tests__/win-calculator.test.js +10 -7
- package/dist/__tests__/win-calculator.test.js.map +1 -1
- package/dist/bonnys-fortune-v1.game-engine.d.ts +3 -3
- package/dist/bonnys-fortune-v1.game-engine.js +8 -5
- package/dist/bonnys-fortune-v1.game-engine.js.map +1 -1
- package/dist/game-engine.interface.d.ts +1 -1
- package/dist/helpers/optional-boolean-mapper.d.ts +1 -1
- package/dist/helpers/validation-helper.d.ts +1 -1
- package/dist/logic/bonnys-fortune.game-logic.js +2 -0
- package/dist/logic/bonnys-fortune.game-logic.js.map +1 -1
- package/dist/logic/bonnys-fortune.types.d.ts +7 -1
- package/dist/logic/handlers/collect-feature-bonus.handler.js.map +1 -1
- package/dist/logic/handlers/steering-to-the-fortune-bonus.handler.js +6 -5
- package/dist/logic/handlers/steering-to-the-fortune-bonus.handler.js.map +1 -1
- package/dist/logic/handlers/treasure-hunt-bonus.handler.js.map +1 -1
- package/dist/rng/rng-client.factory.js +3 -3
- package/dist/rng/rng-client.factory.js.map +1 -1
- package/dist/validation/bonnys-fortune/bonnys-fortune-config.dto.js.map +1 -1
- package/dist/validation/custom-decorators/IsNestedIntArray.js.map +1 -1
- package/dist/validation/game-logic-config-validation.service.d.ts +1 -1
- package/dist/validation/game-logic-config-validation.service.js.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/base-game.test.ts +26 -17
- package/src/__tests__/bonus-handlers.test.ts +16 -21
- package/src/__tests__/bonus-meters.test.ts +13 -8
- package/src/__tests__/collect-feature-handler.test.ts +39 -26
- package/src/__tests__/comprehensive.test.ts +40 -36
- package/src/__tests__/error-paths.test.ts +29 -18
- package/src/__tests__/helpers/test-engine-factory.ts +127 -40
- package/src/__tests__/integration-1000-spin.test.ts +53 -48
- package/src/__tests__/rng-gli19-compliance.test.ts +17 -17
- package/src/__tests__/rng-security.test.ts +11 -8
- package/src/__tests__/rtp-simulation.test.ts +17 -10
- package/src/__tests__/state-transitions.test.ts +8 -4
- package/src/__tests__/steering-fortune-handler.test.ts +30 -18
- package/src/__tests__/symbol-distribution.test.ts +19 -16
- package/src/__tests__/treasure-hunt-handler.test.ts +36 -22
- package/src/__tests__/win-calculator.test.ts +14 -10
- package/src/bonnys-fortune-v1.game-engine.ts +18 -8
- package/src/domain/types/game-symbols.response.dto.ts +1 -1
- package/src/game-engine.interface.ts +1 -1
- package/src/helpers/validation-helper.ts +1 -1
- package/src/logic/bonnys-fortune.game-logic.ts +3 -0
- package/src/logic/bonnys-fortune.types.ts +10 -1
- package/src/logic/handlers/collect-feature-bonus.handler.ts +5 -2
- package/src/logic/handlers/steering-to-the-fortune-bonus.handler.ts +7 -6
- package/src/logic/handlers/treasure-hunt-bonus.handler.ts +2 -2
- package/src/rng/rng-client.factory.ts +3 -3
- package/src/validation/bonnys-fortune/bonnys-fortune-config.dto.ts +72 -72
- package/src/validation/custom-decorators/IsNestedIntArray.ts +1 -1
- package/src/validation/game-logic-config-validation.service.ts +2 -2
|
@@ -65,7 +65,7 @@ describe('GLI-19 Compliance — Bonnys Fortune', () => {
|
|
|
65
65
|
const session = await initSession(engine);
|
|
66
66
|
const result = await executeSpin(engine, session.publicState, session.privateState);
|
|
67
67
|
|
|
68
|
-
const outcome = result.rngOutcome
|
|
68
|
+
const outcome = result.rngOutcome!;
|
|
69
69
|
const validation = validateRngOutcome(outcome);
|
|
70
70
|
|
|
71
71
|
expect(validation.valid).toBe(true);
|
|
@@ -115,7 +115,7 @@ describe('GLI-19 Compliance — Bonnys Fortune', () => {
|
|
|
115
115
|
expect(result.rngOutcome).toBeDefined();
|
|
116
116
|
expect(Object.keys(result.rngOutcome || {}).length).toBeGreaterThan(0);
|
|
117
117
|
|
|
118
|
-
const validation = validateRngOutcome(result.rngOutcome);
|
|
118
|
+
const validation = validateRngOutcome(result.rngOutcome!);
|
|
119
119
|
expect(validation.valid).toBe(true);
|
|
120
120
|
|
|
121
121
|
pub = result.publicState;
|
|
@@ -154,7 +154,7 @@ describe('GLI-19 Compliance — Bonnys Fortune', () => {
|
|
|
154
154
|
const session = await initSession(engine);
|
|
155
155
|
const result = await executeSpin(engine, session.publicState, session.privateState);
|
|
156
156
|
|
|
157
|
-
const hash = hashRngOutcome(result.rngOutcome);
|
|
157
|
+
const hash = hashRngOutcome(result.rngOutcome!);
|
|
158
158
|
expect(isValidSha256(hash)).toBe(true);
|
|
159
159
|
});
|
|
160
160
|
|
|
@@ -165,8 +165,8 @@ describe('GLI-19 Compliance — Bonnys Fortune', () => {
|
|
|
165
165
|
const result1 = await executeSpin(engine, session.publicState, session.privateState);
|
|
166
166
|
const result2 = await executeSpin(engine, result1.publicState, result1.privateState);
|
|
167
167
|
|
|
168
|
-
const hash1 = hashRngOutcome(result1.rngOutcome);
|
|
169
|
-
const hash2 = hashRngOutcome(result2.rngOutcome);
|
|
168
|
+
const hash1 = hashRngOutcome(result1.rngOutcome!);
|
|
169
|
+
const hash2 = hashRngOutcome(result2.rngOutcome!);
|
|
170
170
|
|
|
171
171
|
expect(hash1).not.toBe(hash2);
|
|
172
172
|
});
|
|
@@ -176,8 +176,8 @@ describe('GLI-19 Compliance — Bonnys Fortune', () => {
|
|
|
176
176
|
const session = await initSession(engine);
|
|
177
177
|
const result = await executeSpin(engine, session.publicState, session.privateState);
|
|
178
178
|
|
|
179
|
-
const hash1 = hashRngOutcome(result.rngOutcome);
|
|
180
|
-
const hash2 = hashRngOutcome(result.rngOutcome);
|
|
179
|
+
const hash1 = hashRngOutcome(result.rngOutcome!);
|
|
180
|
+
const hash2 = hashRngOutcome(result.rngOutcome!);
|
|
181
181
|
|
|
182
182
|
expect(hash1).toBe(hash2);
|
|
183
183
|
});
|
|
@@ -187,7 +187,7 @@ describe('GLI-19 Compliance — Bonnys Fortune', () => {
|
|
|
187
187
|
const session = await initSession(engine);
|
|
188
188
|
const result = await executeSpin(engine, session.publicState, session.privateState);
|
|
189
189
|
|
|
190
|
-
const originalHash = hashRngOutcome(result.rngOutcome);
|
|
190
|
+
const originalHash = hashRngOutcome(result.rngOutcome!);
|
|
191
191
|
|
|
192
192
|
interface RngOutcomeRecord {
|
|
193
193
|
result: number;
|
|
@@ -217,9 +217,9 @@ describe('GLI-19 Compliance — Bonnys Fortune', () => {
|
|
|
217
217
|
max: number;
|
|
218
218
|
}
|
|
219
219
|
|
|
220
|
-
const canonical1 = canonicalizeRngOutcome(result.rngOutcome);
|
|
220
|
+
const canonical1 = canonicalizeRngOutcome(result.rngOutcome!);
|
|
221
221
|
const reversed: Record<string, RngOutcomeRecord> = {};
|
|
222
|
-
const keys = Object.keys(result.rngOutcome).reverse();
|
|
222
|
+
const keys = Object.keys(result.rngOutcome!).reverse();
|
|
223
223
|
for (const key of keys) {
|
|
224
224
|
reversed[key] = (result.rngOutcome as Record<string, RngOutcomeRecord>)[key];
|
|
225
225
|
}
|
|
@@ -255,8 +255,8 @@ describe('GLI-19 Compliance — Bonnys Fortune', () => {
|
|
|
255
255
|
const session2 = await initSession(engine2);
|
|
256
256
|
const result2 = await executeSpin(engine2, session2.publicState, session2.privateState);
|
|
257
257
|
|
|
258
|
-
const hash1 = hashRngOutcome(result1.rngOutcome);
|
|
259
|
-
const hash2 = hashRngOutcome(result2.rngOutcome);
|
|
258
|
+
const hash1 = hashRngOutcome(result1.rngOutcome!);
|
|
259
|
+
const hash2 = hashRngOutcome(result2.rngOutcome!);
|
|
260
260
|
expect(hash1).toBe(hash2);
|
|
261
261
|
});
|
|
262
262
|
|
|
@@ -269,8 +269,8 @@ describe('GLI-19 Compliance — Bonnys Fortune', () => {
|
|
|
269
269
|
const session2 = await initSession(engine2);
|
|
270
270
|
const result2 = await executeSpin(engine2, session2.publicState, session2.privateState);
|
|
271
271
|
|
|
272
|
-
const hash1 = hashRngOutcome(result1.rngOutcome);
|
|
273
|
-
const hash2 = hashRngOutcome(result2.rngOutcome);
|
|
272
|
+
const hash1 = hashRngOutcome(result1.rngOutcome!);
|
|
273
|
+
const hash2 = hashRngOutcome(result2.rngOutcome!);
|
|
274
274
|
expect(hash1).not.toBe(hash2);
|
|
275
275
|
});
|
|
276
276
|
});
|
|
@@ -305,7 +305,7 @@ describe('GLI-19 Compliance — Bonnys Fortune', () => {
|
|
|
305
305
|
}
|
|
306
306
|
|
|
307
307
|
const result = await executeSpin(engine, pub, priv);
|
|
308
|
-
const hash = hashRngOutcome(result.rngOutcome);
|
|
308
|
+
const hash = hashRngOutcome(result.rngOutcome!);
|
|
309
309
|
expect(isValidSha256(hash)).toBe(true);
|
|
310
310
|
hashChain.push(hash);
|
|
311
311
|
|
|
@@ -330,7 +330,7 @@ describe('GLI-19 Compliance — Bonnys Fortune', () => {
|
|
|
330
330
|
|
|
331
331
|
for (let i = 0; i < 3; i++) {
|
|
332
332
|
const result = await executeSpin(engine, pub, priv);
|
|
333
|
-
const outcomeHash = hashRngOutcome(result.rngOutcome);
|
|
333
|
+
const outcomeHash = hashRngOutcome(result.rngOutcome!);
|
|
334
334
|
const chainedHash = hashString(prevHash + outcomeHash);
|
|
335
335
|
|
|
336
336
|
expect(isValidSha256(chainedHash)).toBe(true);
|
|
@@ -412,7 +412,7 @@ describe('GLI-19 Compliance — Bonnys Fortune', () => {
|
|
|
412
412
|
|
|
413
413
|
for (let i = 0; i < 10; i++) {
|
|
414
414
|
const result = await executeSpin(engine, pub, priv);
|
|
415
|
-
const validation = validateRngOutcome(result.rngOutcome);
|
|
415
|
+
const validation = validateRngOutcome(result.rngOutcome!);
|
|
416
416
|
expect(validation.valid).toBe(true);
|
|
417
417
|
|
|
418
418
|
pub = result.publicState;
|
|
@@ -24,6 +24,10 @@ import {
|
|
|
24
24
|
validateRngOutcome,
|
|
25
25
|
waitForEngineReady,
|
|
26
26
|
} from './helpers/test-engine-factory';
|
|
27
|
+
import {
|
|
28
|
+
BonnysFortunePublicState,
|
|
29
|
+
BonnysFortunePrivateState,
|
|
30
|
+
} from '../logic/bonnys-fortune.types';
|
|
27
31
|
|
|
28
32
|
describe('RNG Security — Bonnys Fortune', () => {
|
|
29
33
|
beforeEach(() => {
|
|
@@ -42,8 +46,7 @@ describe('RNG Security — Bonnys Fortune', () => {
|
|
|
42
46
|
it('should handle null state gracefully for INIT', async () => {
|
|
43
47
|
const engine = createEngine();
|
|
44
48
|
await waitForEngineReady(engine);
|
|
45
|
-
|
|
46
|
-
const result = await engine.processCommand(null as any, null as any, {
|
|
49
|
+
const result = await engine.processCommand(null as unknown as BonnysFortunePublicState, null as unknown as BonnysFortunePrivateState, {
|
|
47
50
|
id: nextCommandId('init'),
|
|
48
51
|
type: 'INIT_SESSION_STATE',
|
|
49
52
|
payload: { betAmountThresholds: [1.0], defaultBetAmount: 1.0 },
|
|
@@ -61,7 +64,7 @@ describe('RNG Security — Bonnys Fortune', () => {
|
|
|
61
64
|
const session = await initSession(engine);
|
|
62
65
|
|
|
63
66
|
try {
|
|
64
|
-
await executeSpin(engine, null, session.privateState);
|
|
67
|
+
await executeSpin(engine, null as unknown as BonnysFortunePublicState, session.privateState);
|
|
65
68
|
// If no exception, test should still pass as the engine handled it
|
|
66
69
|
} catch (error) {
|
|
67
70
|
expect(error).toBeInstanceOf(Error);
|
|
@@ -74,7 +77,7 @@ describe('RNG Security — Bonnys Fortune', () => {
|
|
|
74
77
|
const session = await initSession(engine);
|
|
75
78
|
|
|
76
79
|
try {
|
|
77
|
-
await executeSpin(engine, session.publicState, null);
|
|
80
|
+
await executeSpin(engine, session.publicState, null as unknown as BonnysFortunePrivateState);
|
|
78
81
|
// If no exception, test should still pass as the engine handled it
|
|
79
82
|
} catch (error) {
|
|
80
83
|
expect(error).toBeInstanceOf(Error);
|
|
@@ -193,7 +196,7 @@ describe('RNG Security — Bonnys Fortune', () => {
|
|
|
193
196
|
const result = await executeSpin(engine, session.publicState, session.privateState);
|
|
194
197
|
|
|
195
198
|
expect(result.success).toBe(true);
|
|
196
|
-
const validation = validateRngOutcome(result.rngOutcome);
|
|
199
|
+
const validation = validateRngOutcome(result.rngOutcome!);
|
|
197
200
|
expect(validation.valid).toBe(true);
|
|
198
201
|
});
|
|
199
202
|
|
|
@@ -251,10 +254,10 @@ describe('RNG Security — Bonnys Fortune', () => {
|
|
|
251
254
|
const engine = createEngine();
|
|
252
255
|
const session = await initSession(engine);
|
|
253
256
|
|
|
254
|
-
const pollutedState = cloneState(session.privateState) as Record<string, unknown>;
|
|
257
|
+
const pollutedState = cloneState(session.privateState) as unknown as Record<string, unknown>;
|
|
255
258
|
Object.setPrototypeOf(pollutedState, { malicious: true });
|
|
256
259
|
|
|
257
|
-
const result = await executeSpin(engine, session.publicState, pollutedState);
|
|
260
|
+
const result = await executeSpin(engine, session.publicState, pollutedState as unknown as BonnysFortunePrivateState);
|
|
258
261
|
expect(result.success).toBe(true);
|
|
259
262
|
expect(result.publicState).toBeDefined();
|
|
260
263
|
expect(typeof result.publicState).toBe('object');
|
|
@@ -288,7 +291,7 @@ describe('RNG Security — Bonnys Fortune', () => {
|
|
|
288
291
|
|
|
289
292
|
const stateBefore = cloneState(session) as { publicState: object; privateState: object };
|
|
290
293
|
const result1 = await executeSpin(engine, session.publicState, session.privateState);
|
|
291
|
-
const result2 = await executeSpin(engine, stateBefore.publicState, stateBefore.privateState);
|
|
294
|
+
const result2 = await executeSpin(engine, stateBefore.publicState as BonnysFortunePublicState, stateBefore.privateState as BonnysFortunePrivateState);
|
|
292
295
|
|
|
293
296
|
expect(result1).toBeDefined();
|
|
294
297
|
expect(result1.success).toBe(true);
|
|
@@ -19,9 +19,24 @@ import {
|
|
|
19
19
|
extractFinalBonusState,
|
|
20
20
|
nextCommandId,
|
|
21
21
|
resetCommandIdCounter,
|
|
22
|
+
BonnysFortuneCommandResult,
|
|
22
23
|
} from './helpers/test-engine-factory';
|
|
23
24
|
import { StatisticalValidator } from '@omnitronix/game-test-utils';
|
|
24
25
|
|
|
26
|
+
/** Extract playerWinning from a command result outcome. */
|
|
27
|
+
function extractPlayerWinning(result: BonnysFortuneCommandResult): number {
|
|
28
|
+
const outcome = result.outcome as unknown as {
|
|
29
|
+
result?: { playerWinning?: number; totalWin?: number };
|
|
30
|
+
playerWinning?: number;
|
|
31
|
+
totalWin?: number;
|
|
32
|
+
} | null;
|
|
33
|
+
return outcome?.result?.playerWinning
|
|
34
|
+
?? outcome?.playerWinning
|
|
35
|
+
?? outcome?.result?.totalWin
|
|
36
|
+
?? outcome?.totalWin
|
|
37
|
+
?? 0;
|
|
38
|
+
}
|
|
39
|
+
|
|
25
40
|
// ---------------------------------------------------------------------------
|
|
26
41
|
// Simulation helpers
|
|
27
42
|
// ---------------------------------------------------------------------------
|
|
@@ -82,11 +97,7 @@ async function runSimulation(
|
|
|
82
97
|
const bonusResult = await startBonusRound(engine, pub, priv, bonusType, betAmount);
|
|
83
98
|
if (bonusResult.success) {
|
|
84
99
|
// BF generates all bonus results upfront
|
|
85
|
-
const bonusWin = (bonusResult
|
|
86
|
-
?? (bonusResult.outcome as any)?.playerWinning
|
|
87
|
-
?? (bonusResult.outcome as any)?.result?.totalWin
|
|
88
|
-
?? (bonusResult.outcome as any)?.totalWin
|
|
89
|
-
?? 0;
|
|
100
|
+
const bonusWin = extractPlayerWinning(bonusResult);
|
|
90
101
|
result.totalWon += bonusWin;
|
|
91
102
|
result.bonusRounds++;
|
|
92
103
|
|
|
@@ -118,11 +129,7 @@ async function runSimulation(
|
|
|
118
129
|
result.totalWagered += betAmount;
|
|
119
130
|
|
|
120
131
|
// BF uses outcome.result.playerWinning or outcome.playerWinning
|
|
121
|
-
const totalWin = (spinResult
|
|
122
|
-
?? (spinResult.outcome as any)?.playerWinning
|
|
123
|
-
?? (spinResult.outcome as any)?.result?.totalWin
|
|
124
|
-
?? (spinResult.outcome as any)?.totalWin
|
|
125
|
-
?? 0;
|
|
132
|
+
const totalWin = extractPlayerWinning(spinResult);
|
|
126
133
|
result.totalWon += totalWin;
|
|
127
134
|
|
|
128
135
|
// Classify win
|
|
@@ -23,6 +23,11 @@ import {
|
|
|
23
23
|
resetCommandIdCounter,
|
|
24
24
|
waitForEngineReady,
|
|
25
25
|
} from './helpers/test-engine-factory';
|
|
26
|
+
import {
|
|
27
|
+
BonnysFortunePublicState,
|
|
28
|
+
BonnysFortunePrivateState,
|
|
29
|
+
BonusTrigger,
|
|
30
|
+
} from '../logic/bonnys-fortune.types';
|
|
26
31
|
|
|
27
32
|
describe('State Transitions — Bonnys Fortune', () => {
|
|
28
33
|
beforeEach(() => {
|
|
@@ -266,7 +271,7 @@ describe('State Transitions — Bonnys Fortune', () => {
|
|
|
266
271
|
expect(bonusResult.success).toBe(true);
|
|
267
272
|
// Pending bonus for this type should be consumed
|
|
268
273
|
const remaining = bonusResult.privateState.pendingBonuses.filter(
|
|
269
|
-
(b:
|
|
274
|
+
(b: BonusTrigger) => b.bonusId === bonus.bonusId,
|
|
270
275
|
);
|
|
271
276
|
expect(remaining.length).toBe(0);
|
|
272
277
|
});
|
|
@@ -320,8 +325,7 @@ describe('State Transitions — Bonnys Fortune', () => {
|
|
|
320
325
|
const engine = createEngine();
|
|
321
326
|
await waitForEngineReady(engine);
|
|
322
327
|
|
|
323
|
-
|
|
324
|
-
const result = await engine.processCommand(null as any, null as any, {
|
|
328
|
+
const result = await engine.processCommand(null as unknown as BonnysFortunePublicState, null as unknown as BonnysFortunePrivateState, {
|
|
325
329
|
id: nextCommandId('symbols'),
|
|
326
330
|
type: 'GET_SYMBOLS',
|
|
327
331
|
payload: {},
|
|
@@ -345,7 +349,7 @@ describe('State Transitions — Bonnys Fortune', () => {
|
|
|
345
349
|
};
|
|
346
350
|
|
|
347
351
|
try {
|
|
348
|
-
const result = await executeSpin(engine, session.publicState, modifiedPriv);
|
|
352
|
+
const result = await executeSpin(engine, session.publicState, modifiedPriv as BonnysFortunePrivateState);
|
|
349
353
|
// Should fail or throw
|
|
350
354
|
if (result.success === false) {
|
|
351
355
|
expect(result.success).toBe(false);
|
|
@@ -20,7 +20,19 @@ import {
|
|
|
20
20
|
resetCommandIdCounter,
|
|
21
21
|
MockRngClient,
|
|
22
22
|
} from './helpers/test-engine-factory';
|
|
23
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
SpinType,
|
|
25
|
+
PrizeType,
|
|
26
|
+
SteeringToTheFortuneResult,
|
|
27
|
+
SpinResultResponse,
|
|
28
|
+
BonusRoundResult,
|
|
29
|
+
} from '../logic/bonnys-fortune.types';
|
|
30
|
+
import { BonnysFortuneCommandResult } from './helpers/test-engine-factory';
|
|
31
|
+
|
|
32
|
+
/** Helper to extract steering fortune results from a bonus command result */
|
|
33
|
+
function getSteeringResults(result: BonnysFortuneCommandResult): SpinResultResponse<SteeringToTheFortuneResult>[] {
|
|
34
|
+
return (result.outcome as unknown as BonusRoundResult<SteeringToTheFortuneResult>).results;
|
|
35
|
+
}
|
|
24
36
|
|
|
25
37
|
describe('Steering Fortune Handler - Bonnys Fortune', () => {
|
|
26
38
|
beforeEach(() => {
|
|
@@ -50,9 +62,9 @@ describe('Steering Fortune Handler - Bonnys Fortune', () => {
|
|
|
50
62
|
);
|
|
51
63
|
|
|
52
64
|
expect(bonusResult.success).toBe(true);
|
|
53
|
-
expect(bonusResult
|
|
65
|
+
expect(getSteeringResults(bonusResult)).toBeDefined();
|
|
54
66
|
// Steering Fortune produces exactly 1 result per spin
|
|
55
|
-
expect(bonusResult.
|
|
67
|
+
expect(getSteeringResults(bonusResult).length).toBe(1);
|
|
56
68
|
});
|
|
57
69
|
|
|
58
70
|
it('should use weighted draw for wheel segment selection', async () => {
|
|
@@ -104,7 +116,7 @@ describe('Steering Fortune Handler - Bonnys Fortune', () => {
|
|
|
104
116
|
);
|
|
105
117
|
|
|
106
118
|
expect(bonusResult.success).toBe(true);
|
|
107
|
-
expect(bonusResult
|
|
119
|
+
expect(getSteeringResults(bonusResult)[0].result.spinType).toBe(
|
|
108
120
|
SpinType.BONUS_GAME_3_SPIN,
|
|
109
121
|
);
|
|
110
122
|
});
|
|
@@ -133,7 +145,7 @@ describe('Steering Fortune Handler - Bonnys Fortune', () => {
|
|
|
133
145
|
);
|
|
134
146
|
|
|
135
147
|
expect(bonusResult.success).toBe(true);
|
|
136
|
-
const prizeType = bonusResult
|
|
148
|
+
const prizeType = getSteeringResults(bonusResult)[0].result.prizeType;
|
|
137
149
|
expect([PrizeType.MULTIPLIER, PrizeType.BONUS]).toContain(prizeType);
|
|
138
150
|
});
|
|
139
151
|
|
|
@@ -160,7 +172,7 @@ describe('Steering Fortune Handler - Bonnys Fortune', () => {
|
|
|
160
172
|
);
|
|
161
173
|
|
|
162
174
|
expect(bonusResult.success).toBe(true);
|
|
163
|
-
const result = bonusResult
|
|
175
|
+
const result = getSteeringResults(bonusResult)[0].result;
|
|
164
176
|
expect(result.playerWinning).toBeDefined();
|
|
165
177
|
expect(typeof result.playerWinning).toBe('number');
|
|
166
178
|
});
|
|
@@ -187,7 +199,7 @@ describe('Steering Fortune Handler - Bonnys Fortune', () => {
|
|
|
187
199
|
);
|
|
188
200
|
|
|
189
201
|
expect(bonusResult.success).toBe(true);
|
|
190
|
-
const result = bonusResult
|
|
202
|
+
const result = getSteeringResults(bonusResult)[0].result;
|
|
191
203
|
expect(result.prizeValue).toBeDefined();
|
|
192
204
|
});
|
|
193
205
|
|
|
@@ -213,7 +225,7 @@ describe('Steering Fortune Handler - Bonnys Fortune', () => {
|
|
|
213
225
|
);
|
|
214
226
|
|
|
215
227
|
expect(bonusResult.success).toBe(true);
|
|
216
|
-
const result = bonusResult
|
|
228
|
+
const result = getSteeringResults(bonusResult)[0].result;
|
|
217
229
|
expect(result.totalMultiplier).toBeDefined();
|
|
218
230
|
expect(typeof result.totalMultiplier).toBe('number');
|
|
219
231
|
});
|
|
@@ -249,11 +261,11 @@ describe('Steering Fortune Handler - Bonnys Fortune', () => {
|
|
|
249
261
|
|
|
250
262
|
if (!bonusResult.success) continue;
|
|
251
263
|
|
|
252
|
-
const result = bonusResult
|
|
264
|
+
const result = getSteeringResults(bonusResult)[0].result;
|
|
253
265
|
if (result.prizeType === PrizeType.BONUS) {
|
|
254
266
|
expect(result.triggeredBonus).toBeDefined();
|
|
255
|
-
expect(result.triggeredBonus
|
|
256
|
-
expect(result.triggeredBonus
|
|
267
|
+
expect(result.triggeredBonus!.bonusId).toBeDefined();
|
|
268
|
+
expect(result.triggeredBonus!.bonusType).toBeDefined();
|
|
257
269
|
return; // Test passed
|
|
258
270
|
}
|
|
259
271
|
}
|
|
@@ -286,7 +298,7 @@ describe('Steering Fortune Handler - Bonnys Fortune', () => {
|
|
|
286
298
|
expect(bonusResult.success).toBe(true);
|
|
287
299
|
|
|
288
300
|
const finalState = extractFinalBonusState(bonusResult);
|
|
289
|
-
const result = bonusResult
|
|
301
|
+
const result = getSteeringResults(bonusResult)[0].result;
|
|
290
302
|
|
|
291
303
|
if (result.prizeType === PrizeType.BONUS) {
|
|
292
304
|
// Should have pending bonus for chaining
|
|
@@ -352,7 +364,7 @@ describe('Steering Fortune Handler - Bonnys Fortune', () => {
|
|
|
352
364
|
|
|
353
365
|
if (!bonusResult.success) continue;
|
|
354
366
|
|
|
355
|
-
const result = bonusResult
|
|
367
|
+
const result = getSteeringResults(bonusResult)[0].result;
|
|
356
368
|
if (result.prizeType === PrizeType.MULTIPLIER) {
|
|
357
369
|
const finalState = extractFinalBonusState(bonusResult);
|
|
358
370
|
expect(finalState.privateState.nextSpinType).toBe(SpinType.BASE_GAME_SPIN);
|
|
@@ -414,7 +426,7 @@ describe('Steering Fortune Handler - Bonnys Fortune', () => {
|
|
|
414
426
|
);
|
|
415
427
|
|
|
416
428
|
expect(bonusResult.success).toBe(true);
|
|
417
|
-
const result = bonusResult
|
|
429
|
+
const result = getSteeringResults(bonusResult)[0].result;
|
|
418
430
|
|
|
419
431
|
if (result.prizeType === PrizeType.MULTIPLIER) {
|
|
420
432
|
expect(typeof result.prizeValue).toBe('number');
|
|
@@ -447,7 +459,7 @@ describe('Steering Fortune Handler - Bonnys Fortune', () => {
|
|
|
447
459
|
);
|
|
448
460
|
|
|
449
461
|
expect(bonusResult.success).toBe(true);
|
|
450
|
-
expect(bonusResult
|
|
462
|
+
expect(getSteeringResults(bonusResult)[0].result.playerWinning).toBeGreaterThanOrEqual(
|
|
451
463
|
0,
|
|
452
464
|
);
|
|
453
465
|
});
|
|
@@ -482,7 +494,7 @@ describe('Steering Fortune Handler - Bonnys Fortune', () => {
|
|
|
482
494
|
);
|
|
483
495
|
|
|
484
496
|
if (bonusResult.success) {
|
|
485
|
-
const result = bonusResult
|
|
497
|
+
const result = getSteeringResults(bonusResult)[0].result;
|
|
486
498
|
if (result.prizeType === PrizeType.MULTIPLIER) {
|
|
487
499
|
winnings.push(result.playerWinning);
|
|
488
500
|
}
|
|
@@ -499,7 +511,7 @@ describe('Steering Fortune Handler - Bonnys Fortune', () => {
|
|
|
499
511
|
|
|
500
512
|
describe('RNG Determinism', () => {
|
|
501
513
|
it('should produce consistent results with same LCG seed', async () => {
|
|
502
|
-
const results:
|
|
514
|
+
const results: SteeringToTheFortuneResult[] = [];
|
|
503
515
|
|
|
504
516
|
for (let i = 0; i < 2; i++) {
|
|
505
517
|
resetCommandIdCounter();
|
|
@@ -523,7 +535,7 @@ describe('Steering Fortune Handler - Bonnys Fortune', () => {
|
|
|
523
535
|
bonus.bonusType,
|
|
524
536
|
);
|
|
525
537
|
|
|
526
|
-
results.push(bonusResult
|
|
538
|
+
results.push(getSteeringResults(bonusResult)[0].result);
|
|
527
539
|
}
|
|
528
540
|
|
|
529
541
|
expect(results.length).toBe(2);
|
|
@@ -15,8 +15,23 @@ import {
|
|
|
15
15
|
initSession,
|
|
16
16
|
safeExecuteSpin,
|
|
17
17
|
resetCommandIdCounter,
|
|
18
|
+
BonnysFortuneCommandResult,
|
|
18
19
|
} from './helpers/test-engine-factory';
|
|
19
20
|
|
|
21
|
+
/** Extract screen from a spin outcome (BF may have different shapes). */
|
|
22
|
+
function extractScreen(result: BonnysFortuneCommandResult): number[][] | null {
|
|
23
|
+
const outcome = result.outcome as unknown as {
|
|
24
|
+
result?: { screen?: number[][] };
|
|
25
|
+
screens?: Array<{ screen?: number[][] }>;
|
|
26
|
+
screen?: number[][];
|
|
27
|
+
} | null;
|
|
28
|
+
return outcome?.result?.screen
|
|
29
|
+
?? outcome?.screens?.[0]?.screen
|
|
30
|
+
?? (result.publicState as unknown as { screen?: number[][] })?.screen
|
|
31
|
+
?? outcome?.screen
|
|
32
|
+
?? null;
|
|
33
|
+
}
|
|
34
|
+
|
|
20
35
|
// ---------------------------------------------------------------------------
|
|
21
36
|
// Constants
|
|
22
37
|
// ---------------------------------------------------------------------------
|
|
@@ -56,10 +71,7 @@ describe('Symbol Distribution — Bonnys Fortune', () => {
|
|
|
56
71
|
|
|
57
72
|
// Get screen from outcome (5 reels × 3 rows = 15 positions)
|
|
58
73
|
// BF outcome: result.screen
|
|
59
|
-
const screen = (result
|
|
60
|
-
?? result.outcome?.screens?.[0]?.screen
|
|
61
|
-
?? result.publicState?.screen
|
|
62
|
-
?? (result.outcome as any)?.screen;
|
|
74
|
+
const screen = extractScreen(result);
|
|
63
75
|
|
|
64
76
|
if (screen) {
|
|
65
77
|
for (const reel of screen) {
|
|
@@ -107,10 +119,7 @@ describe('Symbol Distribution — Bonnys Fortune', () => {
|
|
|
107
119
|
if (!result) break;
|
|
108
120
|
|
|
109
121
|
// BF outcome: result.screen
|
|
110
|
-
const screen = (result
|
|
111
|
-
?? result.outcome?.screens?.[0]?.screen
|
|
112
|
-
?? result.publicState?.screen
|
|
113
|
-
?? (result.outcome as any)?.screen;
|
|
122
|
+
const screen = extractScreen(result);
|
|
114
123
|
|
|
115
124
|
if (screen) {
|
|
116
125
|
for (const reel of screen) {
|
|
@@ -154,10 +163,7 @@ describe('Symbol Distribution — Bonnys Fortune', () => {
|
|
|
154
163
|
if (!result) break;
|
|
155
164
|
|
|
156
165
|
// BF outcome: result.screen
|
|
157
|
-
const screen = (result
|
|
158
|
-
?? result.outcome?.screens?.[0]?.screen
|
|
159
|
-
?? result.publicState?.screen
|
|
160
|
-
?? (result.outcome as any)?.screen;
|
|
166
|
+
const screen = extractScreen(result);
|
|
161
167
|
|
|
162
168
|
if (screen) {
|
|
163
169
|
for (const reel of screen) {
|
|
@@ -197,10 +203,7 @@ describe('Symbol Distribution — Bonnys Fortune', () => {
|
|
|
197
203
|
if (!result) break;
|
|
198
204
|
|
|
199
205
|
// BF outcome: result.screen
|
|
200
|
-
const screen = (result
|
|
201
|
-
?? result.outcome?.screens?.[0]?.screen
|
|
202
|
-
?? result.publicState?.screen
|
|
203
|
-
?? (result.outcome as any)?.screen;
|
|
206
|
+
const screen = extractScreen(result);
|
|
204
207
|
|
|
205
208
|
if (screen) {
|
|
206
209
|
const unique = new Set<number>();
|
|
@@ -19,8 +19,19 @@ import {
|
|
|
19
19
|
nextCommandId,
|
|
20
20
|
resetCommandIdCounter,
|
|
21
21
|
MockRngClient,
|
|
22
|
+
BonnysFortuneCommandResult,
|
|
22
23
|
} from './helpers/test-engine-factory';
|
|
23
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
SpinType,
|
|
26
|
+
SpinResultResponse,
|
|
27
|
+
TreasureHuntSpinResult,
|
|
28
|
+
BonusRoundResult,
|
|
29
|
+
} from '../logic/bonnys-fortune.types';
|
|
30
|
+
|
|
31
|
+
/** Helper to extract treasure hunt results from a bonus command result */
|
|
32
|
+
function getTreasureResults(result: BonnysFortuneCommandResult): SpinResultResponse<TreasureHuntSpinResult>[] {
|
|
33
|
+
return (result.outcome as unknown as BonusRoundResult<TreasureHuntSpinResult>).results;
|
|
34
|
+
}
|
|
24
35
|
|
|
25
36
|
describe('Treasure Hunt Handler - Bonnys Fortune', () => {
|
|
26
37
|
beforeEach(() => {
|
|
@@ -50,11 +61,11 @@ describe('Treasure Hunt Handler - Bonnys Fortune', () => {
|
|
|
50
61
|
);
|
|
51
62
|
|
|
52
63
|
expect(bonusResult.success).toBe(true);
|
|
53
|
-
expect(bonusResult
|
|
54
|
-
expect(bonusResult.
|
|
64
|
+
expect(getTreasureResults(bonusResult)).toBeDefined();
|
|
65
|
+
expect(getTreasureResults(bonusResult).length).toBeGreaterThan(0);
|
|
55
66
|
|
|
56
67
|
// Each step should have a selection
|
|
57
|
-
for (const step of bonusResult
|
|
68
|
+
for (const step of getTreasureResults(bonusResult)) {
|
|
58
69
|
expect(step.result.selection).toBeDefined();
|
|
59
70
|
expect(['mini', 'minor', 'major', 'grand']).toContain(step.result.selection);
|
|
60
71
|
}
|
|
@@ -85,7 +96,7 @@ describe('Treasure Hunt Handler - Bonnys Fortune', () => {
|
|
|
85
96
|
|
|
86
97
|
// Each result should have multiplierCounts that accumulate
|
|
87
98
|
let prevTotal = 0;
|
|
88
|
-
for (const step of bonusResult
|
|
99
|
+
for (const step of getTreasureResults(bonusResult)) {
|
|
89
100
|
const counts = step.result.multiplierCounts as Record<string, number>;
|
|
90
101
|
expect(counts).toBeDefined();
|
|
91
102
|
const total = Object.values(counts).reduce((a, b) => a + b, 0);
|
|
@@ -118,7 +129,7 @@ describe('Treasure Hunt Handler - Bonnys Fortune', () => {
|
|
|
118
129
|
expect(bonusResult.success).toBe(true);
|
|
119
130
|
|
|
120
131
|
const validJackpots = ['mini', 'minor', 'major', 'grand'];
|
|
121
|
-
for (const step of bonusResult
|
|
132
|
+
for (const step of getTreasureResults(bonusResult)) {
|
|
122
133
|
expect(validJackpots).toContain(step.result.selection);
|
|
123
134
|
}
|
|
124
135
|
});
|
|
@@ -149,8 +160,9 @@ describe('Treasure Hunt Handler - Bonnys Fortune', () => {
|
|
|
149
160
|
expect(bonusResult.success).toBe(true);
|
|
150
161
|
|
|
151
162
|
// Check that jackpot keys are valid
|
|
152
|
-
const
|
|
153
|
-
|
|
163
|
+
const bonusOutcome = bonusResult.outcome as unknown as BonusRoundResult<TreasureHuntSpinResult>;
|
|
164
|
+
const selections = bonusOutcome.results.map(
|
|
165
|
+
(r: SpinResultResponse<TreasureHuntSpinResult>) => r.result.selection,
|
|
154
166
|
);
|
|
155
167
|
const uniqueSelections = [...new Set(selections)];
|
|
156
168
|
expect(uniqueSelections.length).toBeGreaterThanOrEqual(1);
|
|
@@ -182,7 +194,7 @@ describe('Treasure Hunt Handler - Bonnys Fortune', () => {
|
|
|
182
194
|
|
|
183
195
|
expect(bonusResult.success).toBe(true);
|
|
184
196
|
|
|
185
|
-
for (const step of bonusResult
|
|
197
|
+
for (const step of getTreasureResults(bonusResult)) {
|
|
186
198
|
expect(step.result.multiplier).toBeDefined();
|
|
187
199
|
expect(step.result.multiplier).toBeGreaterThan(0);
|
|
188
200
|
}
|
|
@@ -212,7 +224,7 @@ describe('Treasure Hunt Handler - Bonnys Fortune', () => {
|
|
|
212
224
|
expect(bonusResult.success).toBe(true);
|
|
213
225
|
|
|
214
226
|
const finalStep =
|
|
215
|
-
bonusResult
|
|
227
|
+
getTreasureResults(bonusResult)[getTreasureResults(bonusResult).length - 1];
|
|
216
228
|
// Final step should have winningMultiplier (0 if no win, >0 if win)
|
|
217
229
|
expect(finalStep.result.winningMultiplier).toBeDefined();
|
|
218
230
|
expect(typeof finalStep.result.winningMultiplier).toBe('number');
|
|
@@ -242,7 +254,7 @@ describe('Treasure Hunt Handler - Bonnys Fortune', () => {
|
|
|
242
254
|
|
|
243
255
|
expect(bonusResult.success).toBe(true);
|
|
244
256
|
// When RNG returns 0, weighted draw should select first eligible jackpot
|
|
245
|
-
expect(bonusResult
|
|
257
|
+
expect(getTreasureResults(bonusResult)).toBeDefined();
|
|
246
258
|
});
|
|
247
259
|
});
|
|
248
260
|
|
|
@@ -274,7 +286,7 @@ describe('Treasure Hunt Handler - Bonnys Fortune', () => {
|
|
|
274
286
|
expect(bonusResult.success).toBe(true);
|
|
275
287
|
|
|
276
288
|
// Step 3: Verify all steps completed
|
|
277
|
-
expect(bonusResult.
|
|
289
|
+
expect(getTreasureResults(bonusResult).length).toBeGreaterThan(0);
|
|
278
290
|
|
|
279
291
|
// Step 4: Extract final payout
|
|
280
292
|
const finalState = extractFinalBonusState(bonusResult);
|
|
@@ -308,7 +320,7 @@ describe('Treasure Hunt Handler - Bonnys Fortune', () => {
|
|
|
308
320
|
expect(bonusResult.success).toBe(true);
|
|
309
321
|
|
|
310
322
|
// Total winning across all steps should be consistent
|
|
311
|
-
const results = bonusResult
|
|
323
|
+
const results = getTreasureResults(bonusResult);
|
|
312
324
|
const finalStep = results[results.length - 1];
|
|
313
325
|
expect(finalStep.result.playerWinning).toBeGreaterThanOrEqual(0);
|
|
314
326
|
});
|
|
@@ -355,7 +367,7 @@ describe('Treasure Hunt Handler - Bonnys Fortune', () => {
|
|
|
355
367
|
|
|
356
368
|
expect(bonusResult.success).toBe(true);
|
|
357
369
|
|
|
358
|
-
const results = bonusResult
|
|
370
|
+
const results = getTreasureResults(bonusResult);
|
|
359
371
|
expect(results.length).toBeGreaterThan(0);
|
|
360
372
|
|
|
361
373
|
// Verify sequential step progression
|
|
@@ -394,7 +406,7 @@ describe('Treasure Hunt Handler - Bonnys Fortune', () => {
|
|
|
394
406
|
expect(bonusResult.success).toBe(true);
|
|
395
407
|
|
|
396
408
|
const finalStep =
|
|
397
|
-
bonusResult
|
|
409
|
+
getTreasureResults(bonusResult)[getTreasureResults(bonusResult).length - 1];
|
|
398
410
|
// winningCount is 3 if player won (3-of-a-kind), 0 otherwise
|
|
399
411
|
expect([0, 3]).toContain(finalStep.result.winningCount);
|
|
400
412
|
});
|
|
@@ -423,7 +435,7 @@ describe('Treasure Hunt Handler - Bonnys Fortune', () => {
|
|
|
423
435
|
expect(bonusResult.success).toBe(true);
|
|
424
436
|
|
|
425
437
|
const finalStep =
|
|
426
|
-
bonusResult
|
|
438
|
+
getTreasureResults(bonusResult)[getTreasureResults(bonusResult).length - 1];
|
|
427
439
|
expect(finalStep.result.totalMultiplier).toBeDefined();
|
|
428
440
|
expect(typeof finalStep.result.totalMultiplier).toBe('number');
|
|
429
441
|
});
|
|
@@ -508,8 +520,9 @@ describe('Treasure Hunt Handler - Bonnys Fortune', () => {
|
|
|
508
520
|
expect(bonusResult.success).toBe(true);
|
|
509
521
|
|
|
510
522
|
// Sum all playerWinning values
|
|
511
|
-
const
|
|
512
|
-
|
|
523
|
+
const bonusOutcome2 = bonusResult.outcome as unknown as BonusRoundResult<TreasureHuntSpinResult>;
|
|
524
|
+
const totalWinning = bonusOutcome2.results.reduce(
|
|
525
|
+
(sum: number, step: SpinResultResponse<TreasureHuntSpinResult>) => sum + step.result.playerWinning,
|
|
513
526
|
0,
|
|
514
527
|
);
|
|
515
528
|
expect(totalWinning).toBeGreaterThanOrEqual(0);
|
|
@@ -544,7 +557,7 @@ describe('Treasure Hunt Handler - Bonnys Fortune', () => {
|
|
|
544
557
|
|
|
545
558
|
describe('RNG Determinism', () => {
|
|
546
559
|
it('should produce consistent results with same LCG seed', async () => {
|
|
547
|
-
const results:
|
|
560
|
+
const results: SpinResultResponse<TreasureHuntSpinResult>[][] = [];
|
|
548
561
|
|
|
549
562
|
for (let i = 0; i < 2; i++) {
|
|
550
563
|
const { engine } = createEngineWithLcgRng(54321);
|
|
@@ -567,14 +580,15 @@ describe('Treasure Hunt Handler - Bonnys Fortune', () => {
|
|
|
567
580
|
bonus.bonusType,
|
|
568
581
|
);
|
|
569
582
|
|
|
570
|
-
|
|
583
|
+
const bonusOutcome3 = bonusResult.outcome as unknown as BonusRoundResult<TreasureHuntSpinResult>;
|
|
584
|
+
results.push(bonusOutcome3.results);
|
|
571
585
|
resetCommandIdCounter();
|
|
572
586
|
}
|
|
573
587
|
|
|
574
588
|
expect(results.length).toBe(2);
|
|
575
589
|
// Same seed should produce identical selections
|
|
576
|
-
const selections1 = results[0].map((r
|
|
577
|
-
const selections2 = results[1].map((r
|
|
590
|
+
const selections1 = results[0].map((r) => r.result.selection);
|
|
591
|
+
const selections2 = results[1].map((r) => r.result.selection);
|
|
578
592
|
expect(selections1).toEqual(selections2);
|
|
579
593
|
});
|
|
580
594
|
});
|