@slot-engine/core 0.0.5 → 0.0.7
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/index.d.mts +1073 -857
- package/dist/index.d.ts +1073 -857
- package/dist/index.js +2245 -1867
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2239 -1859
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -6
package/dist/index.js
CHANGED
|
@@ -31,257 +31,104 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
33
|
ClusterWinType: () => ClusterWinType,
|
|
34
|
-
GameConfig: () => GameConfig,
|
|
35
34
|
GameMode: () => GameMode,
|
|
36
35
|
GameSymbol: () => GameSymbol,
|
|
36
|
+
GeneratedReelSet: () => GeneratedReelSet,
|
|
37
37
|
LinesWinType: () => LinesWinType,
|
|
38
38
|
ManywaysWinType: () => ManywaysWinType,
|
|
39
39
|
OptimizationConditions: () => OptimizationConditions,
|
|
40
40
|
OptimizationParameters: () => OptimizationParameters,
|
|
41
41
|
OptimizationScaling: () => OptimizationScaling,
|
|
42
|
-
ReelGenerator: () => ReelGenerator,
|
|
43
42
|
ResultSet: () => ResultSet,
|
|
43
|
+
SPIN_TYPE: () => SPIN_TYPE,
|
|
44
44
|
StandaloneBoard: () => StandaloneBoard,
|
|
45
|
-
|
|
45
|
+
StaticReelSet: () => StaticReelSet,
|
|
46
46
|
createSlotGame: () => createSlotGame,
|
|
47
47
|
defineGameModes: () => defineGameModes,
|
|
48
|
-
defineReelSets: () => defineReelSets,
|
|
49
48
|
defineSymbols: () => defineSymbols,
|
|
50
|
-
defineUserState: () => defineUserState
|
|
51
|
-
weightedRandom: () => weightedRandom
|
|
49
|
+
defineUserState: () => defineUserState
|
|
52
50
|
});
|
|
53
51
|
module.exports = __toCommonJS(index_exports);
|
|
54
52
|
|
|
55
|
-
// src/
|
|
53
|
+
// src/constants.ts
|
|
54
|
+
var SPIN_TYPE = {
|
|
55
|
+
BASE_GAME: "basegame",
|
|
56
|
+
FREE_SPINS: "freespins"
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// src/game-config/index.ts
|
|
56
60
|
var import_assert = __toESM(require("assert"));
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
61
|
+
function createGameConfig(opts) {
|
|
62
|
+
const symbols = /* @__PURE__ */ new Map();
|
|
63
|
+
for (const [key, value] of Object.entries(opts.symbols)) {
|
|
64
|
+
(0, import_assert.default)(value.id === key, `Symbol key "${key}" does not match symbol id "${value.id}"`);
|
|
65
|
+
symbols.set(key, value);
|
|
66
|
+
}
|
|
67
|
+
const getAnticipationTrigger = (spinType) => {
|
|
68
|
+
return Math.min(...Object.keys(opts.scatterToFreespins[spinType]).map(Number)) - 1;
|
|
69
|
+
};
|
|
70
|
+
return {
|
|
71
|
+
padSymbols: opts.padSymbols || 1,
|
|
72
|
+
userState: opts.userState || {},
|
|
73
|
+
...opts,
|
|
74
|
+
symbols,
|
|
75
|
+
anticipationTriggers: {
|
|
76
|
+
[SPIN_TYPE.BASE_GAME]: getAnticipationTrigger(SPIN_TYPE.BASE_GAME),
|
|
77
|
+
[SPIN_TYPE.FREE_SPINS]: getAnticipationTrigger(SPIN_TYPE.FREE_SPINS)
|
|
78
|
+
},
|
|
79
|
+
outputDir: "__build__"
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// src/simulation/index.ts
|
|
84
|
+
var import_fs2 = __toESM(require("fs"));
|
|
85
|
+
var import_path = __toESM(require("path"));
|
|
86
|
+
var import_assert6 = __toESM(require("assert"));
|
|
87
|
+
var import_zlib = __toESM(require("zlib"));
|
|
88
|
+
var import_esbuild = require("esbuild");
|
|
89
|
+
var import_worker_threads = require("worker_threads");
|
|
90
|
+
|
|
91
|
+
// src/result-set/index.ts
|
|
92
|
+
var import_assert2 = __toESM(require("assert"));
|
|
93
|
+
|
|
94
|
+
// src/service/index.ts
|
|
95
|
+
var AbstractService = class {
|
|
91
96
|
/**
|
|
92
|
-
*
|
|
97
|
+
* Function that returns the current game context.
|
|
93
98
|
*/
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
}
|
|
106
|
-
}
|
|
99
|
+
ctx;
|
|
100
|
+
constructor(ctx) {
|
|
101
|
+
this.ctx = ctx;
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
// src/service/rng.ts
|
|
106
|
+
var RngService = class extends AbstractService {
|
|
107
|
+
rng = new RandomNumberGenerator();
|
|
108
|
+
constructor(ctx) {
|
|
109
|
+
super(ctx);
|
|
107
110
|
}
|
|
108
111
|
/**
|
|
109
|
-
*
|
|
112
|
+
* Random weighted selection from a set of items.
|
|
110
113
|
*/
|
|
111
|
-
|
|
112
|
-
const reelSet = this.config.gameModes[gameMode].reelSets.find((rs) => rs.id === id);
|
|
113
|
-
if (!reelSet) {
|
|
114
|
-
throw new Error(
|
|
115
|
-
`Reel set with id "${id}" not found in game mode "${gameMode}". Available reel sets: ${this.config.gameModes[gameMode].reelSets.map((rs) => rs.id).join(", ")}`
|
|
116
|
-
);
|
|
117
|
-
}
|
|
118
|
-
return reelSet.reels;
|
|
119
|
-
}
|
|
114
|
+
weightedRandom = this.rng.weightedRandom.bind(this.rng);
|
|
120
115
|
/**
|
|
121
|
-
*
|
|
116
|
+
* Selects a random item from an array.
|
|
122
117
|
*/
|
|
123
|
-
|
|
124
|
-
const freespinsConfig = this.config.scatterToFreespins[spinType];
|
|
125
|
-
if (!freespinsConfig) {
|
|
126
|
-
throw new Error(
|
|
127
|
-
`No free spins configuration found for spin type "${spinType}". Please check your game configuration.`
|
|
128
|
-
);
|
|
129
|
-
}
|
|
130
|
-
return freespinsConfig[scatterCount] || 0;
|
|
131
|
-
}
|
|
118
|
+
randomItem = this.rng.randomItem.bind(this.rng);
|
|
132
119
|
/**
|
|
133
|
-
*
|
|
120
|
+
* Shuffles an array.
|
|
134
121
|
*/
|
|
135
|
-
|
|
136
|
-
const gameMode = this.config.gameModes[mode];
|
|
137
|
-
if (!gameMode) {
|
|
138
|
-
throw new Error(`Game mode "${mode}" not found in game config.`);
|
|
139
|
-
}
|
|
140
|
-
const resultSet = gameMode.resultSets.find((rs) => rs.criteria === criteria);
|
|
141
|
-
if (!resultSet) {
|
|
142
|
-
throw new Error(
|
|
143
|
-
`Criteria "${criteria}" not found in game mode "${mode}". Available criteria: ${gameMode.resultSets.map((rs) => rs.criteria).join(", ")}`
|
|
144
|
-
);
|
|
145
|
-
}
|
|
146
|
-
return resultSet;
|
|
147
|
-
}
|
|
122
|
+
shuffle = this.rng.shuffle.bind(this.rng);
|
|
148
123
|
/**
|
|
149
|
-
*
|
|
124
|
+
* Generates a random float between two values.
|
|
150
125
|
*/
|
|
151
|
-
|
|
152
|
-
return Array.from(this.config.symbols).map(([n, v]) => v);
|
|
153
|
-
}
|
|
154
|
-
static SPIN_TYPE = {
|
|
155
|
-
BASE_GAME: "basegame",
|
|
156
|
-
FREE_SPINS: "freespins"
|
|
157
|
-
};
|
|
158
|
-
};
|
|
159
|
-
|
|
160
|
-
// src/GameMode.ts
|
|
161
|
-
var import_assert2 = __toESM(require("assert"));
|
|
162
|
-
var GameMode = class {
|
|
163
|
-
name;
|
|
164
|
-
reelsAmount;
|
|
165
|
-
symbolsPerReel;
|
|
166
|
-
cost;
|
|
167
|
-
rtp;
|
|
168
|
-
reelSets;
|
|
169
|
-
resultSets;
|
|
170
|
-
isBonusBuy;
|
|
171
|
-
constructor(opts) {
|
|
172
|
-
this.name = opts.name;
|
|
173
|
-
this.reelsAmount = opts.reelsAmount;
|
|
174
|
-
this.symbolsPerReel = opts.symbolsPerReel;
|
|
175
|
-
this.cost = opts.cost;
|
|
176
|
-
this.rtp = opts.rtp;
|
|
177
|
-
this.reelSets = opts.reelSets;
|
|
178
|
-
this.resultSets = opts.resultSets;
|
|
179
|
-
this.isBonusBuy = opts.isBonusBuy;
|
|
180
|
-
(0, import_assert2.default)(this.rtp >= 0.9 && this.rtp <= 0.99, "RTP must be between 0.9 and 0.99");
|
|
181
|
-
(0, import_assert2.default)(
|
|
182
|
-
this.symbolsPerReel.length === this.reelsAmount,
|
|
183
|
-
"symbolsPerReel length must match reelsAmount."
|
|
184
|
-
);
|
|
185
|
-
(0, import_assert2.default)(this.reelSets.length > 0, "GameMode must have at least one ReelSet defined.");
|
|
186
|
-
}
|
|
187
|
-
};
|
|
188
|
-
|
|
189
|
-
// src/GameSymbol.ts
|
|
190
|
-
var GameSymbol = class _GameSymbol {
|
|
191
|
-
id;
|
|
192
|
-
pays;
|
|
193
|
-
properties;
|
|
194
|
-
constructor(opts) {
|
|
195
|
-
this.id = opts.id;
|
|
196
|
-
this.pays = opts.pays;
|
|
197
|
-
this.properties = new Map(Object.entries(opts.properties || {}));
|
|
198
|
-
if (this.pays && Object.keys(this.pays).length === 0) {
|
|
199
|
-
throw new Error(`GameSymbol "${this.id}" must have pays defined.`);
|
|
200
|
-
}
|
|
201
|
-
}
|
|
126
|
+
randomFloat = this.rng.randomFloat.bind(this.rng);
|
|
202
127
|
/**
|
|
203
|
-
*
|
|
128
|
+
* Sets the seed for the RNG.
|
|
204
129
|
*/
|
|
205
|
-
|
|
206
|
-
if (!symbolOrProperties) {
|
|
207
|
-
console.warn("No symbol or properties provided for comparison.");
|
|
208
|
-
return false;
|
|
209
|
-
}
|
|
210
|
-
if (symbolOrProperties instanceof _GameSymbol) {
|
|
211
|
-
return this.id === symbolOrProperties.id;
|
|
212
|
-
} else {
|
|
213
|
-
for (const [key, value] of Object.entries(symbolOrProperties)) {
|
|
214
|
-
if (!this.properties.has(key) || this.properties.get(key) !== value) {
|
|
215
|
-
return false;
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
return true;
|
|
219
|
-
}
|
|
220
|
-
}
|
|
130
|
+
setSeedIfDifferent = this.rng.setSeedIfDifferent.bind(this.rng);
|
|
221
131
|
};
|
|
222
|
-
|
|
223
|
-
// src/ReelGenerator.ts
|
|
224
|
-
var import_fs2 = __toESM(require("fs"));
|
|
225
|
-
var import_path = __toESM(require("path"));
|
|
226
|
-
|
|
227
|
-
// utils.ts
|
|
228
|
-
var import_fs = __toESM(require("fs"));
|
|
229
|
-
function weightedRandom(weights, rng) {
|
|
230
|
-
const totalWeight = Object.values(weights).reduce(
|
|
231
|
-
(sum, weight) => sum + weight,
|
|
232
|
-
0
|
|
233
|
-
);
|
|
234
|
-
const randomValue = rng.randomFloat(0, 1) * totalWeight;
|
|
235
|
-
let cumulativeWeight = 0;
|
|
236
|
-
for (const [key, weight] of Object.entries(weights)) {
|
|
237
|
-
cumulativeWeight += weight;
|
|
238
|
-
if (randomValue < cumulativeWeight) {
|
|
239
|
-
return key;
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
throw new Error("No item selected in weighted random selection.");
|
|
243
|
-
}
|
|
244
|
-
function randomItem(array, rng) {
|
|
245
|
-
if (array.length === 0) {
|
|
246
|
-
throw new Error("Cannot select a random item from an empty array.");
|
|
247
|
-
}
|
|
248
|
-
const randomIndex = Math.floor(rng.randomFloat(0, 1) * array.length);
|
|
249
|
-
return array[randomIndex];
|
|
250
|
-
}
|
|
251
|
-
function createDirIfNotExists(dirPath) {
|
|
252
|
-
if (!import_fs.default.existsSync(dirPath)) {
|
|
253
|
-
import_fs.default.mkdirSync(dirPath, { recursive: true });
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
function shuffle(array, rng) {
|
|
257
|
-
const newArray = [...array];
|
|
258
|
-
let currentIndex = newArray.length, randomIndex;
|
|
259
|
-
while (currentIndex != 0) {
|
|
260
|
-
randomIndex = Math.floor(rng.randomFloat(0, 1) * currentIndex);
|
|
261
|
-
currentIndex--;
|
|
262
|
-
[newArray[currentIndex], newArray[randomIndex]] = [
|
|
263
|
-
newArray[randomIndex],
|
|
264
|
-
newArray[currentIndex]
|
|
265
|
-
];
|
|
266
|
-
}
|
|
267
|
-
return newArray;
|
|
268
|
-
}
|
|
269
|
-
function writeJsonFile(filePath, data) {
|
|
270
|
-
try {
|
|
271
|
-
import_fs.default.writeFileSync(filePath, JSON.stringify(data, null, 2), {
|
|
272
|
-
encoding: "utf8"
|
|
273
|
-
});
|
|
274
|
-
} catch (error) {
|
|
275
|
-
throw new Error(`Failed to write JSON file at ${filePath}: ${error}`);
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
function writeFile(filePath, data) {
|
|
279
|
-
try {
|
|
280
|
-
import_fs.default.writeFileSync(filePath, data, { encoding: "utf8" });
|
|
281
|
-
} catch (error) {
|
|
282
|
-
throw new Error(`Failed to write file at ${filePath}: ${error}`);
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
132
|
var RandomNumberGenerator = class {
|
|
286
133
|
mIdum;
|
|
287
134
|
mIy;
|
|
@@ -366,7 +213,66 @@ var RandomNumberGenerator = class {
|
|
|
366
213
|
}
|
|
367
214
|
return float * (high - low) + low;
|
|
368
215
|
}
|
|
216
|
+
weightedRandom(weights) {
|
|
217
|
+
const totalWeight = Object.values(weights).reduce(
|
|
218
|
+
(sum, weight) => sum + weight,
|
|
219
|
+
0
|
|
220
|
+
);
|
|
221
|
+
const randomValue = this.randomFloat(0, 1) * totalWeight;
|
|
222
|
+
let cumulativeWeight = 0;
|
|
223
|
+
for (const [key, weight] of Object.entries(weights)) {
|
|
224
|
+
cumulativeWeight += weight;
|
|
225
|
+
if (randomValue < cumulativeWeight) {
|
|
226
|
+
return key;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
throw new Error("No item selected in weighted random selection.");
|
|
230
|
+
}
|
|
231
|
+
randomItem(array) {
|
|
232
|
+
if (array.length === 0) {
|
|
233
|
+
throw new Error("Cannot select a random item from an empty array.");
|
|
234
|
+
}
|
|
235
|
+
const randomIndex = Math.floor(this.randomFloat(0, 1) * array.length);
|
|
236
|
+
return array[randomIndex];
|
|
237
|
+
}
|
|
238
|
+
shuffle(array) {
|
|
239
|
+
const newArray = [...array];
|
|
240
|
+
let currentIndex = newArray.length, randomIndex;
|
|
241
|
+
while (currentIndex != 0) {
|
|
242
|
+
randomIndex = Math.floor(this.randomFloat(0, 1) * currentIndex);
|
|
243
|
+
currentIndex--;
|
|
244
|
+
[newArray[currentIndex], newArray[randomIndex]] = [
|
|
245
|
+
newArray[randomIndex],
|
|
246
|
+
newArray[currentIndex]
|
|
247
|
+
];
|
|
248
|
+
}
|
|
249
|
+
return newArray;
|
|
250
|
+
}
|
|
369
251
|
};
|
|
252
|
+
|
|
253
|
+
// utils.ts
|
|
254
|
+
var import_fs = __toESM(require("fs"));
|
|
255
|
+
function createDirIfNotExists(dirPath) {
|
|
256
|
+
if (!import_fs.default.existsSync(dirPath)) {
|
|
257
|
+
import_fs.default.mkdirSync(dirPath, { recursive: true });
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
function writeJsonFile(filePath, data) {
|
|
261
|
+
try {
|
|
262
|
+
import_fs.default.writeFileSync(filePath, JSON.stringify(data, null, 2), {
|
|
263
|
+
encoding: "utf8"
|
|
264
|
+
});
|
|
265
|
+
} catch (error) {
|
|
266
|
+
throw new Error(`Failed to write JSON file at ${filePath}: ${error}`);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
function writeFile(filePath, data) {
|
|
270
|
+
try {
|
|
271
|
+
import_fs.default.writeFileSync(filePath, data, { encoding: "utf8" });
|
|
272
|
+
} catch (error) {
|
|
273
|
+
throw new Error(`Failed to write file at ${filePath}: ${error}`);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
370
276
|
function copy(obj) {
|
|
371
277
|
return JSON.parse(JSON.stringify(obj));
|
|
372
278
|
}
|
|
@@ -379,814 +285,709 @@ var JSONL = class {
|
|
|
379
285
|
}
|
|
380
286
|
};
|
|
381
287
|
|
|
382
|
-
// src/
|
|
383
|
-
var
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
spaceBetweenSymbols;
|
|
393
|
-
preferStackedSymbols;
|
|
394
|
-
symbolStacks;
|
|
395
|
-
symbolQuotas;
|
|
396
|
-
csvPath = "";
|
|
397
|
-
overrideExisting;
|
|
398
|
-
rng;
|
|
288
|
+
// src/result-set/index.ts
|
|
289
|
+
var ResultSet = class {
|
|
290
|
+
criteria;
|
|
291
|
+
quota;
|
|
292
|
+
multiplier;
|
|
293
|
+
reelWeights;
|
|
294
|
+
userData;
|
|
295
|
+
forceMaxWin;
|
|
296
|
+
forceFreespins;
|
|
297
|
+
evaluate;
|
|
399
298
|
constructor(opts) {
|
|
400
|
-
this.
|
|
401
|
-
this.
|
|
402
|
-
this.
|
|
403
|
-
|
|
404
|
-
this.
|
|
405
|
-
this.
|
|
406
|
-
this.
|
|
407
|
-
this.
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
299
|
+
this.criteria = opts.criteria;
|
|
300
|
+
this.quota = opts.quota;
|
|
301
|
+
this.multiplier = opts.multiplier;
|
|
302
|
+
this.reelWeights = opts.reelWeights;
|
|
303
|
+
this.userData = opts.userData;
|
|
304
|
+
this.forceMaxWin = opts.forceMaxWin;
|
|
305
|
+
this.forceFreespins = opts.forceFreespins;
|
|
306
|
+
this.evaluate = opts.evaluate;
|
|
307
|
+
}
|
|
308
|
+
static assignCriteriaToSimulations(ctx, gameModeName) {
|
|
309
|
+
const rng = new RandomNumberGenerator();
|
|
310
|
+
rng.setSeed(0);
|
|
311
|
+
(0, import_assert2.default)(ctx.simRunsAmount, "Simulation configuration is not set.");
|
|
312
|
+
const simNums = ctx.simRunsAmount[gameModeName];
|
|
313
|
+
const resultSets = ctx.gameConfig.gameModes[gameModeName]?.resultSets;
|
|
314
|
+
if (!resultSets || resultSets.length === 0) {
|
|
315
|
+
throw new Error(`No ResultSets found for game mode: ${gameModeName}.`);
|
|
414
316
|
}
|
|
415
|
-
if (
|
|
416
|
-
|
|
417
|
-
)) {
|
|
418
|
-
throw new Error(
|
|
419
|
-
`spaceBetweenSymbols must be between 1 and 8, got ${this.spaceBetweenSymbols}.`
|
|
420
|
-
);
|
|
317
|
+
if (simNums === void 0 || simNums <= 0) {
|
|
318
|
+
throw new Error(`No simulations configured for game mode "${gameModeName}".`);
|
|
421
319
|
}
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
320
|
+
const totalQuota = resultSets.reduce((sum, rs) => sum + rs.quota, 0);
|
|
321
|
+
const numberOfSimsForCriteria = Object.fromEntries(
|
|
322
|
+
resultSets.map((rs) => {
|
|
323
|
+
const normalizedQuota = totalQuota > 0 ? rs.quota / totalQuota : 0;
|
|
324
|
+
return [rs.criteria, Math.max(Math.floor(normalizedQuota * simNums), 1)];
|
|
325
|
+
})
|
|
326
|
+
);
|
|
327
|
+
let totalSims = Object.values(numberOfSimsForCriteria).reduce(
|
|
328
|
+
(sum, num) => sum + num,
|
|
329
|
+
0
|
|
330
|
+
);
|
|
331
|
+
let reduceSims = totalSims > simNums;
|
|
332
|
+
const criteriaToWeights = Object.fromEntries(
|
|
333
|
+
resultSets.map((rs) => [rs.criteria, rs.quota])
|
|
334
|
+
);
|
|
335
|
+
while (totalSims != simNums) {
|
|
336
|
+
const rs = rng.weightedRandom(criteriaToWeights);
|
|
337
|
+
if (reduceSims && numberOfSimsForCriteria[rs] > 1) {
|
|
338
|
+
numberOfSimsForCriteria[rs] -= 1;
|
|
339
|
+
} else if (!reduceSims) {
|
|
340
|
+
numberOfSimsForCriteria[rs] += 1;
|
|
341
|
+
}
|
|
342
|
+
totalSims = Object.values(numberOfSimsForCriteria).reduce(
|
|
343
|
+
(sum, num) => sum + num,
|
|
344
|
+
0
|
|
425
345
|
);
|
|
346
|
+
reduceSims = totalSims > simNums;
|
|
426
347
|
}
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
if (!config.symbols.has(symbol)) {
|
|
433
|
-
throw new Error(
|
|
434
|
-
`Symbol "${symbol}" of the reel generator ${this.id} for mode ${this.associatedGameModeName} is not defined in the game config`
|
|
435
|
-
);
|
|
348
|
+
let allCriteria = [];
|
|
349
|
+
const simNumsToCriteria = {};
|
|
350
|
+
Object.entries(numberOfSimsForCriteria).forEach(([criteria, num]) => {
|
|
351
|
+
for (let i = 0; i <= num; i++) {
|
|
352
|
+
allCriteria.push(criteria);
|
|
436
353
|
}
|
|
437
354
|
});
|
|
438
|
-
|
|
439
|
-
|
|
355
|
+
allCriteria = rng.shuffle(allCriteria);
|
|
356
|
+
for (let i = 1; i <= Math.min(simNums, allCriteria.length); i++) {
|
|
357
|
+
simNumsToCriteria[i] = allCriteria[i];
|
|
440
358
|
}
|
|
359
|
+
return simNumsToCriteria;
|
|
441
360
|
}
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
const
|
|
450
|
-
|
|
451
|
-
const
|
|
452
|
-
const
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
361
|
+
/**
|
|
362
|
+
* Checks if core criteria is met, e.g. target multiplier or max win.
|
|
363
|
+
*/
|
|
364
|
+
meetsCriteria(ctx) {
|
|
365
|
+
const customEval = this.evaluate?.(copy(ctx));
|
|
366
|
+
const freespinsMet = this.forceFreespins ? ctx.state.triggeredFreespins : true;
|
|
367
|
+
const wallet = ctx.services.wallet._getWallet();
|
|
368
|
+
const multiplierMet = this.multiplier !== void 0 ? wallet.getCurrentWin() === this.multiplier && !this.forceMaxWin : wallet.getCurrentWin() > 0 && (!this.forceMaxWin || true);
|
|
369
|
+
const maxWinMet = this.forceMaxWin ? wallet.getCurrentWin() >= ctx.config.maxWinX : true;
|
|
370
|
+
const coreCriteriaMet = freespinsMet && multiplierMet && maxWinMet;
|
|
371
|
+
const finalResult = customEval !== void 0 ? coreCriteriaMet && customEval === true : coreCriteriaMet;
|
|
372
|
+
if (this.forceMaxWin && maxWinMet) {
|
|
373
|
+
ctx.services.data.record({
|
|
374
|
+
maxwin: true
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
return finalResult;
|
|
458
378
|
}
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
// src/game-state/index.ts
|
|
382
|
+
function createGameState(opts) {
|
|
383
|
+
return {
|
|
384
|
+
currentSimulationId: opts?.currentSimulationId || 0,
|
|
385
|
+
currentGameMode: opts?.currentGameMode || "N/A",
|
|
386
|
+
currentSpinType: opts?.currentSpinType || SPIN_TYPE.BASE_GAME,
|
|
387
|
+
currentResultSet: opts?.currentResultSet || new ResultSet({
|
|
388
|
+
criteria: "N/A",
|
|
389
|
+
quota: 0,
|
|
390
|
+
reelWeights: {
|
|
391
|
+
[SPIN_TYPE.BASE_GAME]: {},
|
|
392
|
+
[SPIN_TYPE.FREE_SPINS]: {}
|
|
393
|
+
}
|
|
394
|
+
}),
|
|
395
|
+
isCriteriaMet: opts?.isCriteriaMet || false,
|
|
396
|
+
currentFreespinAmount: opts?.currentFreespinAmount || 0,
|
|
397
|
+
totalFreespinAmount: opts?.totalFreespinAmount || 0,
|
|
398
|
+
userData: opts?.userData || {},
|
|
399
|
+
triggeredMaxWin: opts?.triggeredMaxWin || false,
|
|
400
|
+
triggeredFreespins: opts?.triggeredFreespins || false
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// src/board/index.ts
|
|
405
|
+
var import_assert3 = __toESM(require("assert"));
|
|
406
|
+
|
|
407
|
+
// src/game-symbol/index.ts
|
|
408
|
+
var GameSymbol = class _GameSymbol {
|
|
409
|
+
id;
|
|
410
|
+
pays;
|
|
411
|
+
properties;
|
|
412
|
+
constructor(opts) {
|
|
413
|
+
this.id = opts.id;
|
|
414
|
+
this.pays = opts.pays;
|
|
415
|
+
this.properties = new Map(Object.entries(opts.properties || {}));
|
|
416
|
+
if (this.pays && Object.keys(this.pays).length === 0) {
|
|
417
|
+
throw new Error(`GameSymbol "${this.id}" must have pays defined.`);
|
|
466
418
|
}
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* Compares this symbol to another symbol or a set of properties.
|
|
422
|
+
*/
|
|
423
|
+
compare(symbolOrProperties) {
|
|
424
|
+
if (!symbolOrProperties) {
|
|
425
|
+
console.warn("No symbol or properties provided for comparison.");
|
|
426
|
+
return false;
|
|
473
427
|
}
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
428
|
+
if (symbolOrProperties instanceof _GameSymbol) {
|
|
429
|
+
return this.id === symbolOrProperties.id;
|
|
430
|
+
} else {
|
|
431
|
+
for (const [key, value] of Object.entries(symbolOrProperties)) {
|
|
432
|
+
if (!this.properties.has(key) || this.properties.get(key) !== value) {
|
|
433
|
+
return false;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
return true;
|
|
477
437
|
}
|
|
478
|
-
return canPlace;
|
|
479
438
|
}
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
// src/board/index.ts
|
|
442
|
+
var Board = class {
|
|
480
443
|
/**
|
|
481
|
-
*
|
|
444
|
+
* The current reels on the board.\
|
|
445
|
+
* Includes only the visible symbols (without padding).
|
|
482
446
|
*/
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
447
|
+
reels;
|
|
448
|
+
/**
|
|
449
|
+
* The top padding symbols on the board.\
|
|
450
|
+
* These are the symbols above the visible area.
|
|
451
|
+
*/
|
|
452
|
+
paddingTop;
|
|
453
|
+
/**
|
|
454
|
+
* The bottom padding symbols on the board.\
|
|
455
|
+
* These are the symbols below the visible area.
|
|
456
|
+
*/
|
|
457
|
+
paddingBottom;
|
|
458
|
+
/**
|
|
459
|
+
* The anticipation values for each reel on the board.\
|
|
460
|
+
* Used for triggering anticipation effects.
|
|
461
|
+
*/
|
|
462
|
+
anticipation;
|
|
463
|
+
lastDrawnReelStops;
|
|
464
|
+
lastUsedReels;
|
|
465
|
+
constructor() {
|
|
466
|
+
this.reels = [];
|
|
467
|
+
this.paddingTop = [];
|
|
468
|
+
this.paddingBottom = [];
|
|
469
|
+
this.anticipation = [];
|
|
470
|
+
this.lastDrawnReelStops = [];
|
|
471
|
+
this.lastUsedReels = [];
|
|
472
|
+
}
|
|
473
|
+
makeEmptyReels(opts) {
|
|
474
|
+
const length = opts.reelsAmount ?? opts.ctx.services.game.getCurrentGameMode().reelsAmount;
|
|
475
|
+
(0, import_assert3.default)(length, "Cannot make empty reels without context or reelsAmount.");
|
|
476
|
+
return Array.from({ length }, () => []);
|
|
477
|
+
}
|
|
478
|
+
countSymbolsOnReel(symbolOrProperties, reelIndex) {
|
|
479
|
+
let total = 0;
|
|
480
|
+
for (const symbol of this.reels[reelIndex]) {
|
|
481
|
+
let matches = true;
|
|
482
|
+
if (symbolOrProperties instanceof GameSymbol) {
|
|
483
|
+
if (symbol.id !== symbolOrProperties.id) matches = false;
|
|
484
|
+
} else {
|
|
485
|
+
for (const [key, value] of Object.entries(symbolOrProperties)) {
|
|
486
|
+
if (!symbol.properties.has(key) || symbol.properties.get(key) !== value) {
|
|
487
|
+
matches = false;
|
|
488
|
+
break;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
496
491
|
}
|
|
497
|
-
if (
|
|
498
|
-
|
|
499
|
-
if (forward >= 1 && dist <= forward) return true;
|
|
500
|
-
const reverse = this.spaceBetweenSymbols[placed.id]?.[symbolId] ?? 0;
|
|
501
|
-
if (reverse >= 1 && dist <= reverse) return true;
|
|
492
|
+
if (matches) {
|
|
493
|
+
total++;
|
|
502
494
|
}
|
|
503
495
|
}
|
|
504
|
-
return
|
|
496
|
+
return total;
|
|
505
497
|
}
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
const
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
498
|
+
countSymbolsOnBoard(symbolOrProperties) {
|
|
499
|
+
let total = 0;
|
|
500
|
+
const onReel = {};
|
|
501
|
+
for (const [ridx, reel] of this.reels.entries()) {
|
|
502
|
+
for (const symbol of reel) {
|
|
503
|
+
let matches = true;
|
|
504
|
+
if (symbolOrProperties instanceof GameSymbol) {
|
|
505
|
+
if (symbol.id !== symbolOrProperties.id) matches = false;
|
|
506
|
+
} else {
|
|
507
|
+
for (const [key, value] of Object.entries(symbolOrProperties)) {
|
|
508
|
+
if (!symbol.properties.has(key) || symbol.properties.get(key) !== value) {
|
|
509
|
+
matches = false;
|
|
510
|
+
break;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
if (matches) {
|
|
515
|
+
total++;
|
|
516
|
+
if (onReel[ridx] === void 0) {
|
|
517
|
+
onReel[ridx] = 1;
|
|
518
|
+
} else {
|
|
519
|
+
onReel[ridx]++;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}
|
|
513
523
|
}
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
)
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
524
|
+
return [total, onReel];
|
|
525
|
+
}
|
|
526
|
+
isSymbolOnAnyReelMultipleTimes(symbol) {
|
|
527
|
+
for (const reel of this.reels) {
|
|
528
|
+
let count = 0;
|
|
529
|
+
for (const sym of reel) {
|
|
530
|
+
if (sym.id === symbol.id) {
|
|
531
|
+
count++;
|
|
532
|
+
}
|
|
533
|
+
if (count > 1) {
|
|
534
|
+
return true;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
523
537
|
}
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
538
|
+
return false;
|
|
539
|
+
}
|
|
540
|
+
getReelStopsForSymbol(reels, symbol) {
|
|
541
|
+
const reelStops = [];
|
|
542
|
+
for (let ridx = 0; ridx < reels.length; ridx++) {
|
|
543
|
+
const reel = reels[ridx];
|
|
544
|
+
const positions = [];
|
|
545
|
+
for (let pos = 0; pos < reel.length; pos++) {
|
|
546
|
+
if (reel[pos].id === symbol.id) {
|
|
547
|
+
positions.push(pos);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
reelStops.push(positions);
|
|
528
551
|
}
|
|
529
|
-
|
|
530
|
-
|
|
552
|
+
return reelStops;
|
|
553
|
+
}
|
|
554
|
+
combineReelStops(opts) {
|
|
555
|
+
const reelsAmount = opts.reelsAmount ?? opts.ctx.services.game.getCurrentGameMode().reelsAmount;
|
|
556
|
+
(0, import_assert3.default)(reelsAmount, "Cannot combine reel stops without context or reelsAmount.");
|
|
557
|
+
const combined = [];
|
|
531
558
|
for (let ridx = 0; ridx < reelsAmount; ridx++) {
|
|
532
|
-
|
|
533
|
-
const
|
|
534
|
-
|
|
535
|
-
let totalReelsQuota = 0;
|
|
536
|
-
for (const [sym, quotaConf] of Object.entries(this.symbolQuotas || {})) {
|
|
537
|
-
const q = typeof quotaConf === "number" ? quotaConf : quotaConf[ridx];
|
|
538
|
-
if (!q) continue;
|
|
539
|
-
reelQuotas[sym] = q;
|
|
540
|
-
totalReelsQuota += q;
|
|
541
|
-
}
|
|
542
|
-
if (totalReelsQuota > 100) {
|
|
543
|
-
throw new Error(
|
|
544
|
-
`Total symbol quotas for reel ${ridx} exceed 100%. Adjust your configuration on ReelGenerator "${this.id}".`
|
|
545
|
-
);
|
|
546
|
-
}
|
|
547
|
-
if (totalReelsQuota > 0) {
|
|
548
|
-
for (const [sym, quota] of Object.entries(reelQuotas)) {
|
|
549
|
-
const quotaCount = Math.max(1, Math.floor(this.rowsAmount * quota / 100));
|
|
550
|
-
quotaCounts[sym] = quotaCount;
|
|
551
|
-
}
|
|
559
|
+
combined[ridx] = [];
|
|
560
|
+
for (const stops of opts.reelStops) {
|
|
561
|
+
combined[ridx] = combined[ridx].concat(stops[ridx]);
|
|
552
562
|
}
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
);
|
|
570
|
-
const toPlace = Math.min(stackSize, remaining);
|
|
571
|
-
placed = this.tryPlaceStack(reel, gameConf, ridx, sym, pos, toPlace);
|
|
572
|
-
}
|
|
573
|
-
if (placed === 0 && reel[pos] === null && this.isSymbolAllowedOnReel(sym, ridx) && !this.violatesSpacing(reel, sym, pos)) {
|
|
574
|
-
reel[pos] = gameConf.config.symbols.get(sym);
|
|
575
|
-
placed = 1;
|
|
576
|
-
}
|
|
577
|
-
remaining -= placed;
|
|
563
|
+
}
|
|
564
|
+
return combined;
|
|
565
|
+
}
|
|
566
|
+
getRandomReelStops(opts) {
|
|
567
|
+
const reelsAmount = opts.reelsAmount ?? opts.ctx.services.game.getCurrentGameMode().reelsAmount;
|
|
568
|
+
(0, import_assert3.default)(reelsAmount, "Cannot get random reel stops without context or reelsAmount.");
|
|
569
|
+
const symProbsOnReels = [];
|
|
570
|
+
const stopPositionsForReels = {};
|
|
571
|
+
for (let ridx = 0; ridx < reelsAmount; ridx++) {
|
|
572
|
+
symProbsOnReels.push(opts.reelStops[ridx].length / opts.reels[ridx].length);
|
|
573
|
+
}
|
|
574
|
+
while (Object.keys(stopPositionsForReels).length !== opts.amount) {
|
|
575
|
+
const possibleReels = [];
|
|
576
|
+
for (let i = 0; i < reelsAmount; i++) {
|
|
577
|
+
if (symProbsOnReels[i] > 0) {
|
|
578
|
+
possibleReels.push(i);
|
|
578
579
|
}
|
|
579
580
|
}
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
);
|
|
629
|
-
}
|
|
630
|
-
chosenSymbolId = weightedRandom(weightsObj, this.rng);
|
|
631
|
-
const hasStackCfg = !!this.resolveStacking(chosenSymbolId, ridx);
|
|
632
|
-
if (!hasStackCfg && this.preferStackedSymbols && reel.length > 0) {
|
|
633
|
-
const prevSymbol = r - 1 >= 0 ? reel[r - 1] : reel[reel.length - 1];
|
|
634
|
-
if (prevSymbol && Math.round(this.rng.randomFloat(1, 100)) <= this.preferStackedSymbols && (!this.spaceBetweenSameSymbols || !this.violatesSpacing(reel, prevSymbol.id, r))) {
|
|
635
|
-
chosenSymbolId = prevSymbol.id;
|
|
636
|
-
}
|
|
637
|
-
}
|
|
638
|
-
}
|
|
639
|
-
const symbol = gameConf.config.symbols.get(chosenSymbolId);
|
|
640
|
-
if (!symbol) {
|
|
641
|
-
throw new Error(
|
|
642
|
-
`Symbol with id "${chosenSymbolId}" not found in the game config symbols map.`
|
|
643
|
-
);
|
|
581
|
+
const possibleProbs = symProbsOnReels.filter((p) => p > 0);
|
|
582
|
+
const weights = Object.fromEntries(
|
|
583
|
+
possibleReels.map((ridx, idx) => [ridx, possibleProbs[idx]])
|
|
584
|
+
);
|
|
585
|
+
const chosenReel = opts.ctx.services.rng.weightedRandom(weights);
|
|
586
|
+
const chosenStop = opts.ctx.services.rng.randomItem(
|
|
587
|
+
opts.reelStops[Number(chosenReel)]
|
|
588
|
+
);
|
|
589
|
+
symProbsOnReels[Number(chosenReel)] = 0;
|
|
590
|
+
stopPositionsForReels[Number(chosenReel)] = chosenStop;
|
|
591
|
+
}
|
|
592
|
+
return stopPositionsForReels;
|
|
593
|
+
}
|
|
594
|
+
getRandomReelset(ctx) {
|
|
595
|
+
const weights = ctx.state.currentResultSet.reelWeights;
|
|
596
|
+
const evalWeights = ctx.state.currentResultSet.reelWeights.evaluate?.(ctx);
|
|
597
|
+
let reelSetId = "";
|
|
598
|
+
if (evalWeights) {
|
|
599
|
+
reelSetId = ctx.services.rng.weightedRandom(evalWeights);
|
|
600
|
+
} else {
|
|
601
|
+
reelSetId = ctx.services.rng.weightedRandom(weights[ctx.state.currentSpinType]);
|
|
602
|
+
}
|
|
603
|
+
const reelSet = ctx.services.game.getReelsetById(ctx.state.currentGameMode, reelSetId);
|
|
604
|
+
return reelSet;
|
|
605
|
+
}
|
|
606
|
+
resetReels(opts) {
|
|
607
|
+
const length = opts.reelsAmount ?? opts.ctx.services.game.getCurrentGameMode().reelsAmount;
|
|
608
|
+
this.reels = this.makeEmptyReels(opts);
|
|
609
|
+
this.anticipation = Array.from({ length }, () => false);
|
|
610
|
+
this.paddingTop = this.makeEmptyReels(opts);
|
|
611
|
+
this.paddingBottom = this.makeEmptyReels(opts);
|
|
612
|
+
}
|
|
613
|
+
drawBoardMixed(opts) {
|
|
614
|
+
this.resetReels(opts);
|
|
615
|
+
const reelsAmount = opts.reelsAmount ?? opts.ctx.services.game.getCurrentGameMode().reelsAmount;
|
|
616
|
+
const symbolsPerReel = opts.symbolsPerReel ?? opts.ctx.services.game.getCurrentGameMode().symbolsPerReel;
|
|
617
|
+
const padSymbols = opts.padSymbols ?? opts.ctx.config.padSymbols;
|
|
618
|
+
const finalReelStops = Array.from(
|
|
619
|
+
{ length: reelsAmount },
|
|
620
|
+
() => null
|
|
621
|
+
);
|
|
622
|
+
if (opts.forcedStops) {
|
|
623
|
+
for (const [r, stopPos] of Object.entries(opts.forcedStops)) {
|
|
624
|
+
const reelIdx = Number(r);
|
|
625
|
+
const symCount = symbolsPerReel[reelIdx];
|
|
626
|
+
finalReelStops[reelIdx] = stopPos - Math.round(opts.ctx.services.rng.randomFloat(0, symCount - 1));
|
|
627
|
+
if (finalReelStops[reelIdx] < 0) {
|
|
628
|
+
finalReelStops[reelIdx] = opts.reels[reelIdx].length + finalReelStops[reelIdx];
|
|
644
629
|
}
|
|
645
|
-
reel[r] = symbol;
|
|
646
630
|
}
|
|
647
|
-
|
|
648
|
-
|
|
631
|
+
}
|
|
632
|
+
for (let i = 0; i < finalReelStops.length; i++) {
|
|
633
|
+
if (finalReelStops[i] === null) {
|
|
634
|
+
finalReelStops[i] = Math.floor(
|
|
635
|
+
opts.ctx.services.rng.randomFloat(0, opts.reels[i].length - 1)
|
|
636
|
+
);
|
|
649
637
|
}
|
|
650
|
-
this.reels.push(reel);
|
|
651
638
|
}
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
() => Array.from({ length: reelsAmount }, () => "")
|
|
655
|
-
);
|
|
639
|
+
this.lastDrawnReelStops = finalReelStops.map((pos) => pos);
|
|
640
|
+
this.lastUsedReels = opts.reels;
|
|
656
641
|
for (let ridx = 0; ridx < reelsAmount; ridx++) {
|
|
657
|
-
|
|
658
|
-
|
|
642
|
+
const reelPos = finalReelStops[ridx];
|
|
643
|
+
for (let p = padSymbols - 1; p >= 0; p--) {
|
|
644
|
+
const topPos = (reelPos - (p + 1)) % opts.reels[ridx].length;
|
|
645
|
+
this.paddingTop[ridx].push(opts.reels[ridx][topPos]);
|
|
646
|
+
const bottomPos = (reelPos + symbolsPerReel[ridx] + p) % opts.reels[ridx].length;
|
|
647
|
+
this.paddingBottom[ridx].unshift(opts.reels[ridx][bottomPos]);
|
|
659
648
|
}
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
if (import_worker_threads.isMainThread) {
|
|
663
|
-
import_fs2.default.writeFileSync(filePath, csvString);
|
|
664
|
-
this.reels = this.parseReelsetCSV(filePath, gameConf);
|
|
665
|
-
console.log(
|
|
666
|
-
`Generated reelset ${this.id} for game mode ${this.associatedGameModeName}`
|
|
667
|
-
);
|
|
668
|
-
}
|
|
669
|
-
}
|
|
670
|
-
/**
|
|
671
|
-
* Reads a reelset CSV file and returns the reels as arrays of GameSymbols.
|
|
672
|
-
*/
|
|
673
|
-
parseReelsetCSV(reelSetPath, { config }) {
|
|
674
|
-
const csvData = import_fs2.default.readFileSync(reelSetPath, "utf8");
|
|
675
|
-
const rows = csvData.split("\n").filter((line) => line.trim() !== "");
|
|
676
|
-
const reels = Array.from(
|
|
677
|
-
{ length: config.gameModes[this.associatedGameModeName].reelsAmount },
|
|
678
|
-
() => []
|
|
679
|
-
);
|
|
680
|
-
rows.forEach((row) => {
|
|
681
|
-
const symsInRow = row.split(",").map((symbolId) => {
|
|
682
|
-
const symbol = config.symbols.get(symbolId.trim());
|
|
649
|
+
for (let row = 0; row < symbolsPerReel[ridx]; row++) {
|
|
650
|
+
const symbol = opts.reels[ridx][(reelPos + row) % opts.reels[ridx].length];
|
|
683
651
|
if (!symbol) {
|
|
684
|
-
throw new Error(`
|
|
652
|
+
throw new Error(`Failed to get symbol at pos ${reelPos + row} on reel ${ridx}`);
|
|
685
653
|
}
|
|
686
|
-
|
|
687
|
-
}
|
|
688
|
-
symsInRow.forEach((symbol, ridx) => {
|
|
689
|
-
reels[ridx].push(symbol);
|
|
690
|
-
});
|
|
691
|
-
});
|
|
692
|
-
const reelLengths = reels.map((r) => r.length);
|
|
693
|
-
const uniqueLengths = new Set(reelLengths);
|
|
694
|
-
if (uniqueLengths.size > 1) {
|
|
695
|
-
throw new Error(
|
|
696
|
-
`Inconsistent reel lengths in reelset CSV at ${reelSetPath}: ${[
|
|
697
|
-
...uniqueLengths
|
|
698
|
-
].join(", ")}`
|
|
699
|
-
);
|
|
654
|
+
this.reels[ridx][row] = symbol;
|
|
655
|
+
}
|
|
700
656
|
}
|
|
701
|
-
return reels;
|
|
702
|
-
}
|
|
703
|
-
};
|
|
704
|
-
|
|
705
|
-
// src/ResultSet.ts
|
|
706
|
-
var import_assert3 = __toESM(require("assert"));
|
|
707
|
-
var ResultSet = class {
|
|
708
|
-
criteria;
|
|
709
|
-
quota;
|
|
710
|
-
multiplier;
|
|
711
|
-
reelWeights;
|
|
712
|
-
userData;
|
|
713
|
-
forceMaxWin;
|
|
714
|
-
forceFreespins;
|
|
715
|
-
evaluate;
|
|
716
|
-
constructor(opts) {
|
|
717
|
-
this.criteria = opts.criteria;
|
|
718
|
-
this.quota = opts.quota;
|
|
719
|
-
this.multiplier = opts.multiplier;
|
|
720
|
-
this.reelWeights = opts.reelWeights;
|
|
721
|
-
this.userData = opts.userData;
|
|
722
|
-
this.forceMaxWin = opts.forceMaxWin;
|
|
723
|
-
this.forceFreespins = opts.forceFreespins;
|
|
724
|
-
this.evaluate = opts.evaluate;
|
|
725
657
|
}
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
const
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
if (simNums === void 0 || simNums <= 0) {
|
|
736
|
-
throw new Error(`No simulations configured for game mode "${gameModeName}".`);
|
|
658
|
+
tumbleBoard(opts) {
|
|
659
|
+
(0, import_assert3.default)(this.lastDrawnReelStops.length > 0, "Cannot tumble board before drawing it.");
|
|
660
|
+
const reelsAmount = opts.reelsAmount ?? opts.ctx.services.game.getCurrentGameMode().reelsAmount;
|
|
661
|
+
const symbolsPerReel = opts.symbolsPerReel ?? opts.ctx.services.game.getCurrentGameMode().symbolsPerReel;
|
|
662
|
+
const padSymbols = opts.padSymbols ?? opts.ctx.config.padSymbols;
|
|
663
|
+
if (!opts.ctx && !reelsAmount && !symbolsPerReel) {
|
|
664
|
+
throw new Error(
|
|
665
|
+
"If ctx is not provided, reelsAmount and symbolsPerReel must be given."
|
|
666
|
+
);
|
|
737
667
|
}
|
|
738
|
-
const
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
const
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
668
|
+
const reels = this.lastUsedReels;
|
|
669
|
+
opts.symbolsToDelete.forEach(({ reelIdx, rowIdx }) => {
|
|
670
|
+
this.reels[reelIdx].splice(rowIdx, 1);
|
|
671
|
+
});
|
|
672
|
+
const newFirstSymbolPositions = {};
|
|
673
|
+
for (let ridx = 0; ridx < reelsAmount; ridx++) {
|
|
674
|
+
while (this.reels[ridx].length < symbolsPerReel[ridx]) {
|
|
675
|
+
const padSymbol = this.paddingTop[ridx].pop();
|
|
676
|
+
if (padSymbol) {
|
|
677
|
+
this.reels[ridx].unshift(padSymbol);
|
|
678
|
+
} else {
|
|
679
|
+
break;
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
const previousStop = this.lastDrawnReelStops[ridx];
|
|
683
|
+
const stopBeforePad = previousStop - padSymbols - 1;
|
|
684
|
+
const symbolsNeeded = symbolsPerReel[ridx] - this.reels[ridx].length;
|
|
685
|
+
for (let s = 0; s < symbolsNeeded; s++) {
|
|
686
|
+
const symbolPos = (stopBeforePad - s + reels[ridx].length) % reels[ridx].length;
|
|
687
|
+
const newSymbol = reels[ridx][symbolPos];
|
|
688
|
+
(0, import_assert3.default)(newSymbol, "Failed to get new symbol for tumbling.");
|
|
689
|
+
this.reels[ridx].unshift(newSymbol);
|
|
690
|
+
newFirstSymbolPositions[ridx] = symbolPos;
|
|
759
691
|
}
|
|
760
|
-
totalSims = Object.values(numberOfSimsForCriteria).reduce(
|
|
761
|
-
(sum, num) => sum + num,
|
|
762
|
-
0
|
|
763
|
-
);
|
|
764
|
-
reduceSims = totalSims > simNums;
|
|
765
692
|
}
|
|
766
|
-
let
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
693
|
+
for (let ridx = 0; ridx < reelsAmount; ridx++) {
|
|
694
|
+
const firstSymbolPos = newFirstSymbolPositions[ridx];
|
|
695
|
+
for (let p = 1; p <= padSymbols; p++) {
|
|
696
|
+
const topPos = (firstSymbolPos - p + reels[ridx].length) % reels[ridx].length;
|
|
697
|
+
const padSymbol = reels[ridx][topPos];
|
|
698
|
+
(0, import_assert3.default)(padSymbol, "Failed to get new padding symbol for tumbling.");
|
|
699
|
+
this.paddingTop[ridx].unshift(padSymbol);
|
|
771
700
|
}
|
|
772
|
-
});
|
|
773
|
-
allCriteria = shuffle(allCriteria, rng);
|
|
774
|
-
for (let i = 1; i <= Math.min(simNums, allCriteria.length); i++) {
|
|
775
|
-
simNumsToCriteria[i] = allCriteria[i];
|
|
776
701
|
}
|
|
777
|
-
|
|
702
|
+
}
|
|
703
|
+
};
|
|
704
|
+
|
|
705
|
+
// src/service/board.ts
|
|
706
|
+
var BoardService = class extends AbstractService {
|
|
707
|
+
board;
|
|
708
|
+
constructor(ctx) {
|
|
709
|
+
super(ctx);
|
|
710
|
+
this.board = new Board();
|
|
778
711
|
}
|
|
779
712
|
/**
|
|
780
|
-
*
|
|
713
|
+
* Resets the board to an empty state.\
|
|
714
|
+
* This is called before drawing a new board.
|
|
781
715
|
*/
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
const multiplierMet = this.multiplier !== void 0 ? ctx.wallet.getCurrentWin() === this.multiplier && !this.forceMaxWin : ctx.wallet.getCurrentWin() > 0 && (!this.forceMaxWin || true);
|
|
786
|
-
const maxWinMet = this.forceMaxWin ? ctx.wallet.getCurrentWin() >= ctx.config.maxWinX : true;
|
|
787
|
-
const coreCriteriaMet = freespinsMet && multiplierMet && maxWinMet;
|
|
788
|
-
const finalResult = customEval !== void 0 ? coreCriteriaMet && customEval === true : coreCriteriaMet;
|
|
789
|
-
if (this.forceMaxWin && maxWinMet) {
|
|
790
|
-
ctx.record({
|
|
791
|
-
maxwin: true
|
|
792
|
-
});
|
|
793
|
-
}
|
|
794
|
-
return finalResult;
|
|
716
|
+
resetBoard() {
|
|
717
|
+
this.resetReels();
|
|
718
|
+
this.board.lastDrawnReelStops = [];
|
|
795
719
|
}
|
|
796
|
-
};
|
|
797
|
-
|
|
798
|
-
// src/Wallet.ts
|
|
799
|
-
var Wallet = class {
|
|
800
|
-
/**
|
|
801
|
-
* Total win amount (as the bet multiplier) from all simulations.
|
|
802
|
-
*/
|
|
803
|
-
cumulativeWins = 0;
|
|
804
|
-
/**
|
|
805
|
-
* Total win amount (as the bet multiplier) per spin type.
|
|
806
|
-
*
|
|
807
|
-
* @example
|
|
808
|
-
* ```ts
|
|
809
|
-
* {
|
|
810
|
-
* basegame: 50,
|
|
811
|
-
* freespins: 100,
|
|
812
|
-
* superfreespins: 200,
|
|
813
|
-
* }
|
|
814
|
-
* ```
|
|
815
|
-
*/
|
|
816
|
-
cumulativeWinsPerSpinType = {
|
|
817
|
-
[GameConfig.SPIN_TYPE.BASE_GAME]: 0,
|
|
818
|
-
[GameConfig.SPIN_TYPE.FREE_SPINS]: 0
|
|
819
|
-
};
|
|
820
|
-
/**
|
|
821
|
-
* Current win amount (as the bet multiplier) for the ongoing simulation.
|
|
822
|
-
*/
|
|
823
|
-
currentWin = 0;
|
|
824
720
|
/**
|
|
825
|
-
*
|
|
826
|
-
*
|
|
827
|
-
* @example
|
|
828
|
-
* ```ts
|
|
829
|
-
* {
|
|
830
|
-
* basegame: 50,
|
|
831
|
-
* freespins: 100,
|
|
832
|
-
* superfreespins: 200,
|
|
833
|
-
* }
|
|
834
|
-
* ```
|
|
721
|
+
* Gets the current reels and symbols on the board.
|
|
835
722
|
*/
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
723
|
+
getBoardReels() {
|
|
724
|
+
return this.board.reels;
|
|
725
|
+
}
|
|
726
|
+
getPaddingTop() {
|
|
727
|
+
return this.board.paddingTop;
|
|
728
|
+
}
|
|
729
|
+
getPaddingBottom() {
|
|
730
|
+
return this.board.paddingBottom;
|
|
731
|
+
}
|
|
732
|
+
getAnticipation() {
|
|
733
|
+
return this.board.anticipation;
|
|
734
|
+
}
|
|
735
|
+
resetReels() {
|
|
736
|
+
this.board.resetReels({
|
|
737
|
+
ctx: this.ctx()
|
|
738
|
+
});
|
|
739
|
+
}
|
|
840
740
|
/**
|
|
841
|
-
*
|
|
842
|
-
* After each spin, this amount is added to `currentWinPerSpinType` and then reset to zero.
|
|
741
|
+
* Sets the anticipation value for a specific reel.
|
|
843
742
|
*/
|
|
844
|
-
|
|
743
|
+
setAnticipationForReel(reelIndex, value) {
|
|
744
|
+
this.board.anticipation[reelIndex] = value;
|
|
745
|
+
}
|
|
845
746
|
/**
|
|
846
|
-
*
|
|
747
|
+
* Counts how many symbols matching the criteria are on a specific reel.
|
|
847
748
|
*/
|
|
848
|
-
|
|
849
|
-
|
|
749
|
+
countSymbolsOnReel(symbolOrProperties, reelIndex) {
|
|
750
|
+
return this.board.countSymbolsOnReel(symbolOrProperties, reelIndex);
|
|
850
751
|
}
|
|
851
752
|
/**
|
|
852
|
-
*
|
|
753
|
+
* Counts how many symbols matching the criteria are on the board.
|
|
853
754
|
*
|
|
854
|
-
*
|
|
855
|
-
* Or generally call this to add wins during a spin.
|
|
755
|
+
* Passing a GameSymbol will compare by ID, passing a properties object will compare by properties.
|
|
856
756
|
*
|
|
857
|
-
*
|
|
757
|
+
* Returns a tuple where the first element is the total count, and the second element is a record of counts per reel index.
|
|
858
758
|
*/
|
|
859
|
-
|
|
860
|
-
this.
|
|
759
|
+
countSymbolsOnBoard(symbolOrProperties) {
|
|
760
|
+
return this.board.countSymbolsOnBoard(symbolOrProperties);
|
|
861
761
|
}
|
|
862
762
|
/**
|
|
863
|
-
*
|
|
763
|
+
* Checks if a symbol appears more than once on any reel in the current reel set.
|
|
864
764
|
*
|
|
865
|
-
*
|
|
866
|
-
* and after a (free) spin is played out to finalize the win.
|
|
765
|
+
* Useful to check for "forbidden" generations, e.g. 2 scatters on one reel.
|
|
867
766
|
*/
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
throw new Error(`Spin type "${spinType}" does not exist in the wallet.`);
|
|
871
|
-
}
|
|
872
|
-
this.currentWinPerSpinType[spinType] += this.currentSpinWin;
|
|
873
|
-
this.currentWin += this.currentSpinWin;
|
|
874
|
-
this.currentSpinWin = 0;
|
|
767
|
+
isSymbolOnAnyReelMultipleTimes(symbol) {
|
|
768
|
+
return this.board.isSymbolOnAnyReelMultipleTimes(symbol);
|
|
875
769
|
}
|
|
876
770
|
/**
|
|
877
|
-
*
|
|
771
|
+
* Gets all reel stops (positions) where the specified symbol appears in the current reel set.\
|
|
772
|
+
* Returns an array of arrays, where each inner array contains the positions for the corresponding reel.
|
|
878
773
|
*/
|
|
879
|
-
|
|
880
|
-
return this.
|
|
774
|
+
getReelStopsForSymbol(reels, symbol) {
|
|
775
|
+
return this.board.getReelStopsForSymbol(reels, symbol);
|
|
881
776
|
}
|
|
882
777
|
/**
|
|
883
|
-
*
|
|
778
|
+
* Combines multiple arrays of reel stops into a single array of reel stops.\
|
|
884
779
|
*/
|
|
885
|
-
|
|
886
|
-
return this.
|
|
780
|
+
combineReelStops(...reelStops) {
|
|
781
|
+
return this.board.combineReelStops({
|
|
782
|
+
ctx: this.ctx(),
|
|
783
|
+
reelStops
|
|
784
|
+
});
|
|
887
785
|
}
|
|
888
786
|
/**
|
|
889
|
-
*
|
|
787
|
+
* From a list of reel stops on reels, selects a random stop for a speficied number of random symbols.
|
|
788
|
+
*
|
|
789
|
+
* Mostly useful for placing scatter symbols on the board.
|
|
890
790
|
*/
|
|
891
|
-
|
|
892
|
-
return this.
|
|
791
|
+
getRandomReelStops(reels, reelStops, amount) {
|
|
792
|
+
return this.board.getRandomReelStops({
|
|
793
|
+
ctx: this.ctx(),
|
|
794
|
+
reels,
|
|
795
|
+
reelStops,
|
|
796
|
+
amount
|
|
797
|
+
});
|
|
893
798
|
}
|
|
894
799
|
/**
|
|
895
|
-
*
|
|
800
|
+
* Selects a random reel set based on the configured weights of the current result set.\
|
|
801
|
+
* Returns the reels as arrays of GameSymbols.
|
|
896
802
|
*/
|
|
897
|
-
|
|
898
|
-
return this.
|
|
803
|
+
getRandomReelset() {
|
|
804
|
+
return this.board.getRandomReelset(this.ctx());
|
|
899
805
|
}
|
|
900
806
|
/**
|
|
901
|
-
*
|
|
902
|
-
*
|
|
903
|
-
* After each (free) spin, this amount should be added to `currentWinPerSpinType` via `confirmSpinWin()`
|
|
807
|
+
* Draws a board using specified reel stops.
|
|
904
808
|
*/
|
|
905
|
-
|
|
906
|
-
this.
|
|
907
|
-
this.addSpinWin(amount);
|
|
809
|
+
drawBoardWithForcedStops(reels, forcedStops) {
|
|
810
|
+
this.drawBoardMixed(reels, forcedStops);
|
|
908
811
|
}
|
|
909
812
|
/**
|
|
910
|
-
*
|
|
813
|
+
* Draws a board using random reel stops.
|
|
911
814
|
*/
|
|
912
|
-
|
|
913
|
-
this.
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
this.
|
|
918
|
-
|
|
815
|
+
drawBoardWithRandomStops(reels) {
|
|
816
|
+
this.drawBoardMixed(reels);
|
|
817
|
+
}
|
|
818
|
+
drawBoardMixed(reels, forcedStops) {
|
|
819
|
+
this.board.drawBoardMixed({
|
|
820
|
+
ctx: this.ctx(),
|
|
821
|
+
reels,
|
|
822
|
+
forcedStops
|
|
823
|
+
});
|
|
919
824
|
}
|
|
920
825
|
/**
|
|
921
|
-
*
|
|
826
|
+
* Tumbles the board. All given symbols will be deleted and new symbols will fall from the top.
|
|
922
827
|
*/
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
this.currentWin = process2(this.currentWin);
|
|
929
|
-
this.cumulativeWins += this.currentWin;
|
|
930
|
-
let spinTypeWins = 0;
|
|
931
|
-
for (const spinType of Object.keys(this.currentWinPerSpinType)) {
|
|
932
|
-
const st = spinType;
|
|
933
|
-
const spinTypeWin = process2(this.currentWinPerSpinType[st]);
|
|
934
|
-
this.cumulativeWinsPerSpinType[st] += spinTypeWin;
|
|
935
|
-
spinTypeWins += spinTypeWin;
|
|
936
|
-
}
|
|
937
|
-
if (process2(spinTypeWins) !== this.currentWin) {
|
|
938
|
-
throw new Error(
|
|
939
|
-
`Inconsistent wallet state: currentWin (${this.currentWin}) does not equal spinTypeWins (${spinTypeWins}).`
|
|
940
|
-
);
|
|
941
|
-
}
|
|
942
|
-
this.resetCurrentWin();
|
|
943
|
-
}
|
|
944
|
-
serialize() {
|
|
945
|
-
return {
|
|
946
|
-
cumulativeWins: this.cumulativeWins,
|
|
947
|
-
cumulativeWinsPerSpinType: this.cumulativeWinsPerSpinType,
|
|
948
|
-
currentWin: this.currentWin,
|
|
949
|
-
currentWinPerSpinType: this.currentWinPerSpinType,
|
|
950
|
-
currentSpinWin: this.currentSpinWin,
|
|
951
|
-
currentTumbleWin: this.currentTumbleWin
|
|
952
|
-
};
|
|
953
|
-
}
|
|
954
|
-
merge(wallet) {
|
|
955
|
-
this.cumulativeWins += wallet.getCumulativeWins();
|
|
956
|
-
const otherWinsPerSpinType = wallet.getCumulativeWinsPerSpinType();
|
|
957
|
-
for (const spinType of Object.keys(this.cumulativeWinsPerSpinType)) {
|
|
958
|
-
this.cumulativeWinsPerSpinType[spinType] += otherWinsPerSpinType[spinType] || 0;
|
|
959
|
-
}
|
|
960
|
-
}
|
|
961
|
-
mergeSerialized(data) {
|
|
962
|
-
this.cumulativeWins += data.cumulativeWins;
|
|
963
|
-
for (const spinType of Object.keys(this.cumulativeWinsPerSpinType)) {
|
|
964
|
-
this.cumulativeWinsPerSpinType[spinType] += data.cumulativeWinsPerSpinType[spinType] || 0;
|
|
965
|
-
}
|
|
966
|
-
this.currentWin += data.currentWin;
|
|
967
|
-
this.currentSpinWin += data.currentSpinWin;
|
|
968
|
-
this.currentTumbleWin += data.currentTumbleWin;
|
|
969
|
-
for (const spinType of Object.keys(this.currentWinPerSpinType)) {
|
|
970
|
-
this.currentWinPerSpinType[spinType] += data.currentWinPerSpinType[spinType] || 0;
|
|
971
|
-
}
|
|
828
|
+
tumbleBoard(symbolsToDelete) {
|
|
829
|
+
this.board.tumbleBoard({
|
|
830
|
+
ctx: this.ctx(),
|
|
831
|
+
symbolsToDelete
|
|
832
|
+
});
|
|
972
833
|
}
|
|
973
834
|
};
|
|
974
835
|
|
|
975
|
-
// src/
|
|
976
|
-
var
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
this.
|
|
836
|
+
// src/service/data.ts
|
|
837
|
+
var import_assert4 = __toESM(require("assert"));
|
|
838
|
+
var DataService = class extends AbstractService {
|
|
839
|
+
recorder;
|
|
840
|
+
book;
|
|
841
|
+
constructor(ctx) {
|
|
842
|
+
super(ctx);
|
|
843
|
+
}
|
|
844
|
+
ensureRecorder() {
|
|
845
|
+
(0, import_assert4.default)(this.recorder, "Recorder not set in DataService. Call setRecorder() first.");
|
|
846
|
+
}
|
|
847
|
+
ensureBook() {
|
|
848
|
+
(0, import_assert4.default)(this.book, "Book not set in DataService. Call setBook() first.");
|
|
985
849
|
}
|
|
986
850
|
/**
|
|
987
|
-
*
|
|
851
|
+
* Intended for internal use only.
|
|
988
852
|
*/
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
this.events.push({ index, ...event });
|
|
853
|
+
_setRecorder(recorder) {
|
|
854
|
+
this.recorder = recorder;
|
|
992
855
|
}
|
|
993
856
|
/**
|
|
994
|
-
*
|
|
857
|
+
* Intended for internal use only.
|
|
995
858
|
*/
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
return Math.round(Math.min(number, ctx.config.maxWinX) * 100) / 100;
|
|
999
|
-
}
|
|
1000
|
-
this.payout = Math.round(process2(ctx.wallet.getCurrentWin()) * 100);
|
|
1001
|
-
this.basegameWins = process2(
|
|
1002
|
-
ctx.wallet.getCurrentWinPerSpinType()[GameConfig.SPIN_TYPE.BASE_GAME] || 0
|
|
1003
|
-
);
|
|
1004
|
-
this.freespinsWins = process2(
|
|
1005
|
-
ctx.wallet.getCurrentWinPerSpinType()[GameConfig.SPIN_TYPE.FREE_SPINS] || 0
|
|
1006
|
-
);
|
|
1007
|
-
}
|
|
1008
|
-
getPayout() {
|
|
1009
|
-
return this.payout;
|
|
1010
|
-
}
|
|
1011
|
-
getBasegameWins() {
|
|
1012
|
-
return this.basegameWins;
|
|
1013
|
-
}
|
|
1014
|
-
getFreespinsWins() {
|
|
1015
|
-
return this.freespinsWins;
|
|
1016
|
-
}
|
|
1017
|
-
serialize() {
|
|
1018
|
-
return {
|
|
1019
|
-
id: this.id,
|
|
1020
|
-
criteria: this.criteria,
|
|
1021
|
-
events: this.events,
|
|
1022
|
-
payout: this.payout,
|
|
1023
|
-
basegameWins: this.basegameWins,
|
|
1024
|
-
freespinsWins: this.freespinsWins
|
|
1025
|
-
};
|
|
859
|
+
_getBook() {
|
|
860
|
+
return this.book;
|
|
1026
861
|
}
|
|
1027
|
-
static fromSerialized(data) {
|
|
1028
|
-
const book = new _Book({ id: data.id });
|
|
1029
|
-
book.criteria = data.criteria;
|
|
1030
|
-
book.events = data.events;
|
|
1031
|
-
book.payout = data.payout;
|
|
1032
|
-
book.basegameWins = data.basegameWins;
|
|
1033
|
-
book.freespinsWins = data.freespinsWins;
|
|
1034
|
-
return book;
|
|
1035
|
-
}
|
|
1036
|
-
};
|
|
1037
|
-
|
|
1038
|
-
// src/GameState.ts
|
|
1039
|
-
var GameState = class extends GameConfig {
|
|
1040
|
-
state;
|
|
1041
862
|
/**
|
|
1042
|
-
*
|
|
863
|
+
* Intended for internal use only.
|
|
1043
864
|
*/
|
|
1044
|
-
|
|
865
|
+
_setBook(book) {
|
|
866
|
+
this.book = book;
|
|
867
|
+
}
|
|
1045
868
|
/**
|
|
1046
|
-
*
|
|
869
|
+
* Intended for internal use only.
|
|
1047
870
|
*/
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
super(opts);
|
|
1051
|
-
this.state = {
|
|
1052
|
-
currentSpinType: GameConfig.SPIN_TYPE.BASE_GAME,
|
|
1053
|
-
library: /* @__PURE__ */ new Map(),
|
|
1054
|
-
book: new Book({ id: 0 }),
|
|
1055
|
-
currentGameMode: "N/A",
|
|
1056
|
-
currentSimulationId: 0,
|
|
1057
|
-
isCriteriaMet: false,
|
|
1058
|
-
currentFreespinAmount: 0,
|
|
1059
|
-
totalFreespinAmount: 0,
|
|
1060
|
-
rng: new RandomNumberGenerator(),
|
|
1061
|
-
userData: opts.userState || {},
|
|
1062
|
-
triggeredMaxWin: false,
|
|
1063
|
-
triggeredFreespins: false,
|
|
1064
|
-
// This is a placeholder ResultSet to avoid null checks elsewhere.
|
|
1065
|
-
currentResultSet: new ResultSet({
|
|
1066
|
-
criteria: "N/A",
|
|
1067
|
-
quota: 0,
|
|
1068
|
-
reelWeights: {
|
|
1069
|
-
[GameConfig.SPIN_TYPE.BASE_GAME]: {},
|
|
1070
|
-
[GameConfig.SPIN_TYPE.FREE_SPINS]: {}
|
|
1071
|
-
}
|
|
1072
|
-
})
|
|
1073
|
-
};
|
|
1074
|
-
this.wallet = new Wallet();
|
|
1075
|
-
this.recorder = {
|
|
1076
|
-
pendingRecords: [],
|
|
1077
|
-
records: []
|
|
1078
|
-
};
|
|
871
|
+
_getRecorder() {
|
|
872
|
+
return this.recorder;
|
|
1079
873
|
}
|
|
1080
874
|
/**
|
|
1081
|
-
*
|
|
875
|
+
* Intended for internal use only.
|
|
1082
876
|
*/
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
resetState() {
|
|
1087
|
-
this.state.rng.setSeedIfDifferent(this.state.currentSimulationId);
|
|
1088
|
-
this.state.book = new Book({ id: this.state.currentSimulationId });
|
|
1089
|
-
this.state.currentSpinType = GameConfig.SPIN_TYPE.BASE_GAME;
|
|
1090
|
-
this.state.currentFreespinAmount = 0;
|
|
1091
|
-
this.state.totalFreespinAmount = 0;
|
|
1092
|
-
this.state.triggeredMaxWin = false;
|
|
1093
|
-
this.state.triggeredFreespins = false;
|
|
1094
|
-
this.wallet.resetCurrentWin();
|
|
1095
|
-
this.clearPendingRecords();
|
|
1096
|
-
this.state.userData = this.config.userState || {};
|
|
877
|
+
_getRecords() {
|
|
878
|
+
this.ensureRecorder();
|
|
879
|
+
return this.recorder.records;
|
|
1097
880
|
}
|
|
1098
881
|
/**
|
|
1099
|
-
*
|
|
882
|
+
* Record data for statistical analysis.
|
|
1100
883
|
*/
|
|
1101
|
-
|
|
1102
|
-
this.
|
|
884
|
+
record(data) {
|
|
885
|
+
this.ensureRecorder();
|
|
886
|
+
this.recorder.pendingRecords.push({
|
|
887
|
+
bookId: this.ctx().state.currentSimulationId,
|
|
888
|
+
properties: Object.fromEntries(
|
|
889
|
+
Object.entries(data).map(([k, v]) => [k, String(v)])
|
|
890
|
+
)
|
|
891
|
+
});
|
|
1103
892
|
}
|
|
1104
893
|
/**
|
|
1105
|
-
*
|
|
894
|
+
* Records a symbol occurrence for statistical analysis.
|
|
895
|
+
*
|
|
896
|
+
* Calls `ctx.services.data.record()` with the provided data.
|
|
1106
897
|
*/
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
const search = Object.entries(pendingRecord.properties).map(([name, value]) => ({ name, value })).sort((a, b) => a.name.localeCompare(b.name));
|
|
1110
|
-
let record = this.recorder.records.find((r) => {
|
|
1111
|
-
if (r.search.length !== search.length) return false;
|
|
1112
|
-
for (let i = 0; i < r.search.length; i++) {
|
|
1113
|
-
if (r.search[i].name !== search[i].name) return false;
|
|
1114
|
-
if (r.search[i].value !== search[i].value) return false;
|
|
1115
|
-
}
|
|
1116
|
-
return true;
|
|
1117
|
-
});
|
|
1118
|
-
if (!record) {
|
|
1119
|
-
record = {
|
|
1120
|
-
search,
|
|
1121
|
-
timesTriggered: 0,
|
|
1122
|
-
bookIds: []
|
|
1123
|
-
};
|
|
1124
|
-
this.recorder.records.push(record);
|
|
1125
|
-
}
|
|
1126
|
-
record.timesTriggered++;
|
|
1127
|
-
if (!record.bookIds.includes(pendingRecord.bookId)) {
|
|
1128
|
-
record.bookIds.push(pendingRecord.bookId);
|
|
1129
|
-
}
|
|
1130
|
-
}
|
|
1131
|
-
this.clearPendingRecords();
|
|
898
|
+
recordSymbolOccurrence(data) {
|
|
899
|
+
this.record(data);
|
|
1132
900
|
}
|
|
1133
901
|
/**
|
|
1134
|
-
*
|
|
902
|
+
* Adds an event to the book.
|
|
1135
903
|
*/
|
|
1136
|
-
|
|
1137
|
-
this.
|
|
1138
|
-
|
|
1139
|
-
properties: Object.fromEntries(
|
|
1140
|
-
Object.entries(data).map(([k, v]) => [k, String(v)])
|
|
1141
|
-
)
|
|
1142
|
-
});
|
|
904
|
+
addBookEvent(event) {
|
|
905
|
+
this.ensureBook();
|
|
906
|
+
this.book.addEvent(event);
|
|
1143
907
|
}
|
|
1144
908
|
/**
|
|
1145
|
-
*
|
|
1146
|
-
*
|
|
1147
|
-
* Calls `this.record()` with the provided data.
|
|
909
|
+
* Intended for internal use only.
|
|
1148
910
|
*/
|
|
1149
|
-
|
|
1150
|
-
this.
|
|
911
|
+
_clearPendingRecords() {
|
|
912
|
+
this.ensureRecorder();
|
|
913
|
+
this.recorder.pendingRecords = [];
|
|
914
|
+
}
|
|
915
|
+
};
|
|
916
|
+
|
|
917
|
+
// src/service/game.ts
|
|
918
|
+
var GameService = class extends AbstractService {
|
|
919
|
+
constructor(ctx) {
|
|
920
|
+
super(ctx);
|
|
1151
921
|
}
|
|
1152
922
|
/**
|
|
1153
|
-
*
|
|
923
|
+
* Retrieves a reel set by its ID within a specific game mode.
|
|
1154
924
|
*/
|
|
1155
|
-
|
|
1156
|
-
|
|
925
|
+
getReelsetById(gameMode, id) {
|
|
926
|
+
const reelSet = this.ctx().config.gameModes[gameMode].reelSets.find(
|
|
927
|
+
(rs) => rs.id === id
|
|
928
|
+
);
|
|
929
|
+
if (!reelSet) {
|
|
930
|
+
throw new Error(
|
|
931
|
+
`Reel set with id "${id}" not found in game mode "${gameMode}". Available reel sets: ${this.ctx().config.gameModes[gameMode].reelSets.map((rs) => rs.id).join(", ")}`
|
|
932
|
+
);
|
|
933
|
+
}
|
|
934
|
+
return reelSet.reels;
|
|
1157
935
|
}
|
|
1158
936
|
/**
|
|
1159
|
-
*
|
|
937
|
+
* Retrieves the number of free spins awarded for a given spin type and scatter count.
|
|
1160
938
|
*/
|
|
1161
|
-
|
|
1162
|
-
this.
|
|
1163
|
-
|
|
939
|
+
getFreeSpinsForScatters(spinType, scatterCount) {
|
|
940
|
+
const freespinsConfig = this.ctx().config.scatterToFreespins[spinType];
|
|
941
|
+
if (!freespinsConfig) {
|
|
942
|
+
throw new Error(
|
|
943
|
+
`No free spins configuration found for spin type "${spinType}". Please check your game configuration.`
|
|
944
|
+
);
|
|
945
|
+
}
|
|
946
|
+
return freespinsConfig[scatterCount] || 0;
|
|
1164
947
|
}
|
|
1165
948
|
/**
|
|
1166
|
-
*
|
|
1167
|
-
*
|
|
1168
|
-
* Also sets `state.triggeredFreespins` to true.
|
|
949
|
+
* Retrieves a result set by its criteria within a specific game mode.
|
|
1169
950
|
*/
|
|
1170
|
-
|
|
1171
|
-
this.
|
|
1172
|
-
|
|
1173
|
-
|
|
951
|
+
getResultSetByCriteria(mode, criteria) {
|
|
952
|
+
const gameMode = this.ctx().config.gameModes[mode];
|
|
953
|
+
if (!gameMode) {
|
|
954
|
+
throw new Error(`Game mode "${mode}" not found in game config.`);
|
|
955
|
+
}
|
|
956
|
+
const resultSet = gameMode.resultSets.find((rs) => rs.criteria === criteria);
|
|
957
|
+
if (!resultSet) {
|
|
958
|
+
throw new Error(
|
|
959
|
+
`Criteria "${criteria}" not found in game mode "${mode}". Available criteria: ${gameMode.resultSets.map((rs) => rs.criteria).join(", ")}`
|
|
960
|
+
);
|
|
961
|
+
}
|
|
962
|
+
return resultSet;
|
|
963
|
+
}
|
|
964
|
+
/**
|
|
965
|
+
* Returns all configured symbols as an array.
|
|
966
|
+
*/
|
|
967
|
+
getSymbolArray() {
|
|
968
|
+
return Array.from(this.ctx().config.symbols).map(([n, v]) => v);
|
|
969
|
+
}
|
|
970
|
+
/**
|
|
971
|
+
* Gets the configuration for the current game mode.
|
|
972
|
+
*/
|
|
973
|
+
getCurrentGameMode() {
|
|
974
|
+
return this.ctx().config.gameModes[this.ctx().state.currentGameMode];
|
|
1174
975
|
}
|
|
1175
976
|
/**
|
|
1176
977
|
* Ensures the requested number of scatters is valid based on the game configuration.\
|
|
1177
978
|
* Returns a valid number of scatters.
|
|
1178
979
|
*/
|
|
1179
980
|
verifyScatterCount(numScatters) {
|
|
1180
|
-
const scatterCounts = this.config.scatterToFreespins[this.state.currentSpinType];
|
|
981
|
+
const scatterCounts = this.ctx().config.scatterToFreespins[this.ctx().state.currentSpinType];
|
|
1181
982
|
if (!scatterCounts) {
|
|
1182
983
|
throw new Error(
|
|
1183
|
-
`No scatter counts defined for spin type "${this.state.currentSpinType}". Please check your game configuration.`
|
|
984
|
+
`No scatter counts defined for spin type "${this.ctx().state.currentSpinType}". Please check your game configuration.`
|
|
1184
985
|
);
|
|
1185
986
|
}
|
|
1186
987
|
const validCounts = Object.keys(scatterCounts).map((key) => parseInt(key, 10));
|
|
1187
988
|
if (validCounts.length === 0) {
|
|
1188
989
|
throw new Error(
|
|
1189
|
-
`No scatter counts defined for spin type "${this.state.currentSpinType}". Please check your game configuration.`
|
|
990
|
+
`No scatter counts defined for spin type "${this.ctx().state.currentSpinType}". Please check your game configuration.`
|
|
1190
991
|
);
|
|
1191
992
|
}
|
|
1192
993
|
if (numScatters < Math.min(...validCounts)) {
|
|
@@ -1197,766 +998,442 @@ var GameState = class extends GameConfig {
|
|
|
1197
998
|
}
|
|
1198
999
|
return numScatters;
|
|
1199
1000
|
}
|
|
1001
|
+
/**
|
|
1002
|
+
* Increases the freespin count by the specified amount.
|
|
1003
|
+
*
|
|
1004
|
+
* Also sets `state.triggeredFreespins` to true.
|
|
1005
|
+
*/
|
|
1006
|
+
awardFreespins(amount) {
|
|
1007
|
+
this.ctx().state.currentFreespinAmount += amount;
|
|
1008
|
+
this.ctx().state.totalFreespinAmount += amount;
|
|
1009
|
+
this.ctx().state.triggeredFreespins = true;
|
|
1010
|
+
}
|
|
1200
1011
|
};
|
|
1201
1012
|
|
|
1202
|
-
// src/
|
|
1203
|
-
var
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
this.
|
|
1211
|
-
this.paddingTop = [];
|
|
1212
|
-
this.paddingBottom = [];
|
|
1213
|
-
this.anticipation = [];
|
|
1214
|
-
this.ctx = opts.ctx;
|
|
1013
|
+
// src/service/wallet.ts
|
|
1014
|
+
var import_assert5 = __toESM(require("assert"));
|
|
1015
|
+
var WalletService = class extends AbstractService {
|
|
1016
|
+
wallet;
|
|
1017
|
+
constructor(ctx) {
|
|
1018
|
+
super(ctx);
|
|
1019
|
+
}
|
|
1020
|
+
ensureWallet() {
|
|
1021
|
+
(0, import_assert5.default)(this.wallet, "Wallet not set in WalletService. Call setWallet() first.");
|
|
1215
1022
|
}
|
|
1216
1023
|
/**
|
|
1217
|
-
*
|
|
1024
|
+
* Intended for internal use only.
|
|
1218
1025
|
*/
|
|
1219
|
-
|
|
1220
|
-
this.
|
|
1026
|
+
_getWallet() {
|
|
1027
|
+
this.ensureWallet();
|
|
1028
|
+
return this.wallet;
|
|
1221
1029
|
}
|
|
1222
1030
|
/**
|
|
1223
|
-
*
|
|
1224
|
-
* This is called before drawing a new board.
|
|
1031
|
+
* Intended for internal use only.
|
|
1225
1032
|
*/
|
|
1226
|
-
|
|
1227
|
-
this.
|
|
1228
|
-
}
|
|
1229
|
-
makeEmptyReels() {
|
|
1230
|
-
return Array.from({ length: this.ctx.getCurrentGameMode().reelsAmount }, () => []);
|
|
1231
|
-
}
|
|
1232
|
-
resetReels() {
|
|
1233
|
-
this.reels = this.makeEmptyReels();
|
|
1234
|
-
this.anticipation = Array.from(
|
|
1235
|
-
{ length: this.ctx.getCurrentGameMode().reelsAmount },
|
|
1236
|
-
() => 0
|
|
1237
|
-
);
|
|
1238
|
-
if (this.ctx.config.padSymbols && this.ctx.config.padSymbols > 0) {
|
|
1239
|
-
this.paddingTop = this.makeEmptyReels();
|
|
1240
|
-
this.paddingBottom = this.makeEmptyReels();
|
|
1241
|
-
}
|
|
1033
|
+
_setWallet(wallet) {
|
|
1034
|
+
this.wallet = wallet;
|
|
1242
1035
|
}
|
|
1243
1036
|
/**
|
|
1244
|
-
*
|
|
1037
|
+
* Adds the given amount to the wallet state.
|
|
1038
|
+
*
|
|
1039
|
+
* After calculating the win for a board, call this method to update the wallet state.\
|
|
1040
|
+
* If your game has tumbling mechanics, you should call this method again after every new tumble and win calculation.
|
|
1245
1041
|
*/
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
let matches = true;
|
|
1250
|
-
if (symbolOrProperties instanceof GameSymbol) {
|
|
1251
|
-
if (symbol.id !== symbolOrProperties.id) matches = false;
|
|
1252
|
-
} else {
|
|
1253
|
-
for (const [key, value] of Object.entries(symbolOrProperties)) {
|
|
1254
|
-
if (!symbol.properties.has(key) || symbol.properties.get(key) !== value) {
|
|
1255
|
-
matches = false;
|
|
1256
|
-
break;
|
|
1257
|
-
}
|
|
1258
|
-
}
|
|
1259
|
-
}
|
|
1260
|
-
if (matches) {
|
|
1261
|
-
total++;
|
|
1262
|
-
}
|
|
1263
|
-
}
|
|
1264
|
-
return total;
|
|
1042
|
+
addSpinWin(amount) {
|
|
1043
|
+
this.ensureWallet();
|
|
1044
|
+
this.wallet.addSpinWin(amount);
|
|
1265
1045
|
}
|
|
1266
1046
|
/**
|
|
1267
|
-
*
|
|
1047
|
+
* Helps to add tumble wins to the wallet state.
|
|
1268
1048
|
*
|
|
1269
|
-
*
|
|
1270
|
-
*
|
|
1271
|
-
* Returns a tuple where the first element is the total count, and the second element is a record of counts per reel index.
|
|
1049
|
+
* This also calls `addSpinWin()` internally, to add the tumble win to the overall spin win.
|
|
1272
1050
|
*/
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
for (const [ridx, reel] of this.reels.entries()) {
|
|
1277
|
-
for (const symbol of reel) {
|
|
1278
|
-
let matches = true;
|
|
1279
|
-
if (symbolOrProperties instanceof GameSymbol) {
|
|
1280
|
-
if (symbol.id !== symbolOrProperties.id) matches = false;
|
|
1281
|
-
} else {
|
|
1282
|
-
for (const [key, value] of Object.entries(symbolOrProperties)) {
|
|
1283
|
-
if (!symbol.properties.has(key) || symbol.properties.get(key) !== value) {
|
|
1284
|
-
matches = false;
|
|
1285
|
-
break;
|
|
1286
|
-
}
|
|
1287
|
-
}
|
|
1288
|
-
}
|
|
1289
|
-
if (matches) {
|
|
1290
|
-
total++;
|
|
1291
|
-
if (onReel[ridx] === void 0) {
|
|
1292
|
-
onReel[ridx] = 1;
|
|
1293
|
-
} else {
|
|
1294
|
-
onReel[ridx]++;
|
|
1295
|
-
}
|
|
1296
|
-
}
|
|
1297
|
-
}
|
|
1298
|
-
}
|
|
1299
|
-
return [total, onReel];
|
|
1051
|
+
addTumbleWin(amount) {
|
|
1052
|
+
this.ensureWallet();
|
|
1053
|
+
this.wallet.addTumbleWin(amount);
|
|
1300
1054
|
}
|
|
1301
1055
|
/**
|
|
1302
|
-
*
|
|
1056
|
+
* Confirms the wins of the current spin.
|
|
1303
1057
|
*
|
|
1304
|
-
*
|
|
1058
|
+
* Should be called after `addSpinWin()`, and after your tumble events are played out,\
|
|
1059
|
+
* and after a (free) spin is played out to finalize the win.
|
|
1305
1060
|
*/
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
for (const sym of reel) {
|
|
1310
|
-
if (sym.id === symbol.id) {
|
|
1311
|
-
count++;
|
|
1312
|
-
}
|
|
1313
|
-
if (count > 1) {
|
|
1314
|
-
return true;
|
|
1315
|
-
}
|
|
1316
|
-
}
|
|
1317
|
-
}
|
|
1318
|
-
return false;
|
|
1061
|
+
confirmSpinWin() {
|
|
1062
|
+
this.ensureWallet();
|
|
1063
|
+
this.wallet.confirmSpinWin(this.ctx().state.currentSpinType);
|
|
1319
1064
|
}
|
|
1320
1065
|
/**
|
|
1321
|
-
*
|
|
1066
|
+
* Gets the total win amount of the current simulation.
|
|
1322
1067
|
*/
|
|
1323
|
-
|
|
1324
|
-
this.
|
|
1068
|
+
getCurrentWin() {
|
|
1069
|
+
this.ensureWallet();
|
|
1070
|
+
return this.wallet.getCurrentWin();
|
|
1325
1071
|
}
|
|
1326
1072
|
/**
|
|
1327
|
-
*
|
|
1073
|
+
* Gets the current spin win amount of the ongoing spin.
|
|
1328
1074
|
*/
|
|
1329
|
-
|
|
1330
|
-
this.
|
|
1075
|
+
getCurrentSpinWin() {
|
|
1076
|
+
this.ensureWallet();
|
|
1077
|
+
return this.wallet.getCurrentSpinWin();
|
|
1331
1078
|
}
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
);
|
|
1338
|
-
if (forcedStops) {
|
|
1339
|
-
for (const [r, stopPos] of Object.entries(forcedStops)) {
|
|
1340
|
-
const reelIdx = Number(r);
|
|
1341
|
-
const symCount = this.ctx.getCurrentGameMode().symbolsPerReel[reelIdx];
|
|
1342
|
-
finalReelStops[reelIdx] = stopPos - Math.round(this.ctx.state.rng.randomFloat(0, symCount - 1));
|
|
1343
|
-
}
|
|
1344
|
-
}
|
|
1345
|
-
for (let i = 0; i < finalReelStops.length; i++) {
|
|
1346
|
-
if (finalReelStops[i] === null) {
|
|
1347
|
-
finalReelStops[i] = Math.floor(
|
|
1348
|
-
this.ctx.state.rng.randomFloat(0, reels[i].length - 1)
|
|
1349
|
-
);
|
|
1350
|
-
}
|
|
1351
|
-
}
|
|
1352
|
-
for (let ridx = 0; ridx < this.ctx.getCurrentGameMode().reelsAmount; ridx++) {
|
|
1353
|
-
const reelPos = finalReelStops[ridx];
|
|
1354
|
-
if (this.ctx.config.padSymbols && this.ctx.config.padSymbols > 0) {
|
|
1355
|
-
for (let p = this.ctx.config.padSymbols - 1; p >= 0; p--) {
|
|
1356
|
-
const topPos = (reelPos - (p + 1)) % reels[ridx].length;
|
|
1357
|
-
this.paddingTop[ridx].push(reels[ridx][topPos]);
|
|
1358
|
-
const bottomPos = (reelPos + this.ctx.getCurrentGameMode().symbolsPerReel[ridx] + p) % reels[ridx].length;
|
|
1359
|
-
this.paddingBottom[ridx].unshift(reels[ridx][bottomPos]);
|
|
1360
|
-
}
|
|
1361
|
-
}
|
|
1362
|
-
for (let row = 0; row < this.ctx.getCurrentGameMode().symbolsPerReel[ridx]; row++) {
|
|
1363
|
-
const symbol = reels[ridx][(reelPos + row) % reels[ridx].length];
|
|
1364
|
-
if (!symbol) {
|
|
1365
|
-
throw new Error(`Failed to get symbol at pos ${reelPos + row} on reel ${ridx}`);
|
|
1366
|
-
}
|
|
1367
|
-
this.reels[ridx][row] = symbol;
|
|
1368
|
-
}
|
|
1369
|
-
}
|
|
1079
|
+
/**
|
|
1080
|
+
* Gets the current tumble win amount of the ongoing spin.
|
|
1081
|
+
*/
|
|
1082
|
+
getCurrentTumbleWin() {
|
|
1083
|
+
this.ensureWallet();
|
|
1084
|
+
return this.wallet.getCurrentTumbleWin();
|
|
1370
1085
|
}
|
|
1371
1086
|
};
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1087
|
+
|
|
1088
|
+
// src/game-context/index.ts
|
|
1089
|
+
function createGameContext(opts) {
|
|
1090
|
+
const context = {
|
|
1091
|
+
config: opts.config,
|
|
1092
|
+
state: createGameState(opts.state),
|
|
1093
|
+
services: {}
|
|
1094
|
+
};
|
|
1095
|
+
const getContext = () => context;
|
|
1096
|
+
function createServices() {
|
|
1097
|
+
return {
|
|
1098
|
+
game: new GameService(getContext),
|
|
1099
|
+
data: new DataService(getContext),
|
|
1100
|
+
board: new BoardService(getContext),
|
|
1101
|
+
wallet: new WalletService(getContext),
|
|
1102
|
+
rng: new RngService(getContext),
|
|
1103
|
+
...opts.services
|
|
1381
1104
|
};
|
|
1382
1105
|
}
|
|
1106
|
+
context.services = createServices();
|
|
1107
|
+
return context;
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
// src/book/index.ts
|
|
1111
|
+
var Book = class _Book {
|
|
1112
|
+
id;
|
|
1113
|
+
criteria = "N/A";
|
|
1114
|
+
events = [];
|
|
1115
|
+
payout = 0;
|
|
1116
|
+
basegameWins = 0;
|
|
1117
|
+
freespinsWins = 0;
|
|
1118
|
+
constructor(opts) {
|
|
1119
|
+
this.id = opts.id;
|
|
1120
|
+
}
|
|
1383
1121
|
/**
|
|
1384
|
-
*
|
|
1385
|
-
* This is called before drawing a new board.
|
|
1122
|
+
* Intended for internal use only.
|
|
1386
1123
|
*/
|
|
1387
|
-
|
|
1388
|
-
this.
|
|
1124
|
+
setCriteria(criteria) {
|
|
1125
|
+
this.criteria = criteria;
|
|
1389
1126
|
}
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
this.
|
|
1395
|
-
this.
|
|
1396
|
-
{ length: this.getCurrentGameMode().reelsAmount },
|
|
1397
|
-
() => 0
|
|
1398
|
-
);
|
|
1399
|
-
if (this.config.padSymbols && this.config.padSymbols > 0) {
|
|
1400
|
-
this.board.paddingTop = this.makeEmptyReels();
|
|
1401
|
-
this.board.paddingBottom = this.makeEmptyReels();
|
|
1402
|
-
}
|
|
1127
|
+
/**
|
|
1128
|
+
* Adds an event to the book.
|
|
1129
|
+
*/
|
|
1130
|
+
addEvent(event) {
|
|
1131
|
+
const index = this.events.length + 1;
|
|
1132
|
+
this.events.push({ index, ...event });
|
|
1403
1133
|
}
|
|
1404
1134
|
/**
|
|
1405
|
-
*
|
|
1135
|
+
* Intended for internal use only.
|
|
1406
1136
|
*/
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
matches = false;
|
|
1417
|
-
break;
|
|
1418
|
-
}
|
|
1419
|
-
}
|
|
1420
|
-
}
|
|
1421
|
-
if (matches) {
|
|
1422
|
-
total++;
|
|
1423
|
-
}
|
|
1424
|
-
}
|
|
1425
|
-
return total;
|
|
1137
|
+
serialize() {
|
|
1138
|
+
return {
|
|
1139
|
+
id: this.id,
|
|
1140
|
+
criteria: this.criteria,
|
|
1141
|
+
events: this.events,
|
|
1142
|
+
payout: this.payout,
|
|
1143
|
+
basegameWins: this.basegameWins,
|
|
1144
|
+
freespinsWins: this.freespinsWins
|
|
1145
|
+
};
|
|
1426
1146
|
}
|
|
1427
1147
|
/**
|
|
1428
|
-
*
|
|
1429
|
-
*
|
|
1430
|
-
* Passing a GameSymbol will compare by ID, passing a properties object will compare by properties.
|
|
1431
|
-
*
|
|
1432
|
-
* Returns a tuple where the first element is the total count, and the second element is a record of counts per reel index.
|
|
1148
|
+
* Intended for internal use only.
|
|
1433
1149
|
*/
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
total++;
|
|
1452
|
-
if (onReel[ridx] === void 0) {
|
|
1453
|
-
onReel[ridx] = 1;
|
|
1454
|
-
} else {
|
|
1455
|
-
onReel[ridx]++;
|
|
1456
|
-
}
|
|
1457
|
-
}
|
|
1458
|
-
}
|
|
1459
|
-
}
|
|
1460
|
-
return [total, onReel];
|
|
1150
|
+
static fromSerialized(data) {
|
|
1151
|
+
const book = new _Book({ id: data.id, criteria: data.criteria });
|
|
1152
|
+
book.events = data.events;
|
|
1153
|
+
book.payout = data.payout;
|
|
1154
|
+
book.basegameWins = data.basegameWins;
|
|
1155
|
+
book.freespinsWins = data.freespinsWins;
|
|
1156
|
+
return book;
|
|
1157
|
+
}
|
|
1158
|
+
};
|
|
1159
|
+
|
|
1160
|
+
// src/recorder/index.ts
|
|
1161
|
+
var Recorder = class {
|
|
1162
|
+
records;
|
|
1163
|
+
pendingRecords;
|
|
1164
|
+
constructor() {
|
|
1165
|
+
this.records = [];
|
|
1166
|
+
this.pendingRecords = [];
|
|
1461
1167
|
}
|
|
1168
|
+
};
|
|
1169
|
+
|
|
1170
|
+
// src/wallet/index.ts
|
|
1171
|
+
var Wallet = class {
|
|
1462
1172
|
/**
|
|
1463
|
-
*
|
|
1173
|
+
* Total win amount (as the bet multiplier) from all simulations.
|
|
1174
|
+
*/
|
|
1175
|
+
cumulativeWins = 0;
|
|
1176
|
+
/**
|
|
1177
|
+
* Total win amount (as the bet multiplier) per spin type.
|
|
1464
1178
|
*
|
|
1465
|
-
*
|
|
1179
|
+
* @example
|
|
1180
|
+
* ```ts
|
|
1181
|
+
* {
|
|
1182
|
+
* basegame: 50,
|
|
1183
|
+
* freespins: 100,
|
|
1184
|
+
* superfreespins: 200,
|
|
1185
|
+
* }
|
|
1186
|
+
* ```
|
|
1466
1187
|
*/
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
if (sym.id === symbol.id) {
|
|
1472
|
-
count++;
|
|
1473
|
-
}
|
|
1474
|
-
if (count > 1) {
|
|
1475
|
-
return true;
|
|
1476
|
-
}
|
|
1477
|
-
}
|
|
1478
|
-
}
|
|
1479
|
-
return false;
|
|
1480
|
-
}
|
|
1188
|
+
cumulativeWinsPerSpinType = {
|
|
1189
|
+
[SPIN_TYPE.BASE_GAME]: 0,
|
|
1190
|
+
[SPIN_TYPE.FREE_SPINS]: 0
|
|
1191
|
+
};
|
|
1481
1192
|
/**
|
|
1482
|
-
*
|
|
1483
|
-
* Returns an array of arrays, where each inner array contains the positions for the corresponding reel.
|
|
1193
|
+
* Current win amount (as the bet multiplier) for the ongoing simulation.
|
|
1484
1194
|
*/
|
|
1485
|
-
|
|
1486
|
-
const reelStops = [];
|
|
1487
|
-
for (let ridx = 0; ridx < reels.length; ridx++) {
|
|
1488
|
-
const reel = reels[ridx];
|
|
1489
|
-
const positions = [];
|
|
1490
|
-
for (let pos = 0; pos < reel.length; pos++) {
|
|
1491
|
-
if (reel[pos].id === symbol.id) {
|
|
1492
|
-
positions.push(pos);
|
|
1493
|
-
}
|
|
1494
|
-
}
|
|
1495
|
-
reelStops.push(positions);
|
|
1496
|
-
}
|
|
1497
|
-
return reelStops;
|
|
1498
|
-
}
|
|
1195
|
+
currentWin = 0;
|
|
1499
1196
|
/**
|
|
1500
|
-
*
|
|
1197
|
+
* Current win amount (as the bet multiplier) for the ongoing simulation per spin type.
|
|
1198
|
+
*
|
|
1199
|
+
* @example
|
|
1200
|
+
* ```ts
|
|
1201
|
+
* {
|
|
1202
|
+
* basegame: 50,
|
|
1203
|
+
* freespins: 100,
|
|
1204
|
+
* superfreespins: 200,
|
|
1205
|
+
* }
|
|
1206
|
+
* ```
|
|
1501
1207
|
*/
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1208
|
+
currentWinPerSpinType = {
|
|
1209
|
+
[SPIN_TYPE.BASE_GAME]: 0,
|
|
1210
|
+
[SPIN_TYPE.FREE_SPINS]: 0
|
|
1211
|
+
};
|
|
1212
|
+
/**
|
|
1213
|
+
* Holds the current win amount for a single (free) spin.\
|
|
1214
|
+
* After each spin, this amount is added to `currentWinPerSpinType` and then reset to zero.
|
|
1215
|
+
*/
|
|
1216
|
+
currentSpinWin = 0;
|
|
1217
|
+
/**
|
|
1218
|
+
* Current win amount (as the bet multiplier) for the ongoing tumble sequence.
|
|
1219
|
+
*/
|
|
1220
|
+
currentTumbleWin = 0;
|
|
1221
|
+
constructor() {
|
|
1511
1222
|
}
|
|
1512
1223
|
/**
|
|
1513
|
-
*
|
|
1224
|
+
* Updates the win for the current spin.
|
|
1514
1225
|
*
|
|
1515
|
-
*
|
|
1226
|
+
* Should be called after each tumble event, if applicable.\
|
|
1227
|
+
* Or generally call this to add wins during a spin.
|
|
1228
|
+
*
|
|
1229
|
+
* After each (free) spin, this amount should be added to `currentWinPerSpinType` via `confirmSpinWin()`
|
|
1516
1230
|
*/
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
const symProbsOnReels = [];
|
|
1520
|
-
const stopPositionsForReels = {};
|
|
1521
|
-
for (let ridx = 0; ridx < reelsAmount; ridx++) {
|
|
1522
|
-
symProbsOnReels.push(reelStops[ridx].length / reels[ridx].length);
|
|
1523
|
-
}
|
|
1524
|
-
while (Object.keys(stopPositionsForReels).length !== amount) {
|
|
1525
|
-
const possibleReels = [];
|
|
1526
|
-
for (let i = 0; i < reelsAmount; i++) {
|
|
1527
|
-
if (symProbsOnReels[i] > 0) {
|
|
1528
|
-
possibleReels.push(i);
|
|
1529
|
-
}
|
|
1530
|
-
}
|
|
1531
|
-
const possibleProbs = symProbsOnReels.filter((p) => p > 0);
|
|
1532
|
-
const weights = Object.fromEntries(
|
|
1533
|
-
possibleReels.map((ridx, idx) => [ridx, possibleProbs[idx]])
|
|
1534
|
-
);
|
|
1535
|
-
const chosenReel = weightedRandom(weights, this.state.rng);
|
|
1536
|
-
const chosenStop = randomItem(reelStops[Number(chosenReel)], this.state.rng);
|
|
1537
|
-
symProbsOnReels[Number(chosenReel)] = 0;
|
|
1538
|
-
stopPositionsForReels[chosenReel] = chosenStop;
|
|
1539
|
-
}
|
|
1540
|
-
return stopPositionsForReels;
|
|
1231
|
+
addSpinWin(amount) {
|
|
1232
|
+
this.currentSpinWin += amount;
|
|
1541
1233
|
}
|
|
1542
1234
|
/**
|
|
1543
|
-
*
|
|
1544
|
-
*
|
|
1235
|
+
* Confirms the wins of the current spin.
|
|
1236
|
+
*
|
|
1237
|
+
* Should be called after `addSpinWin()`, and after your tumble events are played out,\
|
|
1238
|
+
* and after a (free) spin is played out to finalize the win.
|
|
1545
1239
|
*/
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
this
|
|
1550
|
-
);
|
|
1551
|
-
let reelSetId = "";
|
|
1552
|
-
if (evalWeights) {
|
|
1553
|
-
reelSetId = weightedRandom(evalWeights, this.state.rng);
|
|
1554
|
-
} else {
|
|
1555
|
-
reelSetId = weightedRandom(weights[this.state.currentSpinType], this.state.rng);
|
|
1240
|
+
confirmSpinWin(spinType) {
|
|
1241
|
+
if (!Object.keys(this.currentWinPerSpinType).includes(spinType)) {
|
|
1242
|
+
throw new Error(`Spin type "${spinType}" does not exist in the wallet.`);
|
|
1556
1243
|
}
|
|
1557
|
-
|
|
1558
|
-
|
|
1244
|
+
this.currentWinPerSpinType[spinType] += this.currentSpinWin;
|
|
1245
|
+
this.currentWin += this.currentSpinWin;
|
|
1246
|
+
this.currentSpinWin = 0;
|
|
1247
|
+
this.currentTumbleWin = 0;
|
|
1559
1248
|
}
|
|
1560
1249
|
/**
|
|
1561
|
-
*
|
|
1250
|
+
* Returns the accumulated win amount (as the bet multiplier) from all simulations.
|
|
1562
1251
|
*/
|
|
1563
|
-
|
|
1564
|
-
this.
|
|
1252
|
+
getCumulativeWins() {
|
|
1253
|
+
return this.cumulativeWins;
|
|
1565
1254
|
}
|
|
1566
1255
|
/**
|
|
1567
|
-
*
|
|
1256
|
+
* Returns the accumulated win amount (as the bet multiplier) per spin type from all simulations.
|
|
1568
1257
|
*/
|
|
1569
|
-
|
|
1570
|
-
this.
|
|
1571
|
-
}
|
|
1572
|
-
drawBoardMixed(reels, forcedStops) {
|
|
1573
|
-
this.resetReels();
|
|
1574
|
-
const finalReelStops = Array.from(
|
|
1575
|
-
{ length: this.getCurrentGameMode().reelsAmount },
|
|
1576
|
-
() => null
|
|
1577
|
-
);
|
|
1578
|
-
if (forcedStops) {
|
|
1579
|
-
for (const [r, stopPos] of Object.entries(forcedStops)) {
|
|
1580
|
-
const reelIdx = Number(r);
|
|
1581
|
-
const symCount = this.getCurrentGameMode().symbolsPerReel[reelIdx];
|
|
1582
|
-
finalReelStops[reelIdx] = stopPos - Math.round(this.state.rng.randomFloat(0, symCount - 1));
|
|
1583
|
-
if (finalReelStops[reelIdx] < 0) {
|
|
1584
|
-
finalReelStops[reelIdx] = reels[reelIdx].length + finalReelStops[reelIdx];
|
|
1585
|
-
}
|
|
1586
|
-
}
|
|
1587
|
-
}
|
|
1588
|
-
for (let i = 0; i < finalReelStops.length; i++) {
|
|
1589
|
-
if (finalReelStops[i] === null) {
|
|
1590
|
-
finalReelStops[i] = Math.floor(
|
|
1591
|
-
this.state.rng.randomFloat(0, reels[i].length - 1)
|
|
1592
|
-
);
|
|
1593
|
-
}
|
|
1594
|
-
}
|
|
1595
|
-
for (let ridx = 0; ridx < this.getCurrentGameMode().reelsAmount; ridx++) {
|
|
1596
|
-
const reelPos = finalReelStops[ridx];
|
|
1597
|
-
if (this.config.padSymbols && this.config.padSymbols > 0) {
|
|
1598
|
-
for (let p = this.config.padSymbols - 1; p >= 0; p--) {
|
|
1599
|
-
const topPos = (reelPos - (p + 1)) % reels[ridx].length;
|
|
1600
|
-
this.board.paddingTop[ridx].push(reels[ridx][topPos]);
|
|
1601
|
-
const bottomPos = (reelPos + this.getCurrentGameMode().symbolsPerReel[ridx] + p) % reels[ridx].length;
|
|
1602
|
-
this.board.paddingBottom[ridx].unshift(reels[ridx][bottomPos]);
|
|
1603
|
-
}
|
|
1604
|
-
}
|
|
1605
|
-
for (let row = 0; row < this.getCurrentGameMode().symbolsPerReel[ridx]; row++) {
|
|
1606
|
-
const symbol = reels[ridx][(reelPos + row) % reels[ridx].length];
|
|
1607
|
-
if (!symbol) {
|
|
1608
|
-
throw new Error(`Failed to get symbol at pos ${reelPos + row} on reel ${ridx}`);
|
|
1609
|
-
}
|
|
1610
|
-
this.board.reels[ridx][row] = symbol;
|
|
1611
|
-
}
|
|
1612
|
-
}
|
|
1613
|
-
}
|
|
1614
|
-
};
|
|
1615
|
-
|
|
1616
|
-
// src/WinType.ts
|
|
1617
|
-
var WinType = class {
|
|
1618
|
-
payout;
|
|
1619
|
-
winCombinations;
|
|
1620
|
-
ctx;
|
|
1621
|
-
wildSymbol;
|
|
1622
|
-
constructor(opts) {
|
|
1623
|
-
this.payout = 0;
|
|
1624
|
-
this.winCombinations = [];
|
|
1625
|
-
this.wildSymbol = opts?.wildSymbol;
|
|
1258
|
+
getCumulativeWinsPerSpinType() {
|
|
1259
|
+
return this.cumulativeWinsPerSpinType;
|
|
1626
1260
|
}
|
|
1627
1261
|
/**
|
|
1628
|
-
*
|
|
1629
|
-
*
|
|
1630
|
-
* This gives the WinType access to the current board.
|
|
1262
|
+
* Returns the current win amount (as the bet multiplier) for the ongoing simulation.
|
|
1631
1263
|
*/
|
|
1632
|
-
|
|
1633
|
-
this.
|
|
1634
|
-
return this;
|
|
1264
|
+
getCurrentWin() {
|
|
1265
|
+
return this.currentWin;
|
|
1635
1266
|
}
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1267
|
+
/**
|
|
1268
|
+
* Returns the current spin win amount (as the bet multiplier) for the ongoing simulation.
|
|
1269
|
+
*/
|
|
1270
|
+
getCurrentSpinWin() {
|
|
1271
|
+
return this.currentSpinWin;
|
|
1640
1272
|
}
|
|
1641
1273
|
/**
|
|
1642
|
-
*
|
|
1274
|
+
* Returns the current tumble win amount (as the bet multiplier) for the ongoing simulation.
|
|
1643
1275
|
*/
|
|
1644
|
-
|
|
1645
|
-
this.
|
|
1646
|
-
return this;
|
|
1276
|
+
getCurrentTumbleWin() {
|
|
1277
|
+
return this.currentTumbleWin;
|
|
1647
1278
|
}
|
|
1648
1279
|
/**
|
|
1649
|
-
*
|
|
1280
|
+
* Returns the current win amount (as the bet multiplier) per spin type for the ongoing simulation.
|
|
1650
1281
|
*/
|
|
1651
|
-
|
|
1652
|
-
this.
|
|
1653
|
-
const result = func(this, this.ctx);
|
|
1654
|
-
this.payout = result.payout;
|
|
1655
|
-
this.winCombinations = result.winCombinations;
|
|
1656
|
-
return this;
|
|
1282
|
+
getCurrentWinPerSpinType() {
|
|
1283
|
+
return this.currentWinPerSpinType;
|
|
1657
1284
|
}
|
|
1658
1285
|
/**
|
|
1659
|
-
*
|
|
1286
|
+
* Adds a win to `currentSpinWin` and `currentTumbleWin`.
|
|
1287
|
+
*
|
|
1288
|
+
* After each (free) spin, this amount should be added to `currentWinPerSpinType` via `confirmSpinWin()`
|
|
1660
1289
|
*/
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
winCombinations: this.winCombinations
|
|
1665
|
-
};
|
|
1290
|
+
addTumbleWin(amount) {
|
|
1291
|
+
this.currentTumbleWin += amount;
|
|
1292
|
+
this.addSpinWin(amount);
|
|
1666
1293
|
}
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
this.
|
|
1675
|
-
|
|
1676
|
-
|
|
1294
|
+
/**
|
|
1295
|
+
* Intended for internal use only.
|
|
1296
|
+
*
|
|
1297
|
+
* Resets the current win amounts to zero.
|
|
1298
|
+
*/
|
|
1299
|
+
resetCurrentWin() {
|
|
1300
|
+
this.currentWin = 0;
|
|
1301
|
+
this.currentSpinWin = 0;
|
|
1302
|
+
this.currentTumbleWin = 0;
|
|
1303
|
+
for (const spinType of Object.keys(this.currentWinPerSpinType)) {
|
|
1304
|
+
this.currentWinPerSpinType[spinType] = 0;
|
|
1677
1305
|
}
|
|
1678
1306
|
}
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
}
|
|
1688
|
-
for (let i = 0; i < positions.length; i++) {
|
|
1689
|
-
if (positions[i] < 0 || positions[i] >= symsPerReel[i]) {
|
|
1690
|
-
throw new Error(
|
|
1691
|
-
`Line ${lineNum} has an invalid position ${positions[i]} on reel ${i}. Valid range is 0 to ${symsPerReel[i] - 1}.`
|
|
1692
|
-
);
|
|
1693
|
-
}
|
|
1694
|
-
}
|
|
1307
|
+
/**
|
|
1308
|
+
* Intended for internal use only.
|
|
1309
|
+
*
|
|
1310
|
+
* Adds current wins to cumulative wins and resets current wins to zero.
|
|
1311
|
+
*/
|
|
1312
|
+
confirmWins(ctx) {
|
|
1313
|
+
function process2(number) {
|
|
1314
|
+
return Math.round(Math.min(number, ctx.config.maxWinX) * 100) / 100;
|
|
1695
1315
|
}
|
|
1696
|
-
|
|
1697
|
-
|
|
1316
|
+
this.currentWin = process2(this.currentWin);
|
|
1317
|
+
this.cumulativeWins += this.currentWin;
|
|
1318
|
+
let spinTypeWins = 0;
|
|
1319
|
+
for (const spinType of Object.keys(this.currentWinPerSpinType)) {
|
|
1320
|
+
const st = spinType;
|
|
1321
|
+
const spinTypeWin = process2(this.currentWinPerSpinType[st]);
|
|
1322
|
+
this.cumulativeWinsPerSpinType[st] += spinTypeWin;
|
|
1323
|
+
spinTypeWins += spinTypeWin;
|
|
1324
|
+
}
|
|
1325
|
+
if (process2(spinTypeWins) !== this.currentWin) {
|
|
1698
1326
|
throw new Error(
|
|
1699
|
-
`
|
|
1327
|
+
`Inconsistent wallet state: currentWin (${this.currentWin}) does not equal spinTypeWins (${spinTypeWins}).`
|
|
1700
1328
|
);
|
|
1701
1329
|
}
|
|
1330
|
+
this.resetCurrentWin();
|
|
1702
1331
|
}
|
|
1703
|
-
|
|
1704
|
-
|
|
1332
|
+
/**
|
|
1333
|
+
* Intended for internal use only.
|
|
1334
|
+
*
|
|
1335
|
+
* Transfers the win data from the given wallet to the calling book.
|
|
1336
|
+
*/
|
|
1337
|
+
writePayoutToBook(ctx) {
|
|
1338
|
+
function process2(number) {
|
|
1339
|
+
return Math.round(Math.min(number, ctx.config.maxWinX) * 100) / 100;
|
|
1340
|
+
}
|
|
1341
|
+
const wallet = ctx.services.wallet._getWallet();
|
|
1342
|
+
const book = ctx.services.data._getBook();
|
|
1343
|
+
book.payout = Math.round(process2(wallet.getCurrentWin()) * 100);
|
|
1344
|
+
book.basegameWins = process2(
|
|
1345
|
+
wallet.getCurrentWinPerSpinType()[SPIN_TYPE.BASE_GAME] || 0
|
|
1346
|
+
);
|
|
1347
|
+
book.freespinsWins = process2(
|
|
1348
|
+
wallet.getCurrentWinPerSpinType()[SPIN_TYPE.FREE_SPINS] || 0
|
|
1349
|
+
);
|
|
1705
1350
|
}
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
if (wild) leadingWilds++;
|
|
1728
|
-
else baseSymbol = sym;
|
|
1729
|
-
continue;
|
|
1730
|
-
}
|
|
1731
|
-
if (wild) {
|
|
1732
|
-
chain.push(sym);
|
|
1733
|
-
details.push({
|
|
1734
|
-
reelIndex: ridx,
|
|
1735
|
-
posIndex: rowIdx,
|
|
1736
|
-
symbol: sym,
|
|
1737
|
-
isWild: true,
|
|
1738
|
-
substitutedFor: baseSymbol || void 0
|
|
1739
|
-
});
|
|
1740
|
-
continue;
|
|
1741
|
-
}
|
|
1742
|
-
if (!baseSymbol) {
|
|
1743
|
-
baseSymbol = sym;
|
|
1744
|
-
chain.push(sym);
|
|
1745
|
-
details.push({ reelIndex: ridx, posIndex: rowIdx, symbol: sym, isWild: false });
|
|
1746
|
-
continue;
|
|
1747
|
-
}
|
|
1748
|
-
if (sym.id === baseSymbol.id) {
|
|
1749
|
-
chain.push(sym);
|
|
1750
|
-
details.push({ reelIndex: ridx, posIndex: rowIdx, symbol: sym, isWild: false });
|
|
1751
|
-
continue;
|
|
1752
|
-
}
|
|
1753
|
-
break;
|
|
1754
|
-
}
|
|
1755
|
-
if (chain.length === 0) continue;
|
|
1756
|
-
const allWild = chain.every((s) => this.isWild(s));
|
|
1757
|
-
const wildRepresentative = this.wildSymbol instanceof GameSymbol ? this.wildSymbol : null;
|
|
1758
|
-
const len = chain.length;
|
|
1759
|
-
let bestPayout = 0;
|
|
1760
|
-
let bestType = null;
|
|
1761
|
-
let payingSymbol = null;
|
|
1762
|
-
if (baseSymbol?.pays && baseSymbol.pays[len]) {
|
|
1763
|
-
bestPayout = baseSymbol.pays[len];
|
|
1764
|
-
bestType = "substituted";
|
|
1765
|
-
payingSymbol = baseSymbol;
|
|
1766
|
-
}
|
|
1767
|
-
if (allWild && wildRepresentative?.pays && wildRepresentative.pays[len]) {
|
|
1768
|
-
const wildPay = wildRepresentative.pays[len];
|
|
1769
|
-
if (wildPay > bestPayout) {
|
|
1770
|
-
bestPayout = wildPay;
|
|
1771
|
-
bestType = "pure-wild";
|
|
1772
|
-
payingSymbol = wildRepresentative;
|
|
1773
|
-
}
|
|
1774
|
-
}
|
|
1775
|
-
if (!bestPayout || !bestType || !payingSymbol) continue;
|
|
1776
|
-
const minLen = payingSymbol.pays ? Math.min(...Object.keys(payingSymbol.pays).map(Number)) : Infinity;
|
|
1777
|
-
if (len < minLen) continue;
|
|
1778
|
-
const wildCount = details.filter((d) => d.isWild).length;
|
|
1779
|
-
const nonWildCount = len - wildCount;
|
|
1780
|
-
lineWins.push({
|
|
1781
|
-
lineNumber: lineNum,
|
|
1782
|
-
kind: len,
|
|
1783
|
-
payout: bestPayout,
|
|
1784
|
-
symbol: payingSymbol,
|
|
1785
|
-
winType: bestType,
|
|
1786
|
-
substitutedBaseSymbol: bestType === "pure-wild" ? null : baseSymbol,
|
|
1787
|
-
symbols: details,
|
|
1788
|
-
stats: { wildCount, nonWildCount, leadingWilds }
|
|
1789
|
-
});
|
|
1790
|
-
payout += bestPayout;
|
|
1351
|
+
/**
|
|
1352
|
+
* Intended for internal use only.
|
|
1353
|
+
*/
|
|
1354
|
+
serialize() {
|
|
1355
|
+
return {
|
|
1356
|
+
cumulativeWins: this.cumulativeWins,
|
|
1357
|
+
cumulativeWinsPerSpinType: this.cumulativeWinsPerSpinType,
|
|
1358
|
+
currentWin: this.currentWin,
|
|
1359
|
+
currentWinPerSpinType: this.currentWinPerSpinType,
|
|
1360
|
+
currentSpinWin: this.currentSpinWin,
|
|
1361
|
+
currentTumbleWin: this.currentTumbleWin
|
|
1362
|
+
};
|
|
1363
|
+
}
|
|
1364
|
+
/**
|
|
1365
|
+
* Intended for internal use only.
|
|
1366
|
+
*/
|
|
1367
|
+
merge(wallet) {
|
|
1368
|
+
this.cumulativeWins += wallet.getCumulativeWins();
|
|
1369
|
+
const otherWinsPerSpinType = wallet.getCumulativeWinsPerSpinType();
|
|
1370
|
+
for (const spinType of Object.keys(this.cumulativeWinsPerSpinType)) {
|
|
1371
|
+
this.cumulativeWinsPerSpinType[spinType] += otherWinsPerSpinType[spinType] || 0;
|
|
1791
1372
|
}
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1373
|
+
}
|
|
1374
|
+
/**
|
|
1375
|
+
* Intended for internal use only.
|
|
1376
|
+
*/
|
|
1377
|
+
mergeSerialized(data) {
|
|
1378
|
+
this.cumulativeWins += data.cumulativeWins;
|
|
1379
|
+
for (const spinType of Object.keys(this.cumulativeWinsPerSpinType)) {
|
|
1380
|
+
this.cumulativeWinsPerSpinType[spinType] += data.cumulativeWinsPerSpinType[spinType] || 0;
|
|
1381
|
+
}
|
|
1382
|
+
this.currentWin += data.currentWin;
|
|
1383
|
+
this.currentSpinWin += data.currentSpinWin;
|
|
1384
|
+
this.currentTumbleWin += data.currentTumbleWin;
|
|
1385
|
+
for (const spinType of Object.keys(this.currentWinPerSpinType)) {
|
|
1386
|
+
this.currentWinPerSpinType[spinType] += data.currentWinPerSpinType[spinType] || 0;
|
|
1798
1387
|
}
|
|
1799
|
-
this.payout = payout;
|
|
1800
|
-
this.winCombinations = lineWins;
|
|
1801
|
-
return this;
|
|
1802
1388
|
}
|
|
1803
1389
|
};
|
|
1804
1390
|
|
|
1805
|
-
// src/
|
|
1806
|
-
var
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1391
|
+
// src/simulation/index.ts
|
|
1392
|
+
var completedSimulations = 0;
|
|
1393
|
+
var TEMP_FILENAME = "__temp_compiled_src_IGNORE.js";
|
|
1394
|
+
var Simulation = class {
|
|
1395
|
+
gameConfigOpts;
|
|
1396
|
+
gameConfig;
|
|
1397
|
+
simRunsAmount;
|
|
1398
|
+
concurrency;
|
|
1399
|
+
debug = false;
|
|
1400
|
+
actualSims = 0;
|
|
1401
|
+
library;
|
|
1402
|
+
recorder;
|
|
1403
|
+
wallet;
|
|
1404
|
+
constructor(opts, gameConfigOpts) {
|
|
1405
|
+
this.gameConfig = createGameConfig(gameConfigOpts);
|
|
1406
|
+
this.gameConfigOpts = gameConfigOpts;
|
|
1407
|
+
this.simRunsAmount = opts.simRunsAmount || {};
|
|
1408
|
+
this.concurrency = (opts.concurrency || 6) >= 2 ? opts.concurrency || 6 : 2;
|
|
1409
|
+
this.library = /* @__PURE__ */ new Map();
|
|
1410
|
+
this.recorder = new Recorder();
|
|
1411
|
+
this.wallet = new Wallet();
|
|
1412
|
+
const gameModeKeys = Object.keys(this.gameConfig.gameModes);
|
|
1413
|
+
(0, import_assert6.default)(
|
|
1414
|
+
Object.values(this.gameConfig.gameModes).map((m) => gameModeKeys.includes(m.name)).every((v) => v === true),
|
|
1415
|
+
"Game mode name must match its key in the gameModes object."
|
|
1416
|
+
);
|
|
1417
|
+
if (import_worker_threads.isMainThread) {
|
|
1418
|
+
this.preprocessFiles();
|
|
1827
1419
|
}
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1420
|
+
}
|
|
1421
|
+
async runSimulation(opts) {
|
|
1422
|
+
const debug = opts.debug || false;
|
|
1423
|
+
this.debug = debug;
|
|
1424
|
+
const gameModesToSimulate = Object.keys(this.simRunsAmount);
|
|
1425
|
+
const configuredGameModes = Object.keys(this.gameConfig.gameModes);
|
|
1426
|
+
if (gameModesToSimulate.length === 0) {
|
|
1427
|
+
throw new Error("No game modes configured for simulation.");
|
|
1831
1428
|
}
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
this.forceSearch = {};
|
|
1835
|
-
if (typeof searchConditions === "number") {
|
|
1836
|
-
this.searchRange = [searchConditions, searchConditions];
|
|
1837
|
-
}
|
|
1838
|
-
if (Array.isArray(searchConditions)) {
|
|
1839
|
-
if (searchConditions[0] > searchConditions[1] || searchConditions.length !== 2) {
|
|
1840
|
-
throw new Error("Invalid searchConditions range.");
|
|
1841
|
-
}
|
|
1842
|
-
this.searchRange = searchConditions;
|
|
1843
|
-
}
|
|
1844
|
-
if (typeof searchConditions === "object" && !Array.isArray(searchConditions)) {
|
|
1845
|
-
this.searchRange = [-1, -1];
|
|
1846
|
-
this.forceSearch = searchConditions;
|
|
1847
|
-
}
|
|
1848
|
-
this.rtp = rtp;
|
|
1849
|
-
this.avgWin = avgWin;
|
|
1850
|
-
this.hitRate = hitRate;
|
|
1851
|
-
this.priority = priority;
|
|
1852
|
-
}
|
|
1853
|
-
getRtp() {
|
|
1854
|
-
return this.rtp;
|
|
1855
|
-
}
|
|
1856
|
-
getAvgWin() {
|
|
1857
|
-
return this.avgWin;
|
|
1858
|
-
}
|
|
1859
|
-
getHitRate() {
|
|
1860
|
-
return this.hitRate;
|
|
1861
|
-
}
|
|
1862
|
-
getSearchRange() {
|
|
1863
|
-
return this.searchRange;
|
|
1864
|
-
}
|
|
1865
|
-
getForceSearch() {
|
|
1866
|
-
return this.forceSearch;
|
|
1867
|
-
}
|
|
1868
|
-
};
|
|
1869
|
-
|
|
1870
|
-
// src/optimizer/OptimizationScaling.ts
|
|
1871
|
-
var OptimizationScaling = class {
|
|
1872
|
-
config;
|
|
1873
|
-
constructor(opts) {
|
|
1874
|
-
this.config = opts;
|
|
1875
|
-
}
|
|
1876
|
-
getConfig() {
|
|
1877
|
-
return this.config;
|
|
1878
|
-
}
|
|
1879
|
-
};
|
|
1880
|
-
|
|
1881
|
-
// src/optimizer/OptimizationParameters.ts
|
|
1882
|
-
var OptimizationParameters = class _OptimizationParameters {
|
|
1883
|
-
parameters;
|
|
1884
|
-
constructor(opts) {
|
|
1885
|
-
this.parameters = {
|
|
1886
|
-
..._OptimizationParameters.DEFAULT_PARAMETERS,
|
|
1887
|
-
...opts
|
|
1888
|
-
};
|
|
1889
|
-
}
|
|
1890
|
-
static DEFAULT_PARAMETERS = {
|
|
1891
|
-
numShowPigs: 5e3,
|
|
1892
|
-
numPigsPerFence: 1e4,
|
|
1893
|
-
threadsFenceConstruction: 16,
|
|
1894
|
-
threadsShowConstruction: 16,
|
|
1895
|
-
testSpins: [50, 100, 200],
|
|
1896
|
-
testSpinsWeights: [0.3, 0.4, 0.3],
|
|
1897
|
-
simulationTrials: 5e3,
|
|
1898
|
-
graphIndexes: [],
|
|
1899
|
-
run1000Batch: false,
|
|
1900
|
-
minMeanToMedian: 4,
|
|
1901
|
-
maxMeanToMedian: 8,
|
|
1902
|
-
pmbRtp: 1,
|
|
1903
|
-
scoreType: "rtp"
|
|
1904
|
-
};
|
|
1905
|
-
getParameters() {
|
|
1906
|
-
return this.parameters;
|
|
1907
|
-
}
|
|
1908
|
-
};
|
|
1909
|
-
|
|
1910
|
-
// src/Simulation.ts
|
|
1911
|
-
var import_fs3 = __toESM(require("fs"));
|
|
1912
|
-
var import_path2 = __toESM(require("path"));
|
|
1913
|
-
var import_assert5 = __toESM(require("assert"));
|
|
1914
|
-
var import_zlib = __toESM(require("zlib"));
|
|
1915
|
-
var import_esbuild = require("esbuild");
|
|
1916
|
-
var import_worker_threads2 = require("worker_threads");
|
|
1917
|
-
var completedSimulations = 0;
|
|
1918
|
-
var TEMP_FILENAME = "__temp_compiled_src_IGNORE.js";
|
|
1919
|
-
var Simulation = class _Simulation {
|
|
1920
|
-
gameConfigOpts;
|
|
1921
|
-
gameConfig;
|
|
1922
|
-
simRunsAmount;
|
|
1923
|
-
concurrency;
|
|
1924
|
-
wallet;
|
|
1925
|
-
library;
|
|
1926
|
-
records;
|
|
1927
|
-
debug = false;
|
|
1928
|
-
constructor(opts, gameConfigOpts) {
|
|
1929
|
-
this.gameConfig = new GameConfig(gameConfigOpts);
|
|
1930
|
-
this.gameConfigOpts = gameConfigOpts;
|
|
1931
|
-
this.simRunsAmount = opts.simRunsAmount || {};
|
|
1932
|
-
this.concurrency = (opts.concurrency || 6) >= 2 ? opts.concurrency || 6 : 2;
|
|
1933
|
-
this.wallet = new Wallet();
|
|
1934
|
-
this.library = /* @__PURE__ */ new Map();
|
|
1935
|
-
this.records = [];
|
|
1936
|
-
const gameModeKeys = Object.keys(this.gameConfig.config.gameModes);
|
|
1937
|
-
(0, import_assert5.default)(
|
|
1938
|
-
Object.values(this.gameConfig.config.gameModes).map((m) => gameModeKeys.includes(m.name)).every((v) => v === true),
|
|
1939
|
-
"Game mode name must match its key in the gameModes object."
|
|
1940
|
-
);
|
|
1941
|
-
if (import_worker_threads2.isMainThread) {
|
|
1942
|
-
this.preprocessFiles();
|
|
1943
|
-
}
|
|
1944
|
-
}
|
|
1945
|
-
async runSimulation(opts) {
|
|
1946
|
-
const debug = opts.debug || false;
|
|
1947
|
-
this.debug = debug;
|
|
1948
|
-
const gameModesToSimulate = Object.keys(this.simRunsAmount);
|
|
1949
|
-
const configuredGameModes = Object.keys(this.gameConfig.config.gameModes);
|
|
1950
|
-
if (gameModesToSimulate.length === 0) {
|
|
1951
|
-
throw new Error("No game modes configured for simulation.");
|
|
1952
|
-
}
|
|
1953
|
-
this.gameConfig.generateReelsetFiles();
|
|
1954
|
-
if (import_worker_threads2.isMainThread) {
|
|
1429
|
+
this.generateReelsetFiles();
|
|
1430
|
+
if (import_worker_threads.isMainThread) {
|
|
1955
1431
|
const debugDetails = {};
|
|
1956
1432
|
for (const mode of gameModesToSimulate) {
|
|
1957
1433
|
completedSimulations = 0;
|
|
1958
1434
|
this.wallet = new Wallet();
|
|
1959
1435
|
this.library = /* @__PURE__ */ new Map();
|
|
1436
|
+
this.recorder = new Recorder();
|
|
1960
1437
|
debugDetails[mode] = {};
|
|
1961
1438
|
console.log(`
|
|
1962
1439
|
Simulating game mode: ${mode}`);
|
|
@@ -1971,40 +1448,17 @@ Simulating game mode: ${mode}`);
|
|
|
1971
1448
|
const simNumsToCriteria = ResultSet.assignCriteriaToSimulations(this, mode);
|
|
1972
1449
|
await this.spawnWorkersForGameMode({ mode, simNumsToCriteria });
|
|
1973
1450
|
createDirIfNotExists(
|
|
1974
|
-
|
|
1975
|
-
process.cwd(),
|
|
1976
|
-
this.gameConfig.config.outputDir,
|
|
1977
|
-
"optimization_files"
|
|
1978
|
-
)
|
|
1451
|
+
import_path.default.join(process.cwd(), this.gameConfig.outputDir, "optimization_files")
|
|
1979
1452
|
);
|
|
1980
1453
|
createDirIfNotExists(
|
|
1981
|
-
|
|
1454
|
+
import_path.default.join(process.cwd(), this.gameConfig.outputDir, "publish_files")
|
|
1982
1455
|
);
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
gameMode: mode,
|
|
1990
|
-
library: this.library,
|
|
1991
|
-
gameConfig: this.gameConfig.config
|
|
1992
|
-
});
|
|
1993
|
-
_Simulation.writeRecords({
|
|
1994
|
-
gameMode: mode,
|
|
1995
|
-
records: this.records,
|
|
1996
|
-
gameConfig: this.gameConfig.config,
|
|
1997
|
-
debug: this.debug
|
|
1998
|
-
});
|
|
1999
|
-
await _Simulation.writeBooksJson({
|
|
2000
|
-
gameMode: mode,
|
|
2001
|
-
library: this.library,
|
|
2002
|
-
gameConfig: this.gameConfig.config
|
|
2003
|
-
});
|
|
2004
|
-
_Simulation.writeIndexJson({
|
|
2005
|
-
gameConfig: this.gameConfig.config
|
|
2006
|
-
});
|
|
2007
|
-
debugDetails[mode].rtp = this.wallet.getCumulativeWins() / (runs * this.gameConfig.config.gameModes[mode].cost);
|
|
1456
|
+
this.writeLookupTableCSV(mode);
|
|
1457
|
+
this.writeLookupTableSegmentedCSV(mode);
|
|
1458
|
+
this.writeRecords(mode);
|
|
1459
|
+
await this.writeBooksJson(mode);
|
|
1460
|
+
this.writeIndexJson();
|
|
1461
|
+
debugDetails[mode].rtp = this.wallet.getCumulativeWins() / (runs * this.gameConfig.gameModes[mode].cost);
|
|
2008
1462
|
debugDetails[mode].wins = this.wallet.getCumulativeWins();
|
|
2009
1463
|
debugDetails[mode].winsPerSpinType = this.wallet.getCumulativeWinsPerSpinType();
|
|
2010
1464
|
console.timeEnd(mode);
|
|
@@ -2015,27 +1469,26 @@ Simulating game mode: ${mode}`);
|
|
|
2015
1469
|
let desiredSims = 0;
|
|
2016
1470
|
let actualSims = 0;
|
|
2017
1471
|
const criteriaToRetries = {};
|
|
2018
|
-
if (!
|
|
2019
|
-
const { mode, simStart, simEnd, index } =
|
|
1472
|
+
if (!import_worker_threads.isMainThread) {
|
|
1473
|
+
const { mode, simStart, simEnd, index } = import_worker_threads.workerData;
|
|
2020
1474
|
const simNumsToCriteria = ResultSet.assignCriteriaToSimulations(this, mode);
|
|
2021
1475
|
for (let simId = simStart; simId <= simEnd; simId++) {
|
|
2022
1476
|
if (this.debug) desiredSims++;
|
|
2023
1477
|
const criteria = simNumsToCriteria[simId] || "N/A";
|
|
2024
|
-
const ctx = new SimulationContext(this.gameConfigOpts);
|
|
2025
1478
|
if (!criteriaToRetries[criteria]) {
|
|
2026
1479
|
criteriaToRetries[criteria] = 0;
|
|
2027
1480
|
}
|
|
2028
|
-
|
|
1481
|
+
this.runSingleSimulation({ simId, mode, criteria, index });
|
|
2029
1482
|
if (this.debug) {
|
|
2030
|
-
criteriaToRetries[criteria] +=
|
|
2031
|
-
actualSims +=
|
|
1483
|
+
criteriaToRetries[criteria] += this.actualSims - 1;
|
|
1484
|
+
actualSims += this.actualSims;
|
|
2032
1485
|
}
|
|
2033
1486
|
}
|
|
2034
1487
|
if (this.debug) {
|
|
2035
1488
|
console.log(`Desired ${desiredSims}, Actual ${actualSims}`);
|
|
2036
1489
|
console.log(`Retries per criteria:`, criteriaToRetries);
|
|
2037
1490
|
}
|
|
2038
|
-
|
|
1491
|
+
import_worker_threads.parentPort?.postMessage({
|
|
2039
1492
|
type: "done",
|
|
2040
1493
|
workerNum: index
|
|
2041
1494
|
});
|
|
@@ -2051,7 +1504,7 @@ Simulating game mode: ${mode}`);
|
|
|
2051
1504
|
await Promise.all(
|
|
2052
1505
|
simRangesPerChunk.map(([simStart, simEnd], index) => {
|
|
2053
1506
|
return this.callWorker({
|
|
2054
|
-
basePath: this.gameConfig.
|
|
1507
|
+
basePath: this.gameConfig.outputDir,
|
|
2055
1508
|
mode,
|
|
2056
1509
|
simStart,
|
|
2057
1510
|
simEnd,
|
|
@@ -2074,8 +1527,8 @@ Simulating game mode: ${mode}`);
|
|
|
2074
1527
|
}
|
|
2075
1528
|
}
|
|
2076
1529
|
return new Promise((resolve, reject) => {
|
|
2077
|
-
const scriptPath =
|
|
2078
|
-
const worker = new
|
|
1530
|
+
const scriptPath = import_path.default.join(process.cwd(), basePath, TEMP_FILENAME);
|
|
1531
|
+
const worker = new import_worker_threads.Worker(scriptPath, {
|
|
2079
1532
|
workerData: {
|
|
2080
1533
|
mode,
|
|
2081
1534
|
simStart,
|
|
@@ -2091,7 +1544,7 @@ Simulating game mode: ${mode}`);
|
|
|
2091
1544
|
logArrowProgress(completedSimulations, totalSims);
|
|
2092
1545
|
}
|
|
2093
1546
|
const book = Book.fromSerialized(msg.book);
|
|
2094
|
-
this.library.set(book.id
|
|
1547
|
+
this.library.set(book.id, book);
|
|
2095
1548
|
this.wallet.mergeSerialized(msg.wallet);
|
|
2096
1549
|
this.mergeRecords(msg.records);
|
|
2097
1550
|
} else if (msg.type === "done") {
|
|
@@ -2109,91 +1562,169 @@ Simulating game mode: ${mode}`);
|
|
|
2109
1562
|
});
|
|
2110
1563
|
});
|
|
2111
1564
|
}
|
|
1565
|
+
/**
|
|
1566
|
+
* Will run a single simulation until the specified criteria is met.
|
|
1567
|
+
*/
|
|
1568
|
+
runSingleSimulation(opts) {
|
|
1569
|
+
const { simId, mode, criteria } = opts;
|
|
1570
|
+
const ctx = createGameContext({
|
|
1571
|
+
config: this.gameConfig
|
|
1572
|
+
});
|
|
1573
|
+
ctx.state.currentGameMode = mode;
|
|
1574
|
+
ctx.state.currentSimulationId = simId;
|
|
1575
|
+
ctx.state.isCriteriaMet = false;
|
|
1576
|
+
const resultSet = ctx.services.game.getResultSetByCriteria(
|
|
1577
|
+
ctx.state.currentGameMode,
|
|
1578
|
+
criteria
|
|
1579
|
+
);
|
|
1580
|
+
while (!ctx.state.isCriteriaMet) {
|
|
1581
|
+
this.actualSims++;
|
|
1582
|
+
this.resetSimulation(ctx);
|
|
1583
|
+
ctx.state.currentResultSet = resultSet;
|
|
1584
|
+
this.handleGameFlow(ctx);
|
|
1585
|
+
if (resultSet.meetsCriteria(ctx)) {
|
|
1586
|
+
ctx.state.isCriteriaMet = true;
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
ctx.services.wallet._getWallet().writePayoutToBook(ctx);
|
|
1590
|
+
ctx.services.wallet._getWallet().confirmWins(ctx);
|
|
1591
|
+
if (ctx.services.data._getBook().payout >= ctx.config.maxWinX) {
|
|
1592
|
+
ctx.state.triggeredMaxWin = true;
|
|
1593
|
+
}
|
|
1594
|
+
ctx.services.data.record({
|
|
1595
|
+
criteria: resultSet.criteria
|
|
1596
|
+
});
|
|
1597
|
+
ctx.config.hooks.onSimulationAccepted?.(ctx);
|
|
1598
|
+
this.confirmRecords(ctx);
|
|
1599
|
+
import_worker_threads.parentPort?.postMessage({
|
|
1600
|
+
type: "complete",
|
|
1601
|
+
simId,
|
|
1602
|
+
book: ctx.services.data._getBook().serialize(),
|
|
1603
|
+
wallet: ctx.services.wallet._getWallet().serialize(),
|
|
1604
|
+
records: ctx.services.data._getRecords()
|
|
1605
|
+
});
|
|
1606
|
+
}
|
|
1607
|
+
/**
|
|
1608
|
+
* If a simulation does not meet the required criteria, reset the state to run it again.
|
|
1609
|
+
*
|
|
1610
|
+
* This also runs once before each simulation to ensure a clean state.
|
|
1611
|
+
*/
|
|
1612
|
+
resetSimulation(ctx) {
|
|
1613
|
+
this.resetState(ctx);
|
|
1614
|
+
ctx.services.board.resetBoard();
|
|
1615
|
+
ctx.services.data._setRecorder(new Recorder());
|
|
1616
|
+
ctx.services.wallet._setWallet(new Wallet());
|
|
1617
|
+
ctx.services.data._setBook(
|
|
1618
|
+
new Book({
|
|
1619
|
+
id: ctx.state.currentSimulationId,
|
|
1620
|
+
criteria: ctx.state.currentResultSet.criteria
|
|
1621
|
+
})
|
|
1622
|
+
);
|
|
1623
|
+
}
|
|
1624
|
+
resetState(ctx) {
|
|
1625
|
+
ctx.services.rng.setSeedIfDifferent(ctx.state.currentSimulationId);
|
|
1626
|
+
ctx.state.currentSpinType = SPIN_TYPE.BASE_GAME;
|
|
1627
|
+
ctx.state.currentFreespinAmount = 0;
|
|
1628
|
+
ctx.state.totalFreespinAmount = 0;
|
|
1629
|
+
ctx.state.triggeredMaxWin = false;
|
|
1630
|
+
ctx.state.triggeredFreespins = false;
|
|
1631
|
+
ctx.state.userData = ctx.config.userState || {};
|
|
1632
|
+
}
|
|
1633
|
+
/**
|
|
1634
|
+
* Contains and executes the entire game logic:
|
|
1635
|
+
* - Drawing the board
|
|
1636
|
+
* - Evaluating wins
|
|
1637
|
+
* - Updating wallet
|
|
1638
|
+
* - Handling free spins
|
|
1639
|
+
* - Recording events
|
|
1640
|
+
*
|
|
1641
|
+
* You can customize the game flow by implementing the `onHandleGameFlow` hook in the game configuration.
|
|
1642
|
+
*/
|
|
1643
|
+
handleGameFlow(ctx) {
|
|
1644
|
+
this.gameConfig.hooks.onHandleGameFlow(ctx);
|
|
1645
|
+
}
|
|
2112
1646
|
/**
|
|
2113
1647
|
* Creates a CSV file in the format "simulationId,weight,payout".
|
|
2114
1648
|
*
|
|
2115
1649
|
* `weight` defaults to 1.
|
|
2116
1650
|
*/
|
|
2117
|
-
|
|
2118
|
-
const { gameMode, library, gameConfig } = opts;
|
|
1651
|
+
writeLookupTableCSV(gameMode) {
|
|
2119
1652
|
const rows = [];
|
|
2120
|
-
for (const [bookId, book] of library.entries()) {
|
|
2121
|
-
rows.push(`${book.id},1,${Math.round(book.
|
|
1653
|
+
for (const [bookId, book] of this.library.entries()) {
|
|
1654
|
+
rows.push(`${book.id},1,${Math.round(book.payout)}`);
|
|
2122
1655
|
}
|
|
2123
1656
|
rows.sort((a, b) => Number(a.split(",")[0]) - Number(b.split(",")[0]));
|
|
2124
1657
|
let outputFileName = `lookUpTable_${gameMode}.csv`;
|
|
2125
|
-
let outputFilePath =
|
|
1658
|
+
let outputFilePath = import_path.default.join(this.gameConfig.outputDir, outputFileName);
|
|
2126
1659
|
writeFile(outputFilePath, rows.join("\n"));
|
|
2127
1660
|
outputFileName = `lookUpTable_${gameMode}_0.csv`;
|
|
2128
|
-
outputFilePath =
|
|
1661
|
+
outputFilePath = import_path.default.join(this.gameConfig.outputDir, "publish_files", outputFileName);
|
|
2129
1662
|
writeFile(outputFilePath, rows.join("\n"));
|
|
2130
1663
|
return outputFilePath;
|
|
2131
1664
|
}
|
|
2132
1665
|
/**
|
|
2133
1666
|
* Creates a CSV file in the format "simulationId,criteria,payoutBase,payoutFreespins".
|
|
2134
1667
|
*/
|
|
2135
|
-
|
|
2136
|
-
const { gameMode, library, gameConfig } = opts;
|
|
1668
|
+
writeLookupTableSegmentedCSV(gameMode) {
|
|
2137
1669
|
const rows = [];
|
|
2138
|
-
for (const [bookId, book] of library.entries()) {
|
|
2139
|
-
rows.push(
|
|
2140
|
-
`${book.id},${book.criteria},${book.getBasegameWins()},${book.getFreespinsWins()}`
|
|
2141
|
-
);
|
|
1670
|
+
for (const [bookId, book] of this.library.entries()) {
|
|
1671
|
+
rows.push(`${book.id},${book.criteria},${book.basegameWins},${book.freespinsWins}`);
|
|
2142
1672
|
}
|
|
2143
1673
|
rows.sort((a, b) => Number(a.split(",")[0]) - Number(b.split(",")[0]));
|
|
2144
1674
|
const outputFileName = `lookUpTableSegmented_${gameMode}.csv`;
|
|
2145
|
-
const outputFilePath =
|
|
1675
|
+
const outputFilePath = import_path.default.join(this.gameConfig.outputDir, outputFileName);
|
|
2146
1676
|
writeFile(outputFilePath, rows.join("\n"));
|
|
2147
1677
|
return outputFilePath;
|
|
2148
1678
|
}
|
|
2149
|
-
|
|
2150
|
-
const {
|
|
2151
|
-
const
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
if (debug) _Simulation.logSymbolOccurrences(records);
|
|
1679
|
+
writeRecords(gameMode) {
|
|
1680
|
+
const outputFileName = `force_record_${gameMode}.json`;
|
|
1681
|
+
const outputFilePath = import_path.default.join(this.gameConfig.outputDir, outputFileName);
|
|
1682
|
+
writeFile(outputFilePath, JSON.stringify(this.recorder.records, null, 2));
|
|
1683
|
+
if (this.debug) this.logSymbolOccurrences();
|
|
2155
1684
|
return outputFilePath;
|
|
2156
1685
|
}
|
|
2157
|
-
|
|
2158
|
-
const
|
|
2159
|
-
const outputFilePath = import_path2.default.join(
|
|
1686
|
+
writeIndexJson() {
|
|
1687
|
+
const outputFilePath = import_path.default.join(
|
|
2160
1688
|
process.cwd(),
|
|
2161
|
-
gameConfig.outputDir,
|
|
1689
|
+
this.gameConfig.outputDir,
|
|
2162
1690
|
"publish_files",
|
|
2163
1691
|
"index.json"
|
|
2164
1692
|
);
|
|
2165
|
-
const modes = Object.
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
1693
|
+
const modes = Object.keys(this.simRunsAmount).map((id) => {
|
|
1694
|
+
const mode = this.gameConfig.gameModes[id];
|
|
1695
|
+
(0, import_assert6.default)(mode, `Game mode "${id}" not found in game config.`);
|
|
1696
|
+
return {
|
|
1697
|
+
name: mode.name,
|
|
1698
|
+
cost: mode.cost,
|
|
1699
|
+
events: `books_${mode.name}.jsonl.zst`,
|
|
1700
|
+
weights: `lookUpTable_${mode.name}_0.csv`
|
|
1701
|
+
};
|
|
1702
|
+
});
|
|
2171
1703
|
writeFile(outputFilePath, JSON.stringify({ modes }, null, 2));
|
|
2172
1704
|
}
|
|
2173
|
-
|
|
2174
|
-
const {
|
|
2175
|
-
const
|
|
2176
|
-
const
|
|
2177
|
-
const books = Array.from(library.values()).map((b) => b.serialize()).map((b) => ({
|
|
1705
|
+
async writeBooksJson(gameMode) {
|
|
1706
|
+
const outputFileName = `books_${gameMode}.jsonl`;
|
|
1707
|
+
const outputFilePath = import_path.default.join(this.gameConfig.outputDir, outputFileName);
|
|
1708
|
+
const books = Array.from(this.library.values()).map((b) => b.serialize()).map((b) => ({
|
|
2178
1709
|
id: b.id,
|
|
2179
1710
|
payoutMultiplier: b.payout,
|
|
2180
1711
|
events: b.events
|
|
2181
1712
|
})).sort((a, b) => a.id - b.id);
|
|
2182
1713
|
const contents = JSONL.stringify(books);
|
|
2183
1714
|
writeFile(outputFilePath, contents);
|
|
2184
|
-
const compressedFileName =
|
|
2185
|
-
const compressedFilePath =
|
|
1715
|
+
const compressedFileName = `books_${gameMode}.jsonl.zst`;
|
|
1716
|
+
const compressedFilePath = import_path.default.join(
|
|
2186
1717
|
process.cwd(),
|
|
2187
|
-
gameConfig.outputDir,
|
|
1718
|
+
this.gameConfig.outputDir,
|
|
2188
1719
|
"publish_files",
|
|
2189
1720
|
compressedFileName
|
|
2190
1721
|
);
|
|
2191
|
-
|
|
1722
|
+
import_fs2.default.rmSync(compressedFilePath, { force: true });
|
|
2192
1723
|
const compressed = import_zlib.default.zstdCompressSync(Buffer.from(contents));
|
|
2193
|
-
|
|
1724
|
+
import_fs2.default.writeFileSync(compressedFilePath, compressed);
|
|
2194
1725
|
}
|
|
2195
|
-
|
|
2196
|
-
const validRecords = records.filter(
|
|
1726
|
+
logSymbolOccurrences() {
|
|
1727
|
+
const validRecords = this.recorder.records.filter(
|
|
2197
1728
|
(r) => r.search.some((s) => s.name === "symbolId") && r.search.some((s) => s.name === "kind")
|
|
2198
1729
|
);
|
|
2199
1730
|
const structuredRecords = validRecords.map((r) => {
|
|
@@ -2221,13 +1752,13 @@ Simulating game mode: ${mode}`);
|
|
|
2221
1752
|
* Compiles user configured game to JS for use in different Node processes
|
|
2222
1753
|
*/
|
|
2223
1754
|
preprocessFiles() {
|
|
2224
|
-
const builtFilePath =
|
|
2225
|
-
|
|
1755
|
+
const builtFilePath = import_path.default.join(this.gameConfig.outputDir, TEMP_FILENAME);
|
|
1756
|
+
import_fs2.default.rmSync(builtFilePath, { force: true });
|
|
2226
1757
|
(0, import_esbuild.buildSync)({
|
|
2227
1758
|
entryPoints: [process.cwd()],
|
|
2228
1759
|
bundle: true,
|
|
2229
1760
|
platform: "node",
|
|
2230
|
-
outfile:
|
|
1761
|
+
outfile: import_path.default.join(this.gameConfig.outputDir, TEMP_FILENAME),
|
|
2231
1762
|
external: ["esbuild"]
|
|
2232
1763
|
});
|
|
2233
1764
|
}
|
|
@@ -2247,7 +1778,7 @@ Simulating game mode: ${mode}`);
|
|
|
2247
1778
|
}
|
|
2248
1779
|
mergeRecords(otherRecords) {
|
|
2249
1780
|
for (const otherRecord of otherRecords) {
|
|
2250
|
-
let record = this.records.find((r) => {
|
|
1781
|
+
let record = this.recorder.records.find((r) => {
|
|
2251
1782
|
if (r.search.length !== otherRecord.search.length) return false;
|
|
2252
1783
|
for (let i = 0; i < r.search.length; i++) {
|
|
2253
1784
|
if (r.search[i].name !== otherRecord.search[i].name) return false;
|
|
@@ -2261,7 +1792,7 @@ Simulating game mode: ${mode}`);
|
|
|
2261
1792
|
timesTriggered: 0,
|
|
2262
1793
|
bookIds: []
|
|
2263
1794
|
};
|
|
2264
|
-
this.records.push(record);
|
|
1795
|
+
this.recorder.records.push(record);
|
|
2265
1796
|
}
|
|
2266
1797
|
record.timesTriggered += otherRecord.timesTriggered;
|
|
2267
1798
|
for (const bookId of otherRecord.bookIds) {
|
|
@@ -2271,76 +1802,59 @@ Simulating game mode: ${mode}`);
|
|
|
2271
1802
|
}
|
|
2272
1803
|
}
|
|
2273
1804
|
}
|
|
2274
|
-
};
|
|
2275
|
-
var SimulationContext = class extends Board {
|
|
2276
|
-
constructor(opts) {
|
|
2277
|
-
super(opts);
|
|
2278
|
-
}
|
|
2279
|
-
actualSims = 0;
|
|
2280
1805
|
/**
|
|
2281
|
-
*
|
|
1806
|
+
* Generates reelset CSV files for all game modes.
|
|
2282
1807
|
*/
|
|
2283
|
-
|
|
2284
|
-
const
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
this.handleGameFlow();
|
|
2295
|
-
if (resultSet.meetsCriteria(this)) {
|
|
2296
|
-
this.state.isCriteriaMet = true;
|
|
1808
|
+
generateReelsetFiles() {
|
|
1809
|
+
for (const mode of Object.values(this.gameConfig.gameModes)) {
|
|
1810
|
+
if (mode.reelSets && mode.reelSets.length > 0) {
|
|
1811
|
+
for (const reelSet of Object.values(mode.reelSets)) {
|
|
1812
|
+
reelSet.associatedGameModeName = mode.name;
|
|
1813
|
+
reelSet.generateReels(this);
|
|
1814
|
+
}
|
|
1815
|
+
} else {
|
|
1816
|
+
throw new Error(
|
|
1817
|
+
`Game mode "${mode.name}" has no reel sets defined. Cannot generate reelset files.`
|
|
1818
|
+
);
|
|
2297
1819
|
}
|
|
2298
1820
|
}
|
|
2299
|
-
this.wallet.confirmWins(this);
|
|
2300
|
-
if (this.state.book.getPayout() >= this.config.maxWinX) {
|
|
2301
|
-
this.state.triggeredMaxWin = true;
|
|
2302
|
-
}
|
|
2303
|
-
this.record({
|
|
2304
|
-
criteria: resultSet.criteria
|
|
2305
|
-
});
|
|
2306
|
-
this.config.hooks.onSimulationAccepted?.(this);
|
|
2307
|
-
this.confirmRecords();
|
|
2308
|
-
import_worker_threads2.parentPort?.postMessage({
|
|
2309
|
-
type: "complete",
|
|
2310
|
-
simId,
|
|
2311
|
-
book: this.state.book.serialize(),
|
|
2312
|
-
wallet: this.wallet.serialize(),
|
|
2313
|
-
records: this.getRecords()
|
|
2314
|
-
});
|
|
2315
|
-
}
|
|
2316
|
-
/**
|
|
2317
|
-
* If a simulation does not meet the required criteria, reset the state to run it again.
|
|
2318
|
-
*
|
|
2319
|
-
* This also runs once before each simulation to ensure a clean state.
|
|
2320
|
-
*/
|
|
2321
|
-
resetSimulation() {
|
|
2322
|
-
this.resetState();
|
|
2323
|
-
this.resetBoard();
|
|
2324
1821
|
}
|
|
2325
1822
|
/**
|
|
2326
|
-
*
|
|
2327
|
-
* - Drawing the board
|
|
2328
|
-
* - Evaluating wins
|
|
2329
|
-
* - Updating wallet
|
|
2330
|
-
* - Handling free spins
|
|
2331
|
-
* - Recording events
|
|
2332
|
-
*
|
|
2333
|
-
* You can customize the game flow by implementing the `onHandleGameFlow` hook in the game configuration.
|
|
1823
|
+
* Confirms all pending records and adds them to the main records list.
|
|
2334
1824
|
*/
|
|
2335
|
-
|
|
2336
|
-
|
|
1825
|
+
confirmRecords(ctx) {
|
|
1826
|
+
const recorder = ctx.services.data._getRecorder();
|
|
1827
|
+
for (const pendingRecord of recorder.pendingRecords) {
|
|
1828
|
+
const search = Object.entries(pendingRecord.properties).map(([name, value]) => ({ name, value })).sort((a, b) => a.name.localeCompare(b.name));
|
|
1829
|
+
let record = recorder.records.find((r) => {
|
|
1830
|
+
if (r.search.length !== search.length) return false;
|
|
1831
|
+
for (let i = 0; i < r.search.length; i++) {
|
|
1832
|
+
if (r.search[i].name !== search[i].name) return false;
|
|
1833
|
+
if (r.search[i].value !== search[i].value) return false;
|
|
1834
|
+
}
|
|
1835
|
+
return true;
|
|
1836
|
+
});
|
|
1837
|
+
if (!record) {
|
|
1838
|
+
record = {
|
|
1839
|
+
search,
|
|
1840
|
+
timesTriggered: 0,
|
|
1841
|
+
bookIds: []
|
|
1842
|
+
};
|
|
1843
|
+
recorder.records.push(record);
|
|
1844
|
+
}
|
|
1845
|
+
record.timesTriggered++;
|
|
1846
|
+
if (!record.bookIds.includes(pendingRecord.bookId)) {
|
|
1847
|
+
record.bookIds.push(pendingRecord.bookId);
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
recorder.pendingRecords = [];
|
|
2337
1851
|
}
|
|
2338
1852
|
};
|
|
2339
1853
|
|
|
2340
1854
|
// src/analysis/index.ts
|
|
2341
|
-
var
|
|
2342
|
-
var
|
|
2343
|
-
var
|
|
1855
|
+
var import_fs3 = __toESM(require("fs"));
|
|
1856
|
+
var import_path2 = __toESM(require("path"));
|
|
1857
|
+
var import_assert7 = __toESM(require("assert"));
|
|
2344
1858
|
|
|
2345
1859
|
// src/analysis/utils.ts
|
|
2346
1860
|
function parseLookupTable(content) {
|
|
@@ -2449,7 +1963,7 @@ function getLessBetHitrate(payoutWeights, cost) {
|
|
|
2449
1963
|
}
|
|
2450
1964
|
|
|
2451
1965
|
// src/analysis/index.ts
|
|
2452
|
-
var
|
|
1966
|
+
var import_worker_threads2 = require("worker_threads");
|
|
2453
1967
|
var Analysis = class {
|
|
2454
1968
|
gameConfig;
|
|
2455
1969
|
optimizerConfig;
|
|
@@ -2460,7 +1974,7 @@ var Analysis = class {
|
|
|
2460
1974
|
this.filePaths = {};
|
|
2461
1975
|
}
|
|
2462
1976
|
async runAnalysis(gameModes) {
|
|
2463
|
-
if (!
|
|
1977
|
+
if (!import_worker_threads2.isMainThread) return;
|
|
2464
1978
|
this.filePaths = this.getPathsForModes(gameModes);
|
|
2465
1979
|
this.getNumberStats(gameModes);
|
|
2466
1980
|
this.getWinRanges(gameModes);
|
|
@@ -2470,28 +1984,28 @@ var Analysis = class {
|
|
|
2470
1984
|
const rootPath = process.cwd();
|
|
2471
1985
|
const paths = {};
|
|
2472
1986
|
for (const modeStr of gameModes) {
|
|
2473
|
-
const lut =
|
|
1987
|
+
const lut = import_path2.default.join(
|
|
2474
1988
|
rootPath,
|
|
2475
1989
|
this.gameConfig.outputDir,
|
|
2476
1990
|
`lookUpTable_${modeStr}.csv`
|
|
2477
1991
|
);
|
|
2478
|
-
const lutSegmented =
|
|
1992
|
+
const lutSegmented = import_path2.default.join(
|
|
2479
1993
|
rootPath,
|
|
2480
1994
|
this.gameConfig.outputDir,
|
|
2481
1995
|
`lookUpTableSegmented_${modeStr}.csv`
|
|
2482
1996
|
);
|
|
2483
|
-
const lutOptimized =
|
|
1997
|
+
const lutOptimized = import_path2.default.join(
|
|
2484
1998
|
rootPath,
|
|
2485
1999
|
this.gameConfig.outputDir,
|
|
2486
2000
|
"publish_files",
|
|
2487
2001
|
`lookUpTable_${modeStr}_0.csv`
|
|
2488
2002
|
);
|
|
2489
|
-
const booksJsonl =
|
|
2003
|
+
const booksJsonl = import_path2.default.join(
|
|
2490
2004
|
rootPath,
|
|
2491
2005
|
this.gameConfig.outputDir,
|
|
2492
2006
|
`books_${modeStr}.jsonl`
|
|
2493
2007
|
);
|
|
2494
|
-
const booksJsonlCompressed =
|
|
2008
|
+
const booksJsonlCompressed = import_path2.default.join(
|
|
2495
2009
|
rootPath,
|
|
2496
2010
|
this.gameConfig.outputDir,
|
|
2497
2011
|
"publish_files",
|
|
@@ -2505,8 +2019,8 @@ var Analysis = class {
|
|
|
2505
2019
|
booksJsonlCompressed
|
|
2506
2020
|
};
|
|
2507
2021
|
for (const p of Object.values(paths[modeStr])) {
|
|
2508
|
-
(0,
|
|
2509
|
-
|
|
2022
|
+
(0, import_assert7.default)(
|
|
2023
|
+
import_fs3.default.existsSync(p),
|
|
2510
2024
|
`File "${p}" does not exist. Run optimization to auto-create it.`
|
|
2511
2025
|
);
|
|
2512
2026
|
}
|
|
@@ -2518,7 +2032,7 @@ var Analysis = class {
|
|
|
2518
2032
|
for (const modeStr of gameModes) {
|
|
2519
2033
|
const mode = this.getGameModeConfig(modeStr);
|
|
2520
2034
|
const lutOptimized = parseLookupTable(
|
|
2521
|
-
|
|
2035
|
+
import_fs3.default.readFileSync(this.filePaths[modeStr].lutOptimized, "utf-8")
|
|
2522
2036
|
);
|
|
2523
2037
|
const totalWeight = getTotalLutWeight(lutOptimized);
|
|
2524
2038
|
const payoutWeights = getPayoutWeights(lutOptimized);
|
|
@@ -2539,7 +2053,7 @@ var Analysis = class {
|
|
|
2539
2053
|
});
|
|
2540
2054
|
}
|
|
2541
2055
|
writeJsonFile(
|
|
2542
|
-
|
|
2056
|
+
import_path2.default.join(process.cwd(), this.gameConfig.outputDir, "stats_summary.json"),
|
|
2543
2057
|
stats
|
|
2544
2058
|
);
|
|
2545
2059
|
}
|
|
@@ -2569,7 +2083,7 @@ var Analysis = class {
|
|
|
2569
2083
|
for (const modeStr of gameModes) {
|
|
2570
2084
|
const mode = this.getGameModeConfig(modeStr);
|
|
2571
2085
|
const lutOptimized = parseLookupTable(
|
|
2572
|
-
|
|
2086
|
+
import_fs3.default.readFileSync(this.filePaths[modeStr].lutOptimized, "utf-8")
|
|
2573
2087
|
);
|
|
2574
2088
|
const totalWeight = getTotalLutWeight(lutOptimized);
|
|
2575
2089
|
const payoutWeights = getPayoutWeights(lutOptimized);
|
|
@@ -2577,13 +2091,19 @@ var Analysis = class {
|
|
|
2577
2091
|
}
|
|
2578
2092
|
getGameModeConfig(mode) {
|
|
2579
2093
|
const config = this.gameConfig.gameModes[mode];
|
|
2580
|
-
(0,
|
|
2094
|
+
(0, import_assert7.default)(config, `Game mode "${mode}" not found in game config`);
|
|
2581
2095
|
return config;
|
|
2582
2096
|
}
|
|
2583
2097
|
};
|
|
2584
2098
|
|
|
2099
|
+
// src/optimizer/index.ts
|
|
2100
|
+
var import_path5 = __toESM(require("path"));
|
|
2101
|
+
var import_assert9 = __toESM(require("assert"));
|
|
2102
|
+
var import_child_process = require("child_process");
|
|
2103
|
+
var import_worker_threads3 = require("worker_threads");
|
|
2104
|
+
|
|
2585
2105
|
// src/utils/math-config.ts
|
|
2586
|
-
var
|
|
2106
|
+
var import_path3 = __toESM(require("path"));
|
|
2587
2107
|
function makeMathConfig(optimizer, opts = {}) {
|
|
2588
2108
|
const game = optimizer.getGameConfig();
|
|
2589
2109
|
const gameModesCfg = optimizer.getOptimizerGameModes();
|
|
@@ -2627,14 +2147,14 @@ function makeMathConfig(optimizer, opts = {}) {
|
|
|
2627
2147
|
}))
|
|
2628
2148
|
};
|
|
2629
2149
|
if (writeToFile) {
|
|
2630
|
-
const outPath =
|
|
2150
|
+
const outPath = import_path3.default.join(process.cwd(), game.outputDir, "math_config.json");
|
|
2631
2151
|
writeJsonFile(outPath, config);
|
|
2632
2152
|
}
|
|
2633
2153
|
return config;
|
|
2634
2154
|
}
|
|
2635
2155
|
|
|
2636
2156
|
// src/utils/setup-file.ts
|
|
2637
|
-
var
|
|
2157
|
+
var import_path4 = __toESM(require("path"));
|
|
2638
2158
|
function makeSetupFile(optimizer, gameMode) {
|
|
2639
2159
|
const gameConfig = optimizer.getGameConfig();
|
|
2640
2160
|
const optimizerGameModes = optimizer.getOptimizerGameModes();
|
|
@@ -2670,33 +2190,126 @@ function makeSetupFile(optimizer, gameMode) {
|
|
|
2670
2190
|
`;
|
|
2671
2191
|
content += `simulation_trials;${params.simulationTrials}
|
|
2672
2192
|
`;
|
|
2673
|
-
content += `user_game_build_path;${
|
|
2193
|
+
content += `user_game_build_path;${import_path4.default.join(process.cwd(), gameConfig.outputDir)}
|
|
2674
2194
|
`;
|
|
2675
2195
|
content += `pmb_rtp;${params.pmbRtp}
|
|
2676
2196
|
`;
|
|
2677
|
-
const outPath =
|
|
2197
|
+
const outPath = import_path4.default.join(__dirname, "./optimizer-rust/src", "setup.txt");
|
|
2678
2198
|
writeFile(outPath, content);
|
|
2679
2199
|
}
|
|
2680
2200
|
|
|
2681
|
-
// src/optimizer/
|
|
2682
|
-
var
|
|
2683
|
-
var
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2201
|
+
// src/optimizer/OptimizationConditions.ts
|
|
2202
|
+
var import_assert8 = __toESM(require("assert"));
|
|
2203
|
+
var OptimizationConditions = class {
|
|
2204
|
+
rtp;
|
|
2205
|
+
avgWin;
|
|
2206
|
+
hitRate;
|
|
2207
|
+
searchRange;
|
|
2208
|
+
forceSearch;
|
|
2209
|
+
priority;
|
|
2689
2210
|
constructor(opts) {
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2211
|
+
let { rtp, avgWin, hitRate, searchConditions, priority } = opts;
|
|
2212
|
+
if (rtp == void 0 || rtp === "x") {
|
|
2213
|
+
(0, import_assert8.default)(avgWin !== void 0 && hitRate !== void 0, "If RTP is not specified, hit-rate (hr) and average win amount (av_win) must be given.");
|
|
2214
|
+
rtp = Math.round(avgWin / Number(hitRate) * 1e5) / 1e5;
|
|
2215
|
+
}
|
|
2216
|
+
let noneCount = 0;
|
|
2217
|
+
for (const val of [rtp, avgWin, hitRate]) {
|
|
2218
|
+
if (val === void 0) noneCount++;
|
|
2219
|
+
}
|
|
2220
|
+
(0, import_assert8.default)(noneCount <= 1, "Invalid combination of optimization conditions.");
|
|
2221
|
+
this.searchRange = [-1, -1];
|
|
2222
|
+
this.forceSearch = {};
|
|
2223
|
+
if (typeof searchConditions === "number") {
|
|
2224
|
+
this.searchRange = [searchConditions, searchConditions];
|
|
2225
|
+
}
|
|
2226
|
+
if (Array.isArray(searchConditions)) {
|
|
2227
|
+
if (searchConditions[0] > searchConditions[1] || searchConditions.length !== 2) {
|
|
2228
|
+
throw new Error("Invalid searchConditions range.");
|
|
2229
|
+
}
|
|
2230
|
+
this.searchRange = searchConditions;
|
|
2231
|
+
}
|
|
2232
|
+
if (typeof searchConditions === "object" && !Array.isArray(searchConditions)) {
|
|
2233
|
+
this.searchRange = [-1, -1];
|
|
2234
|
+
this.forceSearch = searchConditions;
|
|
2235
|
+
}
|
|
2236
|
+
this.rtp = rtp;
|
|
2237
|
+
this.avgWin = avgWin;
|
|
2238
|
+
this.hitRate = hitRate;
|
|
2239
|
+
this.priority = priority;
|
|
2240
|
+
}
|
|
2241
|
+
getRtp() {
|
|
2242
|
+
return this.rtp;
|
|
2243
|
+
}
|
|
2244
|
+
getAvgWin() {
|
|
2245
|
+
return this.avgWin;
|
|
2246
|
+
}
|
|
2247
|
+
getHitRate() {
|
|
2248
|
+
return this.hitRate;
|
|
2249
|
+
}
|
|
2250
|
+
getSearchRange() {
|
|
2251
|
+
return this.searchRange;
|
|
2252
|
+
}
|
|
2253
|
+
getForceSearch() {
|
|
2254
|
+
return this.forceSearch;
|
|
2255
|
+
}
|
|
2256
|
+
};
|
|
2257
|
+
|
|
2258
|
+
// src/optimizer/OptimizationScaling.ts
|
|
2259
|
+
var OptimizationScaling = class {
|
|
2260
|
+
config;
|
|
2261
|
+
constructor(opts) {
|
|
2262
|
+
this.config = opts;
|
|
2263
|
+
}
|
|
2264
|
+
getConfig() {
|
|
2265
|
+
return this.config;
|
|
2266
|
+
}
|
|
2267
|
+
};
|
|
2268
|
+
|
|
2269
|
+
// src/optimizer/OptimizationParameters.ts
|
|
2270
|
+
var OptimizationParameters = class _OptimizationParameters {
|
|
2271
|
+
parameters;
|
|
2272
|
+
constructor(opts) {
|
|
2273
|
+
this.parameters = {
|
|
2274
|
+
..._OptimizationParameters.DEFAULT_PARAMETERS,
|
|
2275
|
+
...opts
|
|
2276
|
+
};
|
|
2277
|
+
}
|
|
2278
|
+
static DEFAULT_PARAMETERS = {
|
|
2279
|
+
numShowPigs: 5e3,
|
|
2280
|
+
numPigsPerFence: 1e4,
|
|
2281
|
+
threadsFenceConstruction: 16,
|
|
2282
|
+
threadsShowConstruction: 16,
|
|
2283
|
+
testSpins: [50, 100, 200],
|
|
2284
|
+
testSpinsWeights: [0.3, 0.4, 0.3],
|
|
2285
|
+
simulationTrials: 5e3,
|
|
2286
|
+
graphIndexes: [],
|
|
2287
|
+
run1000Batch: false,
|
|
2288
|
+
minMeanToMedian: 4,
|
|
2289
|
+
maxMeanToMedian: 8,
|
|
2290
|
+
pmbRtp: 1,
|
|
2291
|
+
scoreType: "rtp"
|
|
2292
|
+
};
|
|
2293
|
+
getParameters() {
|
|
2294
|
+
return this.parameters;
|
|
2295
|
+
}
|
|
2296
|
+
};
|
|
2297
|
+
|
|
2298
|
+
// src/optimizer/index.ts
|
|
2299
|
+
var Optimizer = class {
|
|
2300
|
+
gameConfig;
|
|
2301
|
+
gameModes;
|
|
2302
|
+
constructor(opts) {
|
|
2303
|
+
this.gameConfig = opts.game.getConfig();
|
|
2304
|
+
this.gameModes = opts.gameModes;
|
|
2305
|
+
this.verifyConfig();
|
|
2306
|
+
}
|
|
2307
|
+
/**
|
|
2308
|
+
* Runs the optimization process, and runs analysis after.
|
|
2309
|
+
*/
|
|
2310
|
+
async runOptimization({ gameModes }) {
|
|
2311
|
+
if (!import_worker_threads3.isMainThread) return;
|
|
2312
|
+
const mathConfig = makeMathConfig(this, { writeToFile: true });
|
|
2700
2313
|
for (const mode of gameModes) {
|
|
2701
2314
|
const setupFile = makeSetupFile(this, mode);
|
|
2702
2315
|
await this.runSingleOptimization();
|
|
@@ -2720,176 +2333,941 @@ var Optimizer = class {
|
|
|
2720
2333
|
for (const condition of conditions) {
|
|
2721
2334
|
if (!configMode.resultSets.find((r) => r.criteria === condition)) {
|
|
2722
2335
|
throw new Error(
|
|
2723
|
-
`Condition "${condition}" defined in optimizer config for game mode "${k}" does not exist as criteria in any ResultSet of the same game mode.`
|
|
2336
|
+
`Condition "${condition}" defined in optimizer config for game mode "${k}" does not exist as criteria in any ResultSet of the same game mode.`
|
|
2337
|
+
);
|
|
2338
|
+
}
|
|
2339
|
+
}
|
|
2340
|
+
const criteria = configMode.resultSets.map((r) => r.criteria);
|
|
2341
|
+
(0, import_assert9.default)(
|
|
2342
|
+
conditions.every((c) => criteria.includes(c)),
|
|
2343
|
+
`Not all ResultSet criteria in game mode "${k}" are defined as optimization conditions.`
|
|
2344
|
+
);
|
|
2345
|
+
let gameModeRtp = configMode.rtp;
|
|
2346
|
+
let paramRtp = 0;
|
|
2347
|
+
for (const cond of conditions) {
|
|
2348
|
+
const paramConfig = mode.conditions[cond];
|
|
2349
|
+
paramRtp += Number(paramConfig.getRtp());
|
|
2350
|
+
}
|
|
2351
|
+
gameModeRtp = Math.round(gameModeRtp * 1e3) / 1e3;
|
|
2352
|
+
paramRtp = Math.round(paramRtp * 1e3) / 1e3;
|
|
2353
|
+
(0, import_assert9.default)(
|
|
2354
|
+
gameModeRtp === paramRtp,
|
|
2355
|
+
`Sum of all RTP conditions (${paramRtp}) does not match the game mode RTP (${gameModeRtp}) in game mode "${k}".`
|
|
2356
|
+
);
|
|
2357
|
+
}
|
|
2358
|
+
}
|
|
2359
|
+
getGameConfig() {
|
|
2360
|
+
return this.gameConfig;
|
|
2361
|
+
}
|
|
2362
|
+
getOptimizerGameModes() {
|
|
2363
|
+
return this.gameModes;
|
|
2364
|
+
}
|
|
2365
|
+
};
|
|
2366
|
+
async function rustProgram(...args) {
|
|
2367
|
+
return new Promise((resolve, reject) => {
|
|
2368
|
+
const task = (0, import_child_process.spawn)("cargo", ["run", "--release", ...args], {
|
|
2369
|
+
shell: true,
|
|
2370
|
+
cwd: import_path5.default.join(__dirname, "./optimizer-rust"),
|
|
2371
|
+
stdio: "pipe"
|
|
2372
|
+
});
|
|
2373
|
+
task.on("error", (error) => {
|
|
2374
|
+
console.error("Error:", error);
|
|
2375
|
+
reject(error);
|
|
2376
|
+
});
|
|
2377
|
+
task.on("exit", () => {
|
|
2378
|
+
resolve(true);
|
|
2379
|
+
});
|
|
2380
|
+
task.on("close", () => {
|
|
2381
|
+
resolve(true);
|
|
2382
|
+
});
|
|
2383
|
+
task.stdout.on("data", (data) => {
|
|
2384
|
+
console.log(data.toString());
|
|
2385
|
+
});
|
|
2386
|
+
task.stderr.on("data", (data) => {
|
|
2387
|
+
console.log(data.toString());
|
|
2388
|
+
});
|
|
2389
|
+
task.stdout.on("error", (data) => {
|
|
2390
|
+
console.log(data.toString());
|
|
2391
|
+
reject(data.toString());
|
|
2392
|
+
});
|
|
2393
|
+
});
|
|
2394
|
+
}
|
|
2395
|
+
|
|
2396
|
+
// src/slot-game/index.ts
|
|
2397
|
+
var SlotGame = class {
|
|
2398
|
+
configOpts;
|
|
2399
|
+
simulation;
|
|
2400
|
+
optimizer;
|
|
2401
|
+
analyzer;
|
|
2402
|
+
constructor(config) {
|
|
2403
|
+
this.configOpts = config;
|
|
2404
|
+
}
|
|
2405
|
+
/**
|
|
2406
|
+
* Sets up the simulation configuration.\
|
|
2407
|
+
* Must be called before `runTasks()`.
|
|
2408
|
+
*/
|
|
2409
|
+
configureSimulation(opts) {
|
|
2410
|
+
this.simulation = new Simulation(opts, this.configOpts);
|
|
2411
|
+
}
|
|
2412
|
+
/**
|
|
2413
|
+
* Sets up the optimization configuration.\
|
|
2414
|
+
* Must be called before `runTasks()`.
|
|
2415
|
+
*/
|
|
2416
|
+
configureOptimization(opts) {
|
|
2417
|
+
this.optimizer = new Optimizer({
|
|
2418
|
+
game: this,
|
|
2419
|
+
gameModes: opts.gameModes
|
|
2420
|
+
});
|
|
2421
|
+
}
|
|
2422
|
+
/**
|
|
2423
|
+
* Runs the simulation based on the configured settings.
|
|
2424
|
+
*/
|
|
2425
|
+
async runSimulation(opts = {}) {
|
|
2426
|
+
if (!this.simulation) {
|
|
2427
|
+
throw new Error(
|
|
2428
|
+
"Simulation is not configured. Do so by calling configureSimulation() first."
|
|
2429
|
+
);
|
|
2430
|
+
}
|
|
2431
|
+
await this.simulation.runSimulation(opts);
|
|
2432
|
+
}
|
|
2433
|
+
/**
|
|
2434
|
+
* Runs the optimization based on the configured settings.
|
|
2435
|
+
*/
|
|
2436
|
+
async runOptimization(opts) {
|
|
2437
|
+
if (!this.optimizer) {
|
|
2438
|
+
throw new Error(
|
|
2439
|
+
"Optimization is not configured. Do so by calling configureOptimization() first."
|
|
2440
|
+
);
|
|
2441
|
+
}
|
|
2442
|
+
await this.optimizer.runOptimization(opts);
|
|
2443
|
+
}
|
|
2444
|
+
/**
|
|
2445
|
+
* Runs the analysis based on the configured settings.
|
|
2446
|
+
*/
|
|
2447
|
+
async runAnalysis(opts) {
|
|
2448
|
+
if (!this.optimizer) {
|
|
2449
|
+
throw new Error(
|
|
2450
|
+
"Optimization must be configured to run analysis. Do so by calling configureOptimization() first."
|
|
2451
|
+
);
|
|
2452
|
+
}
|
|
2453
|
+
this.analyzer = new Analysis(this.optimizer);
|
|
2454
|
+
await this.analyzer.runAnalysis(opts.gameModes);
|
|
2455
|
+
}
|
|
2456
|
+
/**
|
|
2457
|
+
* Runs the configured tasks: simulation, optimization, and/or analysis.
|
|
2458
|
+
*/
|
|
2459
|
+
async runTasks(opts = {}) {
|
|
2460
|
+
if (!opts.doSimulation && !opts.doOptimization && !opts.doAnalysis) {
|
|
2461
|
+
console.log("No tasks to run. Enable either simulation, optimization or analysis.");
|
|
2462
|
+
}
|
|
2463
|
+
if (opts.doSimulation) {
|
|
2464
|
+
await this.runSimulation(opts.simulationOpts || {});
|
|
2465
|
+
}
|
|
2466
|
+
if (opts.doOptimization) {
|
|
2467
|
+
await this.runOptimization(opts.optimizationOpts || { gameModes: [] });
|
|
2468
|
+
}
|
|
2469
|
+
if (opts.doAnalysis) {
|
|
2470
|
+
await this.runAnalysis(opts.analysisOpts || { gameModes: [] });
|
|
2471
|
+
}
|
|
2472
|
+
}
|
|
2473
|
+
/**
|
|
2474
|
+
* Gets the game configuration.
|
|
2475
|
+
*/
|
|
2476
|
+
getConfig() {
|
|
2477
|
+
return createGameConfig(this.configOpts);
|
|
2478
|
+
}
|
|
2479
|
+
};
|
|
2480
|
+
|
|
2481
|
+
// src/createSlotGame.ts
|
|
2482
|
+
function createSlotGame(opts) {
|
|
2483
|
+
return new SlotGame(opts);
|
|
2484
|
+
}
|
|
2485
|
+
var defineUserState = (data) => data;
|
|
2486
|
+
var defineSymbols = (symbols) => symbols;
|
|
2487
|
+
var defineGameModes = (gameModes) => gameModes;
|
|
2488
|
+
|
|
2489
|
+
// src/game-mode/index.ts
|
|
2490
|
+
var import_assert10 = __toESM(require("assert"));
|
|
2491
|
+
var GameMode = class {
|
|
2492
|
+
name;
|
|
2493
|
+
reelsAmount;
|
|
2494
|
+
symbolsPerReel;
|
|
2495
|
+
cost;
|
|
2496
|
+
rtp;
|
|
2497
|
+
reelSets;
|
|
2498
|
+
resultSets;
|
|
2499
|
+
isBonusBuy;
|
|
2500
|
+
constructor(opts) {
|
|
2501
|
+
this.name = opts.name;
|
|
2502
|
+
this.reelsAmount = opts.reelsAmount;
|
|
2503
|
+
this.symbolsPerReel = opts.symbolsPerReel;
|
|
2504
|
+
this.cost = opts.cost;
|
|
2505
|
+
this.rtp = opts.rtp;
|
|
2506
|
+
this.reelSets = opts.reelSets;
|
|
2507
|
+
this.resultSets = opts.resultSets;
|
|
2508
|
+
this.isBonusBuy = opts.isBonusBuy;
|
|
2509
|
+
(0, import_assert10.default)(this.rtp >= 0.9 && this.rtp <= 0.99, "RTP must be between 0.9 and 0.99");
|
|
2510
|
+
(0, import_assert10.default)(
|
|
2511
|
+
this.symbolsPerReel.length === this.reelsAmount,
|
|
2512
|
+
"symbolsPerReel length must match reelsAmount."
|
|
2513
|
+
);
|
|
2514
|
+
(0, import_assert10.default)(this.reelSets.length > 0, "GameMode must have at least one ReelSet defined.");
|
|
2515
|
+
}
|
|
2516
|
+
};
|
|
2517
|
+
|
|
2518
|
+
// src/win-types/index.ts
|
|
2519
|
+
var WinType = class {
|
|
2520
|
+
payout;
|
|
2521
|
+
winCombinations;
|
|
2522
|
+
ctx;
|
|
2523
|
+
wildSymbol;
|
|
2524
|
+
constructor(opts) {
|
|
2525
|
+
this.ctx = opts.ctx;
|
|
2526
|
+
this.payout = 0;
|
|
2527
|
+
this.winCombinations = [];
|
|
2528
|
+
this.wildSymbol = opts?.wildSymbol;
|
|
2529
|
+
}
|
|
2530
|
+
/**
|
|
2531
|
+
* Implementation of win evaluation logic. Sets `this.payout` and `this.winCombinations`.
|
|
2532
|
+
*/
|
|
2533
|
+
evaluateWins(board) {
|
|
2534
|
+
return this;
|
|
2535
|
+
}
|
|
2536
|
+
/**
|
|
2537
|
+
* Custom post-processing of wins, e.g. for handling multipliers.
|
|
2538
|
+
*/
|
|
2539
|
+
postProcess(func) {
|
|
2540
|
+
const result = func(this, this.ctx);
|
|
2541
|
+
this.payout = result.payout;
|
|
2542
|
+
this.winCombinations = result.winCombinations;
|
|
2543
|
+
return this;
|
|
2544
|
+
}
|
|
2545
|
+
/**
|
|
2546
|
+
* Returns the total payout and detailed win combinations.
|
|
2547
|
+
*/
|
|
2548
|
+
getWins() {
|
|
2549
|
+
return {
|
|
2550
|
+
payout: this.payout,
|
|
2551
|
+
winCombinations: this.winCombinations
|
|
2552
|
+
};
|
|
2553
|
+
}
|
|
2554
|
+
isWild(symbol) {
|
|
2555
|
+
return !!this.wildSymbol && symbol.compare(this.wildSymbol);
|
|
2556
|
+
}
|
|
2557
|
+
};
|
|
2558
|
+
|
|
2559
|
+
// src/win-types/LinesWinType.ts
|
|
2560
|
+
var LinesWinType = class extends WinType {
|
|
2561
|
+
lines;
|
|
2562
|
+
constructor(opts) {
|
|
2563
|
+
super(opts);
|
|
2564
|
+
this.lines = opts.lines;
|
|
2565
|
+
if (Object.keys(this.lines).length === 0) {
|
|
2566
|
+
throw new Error("LinesWinType must have at least one line defined.");
|
|
2567
|
+
}
|
|
2568
|
+
}
|
|
2569
|
+
validateConfig() {
|
|
2570
|
+
const reelsAmount = this.ctx.services.game.getCurrentGameMode().reelsAmount;
|
|
2571
|
+
const symsPerReel = this.ctx.services.game.getCurrentGameMode().symbolsPerReel;
|
|
2572
|
+
for (const [lineNum, positions] of Object.entries(this.lines)) {
|
|
2573
|
+
if (positions.length !== reelsAmount) {
|
|
2574
|
+
throw new Error(
|
|
2575
|
+
`Line ${lineNum} has ${positions.length} positions, but the current game mode has ${reelsAmount} reels.`
|
|
2576
|
+
);
|
|
2577
|
+
}
|
|
2578
|
+
for (let i = 0; i < positions.length; i++) {
|
|
2579
|
+
if (positions[i] < 0 || positions[i] >= symsPerReel[i]) {
|
|
2580
|
+
throw new Error(
|
|
2581
|
+
`Line ${lineNum} has an invalid position ${positions[i]} on reel ${i}. Valid range is 0 to ${symsPerReel[i] - 1}.`
|
|
2582
|
+
);
|
|
2583
|
+
}
|
|
2584
|
+
}
|
|
2585
|
+
}
|
|
2586
|
+
const firstLine = Math.min(...Object.keys(this.lines).map(Number));
|
|
2587
|
+
if (firstLine !== 1) {
|
|
2588
|
+
throw new Error(
|
|
2589
|
+
`Lines must start from 1. Found line ${firstLine} as the first line.`
|
|
2590
|
+
);
|
|
2591
|
+
}
|
|
2592
|
+
}
|
|
2593
|
+
/**
|
|
2594
|
+
* Calculates wins based on the defined paylines and provided board state.\
|
|
2595
|
+
* Retrieve the results using `getWins()` after.
|
|
2596
|
+
*/
|
|
2597
|
+
evaluateWins(board) {
|
|
2598
|
+
this.validateConfig();
|
|
2599
|
+
const lineWins = [];
|
|
2600
|
+
let payout = 0;
|
|
2601
|
+
const reels = board;
|
|
2602
|
+
for (const [lineNumStr, lineDef] of Object.entries(this.lines)) {
|
|
2603
|
+
const lineNum = Number(lineNumStr);
|
|
2604
|
+
let baseSymbol = null;
|
|
2605
|
+
let leadingWilds = 0;
|
|
2606
|
+
const chain = [];
|
|
2607
|
+
const details = [];
|
|
2608
|
+
for (let ridx = 0; ridx < reels.length; ridx++) {
|
|
2609
|
+
const rowIdx = lineDef[ridx];
|
|
2610
|
+
const sym = reels[ridx][rowIdx];
|
|
2611
|
+
if (!sym) throw new Error("Encountered an invalid symbol while evaluating wins.");
|
|
2612
|
+
const wild = this.isWild(sym);
|
|
2613
|
+
if (ridx === 0) {
|
|
2614
|
+
chain.push(sym);
|
|
2615
|
+
details.push({ reelIndex: ridx, posIndex: rowIdx, symbol: sym, isWild: wild });
|
|
2616
|
+
if (wild) leadingWilds++;
|
|
2617
|
+
else baseSymbol = sym;
|
|
2618
|
+
continue;
|
|
2619
|
+
}
|
|
2620
|
+
if (wild) {
|
|
2621
|
+
chain.push(sym);
|
|
2622
|
+
details.push({
|
|
2623
|
+
reelIndex: ridx,
|
|
2624
|
+
posIndex: rowIdx,
|
|
2625
|
+
symbol: sym,
|
|
2626
|
+
isWild: true,
|
|
2627
|
+
substitutedFor: baseSymbol || void 0
|
|
2628
|
+
});
|
|
2629
|
+
continue;
|
|
2630
|
+
}
|
|
2631
|
+
if (!baseSymbol) {
|
|
2632
|
+
baseSymbol = sym;
|
|
2633
|
+
chain.push(sym);
|
|
2634
|
+
details.push({ reelIndex: ridx, posIndex: rowIdx, symbol: sym, isWild: false });
|
|
2635
|
+
continue;
|
|
2636
|
+
}
|
|
2637
|
+
if (sym.id === baseSymbol.id) {
|
|
2638
|
+
chain.push(sym);
|
|
2639
|
+
details.push({ reelIndex: ridx, posIndex: rowIdx, symbol: sym, isWild: false });
|
|
2640
|
+
continue;
|
|
2641
|
+
}
|
|
2642
|
+
break;
|
|
2643
|
+
}
|
|
2644
|
+
if (chain.length === 0) continue;
|
|
2645
|
+
const allWild = chain.every((s) => this.isWild(s));
|
|
2646
|
+
const wildRepresentative = this.wildSymbol instanceof GameSymbol ? this.wildSymbol : null;
|
|
2647
|
+
const len = chain.length;
|
|
2648
|
+
let bestPayout = 0;
|
|
2649
|
+
let bestType = null;
|
|
2650
|
+
let payingSymbol = null;
|
|
2651
|
+
if (baseSymbol?.pays && baseSymbol.pays[len]) {
|
|
2652
|
+
bestPayout = baseSymbol.pays[len];
|
|
2653
|
+
bestType = "substituted";
|
|
2654
|
+
payingSymbol = baseSymbol;
|
|
2655
|
+
}
|
|
2656
|
+
if (allWild && wildRepresentative?.pays && wildRepresentative.pays[len]) {
|
|
2657
|
+
const wildPay = wildRepresentative.pays[len];
|
|
2658
|
+
if (wildPay > bestPayout) {
|
|
2659
|
+
bestPayout = wildPay;
|
|
2660
|
+
bestType = "pure-wild";
|
|
2661
|
+
payingSymbol = wildRepresentative;
|
|
2662
|
+
}
|
|
2663
|
+
}
|
|
2664
|
+
if (!bestPayout || !bestType || !payingSymbol) continue;
|
|
2665
|
+
const minLen = payingSymbol.pays ? Math.min(...Object.keys(payingSymbol.pays).map(Number)) : Infinity;
|
|
2666
|
+
if (len < minLen) continue;
|
|
2667
|
+
const wildCount = details.filter((d) => d.isWild).length;
|
|
2668
|
+
const nonWildCount = len - wildCount;
|
|
2669
|
+
lineWins.push({
|
|
2670
|
+
lineNumber: lineNum,
|
|
2671
|
+
kind: len,
|
|
2672
|
+
payout: bestPayout,
|
|
2673
|
+
symbol: payingSymbol,
|
|
2674
|
+
winType: bestType,
|
|
2675
|
+
substitutedBaseSymbol: bestType === "pure-wild" ? null : baseSymbol,
|
|
2676
|
+
symbols: details,
|
|
2677
|
+
stats: { wildCount, nonWildCount, leadingWilds }
|
|
2678
|
+
});
|
|
2679
|
+
payout += bestPayout;
|
|
2680
|
+
}
|
|
2681
|
+
for (const win of lineWins) {
|
|
2682
|
+
this.ctx.services.data.recordSymbolOccurrence({
|
|
2683
|
+
kind: win.kind,
|
|
2684
|
+
symbolId: win.symbol.id,
|
|
2685
|
+
spinType: this.ctx.state.currentSpinType
|
|
2686
|
+
});
|
|
2687
|
+
}
|
|
2688
|
+
this.payout = payout;
|
|
2689
|
+
this.winCombinations = lineWins;
|
|
2690
|
+
return this;
|
|
2691
|
+
}
|
|
2692
|
+
};
|
|
2693
|
+
|
|
2694
|
+
// src/win-types/ClusterWinType.ts
|
|
2695
|
+
var ClusterWinType = class extends WinType {
|
|
2696
|
+
};
|
|
2697
|
+
|
|
2698
|
+
// src/win-types/ManywaysWinType.ts
|
|
2699
|
+
var ManywaysWinType = class extends WinType {
|
|
2700
|
+
};
|
|
2701
|
+
|
|
2702
|
+
// src/reel-set/GeneratedReelSet.ts
|
|
2703
|
+
var import_fs5 = __toESM(require("fs"));
|
|
2704
|
+
var import_path7 = __toESM(require("path"));
|
|
2705
|
+
var import_worker_threads4 = require("worker_threads");
|
|
2706
|
+
|
|
2707
|
+
// src/reel-set/index.ts
|
|
2708
|
+
var import_fs4 = __toESM(require("fs"));
|
|
2709
|
+
var import_path6 = __toESM(require("path"));
|
|
2710
|
+
var ReelSet = class {
|
|
2711
|
+
id;
|
|
2712
|
+
associatedGameModeName;
|
|
2713
|
+
reels;
|
|
2714
|
+
rng;
|
|
2715
|
+
constructor(opts) {
|
|
2716
|
+
this.id = opts.id;
|
|
2717
|
+
this.associatedGameModeName = "";
|
|
2718
|
+
this.reels = [];
|
|
2719
|
+
this.rng = new RandomNumberGenerator();
|
|
2720
|
+
this.rng.setSeed(opts.seed ?? 0);
|
|
2721
|
+
}
|
|
2722
|
+
generateReels(simulation) {
|
|
2723
|
+
throw new Error("Not implemented");
|
|
2724
|
+
}
|
|
2725
|
+
/**
|
|
2726
|
+
* Reads a reelset CSV file and returns the reels as arrays of GameSymbols.
|
|
2727
|
+
*/
|
|
2728
|
+
parseReelsetCSV(reelSetPath, config) {
|
|
2729
|
+
if (!import_fs4.default.existsSync(reelSetPath)) {
|
|
2730
|
+
throw new Error(`Reelset CSV file not found at path: ${reelSetPath}`);
|
|
2731
|
+
}
|
|
2732
|
+
const allowedExtensions = [".csv"];
|
|
2733
|
+
const ext = import_path6.default.extname(reelSetPath).toLowerCase();
|
|
2734
|
+
if (!allowedExtensions.includes(ext)) {
|
|
2735
|
+
throw new Error(
|
|
2736
|
+
`Invalid file extension for reelset CSV: ${ext}. Allowed extensions are: ${allowedExtensions.join(
|
|
2737
|
+
", "
|
|
2738
|
+
)}`
|
|
2739
|
+
);
|
|
2740
|
+
}
|
|
2741
|
+
const csvData = import_fs4.default.readFileSync(reelSetPath, "utf8");
|
|
2742
|
+
const rows = csvData.split("\n").filter((line) => line.trim() !== "");
|
|
2743
|
+
const reels = Array.from(
|
|
2744
|
+
{ length: config.gameModes[this.associatedGameModeName].reelsAmount },
|
|
2745
|
+
() => []
|
|
2746
|
+
);
|
|
2747
|
+
rows.forEach((row) => {
|
|
2748
|
+
const symsInRow = row.split(",").map((symbolId) => {
|
|
2749
|
+
const symbol = config.symbols.get(symbolId.trim());
|
|
2750
|
+
if (!symbol) {
|
|
2751
|
+
throw new Error(`Symbol with id "${symbolId}" not found in game config.`);
|
|
2752
|
+
}
|
|
2753
|
+
return symbol;
|
|
2754
|
+
});
|
|
2755
|
+
symsInRow.forEach((symbol, ridx) => {
|
|
2756
|
+
if (ridx >= reels.length) {
|
|
2757
|
+
throw new Error(
|
|
2758
|
+
`Row in reelset CSV has more symbols than expected reels amount (${reels.length})`
|
|
2759
|
+
);
|
|
2760
|
+
}
|
|
2761
|
+
reels[ridx].push(symbol);
|
|
2762
|
+
});
|
|
2763
|
+
});
|
|
2764
|
+
const reelLengths = reels.map((r) => r.length);
|
|
2765
|
+
const uniqueLengths = new Set(reelLengths);
|
|
2766
|
+
if (uniqueLengths.size > 1) {
|
|
2767
|
+
throw new Error(
|
|
2768
|
+
`Inconsistent reel lengths in reelset CSV at ${reelSetPath}: ${[
|
|
2769
|
+
...uniqueLengths
|
|
2770
|
+
].join(", ")}`
|
|
2771
|
+
);
|
|
2772
|
+
}
|
|
2773
|
+
return reels;
|
|
2774
|
+
}
|
|
2775
|
+
};
|
|
2776
|
+
|
|
2777
|
+
// src/reel-set/GeneratedReelSet.ts
|
|
2778
|
+
var GeneratedReelSet = class extends ReelSet {
|
|
2779
|
+
symbolWeights = /* @__PURE__ */ new Map();
|
|
2780
|
+
rowsAmount;
|
|
2781
|
+
limitSymbolsToReels;
|
|
2782
|
+
spaceBetweenSameSymbols;
|
|
2783
|
+
spaceBetweenSymbols;
|
|
2784
|
+
preferStackedSymbols;
|
|
2785
|
+
symbolStacks;
|
|
2786
|
+
symbolQuotas;
|
|
2787
|
+
overrideExisting;
|
|
2788
|
+
constructor(opts) {
|
|
2789
|
+
super(opts);
|
|
2790
|
+
this.id = opts.id;
|
|
2791
|
+
this.symbolWeights = new Map(Object.entries(opts.symbolWeights));
|
|
2792
|
+
this.rowsAmount = opts.rowsAmount || 250;
|
|
2793
|
+
if (opts.limitSymbolsToReels) this.limitSymbolsToReels = opts.limitSymbolsToReels;
|
|
2794
|
+
this.overrideExisting = opts.overrideExisting || false;
|
|
2795
|
+
this.spaceBetweenSameSymbols = opts.spaceBetweenSameSymbols;
|
|
2796
|
+
this.spaceBetweenSymbols = opts.spaceBetweenSymbols;
|
|
2797
|
+
this.preferStackedSymbols = opts.preferStackedSymbols;
|
|
2798
|
+
this.symbolStacks = opts.symbolStacks;
|
|
2799
|
+
this.symbolQuotas = opts.symbolQuotas;
|
|
2800
|
+
if (typeof this.spaceBetweenSameSymbols == "number" && (this.spaceBetweenSameSymbols < 1 || this.spaceBetweenSameSymbols > 8) || typeof this.spaceBetweenSameSymbols == "object" && Object.values(this.spaceBetweenSameSymbols).some((v) => v < 1 || v > 8)) {
|
|
2801
|
+
throw new Error(
|
|
2802
|
+
`spaceBetweenSameSymbols must be between 1 and 8, got ${this.spaceBetweenSameSymbols}.`
|
|
2803
|
+
);
|
|
2804
|
+
}
|
|
2805
|
+
if (Object.values(this.spaceBetweenSymbols || {}).some(
|
|
2806
|
+
(o) => Object.values(o).some((v) => v < 1 || v > 8)
|
|
2807
|
+
)) {
|
|
2808
|
+
throw new Error(
|
|
2809
|
+
`spaceBetweenSymbols must be between 1 and 8, got ${this.spaceBetweenSymbols}.`
|
|
2810
|
+
);
|
|
2811
|
+
}
|
|
2812
|
+
if (this.preferStackedSymbols && (this.preferStackedSymbols < 0 || this.preferStackedSymbols > 100)) {
|
|
2813
|
+
throw new Error(
|
|
2814
|
+
`preferStackedSymbols must be between 0 and 100, got ${this.preferStackedSymbols}.`
|
|
2815
|
+
);
|
|
2816
|
+
}
|
|
2817
|
+
}
|
|
2818
|
+
validateConfig(config) {
|
|
2819
|
+
this.symbolWeights.forEach((_, symbol) => {
|
|
2820
|
+
if (!config.symbols.has(symbol)) {
|
|
2821
|
+
throw new Error(
|
|
2822
|
+
`Symbol "${symbol}" of the reel generator ${this.id} for mode ${this.associatedGameModeName} is not defined in the game config`
|
|
2823
|
+
);
|
|
2824
|
+
}
|
|
2825
|
+
});
|
|
2826
|
+
if (this.limitSymbolsToReels && Object.keys(this.limitSymbolsToReels).length == 0) {
|
|
2827
|
+
this.limitSymbolsToReels = void 0;
|
|
2828
|
+
}
|
|
2829
|
+
}
|
|
2830
|
+
isSymbolAllowedOnReel(symbolId, reelIdx) {
|
|
2831
|
+
if (!this.limitSymbolsToReels) return true;
|
|
2832
|
+
const allowedReels = this.limitSymbolsToReels[symbolId];
|
|
2833
|
+
if (!allowedReels || allowedReels.length === 0) return true;
|
|
2834
|
+
return allowedReels.includes(reelIdx);
|
|
2835
|
+
}
|
|
2836
|
+
resolveStacking(symbolId, reelIdx) {
|
|
2837
|
+
const cfg = this.symbolStacks?.[symbolId];
|
|
2838
|
+
if (!cfg) return null;
|
|
2839
|
+
const STACKING_MIN = 1;
|
|
2840
|
+
const STACKING_MAX = 4;
|
|
2841
|
+
const chance = typeof cfg.chance === "number" ? cfg.chance : cfg.chance?.[reelIdx] ?? 0;
|
|
2842
|
+
if (chance <= 0) return null;
|
|
2843
|
+
let min = typeof cfg.min === "number" ? cfg.min : cfg.min?.[reelIdx] ?? STACKING_MIN;
|
|
2844
|
+
let max = typeof cfg.max === "number" ? cfg.max : cfg.max?.[reelIdx] ?? STACKING_MAX;
|
|
2845
|
+
return { chance, min, max };
|
|
2846
|
+
}
|
|
2847
|
+
tryPlaceStack(reel, config, reelIdx, symbolId, startIndex, maxStack) {
|
|
2848
|
+
if (!this.isSymbolAllowedOnReel(symbolId, reelIdx)) return 0;
|
|
2849
|
+
let canPlace = 0;
|
|
2850
|
+
for (let j = 0; j < maxStack; j++) {
|
|
2851
|
+
const idx = (startIndex + j) % this.rowsAmount;
|
|
2852
|
+
if (reel[idx] !== null) break;
|
|
2853
|
+
canPlace++;
|
|
2854
|
+
}
|
|
2855
|
+
if (canPlace === 0) return 0;
|
|
2856
|
+
const symObj = config.symbols.get(symbolId);
|
|
2857
|
+
if (!symObj) {
|
|
2858
|
+
throw new Error(
|
|
2859
|
+
`Symbol with id "${symbolId}" not found in the game config symbols map.`
|
|
2860
|
+
);
|
|
2861
|
+
}
|
|
2862
|
+
for (let j = 0; j < canPlace; j++) {
|
|
2863
|
+
const idx = (startIndex + j) % reel.length;
|
|
2864
|
+
reel[idx] = symObj;
|
|
2865
|
+
}
|
|
2866
|
+
return canPlace;
|
|
2867
|
+
}
|
|
2868
|
+
/**
|
|
2869
|
+
* Checks if a symbol can be placed at the target index without violating spacing rules.
|
|
2870
|
+
*/
|
|
2871
|
+
violatesSpacing(reel, symbolId, targetIndex) {
|
|
2872
|
+
const circDist = (a, b) => {
|
|
2873
|
+
const diff = Math.abs(a - b);
|
|
2874
|
+
return Math.min(diff, this.rowsAmount - diff);
|
|
2875
|
+
};
|
|
2876
|
+
const spacingType = this.spaceBetweenSameSymbols ?? void 0;
|
|
2877
|
+
const sameSpacing = typeof spacingType === "number" ? spacingType : spacingType?.[symbolId] ?? 0;
|
|
2878
|
+
for (let i = 0; i <= reel.length; i++) {
|
|
2879
|
+
const placed = reel[i];
|
|
2880
|
+
if (!placed) continue;
|
|
2881
|
+
const dist = circDist(targetIndex, i);
|
|
2882
|
+
if (sameSpacing >= 1 && placed.id === symbolId) {
|
|
2883
|
+
if (dist <= sameSpacing) return true;
|
|
2884
|
+
}
|
|
2885
|
+
if (this.spaceBetweenSymbols) {
|
|
2886
|
+
const forward = this.spaceBetweenSymbols[symbolId]?.[placed.id] ?? 0;
|
|
2887
|
+
if (forward >= 1 && dist <= forward) return true;
|
|
2888
|
+
const reverse = this.spaceBetweenSymbols[placed.id]?.[symbolId] ?? 0;
|
|
2889
|
+
if (reverse >= 1 && dist <= reverse) return true;
|
|
2890
|
+
}
|
|
2891
|
+
}
|
|
2892
|
+
return false;
|
|
2893
|
+
}
|
|
2894
|
+
generateReels({ gameConfig: config }) {
|
|
2895
|
+
this.validateConfig(config);
|
|
2896
|
+
const gameMode = config.gameModes[this.associatedGameModeName];
|
|
2897
|
+
if (!gameMode) {
|
|
2898
|
+
throw new Error(
|
|
2899
|
+
`Error generating reels for game mode "${this.associatedGameModeName}". It's not defined in the game config.`
|
|
2900
|
+
);
|
|
2901
|
+
}
|
|
2902
|
+
const filePath = import_path7.default.join(
|
|
2903
|
+
config.outputDir,
|
|
2904
|
+
`reels_${this.associatedGameModeName}-${this.id}.csv`
|
|
2905
|
+
);
|
|
2906
|
+
const exists = import_fs5.default.existsSync(filePath);
|
|
2907
|
+
if (exists && !this.overrideExisting) {
|
|
2908
|
+
this.reels = this.parseReelsetCSV(filePath, config);
|
|
2909
|
+
return;
|
|
2910
|
+
}
|
|
2911
|
+
if (!exists && this.symbolWeights.size === 0) {
|
|
2912
|
+
throw new Error(
|
|
2913
|
+
`Cannot generate reels for generator "${this.id}" of mode "${this.associatedGameModeName}" because the symbol weights are empty.`
|
|
2914
|
+
);
|
|
2915
|
+
}
|
|
2916
|
+
const reelsAmount = gameMode.reelsAmount;
|
|
2917
|
+
const weightsObj = Object.fromEntries(this.symbolWeights);
|
|
2918
|
+
for (let ridx = 0; ridx < reelsAmount; ridx++) {
|
|
2919
|
+
const reel = new Array(this.rowsAmount).fill(null);
|
|
2920
|
+
const reelQuotas = {};
|
|
2921
|
+
const quotaCounts = {};
|
|
2922
|
+
let totalReelsQuota = 0;
|
|
2923
|
+
for (const [sym, quotaConf] of Object.entries(this.symbolQuotas || {})) {
|
|
2924
|
+
const q = typeof quotaConf === "number" ? quotaConf : quotaConf[ridx];
|
|
2925
|
+
if (!q) continue;
|
|
2926
|
+
reelQuotas[sym] = q;
|
|
2927
|
+
totalReelsQuota += q;
|
|
2928
|
+
}
|
|
2929
|
+
if (totalReelsQuota > 100) {
|
|
2930
|
+
throw new Error(
|
|
2931
|
+
`Total symbol quotas for reel ${ridx} exceed 100%. Adjust your configuration on reel set "${this.id}".`
|
|
2932
|
+
);
|
|
2933
|
+
}
|
|
2934
|
+
if (totalReelsQuota > 0) {
|
|
2935
|
+
for (const [sym, quota] of Object.entries(reelQuotas)) {
|
|
2936
|
+
const quotaCount = Math.max(1, Math.floor(this.rowsAmount * quota / 100));
|
|
2937
|
+
quotaCounts[sym] = quotaCount;
|
|
2938
|
+
}
|
|
2939
|
+
}
|
|
2940
|
+
for (const [sym, targetCount] of Object.entries(quotaCounts)) {
|
|
2941
|
+
let remaining = targetCount;
|
|
2942
|
+
let attempts = 0;
|
|
2943
|
+
while (remaining > 0) {
|
|
2944
|
+
if (attempts++ > this.rowsAmount * 10) {
|
|
2945
|
+
throw new Error(
|
|
2946
|
+
`Failed to place ${targetCount} of symbol ${sym} on reel ${ridx} (likely spacing/stacking too strict).`
|
|
2947
|
+
);
|
|
2948
|
+
}
|
|
2949
|
+
const pos = Math.round(this.rng.randomFloat(0, this.rowsAmount - 1));
|
|
2950
|
+
const stackCfg = this.resolveStacking(sym, ridx);
|
|
2951
|
+
let placed = 0;
|
|
2952
|
+
if (stackCfg && Math.round(this.rng.randomFloat(1, 100)) <= stackCfg.chance) {
|
|
2953
|
+
const stackSize = Math.max(
|
|
2954
|
+
0,
|
|
2955
|
+
Math.round(this.rng.randomFloat(stackCfg.min, stackCfg.max))
|
|
2956
|
+
);
|
|
2957
|
+
const toPlace = Math.min(stackSize, remaining);
|
|
2958
|
+
placed = this.tryPlaceStack(reel, config, ridx, sym, pos, toPlace);
|
|
2959
|
+
}
|
|
2960
|
+
if (placed === 0 && reel[pos] === null && this.isSymbolAllowedOnReel(sym, ridx) && !this.violatesSpacing(reel, sym, pos)) {
|
|
2961
|
+
reel[pos] = config.symbols.get(sym);
|
|
2962
|
+
placed = 1;
|
|
2963
|
+
}
|
|
2964
|
+
remaining -= placed;
|
|
2965
|
+
}
|
|
2966
|
+
}
|
|
2967
|
+
for (let r = 0; r < this.rowsAmount; r++) {
|
|
2968
|
+
if (reel[r] !== null) continue;
|
|
2969
|
+
let chosenSymbolId = this.rng.weightedRandom(weightsObj);
|
|
2970
|
+
const nextHasStackCfg = !!this.resolveStacking(chosenSymbolId, ridx);
|
|
2971
|
+
if (!nextHasStackCfg && this.preferStackedSymbols && reel.length > 0) {
|
|
2972
|
+
const prevSymbol = r - 1 >= 0 ? reel[r - 1] : reel[reel.length - 1];
|
|
2973
|
+
if (prevSymbol && Math.round(this.rng.randomFloat(1, 100)) <= this.preferStackedSymbols && (!this.spaceBetweenSameSymbols || !this.violatesSpacing(reel, prevSymbol.id, r))) {
|
|
2974
|
+
chosenSymbolId = prevSymbol.id;
|
|
2975
|
+
}
|
|
2976
|
+
}
|
|
2977
|
+
if (this.preferStackedSymbols && reel.length > 0) {
|
|
2978
|
+
const prevSymbol = r - 1 >= 0 ? reel[r - 1] : reel[reel.length - 1];
|
|
2979
|
+
if (Math.round(this.rng.randomFloat(1, 100)) <= this.preferStackedSymbols && (!this.spaceBetweenSameSymbols || !this.violatesSpacing(reel, prevSymbol.id, r))) {
|
|
2980
|
+
chosenSymbolId = prevSymbol.id;
|
|
2981
|
+
}
|
|
2982
|
+
}
|
|
2983
|
+
const stackCfg = this.resolveStacking(chosenSymbolId, ridx);
|
|
2984
|
+
if (stackCfg && this.isSymbolAllowedOnReel(chosenSymbolId, ridx)) {
|
|
2985
|
+
const roll = Math.round(this.rng.randomFloat(1, 100));
|
|
2986
|
+
if (roll <= stackCfg.chance) {
|
|
2987
|
+
const desiredSize = Math.max(
|
|
2988
|
+
1,
|
|
2989
|
+
Math.round(this.rng.randomFloat(stackCfg.min, stackCfg.max))
|
|
2990
|
+
);
|
|
2991
|
+
const placed = this.tryPlaceStack(
|
|
2992
|
+
reel,
|
|
2993
|
+
config,
|
|
2994
|
+
ridx,
|
|
2995
|
+
chosenSymbolId,
|
|
2996
|
+
r,
|
|
2997
|
+
desiredSize
|
|
2998
|
+
);
|
|
2999
|
+
if (placed > 0) {
|
|
3000
|
+
r += placed - 1;
|
|
3001
|
+
continue;
|
|
3002
|
+
}
|
|
3003
|
+
}
|
|
3004
|
+
}
|
|
3005
|
+
let tries = 0;
|
|
3006
|
+
const maxTries = 2500;
|
|
3007
|
+
while (!this.isSymbolAllowedOnReel(chosenSymbolId, ridx) || this.violatesSpacing(reel, chosenSymbolId, r)) {
|
|
3008
|
+
if (++tries > maxTries) {
|
|
3009
|
+
throw new Error(
|
|
3010
|
+
[
|
|
3011
|
+
`Failed to place a symbol on reel ${ridx} at position ${r} after ${maxTries} attempts.
|
|
3012
|
+
`,
|
|
3013
|
+
"Try to change the seed or adjust your configuration.\n"
|
|
3014
|
+
].join(" ")
|
|
3015
|
+
);
|
|
3016
|
+
}
|
|
3017
|
+
chosenSymbolId = this.rng.weightedRandom(weightsObj);
|
|
3018
|
+
const hasStackCfg = !!this.resolveStacking(chosenSymbolId, ridx);
|
|
3019
|
+
if (!hasStackCfg && this.preferStackedSymbols && reel.length > 0) {
|
|
3020
|
+
const prevSymbol = r - 1 >= 0 ? reel[r - 1] : reel[reel.length - 1];
|
|
3021
|
+
if (prevSymbol && Math.round(this.rng.randomFloat(1, 100)) <= this.preferStackedSymbols && (!this.spaceBetweenSameSymbols || !this.violatesSpacing(reel, prevSymbol.id, r))) {
|
|
3022
|
+
chosenSymbolId = prevSymbol.id;
|
|
3023
|
+
}
|
|
3024
|
+
}
|
|
3025
|
+
}
|
|
3026
|
+
const symbol = config.symbols.get(chosenSymbolId);
|
|
3027
|
+
if (!symbol) {
|
|
3028
|
+
throw new Error(
|
|
3029
|
+
`Symbol with id "${chosenSymbolId}" not found in the game config symbols map.`
|
|
2724
3030
|
);
|
|
2725
3031
|
}
|
|
3032
|
+
reel[r] = symbol;
|
|
2726
3033
|
}
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
conditions.every((c) => criteria.includes(c)),
|
|
2730
|
-
`Not all ResultSet criteria in game mode "${k}" are defined as optimization conditions.`
|
|
2731
|
-
);
|
|
2732
|
-
let gameModeRtp = configMode.rtp;
|
|
2733
|
-
let paramRtp = 0;
|
|
2734
|
-
for (const cond of conditions) {
|
|
2735
|
-
const paramConfig = mode.conditions[cond];
|
|
2736
|
-
paramRtp += Number(paramConfig.getRtp());
|
|
3034
|
+
if (reel.some((s) => s === null)) {
|
|
3035
|
+
throw new Error(`Reel ${ridx} has unfilled positions after generation.`);
|
|
2737
3036
|
}
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
3037
|
+
this.reels.push(reel);
|
|
3038
|
+
}
|
|
3039
|
+
const csvRows = Array.from(
|
|
3040
|
+
{ length: this.rowsAmount },
|
|
3041
|
+
() => Array.from({ length: reelsAmount }, () => "")
|
|
3042
|
+
);
|
|
3043
|
+
for (let ridx = 0; ridx < reelsAmount; ridx++) {
|
|
3044
|
+
for (let r = 0; r < this.rowsAmount; r++) {
|
|
3045
|
+
csvRows[r][ridx] = this.reels[ridx][r].id;
|
|
3046
|
+
}
|
|
3047
|
+
}
|
|
3048
|
+
const csvString = csvRows.map((row) => row.join(",")).join("\n");
|
|
3049
|
+
if (import_worker_threads4.isMainThread) {
|
|
3050
|
+
import_fs5.default.writeFileSync(filePath, csvString);
|
|
3051
|
+
this.reels = this.parseReelsetCSV(filePath, config);
|
|
3052
|
+
console.log(
|
|
3053
|
+
`Generated reelset ${this.id} for game mode ${this.associatedGameModeName}`
|
|
2743
3054
|
);
|
|
2744
3055
|
}
|
|
2745
3056
|
}
|
|
2746
|
-
|
|
2747
|
-
|
|
3057
|
+
};
|
|
3058
|
+
|
|
3059
|
+
// src/reel-set/StaticReelSet.ts
|
|
3060
|
+
var import_assert11 = __toESM(require("assert"));
|
|
3061
|
+
var StaticReelSet = class extends ReelSet {
|
|
3062
|
+
reels;
|
|
3063
|
+
csvPath;
|
|
3064
|
+
_strReels;
|
|
3065
|
+
constructor(opts) {
|
|
3066
|
+
super(opts);
|
|
3067
|
+
this.reels = [];
|
|
3068
|
+
this._strReels = opts.reels || [];
|
|
3069
|
+
this.csvPath = opts.csvPath || "";
|
|
3070
|
+
(0, import_assert11.default)(
|
|
3071
|
+
opts.reels || opts.csvPath,
|
|
3072
|
+
`Either 'reels' or 'csvPath' must be provided for StaticReelSet ${this.id}`
|
|
3073
|
+
);
|
|
2748
3074
|
}
|
|
2749
|
-
|
|
2750
|
-
|
|
3075
|
+
validateConfig(config) {
|
|
3076
|
+
this.reels.forEach((reel) => {
|
|
3077
|
+
reel.forEach((symbol) => {
|
|
3078
|
+
if (!config.symbols.has(symbol.id)) {
|
|
3079
|
+
throw new Error(
|
|
3080
|
+
`Symbol "${symbol}" of the reel set ${this.id} for mode ${this.associatedGameModeName} is not defined in the game config`
|
|
3081
|
+
);
|
|
3082
|
+
}
|
|
3083
|
+
});
|
|
3084
|
+
});
|
|
3085
|
+
if (this.csvPath && this._strReels.length > 0) {
|
|
3086
|
+
throw new Error(
|
|
3087
|
+
`Both 'csvPath' and 'reels' are provided for StaticReelSet ${this.id}. Please provide only one.`
|
|
3088
|
+
);
|
|
3089
|
+
}
|
|
3090
|
+
}
|
|
3091
|
+
generateReels({ gameConfig: config }) {
|
|
3092
|
+
this.validateConfig(config);
|
|
3093
|
+
if (this._strReels.length > 0) {
|
|
3094
|
+
this.reels = this._strReels.map((reel) => {
|
|
3095
|
+
return reel.map((symbolId) => {
|
|
3096
|
+
const symbol = config.symbols.get(symbolId);
|
|
3097
|
+
if (!symbol) {
|
|
3098
|
+
throw new Error(
|
|
3099
|
+
`Symbol "${symbolId}" of the reel set ${this.id} for mode ${this.associatedGameModeName} is not defined in the game config`
|
|
3100
|
+
);
|
|
3101
|
+
}
|
|
3102
|
+
return symbol;
|
|
3103
|
+
});
|
|
3104
|
+
});
|
|
3105
|
+
}
|
|
3106
|
+
if (this.csvPath) {
|
|
3107
|
+
this.reels = this.parseReelsetCSV(this.csvPath, config);
|
|
3108
|
+
}
|
|
2751
3109
|
}
|
|
2752
3110
|
};
|
|
2753
|
-
async function rustProgram(...args) {
|
|
2754
|
-
return new Promise((resolve, reject) => {
|
|
2755
|
-
const task = (0, import_child_process.spawn)("cargo", ["run", "--release", ...args], {
|
|
2756
|
-
shell: true,
|
|
2757
|
-
cwd: import_path6.default.join(__dirname, "./optimizer-rust"),
|
|
2758
|
-
stdio: "pipe"
|
|
2759
|
-
});
|
|
2760
|
-
task.on("error", (error) => {
|
|
2761
|
-
console.error("Error:", error);
|
|
2762
|
-
reject(error);
|
|
2763
|
-
});
|
|
2764
|
-
task.on("exit", () => {
|
|
2765
|
-
resolve(true);
|
|
2766
|
-
});
|
|
2767
|
-
task.on("close", () => {
|
|
2768
|
-
resolve(true);
|
|
2769
|
-
});
|
|
2770
|
-
task.stdout.on("data", (data) => {
|
|
2771
|
-
console.log(data.toString());
|
|
2772
|
-
});
|
|
2773
|
-
task.stderr.on("data", (data) => {
|
|
2774
|
-
console.log(data.toString());
|
|
2775
|
-
});
|
|
2776
|
-
task.stdout.on("error", (data) => {
|
|
2777
|
-
console.log(data.toString());
|
|
2778
|
-
reject(data.toString());
|
|
2779
|
-
});
|
|
2780
|
-
});
|
|
2781
|
-
}
|
|
2782
3111
|
|
|
2783
|
-
// src/
|
|
2784
|
-
var
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
3112
|
+
// src/board/StandaloneBoard.ts
|
|
3113
|
+
var StandaloneBoard = class {
|
|
3114
|
+
board;
|
|
3115
|
+
ctx;
|
|
3116
|
+
reelsAmount;
|
|
3117
|
+
symbolsPerReel;
|
|
3118
|
+
padSymbols;
|
|
3119
|
+
constructor(opts) {
|
|
3120
|
+
this.board = new Board();
|
|
3121
|
+
this.ctx = opts.ctx;
|
|
3122
|
+
this.reelsAmount = opts.reelsAmount;
|
|
3123
|
+
this.symbolsPerReel = opts.symbolsPerReel;
|
|
3124
|
+
this.padSymbols = opts.padSymbols;
|
|
2791
3125
|
}
|
|
2792
3126
|
/**
|
|
2793
|
-
*
|
|
2794
|
-
*
|
|
3127
|
+
* Resets the board to an empty state.\
|
|
3128
|
+
* This is called before drawing a new board.
|
|
2795
3129
|
*/
|
|
2796
|
-
|
|
2797
|
-
this.
|
|
3130
|
+
resetBoard() {
|
|
3131
|
+
this.resetReels();
|
|
3132
|
+
this.board.lastDrawnReelStops = [];
|
|
2798
3133
|
}
|
|
2799
3134
|
/**
|
|
2800
|
-
*
|
|
2801
|
-
* Must be called before `runTasks()`.
|
|
3135
|
+
* Gets the current reels and symbols on the board.
|
|
2802
3136
|
*/
|
|
2803
|
-
|
|
2804
|
-
this.
|
|
2805
|
-
|
|
2806
|
-
|
|
3137
|
+
getBoardReels() {
|
|
3138
|
+
return this.board.reels;
|
|
3139
|
+
}
|
|
3140
|
+
getPaddingTop() {
|
|
3141
|
+
return this.board.paddingTop;
|
|
3142
|
+
}
|
|
3143
|
+
getPaddingBottom() {
|
|
3144
|
+
return this.board.paddingBottom;
|
|
3145
|
+
}
|
|
3146
|
+
resetReels() {
|
|
3147
|
+
this.board.resetReels({
|
|
3148
|
+
ctx: this.ctx
|
|
2807
3149
|
});
|
|
2808
3150
|
}
|
|
2809
3151
|
/**
|
|
2810
|
-
*
|
|
3152
|
+
* Sets the anticipation value for a specific reel.
|
|
2811
3153
|
*/
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
throw new Error(
|
|
2815
|
-
"Simulation is not configured. Do so by calling configureSimulation() first."
|
|
2816
|
-
);
|
|
2817
|
-
}
|
|
2818
|
-
await this.simulation.runSimulation(opts);
|
|
3154
|
+
setAnticipationForReel(reelIndex, value) {
|
|
3155
|
+
this.board.anticipation[reelIndex] = value;
|
|
2819
3156
|
}
|
|
2820
3157
|
/**
|
|
2821
|
-
*
|
|
3158
|
+
* Counts how many symbols matching the criteria are on a specific reel.
|
|
2822
3159
|
*/
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
throw new Error(
|
|
2826
|
-
"Optimization is not configured. Do so by calling configureOptimization() first."
|
|
2827
|
-
);
|
|
2828
|
-
}
|
|
2829
|
-
await this.optimizer.runOptimization(opts);
|
|
3160
|
+
countSymbolsOnReel(symbolOrProperties, reelIndex) {
|
|
3161
|
+
return this.board.countSymbolsOnReel(symbolOrProperties, reelIndex);
|
|
2830
3162
|
}
|
|
2831
3163
|
/**
|
|
2832
|
-
*
|
|
3164
|
+
* Counts how many symbols matching the criteria are on the board.
|
|
3165
|
+
*
|
|
3166
|
+
* Passing a GameSymbol will compare by ID, passing a properties object will compare by properties.
|
|
3167
|
+
*
|
|
3168
|
+
* Returns a tuple where the first element is the total count, and the second element is a record of counts per reel index.
|
|
2833
3169
|
*/
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
throw new Error(
|
|
2837
|
-
"Optimization must be configured to run analysis. Do so by calling configureOptimization() first."
|
|
2838
|
-
);
|
|
2839
|
-
}
|
|
2840
|
-
this.analyzer = new Analysis(this.optimizer);
|
|
2841
|
-
await this.analyzer.runAnalysis(opts.gameModes);
|
|
3170
|
+
countSymbolsOnBoard(symbolOrProperties) {
|
|
3171
|
+
return this.board.countSymbolsOnBoard(symbolOrProperties);
|
|
2842
3172
|
}
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
if (opts.doOptimization) {
|
|
2851
|
-
await this.runOptimization(opts.optimizationOpts || { gameModes: [] });
|
|
2852
|
-
}
|
|
2853
|
-
if (opts.doAnalysis) {
|
|
2854
|
-
await this.runAnalysis(opts.analysisOpts || { gameModes: [] });
|
|
2855
|
-
}
|
|
3173
|
+
/**
|
|
3174
|
+
* Checks if a symbol appears more than once on any reel in the current reel set.
|
|
3175
|
+
*
|
|
3176
|
+
* Useful to check for "forbidden" generations, e.g. 2 scatters on one reel.
|
|
3177
|
+
*/
|
|
3178
|
+
isSymbolOnAnyReelMultipleTimes(symbol) {
|
|
3179
|
+
return this.board.isSymbolOnAnyReelMultipleTimes(symbol);
|
|
2856
3180
|
}
|
|
2857
3181
|
/**
|
|
2858
|
-
* Gets the
|
|
3182
|
+
* Gets all reel stops (positions) where the specified symbol appears in the current reel set.\
|
|
3183
|
+
* Returns an array of arrays, where each inner array contains the positions for the corresponding reel.
|
|
2859
3184
|
*/
|
|
2860
|
-
|
|
2861
|
-
return
|
|
3185
|
+
getReelStopsForSymbol(reels, symbol) {
|
|
3186
|
+
return this.board.getReelStopsForSymbol(reels, symbol);
|
|
3187
|
+
}
|
|
3188
|
+
/**
|
|
3189
|
+
* Combines multiple arrays of reel stops into a single array of reel stops.\
|
|
3190
|
+
*/
|
|
3191
|
+
combineReelStops(...reelStops) {
|
|
3192
|
+
return this.board.combineReelStops({
|
|
3193
|
+
ctx: this.ctx,
|
|
3194
|
+
reelStops
|
|
3195
|
+
});
|
|
3196
|
+
}
|
|
3197
|
+
/**
|
|
3198
|
+
* From a list of reel stops on reels, selects a random stop for a speficied number of random symbols.
|
|
3199
|
+
*
|
|
3200
|
+
* Mostly useful for placing scatter symbols on the board.
|
|
3201
|
+
*/
|
|
3202
|
+
getRandomReelStops(reels, reelStops, amount) {
|
|
3203
|
+
return this.board.getRandomReelStops({
|
|
3204
|
+
ctx: this.ctx,
|
|
3205
|
+
reels,
|
|
3206
|
+
reelsAmount: this.reelsAmount,
|
|
3207
|
+
reelStops,
|
|
3208
|
+
amount
|
|
3209
|
+
});
|
|
3210
|
+
}
|
|
3211
|
+
/**
|
|
3212
|
+
* Selects a random reel set based on the configured weights of the current result set.\
|
|
3213
|
+
* Returns the reels as arrays of GameSymbols.
|
|
3214
|
+
*/
|
|
3215
|
+
getRandomReelset() {
|
|
3216
|
+
return this.board.getRandomReelset(this.ctx);
|
|
3217
|
+
}
|
|
3218
|
+
/**
|
|
3219
|
+
* Draws a board using specified reel stops.
|
|
3220
|
+
*/
|
|
3221
|
+
drawBoardWithForcedStops(reels, forcedStops) {
|
|
3222
|
+
this.drawBoardMixed(reels, forcedStops);
|
|
3223
|
+
}
|
|
3224
|
+
/**
|
|
3225
|
+
* Draws a board using random reel stops.
|
|
3226
|
+
*/
|
|
3227
|
+
drawBoardWithRandomStops(reels) {
|
|
3228
|
+
this.drawBoardMixed(reels);
|
|
3229
|
+
}
|
|
3230
|
+
drawBoardMixed(reels, forcedStops) {
|
|
3231
|
+
this.board.drawBoardMixed({
|
|
3232
|
+
ctx: this.ctx,
|
|
3233
|
+
reels,
|
|
3234
|
+
forcedStops,
|
|
3235
|
+
reelsAmount: this.reelsAmount,
|
|
3236
|
+
symbolsPerReel: this.symbolsPerReel,
|
|
3237
|
+
padSymbols: this.padSymbols
|
|
3238
|
+
});
|
|
3239
|
+
}
|
|
3240
|
+
/**
|
|
3241
|
+
* Tumbles the board. All given symbols will be deleted and new symbols will fall from the top.
|
|
3242
|
+
*/
|
|
3243
|
+
tumbleBoard(symbolsToDelete) {
|
|
3244
|
+
this.board.tumbleBoard({
|
|
3245
|
+
ctx: this.ctx,
|
|
3246
|
+
symbolsToDelete,
|
|
3247
|
+
reelsAmount: this.reelsAmount,
|
|
3248
|
+
symbolsPerReel: this.symbolsPerReel,
|
|
3249
|
+
padSymbols: this.padSymbols
|
|
3250
|
+
});
|
|
2862
3251
|
}
|
|
2863
3252
|
};
|
|
2864
|
-
|
|
2865
|
-
// index.ts
|
|
2866
|
-
function createSlotGame(opts) {
|
|
2867
|
-
return new SlotGame(opts);
|
|
2868
|
-
}
|
|
2869
|
-
var defineUserState = (data) => data;
|
|
2870
|
-
var defineSymbols = (symbols) => symbols;
|
|
2871
|
-
var defineGameModes = (gameModes) => gameModes;
|
|
2872
|
-
var defineReelSets = (reelSets) => reelSets;
|
|
2873
3253
|
// Annotate the CommonJS export names for ESM import in node:
|
|
2874
3254
|
0 && (module.exports = {
|
|
2875
3255
|
ClusterWinType,
|
|
2876
|
-
GameConfig,
|
|
2877
3256
|
GameMode,
|
|
2878
3257
|
GameSymbol,
|
|
3258
|
+
GeneratedReelSet,
|
|
2879
3259
|
LinesWinType,
|
|
2880
3260
|
ManywaysWinType,
|
|
2881
3261
|
OptimizationConditions,
|
|
2882
3262
|
OptimizationParameters,
|
|
2883
3263
|
OptimizationScaling,
|
|
2884
|
-
ReelGenerator,
|
|
2885
3264
|
ResultSet,
|
|
3265
|
+
SPIN_TYPE,
|
|
2886
3266
|
StandaloneBoard,
|
|
2887
|
-
|
|
3267
|
+
StaticReelSet,
|
|
2888
3268
|
createSlotGame,
|
|
2889
3269
|
defineGameModes,
|
|
2890
|
-
defineReelSets,
|
|
2891
3270
|
defineSymbols,
|
|
2892
|
-
defineUserState
|
|
2893
|
-
weightedRandom
|
|
3271
|
+
defineUserState
|
|
2894
3272
|
});
|
|
2895
3273
|
//# sourceMappingURL=index.js.map
|