@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
@@ -16,7 +16,14 @@ import {
16
16
  nextCommandId,
17
17
  resetCommandIdCounter,
18
18
  waitForEngineReady,
19
+ getOutcome,
19
20
  } from './helpers/test-engine-factory';
21
+ import {
22
+ BonnysFortunePublicState,
23
+ BonnysFortunePrivateState,
24
+ SpinResultResponse,
25
+ BonnysFortuneBaseGameSpinResult,
26
+ } from '../logic/bonnys-fortune.types';
20
27
 
21
28
  describe('Error Paths — Bonnys Fortune', () => {
22
29
  beforeEach(() => {
@@ -28,7 +35,7 @@ describe('Error Paths — Bonnys Fortune', () => {
28
35
  const engine = createEngine();
29
36
  await waitForEngineReady(engine);
30
37
 
31
- const result = await engine.processCommand(null as any, null as any, {
38
+ const result = await engine.processCommand(null as unknown as BonnysFortunePublicState, null as unknown as BonnysFortunePrivateState, {
32
39
  id: nextCommandId('init'),
33
40
  type: 'INIT_SESSION_STATE',
34
41
  payload: { betAmountThresholds: [1.0], defaultBetAmount: 1.0 },
@@ -42,7 +49,7 @@ describe('Error Paths — Bonnys Fortune', () => {
42
49
  const session = await initSession(engine);
43
50
 
44
51
  try {
45
- await executeSpin(engine, null, session.privateState);
52
+ await executeSpin(engine, null as unknown as BonnysFortunePublicState, session.privateState);
46
53
  } catch (error) {
47
54
  expect(error).toBeDefined();
48
55
  }
@@ -53,7 +60,7 @@ describe('Error Paths — Bonnys Fortune', () => {
53
60
  const session = await initSession(engine);
54
61
 
55
62
  try {
56
- await executeSpin(engine, session.publicState, null);
63
+ await executeSpin(engine, session.publicState, null as unknown as BonnysFortunePrivateState);
57
64
  } catch (error) {
58
65
  expect(error).toBeDefined();
59
66
  }
@@ -64,7 +71,7 @@ describe('Error Paths — Bonnys Fortune', () => {
64
71
  const session = await initSession(engine);
65
72
 
66
73
  try {
67
- await executeSpin(engine, undefined, undefined);
74
+ await executeSpin(engine, undefined as unknown as BonnysFortunePublicState, undefined as unknown as BonnysFortunePrivateState);
68
75
  } catch (error) {
69
76
  expect(error).toBeDefined();
70
77
  }
@@ -135,7 +142,8 @@ describe('Error Paths — Bonnys Fortune', () => {
135
142
  },
136
143
  );
137
144
  if (result.success && result.outcome) {
138
- expect((result.outcome as any).playerWinning).toBeGreaterThanOrEqual(0);
145
+ const outcome = result.outcome as unknown as { result?: { playerWinning: number }; playerWinning?: number };
146
+ expect(outcome.result?.playerWinning ?? outcome.playerWinning ?? 0).toBeGreaterThanOrEqual(0);
139
147
  }
140
148
  } catch (error) {
141
149
  expect(error).toBeDefined();
@@ -204,7 +212,7 @@ describe('Error Paths — Bonnys Fortune', () => {
204
212
  type: 'SPIN',
205
213
  payload: {
206
214
  sessionId: 'test',
207
- betAmount: '1.0' as any,
215
+ betAmount: '1.0' as unknown as number,
208
216
  gameCode: 'bonnys-fortune',
209
217
  gameVersion: '1.0.0',
210
218
  },
@@ -241,7 +249,7 @@ describe('Error Paths — Bonnys Fortune', () => {
241
249
  await waitForEngineReady(engine);
242
250
 
243
251
  try {
244
- const result = await engine.processCommand(null as any, null as any, {
252
+ const result = await engine.processCommand(null as unknown as BonnysFortunePublicState, null as unknown as BonnysFortunePrivateState, {
245
253
  id: nextCommandId('init'),
246
254
  type: 'INIT_SESSION_STATE',
247
255
  payload: { betAmountThresholds: [], defaultBetAmount: 1.0 },
@@ -361,11 +369,11 @@ describe('Error Paths — Bonnys Fortune', () => {
361
369
  const engine = createEngine();
362
370
  const session = await initSession(engine);
363
371
 
364
- const corruptedPriv = { ...session.privateState };
372
+ const corruptedPriv = { ...session.privateState } as Partial<BonnysFortunePrivateState> & Omit<BonnysFortunePrivateState, 'nextSpinType'>;
365
373
  delete corruptedPriv.nextSpinType;
366
374
 
367
375
  try {
368
- const result = await executeSpin(engine, session.publicState, corruptedPriv);
376
+ const result = await executeSpin(engine, session.publicState, corruptedPriv as unknown as BonnysFortunePrivateState);
369
377
  expect(result).toBeDefined();
370
378
  } catch (error) {
371
379
  expect(error).toBeDefined();
@@ -378,11 +386,11 @@ describe('Error Paths — Bonnys Fortune', () => {
378
386
 
379
387
  const corruptedPriv = {
380
388
  ...session.privateState,
381
- bonusMetersByAmount: null as any,
389
+ bonusMetersByAmount: null as unknown as BonnysFortunePrivateState['bonusMetersByAmount'],
382
390
  };
383
391
 
384
392
  try {
385
- const result = await executeSpin(engine, session.publicState, corruptedPriv);
393
+ const result = await executeSpin(engine, session.publicState, corruptedPriv as unknown as BonnysFortunePrivateState);
386
394
  expect(result).toBeDefined();
387
395
  } catch (error) {
388
396
  expect(error).toBeDefined();
@@ -395,7 +403,7 @@ describe('Error Paths — Bonnys Fortune', () => {
395
403
 
396
404
  const corruptedPub = {
397
405
  ...session.publicState,
398
- bonusMeters: null as any,
406
+ bonusMeters: null as unknown as BonnysFortunePublicState['bonusMeters'],
399
407
  };
400
408
 
401
409
  try {
@@ -421,8 +429,9 @@ describe('Error Paths — Bonnys Fortune', () => {
421
429
  const result = await executeSpin(engine, pub, priv);
422
430
  expect(result.success).toBe(true);
423
431
 
424
- if (result.outcome?.playerWinning !== undefined) {
425
- expect(result.outcome.playerWinning).toBeGreaterThanOrEqual(0);
432
+ const o = getOutcome(result);
433
+ if (o.playerWinning !== undefined) {
434
+ expect(o.playerWinning).toBeGreaterThanOrEqual(0);
426
435
  }
427
436
 
428
437
  pub = result.publicState;
@@ -437,8 +446,9 @@ describe('Error Paths — Bonnys Fortune', () => {
437
446
  const result = await executeSpin(engine, session.publicState, session.privateState);
438
447
  expect(result.success).toBe(true);
439
448
 
440
- if (result.outcome?.playerWinning !== undefined) {
441
- expect(typeof result.outcome.playerWinning).toBe('number');
449
+ const o1 = getOutcome(result);
450
+ if (o1.playerWinning !== undefined) {
451
+ expect(typeof o1.playerWinning).toBe('number');
442
452
  }
443
453
  });
444
454
 
@@ -449,8 +459,9 @@ describe('Error Paths — Bonnys Fortune', () => {
449
459
  const result = await executeSpin(engine, session.publicState, session.privateState);
450
460
  expect(result.success).toBe(true);
451
461
 
452
- if (result.outcome?.playerWinning !== undefined) {
453
- expect(Number.isFinite(result.outcome.playerWinning)).toBe(true);
462
+ const o2 = getOutcome(result);
463
+ if (o2.playerWinning !== undefined) {
464
+ expect(Number.isFinite(o2.playerWinning)).toBe(true);
454
465
  }
455
466
  });
456
467
  });
@@ -8,6 +8,19 @@
8
8
  * through our mock — no need to rebuild gameLogic or handlers.
9
9
  */
10
10
  import { BonnysFortuneV1GameEngine } from '../../bonnys-fortune-v1.game-engine';
11
+ import {
12
+ BonnysFortunePublicState,
13
+ BonnysFortunePrivateState,
14
+ BonnysFortuneOutcome,
15
+ SpinResultResponse,
16
+ SpinResult,
17
+ BonusRoundResult,
18
+ } from '../../logic/bonnys-fortune.types';
19
+ import {
20
+ CommandProcessingResult,
21
+ RngOutcome,
22
+ } from '../../game-engine.interface';
23
+ import { IRngClient } from '../../rng/rng-client.interface';
11
24
  import {
12
25
  MockRngClient,
13
26
  LcgRngClient,
@@ -67,6 +80,70 @@ export {
67
80
  AssertionBounds,
68
81
  } from '@omnitronix/game-test-utils';
69
82
 
83
+ /** Type alias for the full command processing result from the engine. */
84
+ export type BonnysFortuneCommandResult = CommandProcessingResult<
85
+ BonnysFortunePublicState,
86
+ BonnysFortunePrivateState,
87
+ BonnysFortuneOutcome
88
+ >;
89
+
90
+ /**
91
+ * Loose outcome interface for test assertions.
92
+ * Tests frequently access properties across multiple outcome union members
93
+ * without narrowing. This interface provides a test-safe accessor pattern.
94
+ */
95
+ export interface TestOutcome {
96
+ result?: {
97
+ screen?: number[][];
98
+ playerWinning?: number;
99
+ spinType?: string;
100
+ nextSpinType?: string;
101
+ winResult?: unknown;
102
+ [key: string]: unknown;
103
+ };
104
+ results?: Array<{
105
+ result?: {
106
+ playerWinning?: number;
107
+ spinType?: string;
108
+ nextSpinType?: string;
109
+ prizeType?: string;
110
+ prizeValue?: unknown;
111
+ totalMultiplier?: number;
112
+ triggeredBonus?: unknown;
113
+ [key: string]: unknown;
114
+ };
115
+ publicState?: BonnysFortunePublicState;
116
+ privateState?: BonnysFortunePrivateState;
117
+ [key: string]: unknown;
118
+ }>;
119
+ screen?: number[][];
120
+ playerWinning?: number;
121
+ triggeredBonus?: unknown;
122
+ nextSpinType?: string;
123
+ autoStarted?: boolean;
124
+ requiresStartCommand?: boolean;
125
+ updatedMeter?: unknown;
126
+ publicState?: BonnysFortunePublicState;
127
+ privateState?: BonnysFortunePrivateState;
128
+ [key: string]: unknown;
129
+ }
130
+
131
+ /**
132
+ * Cast outcome to a specific type for tests that know the expected shape.
133
+ * Uses `as unknown as T` pattern recommended for test fixtures.
134
+ */
135
+ export function outcomeAs<T>(result: BonnysFortuneCommandResult): T {
136
+ return result.outcome as unknown as T;
137
+ }
138
+
139
+ /**
140
+ * Get the outcome from a command result as a test-friendly object.
141
+ * This avoids TypeScript union narrowing issues in test assertions.
142
+ */
143
+ export function getOutcome(result: BonnysFortuneCommandResult): TestOutcome {
144
+ return (result.outcome ?? {}) as unknown as TestOutcome;
145
+ }
146
+
70
147
  /**
71
148
  * Create a standard engine with default (DummyRngClient) RNG.
72
149
  */
@@ -74,6 +151,14 @@ export function createEngine(): BonnysFortuneV1GameEngine {
74
151
  return new BonnysFortuneV1GameEngine();
75
152
  }
76
153
 
154
+ /**
155
+ * Helper to replace the rngClient on a shared RngService.
156
+ * The rngClient is private readonly, so we use a type assertion.
157
+ */
158
+ function replaceRngClient(engine: BonnysFortuneV1GameEngine, client: IRngClient): void {
159
+ (engine.rngService as unknown as { rngClient: IRngClient }).rngClient = client;
160
+ }
161
+
77
162
  /**
78
163
  * Create an engine with a MockRngClient injected.
79
164
  */
@@ -82,7 +167,7 @@ export function createEngineWithMockRng(
82
167
  ): { engine: BonnysFortuneV1GameEngine; mockRng: MockRngClient } {
83
168
  const engine = new BonnysFortuneV1GameEngine();
84
169
  const mockRng = new MockRngClient(values);
85
- (engine.rngService as any).rngClient = mockRng;
170
+ replaceRngClient(engine, mockRng);
86
171
  return { engine, mockRng };
87
172
  }
88
173
 
@@ -94,7 +179,7 @@ export function createEngineWithLcgRng(
94
179
  ): { engine: BonnysFortuneV1GameEngine; lcgRng: LcgRngClient } {
95
180
  const engine = new BonnysFortuneV1GameEngine();
96
181
  const lcgRng = new LcgRngClient(seed);
97
- (engine.rngService as any).rngClient = lcgRng;
182
+ replaceRngClient(engine, lcgRng);
98
183
  return { engine, lcgRng };
99
184
  }
100
185
 
@@ -105,7 +190,7 @@ export function injectMockRng(
105
190
  engine: BonnysFortuneV1GameEngine,
106
191
  mockRng: MockRngClient,
107
192
  ): void {
108
- (engine.rngService as any).rngClient = mockRng;
193
+ replaceRngClient(engine, mockRng);
109
194
  }
110
195
 
111
196
  // ---------------------------------------------------------------------------
@@ -117,12 +202,12 @@ export function injectMockRng(
117
202
  * The engine constructor fires preload() without await (fire-and-forget).
118
203
  * We poll for `engine.reels` to be defined before proceeding.
119
204
  */
120
- export async function waitForEngineReady(engine: any, timeout = 5000): Promise<void> {
205
+ export async function waitForEngineReady(engine: BonnysFortuneV1GameEngine, timeout = 5000): Promise<void> {
121
206
  const start = Date.now();
122
- while (!engine.reels && Date.now() - start < timeout) {
207
+ while (!(engine as unknown as { reels: unknown }).reels && Date.now() - start < timeout) {
123
208
  await new Promise(resolve => setTimeout(resolve, 10));
124
209
  }
125
- if (!engine.reels) {
210
+ if (!(engine as unknown as { reels: unknown }).reels) {
126
211
  throw new Error('Engine preload did not complete within timeout');
127
212
  }
128
213
  }
@@ -132,13 +217,13 @@ export async function waitForEngineReady(engine: any, timeout = 5000): Promise<v
132
217
  // ---------------------------------------------------------------------------
133
218
 
134
219
  export interface SessionState {
135
- publicState: any;
136
- privateState: any;
220
+ publicState: BonnysFortunePublicState;
221
+ privateState: BonnysFortunePrivateState;
137
222
  }
138
223
 
139
224
  /**
140
225
  * Initialize a session on the engine.
141
- * Waits for engine preload to complete before issuing any commands.
226
+ * Waits for engine preload to complete before issuing commands.
142
227
  */
143
228
  export async function initSession(
144
229
  engine: BonnysFortuneV1GameEngine,
@@ -146,7 +231,7 @@ export async function initSession(
146
231
  betAmountThresholds: number[] = [0.1, 0.2, 0.5, 1, 2, 5, 10, 20, 50, 100],
147
232
  ): Promise<SessionState> {
148
233
  await waitForEngineReady(engine);
149
- const result = await engine.processCommand(null as any, null as any, {
234
+ const result = await engine.processCommand(null as unknown as BonnysFortunePublicState, null as unknown as BonnysFortunePrivateState, {
150
235
  id: nextCommandId('init'),
151
236
  type: 'INIT_SESSION_STATE',
152
237
  payload: {
@@ -167,10 +252,10 @@ export async function initSession(
167
252
  */
168
253
  export async function executeSpin(
169
254
  engine: BonnysFortuneV1GameEngine,
170
- publicState: any,
171
- privateState: any,
255
+ publicState: BonnysFortunePublicState,
256
+ privateState: BonnysFortunePrivateState,
172
257
  betAmount: number = 1.0,
173
- ): Promise<any> {
258
+ ): Promise<BonnysFortuneCommandResult> {
174
259
  const result = await engine.processCommand(publicState, privateState, {
175
260
  id: nextCommandId('spin'),
176
261
  type: 'SPIN',
@@ -194,14 +279,15 @@ export async function executeSpin(
194
279
  */
195
280
  export async function safeExecuteSpin(
196
281
  engine: BonnysFortuneV1GameEngine,
197
- publicState: any,
198
- privateState: any,
282
+ publicState: BonnysFortunePublicState,
283
+ privateState: BonnysFortunePrivateState,
199
284
  betAmount: number = 1.0,
200
- ): Promise<any | null> {
285
+ ): Promise<BonnysFortuneCommandResult | null> {
201
286
  try {
202
287
  return await executeSpin(engine, publicState, privateState, betAmount);
203
- } catch (error: any) {
288
+ } catch (error: unknown) {
204
289
  if (
290
+ error instanceof Error &&
205
291
  error.message?.includes('Cannot execute base spin')
206
292
  ) {
207
293
  return null;
@@ -215,12 +301,12 @@ export async function safeExecuteSpin(
215
301
  */
216
302
  export async function executeSpins(
217
303
  engine: BonnysFortuneV1GameEngine,
218
- publicState: any,
219
- privateState: any,
304
+ publicState: BonnysFortunePublicState,
305
+ privateState: BonnysFortunePrivateState,
220
306
  count: number,
221
307
  betAmount: number = 1.0,
222
- ): Promise<any[]> {
223
- const results: any[] = [];
308
+ ): Promise<BonnysFortuneCommandResult[]> {
309
+ const results: BonnysFortuneCommandResult[] = [];
224
310
  let currentPublic = publicState;
225
311
  let currentPrivate = privateState;
226
312
 
@@ -239,11 +325,11 @@ export async function executeSpins(
239
325
  */
240
326
  export async function startBonusRound(
241
327
  engine: BonnysFortuneV1GameEngine,
242
- publicState: any,
243
- privateState: any,
328
+ publicState: BonnysFortunePublicState,
329
+ privateState: BonnysFortunePrivateState,
244
330
  bonusType: string = 'bonusGame1',
245
331
  betAmount: number = 1.0,
246
- ): Promise<any> {
332
+ ): Promise<BonnysFortuneCommandResult> {
247
333
  const bonusId = privateState.pendingBonuses?.[0]?.bonusId ?? 'test-bonus-id';
248
334
 
249
335
  const result = await engine.processCommand(publicState, privateState, {
@@ -267,11 +353,11 @@ export async function startBonusRound(
267
353
  */
268
354
  export async function debugTriggerBonus(
269
355
  engine: BonnysFortuneV1GameEngine,
270
- publicState: any,
271
- privateState: any,
356
+ publicState: BonnysFortunePublicState,
357
+ privateState: BonnysFortunePrivateState,
272
358
  bonusType: string = 'bonusGame1',
273
359
  betAmount: number = 1.0,
274
- ): Promise<any> {
360
+ ): Promise<BonnysFortuneCommandResult> {
275
361
  const result = await engine.processCommand(publicState, privateState, {
276
362
  id: nextCommandId('debug'),
277
363
  type: 'DEBUG_TRIGGER_BONUS',
@@ -290,11 +376,11 @@ export async function debugTriggerBonus(
290
376
  */
291
377
  export async function debugUpdateBonusMeter(
292
378
  engine: BonnysFortuneV1GameEngine,
293
- publicState: any,
294
- privateState: any,
379
+ publicState: BonnysFortunePublicState,
380
+ privateState: BonnysFortunePrivateState,
295
381
  meterId: string,
296
382
  progress: number,
297
- ): Promise<any> {
383
+ ): Promise<BonnysFortuneCommandResult> {
298
384
  const result = await engine.processCommand(publicState, privateState, {
299
385
  id: nextCommandId('debug-meter'),
300
386
  type: 'DEBUG_UPDATE_BONUS_METER_PROGRESS',
@@ -313,12 +399,13 @@ export async function debugUpdateBonusMeter(
313
399
  * are the ORIGINAL input states. The actual final state (with nextSpinType
314
400
  * transitioned back to BASE_GAME_SPIN) is in the last result entry.
315
401
  */
316
- export function extractFinalBonusState(bonusResult: any): {
317
- publicState: any;
318
- privateState: any;
402
+ export function extractFinalBonusState(bonusResult: BonnysFortuneCommandResult): {
403
+ publicState: BonnysFortunePublicState;
404
+ privateState: BonnysFortunePrivateState;
319
405
  } {
320
- const results = bonusResult.outcome?.results;
321
- if (results?.length > 0) {
406
+ const outcome = bonusResult.outcome as BonusRoundResult<SpinResult> | null | undefined;
407
+ const results = outcome?.results;
408
+ if (results && results.length > 0) {
322
409
  const lastResult = results[results.length - 1];
323
410
  return {
324
411
  publicState: lastResult.publicState ?? bonusResult.publicState,
@@ -337,7 +424,7 @@ export function extractFinalBonusState(bonusResult: any): {
337
424
  export function getRngOutcome(
338
425
  engine: BonnysFortuneV1GameEngine,
339
426
  commandId: string,
340
- ): any {
427
+ ): RngOutcome {
341
428
  return engine.rngService.getRngOutcomeForCommand(commandId);
342
429
  }
343
430
 
@@ -346,7 +433,7 @@ export function getRngOutcome(
346
433
  */
347
434
  export function getAllRngOutcomes(
348
435
  engine: BonnysFortuneV1GameEngine,
349
- ): any {
436
+ ): Record<string, RngOutcome> {
350
437
  return engine.rngService.getAllRngOutcome();
351
438
  }
352
439
 
@@ -358,9 +445,9 @@ export async function runSpinCycle(
358
445
  spinCount: number,
359
446
  betAmount: number = 1.0,
360
447
  ): Promise<{
361
- results: any[];
362
- finalPublicState: any;
363
- finalPrivateState: any;
448
+ results: BonnysFortuneCommandResult[];
449
+ finalPublicState: BonnysFortunePublicState;
450
+ finalPrivateState: BonnysFortunePrivateState;
364
451
  }> {
365
452
  const session = await initSession(engine, betAmount);
366
453
  const results = await executeSpins(
@@ -16,7 +16,31 @@ import {
16
16
  extractFinalBonusState,
17
17
  debugTriggerBonus,
18
18
  resetCommandIdCounter,
19
+ BonnysFortuneCommandResult,
20
+ getOutcome,
19
21
  } from './helpers/test-engine-factory';
22
+ import { BonnysFortuneV1GameEngine } from '../bonnys-fortune-v1.game-engine';
23
+ import {
24
+ BonnysFortunePublicState,
25
+ BonnysFortunePrivateState,
26
+ SpinResult,
27
+ BonusRoundResult,
28
+ SpinResultResponse,
29
+ } from '../logic/bonnys-fortune.types';
30
+
31
+ /** Extract playerWinning from a command result outcome. */
32
+ function extractPlayerWinning(result: BonnysFortuneCommandResult): number {
33
+ const outcome = result.outcome as unknown as {
34
+ result?: { playerWinning?: number; totalWin?: number };
35
+ playerWinning?: number;
36
+ totalWin?: number;
37
+ } | null;
38
+ return outcome?.result?.playerWinning
39
+ ?? outcome?.playerWinning
40
+ ?? outcome?.result?.totalWin
41
+ ?? outcome?.totalWin
42
+ ?? 0;
43
+ }
20
44
 
21
45
  // ---------------------------------------------------------------------------
22
46
  // Helpers
@@ -24,15 +48,15 @@ import {
24
48
 
25
49
  /** Drain all pending bonuses, including chained ones (e.g. bonusGame3 -> bonusGame1). */
26
50
  async function drainAllBonuses(
27
- engine: any,
28
- pub: any,
29
- priv: any,
51
+ engine: BonnysFortuneV1GameEngine,
52
+ pub: BonnysFortunePublicState,
53
+ priv: BonnysFortunePrivateState,
30
54
  betAmount: number,
31
55
  tracker: {
32
56
  bonusTriggersPerType: Record<string, number>;
33
57
  totalWon: number;
34
58
  },
35
- ): Promise<{ pub: any; priv: any }> {
59
+ ): Promise<{ pub: BonnysFortunePublicState; priv: BonnysFortunePrivateState }> {
36
60
  let maxDepth = 10; // safety guard against infinite chaining
37
61
  while (priv.pendingBonuses?.length > 0 && maxDepth-- > 0) {
38
62
  const bonus = priv.pendingBonuses[0];
@@ -51,22 +75,14 @@ async function drainAllBonuses(
51
75
  expect(bonusResult.success).toBe(true);
52
76
 
53
77
  // Sum bonus winnings from results array
54
- if (bonusResult.outcome?.results) {
55
- for (const r of bonusResult.outcome.results) {
56
- const win =
57
- r.result?.playerWinning ??
58
- r.playerWinning ??
59
- r.result?.totalWin ??
60
- r.totalWin ??
61
- 0;
78
+ const bonusOutcome = bonusResult.outcome as unknown as BonusRoundResult<SpinResult> | null;
79
+ if (bonusOutcome?.results) {
80
+ for (const r of bonusOutcome.results) {
81
+ const win = r.result?.playerWinning ?? 0;
62
82
  tracker.totalWon += win;
63
83
  }
64
84
  } else {
65
- const win =
66
- (bonusResult.outcome as any)?.result?.playerWinning ??
67
- (bonusResult.outcome as any)?.playerWinning ??
68
- 0;
69
- tracker.totalWon += win;
85
+ tracker.totalWon += extractPlayerWinning(bonusResult);
70
86
  }
71
87
 
72
88
  const finalState = extractFinalBonusState(bonusResult);
@@ -77,7 +93,7 @@ async function drainAllBonuses(
77
93
  }
78
94
 
79
95
  /** Validate state consistency assertions after any command. */
80
- function assertStateConsistency(pub: any, priv: any): void {
96
+ function assertStateConsistency(pub: BonnysFortunePublicState, priv: BonnysFortunePrivateState): void {
81
97
  // publicState.bonusMeters has exactly 3 keys
82
98
  expect(pub.bonusMeters).toBeDefined();
83
99
  expect(Object.keys(pub.bonusMeters).length).toBe(3);
@@ -177,10 +193,7 @@ describe('Integration: 1000-Spin Simulation — Bonnys Fortune', () => {
177
193
  spinCount++;
178
194
  totalWagered += betAmount;
179
195
 
180
- const win =
181
- (retry.outcome as any)?.result?.playerWinning ??
182
- (retry.outcome as any)?.playerWinning ??
183
- 0;
196
+ const win = extractPlayerWinning(retry);
184
197
  totalWon += win;
185
198
 
186
199
  pub = retry.publicState;
@@ -191,10 +204,7 @@ describe('Integration: 1000-Spin Simulation — Bonnys Fortune', () => {
191
204
  spinCount++;
192
205
  totalWagered += betAmount;
193
206
 
194
- const win =
195
- (spinResult.outcome as any)?.result?.playerWinning ??
196
- (spinResult.outcome as any)?.playerWinning ??
197
- 0;
207
+ const win = extractPlayerWinning(spinResult);
198
208
  totalWon += win;
199
209
 
200
210
  pub = spinResult.publicState;
@@ -267,9 +277,10 @@ describe('Integration: 1000-Spin Simulation — Bonnys Fortune', () => {
267
277
 
268
278
  // Outcome assertions
269
279
  expect(debugResult.outcome).toBeDefined();
270
- expect(debugResult.outcome.nextSpinType).toBe(expectedNextSpinType);
271
- expect(debugResult.outcome.autoStarted).toBe(false);
272
- expect(debugResult.outcome.requiresStartCommand).toBe(true);
280
+ const debugOutcome = getOutcome(debugResult);
281
+ expect(debugOutcome.nextSpinType).toBe(expectedNextSpinType);
282
+ expect(debugOutcome.autoStarted).toBe(false);
283
+ expect(debugOutcome.requiresStartCommand).toBe(true);
273
284
 
274
285
  // Pending bonuses
275
286
  expect(debugResult.privateState.pendingBonuses.length).toBe(1);
@@ -285,9 +296,10 @@ describe('Integration: 1000-Spin Simulation — Bonnys Fortune', () => {
285
296
 
286
297
  expect(bonusResult.success).toBe(true);
287
298
  expect(bonusResult.outcome).toBeDefined();
288
- expect(bonusResult.outcome.results).toBeDefined();
289
- expect(Array.isArray(bonusResult.outcome.results)).toBe(true);
290
- expect(bonusResult.outcome.results.length).toBeGreaterThan(0);
299
+ const bonusOutcome = getOutcome(bonusResult);
300
+ expect(bonusOutcome.results).toBeDefined();
301
+ expect(Array.isArray(bonusOutcome.results)).toBe(true);
302
+ expect(bonusOutcome.results!.length).toBeGreaterThan(0);
291
303
 
292
304
  // Final state
293
305
  const finalState = extractFinalBonusState(bonusResult);
@@ -370,13 +382,11 @@ describe('Integration: 1000-Spin Simulation — Bonnys Fortune', () => {
370
382
  completions++;
371
383
 
372
384
  // Track total wins from bonus results
373
- if (bonusResult.outcome?.results) {
374
- for (const r of bonusResult.outcome.results) {
385
+ const stressOutcome = getOutcome(bonusResult);
386
+ if (stressOutcome.results) {
387
+ for (const r of stressOutcome.results) {
375
388
  const w =
376
389
  r.result?.playerWinning ??
377
- r.playerWinning ??
378
- r.result?.totalWin ??
379
- r.totalWin ??
380
390
  0;
381
391
  wins += w;
382
392
  }
@@ -467,10 +477,7 @@ describe('Integration: 1000-Spin Simulation — Bonnys Fortune', () => {
467
477
  paidSpins++;
468
478
  totalWagered += betAmount;
469
479
 
470
- const win =
471
- (spinResult.outcome as any)?.result?.playerWinning ??
472
- (spinResult.outcome as any)?.playerWinning ??
473
- 0;
480
+ const win = extractPlayerWinning(spinResult);
474
481
  totalWon += win;
475
482
 
476
483
  pub = spinResult.publicState;
@@ -550,10 +557,11 @@ describe('Integration: 1000-Spin Simulation — Bonnys Fortune', () => {
550
557
  expect(bonusResult.success).toBe(true);
551
558
 
552
559
  // Check winnings are non-negative
553
- if (bonusResult.outcome?.results) {
554
- for (const r of bonusResult.outcome.results) {
560
+ const consistencyOutcome = getOutcome(bonusResult);
561
+ if (consistencyOutcome.results) {
562
+ for (const r of consistencyOutcome.results) {
555
563
  const w =
556
- r.result?.playerWinning ?? r.playerWinning ?? 0;
564
+ r.result?.playerWinning ?? 0;
557
565
  expect(w).toBeGreaterThanOrEqual(0);
558
566
  }
559
567
  }
@@ -578,10 +586,7 @@ describe('Integration: 1000-Spin Simulation — Bonnys Fortune', () => {
578
586
  if (!spinResult) continue;
579
587
 
580
588
  // Check spin winning is non-negative
581
- const spinWin =
582
- (spinResult.outcome as any)?.result?.playerWinning ??
583
- (spinResult.outcome as any)?.playerWinning ??
584
- 0;
589
+ const spinWin = extractPlayerWinning(spinResult);
585
590
  expect(spinWin).toBeGreaterThanOrEqual(0);
586
591
 
587
592
  pub = spinResult.publicState;