@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
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
425
|
-
|
|
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
|
-
|
|
441
|
-
|
|
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
|
-
|
|
453
|
-
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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:
|
|
136
|
-
privateState:
|
|
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
|
|
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
|
|
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:
|
|
171
|
-
privateState:
|
|
255
|
+
publicState: BonnysFortunePublicState,
|
|
256
|
+
privateState: BonnysFortunePrivateState,
|
|
172
257
|
betAmount: number = 1.0,
|
|
173
|
-
): Promise<
|
|
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:
|
|
198
|
-
privateState:
|
|
282
|
+
publicState: BonnysFortunePublicState,
|
|
283
|
+
privateState: BonnysFortunePrivateState,
|
|
199
284
|
betAmount: number = 1.0,
|
|
200
|
-
): Promise<
|
|
285
|
+
): Promise<BonnysFortuneCommandResult | null> {
|
|
201
286
|
try {
|
|
202
287
|
return await executeSpin(engine, publicState, privateState, betAmount);
|
|
203
|
-
} catch (error:
|
|
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:
|
|
219
|
-
privateState:
|
|
304
|
+
publicState: BonnysFortunePublicState,
|
|
305
|
+
privateState: BonnysFortunePrivateState,
|
|
220
306
|
count: number,
|
|
221
307
|
betAmount: number = 1.0,
|
|
222
|
-
): Promise<
|
|
223
|
-
const results:
|
|
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:
|
|
243
|
-
privateState:
|
|
328
|
+
publicState: BonnysFortunePublicState,
|
|
329
|
+
privateState: BonnysFortunePrivateState,
|
|
244
330
|
bonusType: string = 'bonusGame1',
|
|
245
331
|
betAmount: number = 1.0,
|
|
246
|
-
): Promise<
|
|
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:
|
|
271
|
-
privateState:
|
|
356
|
+
publicState: BonnysFortunePublicState,
|
|
357
|
+
privateState: BonnysFortunePrivateState,
|
|
272
358
|
bonusType: string = 'bonusGame1',
|
|
273
359
|
betAmount: number = 1.0,
|
|
274
|
-
): Promise<
|
|
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:
|
|
294
|
-
privateState:
|
|
379
|
+
publicState: BonnysFortunePublicState,
|
|
380
|
+
privateState: BonnysFortunePrivateState,
|
|
295
381
|
meterId: string,
|
|
296
382
|
progress: number,
|
|
297
|
-
): Promise<
|
|
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:
|
|
317
|
-
publicState:
|
|
318
|
-
privateState:
|
|
402
|
+
export function extractFinalBonusState(bonusResult: BonnysFortuneCommandResult): {
|
|
403
|
+
publicState: BonnysFortunePublicState;
|
|
404
|
+
privateState: BonnysFortunePrivateState;
|
|
319
405
|
} {
|
|
320
|
-
const
|
|
321
|
-
|
|
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
|
-
):
|
|
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
|
-
):
|
|
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:
|
|
362
|
-
finalPublicState:
|
|
363
|
-
finalPrivateState:
|
|
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:
|
|
28
|
-
pub:
|
|
29
|
-
priv:
|
|
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:
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
271
|
-
expect(
|
|
272
|
-
expect(
|
|
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
|
-
|
|
289
|
-
expect(
|
|
290
|
-
expect(
|
|
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
|
-
|
|
374
|
-
|
|
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
|
-
|
|
554
|
-
|
|
560
|
+
const consistencyOutcome = getOutcome(bonusResult);
|
|
561
|
+
if (consistencyOutcome.results) {
|
|
562
|
+
for (const r of consistencyOutcome.results) {
|
|
555
563
|
const w =
|
|
556
|
-
r.result?.playerWinning ??
|
|
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;
|