@omnitronix/bonnys-fortune-game-engine 1.3.2 → 1.5.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 (175) hide show
  1. package/README.md +25 -15
  2. package/dist/__tests__/base-game.test.js +255 -0
  3. package/dist/__tests__/base-game.test.js.map +1 -0
  4. package/dist/__tests__/bonnys-fortune-v1.game-engine.test.js +1 -1
  5. package/dist/__tests__/bonus-handlers.test.js +303 -0
  6. package/dist/__tests__/bonus-handlers.test.js.map +1 -0
  7. package/dist/__tests__/bonus-meters.test.js +508 -0
  8. package/dist/__tests__/bonus-meters.test.js.map +1 -0
  9. package/dist/__tests__/collect-feature-handler.test.js +386 -0
  10. package/dist/__tests__/collect-feature-handler.test.js.map +1 -0
  11. package/dist/__tests__/comprehensive.test.js +747 -0
  12. package/dist/__tests__/comprehensive.test.js.map +1 -0
  13. package/dist/__tests__/error-paths.test.js +373 -0
  14. package/dist/__tests__/error-paths.test.js.map +1 -0
  15. package/dist/__tests__/helpers/test-engine-factory.js +268 -0
  16. package/dist/__tests__/helpers/test-engine-factory.js.map +1 -0
  17. package/dist/__tests__/rng-gli19-compliance.test.js +292 -0
  18. package/dist/__tests__/rng-gli19-compliance.test.js.map +1 -0
  19. package/dist/__tests__/rng-security.test.js +383 -0
  20. package/dist/__tests__/rng-security.test.js.map +1 -0
  21. package/dist/__tests__/rtp-simulation.test.js +242 -0
  22. package/dist/__tests__/rtp-simulation.test.js.map +1 -0
  23. package/dist/__tests__/state-transitions.test.js +307 -0
  24. package/dist/__tests__/state-transitions.test.js.map +1 -0
  25. package/dist/__tests__/steering-fortune-handler.test.js +286 -0
  26. package/dist/__tests__/steering-fortune-handler.test.js.map +1 -0
  27. package/dist/__tests__/symbol-distribution.test.js +195 -0
  28. package/dist/__tests__/symbol-distribution.test.js.map +1 -0
  29. package/dist/__tests__/treasure-hunt-handler.test.js +295 -0
  30. package/dist/__tests__/treasure-hunt-handler.test.js.map +1 -0
  31. package/dist/__tests__/win-calculator.test.js +515 -0
  32. package/dist/__tests__/win-calculator.test.js.map +1 -0
  33. package/dist/bonnys-fortune-v1.game-engine.js +14 -25
  34. package/dist/bonnys-fortune-v1.game-engine.js.map +1 -1
  35. package/dist/config/game-logic-config/game-logic-config.js +0 -2
  36. package/dist/config/game-logic-config/game-logic-config.js.map +1 -1
  37. package/dist/domain/types/{cheat-trigger-bonus-command.js → debug-trigger-bonus-command.js} +1 -1
  38. package/dist/domain/types/debug-trigger-bonus-command.js.map +1 -0
  39. package/dist/domain/types/{cheat-trigger-bonus-request.dto.js → debug-trigger-bonus-request.dto.js} +1 -1
  40. package/dist/domain/types/debug-trigger-bonus-request.dto.js.map +1 -0
  41. package/dist/domain/types/{cheat-update-bonus-meter-progress-command.js → debug-update-bonus-meter-progress-command.js} +1 -1
  42. package/dist/domain/types/debug-update-bonus-meter-progress-command.js.map +1 -0
  43. package/dist/domain/types/{cheat-update-bonus-meter-progress-request.dto.js → debug-update-bonus-meter-progress-request.dto.js} +1 -1
  44. package/dist/domain/types/debug-update-bonus-meter-progress-request.dto.js.map +1 -0
  45. package/dist/index.js.map +1 -1
  46. package/dist/logic/bonnys-fortune.game-logic.js +37 -17
  47. package/dist/logic/bonnys-fortune.game-logic.js.map +1 -1
  48. package/dist/logic/handlers/base-game.handler.js +1 -5
  49. package/dist/logic/handlers/base-game.handler.js.map +1 -1
  50. package/dist/rng/DummyRngClient.js +8 -1
  51. package/dist/rng/DummyRngClient.js.map +1 -1
  52. package/dist/rng/rng-client.factory.js.map +1 -1
  53. package/dist/validation/bonnys-fortune/bonnys-fortune-config.dto.js +0 -9
  54. package/dist/validation/bonnys-fortune/bonnys-fortune-config.dto.js.map +1 -1
  55. package/package.json +6 -6
  56. package/dist/__tests__/bonnys-fortune-v1.game-engine.test.d.ts +0 -1
  57. package/dist/bonnys-fortune-v1.game-engine.d.ts +0 -36
  58. package/dist/config/game-logic-config/file-system.game-logic-config-loader.d.ts +0 -4
  59. package/dist/config/game-logic-config/game-logic-config-loader.d.ts +0 -3
  60. package/dist/config/game-logic-config/game-logic-config.d.ts +0 -2
  61. package/dist/config/reel-strips-config/file-system.reel-strips-config-loader.d.ts +0 -4
  62. package/dist/config/reel-strips-config/reel-strip-option.dto.d.ts +0 -4
  63. package/dist/config/reel-strips-config/reel-strips-config-loader.d.ts +0 -3
  64. package/dist/config/reel-strips-config/reel-strips-config.dto.d.ts +0 -5
  65. package/dist/config/reel-strips-config/reel.dto.d.ts +0 -5
  66. package/dist/domain/game-round.types.d.ts +0 -9
  67. package/dist/domain/mappers/reel-strips-config.mapper.d.ts +0 -4
  68. package/dist/domain/reel-strip-option.d.ts +0 -7
  69. package/dist/domain/reel-strips-config.d.ts +0 -9
  70. package/dist/domain/reel.d.ts +0 -8
  71. package/dist/domain/types/cheat-trigger-bonus-command.d.ts +0 -5
  72. package/dist/domain/types/cheat-trigger-bonus-command.js.map +0 -1
  73. package/dist/domain/types/cheat-trigger-bonus-request.dto.d.ts +0 -5
  74. package/dist/domain/types/cheat-trigger-bonus-request.dto.js.map +0 -1
  75. package/dist/domain/types/cheat-update-bonus-meter-progress-command.d.ts +0 -5
  76. package/dist/domain/types/cheat-update-bonus-meter-progress-command.js.map +0 -1
  77. package/dist/domain/types/cheat-update-bonus-meter-progress-request.dto.d.ts +0 -5
  78. package/dist/domain/types/cheat-update-bonus-meter-progress-request.dto.js.map +0 -1
  79. package/dist/domain/types/game-spin-command.d.ts +0 -8
  80. package/dist/domain/types/game-spin-input.dto.d.ts +0 -8
  81. package/dist/domain/types/game-spin-output.dto.d.ts +0 -119
  82. package/dist/domain/types/game-symbols.response.dto.d.ts +0 -10
  83. package/dist/domain/types/reel-strip-option.dto.d.ts +0 -4
  84. package/dist/domain/types/reel-strips-config.dto.d.ts +0 -5
  85. package/dist/domain/types/reel.dto.d.ts +0 -5
  86. package/dist/domain/types/start-bonus-round-command.d.ts +0 -8
  87. package/dist/domain/types/start-bonus-round-input.dto.d.ts +0 -10
  88. package/dist/game-engine.interface.d.ts +0 -41
  89. package/dist/helpers/generate-hash.d.ts +0 -1
  90. package/dist/helpers/number-helper.d.ts +0 -23
  91. package/dist/helpers/optional-boolean-mapper.d.ts +0 -1
  92. package/dist/helpers/uuid-helper.d.ts +0 -3
  93. package/dist/helpers/validation-helper.d.ts +0 -14
  94. package/dist/index.d.ts +0 -2
  95. package/dist/logger/logger.d.ts +0 -22
  96. package/dist/logger/logger.js +0 -140
  97. package/dist/logger/logger.js.map +0 -1
  98. package/dist/logic/bonnys-fortune.game-logic.d.ts +0 -32
  99. package/dist/logic/bonnys-fortune.spin-generator.d.ts +0 -19
  100. package/dist/logic/bonnys-fortune.types.d.ts +0 -170
  101. package/dist/logic/bonnys-fortune.win-calculator.d.ts +0 -10
  102. package/dist/logic/game-logic-config.interface.d.ts +0 -180
  103. package/dist/logic/handlers/base-game.handler.d.ts +0 -15
  104. package/dist/logic/handlers/collect-feature-bonus.handler.d.ts +0 -13
  105. package/dist/logic/handlers/free-spins-bonus.handler.d.ts +0 -11
  106. package/dist/logic/handlers/steering-to-the-fortune-bonus.handler.d.ts +0 -11
  107. package/dist/logic/handlers/treasure-hunt-bonus.handler.d.ts +0 -15
  108. package/dist/rng/DummyRngClient.d.ts +0 -18
  109. package/dist/rng/rng-client.factory.d.ts +0 -7
  110. package/dist/rng/rng-client.interface.d.ts +0 -16
  111. package/dist/rng/rng-service.d.ts +0 -26
  112. package/dist/validation/abstract-game-logic-config.validator.d.ts +0 -4
  113. package/dist/validation/bonnys-fortune/bonnys-fortune-config.dto.d.ts +0 -131
  114. package/dist/validation/bonnys-fortune/bonnys-fortune-config.validator.d.ts +0 -5
  115. package/dist/validation/custom-decorators/IsNestedIntArray.d.ts +0 -2
  116. package/dist/validation/game-logic-config-validation.service.d.ts +0 -8
  117. package/dist/validation/reel-strips-config-validation.service.d.ts +0 -5
  118. package/src/__tests__/bonnys-fortune-v1.game-engine.test.ts +0 -80
  119. package/src/bonnys-fortune-v1.game-engine.ts +0 -314
  120. package/src/config/game-logic-config/file-system.game-logic-config-loader.ts +0 -27
  121. package/src/config/game-logic-config/game-logic-config-loader.ts +0 -3
  122. package/src/config/game-logic-config/game-logic-config.ts +0 -382
  123. package/src/config/reel-strips-config/file-system.reel-strips-config-loader.ts +0 -43
  124. package/src/config/reel-strips-config/reel-strip-option.dto.ts +0 -13
  125. package/src/config/reel-strips-config/reel-strips-config-loader.ts +0 -9
  126. package/src/config/reel-strips-config/reel-strips-config.dto.ts +0 -16
  127. package/src/config/reel-strips-config/reel.dto.ts +0 -16
  128. package/src/config/reel-strips-config/reels-BASE.csv +0 -52
  129. package/src/config/reel-strips-config/reels-BONUS.csv +0 -52
  130. package/src/domain/game-round.types.ts +0 -10
  131. package/src/domain/mappers/reel-strips-config.mapper.ts +0 -16
  132. package/src/domain/reel-strip-option.ts +0 -15
  133. package/src/domain/reel-strips-config.ts +0 -21
  134. package/src/domain/reel.ts +0 -17
  135. package/src/domain/types/cheat-trigger-bonus-command.ts +0 -5
  136. package/src/domain/types/cheat-trigger-bonus-request.dto.ts +0 -5
  137. package/src/domain/types/cheat-update-bonus-meter-progress-command.ts +0 -6
  138. package/src/domain/types/cheat-update-bonus-meter-progress-request.dto.ts +0 -5
  139. package/src/domain/types/game-spin-command.ts +0 -8
  140. package/src/domain/types/game-spin-input.dto.ts +0 -8
  141. package/src/domain/types/game-spin-output.dto.ts +0 -142
  142. package/src/domain/types/game-symbols.response.dto.ts +0 -11
  143. package/src/domain/types/reel-strip-option.dto.ts +0 -13
  144. package/src/domain/types/reel-strips-config.dto.ts +0 -15
  145. package/src/domain/types/reel.dto.ts +0 -15
  146. package/src/domain/types/start-bonus-round-command.ts +0 -8
  147. package/src/domain/types/start-bonus-round-input.dto.ts +0 -10
  148. package/src/game-engine.interface.ts +0 -59
  149. package/src/helpers/generate-hash.ts +0 -5
  150. package/src/helpers/number-helper.ts +0 -41
  151. package/src/helpers/optional-boolean-mapper.ts +0 -5
  152. package/src/helpers/uuid-helper.ts +0 -7
  153. package/src/helpers/validation-helper.ts +0 -27
  154. package/src/index.ts +0 -3
  155. package/src/logger/logger.ts +0 -178
  156. package/src/logic/bonnys-fortune.game-logic.ts +0 -490
  157. package/src/logic/bonnys-fortune.spin-generator.ts +0 -277
  158. package/src/logic/bonnys-fortune.types.ts +0 -223
  159. package/src/logic/bonnys-fortune.win-calculator.ts +0 -210
  160. package/src/logic/game-logic-config.interface.ts +0 -176
  161. package/src/logic/handlers/base-game.handler.ts +0 -221
  162. package/src/logic/handlers/collect-feature-bonus.handler.ts +0 -301
  163. package/src/logic/handlers/free-spins-bonus.handler.ts +0 -119
  164. package/src/logic/handlers/steering-to-the-fortune-bonus.handler.ts +0 -118
  165. package/src/logic/handlers/treasure-hunt-bonus.handler.ts +0 -232
  166. package/src/rng/DummyRngClient.ts +0 -108
  167. package/src/rng/rng-client.factory.ts +0 -27
  168. package/src/rng/rng-client.interface.ts +0 -38
  169. package/src/rng/rng-service.ts +0 -130
  170. package/src/validation/abstract-game-logic-config.validator.ts +0 -20
  171. package/src/validation/bonnys-fortune/bonnys-fortune-config.dto.ts +0 -379
  172. package/src/validation/bonnys-fortune/bonnys-fortune-config.validator.ts +0 -8
  173. package/src/validation/custom-decorators/IsNestedIntArray.ts +0 -29
  174. package/src/validation/game-logic-config-validation.service.ts +0 -28
  175. package/src/validation/reel-strips-config-validation.service.ts +0 -29
@@ -0,0 +1,747 @@
1
+ "use strict";
2
+ /**
3
+ * Comprehensive Test Suite for Bonny's Fortune Game Engine
4
+ *
5
+ * Coverage:
6
+ * - Session initialization
7
+ * - Base game spins with bonus meter accumulation
8
+ * - bonusGame1 (Treasure Hunt) flow
9
+ * - bonusGame2 (Free Spins) flow
10
+ * - bonusGame3 (Steering to the Fortune) flow
11
+ * - collectFeature flow
12
+ * - Bonus meter progression
13
+ * - Spin/Bonus combinations
14
+ * - State persistence
15
+ * - Error handling
16
+ */
17
+ Object.defineProperty(exports, "__esModule", { value: true });
18
+ const bonnys_fortune_v1_game_engine_1 = require("../bonnys-fortune-v1.game-engine");
19
+ describe("Bonny's Fortune - Comprehensive Test Suite", () => {
20
+ let engine;
21
+ beforeEach(() => {
22
+ engine = new bonnys_fortune_v1_game_engine_1.BonnysFortuneV1GameEngine();
23
+ });
24
+ // ============================================================
25
+ // SESSION INITIALIZATION
26
+ // ============================================================
27
+ describe('Session Initialization', () => {
28
+ it('should initialize session with default config', async () => {
29
+ const command = {
30
+ id: 'init-1',
31
+ type: 'INIT_SESSION_STATE',
32
+ payload: {
33
+ betAmount: 1.0,
34
+ currency: 'EUR',
35
+ },
36
+ };
37
+ const result = await engine.processCommand(null, null, command);
38
+ expect(result.success).toBe(true);
39
+ expect(result.publicState).toBeDefined();
40
+ expect(result.privateState).toBeDefined();
41
+ });
42
+ it('should initialize with bonus meters', async () => {
43
+ const result = await engine.processCommand(null, null, {
44
+ id: 'init-meters',
45
+ type: 'INIT_SESSION_STATE',
46
+ payload: { betAmount: 1.0 },
47
+ });
48
+ expect(result.success).toBe(true);
49
+ expect(result.publicState.bonusMeters).toBeDefined();
50
+ });
51
+ it('should initialize session with various bet amounts', async () => {
52
+ const betAmounts = [0.5, 1.0, 2.0, 5.0, 10.0];
53
+ for (const betAmount of betAmounts) {
54
+ const result = await engine.processCommand(null, null, {
55
+ id: `init-${betAmount}`,
56
+ type: 'INIT_SESSION_STATE',
57
+ payload: { betAmount },
58
+ });
59
+ expect(result.success).toBe(true);
60
+ }
61
+ });
62
+ it('should return game engine info', () => {
63
+ const info = engine.getGameEngineInfo();
64
+ expect(info.gameCode).toBe('bonnys-fortune');
65
+ expect(info.version).toBeDefined();
66
+ expect(info.rtp).toBeGreaterThan(90);
67
+ expect(info.gameType).toBe('slot');
68
+ });
69
+ });
70
+ // ============================================================
71
+ // BASE GAME SPINS
72
+ // ============================================================
73
+ describe('Base Game Spins', () => {
74
+ let publicState;
75
+ let privateState;
76
+ beforeEach(async () => {
77
+ const initResult = await engine.processCommand(null, null, {
78
+ id: 'init',
79
+ type: 'INIT_SESSION_STATE',
80
+ payload: { betAmount: 1.0 },
81
+ });
82
+ publicState = initResult.publicState;
83
+ privateState = initResult.privateState;
84
+ });
85
+ it('should execute a base game spin', async () => {
86
+ const result = await engine.processCommand(publicState, privateState, {
87
+ id: 'spin-1',
88
+ type: 'SPIN',
89
+ payload: {
90
+ sessionId: 'test-session',
91
+ betAmount: 1.0,
92
+ },
93
+ });
94
+ expect(result.success).toBe(true);
95
+ expect(result.publicState).toBeDefined();
96
+ expect(result.privateState).toBeDefined();
97
+ expect(result.outcome).toBeDefined();
98
+ });
99
+ it('should update bonus meters on spin', async () => {
100
+ const result = await engine.processCommand(publicState, privateState, {
101
+ id: 'spin-meter',
102
+ type: 'SPIN',
103
+ payload: {
104
+ sessionId: 'test',
105
+ betAmount: 1.0,
106
+ },
107
+ });
108
+ expect(result.success).toBe(true);
109
+ expect(result.publicState.bonusMeters).toBeDefined();
110
+ });
111
+ it('should handle multiple consecutive spins', async () => {
112
+ let currentPublic = publicState;
113
+ let currentPrivate = privateState;
114
+ for (let i = 0; i < 10; i++) {
115
+ const result = await engine.processCommand(currentPublic, currentPrivate, {
116
+ id: `spin-${i}`,
117
+ type: 'SPIN',
118
+ payload: {
119
+ sessionId: 'test',
120
+ betAmount: 1.0,
121
+ },
122
+ });
123
+ expect(result.success).toBe(true);
124
+ currentPublic = result.publicState;
125
+ currentPrivate = result.privateState;
126
+ }
127
+ });
128
+ it('should accumulate bonus meter progress across spins', async () => {
129
+ let currentPublic = publicState;
130
+ let currentPrivate = privateState;
131
+ // Perform multiple spins to accumulate meter progress
132
+ for (let i = 0; i < 10; i++) {
133
+ const result = await engine.processCommand(currentPublic, currentPrivate, {
134
+ id: `spin-${i}`,
135
+ type: 'SPIN',
136
+ payload: {
137
+ sessionId: 'test',
138
+ betAmount: 1.0,
139
+ },
140
+ });
141
+ expect(result.success).toBe(true);
142
+ currentPublic = result.publicState;
143
+ currentPrivate = result.privateState;
144
+ }
145
+ // Bonus meters should exist
146
+ expect(currentPublic.bonusMeters).toBeDefined();
147
+ });
148
+ });
149
+ // ============================================================
150
+ // BONUS GAME 1 - TREASURE HUNT
151
+ // ============================================================
152
+ describe('bonusGame1 (Treasure Hunt) Flow', () => {
153
+ let publicState;
154
+ let privateState;
155
+ beforeEach(async () => {
156
+ const initResult = await engine.processCommand(null, null, {
157
+ id: 'init',
158
+ type: 'INIT_SESSION_STATE',
159
+ payload: { betAmount: 1.0 },
160
+ });
161
+ publicState = initResult.publicState;
162
+ privateState = initResult.privateState;
163
+ });
164
+ it('should trigger bonusGame1 via debug command', async () => {
165
+ const result = await engine.processCommand(publicState, privateState, {
166
+ id: 'debug-bg1',
167
+ type: 'DEBUG_TRIGGER_BONUS',
168
+ payload: {
169
+ sessionId: 'test-session',
170
+ bonusType: 'bonusGame1',
171
+ betAmount: 1.0,
172
+ },
173
+ });
174
+ expect(result.success).toBe(true);
175
+ });
176
+ it('should complete full bonusGame1 flow: trigger → continue spinning', async () => {
177
+ // Trigger bonus
178
+ const triggerResult = await engine.processCommand(publicState, privateState, {
179
+ id: 'trigger-bg1',
180
+ type: 'DEBUG_TRIGGER_BONUS',
181
+ payload: {
182
+ sessionId: 'test',
183
+ bonusType: 'bonusGame1',
184
+ betAmount: 1.0,
185
+ },
186
+ });
187
+ expect(triggerResult.success).toBe(true);
188
+ // Continue with spins after bonus trigger
189
+ let currentPublic = triggerResult.publicState;
190
+ let currentPrivate = triggerResult.privateState;
191
+ for (let i = 0; i < 3; i++) {
192
+ const spinResult = await engine.processCommand(currentPublic, currentPrivate, {
193
+ id: `spin-bg1-${i}`,
194
+ type: 'SPIN',
195
+ payload: {
196
+ sessionId: 'test',
197
+ betAmount: 1.0,
198
+ },
199
+ });
200
+ expect(spinResult.success).toBe(true);
201
+ currentPublic = spinResult.publicState;
202
+ currentPrivate = spinResult.privateState;
203
+ }
204
+ });
205
+ it('should handle multiple bonusGame1 triggers', async () => {
206
+ let currentPublic = publicState;
207
+ let currentPrivate = privateState;
208
+ for (let i = 0; i < 3; i++) {
209
+ const result = await engine.processCommand(currentPublic, currentPrivate, {
210
+ id: `trigger-bg1-${i}`,
211
+ type: 'DEBUG_TRIGGER_BONUS',
212
+ payload: {
213
+ sessionId: 'test',
214
+ bonusType: 'bonusGame1',
215
+ betAmount: 1.0,
216
+ },
217
+ });
218
+ expect(result.success).toBe(true);
219
+ currentPublic = result.publicState;
220
+ currentPrivate = result.privateState;
221
+ }
222
+ });
223
+ });
224
+ // ============================================================
225
+ // BONUS GAME 2 - FREE SPINS
226
+ // ============================================================
227
+ describe('bonusGame2 (Free Spins) Flow', () => {
228
+ let publicState;
229
+ let privateState;
230
+ beforeEach(async () => {
231
+ const initResult = await engine.processCommand(null, null, {
232
+ id: 'init',
233
+ type: 'INIT_SESSION_STATE',
234
+ payload: { betAmount: 1.0 },
235
+ });
236
+ publicState = initResult.publicState;
237
+ privateState = initResult.privateState;
238
+ });
239
+ it('should trigger bonusGame2 via debug command', async () => {
240
+ const result = await engine.processCommand(publicState, privateState, {
241
+ id: 'debug-bg2',
242
+ type: 'DEBUG_TRIGGER_BONUS',
243
+ payload: {
244
+ sessionId: 'test-session',
245
+ bonusType: 'bonusGame2',
246
+ betAmount: 1.0,
247
+ },
248
+ });
249
+ expect(result.success).toBe(true);
250
+ });
251
+ it('should complete full bonusGame2 flow: trigger → continue with spins', async () => {
252
+ // Trigger bonus
253
+ const triggerResult = await engine.processCommand(publicState, privateState, {
254
+ id: 'trigger-bg2',
255
+ type: 'DEBUG_TRIGGER_BONUS',
256
+ payload: {
257
+ sessionId: 'test',
258
+ bonusType: 'bonusGame2',
259
+ betAmount: 1.0,
260
+ },
261
+ });
262
+ expect(triggerResult.success).toBe(true);
263
+ // Continue with spins after bonus trigger
264
+ let currentPublic = triggerResult.publicState;
265
+ let currentPrivate = triggerResult.privateState;
266
+ for (let i = 0; i < 3; i++) {
267
+ const spinResult = await engine.processCommand(currentPublic, currentPrivate, {
268
+ id: `spin-bg2-${i}`,
269
+ type: 'SPIN',
270
+ payload: {
271
+ sessionId: 'test',
272
+ betAmount: 1.0,
273
+ },
274
+ });
275
+ expect(spinResult.success).toBe(true);
276
+ currentPublic = spinResult.publicState;
277
+ currentPrivate = spinResult.privateState;
278
+ }
279
+ });
280
+ });
281
+ // ============================================================
282
+ // BONUS GAME 3 - STEERING TO THE FORTUNE
283
+ // ============================================================
284
+ describe('bonusGame3 (Steering to the Fortune) Flow', () => {
285
+ let publicState;
286
+ let privateState;
287
+ beforeEach(async () => {
288
+ const initResult = await engine.processCommand(null, null, {
289
+ id: 'init',
290
+ type: 'INIT_SESSION_STATE',
291
+ payload: { betAmount: 1.0 },
292
+ });
293
+ publicState = initResult.publicState;
294
+ privateState = initResult.privateState;
295
+ });
296
+ it('should trigger bonusGame3 via debug command', async () => {
297
+ const result = await engine.processCommand(publicState, privateState, {
298
+ id: 'debug-bg3',
299
+ type: 'DEBUG_TRIGGER_BONUS',
300
+ payload: {
301
+ sessionId: 'test-session',
302
+ bonusType: 'bonusGame3',
303
+ betAmount: 1.0,
304
+ },
305
+ });
306
+ expect(result.success).toBe(true);
307
+ });
308
+ it('should complete full bonusGame3 flow: trigger → continue with spins', async () => {
309
+ // Trigger bonus
310
+ const triggerResult = await engine.processCommand(publicState, privateState, {
311
+ id: 'trigger-bg3',
312
+ type: 'DEBUG_TRIGGER_BONUS',
313
+ payload: {
314
+ sessionId: 'test',
315
+ bonusType: 'bonusGame3',
316
+ betAmount: 1.0,
317
+ },
318
+ });
319
+ expect(triggerResult.success).toBe(true);
320
+ // Continue with spins after bonus trigger
321
+ let currentPublic = triggerResult.publicState;
322
+ let currentPrivate = triggerResult.privateState;
323
+ for (let i = 0; i < 3; i++) {
324
+ const spinResult = await engine.processCommand(currentPublic, currentPrivate, {
325
+ id: `spin-bg3-${i}`,
326
+ type: 'SPIN',
327
+ payload: {
328
+ sessionId: 'test',
329
+ betAmount: 1.0,
330
+ },
331
+ });
332
+ expect(spinResult.success).toBe(true);
333
+ currentPublic = spinResult.publicState;
334
+ currentPrivate = spinResult.privateState;
335
+ }
336
+ });
337
+ });
338
+ // ============================================================
339
+ // COLLECT FEATURE
340
+ // ============================================================
341
+ describe('collectFeature Flow', () => {
342
+ let publicState;
343
+ let privateState;
344
+ beforeEach(async () => {
345
+ const initResult = await engine.processCommand(null, null, {
346
+ id: 'init',
347
+ type: 'INIT_SESSION_STATE',
348
+ payload: { betAmount: 1.0 },
349
+ });
350
+ publicState = initResult.publicState;
351
+ privateState = initResult.privateState;
352
+ });
353
+ it('should trigger collectFeature via debug command', async () => {
354
+ const result = await engine.processCommand(publicState, privateState, {
355
+ id: 'debug-collect',
356
+ type: 'DEBUG_TRIGGER_BONUS',
357
+ payload: {
358
+ sessionId: 'test-session',
359
+ bonusType: 'collectFeature',
360
+ betAmount: 1.0,
361
+ },
362
+ });
363
+ expect(result.success).toBe(true);
364
+ });
365
+ it('should handle collectFeature with various bet amounts', async () => {
366
+ const betAmounts = [1.0, 2.0, 5.0];
367
+ for (const betAmount of betAmounts) {
368
+ const init = await engine.processCommand(null, null, {
369
+ id: `init-${betAmount}`,
370
+ type: 'INIT_SESSION_STATE',
371
+ payload: { betAmount },
372
+ });
373
+ const result = await engine.processCommand(init.publicState, init.privateState, {
374
+ id: `collect-${betAmount}`,
375
+ type: 'DEBUG_TRIGGER_BONUS',
376
+ payload: {
377
+ sessionId: 'test',
378
+ bonusType: 'collectFeature',
379
+ betAmount,
380
+ },
381
+ });
382
+ expect(result.success).toBe(true);
383
+ }
384
+ });
385
+ });
386
+ // ============================================================
387
+ // BONUS METER MANIPULATION
388
+ // ============================================================
389
+ describe('Bonus Meter Manipulation', () => {
390
+ let publicState;
391
+ let privateState;
392
+ beforeEach(async () => {
393
+ const initResult = await engine.processCommand(null, null, {
394
+ id: 'init',
395
+ type: 'INIT_SESSION_STATE',
396
+ payload: { betAmount: 1.0 },
397
+ });
398
+ publicState = initResult.publicState;
399
+ privateState = initResult.privateState;
400
+ });
401
+ it('should update bonus meter progress via debug command', async () => {
402
+ const result = await engine.processCommand(publicState, privateState, {
403
+ id: 'update-meter',
404
+ type: 'DEBUG_UPDATE_BONUS_METER_PROGRESS',
405
+ payload: {
406
+ sessionId: 'test',
407
+ bonusType: 'bonusGame1',
408
+ progress: 50,
409
+ },
410
+ });
411
+ expect(result).toBeDefined();
412
+ expect(result.success).toBe(true);
413
+ });
414
+ it('should track meter progress across multiple updates', async () => {
415
+ let currentPublic = publicState;
416
+ let currentPrivate = privateState;
417
+ const bonusTypes = ['bonusGame1', 'bonusGame2', 'bonusGame3'];
418
+ for (const bonusType of bonusTypes) {
419
+ const result = await engine.processCommand(currentPublic, currentPrivate, {
420
+ id: `update-${bonusType}`,
421
+ type: 'DEBUG_UPDATE_BONUS_METER_PROGRESS',
422
+ payload: {
423
+ sessionId: 'test',
424
+ bonusType,
425
+ progress: 25,
426
+ },
427
+ });
428
+ expect(result).toBeDefined();
429
+ expect(result.success).toBe(true);
430
+ currentPublic = result.publicState || currentPublic;
431
+ currentPrivate = result.privateState || currentPrivate;
432
+ }
433
+ });
434
+ });
435
+ // ============================================================
436
+ // SPIN/BONUS COMBINATIONS
437
+ // ============================================================
438
+ describe('Spin/Bonus Combinations', () => {
439
+ let publicState;
440
+ let privateState;
441
+ beforeEach(async () => {
442
+ const initResult = await engine.processCommand(null, null, {
443
+ id: 'init',
444
+ type: 'INIT_SESSION_STATE',
445
+ payload: { betAmount: 1.0 },
446
+ });
447
+ publicState = initResult.publicState;
448
+ privateState = initResult.privateState;
449
+ });
450
+ it('should handle spin → bonus → spin sequence', async () => {
451
+ // Initial spins
452
+ let current = await engine.processCommand(publicState, privateState, {
453
+ id: 'spin-1',
454
+ type: 'SPIN',
455
+ payload: {
456
+ sessionId: 'test',
457
+ betAmount: 1.0,
458
+ },
459
+ });
460
+ expect(current.success).toBe(true);
461
+ current = await engine.processCommand(current.publicState, current.privateState, {
462
+ id: 'spin-2',
463
+ type: 'SPIN',
464
+ payload: {
465
+ sessionId: 'test',
466
+ betAmount: 1.0,
467
+ },
468
+ });
469
+ expect(current.success).toBe(true);
470
+ // Trigger bonus
471
+ const bonusResult = await engine.processCommand(current.publicState, current.privateState, {
472
+ id: 'trigger-bonus',
473
+ type: 'DEBUG_TRIGGER_BONUS',
474
+ payload: {
475
+ sessionId: 'test',
476
+ bonusType: 'bonusGame1',
477
+ betAmount: 1.0,
478
+ },
479
+ });
480
+ expect(bonusResult.success).toBe(true);
481
+ // Continue with more spins
482
+ const afterBonus = await engine.processCommand(bonusResult.publicState, bonusResult.privateState, {
483
+ id: 'spin-after',
484
+ type: 'SPIN',
485
+ payload: {
486
+ sessionId: 'test',
487
+ betAmount: 1.0,
488
+ },
489
+ });
490
+ expect(afterBonus).toBeDefined();
491
+ });
492
+ it('should handle all bonus types in sequence', async () => {
493
+ const bonusTypes = ['bonusGame1', 'bonusGame2', 'bonusGame3', 'collectFeature'];
494
+ let currentPublic = publicState;
495
+ let currentPrivate = privateState;
496
+ for (const bonusType of bonusTypes) {
497
+ // Spin first
498
+ const spin = await engine.processCommand(currentPublic, currentPrivate, {
499
+ id: `spin-${bonusType}`,
500
+ type: 'SPIN',
501
+ payload: {
502
+ sessionId: 'test',
503
+ betAmount: 1.0,
504
+ },
505
+ });
506
+ expect(spin.success).toBe(true);
507
+ // Trigger bonus
508
+ const bonus = await engine.processCommand(spin.publicState, spin.privateState, {
509
+ id: `trigger-${bonusType}`,
510
+ type: 'DEBUG_TRIGGER_BONUS',
511
+ payload: {
512
+ sessionId: 'test',
513
+ bonusType,
514
+ betAmount: 1.0,
515
+ },
516
+ });
517
+ expect(bonus.success).toBe(true);
518
+ currentPublic = bonus.publicState;
519
+ currentPrivate = bonus.privateState;
520
+ }
521
+ });
522
+ it('should handle rapid bonus type switching', async () => {
523
+ const bonusSequence = [
524
+ 'bonusGame1',
525
+ 'bonusGame2',
526
+ 'bonusGame1',
527
+ 'bonusGame3',
528
+ 'collectFeature',
529
+ 'bonusGame2',
530
+ ];
531
+ let currentPublic = publicState;
532
+ let currentPrivate = privateState;
533
+ for (let i = 0; i < bonusSequence.length; i++) {
534
+ const result = await engine.processCommand(currentPublic, currentPrivate, {
535
+ id: `rapid-${i}`,
536
+ type: 'DEBUG_TRIGGER_BONUS',
537
+ payload: {
538
+ sessionId: 'test',
539
+ bonusType: bonusSequence[i],
540
+ betAmount: 1.0,
541
+ },
542
+ });
543
+ expect(result.success).toBe(true);
544
+ currentPublic = result.publicState;
545
+ currentPrivate = result.privateState;
546
+ }
547
+ });
548
+ it('should handle mixed bet amounts with different bonus types', async () => {
549
+ const configs = [
550
+ { betAmount: 1.0, bonusType: 'bonusGame1' },
551
+ { betAmount: 2.0, bonusType: 'bonusGame2' },
552
+ { betAmount: 5.0, bonusType: 'bonusGame3' },
553
+ { betAmount: 10.0, bonusType: 'collectFeature' },
554
+ ];
555
+ for (const { betAmount, bonusType } of configs) {
556
+ const init = await engine.processCommand(null, null, {
557
+ id: `init-${betAmount}`,
558
+ type: 'INIT_SESSION_STATE',
559
+ payload: { betAmount },
560
+ });
561
+ // Spin
562
+ const spin = await engine.processCommand(init.publicState, init.privateState, {
563
+ id: `spin-${betAmount}`,
564
+ type: 'SPIN',
565
+ payload: {
566
+ sessionId: 'test',
567
+ betAmount,
568
+ },
569
+ });
570
+ expect(spin.success).toBe(true);
571
+ // Trigger bonus
572
+ const bonus = await engine.processCommand(spin.publicState, spin.privateState, {
573
+ id: `bonus-${betAmount}`,
574
+ type: 'DEBUG_TRIGGER_BONUS',
575
+ payload: {
576
+ sessionId: 'test',
577
+ bonusType,
578
+ betAmount,
579
+ },
580
+ });
581
+ expect(bonus.success).toBe(true);
582
+ }
583
+ });
584
+ });
585
+ // ============================================================
586
+ // DEBUG TRIGGER ERROR CASES
587
+ // ============================================================
588
+ describe('Debug Trigger Error Cases', () => {
589
+ let publicState;
590
+ let privateState;
591
+ beforeEach(async () => {
592
+ const initResult = await engine.processCommand(null, null, {
593
+ id: 'init',
594
+ type: 'INIT_SESSION_STATE',
595
+ payload: { betAmount: 1.0 },
596
+ });
597
+ publicState = initResult.publicState;
598
+ privateState = initResult.privateState;
599
+ });
600
+ it('should accept any bonus type in debug mode', async () => {
601
+ // Note: DEBUG_TRIGGER_BONUS does not validate bonus types
602
+ // It accepts any string as bonusType for flexibility
603
+ const result = await engine.processCommand(publicState, privateState, {
604
+ id: 'any-bonus',
605
+ type: 'DEBUG_TRIGGER_BONUS',
606
+ payload: {
607
+ sessionId: 'test',
608
+ bonusType: 'CUSTOM_BONUS_TYPE',
609
+ betAmount: 1.0,
610
+ },
611
+ });
612
+ expect(result.success).toBe(true);
613
+ });
614
+ it('should throw error without initialized state', async () => {
615
+ // Engine throws TypeError when state is null
616
+ // This is expected behavior - state must be initialized
617
+ await expect(engine.processCommand(null, null, {
618
+ id: 'no-state',
619
+ type: 'DEBUG_TRIGGER_BONUS',
620
+ payload: {
621
+ sessionId: 'test',
622
+ bonusType: 'bonusGame1',
623
+ betAmount: 1.0,
624
+ },
625
+ })).rejects.toThrow();
626
+ });
627
+ });
628
+ // ============================================================
629
+ // SESSION MANAGEMENT
630
+ // ============================================================
631
+ describe('Session Management', () => {
632
+ let publicState;
633
+ let privateState;
634
+ beforeEach(async () => {
635
+ const initResult = await engine.processCommand(null, null, {
636
+ id: 'init',
637
+ type: 'INIT_SESSION_STATE',
638
+ payload: { betAmount: 1.0 },
639
+ });
640
+ publicState = initResult.publicState;
641
+ privateState = initResult.privateState;
642
+ });
643
+ it('should handle get symbols command', async () => {
644
+ const result = await engine.processCommand(publicState, privateState, {
645
+ id: 'symbols',
646
+ type: 'GET_SYMBOLS',
647
+ payload: {},
648
+ });
649
+ expect(result).toBeDefined();
650
+ });
651
+ });
652
+ // ============================================================
653
+ // STATE PERSISTENCE
654
+ // ============================================================
655
+ describe('State Persistence', () => {
656
+ let publicState;
657
+ let privateState;
658
+ beforeEach(async () => {
659
+ const initResult = await engine.processCommand(null, null, {
660
+ id: 'init',
661
+ type: 'INIT_SESSION_STATE',
662
+ payload: { betAmount: 1.0 },
663
+ });
664
+ publicState = initResult.publicState;
665
+ privateState = initResult.privateState;
666
+ });
667
+ it('should serialize and deserialize state correctly', async () => {
668
+ // Spin to have meaningful state
669
+ const spin = await engine.processCommand(publicState, privateState, {
670
+ id: 'spin',
671
+ type: 'SPIN',
672
+ payload: {
673
+ sessionId: 'test',
674
+ betAmount: 1.0,
675
+ },
676
+ });
677
+ // Serialize
678
+ const serializedPublic = JSON.stringify(spin.publicState);
679
+ const serializedPrivate = JSON.stringify(spin.privateState);
680
+ // Deserialize
681
+ const restoredPublic = JSON.parse(serializedPublic);
682
+ const restoredPrivate = JSON.parse(serializedPrivate);
683
+ // Verify serialization round-trip preserves data
684
+ expect(restoredPublic).toEqual(spin.publicState);
685
+ expect(restoredPrivate).toEqual(spin.privateState);
686
+ // Continue with restored state only if base game spin is expected
687
+ // (DummyRng may trigger bonus meters, changing nextSpinType)
688
+ if (restoredPrivate.nextSpinType === 'BASE_GAME_SPIN') {
689
+ const result = await engine.processCommand(restoredPublic, restoredPrivate, {
690
+ id: 'spin-2',
691
+ type: 'SPIN',
692
+ payload: {
693
+ sessionId: 'test',
694
+ betAmount: 1.0,
695
+ },
696
+ });
697
+ expect(result.success).toBe(true);
698
+ }
699
+ });
700
+ it('should preserve bonus meters across reconnection', async () => {
701
+ // Spin to accumulate meter
702
+ const spin = await engine.processCommand(publicState, privateState, {
703
+ id: 'spin',
704
+ type: 'SPIN',
705
+ payload: { sessionId: 'test', betAmount: 1.0 },
706
+ });
707
+ // Serialize and restore
708
+ const serialized = JSON.stringify(spin.publicState);
709
+ const restored = JSON.parse(serialized);
710
+ expect(restored.bonusMeters).toBeDefined();
711
+ });
712
+ it('should maintain state across multiple operations', async () => {
713
+ let currentPublic = publicState;
714
+ let currentPrivate = privateState;
715
+ // Multiple spins
716
+ for (let i = 0; i < 5; i++) {
717
+ const result = await engine.processCommand(currentPublic, currentPrivate, {
718
+ id: `spin-${i}`,
719
+ type: 'SPIN',
720
+ payload: {
721
+ sessionId: 'test',
722
+ betAmount: 1.0,
723
+ },
724
+ });
725
+ expect(result.success).toBe(true);
726
+ currentPublic = result.publicState;
727
+ currentPrivate = result.privateState;
728
+ }
729
+ // Trigger various bonuses
730
+ for (const bonusType of ['bonusGame1', 'bonusGame2']) {
731
+ const bonus = await engine.processCommand(currentPublic, currentPrivate, {
732
+ id: `bonus-${bonusType}`,
733
+ type: 'DEBUG_TRIGGER_BONUS',
734
+ payload: {
735
+ sessionId: 'test',
736
+ bonusType,
737
+ betAmount: 1.0,
738
+ },
739
+ });
740
+ expect(bonus.success).toBe(true);
741
+ currentPublic = bonus.publicState;
742
+ currentPrivate = bonus.privateState;
743
+ }
744
+ });
745
+ });
746
+ });
747
+ //# sourceMappingURL=comprehensive.test.js.map