@omnitronix/bonnys-fortune-game-engine 1.3.1 → 1.3.2
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/README.md +15 -15
- package/dist/__tests__/bonnys-fortune-v1.game-engine.test.d.ts +1 -0
- package/dist/bonnys-fortune-v1.game-engine.d.ts +36 -0
- package/dist/bonnys-fortune-v1.game-engine.js +23 -12
- package/dist/bonnys-fortune-v1.game-engine.js.map +1 -1
- package/dist/config/game-logic-config/file-system.game-logic-config-loader.d.ts +4 -0
- package/dist/config/game-logic-config/game-logic-config-loader.d.ts +3 -0
- package/dist/config/game-logic-config/game-logic-config.d.ts +2 -0
- package/dist/config/game-logic-config/game-logic-config.js +2 -0
- package/dist/config/game-logic-config/game-logic-config.js.map +1 -1
- package/dist/config/reel-strips-config/file-system.reel-strips-config-loader.d.ts +4 -0
- package/dist/config/reel-strips-config/reel-strip-option.dto.d.ts +4 -0
- package/dist/config/reel-strips-config/reel-strips-config-loader.d.ts +3 -0
- package/dist/config/reel-strips-config/reel-strips-config.dto.d.ts +5 -0
- package/dist/config/reel-strips-config/reel.dto.d.ts +5 -0
- package/dist/domain/game-round.types.d.ts +9 -0
- package/dist/domain/mappers/reel-strips-config.mapper.d.ts +4 -0
- package/dist/domain/reel-strip-option.d.ts +7 -0
- package/dist/domain/reel-strips-config.d.ts +9 -0
- package/dist/domain/reel.d.ts +8 -0
- package/dist/domain/types/cheat-trigger-bonus-command.d.ts +5 -0
- package/dist/domain/types/{debug-trigger-bonus-command.js → cheat-trigger-bonus-command.js} +1 -1
- package/dist/domain/types/cheat-trigger-bonus-command.js.map +1 -0
- package/dist/domain/types/cheat-trigger-bonus-request.dto.d.ts +5 -0
- package/dist/domain/types/{debug-trigger-bonus-request.dto.js → cheat-trigger-bonus-request.dto.js} +1 -1
- package/dist/domain/types/cheat-trigger-bonus-request.dto.js.map +1 -0
- package/dist/domain/types/cheat-update-bonus-meter-progress-command.d.ts +5 -0
- package/dist/domain/types/{debug-update-bonus-meter-progress-command.js → cheat-update-bonus-meter-progress-command.js} +1 -1
- package/dist/domain/types/cheat-update-bonus-meter-progress-command.js.map +1 -0
- package/dist/domain/types/cheat-update-bonus-meter-progress-request.dto.d.ts +5 -0
- package/dist/domain/types/{debug-update-bonus-meter-progress-request.dto.js → cheat-update-bonus-meter-progress-request.dto.js} +1 -1
- package/dist/domain/types/cheat-update-bonus-meter-progress-request.dto.js.map +1 -0
- package/dist/domain/types/game-spin-command.d.ts +8 -0
- package/dist/domain/types/game-spin-input.dto.d.ts +8 -0
- package/dist/domain/types/game-spin-output.dto.d.ts +119 -0
- package/dist/domain/types/game-symbols.response.dto.d.ts +10 -0
- package/dist/domain/types/reel-strip-option.dto.d.ts +4 -0
- package/dist/domain/types/reel-strips-config.dto.d.ts +5 -0
- package/dist/domain/types/reel.dto.d.ts +5 -0
- package/dist/domain/types/start-bonus-round-command.d.ts +8 -0
- package/dist/domain/types/start-bonus-round-input.dto.d.ts +10 -0
- package/dist/game-engine.interface.d.ts +41 -0
- package/dist/helpers/generate-hash.d.ts +1 -0
- package/dist/helpers/number-helper.d.ts +23 -0
- package/dist/helpers/optional-boolean-mapper.d.ts +1 -0
- package/dist/helpers/uuid-helper.d.ts +3 -0
- package/dist/helpers/validation-helper.d.ts +14 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js.map +1 -1
- package/dist/logger/logger.d.ts +22 -0
- package/dist/logger/logger.js +4 -5
- package/dist/logger/logger.js.map +1 -1
- package/dist/logic/bonnys-fortune.game-logic.d.ts +32 -0
- package/dist/logic/bonnys-fortune.game-logic.js +15 -11
- package/dist/logic/bonnys-fortune.game-logic.js.map +1 -1
- package/dist/logic/bonnys-fortune.spin-generator.d.ts +19 -0
- package/dist/logic/bonnys-fortune.types.d.ts +170 -0
- package/dist/logic/bonnys-fortune.win-calculator.d.ts +10 -0
- package/dist/logic/game-logic-config.interface.d.ts +180 -0
- package/dist/logic/handlers/base-game.handler.d.ts +15 -0
- package/dist/logic/handlers/base-game.handler.js +5 -1
- package/dist/logic/handlers/base-game.handler.js.map +1 -1
- package/dist/logic/handlers/collect-feature-bonus.handler.d.ts +13 -0
- package/dist/logic/handlers/free-spins-bonus.handler.d.ts +11 -0
- package/dist/logic/handlers/steering-to-the-fortune-bonus.handler.d.ts +11 -0
- package/dist/logic/handlers/treasure-hunt-bonus.handler.d.ts +15 -0
- package/dist/rng/DummyRngClient.d.ts +18 -0
- package/dist/rng/DummyRngClient.js +1 -8
- package/dist/rng/DummyRngClient.js.map +1 -1
- package/dist/rng/rng-client.factory.d.ts +7 -0
- package/dist/rng/rng-client.factory.js.map +1 -1
- package/dist/rng/rng-client.interface.d.ts +16 -0
- package/dist/rng/rng-service.d.ts +26 -0
- package/dist/validation/abstract-game-logic-config.validator.d.ts +4 -0
- package/dist/validation/bonnys-fortune/bonnys-fortune-config.dto.d.ts +131 -0
- package/dist/validation/bonnys-fortune/bonnys-fortune-config.dto.js +9 -0
- package/dist/validation/bonnys-fortune/bonnys-fortune-config.dto.js.map +1 -1
- package/dist/validation/bonnys-fortune/bonnys-fortune-config.validator.d.ts +5 -0
- package/dist/validation/custom-decorators/IsNestedIntArray.d.ts +2 -0
- package/dist/validation/game-logic-config-validation.service.d.ts +8 -0
- package/dist/validation/reel-strips-config-validation.service.d.ts +5 -0
- package/package.json +4 -3
- package/src/__tests__/bonnys-fortune-v1.game-engine.test.ts +80 -0
- package/src/bonnys-fortune-v1.game-engine.ts +314 -0
- package/src/config/game-logic-config/file-system.game-logic-config-loader.ts +27 -0
- package/src/config/game-logic-config/game-logic-config-loader.ts +3 -0
- package/src/config/game-logic-config/game-logic-config.ts +382 -0
- package/src/config/reel-strips-config/file-system.reel-strips-config-loader.ts +43 -0
- package/src/config/reel-strips-config/reel-strip-option.dto.ts +13 -0
- package/src/config/reel-strips-config/reel-strips-config-loader.ts +9 -0
- package/src/config/reel-strips-config/reel-strips-config.dto.ts +16 -0
- package/src/config/reel-strips-config/reel.dto.ts +16 -0
- package/src/config/reel-strips-config/reels-BASE.csv +52 -0
- package/src/config/reel-strips-config/reels-BONUS.csv +52 -0
- package/src/domain/game-round.types.ts +10 -0
- package/src/domain/mappers/reel-strips-config.mapper.ts +16 -0
- package/src/domain/reel-strip-option.ts +15 -0
- package/src/domain/reel-strips-config.ts +21 -0
- package/src/domain/reel.ts +17 -0
- package/src/domain/types/cheat-trigger-bonus-command.ts +5 -0
- package/src/domain/types/cheat-trigger-bonus-request.dto.ts +5 -0
- package/src/domain/types/cheat-update-bonus-meter-progress-command.ts +6 -0
- package/src/domain/types/cheat-update-bonus-meter-progress-request.dto.ts +5 -0
- package/src/domain/types/game-spin-command.ts +8 -0
- package/src/domain/types/game-spin-input.dto.ts +8 -0
- package/src/domain/types/game-spin-output.dto.ts +142 -0
- package/src/domain/types/game-symbols.response.dto.ts +11 -0
- package/src/domain/types/reel-strip-option.dto.ts +13 -0
- package/src/domain/types/reel-strips-config.dto.ts +15 -0
- package/src/domain/types/reel.dto.ts +15 -0
- package/src/domain/types/start-bonus-round-command.ts +8 -0
- package/src/domain/types/start-bonus-round-input.dto.ts +10 -0
- package/src/game-engine.interface.ts +59 -0
- package/src/helpers/generate-hash.ts +5 -0
- package/src/helpers/number-helper.ts +41 -0
- package/src/helpers/optional-boolean-mapper.ts +5 -0
- package/src/helpers/uuid-helper.ts +7 -0
- package/src/helpers/validation-helper.ts +27 -0
- package/src/index.ts +3 -0
- package/src/logger/logger.ts +178 -0
- package/src/logic/bonnys-fortune.game-logic.ts +490 -0
- package/src/logic/bonnys-fortune.spin-generator.ts +277 -0
- package/src/logic/bonnys-fortune.types.ts +223 -0
- package/src/logic/bonnys-fortune.win-calculator.ts +210 -0
- package/src/logic/game-logic-config.interface.ts +176 -0
- package/src/logic/handlers/base-game.handler.ts +221 -0
- package/src/logic/handlers/collect-feature-bonus.handler.ts +301 -0
- package/src/logic/handlers/free-spins-bonus.handler.ts +119 -0
- package/src/logic/handlers/steering-to-the-fortune-bonus.handler.ts +118 -0
- package/src/logic/handlers/treasure-hunt-bonus.handler.ts +232 -0
- package/src/rng/DummyRngClient.ts +108 -0
- package/src/rng/rng-client.factory.ts +27 -0
- package/src/rng/rng-client.interface.ts +38 -0
- package/src/rng/rng-service.ts +130 -0
- package/src/validation/abstract-game-logic-config.validator.ts +20 -0
- package/src/validation/bonnys-fortune/bonnys-fortune-config.dto.ts +379 -0
- package/src/validation/bonnys-fortune/bonnys-fortune-config.validator.ts +8 -0
- package/src/validation/custom-decorators/IsNestedIntArray.ts +29 -0
- package/src/validation/game-logic-config-validation.service.ts +28 -0
- package/src/validation/reel-strips-config-validation.service.ts +29 -0
- package/dist/__tests__/comprehensive.test.js +0 -741
- package/dist/__tests__/comprehensive.test.js.map +0 -1
- package/dist/domain/types/debug-trigger-bonus-command.js.map +0 -1
- package/dist/domain/types/debug-trigger-bonus-request.dto.js.map +0 -1
- package/dist/domain/types/debug-update-bonus-meter-progress-command.js.map +0 -1
- package/dist/domain/types/debug-update-bonus-meter-progress-request.dto.js.map +0 -1
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import { BonnysFortuneGameLogicConfigInterface } from './game-logic-config.interface';
|
|
2
|
+
import { Reel } from '../domain/reel';
|
|
3
|
+
import { ReelStripsConfig } from '../domain/reel-strips-config';
|
|
4
|
+
import { RngService } from '../rng/rng-service';
|
|
5
|
+
|
|
6
|
+
export class BonnysFortuneSpinGenerator {
|
|
7
|
+
constructor(private readonly rngService: RngService) {}
|
|
8
|
+
|
|
9
|
+
public async generateScreenWithLayout(
|
|
10
|
+
reelStripsConfig: ReelStripsConfig,
|
|
11
|
+
layout: { reels: number; rows: number },
|
|
12
|
+
optionWeights: number[],
|
|
13
|
+
commandId: string,
|
|
14
|
+
actionId: string,
|
|
15
|
+
): Promise<number[][]> {
|
|
16
|
+
const { reels, rows } = layout;
|
|
17
|
+
|
|
18
|
+
const selectedOptionIndex = await this.selectReelOption(
|
|
19
|
+
optionWeights,
|
|
20
|
+
commandId,
|
|
21
|
+
`${actionId}_reel_option_selection`,
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
return Promise.all(
|
|
25
|
+
Array.from({ length: reels }, async (_, reelIndex) =>
|
|
26
|
+
this.drawReelColumn(
|
|
27
|
+
reelStripsConfig.getReel(reelIndex),
|
|
28
|
+
rows,
|
|
29
|
+
selectedOptionIndex,
|
|
30
|
+
commandId,
|
|
31
|
+
actionId,
|
|
32
|
+
reelIndex,
|
|
33
|
+
),
|
|
34
|
+
),
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
public async generateBaseScreen(
|
|
39
|
+
config: BonnysFortuneGameLogicConfigInterface,
|
|
40
|
+
reelStripsConfig: ReelStripsConfig,
|
|
41
|
+
commandId: string,
|
|
42
|
+
actionId: string,
|
|
43
|
+
): Promise<number[][]> {
|
|
44
|
+
const { reels, rows } = config.baseGame.baseGameConfig.layout;
|
|
45
|
+
const optionWeights =
|
|
46
|
+
config.baseGame.baseGameConfig.reelsOptions.optionWeights;
|
|
47
|
+
|
|
48
|
+
const selectedOptionIndex = await this.selectReelOption(
|
|
49
|
+
optionWeights,
|
|
50
|
+
commandId,
|
|
51
|
+
`${actionId}_reel_option_selection`,
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
return Promise.all(
|
|
55
|
+
Array.from({ length: reels }, async (_, reelIndex) =>
|
|
56
|
+
this.drawReelColumn(
|
|
57
|
+
reelStripsConfig.getReel(reelIndex),
|
|
58
|
+
rows,
|
|
59
|
+
selectedOptionIndex,
|
|
60
|
+
commandId,
|
|
61
|
+
actionId,
|
|
62
|
+
reelIndex,
|
|
63
|
+
),
|
|
64
|
+
),
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
private async selectReelOption(
|
|
69
|
+
optionWeights: number[],
|
|
70
|
+
commandId: string,
|
|
71
|
+
actionId: string,
|
|
72
|
+
): Promise<number> {
|
|
73
|
+
const total = optionWeights.reduce((acc, w) => acc + w, 0);
|
|
74
|
+
const result = await this.rngService.getSingleNumber(
|
|
75
|
+
0,
|
|
76
|
+
total - 1,
|
|
77
|
+
commandId,
|
|
78
|
+
actionId,
|
|
79
|
+
);
|
|
80
|
+
const rand = result.value;
|
|
81
|
+
let cumulative = 0;
|
|
82
|
+
for (let i = 0; i < optionWeights.length; i++) {
|
|
83
|
+
cumulative += optionWeights[i];
|
|
84
|
+
if (rand < cumulative) {
|
|
85
|
+
return i;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return optionWeights.length - 1;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private async drawReelColumn(
|
|
92
|
+
reel: Reel,
|
|
93
|
+
rowsCount: number,
|
|
94
|
+
optionIndex: number,
|
|
95
|
+
commandId: string,
|
|
96
|
+
actionId: string,
|
|
97
|
+
reelIndex: number,
|
|
98
|
+
): Promise<number[]> {
|
|
99
|
+
const stopActionId = `${actionId}_reel_${reelIndex}_stop`;
|
|
100
|
+
const option = reel.getOption(optionIndex);
|
|
101
|
+
|
|
102
|
+
const resultStop = await this.rngService.getSingleNumber(
|
|
103
|
+
0,
|
|
104
|
+
option.length - 1,
|
|
105
|
+
commandId,
|
|
106
|
+
stopActionId,
|
|
107
|
+
);
|
|
108
|
+
const stopPos = resultStop.value;
|
|
109
|
+
|
|
110
|
+
return Array.from({ length: rowsCount }, (_, row) =>
|
|
111
|
+
option.getSymbolAt(stopPos + row),
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
public async applyBonusSymbols(
|
|
116
|
+
screen: number[][],
|
|
117
|
+
config: BonnysFortuneGameLogicConfigInterface,
|
|
118
|
+
commandId: string,
|
|
119
|
+
actionId: string,
|
|
120
|
+
): Promise<number[][]> {
|
|
121
|
+
const screenCopy = screen.map((col) => [...col]);
|
|
122
|
+
const reelsCount = config.baseGame.baseGameConfig.layout.reels;
|
|
123
|
+
|
|
124
|
+
const hWeights = [
|
|
125
|
+
config.baseGame.paidHSymbolWeights.None,
|
|
126
|
+
config.baseGame.paidHSymbolWeights.H1,
|
|
127
|
+
config.baseGame.paidHSymbolWeights.H2,
|
|
128
|
+
config.baseGame.paidHSymbolWeights.H3,
|
|
129
|
+
];
|
|
130
|
+
|
|
131
|
+
const pickedHIndex = await this.getRandomWeightedIndex(
|
|
132
|
+
hWeights,
|
|
133
|
+
commandId,
|
|
134
|
+
`${actionId}_h_symbol_selection`,
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
if (pickedHIndex > 0) {
|
|
138
|
+
const hName = `H${pickedHIndex}` as 'H1' | 'H2' | 'H3';
|
|
139
|
+
const hId =
|
|
140
|
+
config.baseGame.symbolMultipliers.find((s) => s.name === hName)
|
|
141
|
+
?.symbolId ?? 0;
|
|
142
|
+
await this.placeHSymbols(
|
|
143
|
+
screenCopy,
|
|
144
|
+
reelsCount,
|
|
145
|
+
config.baseGame.paidHSymbolReelWeights,
|
|
146
|
+
hName,
|
|
147
|
+
hId,
|
|
148
|
+
commandId,
|
|
149
|
+
`${actionId}_h_symbols`,
|
|
150
|
+
);
|
|
151
|
+
} else {
|
|
152
|
+
await this.placeScatterSymbols(
|
|
153
|
+
screenCopy,
|
|
154
|
+
reelsCount,
|
|
155
|
+
config.baseGame.paidScatterReelWeights,
|
|
156
|
+
config.baseGame.symbolDefinitions.specialSymbols.SCATTER,
|
|
157
|
+
commandId,
|
|
158
|
+
`${actionId}_scatter_symbols`,
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return screenCopy;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
private async placeHSymbols(
|
|
166
|
+
screen: number[][],
|
|
167
|
+
reelsCount: number,
|
|
168
|
+
reelWeights: Record<string, number[]>,
|
|
169
|
+
symbolName: 'H1' | 'H2' | 'H3',
|
|
170
|
+
symbolId: number,
|
|
171
|
+
commandId: string,
|
|
172
|
+
actionId: string,
|
|
173
|
+
): Promise<void> {
|
|
174
|
+
const result = await this.rngService.getSingleNumber(
|
|
175
|
+
0,
|
|
176
|
+
reelsCount - 1,
|
|
177
|
+
commandId,
|
|
178
|
+
`${actionId}_guaranteed_reel`,
|
|
179
|
+
);
|
|
180
|
+
const guaranteedReel = result.value;
|
|
181
|
+
await this.placeSymbolRandomlyOnReel(
|
|
182
|
+
screen,
|
|
183
|
+
guaranteedReel,
|
|
184
|
+
symbolId,
|
|
185
|
+
commandId,
|
|
186
|
+
`${actionId}_guaranteed_placement`,
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
for (let reel = 0; reel < reelsCount; reel++) {
|
|
190
|
+
if (reel === guaranteedReel) continue;
|
|
191
|
+
const weights = reelWeights[symbolName];
|
|
192
|
+
if (
|
|
193
|
+
weights &&
|
|
194
|
+
(await this.getRandomWeightedIndex(
|
|
195
|
+
weights,
|
|
196
|
+
commandId,
|
|
197
|
+
`${actionId}_reel_${reel}_weighted`,
|
|
198
|
+
)) === 0
|
|
199
|
+
) {
|
|
200
|
+
await this.placeSymbolRandomlyOnReel(
|
|
201
|
+
screen,
|
|
202
|
+
reel,
|
|
203
|
+
symbolId,
|
|
204
|
+
commandId,
|
|
205
|
+
`${actionId}_reel_${reel}_placement`,
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
private async placeScatterSymbols(
|
|
212
|
+
screen: number[][],
|
|
213
|
+
reelsCount: number,
|
|
214
|
+
scatterWeights: Record<string, number[]>,
|
|
215
|
+
scatterId: number,
|
|
216
|
+
commandId: string,
|
|
217
|
+
actionId: string,
|
|
218
|
+
): Promise<void> {
|
|
219
|
+
for (let reel = 0; reel < reelsCount; reel++) {
|
|
220
|
+
const weights =
|
|
221
|
+
scatterWeights[`reel${reel + 1}` as keyof typeof scatterWeights];
|
|
222
|
+
if (
|
|
223
|
+
weights &&
|
|
224
|
+
(await this.getRandomWeightedIndex(
|
|
225
|
+
weights,
|
|
226
|
+
commandId,
|
|
227
|
+
`${actionId}_scatter_reel_${reel}`,
|
|
228
|
+
)) === 0
|
|
229
|
+
) {
|
|
230
|
+
await this.placeSymbolRandomlyOnReel(
|
|
231
|
+
screen,
|
|
232
|
+
reel,
|
|
233
|
+
scatterId,
|
|
234
|
+
commandId,
|
|
235
|
+
`${actionId}_scatter_placement_${reel}`,
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
private async placeSymbolRandomlyOnReel(
|
|
242
|
+
screen: number[][],
|
|
243
|
+
reel: number,
|
|
244
|
+
symbolId: number,
|
|
245
|
+
commandId: string,
|
|
246
|
+
actionId: string,
|
|
247
|
+
): Promise<void> {
|
|
248
|
+
const result = await this.rngService.getSingleNumber(
|
|
249
|
+
0,
|
|
250
|
+
screen[reel].length - 1,
|
|
251
|
+
commandId,
|
|
252
|
+
actionId,
|
|
253
|
+
);
|
|
254
|
+
screen[reel][result.value] = symbolId;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
private async getRandomWeightedIndex(
|
|
258
|
+
weights: number[],
|
|
259
|
+
commandId: string,
|
|
260
|
+
actionId: string,
|
|
261
|
+
): Promise<number> {
|
|
262
|
+
const total = weights.reduce((acc, w) => acc + w, 0);
|
|
263
|
+
const result = await this.rngService.getSingleNumber(
|
|
264
|
+
0,
|
|
265
|
+
total - 1,
|
|
266
|
+
commandId,
|
|
267
|
+
actionId,
|
|
268
|
+
);
|
|
269
|
+
const rand = result.value;
|
|
270
|
+
let cumulative = 0;
|
|
271
|
+
for (let i = 0; i < weights.length; i++) {
|
|
272
|
+
cumulative += weights[i];
|
|
273
|
+
if (rand < cumulative) return i;
|
|
274
|
+
}
|
|
275
|
+
return weights.length - 1;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
export interface WinLine {
|
|
2
|
+
symbol: number;
|
|
3
|
+
consecutiveReels: number;
|
|
4
|
+
ways: number;
|
|
5
|
+
multiplier: number;
|
|
6
|
+
win: number;
|
|
7
|
+
positions: [number, number][];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface MeterState {
|
|
11
|
+
symbolName: string;
|
|
12
|
+
symbolId: number;
|
|
13
|
+
threshold: number;
|
|
14
|
+
progress: number;
|
|
15
|
+
level: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type MetersState = Record<string, MeterState>;
|
|
19
|
+
|
|
20
|
+
export interface BonnysFortunePublicState {
|
|
21
|
+
currentBetAmount: number;
|
|
22
|
+
bonusMeters: MetersState;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface BonnysFortunePrivateState {
|
|
26
|
+
screen?: number[][];
|
|
27
|
+
nextSpinType: SpinType;
|
|
28
|
+
pendingBonuses: BonusTrigger[];
|
|
29
|
+
activeBonus?: BonusTrigger;
|
|
30
|
+
bonusMetersByAmount: Record<number, MetersState>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface BonusTrigger {
|
|
34
|
+
bonusId: string;
|
|
35
|
+
bonusType: string;
|
|
36
|
+
betAmount: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface SpinResult {
|
|
40
|
+
spinType: string;
|
|
41
|
+
playerWinning: number;
|
|
42
|
+
nextSpinType: SpinType | string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface SpinResultResponse<T extends SpinResult> {
|
|
46
|
+
result: T;
|
|
47
|
+
publicState: BonnysFortunePublicState;
|
|
48
|
+
privateState: BonnysFortunePrivateState;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface BonnysFortuneBaseGameSpinResult extends SpinResult {
|
|
52
|
+
spinType: SpinType.BASE_GAME_SPIN;
|
|
53
|
+
screen: number[][];
|
|
54
|
+
winResult: {
|
|
55
|
+
totalWin: number;
|
|
56
|
+
winningLines: WinLine[];
|
|
57
|
+
};
|
|
58
|
+
triggeredBonus?: BonusTrigger;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface TreasureHuntSpinResult extends SpinResult {
|
|
62
|
+
spinType: SpinType;
|
|
63
|
+
selection: string;
|
|
64
|
+
multiplier: number;
|
|
65
|
+
multiplierCounts: Record<string, number>;
|
|
66
|
+
winningMultiplier: number | null;
|
|
67
|
+
winningCount: number;
|
|
68
|
+
totalMultiplier: number;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface FreeSpinResult {
|
|
72
|
+
spinType: SpinType.BONUS_GAME_2_SPIN;
|
|
73
|
+
screen: number[][];
|
|
74
|
+
winResult: {
|
|
75
|
+
totalWin: number;
|
|
76
|
+
winningLines: WinLine[];
|
|
77
|
+
};
|
|
78
|
+
playerWinning: number;
|
|
79
|
+
nextSpinType: SpinType;
|
|
80
|
+
multiplier: number;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface SteeringToTheFortuneResult {
|
|
84
|
+
spinType: SpinType;
|
|
85
|
+
playerWinning: number;
|
|
86
|
+
nextSpinType: SpinType;
|
|
87
|
+
prizeType: string;
|
|
88
|
+
prizeValue: number | string;
|
|
89
|
+
totalMultiplier: number;
|
|
90
|
+
triggeredBonus?: BonusTrigger;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface CollectFeatureSpinResult {
|
|
94
|
+
spinIndex: number;
|
|
95
|
+
spinType: SpinType;
|
|
96
|
+
symbols: string[];
|
|
97
|
+
slashed: boolean[];
|
|
98
|
+
currentValues: number;
|
|
99
|
+
currentMultipliers: number;
|
|
100
|
+
totalValues: number;
|
|
101
|
+
totalMultipliers: number;
|
|
102
|
+
playerWinning: number;
|
|
103
|
+
totalWin: number;
|
|
104
|
+
spinsLeft: number;
|
|
105
|
+
nextSpinType: SpinType;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface BonusRoundResult<TSpin extends SpinResult> {
|
|
109
|
+
results: SpinResultResponse<TSpin>[];
|
|
110
|
+
publicState: BonnysFortunePublicState;
|
|
111
|
+
privateState: BonnysFortunePrivateState;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export interface BonusRoundResultWithActiveBonus<TSpin extends SpinResult>
|
|
115
|
+
extends BonusRoundResult<TSpin> {
|
|
116
|
+
activeBonus: BonusTrigger;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export type BonnysFortuneGameLogicSpinResult =
|
|
120
|
+
| SpinResultResponse<BonnysFortuneBaseGameSpinResult>
|
|
121
|
+
| SpinResultResponse<FreeSpinResult>
|
|
122
|
+
| SpinResultResponse<TreasureHuntSpinResult>
|
|
123
|
+
| SpinResultResponse<SteeringToTheFortuneResult>
|
|
124
|
+
| SpinResultResponse<CollectFeatureSpinResult>;
|
|
125
|
+
|
|
126
|
+
export type BonnysFortuneBonusGameLogicSpinResult =
|
|
127
|
+
| BonusRoundResult<FreeSpinResult>
|
|
128
|
+
| BonusRoundResult<TreasureHuntSpinResult>
|
|
129
|
+
| BonusRoundResult<SteeringToTheFortuneResult>
|
|
130
|
+
| BonusRoundResult<CollectFeatureSpinResult>;
|
|
131
|
+
|
|
132
|
+
export type StartBonusRoundResult =
|
|
133
|
+
| BonusRoundResultWithActiveBonus<FreeSpinResult>
|
|
134
|
+
| BonusRoundResultWithActiveBonus<TreasureHuntSpinResult>
|
|
135
|
+
| BonusRoundResultWithActiveBonus<SteeringToTheFortuneResult>
|
|
136
|
+
| BonusRoundResultWithActiveBonus<CollectFeatureSpinResult>;
|
|
137
|
+
|
|
138
|
+
export enum SpinType {
|
|
139
|
+
BASE_GAME_SPIN = 'BASE_GAME_SPIN',
|
|
140
|
+
BONUS_GAME_1_SPIN = 'BONUS_GAME_1_SPIN',
|
|
141
|
+
BONUS_GAME_2_SPIN = 'BONUS_GAME_2_SPIN',
|
|
142
|
+
BONUS_GAME_3_SPIN = 'BONUS_GAME_3_SPIN',
|
|
143
|
+
COLLECT_FEATURE_SPIN = 'COLLECT_FEATURE_SPIN',
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export enum PrizeType {
|
|
147
|
+
MULTIPLIER = 'MULTIPLIER',
|
|
148
|
+
BONUS = 'BONUS',
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export interface ExtractedConfigData {
|
|
152
|
+
baseMin: number;
|
|
153
|
+
baseMax: number;
|
|
154
|
+
WILD: number;
|
|
155
|
+
reels: number;
|
|
156
|
+
rows: number;
|
|
157
|
+
minLength: number;
|
|
158
|
+
maxLength: number;
|
|
159
|
+
multipliers: Map<number, Record<string, number>>;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export type SymbolsCountMatrix = number[][];
|
|
163
|
+
|
|
164
|
+
export type SymbolPositionsMatrix = boolean[][][];
|
|
165
|
+
|
|
166
|
+
export interface SymbolAnalysisResult {
|
|
167
|
+
symbolsCount: SymbolsCountMatrix;
|
|
168
|
+
symbolPositions: SymbolPositionsMatrix;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export interface FindWinningLinesParams {
|
|
172
|
+
symbolsCount: SymbolsCountMatrix;
|
|
173
|
+
symbolPositions: SymbolPositionsMatrix;
|
|
174
|
+
configData: ExtractedConfigData;
|
|
175
|
+
betStake: number;
|
|
176
|
+
currentMultiplier: number;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export interface CalculateWinsResult {
|
|
180
|
+
totalWin: number;
|
|
181
|
+
winningLines: WinLine[];
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export interface CheatTriggerBonusOutcome {
|
|
185
|
+
triggeredBonus: BonusTrigger;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export interface CheatUpdateBonusMeterProgressOutcome {
|
|
189
|
+
updatedMeter: MeterState;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export interface CheatTriggerBonusResult {
|
|
193
|
+
publicState: BonnysFortunePublicState;
|
|
194
|
+
privateState: BonnysFortunePrivateState;
|
|
195
|
+
outcome: CheatTriggerBonusOutcome;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export interface CheatUpdateBonusMeterProgressResult {
|
|
199
|
+
publicState: BonnysFortunePublicState;
|
|
200
|
+
privateState: BonnysFortunePrivateState;
|
|
201
|
+
outcome: CheatUpdateBonusMeterProgressOutcome;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export interface GetSymbolsOutcome {
|
|
205
|
+
symbolId: number;
|
|
206
|
+
name: string;
|
|
207
|
+
multipliers: Record<'x3' | 'x4' | 'x5', number>;
|
|
208
|
+
isWild: boolean | undefined;
|
|
209
|
+
isScatter: boolean | undefined;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export interface GetSymbolsResult {
|
|
213
|
+
symbols: GetSymbolsOutcome[];
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export type BonnysFortuneOutcome =
|
|
217
|
+
| BonnysFortuneGameLogicSpinResult
|
|
218
|
+
| BonnysFortuneBonusGameLogicSpinResult
|
|
219
|
+
| SymbolsCountMatrix
|
|
220
|
+
| CheatTriggerBonusOutcome
|
|
221
|
+
| CheatUpdateBonusMeterProgressOutcome
|
|
222
|
+
| GetSymbolsOutcome
|
|
223
|
+
| null;
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CalculateWinsResult,
|
|
3
|
+
ExtractedConfigData,
|
|
4
|
+
FindWinningLinesParams,
|
|
5
|
+
SpinType,
|
|
6
|
+
SymbolAnalysisResult,
|
|
7
|
+
SymbolPositionsMatrix,
|
|
8
|
+
SymbolsCountMatrix,
|
|
9
|
+
WinLine,
|
|
10
|
+
} from './bonnys-fortune.types';
|
|
11
|
+
|
|
12
|
+
import { BonnysFortuneGameLogicConfigInterface } from './game-logic-config.interface';
|
|
13
|
+
import { NumberHelper } from '../helpers/number-helper';
|
|
14
|
+
|
|
15
|
+
export class BonnysFortuneWinCalculator {
|
|
16
|
+
public static calculateWins(
|
|
17
|
+
screen: number[][],
|
|
18
|
+
config: BonnysFortuneGameLogicConfigInterface,
|
|
19
|
+
betStake: number,
|
|
20
|
+
currentMultiplier: number,
|
|
21
|
+
spinType: SpinType,
|
|
22
|
+
spinTypeIndex?: number, // Index of spin type in bonus game 2 config
|
|
23
|
+
): CalculateWinsResult {
|
|
24
|
+
if (!this.isEligibleSpin(spinType))
|
|
25
|
+
return { totalWin: 0, winningLines: [] };
|
|
26
|
+
|
|
27
|
+
const configData = this.buildConfigData(config, spinType, spinTypeIndex);
|
|
28
|
+
const { symbolsCount, symbolPositions } = this.analyzeScreenSymbols(
|
|
29
|
+
screen,
|
|
30
|
+
configData,
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
return this.findWinningLines({
|
|
34
|
+
symbolsCount,
|
|
35
|
+
symbolPositions,
|
|
36
|
+
configData,
|
|
37
|
+
betStake,
|
|
38
|
+
currentMultiplier,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
private static isEligibleSpin(spinType: SpinType): boolean {
|
|
43
|
+
return (
|
|
44
|
+
spinType === SpinType.BASE_GAME_SPIN ||
|
|
45
|
+
spinType === SpinType.BONUS_GAME_2_SPIN
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
private static buildConfigData(
|
|
50
|
+
config: BonnysFortuneGameLogicConfigInterface,
|
|
51
|
+
spinType: SpinType,
|
|
52
|
+
spinTypeIndex?: number,
|
|
53
|
+
): ExtractedConfigData {
|
|
54
|
+
const { minIndex: baseMin, maxIndex: baseMax } =
|
|
55
|
+
config.baseGame.symbolDefinitions.baseSymbols;
|
|
56
|
+
const WILD = config.baseGame.symbolDefinitions.specialSymbols.WILD;
|
|
57
|
+
|
|
58
|
+
let rows: number, reels: number, minLength: number, maxLength: number;
|
|
59
|
+
|
|
60
|
+
if (
|
|
61
|
+
spinType === SpinType.BONUS_GAME_2_SPIN &&
|
|
62
|
+
spinTypeIndex !== undefined
|
|
63
|
+
) {
|
|
64
|
+
const spinConfig =
|
|
65
|
+
config.bonusGames.bonusGame2.config.spinTypes[spinTypeIndex];
|
|
66
|
+
rows = spinConfig.layout.rows;
|
|
67
|
+
reels = spinConfig.reelsOptionsWeights.length;
|
|
68
|
+
minLength = config.baseGame.baseGameConfig.winConfig.minLength;
|
|
69
|
+
maxLength = config.baseGame.baseGameConfig.winConfig.maxLength;
|
|
70
|
+
} else {
|
|
71
|
+
rows = config.baseGame.baseGameConfig.layout.rows;
|
|
72
|
+
reels = config.baseGame.baseGameConfig.layout.reels;
|
|
73
|
+
minLength = config.baseGame.baseGameConfig.winConfig.minLength;
|
|
74
|
+
maxLength = config.baseGame.baseGameConfig.winConfig.maxLength;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const multipliers = new Map<number, Record<string, number>>();
|
|
78
|
+
for (const symbolConfig of config.baseGame.symbolMultipliers) {
|
|
79
|
+
multipliers.set(symbolConfig.symbolId, symbolConfig.multipliers || {});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
baseMin,
|
|
84
|
+
baseMax,
|
|
85
|
+
WILD,
|
|
86
|
+
reels,
|
|
87
|
+
rows,
|
|
88
|
+
minLength,
|
|
89
|
+
maxLength,
|
|
90
|
+
multipliers,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private static analyzeScreenSymbols(
|
|
95
|
+
screen: number[][],
|
|
96
|
+
configData: ExtractedConfigData,
|
|
97
|
+
): SymbolAnalysisResult {
|
|
98
|
+
const { baseMin, baseMax, WILD, reels, rows } = configData;
|
|
99
|
+
|
|
100
|
+
const symbolsCount: SymbolsCountMatrix = Array.from(
|
|
101
|
+
{ length: baseMax - baseMin + 1 },
|
|
102
|
+
() => Array(reels).fill(0),
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
const symbolPositions: SymbolPositionsMatrix = Array.from(
|
|
106
|
+
{ length: baseMax - baseMin + 1 },
|
|
107
|
+
() => Array.from({ length: reels }, () => Array(rows).fill(false)),
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
for (let reel = 0; reel < reels; reel++) {
|
|
111
|
+
for (let row = 0; row < rows; row++) {
|
|
112
|
+
const symbol = screen[reel][row];
|
|
113
|
+
if (symbol >= baseMin && symbol <= baseMax) {
|
|
114
|
+
const index = symbol - baseMin;
|
|
115
|
+
symbolsCount[index][reel]++;
|
|
116
|
+
symbolPositions[index][reel][row] = true;
|
|
117
|
+
} else if (symbol === WILD) {
|
|
118
|
+
this.markWildPositions(
|
|
119
|
+
baseMin,
|
|
120
|
+
baseMax,
|
|
121
|
+
reel,
|
|
122
|
+
row,
|
|
123
|
+
symbolsCount,
|
|
124
|
+
symbolPositions,
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return { symbolsCount, symbolPositions };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private static markWildPositions(
|
|
134
|
+
baseMin: number,
|
|
135
|
+
baseMax: number,
|
|
136
|
+
reel: number,
|
|
137
|
+
row: number,
|
|
138
|
+
symbolsCount: SymbolsCountMatrix,
|
|
139
|
+
symbolPositions: SymbolPositionsMatrix,
|
|
140
|
+
): void {
|
|
141
|
+
for (let baseSymbol = baseMin; baseSymbol <= baseMax; baseSymbol++) {
|
|
142
|
+
const index = baseSymbol - baseMin;
|
|
143
|
+
symbolsCount[index][reel]++;
|
|
144
|
+
symbolPositions[index][reel][row] = true;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
private static findWinningLines({
|
|
149
|
+
symbolsCount,
|
|
150
|
+
symbolPositions,
|
|
151
|
+
configData,
|
|
152
|
+
betStake,
|
|
153
|
+
currentMultiplier,
|
|
154
|
+
}: FindWinningLinesParams): CalculateWinsResult {
|
|
155
|
+
const { baseMin, baseMax, reels, rows, minLength, maxLength, multipliers } =
|
|
156
|
+
configData;
|
|
157
|
+
|
|
158
|
+
let totalWin = 0;
|
|
159
|
+
const winningLines: WinLine[] = [];
|
|
160
|
+
|
|
161
|
+
for (let symbol = baseMin; symbol <= baseMax; symbol++) {
|
|
162
|
+
const idx = symbol - baseMin;
|
|
163
|
+
let ways = 1;
|
|
164
|
+
let consecutiveReels = 0;
|
|
165
|
+
const positions: [number, number][] = [];
|
|
166
|
+
|
|
167
|
+
for (let reel = 0; reel < reels; reel++) {
|
|
168
|
+
const countOnReel = symbolsCount[idx][reel];
|
|
169
|
+
if (countOnReel > 0) {
|
|
170
|
+
consecutiveReels++;
|
|
171
|
+
ways = NumberHelper.safeMultiply(ways, countOnReel, 6);
|
|
172
|
+
for (let row = 0; row < rows; row++) {
|
|
173
|
+
if (symbolPositions[idx][reel][row]) positions.push([reel, row]);
|
|
174
|
+
}
|
|
175
|
+
} else break;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (consecutiveReels >= minLength && consecutiveReels <= maxLength) {
|
|
179
|
+
const multiplier =
|
|
180
|
+
multipliers.get(symbol)?.[`x${consecutiveReels}`] ?? 0;
|
|
181
|
+
if (multiplier > 0) {
|
|
182
|
+
let winAmount = ways;
|
|
183
|
+
winAmount = NumberHelper.safeMultiply(winAmount, multiplier, 6);
|
|
184
|
+
winAmount = NumberHelper.safeMultiply(winAmount, betStake, 6);
|
|
185
|
+
winAmount = NumberHelper.safeMultiply(
|
|
186
|
+
winAmount,
|
|
187
|
+
currentMultiplier,
|
|
188
|
+
6,
|
|
189
|
+
);
|
|
190
|
+
winAmount = NumberHelper.toFixedNumber(winAmount, 2);
|
|
191
|
+
|
|
192
|
+
totalWin = NumberHelper.safeSum(totalWin, winAmount, 6);
|
|
193
|
+
|
|
194
|
+
winningLines.push({
|
|
195
|
+
symbol,
|
|
196
|
+
consecutiveReels,
|
|
197
|
+
ways,
|
|
198
|
+
multiplier,
|
|
199
|
+
win: winAmount,
|
|
200
|
+
positions,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
totalWin = NumberHelper.toFixedNumber(totalWin, 2);
|
|
207
|
+
|
|
208
|
+
return { totalWin, winningLines };
|
|
209
|
+
}
|
|
210
|
+
}
|