@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.
Files changed (79) hide show
  1. package/dist/__tests__/base-game.test.js +17 -10
  2. package/dist/__tests__/base-game.test.js.map +1 -1
  3. package/dist/__tests__/bonus-handlers.test.js +15 -21
  4. package/dist/__tests__/bonus-handlers.test.js.map +1 -1
  5. package/dist/__tests__/bonus-meters.test.js.map +1 -1
  6. package/dist/__tests__/collect-feature-handler.test.js +30 -24
  7. package/dist/__tests__/collect-feature-handler.test.js.map +1 -1
  8. package/dist/__tests__/comprehensive.test.js.map +1 -1
  9. package/dist/__tests__/error-paths.test.js +11 -7
  10. package/dist/__tests__/error-paths.test.js.map +1 -1
  11. package/dist/__tests__/helpers/test-engine-factory.d.ts +72 -18
  12. package/dist/__tests__/helpers/test-engine-factory.js +32 -7
  13. package/dist/__tests__/helpers/test-engine-factory.js.map +1 -1
  14. package/dist/__tests__/integration-1000-spin.test.js +33 -37
  15. package/dist/__tests__/integration-1000-spin.test.js.map +1 -1
  16. package/dist/__tests__/rng-gli19-compliance.test.js.map +1 -1
  17. package/dist/__tests__/rng-security.test.js +0 -1
  18. package/dist/__tests__/rng-security.test.js.map +1 -1
  19. package/dist/__tests__/rtp-simulation.test.js +11 -10
  20. package/dist/__tests__/rtp-simulation.test.js.map +1 -1
  21. package/dist/__tests__/state-transitions.test.js +0 -1
  22. package/dist/__tests__/state-transitions.test.js.map +1 -1
  23. package/dist/__tests__/steering-fortune-handler.test.js +18 -14
  24. package/dist/__tests__/steering-fortune-handler.test.js.map +1 -1
  25. package/dist/__tests__/symbol-distribution.test.js +13 -16
  26. package/dist/__tests__/symbol-distribution.test.js.map +1 -1
  27. package/dist/__tests__/treasure-hunt-handler.test.js +23 -16
  28. package/dist/__tests__/treasure-hunt-handler.test.js.map +1 -1
  29. package/dist/__tests__/win-calculator.test.js +10 -7
  30. package/dist/__tests__/win-calculator.test.js.map +1 -1
  31. package/dist/bonnys-fortune-v1.game-engine.d.ts +3 -3
  32. package/dist/bonnys-fortune-v1.game-engine.js +8 -5
  33. package/dist/bonnys-fortune-v1.game-engine.js.map +1 -1
  34. package/dist/game-engine.interface.d.ts +1 -1
  35. package/dist/helpers/optional-boolean-mapper.d.ts +1 -1
  36. package/dist/helpers/validation-helper.d.ts +1 -1
  37. package/dist/logic/bonnys-fortune.game-logic.js +2 -0
  38. package/dist/logic/bonnys-fortune.game-logic.js.map +1 -1
  39. package/dist/logic/bonnys-fortune.types.d.ts +7 -1
  40. package/dist/logic/handlers/collect-feature-bonus.handler.js.map +1 -1
  41. package/dist/logic/handlers/steering-to-the-fortune-bonus.handler.js +6 -5
  42. package/dist/logic/handlers/steering-to-the-fortune-bonus.handler.js.map +1 -1
  43. package/dist/logic/handlers/treasure-hunt-bonus.handler.js.map +1 -1
  44. package/dist/rng/rng-client.factory.js +3 -3
  45. package/dist/rng/rng-client.factory.js.map +1 -1
  46. package/dist/validation/bonnys-fortune/bonnys-fortune-config.dto.js.map +1 -1
  47. package/dist/validation/custom-decorators/IsNestedIntArray.js.map +1 -1
  48. package/dist/validation/game-logic-config-validation.service.d.ts +1 -1
  49. package/dist/validation/game-logic-config-validation.service.js.map +1 -1
  50. package/package.json +3 -3
  51. package/src/__tests__/base-game.test.ts +26 -17
  52. package/src/__tests__/bonus-handlers.test.ts +16 -21
  53. package/src/__tests__/bonus-meters.test.ts +13 -8
  54. package/src/__tests__/collect-feature-handler.test.ts +39 -26
  55. package/src/__tests__/comprehensive.test.ts +40 -36
  56. package/src/__tests__/error-paths.test.ts +29 -18
  57. package/src/__tests__/helpers/test-engine-factory.ts +127 -40
  58. package/src/__tests__/integration-1000-spin.test.ts +53 -48
  59. package/src/__tests__/rng-gli19-compliance.test.ts +17 -17
  60. package/src/__tests__/rng-security.test.ts +11 -8
  61. package/src/__tests__/rtp-simulation.test.ts +17 -10
  62. package/src/__tests__/state-transitions.test.ts +8 -4
  63. package/src/__tests__/steering-fortune-handler.test.ts +30 -18
  64. package/src/__tests__/symbol-distribution.test.ts +19 -16
  65. package/src/__tests__/treasure-hunt-handler.test.ts +36 -22
  66. package/src/__tests__/win-calculator.test.ts +14 -10
  67. package/src/bonnys-fortune-v1.game-engine.ts +18 -8
  68. package/src/domain/types/game-symbols.response.dto.ts +1 -1
  69. package/src/game-engine.interface.ts +1 -1
  70. package/src/helpers/validation-helper.ts +1 -1
  71. package/src/logic/bonnys-fortune.game-logic.ts +3 -0
  72. package/src/logic/bonnys-fortune.types.ts +10 -1
  73. package/src/logic/handlers/collect-feature-bonus.handler.ts +5 -2
  74. package/src/logic/handlers/steering-to-the-fortune-bonus.handler.ts +7 -6
  75. package/src/logic/handlers/treasure-hunt-bonus.handler.ts +2 -2
  76. package/src/rng/rng-client.factory.ts +3 -3
  77. package/src/validation/bonnys-fortune/bonnys-fortune-config.dto.ts +72 -72
  78. package/src/validation/custom-decorators/IsNestedIntArray.ts +1 -1
  79. 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
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
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.outcome as any)?.result?.playerWinning
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.outcome as any)?.result?.playerWinning
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: any) => b.bonusId === bonus.bonusId,
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
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
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 { SpinType, PrizeType } from '../logic/bonnys-fortune.types';
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.outcome?.results).toBeDefined();
65
+ expect(getSteeringResults(bonusResult)).toBeDefined();
54
66
  // Steering Fortune produces exactly 1 result per spin
55
- expect(bonusResult.outcome.results.length).toBe(1);
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.outcome.results[0].result.spinType).toBe(
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.outcome.results[0].result.prizeType;
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.outcome.results[0].result;
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.outcome.results[0].result;
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.outcome.results[0].result;
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.outcome.results[0].result;
264
+ const result = getSteeringResults(bonusResult)[0].result;
253
265
  if (result.prizeType === PrizeType.BONUS) {
254
266
  expect(result.triggeredBonus).toBeDefined();
255
- expect(result.triggeredBonus.bonusId).toBeDefined();
256
- expect(result.triggeredBonus.bonusType).toBeDefined();
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.outcome.results[0].result;
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.outcome.results[0].result;
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.outcome.results[0].result;
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.outcome.results[0].result.playerWinning).toBeGreaterThanOrEqual(
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.outcome.results[0].result;
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: any[] = [];
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.outcome.results[0].result);
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.outcome as any)?.result?.screen
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.outcome as any)?.result?.screen
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.outcome as any)?.result?.screen
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.outcome as any)?.result?.screen
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 { SpinType } from '../logic/bonnys-fortune.types';
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.outcome?.results).toBeDefined();
54
- expect(bonusResult.outcome.results.length).toBeGreaterThan(0);
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.outcome.results) {
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.outcome.results) {
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.outcome.results) {
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 selections = bonusResult.outcome.results.map(
153
- (r: any) => r.result.selection,
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.outcome.results) {
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.outcome.results[bonusResult.outcome.results.length - 1];
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.outcome.results).toBeDefined();
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.outcome?.results?.length).toBeGreaterThan(0);
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.outcome.results;
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.outcome.results;
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.outcome.results[bonusResult.outcome.results.length - 1];
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.outcome.results[bonusResult.outcome.results.length - 1];
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 totalWinning = bonusResult.outcome.results.reduce(
512
- (sum: number, step: any) => sum + step.result.playerWinning,
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: any[] = [];
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
- results.push(bonusResult.outcome.results);
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: any) => r.result.selection);
577
- const selections2 = results[1].map((r: any) => r.result.selection);
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
  });