@slot-engine/core 0.1.14 → 0.2.1
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 +219 -92
- package/dist/index.d.ts +219 -92
- package/dist/index.js +1136 -482
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1137 -479
- package/dist/index.mjs.map +1 -1
- package/package.json +11 -2
package/dist/index.mjs
CHANGED
|
@@ -1,11 +1,63 @@
|
|
|
1
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
2
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
3
|
+
}) : x)(function(x) {
|
|
4
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
5
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
6
|
+
});
|
|
7
|
+
|
|
1
8
|
// src/constants.ts
|
|
2
9
|
var SPIN_TYPE = {
|
|
3
10
|
BASE_GAME: "basegame",
|
|
4
11
|
FREE_SPINS: "freespins"
|
|
5
12
|
};
|
|
13
|
+
var CLI_ARGS = {
|
|
14
|
+
RUN: "slot-engine-run"
|
|
15
|
+
};
|
|
6
16
|
|
|
7
17
|
// src/game-config/index.ts
|
|
8
18
|
import assert from "assert";
|
|
19
|
+
|
|
20
|
+
// src/utils/file-paths.ts
|
|
21
|
+
import path from "path";
|
|
22
|
+
function createPermanentFilePaths(basePath) {
|
|
23
|
+
return {
|
|
24
|
+
base: basePath,
|
|
25
|
+
books: (mode) => path.join(basePath, `books_${mode}.jsonl`),
|
|
26
|
+
booksIndexMeta: (mode) => path.join(basePath, `books_${mode}.index.meta.json`),
|
|
27
|
+
booksIndex: (mode, worker) => path.join(basePath, "books_chunks", `books_${mode}_${worker}.index.txt`),
|
|
28
|
+
booksChunk: (mode, worker, chunk) => path.join(
|
|
29
|
+
basePath,
|
|
30
|
+
"books_chunks",
|
|
31
|
+
`books_${mode}_chunk_${worker}-${chunk}.jsonl.zst`
|
|
32
|
+
),
|
|
33
|
+
booksCompressed: (mode) => path.join(basePath, "publish_files", `books_${mode}.jsonl.zst`),
|
|
34
|
+
booksUncompressed: (mode) => path.join(basePath, `books_${mode}.jsonl`),
|
|
35
|
+
lookupTable: (mode) => path.join(basePath, `lookUpTable_${mode}.csv`),
|
|
36
|
+
lookupTableIndex: (mode) => path.join(basePath, `lookUpTable_${mode}.index`),
|
|
37
|
+
lookupTableSegmented: (mode) => path.join(basePath, `lookUpTableSegmented_${mode}.csv`),
|
|
38
|
+
lookupTableSegmentedIndex: (mode) => path.join(basePath, `lookUpTableSegmented_${mode}.index`),
|
|
39
|
+
lookupTablePublish: (mode) => path.join(basePath, "publish_files", `lookUpTable_${mode}_0.csv`),
|
|
40
|
+
forceRecords: (mode) => path.join(basePath, `force_record_${mode}.json`),
|
|
41
|
+
forceKeys: (mode) => path.join(basePath, `force_keys_${mode}.json`),
|
|
42
|
+
indexJson: path.join(basePath, "publish_files", "index.json"),
|
|
43
|
+
optimizationFiles: path.join(basePath, "optimization_files"),
|
|
44
|
+
publishFiles: path.join(basePath, "publish_files"),
|
|
45
|
+
simulationSummary: path.join(basePath, "simulation_summary.json"),
|
|
46
|
+
statsPayouts: path.join(basePath, "stats_payouts.json"),
|
|
47
|
+
statsSummary: path.join(basePath, "stats_summary.json")
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function createTemporaryFilePaths(basePath, tempFolder) {
|
|
51
|
+
return {
|
|
52
|
+
tempBooks: (mode, i) => path.join(basePath, tempFolder, `temp_books_${mode}_${i}.jsonl`),
|
|
53
|
+
tempLookupTable: (mode, i) => path.join(basePath, tempFolder, `temp_lookup_${mode}_${i}.csv`),
|
|
54
|
+
tempLookupTableSegmented: (mode, i) => path.join(basePath, tempFolder, `temp_lookup_segmented_${mode}_${i}.csv`),
|
|
55
|
+
tempRecords: (mode) => path.join(basePath, tempFolder, `temp_records_${mode}.jsonl`)
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// src/game-config/index.ts
|
|
60
|
+
import path2 from "path";
|
|
9
61
|
function createGameConfig(opts) {
|
|
10
62
|
const symbols = /* @__PURE__ */ new Map();
|
|
11
63
|
for (const [key, value] of Object.entries(opts.symbols)) {
|
|
@@ -15,70 +67,42 @@ function createGameConfig(opts) {
|
|
|
15
67
|
const getAnticipationTrigger = (spinType) => {
|
|
16
68
|
return Math.min(...Object.keys(opts.scatterToFreespins[spinType] || {}).map(Number)) - 1;
|
|
17
69
|
};
|
|
70
|
+
const rootDir = opts.rootDir || process.cwd();
|
|
71
|
+
const outputDir = "__build__";
|
|
72
|
+
const basePath = path2.join(rootDir, outputDir);
|
|
18
73
|
return {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
74
|
+
config: {
|
|
75
|
+
padSymbols: opts.padSymbols || 1,
|
|
76
|
+
userState: opts.userState || {},
|
|
77
|
+
...opts,
|
|
78
|
+
symbols,
|
|
79
|
+
anticipationTriggers: {
|
|
80
|
+
[SPIN_TYPE.BASE_GAME]: getAnticipationTrigger(SPIN_TYPE.BASE_GAME),
|
|
81
|
+
[SPIN_TYPE.FREE_SPINS]: getAnticipationTrigger(SPIN_TYPE.FREE_SPINS)
|
|
82
|
+
}
|
|
26
83
|
},
|
|
27
|
-
|
|
28
|
-
|
|
84
|
+
metadata: {
|
|
85
|
+
outputDir,
|
|
86
|
+
rootDir,
|
|
87
|
+
isCustomRoot: !!opts.rootDir,
|
|
88
|
+
paths: createPermanentFilePaths(basePath)
|
|
89
|
+
}
|
|
29
90
|
};
|
|
30
91
|
}
|
|
31
92
|
|
|
32
93
|
// src/simulation/index.ts
|
|
33
|
-
import
|
|
34
|
-
import
|
|
35
|
-
import
|
|
94
|
+
import fs3 from "fs";
|
|
95
|
+
import path3 from "path";
|
|
96
|
+
import assert5 from "assert";
|
|
36
97
|
import zlib from "zlib";
|
|
37
|
-
import
|
|
98
|
+
import readline from "readline";
|
|
38
99
|
import { buildSync } from "esbuild";
|
|
39
|
-
import { Worker, isMainThread, parentPort, workerData } from "worker_threads";
|
|
100
|
+
import { Worker, isMainThread as isMainThread2, parentPort as parentPort2, workerData } from "worker_threads";
|
|
40
101
|
|
|
41
102
|
// src/result-set/index.ts
|
|
42
103
|
import assert2 from "assert";
|
|
43
104
|
|
|
44
|
-
// src/
|
|
45
|
-
var AbstractService = class {
|
|
46
|
-
/**
|
|
47
|
-
* Function that returns the current game context.
|
|
48
|
-
*/
|
|
49
|
-
ctx;
|
|
50
|
-
constructor(ctx) {
|
|
51
|
-
this.ctx = ctx;
|
|
52
|
-
}
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
// src/service/rng.ts
|
|
56
|
-
var RngService = class extends AbstractService {
|
|
57
|
-
rng = new RandomNumberGenerator();
|
|
58
|
-
constructor(ctx) {
|
|
59
|
-
super(ctx);
|
|
60
|
-
}
|
|
61
|
-
/**
|
|
62
|
-
* Random weighted selection from a set of items.
|
|
63
|
-
*/
|
|
64
|
-
weightedRandom = this.rng.weightedRandom.bind(this.rng);
|
|
65
|
-
/**
|
|
66
|
-
* Selects a random item from an array.
|
|
67
|
-
*/
|
|
68
|
-
randomItem = this.rng.randomItem.bind(this.rng);
|
|
69
|
-
/**
|
|
70
|
-
* Shuffles an array.
|
|
71
|
-
*/
|
|
72
|
-
shuffle = this.rng.shuffle.bind(this.rng);
|
|
73
|
-
/**
|
|
74
|
-
* Generates a random float between two values.
|
|
75
|
-
*/
|
|
76
|
-
randomFloat = this.rng.randomFloat.bind(this.rng);
|
|
77
|
-
/**
|
|
78
|
-
* Sets the seed for the RNG.
|
|
79
|
-
*/
|
|
80
|
-
setSeedIfDifferent = this.rng.setSeedIfDifferent.bind(this.rng);
|
|
81
|
-
};
|
|
105
|
+
// src/rng/index.ts
|
|
82
106
|
var RandomNumberGenerator = class {
|
|
83
107
|
mIdum;
|
|
84
108
|
mIy;
|
|
@@ -270,10 +294,10 @@ var ResultSet = class {
|
|
|
270
294
|
const freespinsMet = this.forceFreespins ? ctx.state.triggeredFreespins : true;
|
|
271
295
|
const wallet = ctx.services.wallet._getWallet();
|
|
272
296
|
const multiplierMet = this.forceMaxWin ? true : this.multiplier !== void 0 ? wallet.getCurrentWin() === this.multiplier : wallet.getCurrentWin() > 0;
|
|
273
|
-
const
|
|
274
|
-
const coreCriteriaMet = freespinsMet && multiplierMet &&
|
|
297
|
+
const respectsMaxWin = this.forceMaxWin ? wallet.getCurrentWin() >= ctx.config.maxWinX : wallet.getCurrentWin() < ctx.config.maxWinX;
|
|
298
|
+
const coreCriteriaMet = freespinsMet && multiplierMet && respectsMaxWin;
|
|
275
299
|
const finalResult = customEval !== void 0 ? coreCriteriaMet && customEval === true : coreCriteriaMet;
|
|
276
|
-
if (this.forceMaxWin &&
|
|
300
|
+
if (this.forceMaxWin && respectsMaxWin) {
|
|
277
301
|
ctx.services.data.record({
|
|
278
302
|
maxwin: true
|
|
279
303
|
});
|
|
@@ -308,13 +332,34 @@ function createGameState(opts) {
|
|
|
308
332
|
// src/recorder/index.ts
|
|
309
333
|
var Recorder = class {
|
|
310
334
|
records;
|
|
335
|
+
recordsMap;
|
|
311
336
|
pendingRecords;
|
|
312
337
|
constructor() {
|
|
313
338
|
this.records = [];
|
|
339
|
+
this.recordsMap = /* @__PURE__ */ new Map();
|
|
340
|
+
this.pendingRecords = [];
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Intended for internal use only.
|
|
344
|
+
*/
|
|
345
|
+
_reset() {
|
|
346
|
+
this.records = [];
|
|
347
|
+
this.recordsMap.clear();
|
|
314
348
|
this.pendingRecords = [];
|
|
315
349
|
}
|
|
316
350
|
};
|
|
317
351
|
|
|
352
|
+
// src/service/index.ts
|
|
353
|
+
var AbstractService = class {
|
|
354
|
+
/**
|
|
355
|
+
* Function that returns the current game context.
|
|
356
|
+
*/
|
|
357
|
+
ctx;
|
|
358
|
+
constructor(ctx) {
|
|
359
|
+
this.ctx = ctx;
|
|
360
|
+
}
|
|
361
|
+
};
|
|
362
|
+
|
|
318
363
|
// src/board/index.ts
|
|
319
364
|
import assert3 from "assert";
|
|
320
365
|
|
|
@@ -903,19 +948,13 @@ var BoardService = class extends AbstractService {
|
|
|
903
948
|
};
|
|
904
949
|
|
|
905
950
|
// src/service/data.ts
|
|
906
|
-
import
|
|
951
|
+
import { isMainThread, parentPort } from "worker_threads";
|
|
907
952
|
var DataService = class extends AbstractService {
|
|
908
953
|
recorder;
|
|
909
954
|
book;
|
|
910
955
|
constructor(ctx) {
|
|
911
956
|
super(ctx);
|
|
912
957
|
}
|
|
913
|
-
ensureRecorder() {
|
|
914
|
-
assert4(this.recorder, "Recorder not set in DataService. Call setRecorder() first.");
|
|
915
|
-
}
|
|
916
|
-
ensureBook() {
|
|
917
|
-
assert4(this.book, "Book not set in DataService. Call setBook() first.");
|
|
918
|
-
}
|
|
919
958
|
/**
|
|
920
959
|
* Intended for internal use only.
|
|
921
960
|
*/
|
|
@@ -944,14 +983,12 @@ var DataService = class extends AbstractService {
|
|
|
944
983
|
* Intended for internal use only.
|
|
945
984
|
*/
|
|
946
985
|
_getRecords() {
|
|
947
|
-
this.ensureRecorder();
|
|
948
986
|
return this.recorder.records;
|
|
949
987
|
}
|
|
950
988
|
/**
|
|
951
989
|
* Record data for statistical analysis.
|
|
952
990
|
*/
|
|
953
991
|
record(data) {
|
|
954
|
-
this.ensureRecorder();
|
|
955
992
|
this.recorder.pendingRecords.push({
|
|
956
993
|
bookId: this.ctx().state.currentSimulationId,
|
|
957
994
|
properties: Object.fromEntries(
|
|
@@ -971,14 +1008,19 @@ var DataService = class extends AbstractService {
|
|
|
971
1008
|
* Adds an event to the book.
|
|
972
1009
|
*/
|
|
973
1010
|
addBookEvent(event) {
|
|
974
|
-
this.ensureBook();
|
|
975
1011
|
this.book.addEvent(event);
|
|
976
1012
|
}
|
|
1013
|
+
/**
|
|
1014
|
+
* Write a log message to the terminal UI.
|
|
1015
|
+
*/
|
|
1016
|
+
log(message) {
|
|
1017
|
+
if (isMainThread) return;
|
|
1018
|
+
parentPort?.postMessage({ type: "user-log", message });
|
|
1019
|
+
}
|
|
977
1020
|
/**
|
|
978
1021
|
* Intended for internal use only.
|
|
979
1022
|
*/
|
|
980
1023
|
_clearPendingRecords() {
|
|
981
|
-
this.ensureRecorder();
|
|
982
1024
|
this.recorder.pendingRecords = [];
|
|
983
1025
|
}
|
|
984
1026
|
};
|
|
@@ -1119,21 +1161,44 @@ var GameService = class extends AbstractService {
|
|
|
1119
1161
|
}
|
|
1120
1162
|
};
|
|
1121
1163
|
|
|
1164
|
+
// src/service/rng.ts
|
|
1165
|
+
var RngService = class extends AbstractService {
|
|
1166
|
+
rng = new RandomNumberGenerator();
|
|
1167
|
+
constructor(ctx) {
|
|
1168
|
+
super(ctx);
|
|
1169
|
+
}
|
|
1170
|
+
/**
|
|
1171
|
+
* Random weighted selection from a set of items.
|
|
1172
|
+
*/
|
|
1173
|
+
weightedRandom = this.rng.weightedRandom.bind(this.rng);
|
|
1174
|
+
/**
|
|
1175
|
+
* Selects a random item from an array.
|
|
1176
|
+
*/
|
|
1177
|
+
randomItem = this.rng.randomItem.bind(this.rng);
|
|
1178
|
+
/**
|
|
1179
|
+
* Shuffles an array.
|
|
1180
|
+
*/
|
|
1181
|
+
shuffle = this.rng.shuffle.bind(this.rng);
|
|
1182
|
+
/**
|
|
1183
|
+
* Generates a random float between two values.
|
|
1184
|
+
*/
|
|
1185
|
+
randomFloat = this.rng.randomFloat.bind(this.rng);
|
|
1186
|
+
/**
|
|
1187
|
+
* Sets the seed for the RNG.
|
|
1188
|
+
*/
|
|
1189
|
+
setSeedIfDifferent = this.rng.setSeedIfDifferent.bind(this.rng);
|
|
1190
|
+
};
|
|
1191
|
+
|
|
1122
1192
|
// src/service/wallet.ts
|
|
1123
|
-
import assert5 from "assert";
|
|
1124
1193
|
var WalletService = class extends AbstractService {
|
|
1125
1194
|
wallet;
|
|
1126
1195
|
constructor(ctx) {
|
|
1127
1196
|
super(ctx);
|
|
1128
1197
|
}
|
|
1129
|
-
ensureWallet() {
|
|
1130
|
-
assert5(this.wallet, "Wallet not set in WalletService. Call setWallet() first.");
|
|
1131
|
-
}
|
|
1132
1198
|
/**
|
|
1133
1199
|
* Intended for internal use only.
|
|
1134
1200
|
*/
|
|
1135
1201
|
_getWallet() {
|
|
1136
|
-
this.ensureWallet();
|
|
1137
1202
|
return this.wallet;
|
|
1138
1203
|
}
|
|
1139
1204
|
/**
|
|
@@ -1149,7 +1214,6 @@ var WalletService = class extends AbstractService {
|
|
|
1149
1214
|
* If your game has tumbling mechanics, you should call this method again after every new tumble and win calculation.
|
|
1150
1215
|
*/
|
|
1151
1216
|
addSpinWin(amount) {
|
|
1152
|
-
this.ensureWallet();
|
|
1153
1217
|
this.wallet.addSpinWin(amount);
|
|
1154
1218
|
}
|
|
1155
1219
|
/**
|
|
@@ -1158,7 +1222,6 @@ var WalletService = class extends AbstractService {
|
|
|
1158
1222
|
* This also calls `addSpinWin()` internally, to add the tumble win to the overall spin win.
|
|
1159
1223
|
*/
|
|
1160
1224
|
addTumbleWin(amount) {
|
|
1161
|
-
this.ensureWallet();
|
|
1162
1225
|
this.wallet.addTumbleWin(amount);
|
|
1163
1226
|
}
|
|
1164
1227
|
/**
|
|
@@ -1168,28 +1231,24 @@ var WalletService = class extends AbstractService {
|
|
|
1168
1231
|
* and after a (free) spin is played out to finalize the win.
|
|
1169
1232
|
*/
|
|
1170
1233
|
confirmSpinWin() {
|
|
1171
|
-
this.ensureWallet();
|
|
1172
1234
|
this.wallet.confirmSpinWin(this.ctx().state.currentSpinType);
|
|
1173
1235
|
}
|
|
1174
1236
|
/**
|
|
1175
1237
|
* Gets the total win amount of the current simulation.
|
|
1176
1238
|
*/
|
|
1177
1239
|
getCurrentWin() {
|
|
1178
|
-
this.ensureWallet();
|
|
1179
1240
|
return this.wallet.getCurrentWin();
|
|
1180
1241
|
}
|
|
1181
1242
|
/**
|
|
1182
1243
|
* Gets the current spin win amount of the ongoing spin.
|
|
1183
1244
|
*/
|
|
1184
1245
|
getCurrentSpinWin() {
|
|
1185
|
-
this.ensureWallet();
|
|
1186
1246
|
return this.wallet.getCurrentSpinWin();
|
|
1187
1247
|
}
|
|
1188
1248
|
/**
|
|
1189
1249
|
* Gets the current tumble win amount of the ongoing spin.
|
|
1190
1250
|
*/
|
|
1191
1251
|
getCurrentTumbleWin() {
|
|
1192
|
-
this.ensureWallet();
|
|
1193
1252
|
return this.wallet.getCurrentTumbleWin();
|
|
1194
1253
|
}
|
|
1195
1254
|
};
|
|
@@ -1218,7 +1277,6 @@ function createGameContext(opts) {
|
|
|
1218
1277
|
|
|
1219
1278
|
// utils.ts
|
|
1220
1279
|
import fs from "fs";
|
|
1221
|
-
import readline from "readline";
|
|
1222
1280
|
function createDirIfNotExists(dirPath) {
|
|
1223
1281
|
if (!fs.existsSync(dirPath)) {
|
|
1224
1282
|
fs.mkdirSync(dirPath, { recursive: true });
|
|
@@ -1243,37 +1301,6 @@ function writeFile(filePath, data) {
|
|
|
1243
1301
|
function copy(obj) {
|
|
1244
1302
|
return JSON.parse(JSON.stringify(obj));
|
|
1245
1303
|
}
|
|
1246
|
-
var JSONL = class {
|
|
1247
|
-
static stringify(array) {
|
|
1248
|
-
return array.map((object) => JSON.stringify(object)).join("\n");
|
|
1249
|
-
}
|
|
1250
|
-
static parse(jsonl) {
|
|
1251
|
-
return jsonl.split("\n").filter((s) => s !== "").map((str) => JSON.parse(str));
|
|
1252
|
-
}
|
|
1253
|
-
static async convertToJson(inputPath, outputPath) {
|
|
1254
|
-
const writeStream = fs.createWriteStream(outputPath, { encoding: "utf-8" });
|
|
1255
|
-
writeStream.write("[\n");
|
|
1256
|
-
const rl = readline.createInterface({
|
|
1257
|
-
input: fs.createReadStream(inputPath),
|
|
1258
|
-
crlfDelay: Infinity
|
|
1259
|
-
});
|
|
1260
|
-
let isFirst = true;
|
|
1261
|
-
for await (const line of rl) {
|
|
1262
|
-
if (line.trim() === "") continue;
|
|
1263
|
-
if (!isFirst) {
|
|
1264
|
-
writeStream.write(",\n");
|
|
1265
|
-
}
|
|
1266
|
-
writeStream.write(line);
|
|
1267
|
-
isFirst = false;
|
|
1268
|
-
}
|
|
1269
|
-
writeStream.write("\n]");
|
|
1270
|
-
writeStream.end();
|
|
1271
|
-
return new Promise((resolve, reject) => {
|
|
1272
|
-
writeStream.on("finish", () => resolve());
|
|
1273
|
-
writeStream.on("error", reject);
|
|
1274
|
-
});
|
|
1275
|
-
}
|
|
1276
|
-
};
|
|
1277
1304
|
function round(value, decimals) {
|
|
1278
1305
|
return Number(Math.round(Number(value + "e" + decimals)) + "e-" + decimals);
|
|
1279
1306
|
}
|
|
@@ -1293,16 +1320,26 @@ var Book = class {
|
|
|
1293
1320
|
/**
|
|
1294
1321
|
* Intended for internal use only.
|
|
1295
1322
|
*/
|
|
1296
|
-
|
|
1323
|
+
_reset(id, criteria) {
|
|
1324
|
+
this.id = id;
|
|
1325
|
+
this.criteria = criteria;
|
|
1326
|
+
this.events = [];
|
|
1327
|
+
this.payout = 0;
|
|
1328
|
+
this.basegameWins = 0;
|
|
1329
|
+
this.freespinsWins = 0;
|
|
1330
|
+
}
|
|
1331
|
+
/**
|
|
1332
|
+
* Intended for internal use only.
|
|
1333
|
+
*/
|
|
1334
|
+
_setCriteria(criteria) {
|
|
1297
1335
|
this.criteria = criteria;
|
|
1298
1336
|
}
|
|
1299
1337
|
/**
|
|
1300
1338
|
* Adds an event to the book.
|
|
1301
1339
|
*/
|
|
1302
1340
|
addEvent(event) {
|
|
1303
|
-
const index = this.events.length + 1;
|
|
1304
1341
|
this.events.push({
|
|
1305
|
-
index,
|
|
1342
|
+
index: this.events.length + 1,
|
|
1306
1343
|
type: event.type,
|
|
1307
1344
|
data: copy(event.data)
|
|
1308
1345
|
});
|
|
@@ -1310,7 +1347,7 @@ var Book = class {
|
|
|
1310
1347
|
/**
|
|
1311
1348
|
* Intended for internal use only.
|
|
1312
1349
|
*/
|
|
1313
|
-
|
|
1350
|
+
_serialize() {
|
|
1314
1351
|
return {
|
|
1315
1352
|
id: this.id,
|
|
1316
1353
|
criteria: this.criteria,
|
|
@@ -1375,6 +1412,23 @@ var Wallet = class {
|
|
|
1375
1412
|
currentTumbleWin = 0;
|
|
1376
1413
|
constructor() {
|
|
1377
1414
|
}
|
|
1415
|
+
/**
|
|
1416
|
+
* Intended for internal use only.
|
|
1417
|
+
*/
|
|
1418
|
+
_reset() {
|
|
1419
|
+
this.cumulativeWins = 0;
|
|
1420
|
+
this.cumulativeWinsPerSpinType = {
|
|
1421
|
+
[SPIN_TYPE.BASE_GAME]: 0,
|
|
1422
|
+
[SPIN_TYPE.FREE_SPINS]: 0
|
|
1423
|
+
};
|
|
1424
|
+
this.currentWin = 0;
|
|
1425
|
+
this.currentWinPerSpinType = {
|
|
1426
|
+
[SPIN_TYPE.BASE_GAME]: 0,
|
|
1427
|
+
[SPIN_TYPE.FREE_SPINS]: 0
|
|
1428
|
+
};
|
|
1429
|
+
this.currentSpinWin = 0;
|
|
1430
|
+
this.currentTumbleWin = 0;
|
|
1431
|
+
}
|
|
1378
1432
|
/**
|
|
1379
1433
|
* Updates the win for the current spin.
|
|
1380
1434
|
*
|
|
@@ -1547,7 +1601,9 @@ var Wallet = class {
|
|
|
1547
1601
|
import { pipeline } from "stream/promises";
|
|
1548
1602
|
|
|
1549
1603
|
// src/simulation/utils.ts
|
|
1550
|
-
import
|
|
1604
|
+
import fs2 from "fs";
|
|
1605
|
+
import assert4 from "assert";
|
|
1606
|
+
import chalk from "chalk";
|
|
1551
1607
|
function hashStringToInt(input) {
|
|
1552
1608
|
let h = 2166136261;
|
|
1553
1609
|
for (let i = 0; i < input.length; i++) {
|
|
@@ -1560,7 +1616,7 @@ function splitCountsAcrossChunks(totalCounts, chunkSizes) {
|
|
|
1560
1616
|
const total = chunkSizes.reduce((a, b) => a + b, 0);
|
|
1561
1617
|
const allCriteria = Object.keys(totalCounts);
|
|
1562
1618
|
const totalCountsSum = allCriteria.reduce((s, c) => s + (totalCounts[c] ?? 0), 0);
|
|
1563
|
-
|
|
1619
|
+
assert4(
|
|
1564
1620
|
totalCountsSum === total,
|
|
1565
1621
|
`Counts (${totalCountsSum}) must match chunk total (${total}).`
|
|
1566
1622
|
);
|
|
@@ -1593,9 +1649,9 @@ function splitCountsAcrossChunks(totalCounts, chunkSizes) {
|
|
|
1593
1649
|
for (let target = 0; target < chunkSizes.length; target++) {
|
|
1594
1650
|
while (deficits[target] > 0) {
|
|
1595
1651
|
const src = deficits.findIndex((d) => d < 0);
|
|
1596
|
-
|
|
1652
|
+
assert4(src !== -1, "No surplus chunk found, but deficits remain.");
|
|
1597
1653
|
const crit = allCriteria.find((c) => (perChunk[src][c] ?? 0) > 0);
|
|
1598
|
-
|
|
1654
|
+
assert4(crit, `No movable criteria found from surplus chunk ${src}.`);
|
|
1599
1655
|
perChunk[src][crit] -= 1;
|
|
1600
1656
|
perChunk[target][crit] = (perChunk[target][crit] ?? 0) + 1;
|
|
1601
1657
|
totals[src] -= 1;
|
|
@@ -1606,14 +1662,14 @@ function splitCountsAcrossChunks(totalCounts, chunkSizes) {
|
|
|
1606
1662
|
}
|
|
1607
1663
|
totals = chunkTotals();
|
|
1608
1664
|
for (let i = 0; i < chunkSizes.length; i++) {
|
|
1609
|
-
|
|
1665
|
+
assert4(
|
|
1610
1666
|
totals[i] === chunkSizes[i],
|
|
1611
1667
|
`Chunk ${i} size mismatch. Expected ${chunkSizes[i]}, got ${totals[i]}`
|
|
1612
1668
|
);
|
|
1613
1669
|
}
|
|
1614
1670
|
for (const c of allCriteria) {
|
|
1615
1671
|
const sum = perChunk.reduce((s, m) => s + (m[c] ?? 0), 0);
|
|
1616
|
-
|
|
1672
|
+
assert4(sum === (totalCounts[c] ?? 0), `Chunk split mismatch for criteria "${c}"`);
|
|
1617
1673
|
}
|
|
1618
1674
|
return perChunk;
|
|
1619
1675
|
}
|
|
@@ -1644,8 +1700,306 @@ function createCriteriaSampler(counts, seed) {
|
|
|
1644
1700
|
return keys.find((k) => (remaining[k] ?? 0) > 0) ?? "N/A";
|
|
1645
1701
|
};
|
|
1646
1702
|
}
|
|
1703
|
+
async function makeLutIndexFromPublishLut(lutPublishPath, lutIndexPath) {
|
|
1704
|
+
console.log(chalk.gray(`Regenerating LUT index file...`));
|
|
1705
|
+
if (!fs2.existsSync(lutPublishPath)) {
|
|
1706
|
+
console.warn(
|
|
1707
|
+
chalk.yellow(
|
|
1708
|
+
`LUT publish file does not exist when regenerating index file: ${lutPublishPath}`
|
|
1709
|
+
)
|
|
1710
|
+
);
|
|
1711
|
+
return;
|
|
1712
|
+
}
|
|
1713
|
+
try {
|
|
1714
|
+
const lutPublishStream = fs2.createReadStream(lutPublishPath, {
|
|
1715
|
+
highWaterMark: 500 * 1024 * 1024
|
|
1716
|
+
});
|
|
1717
|
+
const rl = __require("readline").createInterface({
|
|
1718
|
+
input: lutPublishStream,
|
|
1719
|
+
crlfDelay: Infinity
|
|
1720
|
+
});
|
|
1721
|
+
const lutIndexStream = fs2.createWriteStream(lutIndexPath, {
|
|
1722
|
+
highWaterMark: 500 * 1024 * 1024
|
|
1723
|
+
});
|
|
1724
|
+
let offset = 0n;
|
|
1725
|
+
for await (const line of rl) {
|
|
1726
|
+
if (!line.trim()) continue;
|
|
1727
|
+
const indexBuffer = Buffer.alloc(8);
|
|
1728
|
+
indexBuffer.writeBigUInt64LE(offset);
|
|
1729
|
+
if (!lutIndexStream.write(indexBuffer)) {
|
|
1730
|
+
await new Promise((resolve) => lutIndexStream.once("drain", resolve));
|
|
1731
|
+
}
|
|
1732
|
+
offset += BigInt(Buffer.byteLength(line + "\n", "utf8"));
|
|
1733
|
+
}
|
|
1734
|
+
lutIndexStream.end();
|
|
1735
|
+
await new Promise((resolve) => lutIndexStream.on("finish", resolve));
|
|
1736
|
+
} catch (error) {
|
|
1737
|
+
throw new Error(`Error generating LUT index from publish LUT: ${error}`);
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1647
1740
|
|
|
1648
1741
|
// src/simulation/index.ts
|
|
1742
|
+
import { io } from "socket.io-client";
|
|
1743
|
+
import chalk2 from "chalk";
|
|
1744
|
+
|
|
1745
|
+
// src/tui/index.ts
|
|
1746
|
+
var TerminalUi = class {
|
|
1747
|
+
progress = 0;
|
|
1748
|
+
timeRemaining = 0;
|
|
1749
|
+
gameMode;
|
|
1750
|
+
currentSims = 0;
|
|
1751
|
+
totalSims = 0;
|
|
1752
|
+
logs = [];
|
|
1753
|
+
logScrollOffset = 0;
|
|
1754
|
+
isScrolled = false;
|
|
1755
|
+
maxLogs = 500;
|
|
1756
|
+
totalLogs = 0;
|
|
1757
|
+
minWidth = 50;
|
|
1758
|
+
minHeight = 12;
|
|
1759
|
+
isRendering = false;
|
|
1760
|
+
renderInterval = null;
|
|
1761
|
+
resizeHandler;
|
|
1762
|
+
sigintHandler;
|
|
1763
|
+
keyHandler;
|
|
1764
|
+
constructor(opts) {
|
|
1765
|
+
this.gameMode = opts.gameMode;
|
|
1766
|
+
this.resizeHandler = () => {
|
|
1767
|
+
this.clearScreen();
|
|
1768
|
+
this.render();
|
|
1769
|
+
};
|
|
1770
|
+
this.sigintHandler = () => {
|
|
1771
|
+
this.stop();
|
|
1772
|
+
process.exit(0);
|
|
1773
|
+
};
|
|
1774
|
+
this.keyHandler = (data) => {
|
|
1775
|
+
const key = data.toString();
|
|
1776
|
+
if (key === "j" || key === "\x1B[A") {
|
|
1777
|
+
this.scrollUp();
|
|
1778
|
+
} else if (key === "k" || key === "\x1B[B") {
|
|
1779
|
+
this.scrollDown();
|
|
1780
|
+
} else if (key === "l") {
|
|
1781
|
+
this.scrollToBottom();
|
|
1782
|
+
} else if (key === "q" || key === "") {
|
|
1783
|
+
this.stop();
|
|
1784
|
+
process.exit(0);
|
|
1785
|
+
}
|
|
1786
|
+
};
|
|
1787
|
+
process.stdout.on("resize", this.resizeHandler);
|
|
1788
|
+
process.on("SIGINT", this.sigintHandler);
|
|
1789
|
+
}
|
|
1790
|
+
get terminalWidth() {
|
|
1791
|
+
return process.stdout.columns || 80;
|
|
1792
|
+
}
|
|
1793
|
+
get terminalHeight() {
|
|
1794
|
+
return process.stdout.rows || 24;
|
|
1795
|
+
}
|
|
1796
|
+
get isTooSmall() {
|
|
1797
|
+
return this.terminalWidth < this.minWidth || this.terminalHeight < this.minHeight;
|
|
1798
|
+
}
|
|
1799
|
+
start() {
|
|
1800
|
+
this.enterAltScreen();
|
|
1801
|
+
this.hideCursor();
|
|
1802
|
+
this.clearScreen();
|
|
1803
|
+
if (process.stdin.isTTY) {
|
|
1804
|
+
process.stdin.setRawMode(true);
|
|
1805
|
+
process.stdin.resume();
|
|
1806
|
+
process.stdin.on("data", this.keyHandler);
|
|
1807
|
+
}
|
|
1808
|
+
this.render();
|
|
1809
|
+
this.renderInterval = setInterval(() => this.render(), 100);
|
|
1810
|
+
}
|
|
1811
|
+
stop() {
|
|
1812
|
+
if (this.renderInterval) {
|
|
1813
|
+
clearInterval(this.renderInterval);
|
|
1814
|
+
this.renderInterval = null;
|
|
1815
|
+
}
|
|
1816
|
+
if (process.stdin.isTTY) {
|
|
1817
|
+
process.stdin.off("data", this.keyHandler);
|
|
1818
|
+
process.stdin.setRawMode(false);
|
|
1819
|
+
process.stdin.pause();
|
|
1820
|
+
}
|
|
1821
|
+
this.showCursor();
|
|
1822
|
+
this.clearScreen();
|
|
1823
|
+
this.exitAltScreen();
|
|
1824
|
+
process.stdout.off("resize", this.resizeHandler);
|
|
1825
|
+
process.off("SIGINT", this.sigintHandler);
|
|
1826
|
+
}
|
|
1827
|
+
setProgress(progress, timeRemaining, completedSims) {
|
|
1828
|
+
this.progress = Math.max(0, Math.min(100, progress));
|
|
1829
|
+
this.timeRemaining = Math.max(0, timeRemaining);
|
|
1830
|
+
this.currentSims = completedSims;
|
|
1831
|
+
}
|
|
1832
|
+
setDetails(opts) {
|
|
1833
|
+
this.gameMode = opts.gameMode;
|
|
1834
|
+
this.totalSims = opts.totalSims;
|
|
1835
|
+
}
|
|
1836
|
+
log(message) {
|
|
1837
|
+
this.logs.push({ i: this.totalLogs, m: message });
|
|
1838
|
+
this.totalLogs++;
|
|
1839
|
+
if (this.logs.length > this.maxLogs) {
|
|
1840
|
+
const excess = this.logs.length - this.maxLogs;
|
|
1841
|
+
this.logs.splice(0, excess);
|
|
1842
|
+
this.logScrollOffset = Math.min(
|
|
1843
|
+
this.logScrollOffset,
|
|
1844
|
+
Math.max(0, this.logs.length - this.getLogAreaHeight())
|
|
1845
|
+
);
|
|
1846
|
+
}
|
|
1847
|
+
if (!this.isScrolled) this.scrollToBottom();
|
|
1848
|
+
}
|
|
1849
|
+
scrollUp(lines = 1) {
|
|
1850
|
+
this.logScrollOffset = Math.max(0, this.logScrollOffset - lines);
|
|
1851
|
+
this.isScrolled = true;
|
|
1852
|
+
}
|
|
1853
|
+
scrollDown(lines = 1) {
|
|
1854
|
+
const maxOffset = Math.max(0, this.logs.length - this.getLogAreaHeight());
|
|
1855
|
+
this.logScrollOffset = Math.min(maxOffset, this.logScrollOffset + lines);
|
|
1856
|
+
if (this.logScrollOffset >= maxOffset) {
|
|
1857
|
+
this.isScrolled = false;
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
scrollToBottom() {
|
|
1861
|
+
const maxOffset = Math.max(0, this.logs.length - this.getLogAreaHeight());
|
|
1862
|
+
this.logScrollOffset = maxOffset;
|
|
1863
|
+
this.isScrolled = false;
|
|
1864
|
+
}
|
|
1865
|
+
clearLogs() {
|
|
1866
|
+
this.logs = [];
|
|
1867
|
+
this.logScrollOffset = 0;
|
|
1868
|
+
}
|
|
1869
|
+
getLogAreaHeight() {
|
|
1870
|
+
return Math.max(1, this.terminalHeight - 8);
|
|
1871
|
+
}
|
|
1872
|
+
enterAltScreen() {
|
|
1873
|
+
process.stdout.write("\x1B[?1049h");
|
|
1874
|
+
}
|
|
1875
|
+
exitAltScreen() {
|
|
1876
|
+
process.stdout.write("\x1B[?1049l");
|
|
1877
|
+
}
|
|
1878
|
+
hideCursor() {
|
|
1879
|
+
process.stdout.write("\x1B[?25l");
|
|
1880
|
+
}
|
|
1881
|
+
showCursor() {
|
|
1882
|
+
process.stdout.write("\x1B[?25h");
|
|
1883
|
+
}
|
|
1884
|
+
clearScreen() {
|
|
1885
|
+
process.stdout.write("\x1B[2J\x1B[H");
|
|
1886
|
+
}
|
|
1887
|
+
moveTo(row, col) {
|
|
1888
|
+
process.stdout.write(`\x1B[${row};${col}H`);
|
|
1889
|
+
}
|
|
1890
|
+
render() {
|
|
1891
|
+
if (this.isRendering) return;
|
|
1892
|
+
this.isRendering = true;
|
|
1893
|
+
try {
|
|
1894
|
+
this.moveTo(1, 1);
|
|
1895
|
+
if (this.isTooSmall) {
|
|
1896
|
+
this.clearScreen();
|
|
1897
|
+
let msg = "Terminal too small.";
|
|
1898
|
+
let row = Math.floor(this.terminalHeight / 2);
|
|
1899
|
+
let col = Math.max(1, Math.floor((this.terminalWidth - msg.length) / 2));
|
|
1900
|
+
this.moveTo(row, col);
|
|
1901
|
+
process.stdout.write(msg);
|
|
1902
|
+
msg = "Try resizing or restarting the terminal.";
|
|
1903
|
+
row += 1;
|
|
1904
|
+
col = Math.max(1, Math.floor((this.terminalWidth - msg.length) / 2));
|
|
1905
|
+
this.moveTo(row, col);
|
|
1906
|
+
process.stdout.write(msg);
|
|
1907
|
+
return;
|
|
1908
|
+
}
|
|
1909
|
+
const lines = [];
|
|
1910
|
+
const width = this.terminalWidth;
|
|
1911
|
+
lines.push(this.boxLine("top", width));
|
|
1912
|
+
const canScrollUp = this.logScrollOffset > 0;
|
|
1913
|
+
const topHint = canScrollUp ? "\u2191 scroll up (j)" : "";
|
|
1914
|
+
lines.push(this.contentLine(this.centerText(topHint, width - 2), width));
|
|
1915
|
+
const logAreaHeight = this.getLogAreaHeight();
|
|
1916
|
+
const visibleLogs = this.getVisibleLogs(logAreaHeight);
|
|
1917
|
+
for (const log of visibleLogs) {
|
|
1918
|
+
lines.push(
|
|
1919
|
+
this.contentLine(` (${log.i}) ` + this.truncate(log.m, width - 4), width)
|
|
1920
|
+
);
|
|
1921
|
+
}
|
|
1922
|
+
for (let i = visibleLogs.length; i < logAreaHeight; i++) {
|
|
1923
|
+
lines.push(this.contentLine("", width));
|
|
1924
|
+
}
|
|
1925
|
+
const canScrollDown = this.logScrollOffset < Math.max(0, this.logs.length - logAreaHeight);
|
|
1926
|
+
const bottomHint = canScrollDown ? "\u2193 scroll down (k) \u2193 jump to newest (l)" : "";
|
|
1927
|
+
lines.push(this.contentLine(this.centerText(bottomHint, width - 2), width));
|
|
1928
|
+
lines.push(this.boxLine("middle", width));
|
|
1929
|
+
const modeText = `Mode: ${this.gameMode}`;
|
|
1930
|
+
const simsText = `${this.currentSims}/${this.totalSims}`;
|
|
1931
|
+
const infoLine = this.createInfoLine(modeText, simsText, width);
|
|
1932
|
+
lines.push(infoLine);
|
|
1933
|
+
lines.push(this.boxLine("middle", width));
|
|
1934
|
+
lines.push(this.createProgressLine(width));
|
|
1935
|
+
lines.push(this.boxLine("bottom", width));
|
|
1936
|
+
this.moveTo(1, 1);
|
|
1937
|
+
process.stdout.write(lines.join("\n"));
|
|
1938
|
+
} finally {
|
|
1939
|
+
this.isRendering = false;
|
|
1940
|
+
}
|
|
1941
|
+
}
|
|
1942
|
+
getVisibleLogs(height) {
|
|
1943
|
+
const start = this.logScrollOffset;
|
|
1944
|
+
const end = start + height;
|
|
1945
|
+
return this.logs.slice(start, end);
|
|
1946
|
+
}
|
|
1947
|
+
boxLine(type, width) {
|
|
1948
|
+
const chars = {
|
|
1949
|
+
top: { left: "\u250C", right: "\u2510", fill: "\u2500" },
|
|
1950
|
+
middle: { left: "\u251C", right: "\u2524", fill: "\u2500" },
|
|
1951
|
+
bottom: { left: "\u2514", right: "\u2518", fill: "\u2500" }
|
|
1952
|
+
};
|
|
1953
|
+
const c = chars[type];
|
|
1954
|
+
return c.left + c.fill.repeat(width - 2) + c.right;
|
|
1955
|
+
}
|
|
1956
|
+
contentLine(content, width) {
|
|
1957
|
+
const innerWidth = width - 2;
|
|
1958
|
+
const paddedContent = content.padEnd(innerWidth).slice(0, innerWidth);
|
|
1959
|
+
return "\u2502" + paddedContent + "\u2502";
|
|
1960
|
+
}
|
|
1961
|
+
createInfoLine(left, right, width) {
|
|
1962
|
+
const innerWidth = width - 2;
|
|
1963
|
+
const separator = " \u2502 ";
|
|
1964
|
+
const availableForText = innerWidth - separator.length;
|
|
1965
|
+
const leftWidth = Math.floor(availableForText / 2);
|
|
1966
|
+
const rightWidth = availableForText - leftWidth;
|
|
1967
|
+
const leftTruncated = this.truncate(left, leftWidth);
|
|
1968
|
+
const rightTruncated = this.truncate(right, rightWidth);
|
|
1969
|
+
const leftPadded = this.centerText(leftTruncated, leftWidth);
|
|
1970
|
+
const rightPadded = this.centerText(rightTruncated, rightWidth);
|
|
1971
|
+
return "\u2502" + leftPadded + separator + rightPadded + "\u2502";
|
|
1972
|
+
}
|
|
1973
|
+
createProgressLine(width) {
|
|
1974
|
+
const innerWidth = width - 2;
|
|
1975
|
+
const timeStr = this.formatTime(this.timeRemaining);
|
|
1976
|
+
const percentStr = `${this.progress.toFixed(2)}%`;
|
|
1977
|
+
const rightInfo = `${timeStr} ${percentStr}`;
|
|
1978
|
+
const barWidth = Math.max(10, innerWidth - rightInfo.length - 3);
|
|
1979
|
+
const filledWidth = Math.round(this.progress / 100 * barWidth);
|
|
1980
|
+
const emptyWidth = barWidth - filledWidth;
|
|
1981
|
+
const bar = "\u2588".repeat(filledWidth) + "-".repeat(emptyWidth);
|
|
1982
|
+
const content = ` ${bar} ${rightInfo}`;
|
|
1983
|
+
return this.contentLine(content, width);
|
|
1984
|
+
}
|
|
1985
|
+
formatTime(seconds) {
|
|
1986
|
+
return new Date(seconds * 1e3).toISOString().substr(11, 8);
|
|
1987
|
+
}
|
|
1988
|
+
centerText(text, width) {
|
|
1989
|
+
if (text.length >= width) return text.slice(0, width);
|
|
1990
|
+
const padding = width - text.length;
|
|
1991
|
+
const leftPad = Math.floor(padding / 2);
|
|
1992
|
+
const rightPad = padding - leftPad;
|
|
1993
|
+
return " ".repeat(leftPad) + text + " ".repeat(rightPad);
|
|
1994
|
+
}
|
|
1995
|
+
truncate(text, maxLength) {
|
|
1996
|
+
if (text.length <= maxLength) return text;
|
|
1997
|
+
return text.slice(0, maxLength - 3) + "...";
|
|
1998
|
+
}
|
|
1999
|
+
};
|
|
2000
|
+
|
|
2001
|
+
// src/simulation/index.ts
|
|
2002
|
+
import { Readable } from "stream";
|
|
1649
2003
|
var completedSimulations = 0;
|
|
1650
2004
|
var TEMP_FILENAME = "__temp_compiled_src_IGNORE.js";
|
|
1651
2005
|
var TEMP_FOLDER = "temp_files";
|
|
@@ -1654,70 +2008,114 @@ var Simulation = class {
|
|
|
1654
2008
|
gameConfig;
|
|
1655
2009
|
simRunsAmount;
|
|
1656
2010
|
concurrency;
|
|
2011
|
+
makeUncompressedBooks;
|
|
1657
2012
|
debug = false;
|
|
1658
2013
|
actualSims = 0;
|
|
1659
|
-
wallet;
|
|
2014
|
+
wallet = new Wallet();
|
|
2015
|
+
summary = {};
|
|
1660
2016
|
recordsWriteStream;
|
|
1661
2017
|
hasWrittenRecord = false;
|
|
1662
2018
|
streamHighWaterMark = 500 * 1024 * 1024;
|
|
1663
2019
|
maxPendingSims;
|
|
1664
2020
|
maxHighWaterMark;
|
|
2021
|
+
panelPort = 7770;
|
|
2022
|
+
panelActive = false;
|
|
2023
|
+
panelWsUrl;
|
|
2024
|
+
socket;
|
|
2025
|
+
tui;
|
|
2026
|
+
tempBookIndexPaths = [];
|
|
2027
|
+
bookIndexMetas = [];
|
|
1665
2028
|
PATHS = {};
|
|
1666
2029
|
// Worker related
|
|
1667
2030
|
credits = 0;
|
|
1668
2031
|
creditWaiters = [];
|
|
1669
2032
|
creditListenerInit = false;
|
|
2033
|
+
bookBuffers = /* @__PURE__ */ new Map();
|
|
2034
|
+
bookBufferSizes = /* @__PURE__ */ new Map();
|
|
2035
|
+
bookChunkIndexes = /* @__PURE__ */ new Map();
|
|
1670
2036
|
constructor(opts, gameConfigOpts) {
|
|
1671
|
-
|
|
2037
|
+
const { config, metadata } = createGameConfig(gameConfigOpts);
|
|
2038
|
+
this.gameConfig = { ...config, ...metadata };
|
|
1672
2039
|
this.gameConfigOpts = gameConfigOpts;
|
|
2040
|
+
this.makeUncompressedBooks = opts.makeUncompressedBooks || false;
|
|
1673
2041
|
this.simRunsAmount = opts.simRunsAmount || {};
|
|
1674
2042
|
this.concurrency = (opts.concurrency || 6) >= 2 ? opts.concurrency || 6 : 2;
|
|
1675
|
-
this.
|
|
1676
|
-
this.
|
|
1677
|
-
this.maxHighWaterMark = (opts.maxDiskBuffer ?? 50) * 1024 * 1024;
|
|
2043
|
+
this.maxPendingSims = opts.maxPendingSims ?? 25;
|
|
2044
|
+
this.maxHighWaterMark = (opts.maxDiskBuffer ?? 150) * 1024 * 1024;
|
|
1678
2045
|
const gameModeKeys = Object.keys(this.gameConfig.gameModes);
|
|
1679
|
-
|
|
2046
|
+
assert5(
|
|
1680
2047
|
Object.values(this.gameConfig.gameModes).map((m) => gameModeKeys.includes(m.name)).every((v) => v === true),
|
|
1681
2048
|
"Game mode name must match its key in the gameModes object."
|
|
1682
2049
|
);
|
|
1683
|
-
|
|
2050
|
+
const basePath = path3.join(this.gameConfig.rootDir, this.gameConfig.outputDir);
|
|
1684
2051
|
this.PATHS = {
|
|
1685
|
-
...
|
|
1686
|
-
|
|
1687
|
-
booksCompressed: (mode) => path.join(this.PATHS.base, "publish_files", `books_${mode}.jsonl.zst`),
|
|
1688
|
-
tempBooks: (mode, i) => path.join(this.PATHS.base, TEMP_FOLDER, `temp_books_${mode}_${i}.jsonl`),
|
|
1689
|
-
lookupTable: (mode) => path.join(this.PATHS.base, `lookUpTable_${mode}.csv`),
|
|
1690
|
-
tempLookupTable: (mode, i) => path.join(this.PATHS.base, TEMP_FOLDER, `temp_lookup_${mode}_${i}.csv`),
|
|
1691
|
-
lookupTableSegmented: (mode) => path.join(this.PATHS.base, `lookUpTableSegmented_${mode}.csv`),
|
|
1692
|
-
tempLookupTableSegmented: (mode, i) => path.join(this.PATHS.base, TEMP_FOLDER, `temp_lookup_segmented_${mode}_${i}.csv`),
|
|
1693
|
-
lookupTablePublish: (mode) => path.join(this.PATHS.base, "publish_files", `lookUpTable_${mode}_0.csv`),
|
|
1694
|
-
tempRecords: (mode) => path.join(this.PATHS.base, TEMP_FOLDER, `temp_records_${mode}.jsonl`),
|
|
1695
|
-
forceRecords: (mode) => path.join(this.PATHS.base, `force_record_${mode}.json`),
|
|
1696
|
-
indexJson: path.join(this.PATHS.base, "publish_files", "index.json"),
|
|
1697
|
-
optimizationFiles: path.join(this.PATHS.base, "optimization_files"),
|
|
1698
|
-
publishFiles: path.join(this.PATHS.base, "publish_files")
|
|
2052
|
+
...createPermanentFilePaths(basePath),
|
|
2053
|
+
...createTemporaryFilePaths(basePath, TEMP_FOLDER)
|
|
1699
2054
|
};
|
|
2055
|
+
this.tui = new TerminalUi({
|
|
2056
|
+
gameMode: "N/A"
|
|
2057
|
+
});
|
|
1700
2058
|
}
|
|
1701
2059
|
async runSimulation(opts) {
|
|
1702
2060
|
const debug = opts.debug || false;
|
|
1703
2061
|
this.debug = debug;
|
|
2062
|
+
let statusMessage = "";
|
|
1704
2063
|
const gameModesToSimulate = Object.keys(this.simRunsAmount);
|
|
1705
2064
|
const configuredGameModes = Object.keys(this.gameConfig.gameModes);
|
|
1706
2065
|
if (gameModesToSimulate.length === 0) {
|
|
1707
2066
|
throw new Error("No game modes configured for simulation.");
|
|
1708
2067
|
}
|
|
1709
2068
|
this.generateReelsetFiles();
|
|
1710
|
-
if (
|
|
2069
|
+
if (isMainThread2) {
|
|
1711
2070
|
this.preprocessFiles();
|
|
1712
|
-
|
|
2071
|
+
this.panelPort = opts.panelPort || 7770;
|
|
2072
|
+
this.panelWsUrl = `http://localhost:${this.panelPort}`;
|
|
2073
|
+
await new Promise((resolve) => {
|
|
2074
|
+
try {
|
|
2075
|
+
this.socket = io(this.panelWsUrl, {
|
|
2076
|
+
path: "/ws",
|
|
2077
|
+
transports: ["websocket", "polling"],
|
|
2078
|
+
withCredentials: true,
|
|
2079
|
+
autoConnect: false,
|
|
2080
|
+
reconnection: false
|
|
2081
|
+
});
|
|
2082
|
+
this.socket.connect();
|
|
2083
|
+
this.socket.once("connect", () => {
|
|
2084
|
+
this.panelActive = true;
|
|
2085
|
+
resolve();
|
|
2086
|
+
});
|
|
2087
|
+
this.socket.once("connect_error", () => {
|
|
2088
|
+
this.socket?.close();
|
|
2089
|
+
this.socket = void 0;
|
|
2090
|
+
resolve();
|
|
2091
|
+
});
|
|
2092
|
+
} catch (error) {
|
|
2093
|
+
this.socket = void 0;
|
|
2094
|
+
resolve();
|
|
2095
|
+
}
|
|
2096
|
+
});
|
|
2097
|
+
this.tui?.start();
|
|
2098
|
+
fs3.rmSync(path3.join(this.PATHS.base, "books_chunks"), {
|
|
2099
|
+
recursive: true,
|
|
2100
|
+
force: true
|
|
2101
|
+
});
|
|
1713
2102
|
for (const mode of gameModesToSimulate) {
|
|
1714
2103
|
completedSimulations = 0;
|
|
1715
2104
|
this.wallet = new Wallet();
|
|
2105
|
+
this.tui?.setDetails({
|
|
2106
|
+
gameMode: mode,
|
|
2107
|
+
totalSims: this.simRunsAmount[mode] || 0
|
|
2108
|
+
});
|
|
1716
2109
|
this.hasWrittenRecord = false;
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
2110
|
+
this.bookIndexMetas = [];
|
|
2111
|
+
this.tempBookIndexPaths = [];
|
|
2112
|
+
this.bookChunkIndexes = /* @__PURE__ */ new Map();
|
|
2113
|
+
this.bookBuffers = /* @__PURE__ */ new Map();
|
|
2114
|
+
this.bookBufferSizes = /* @__PURE__ */ new Map();
|
|
2115
|
+
const startTime = Date.now();
|
|
2116
|
+
statusMessage = `Simulating mode "${mode}" with ${this.simRunsAmount[mode]} runs.`;
|
|
2117
|
+
this.tui?.log(statusMessage);
|
|
2118
|
+
this.sendSimulationStatus(statusMessage);
|
|
1721
2119
|
const runs = this.simRunsAmount[mode] || 0;
|
|
1722
2120
|
if (runs <= 0) continue;
|
|
1723
2121
|
if (!configuredGameModes.includes(mode)) {
|
|
@@ -1725,16 +2123,19 @@ Simulating game mode: ${mode}`);
|
|
|
1725
2123
|
`Tried to simulate game mode "${mode}", but it's not configured in the game config.`
|
|
1726
2124
|
);
|
|
1727
2125
|
}
|
|
1728
|
-
|
|
2126
|
+
this.summary[mode] = {
|
|
2127
|
+
total: { numSims: runs, bsWins: 0, fsWins: 0, rtp: 0 },
|
|
2128
|
+
criteria: {}
|
|
2129
|
+
};
|
|
1729
2130
|
const tempRecordsPath = this.PATHS.tempRecords(mode);
|
|
1730
2131
|
createDirIfNotExists(this.PATHS.base);
|
|
1731
|
-
createDirIfNotExists(
|
|
1732
|
-
this.recordsWriteStream =
|
|
2132
|
+
createDirIfNotExists(path3.join(this.PATHS.base, TEMP_FOLDER));
|
|
2133
|
+
this.recordsWriteStream = fs3.createWriteStream(tempRecordsPath, {
|
|
1733
2134
|
highWaterMark: this.maxHighWaterMark
|
|
1734
2135
|
}).setMaxListeners(30);
|
|
1735
2136
|
const criteriaCounts = ResultSet.getNumberOfSimsForCriteria(this, mode);
|
|
1736
2137
|
const totalSims = Object.values(criteriaCounts).reduce((a, b) => a + b, 0);
|
|
1737
|
-
|
|
2138
|
+
assert5(
|
|
1738
2139
|
totalSims === runs,
|
|
1739
2140
|
`Criteria mismatch for mode "${mode}". Expected ${runs}, got ${totalSims}`
|
|
1740
2141
|
);
|
|
@@ -1749,46 +2150,52 @@ Simulating game mode: ${mode}`);
|
|
|
1749
2150
|
});
|
|
1750
2151
|
createDirIfNotExists(this.PATHS.optimizationFiles);
|
|
1751
2152
|
createDirIfNotExists(this.PATHS.publishFiles);
|
|
1752
|
-
|
|
1753
|
-
|
|
2153
|
+
statusMessage = `Writing final files for game mode "${mode}". This may take a while...`;
|
|
2154
|
+
this.tui?.log(statusMessage);
|
|
2155
|
+
this.sendSimulationStatus(statusMessage);
|
|
2156
|
+
writeFile(
|
|
2157
|
+
this.PATHS.booksIndexMeta(mode),
|
|
2158
|
+
JSON.stringify(
|
|
2159
|
+
this.bookIndexMetas.sort((a, b) => a.worker - b.worker),
|
|
2160
|
+
null,
|
|
2161
|
+
2
|
|
2162
|
+
)
|
|
1754
2163
|
);
|
|
2164
|
+
const booksPath = this.PATHS.booksCompressed(mode);
|
|
1755
2165
|
try {
|
|
1756
|
-
const finalBookStream =
|
|
2166
|
+
const finalBookStream = fs3.createWriteStream(booksPath, {
|
|
1757
2167
|
highWaterMark: this.streamHighWaterMark
|
|
1758
2168
|
});
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
}
|
|
2169
|
+
for (const { worker, chunks: chunks2 } of this.bookIndexMetas) {
|
|
2170
|
+
for (let chunk = 0; chunk < chunks2; chunk++) {
|
|
2171
|
+
const bookChunkPath = this.PATHS.booksChunk(mode, worker, chunk);
|
|
2172
|
+
if (!fs3.existsSync(bookChunkPath)) continue;
|
|
2173
|
+
const chunkData = fs3.readFileSync(bookChunkPath);
|
|
2174
|
+
if (!finalBookStream.write(chunkData)) {
|
|
2175
|
+
await new Promise((r) => finalBookStream.once("drain", () => r()));
|
|
1767
2176
|
}
|
|
1768
|
-
const content = fs2.createReadStream(tempBookPath);
|
|
1769
|
-
for await (const chunk of content) {
|
|
1770
|
-
if (!finalBookStream.write(chunk)) {
|
|
1771
|
-
await new Promise((r) => finalBookStream.once("drain", r));
|
|
1772
|
-
}
|
|
1773
|
-
}
|
|
1774
|
-
fs2.rmSync(tempBookPath);
|
|
1775
|
-
isFirstChunk = false;
|
|
1776
2177
|
}
|
|
1777
2178
|
}
|
|
1778
2179
|
finalBookStream.end();
|
|
1779
|
-
await new Promise((
|
|
2180
|
+
await new Promise((r) => finalBookStream.on("finish", () => r()));
|
|
1780
2181
|
} catch (error) {
|
|
1781
2182
|
throw new Error(`Error merging book files: ${error.message}`);
|
|
1782
2183
|
}
|
|
1783
2184
|
const lutPath = this.PATHS.lookupTable(mode);
|
|
1784
2185
|
const lutPathPublish = this.PATHS.lookupTablePublish(mode);
|
|
1785
2186
|
const lutSegmentedPath = this.PATHS.lookupTableSegmented(mode);
|
|
1786
|
-
await this.mergeCsv(
|
|
1787
|
-
|
|
2187
|
+
await this.mergeCsv(
|
|
2188
|
+
chunks,
|
|
2189
|
+
lutPath,
|
|
2190
|
+
(i) => `temp_lookup_${mode}_${i}.csv`,
|
|
2191
|
+
this.PATHS.lookupTableIndex(mode)
|
|
2192
|
+
);
|
|
2193
|
+
fs3.copyFileSync(lutPath, lutPathPublish);
|
|
1788
2194
|
await this.mergeCsv(
|
|
1789
2195
|
chunks,
|
|
1790
2196
|
lutSegmentedPath,
|
|
1791
|
-
(i) => `temp_lookup_segmented_${mode}_${i}.csv
|
|
2197
|
+
(i) => `temp_lookup_segmented_${mode}_${i}.csv`,
|
|
2198
|
+
this.PATHS.lookupTableSegmentedIndex(mode)
|
|
1792
2199
|
);
|
|
1793
2200
|
if (this.recordsWriteStream) {
|
|
1794
2201
|
await new Promise((resolve) => {
|
|
@@ -1799,29 +2206,52 @@ Simulating game mode: ${mode}`);
|
|
|
1799
2206
|
this.recordsWriteStream = void 0;
|
|
1800
2207
|
}
|
|
1801
2208
|
await this.writeRecords(mode);
|
|
1802
|
-
await this.writeBooksJson(mode);
|
|
1803
2209
|
this.writeIndexJson();
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
this.
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
2210
|
+
if (this.makeUncompressedBooks) {
|
|
2211
|
+
statusMessage = `Creating decompressed book file for mode "${mode}". This may take a while...`;
|
|
2212
|
+
this.tui?.log(statusMessage);
|
|
2213
|
+
this.sendSimulationStatus(statusMessage);
|
|
2214
|
+
const uncompressedBooksPath = this.PATHS.booksUncompressed(mode);
|
|
2215
|
+
const outputStream = fs3.createWriteStream(uncompressedBooksPath, {
|
|
2216
|
+
highWaterMark: this.streamHighWaterMark
|
|
2217
|
+
});
|
|
2218
|
+
try {
|
|
2219
|
+
for (const { worker, chunks: chunks2 } of this.bookIndexMetas) {
|
|
2220
|
+
for (let chunk = 0; chunk < chunks2; chunk++) {
|
|
2221
|
+
const bookChunkPath = this.PATHS.booksChunk(mode, worker, chunk);
|
|
2222
|
+
if (!fs3.existsSync(bookChunkPath)) continue;
|
|
2223
|
+
const inputStream = fs3.createReadStream(bookChunkPath);
|
|
2224
|
+
const compress = zlib.createZstdDecompress();
|
|
2225
|
+
for await (const decompChunk of inputStream.pipe(compress)) {
|
|
2226
|
+
if (!outputStream.write(decompChunk)) {
|
|
2227
|
+
await new Promise((r) => outputStream.once("drain", () => r()));
|
|
2228
|
+
}
|
|
2229
|
+
}
|
|
2230
|
+
}
|
|
2231
|
+
}
|
|
2232
|
+
outputStream.end();
|
|
2233
|
+
await new Promise((r) => outputStream.on("finish", () => r()));
|
|
2234
|
+
} catch (error) {
|
|
2235
|
+
statusMessage = chalk2.yellow(
|
|
2236
|
+
`Error creating uncompressed book file: ${error.message}`
|
|
2237
|
+
);
|
|
2238
|
+
this.tui?.log(statusMessage);
|
|
2239
|
+
this.sendSimulationStatus(statusMessage);
|
|
2240
|
+
}
|
|
2241
|
+
}
|
|
2242
|
+
const endTime = Date.now();
|
|
2243
|
+
const prettyTime = new Date(endTime - startTime).toISOString().slice(11, -1);
|
|
2244
|
+
statusMessage = `Mode ${mode} done! Time taken: ${prettyTime}`;
|
|
2245
|
+
this.tui?.log(statusMessage);
|
|
2246
|
+
this.sendSimulationStatus(statusMessage);
|
|
1817
2247
|
}
|
|
1818
|
-
|
|
1819
|
-
|
|
2248
|
+
this.tui?.stop();
|
|
2249
|
+
await this.printSimulationSummary();
|
|
1820
2250
|
}
|
|
1821
2251
|
let desiredSims = 0;
|
|
1822
2252
|
let actualSims = 0;
|
|
1823
2253
|
const criteriaToRetries = {};
|
|
1824
|
-
if (!
|
|
2254
|
+
if (!isMainThread2) {
|
|
1825
2255
|
const { mode, simStart, simEnd, index, criteriaCounts } = workerData;
|
|
1826
2256
|
const seed = hashStringToInt(mode) + index >>> 0;
|
|
1827
2257
|
const nextCriteria = createCriteriaSampler(criteriaCounts, seed);
|
|
@@ -1836,16 +2266,16 @@ Simulating game mode: ${mode}`);
|
|
|
1836
2266
|
actualSims += this.actualSims;
|
|
1837
2267
|
}
|
|
1838
2268
|
}
|
|
1839
|
-
|
|
1840
|
-
console.log(`Desired ${desiredSims}, Actual ${actualSims}`);
|
|
1841
|
-
console.log(`Retries per criteria:`, criteriaToRetries);
|
|
1842
|
-
}
|
|
1843
|
-
parentPort?.postMessage({
|
|
2269
|
+
parentPort2?.postMessage({
|
|
1844
2270
|
type: "done",
|
|
1845
2271
|
workerNum: index
|
|
1846
2272
|
});
|
|
1847
|
-
|
|
1848
|
-
|
|
2273
|
+
parentPort2?.removeAllListeners();
|
|
2274
|
+
parentPort2?.close();
|
|
2275
|
+
}
|
|
2276
|
+
if (this.socket && this.panelActive) {
|
|
2277
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
2278
|
+
this.socket?.close();
|
|
1849
2279
|
}
|
|
1850
2280
|
}
|
|
1851
2281
|
/**
|
|
@@ -1853,39 +2283,36 @@ Simulating game mode: ${mode}`);
|
|
|
1853
2283
|
*/
|
|
1854
2284
|
async spawnWorkersForGameMode(opts) {
|
|
1855
2285
|
const { mode, chunks, chunkCriteriaCounts, totalSims } = opts;
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
2286
|
+
try {
|
|
2287
|
+
await Promise.all(
|
|
2288
|
+
chunks.map(([simStart, simEnd], index) => {
|
|
2289
|
+
return this.callWorker({
|
|
2290
|
+
basePath: this.PATHS.base,
|
|
2291
|
+
mode,
|
|
2292
|
+
simStart,
|
|
2293
|
+
simEnd,
|
|
2294
|
+
index,
|
|
2295
|
+
totalSims,
|
|
2296
|
+
criteriaCounts: chunkCriteriaCounts[index]
|
|
2297
|
+
});
|
|
2298
|
+
})
|
|
2299
|
+
);
|
|
2300
|
+
} catch (error) {
|
|
2301
|
+
this.tui?.stop();
|
|
2302
|
+
throw error;
|
|
2303
|
+
}
|
|
1869
2304
|
}
|
|
1870
2305
|
async callWorker(opts) {
|
|
1871
2306
|
const { mode, simEnd, simStart, basePath, index, totalSims, criteriaCounts } = opts;
|
|
1872
|
-
function logArrowProgress(current, total) {
|
|
1873
|
-
const percentage = current / total * 100;
|
|
1874
|
-
const progressBarLength = 50;
|
|
1875
|
-
const filledLength = Math.round(progressBarLength * current / total);
|
|
1876
|
-
const bar = "\u2588".repeat(filledLength) + "-".repeat(progressBarLength - filledLength);
|
|
1877
|
-
process.stdout.write(`\r[${bar}] ${percentage.toFixed(2)}% (${current}/${total})`);
|
|
1878
|
-
if (current === total) {
|
|
1879
|
-
process.stdout.write("\n");
|
|
1880
|
-
}
|
|
1881
|
-
}
|
|
1882
2307
|
const write = async (stream, chunk) => {
|
|
1883
2308
|
if (!stream.write(chunk)) {
|
|
1884
2309
|
await new Promise((resolve) => stream.once("drain", resolve));
|
|
1885
2310
|
}
|
|
1886
2311
|
};
|
|
1887
2312
|
return new Promise((resolve, reject) => {
|
|
1888
|
-
const scriptPath =
|
|
2313
|
+
const scriptPath = path3.join(basePath, TEMP_FILENAME);
|
|
2314
|
+
createDirIfNotExists(path3.join(this.PATHS.base, "books_chunks"));
|
|
2315
|
+
const startTime = Date.now();
|
|
1889
2316
|
const worker = new Worker(scriptPath, {
|
|
1890
2317
|
workerData: {
|
|
1891
2318
|
mode,
|
|
@@ -1896,50 +2323,137 @@ Simulating game mode: ${mode}`);
|
|
|
1896
2323
|
}
|
|
1897
2324
|
});
|
|
1898
2325
|
worker.postMessage({ type: "credit", amount: this.maxPendingSims });
|
|
1899
|
-
const
|
|
1900
|
-
|
|
2326
|
+
const flushBookChunk = async () => {
|
|
2327
|
+
if (this.bookBuffers.get(index)?.length === 0) return;
|
|
2328
|
+
if (!this.bookChunkIndexes.has(index)) {
|
|
2329
|
+
this.bookChunkIndexes.set(index, 0);
|
|
2330
|
+
}
|
|
2331
|
+
const chunkIndex = this.bookChunkIndexes.get(index);
|
|
2332
|
+
const bookChunkPath = this.PATHS.booksChunk(mode, index, chunkIndex);
|
|
2333
|
+
const data = this.bookBuffers.get(index).join("\n") + "\n";
|
|
2334
|
+
await pipeline(
|
|
2335
|
+
Readable.from([Buffer.from(data, "utf8")]),
|
|
2336
|
+
zlib.createZstdCompress(),
|
|
2337
|
+
fs3.createWriteStream(bookChunkPath)
|
|
2338
|
+
);
|
|
2339
|
+
this.bookBuffers.set(index, []);
|
|
2340
|
+
this.bookBufferSizes.set(index, 0);
|
|
2341
|
+
this.bookChunkIndexes.set(index, chunkIndex + 1);
|
|
2342
|
+
};
|
|
2343
|
+
const booksIndexPath = this.PATHS.booksIndex(mode, index);
|
|
2344
|
+
const booksIndexStream = fs3.createWriteStream(booksIndexPath, {
|
|
1901
2345
|
highWaterMark: this.maxHighWaterMark
|
|
1902
2346
|
});
|
|
1903
2347
|
const tempLookupPath = this.PATHS.tempLookupTable(mode, index);
|
|
1904
|
-
const lookupStream =
|
|
2348
|
+
const lookupStream = fs3.createWriteStream(tempLookupPath, {
|
|
1905
2349
|
highWaterMark: this.maxHighWaterMark
|
|
1906
2350
|
});
|
|
1907
2351
|
const tempLookupSegPath = this.PATHS.tempLookupTableSegmented(mode, index);
|
|
1908
|
-
const lookupSegmentedStream =
|
|
2352
|
+
const lookupSegmentedStream = fs3.createWriteStream(tempLookupSegPath, {
|
|
1909
2353
|
highWaterMark: this.maxHighWaterMark
|
|
1910
2354
|
});
|
|
1911
2355
|
let writeChain = Promise.resolve();
|
|
1912
2356
|
worker.on("message", (msg) => {
|
|
1913
|
-
if (msg.type === "log") {
|
|
2357
|
+
if (msg.type === "log" || msg.type === "user-log") {
|
|
2358
|
+
this.tui?.log(msg.message);
|
|
1914
2359
|
return;
|
|
1915
2360
|
}
|
|
2361
|
+
if (msg.type === "log-exit") {
|
|
2362
|
+
this.tui?.log(msg.message);
|
|
2363
|
+
this.tui?.stop();
|
|
2364
|
+
console.log(msg.message);
|
|
2365
|
+
process.exit(1);
|
|
2366
|
+
}
|
|
1916
2367
|
if (msg.type === "complete") {
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
2368
|
+
completedSimulations++;
|
|
2369
|
+
if (completedSimulations % 250 === 0 || completedSimulations === totalSims) {
|
|
2370
|
+
const percentage = completedSimulations / totalSims * 100;
|
|
2371
|
+
this.tui?.setProgress(
|
|
2372
|
+
percentage,
|
|
2373
|
+
this.getTimeRemaining(startTime, totalSims),
|
|
2374
|
+
completedSimulations
|
|
2375
|
+
);
|
|
2376
|
+
}
|
|
2377
|
+
if (this.socket && this.panelActive) {
|
|
2378
|
+
if (completedSimulations % 1e3 === 0 || completedSimulations === totalSims) {
|
|
2379
|
+
this.socket.emit("simulationProgress", {
|
|
2380
|
+
mode,
|
|
2381
|
+
percentage: completedSimulations / totalSims * 100,
|
|
2382
|
+
current: completedSimulations,
|
|
2383
|
+
total: totalSims,
|
|
2384
|
+
timeRemaining: this.getTimeRemaining(startTime, totalSims)
|
|
2385
|
+
});
|
|
2386
|
+
this.socket.emit(
|
|
2387
|
+
"simulationShouldStop",
|
|
2388
|
+
this.gameConfig.id,
|
|
2389
|
+
(shouldStop) => {
|
|
2390
|
+
if (shouldStop) {
|
|
2391
|
+
worker.terminate();
|
|
2392
|
+
}
|
|
2393
|
+
}
|
|
2394
|
+
);
|
|
1921
2395
|
}
|
|
2396
|
+
}
|
|
2397
|
+
writeChain = writeChain.then(async () => {
|
|
1922
2398
|
const book = msg.book;
|
|
1923
2399
|
const bookData = {
|
|
1924
2400
|
id: book.id,
|
|
1925
2401
|
payoutMultiplier: book.payout,
|
|
1926
2402
|
events: book.events
|
|
1927
2403
|
};
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
2404
|
+
if (!this.summary[mode]?.criteria[book.criteria]) {
|
|
2405
|
+
this.summary[mode].criteria[book.criteria] = {
|
|
2406
|
+
numSims: 0,
|
|
2407
|
+
bsWins: 0,
|
|
2408
|
+
fsWins: 0,
|
|
2409
|
+
rtp: 0
|
|
2410
|
+
};
|
|
2411
|
+
}
|
|
2412
|
+
const bsWins = round(book.basegameWins, 4);
|
|
2413
|
+
const fsWins = round(book.freespinsWins, 4);
|
|
2414
|
+
this.summary[mode].criteria[book.criteria].numSims += 1;
|
|
2415
|
+
this.summary[mode].total.bsWins += bsWins;
|
|
2416
|
+
this.summary[mode].total.fsWins += fsWins;
|
|
2417
|
+
this.summary[mode].criteria[book.criteria].bsWins += bsWins;
|
|
2418
|
+
this.summary[mode].criteria[book.criteria].fsWins += fsWins;
|
|
2419
|
+
const bookLine = JSON.stringify(bookData);
|
|
2420
|
+
const lineSize = Buffer.byteLength(bookLine + "\n", "utf8");
|
|
2421
|
+
if (this.bookBuffers.has(index)) {
|
|
2422
|
+
this.bookBuffers.get(index).push(bookLine);
|
|
2423
|
+
this.bookBufferSizes.set(
|
|
2424
|
+
index,
|
|
2425
|
+
this.bookBufferSizes.get(index) + lineSize
|
|
2426
|
+
);
|
|
2427
|
+
} else {
|
|
2428
|
+
this.bookBuffers.set(index, [bookLine]);
|
|
2429
|
+
this.bookBufferSizes.set(index, lineSize);
|
|
2430
|
+
}
|
|
2431
|
+
if (!this.tempBookIndexPaths.includes(booksIndexPath)) {
|
|
2432
|
+
this.tempBookIndexPaths.push(booksIndexPath);
|
|
2433
|
+
}
|
|
2434
|
+
await Promise.all([
|
|
2435
|
+
write(
|
|
2436
|
+
booksIndexStream,
|
|
2437
|
+
`${book.id},${index},${this.bookChunkIndexes.get(index) || 0}
|
|
1935
2438
|
`
|
|
1936
|
-
|
|
2439
|
+
),
|
|
2440
|
+
write(lookupStream, `${book.id},1,${Math.round(book.payout)}
|
|
2441
|
+
`),
|
|
2442
|
+
write(
|
|
2443
|
+
lookupSegmentedStream,
|
|
2444
|
+
`${book.id},${book.criteria},${book.basegameWins},${book.freespinsWins}
|
|
2445
|
+
`
|
|
2446
|
+
)
|
|
2447
|
+
]);
|
|
2448
|
+
if (this.bookBufferSizes.get(index) >= 10 * 1024 * 1024) {
|
|
2449
|
+
await flushBookChunk();
|
|
2450
|
+
}
|
|
1937
2451
|
if (this.recordsWriteStream) {
|
|
1938
2452
|
for (const record of msg.records) {
|
|
1939
2453
|
const recordPrefix = this.hasWrittenRecord ? "\n" : "";
|
|
1940
2454
|
await write(
|
|
1941
2455
|
this.recordsWriteStream,
|
|
1942
|
-
recordPrefix +
|
|
2456
|
+
recordPrefix + JSON.stringify(record)
|
|
1943
2457
|
);
|
|
1944
2458
|
this.hasWrittenRecord = true;
|
|
1945
2459
|
}
|
|
@@ -1951,31 +2465,35 @@ Simulating game mode: ${mode}`);
|
|
|
1951
2465
|
}
|
|
1952
2466
|
if (msg.type === "done") {
|
|
1953
2467
|
writeChain.then(async () => {
|
|
1954
|
-
|
|
2468
|
+
await flushBookChunk();
|
|
1955
2469
|
lookupStream.end();
|
|
1956
2470
|
lookupSegmentedStream.end();
|
|
2471
|
+
booksIndexStream.end();
|
|
1957
2472
|
await Promise.all([
|
|
1958
|
-
new Promise((r) => bookStream.on("finish", () => r())),
|
|
1959
2473
|
new Promise((r) => lookupStream.on("finish", () => r())),
|
|
1960
|
-
new Promise((r) => lookupSegmentedStream.on("finish", () => r()))
|
|
2474
|
+
new Promise((r) => lookupSegmentedStream.on("finish", () => r())),
|
|
2475
|
+
new Promise((r) => booksIndexStream.on("finish", () => r()))
|
|
1961
2476
|
]);
|
|
2477
|
+
const bookIndexMeta = {
|
|
2478
|
+
worker: index,
|
|
2479
|
+
chunks: this.bookChunkIndexes.get(index) + 2,
|
|
2480
|
+
simStart,
|
|
2481
|
+
simEnd
|
|
2482
|
+
};
|
|
2483
|
+
this.bookIndexMetas.push(bookIndexMeta);
|
|
1962
2484
|
resolve(true);
|
|
1963
2485
|
}).catch(reject);
|
|
1964
2486
|
return;
|
|
1965
2487
|
}
|
|
1966
2488
|
});
|
|
1967
2489
|
worker.on("error", (error) => {
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
`);
|
|
1971
|
-
process.stdout.write(`
|
|
1972
|
-
${error.stack}
|
|
1973
|
-
`);
|
|
1974
|
-
reject(error);
|
|
2490
|
+
this.tui?.log(error.message);
|
|
2491
|
+
resolve(error);
|
|
1975
2492
|
});
|
|
1976
2493
|
worker.on("exit", (code) => {
|
|
1977
2494
|
if (code !== 0) {
|
|
1978
|
-
|
|
2495
|
+
this.tui?.log(chalk2.yellow(`Worker stopped with exit code ${code}`));
|
|
2496
|
+
resolve(false);
|
|
1979
2497
|
}
|
|
1980
2498
|
});
|
|
1981
2499
|
});
|
|
@@ -1985,12 +2503,21 @@ ${error.stack}
|
|
|
1985
2503
|
*/
|
|
1986
2504
|
runSingleSimulation(opts) {
|
|
1987
2505
|
const { simId, mode, criteria } = opts;
|
|
2506
|
+
let retries = 0;
|
|
1988
2507
|
const ctx = createGameContext({
|
|
1989
2508
|
config: this.gameConfig
|
|
1990
2509
|
});
|
|
1991
2510
|
ctx.state.currentGameMode = mode;
|
|
1992
2511
|
ctx.state.currentSimulationId = simId;
|
|
1993
2512
|
ctx.state.isCriteriaMet = false;
|
|
2513
|
+
ctx.services.data._setBook(
|
|
2514
|
+
new Book({
|
|
2515
|
+
id: simId,
|
|
2516
|
+
criteria
|
|
2517
|
+
})
|
|
2518
|
+
);
|
|
2519
|
+
ctx.services.wallet._setWallet(new Wallet());
|
|
2520
|
+
ctx.services.data._setRecorder(new Recorder());
|
|
1994
2521
|
const resultSet = ctx.services.game.getResultSetByCriteria(
|
|
1995
2522
|
ctx.state.currentGameMode,
|
|
1996
2523
|
criteria
|
|
@@ -2003,6 +2530,21 @@ ${error.stack}
|
|
|
2003
2530
|
if (resultSet.meetsCriteria(ctx)) {
|
|
2004
2531
|
ctx.state.isCriteriaMet = true;
|
|
2005
2532
|
}
|
|
2533
|
+
retries++;
|
|
2534
|
+
if (!ctx.state.isCriteriaMet && retries % 1e4 === 0) {
|
|
2535
|
+
parentPort2?.postMessage({
|
|
2536
|
+
type: "log",
|
|
2537
|
+
message: chalk2.yellow(
|
|
2538
|
+
`Excessive retries @ #${simId} @ criteria "${criteria}": ${retries} retries`
|
|
2539
|
+
)
|
|
2540
|
+
});
|
|
2541
|
+
}
|
|
2542
|
+
if (!ctx.state.isCriteriaMet && retries % 5e4 === 0) {
|
|
2543
|
+
parentPort2?.postMessage({
|
|
2544
|
+
type: "log-exit",
|
|
2545
|
+
message: chalk2.red("Possible infinite loop detected, exiting simulation.")
|
|
2546
|
+
});
|
|
2547
|
+
}
|
|
2006
2548
|
}
|
|
2007
2549
|
ctx.services.wallet._getWallet().writePayoutToBook(ctx);
|
|
2008
2550
|
ctx.services.wallet._getWallet().confirmWins(ctx);
|
|
@@ -2014,10 +2556,10 @@ ${error.stack}
|
|
|
2014
2556
|
});
|
|
2015
2557
|
ctx.config.hooks.onSimulationAccepted?.(ctx);
|
|
2016
2558
|
this.confirmRecords(ctx);
|
|
2017
|
-
|
|
2559
|
+
parentPort2?.postMessage({
|
|
2018
2560
|
type: "complete",
|
|
2019
2561
|
simId,
|
|
2020
|
-
book: ctx.services.data._getBook().
|
|
2562
|
+
book: ctx.services.data._getBook()._serialize(),
|
|
2021
2563
|
wallet: ctx.services.wallet._getWallet().serialize(),
|
|
2022
2564
|
records: ctx.services.data._getRecords()
|
|
2023
2565
|
});
|
|
@@ -2025,7 +2567,7 @@ ${error.stack}
|
|
|
2025
2567
|
initCreditListener() {
|
|
2026
2568
|
if (this.creditListenerInit) return;
|
|
2027
2569
|
this.creditListenerInit = true;
|
|
2028
|
-
|
|
2570
|
+
parentPort2?.on("message", (msg) => {
|
|
2029
2571
|
if (msg?.type !== "credit") return;
|
|
2030
2572
|
const amount = Number(msg?.amount ?? 0);
|
|
2031
2573
|
if (!Number.isFinite(amount) || amount <= 0) return;
|
|
@@ -2055,17 +2597,10 @@ ${error.stack}
|
|
|
2055
2597
|
resetSimulation(ctx) {
|
|
2056
2598
|
this.resetState(ctx);
|
|
2057
2599
|
ctx.services.board.resetBoard();
|
|
2058
|
-
ctx.services.data.
|
|
2059
|
-
ctx.services.wallet.
|
|
2060
|
-
ctx.services.data.
|
|
2061
|
-
|
|
2062
|
-
id: ctx.state.currentSimulationId,
|
|
2063
|
-
criteria: ctx.state.currentResultSet.criteria
|
|
2064
|
-
})
|
|
2065
|
-
);
|
|
2066
|
-
Object.values(ctx.config.gameModes).forEach((mode) => {
|
|
2067
|
-
mode._resetTempValues();
|
|
2068
|
-
});
|
|
2600
|
+
ctx.services.data._getRecorder()._reset();
|
|
2601
|
+
ctx.services.wallet._getWallet()._reset();
|
|
2602
|
+
ctx.services.data._getBook()._reset(ctx.state.currentSimulationId, ctx.state.currentResultSet.criteria);
|
|
2603
|
+
ctx.services.game.getCurrentGameMode()._resetTempValues();
|
|
2069
2604
|
}
|
|
2070
2605
|
resetState(ctx) {
|
|
2071
2606
|
ctx.services.rng.setSeedIfDifferent(ctx.state.currentSimulationId);
|
|
@@ -2092,18 +2627,25 @@ ${error.stack}
|
|
|
2092
2627
|
async writeRecords(mode) {
|
|
2093
2628
|
const tempRecordsPath = this.PATHS.tempRecords(mode);
|
|
2094
2629
|
const forceRecordsPath = this.PATHS.forceRecords(mode);
|
|
2630
|
+
const allSearchKeysAndValues = /* @__PURE__ */ new Map();
|
|
2095
2631
|
const aggregatedRecords = /* @__PURE__ */ new Map();
|
|
2096
|
-
if (
|
|
2097
|
-
const fileStream =
|
|
2632
|
+
if (fs3.existsSync(tempRecordsPath)) {
|
|
2633
|
+
const fileStream = fs3.createReadStream(tempRecordsPath, {
|
|
2098
2634
|
highWaterMark: this.streamHighWaterMark
|
|
2099
2635
|
});
|
|
2100
|
-
const rl =
|
|
2636
|
+
const rl = readline.createInterface({
|
|
2101
2637
|
input: fileStream,
|
|
2102
2638
|
crlfDelay: Infinity
|
|
2103
2639
|
});
|
|
2104
2640
|
for await (const line of rl) {
|
|
2105
2641
|
if (line.trim() === "") continue;
|
|
2106
2642
|
const record = JSON.parse(line);
|
|
2643
|
+
for (const entry of record.search) {
|
|
2644
|
+
if (!allSearchKeysAndValues.has(entry.name)) {
|
|
2645
|
+
allSearchKeysAndValues.set(entry.name, /* @__PURE__ */ new Set());
|
|
2646
|
+
}
|
|
2647
|
+
allSearchKeysAndValues.get(entry.name).add(String(entry.value));
|
|
2648
|
+
}
|
|
2107
2649
|
const key = JSON.stringify(record.search);
|
|
2108
2650
|
let existing = aggregatedRecords.get(key);
|
|
2109
2651
|
if (!existing) {
|
|
@@ -2120,8 +2662,9 @@ ${error.stack}
|
|
|
2120
2662
|
}
|
|
2121
2663
|
}
|
|
2122
2664
|
}
|
|
2123
|
-
|
|
2124
|
-
|
|
2665
|
+
fs3.rmSync(forceRecordsPath, { force: true });
|
|
2666
|
+
fs3.rmSync(this.PATHS.forceKeys(mode), { force: true });
|
|
2667
|
+
const writeStream = fs3.createWriteStream(forceRecordsPath, { encoding: "utf-8" });
|
|
2125
2668
|
writeStream.write("[\n");
|
|
2126
2669
|
let isFirst = true;
|
|
2127
2670
|
for (const record of aggregatedRecords.values()) {
|
|
@@ -2136,13 +2679,17 @@ ${error.stack}
|
|
|
2136
2679
|
await new Promise((resolve) => {
|
|
2137
2680
|
writeStream.on("finish", () => resolve());
|
|
2138
2681
|
});
|
|
2139
|
-
|
|
2682
|
+
const forceJson = Object.fromEntries(
|
|
2683
|
+
Array.from(allSearchKeysAndValues.entries()).sort(([a], [b]) => a.localeCompare(b)).map(([key, values]) => [key, Array.from(values)])
|
|
2684
|
+
);
|
|
2685
|
+
writeFile(this.PATHS.forceKeys(mode), JSON.stringify(forceJson, null, 2));
|
|
2686
|
+
fs3.rmSync(tempRecordsPath, { force: true });
|
|
2140
2687
|
}
|
|
2141
2688
|
writeIndexJson() {
|
|
2142
2689
|
const outputFilePath = this.PATHS.indexJson;
|
|
2143
2690
|
const modes = Object.keys(this.simRunsAmount).map((id) => {
|
|
2144
2691
|
const mode = this.gameConfig.gameModes[id];
|
|
2145
|
-
|
|
2692
|
+
assert5(mode, `Game mode "${id}" not found in game config.`);
|
|
2146
2693
|
return {
|
|
2147
2694
|
name: mode.name,
|
|
2148
2695
|
cost: mode.cost,
|
|
@@ -2152,38 +2699,26 @@ ${error.stack}
|
|
|
2152
2699
|
});
|
|
2153
2700
|
writeFile(outputFilePath, JSON.stringify({ modes }, null, 2));
|
|
2154
2701
|
}
|
|
2155
|
-
async writeBooksJson(gameMode) {
|
|
2156
|
-
const outputFilePath = this.PATHS.books(gameMode);
|
|
2157
|
-
const compressedFilePath = this.PATHS.booksCompressed(gameMode);
|
|
2158
|
-
fs2.rmSync(compressedFilePath, { force: true });
|
|
2159
|
-
if (fs2.existsSync(outputFilePath)) {
|
|
2160
|
-
await pipeline(
|
|
2161
|
-
fs2.createReadStream(outputFilePath),
|
|
2162
|
-
zlib.createZstdCompress(),
|
|
2163
|
-
fs2.createWriteStream(compressedFilePath)
|
|
2164
|
-
);
|
|
2165
|
-
}
|
|
2166
|
-
}
|
|
2167
2702
|
/**
|
|
2168
2703
|
* Compiles user configured game to JS for use in different Node processes
|
|
2169
2704
|
*/
|
|
2170
2705
|
preprocessFiles() {
|
|
2171
|
-
const builtFilePath =
|
|
2706
|
+
const builtFilePath = path3.join(
|
|
2172
2707
|
this.gameConfig.rootDir,
|
|
2173
2708
|
this.gameConfig.outputDir,
|
|
2174
2709
|
TEMP_FILENAME
|
|
2175
2710
|
);
|
|
2176
|
-
|
|
2711
|
+
fs3.rmSync(builtFilePath, { force: true });
|
|
2177
2712
|
buildSync({
|
|
2178
2713
|
entryPoints: [this.gameConfig.rootDir],
|
|
2179
2714
|
bundle: true,
|
|
2180
2715
|
platform: "node",
|
|
2181
|
-
outfile:
|
|
2716
|
+
outfile: path3.join(
|
|
2182
2717
|
this.gameConfig.rootDir,
|
|
2183
2718
|
this.gameConfig.outputDir,
|
|
2184
2719
|
TEMP_FILENAME
|
|
2185
2720
|
),
|
|
2186
|
-
external: ["esbuild"]
|
|
2721
|
+
external: ["esbuild", "yargs"]
|
|
2187
2722
|
});
|
|
2188
2723
|
}
|
|
2189
2724
|
getSimRangesForChunks(total, chunks) {
|
|
@@ -2218,27 +2753,62 @@ ${error.stack}
|
|
|
2218
2753
|
}
|
|
2219
2754
|
}
|
|
2220
2755
|
}
|
|
2221
|
-
|
|
2756
|
+
getTimeRemaining(startTime, totalSims) {
|
|
2757
|
+
const elapsedTime = Date.now() - startTime;
|
|
2758
|
+
const simsLeft = totalSims - completedSimulations;
|
|
2759
|
+
const timePerSim = elapsedTime / completedSimulations;
|
|
2760
|
+
const timeRemaining = Math.round(simsLeft * timePerSim / 1e3);
|
|
2761
|
+
return timeRemaining;
|
|
2762
|
+
}
|
|
2763
|
+
async mergeCsv(chunks, outPath, tempName, lutIndexPath) {
|
|
2222
2764
|
try {
|
|
2223
|
-
|
|
2224
|
-
const
|
|
2765
|
+
fs3.rmSync(outPath, { force: true });
|
|
2766
|
+
const lutStream = fs3.createWriteStream(outPath, {
|
|
2225
2767
|
highWaterMark: this.streamHighWaterMark
|
|
2226
2768
|
});
|
|
2769
|
+
const lutIndexStream = lutIndexPath ? fs3.createWriteStream(lutIndexPath, {
|
|
2770
|
+
highWaterMark: this.streamHighWaterMark
|
|
2771
|
+
}) : void 0;
|
|
2772
|
+
let offset = 0n;
|
|
2227
2773
|
for (let i = 0; i < chunks.length; i++) {
|
|
2228
|
-
const
|
|
2229
|
-
if (!
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2774
|
+
const tempLutChunk = path3.join(this.PATHS.base, TEMP_FOLDER, tempName(i));
|
|
2775
|
+
if (!fs3.existsSync(tempLutChunk)) continue;
|
|
2776
|
+
if (lutIndexStream) {
|
|
2777
|
+
const rl = readline.createInterface({
|
|
2778
|
+
input: fs3.createReadStream(tempLutChunk),
|
|
2779
|
+
crlfDelay: Infinity
|
|
2780
|
+
});
|
|
2781
|
+
for await (const line of rl) {
|
|
2782
|
+
if (!line.trim()) continue;
|
|
2783
|
+
const indexBuffer = Buffer.alloc(8);
|
|
2784
|
+
indexBuffer.writeBigUInt64LE(offset);
|
|
2785
|
+
if (!lutIndexStream.write(indexBuffer)) {
|
|
2786
|
+
await new Promise((resolve) => lutIndexStream.once("drain", resolve));
|
|
2787
|
+
}
|
|
2788
|
+
const lineWithNewline = line + "\n";
|
|
2789
|
+
if (!lutStream.write(lineWithNewline)) {
|
|
2790
|
+
await new Promise((resolve) => lutStream.once("drain", resolve));
|
|
2791
|
+
}
|
|
2792
|
+
offset += BigInt(Buffer.byteLength(lineWithNewline, "utf8"));
|
|
2793
|
+
}
|
|
2794
|
+
} else {
|
|
2795
|
+
const tempChunkStream = fs3.createReadStream(tempLutChunk, {
|
|
2796
|
+
highWaterMark: this.streamHighWaterMark
|
|
2797
|
+
});
|
|
2798
|
+
for await (const buf of tempChunkStream) {
|
|
2799
|
+
if (!lutStream.write(buf)) {
|
|
2800
|
+
await new Promise((resolve) => lutStream.once("drain", resolve));
|
|
2801
|
+
}
|
|
2236
2802
|
}
|
|
2237
2803
|
}
|
|
2238
|
-
|
|
2804
|
+
fs3.rmSync(tempLutChunk);
|
|
2239
2805
|
}
|
|
2240
|
-
|
|
2241
|
-
|
|
2806
|
+
lutStream.end();
|
|
2807
|
+
lutIndexStream?.end();
|
|
2808
|
+
await Promise.all([
|
|
2809
|
+
new Promise((resolve) => lutStream.on("finish", resolve)),
|
|
2810
|
+
lutIndexStream ? new Promise((resolve) => lutIndexStream.on("finish", resolve)) : Promise.resolve()
|
|
2811
|
+
]);
|
|
2242
2812
|
} catch (error) {
|
|
2243
2813
|
throw new Error(`Error merging CSV files: ${error.message}`);
|
|
2244
2814
|
}
|
|
@@ -2249,21 +2819,16 @@ ${error.stack}
|
|
|
2249
2819
|
confirmRecords(ctx) {
|
|
2250
2820
|
const recorder = ctx.services.data._getRecorder();
|
|
2251
2821
|
for (const pendingRecord of recorder.pendingRecords) {
|
|
2252
|
-
const
|
|
2253
|
-
let record = recorder.
|
|
2254
|
-
if (r.search.length !== search.length) return false;
|
|
2255
|
-
for (let i = 0; i < r.search.length; i++) {
|
|
2256
|
-
if (r.search[i].name !== search[i].name) return false;
|
|
2257
|
-
if (r.search[i].value !== search[i].value) return false;
|
|
2258
|
-
}
|
|
2259
|
-
return true;
|
|
2260
|
-
});
|
|
2822
|
+
const key = Object.keys(pendingRecord.properties).sort().map((k) => `${k}:${pendingRecord.properties[k]}`).join("|");
|
|
2823
|
+
let record = recorder.recordsMap.get(key);
|
|
2261
2824
|
if (!record) {
|
|
2825
|
+
const search = Object.entries(pendingRecord.properties).map(([name, value]) => ({ name, value })).sort((a, b) => a.name.localeCompare(b.name));
|
|
2262
2826
|
record = {
|
|
2263
2827
|
search,
|
|
2264
2828
|
timesTriggered: 0,
|
|
2265
2829
|
bookIds: []
|
|
2266
2830
|
};
|
|
2831
|
+
recorder.recordsMap.set(key, record);
|
|
2267
2832
|
recorder.records.push(record);
|
|
2268
2833
|
}
|
|
2269
2834
|
record.timesTriggered++;
|
|
@@ -2273,18 +2838,76 @@ ${error.stack}
|
|
|
2273
2838
|
}
|
|
2274
2839
|
recorder.pendingRecords = [];
|
|
2275
2840
|
}
|
|
2841
|
+
async printSimulationSummary() {
|
|
2842
|
+
Object.entries(this.summary).forEach(([mode, modeSummary]) => {
|
|
2843
|
+
const modeCost = this.gameConfig.gameModes[mode].cost;
|
|
2844
|
+
Object.entries(modeSummary.criteria).forEach(([criteria, criteriaSummary]) => {
|
|
2845
|
+
const totalWins2 = criteriaSummary.bsWins + criteriaSummary.fsWins;
|
|
2846
|
+
const rtp2 = totalWins2 / (criteriaSummary.numSims * modeCost);
|
|
2847
|
+
this.summary[mode].criteria[criteria].rtp = round(rtp2, 4);
|
|
2848
|
+
this.summary[mode].criteria[criteria].bsWins = round(criteriaSummary.bsWins, 4);
|
|
2849
|
+
this.summary[mode].criteria[criteria].fsWins = round(criteriaSummary.fsWins, 4);
|
|
2850
|
+
});
|
|
2851
|
+
const totalWins = modeSummary.total.bsWins + modeSummary.total.fsWins;
|
|
2852
|
+
const rtp = totalWins / (modeSummary.total.numSims * modeCost);
|
|
2853
|
+
this.summary[mode].total.rtp = round(rtp, 4);
|
|
2854
|
+
this.summary[mode].total.bsWins = round(modeSummary.total.bsWins, 4);
|
|
2855
|
+
this.summary[mode].total.fsWins = round(modeSummary.total.fsWins, 4);
|
|
2856
|
+
});
|
|
2857
|
+
const maxLineLength = 50;
|
|
2858
|
+
let output = chalk2.green.bold("\nSimulation Summary\n");
|
|
2859
|
+
for (const [mode, modeSummary] of Object.entries(this.summary)) {
|
|
2860
|
+
output += "-".repeat(maxLineLength) + "\n\n";
|
|
2861
|
+
output += chalk2.bold.bgWhite(`Mode: ${mode}
|
|
2862
|
+
`);
|
|
2863
|
+
output += `Simulations: ${modeSummary.total.numSims}
|
|
2864
|
+
`;
|
|
2865
|
+
output += `Basegame Wins: ${modeSummary.total.bsWins}
|
|
2866
|
+
`;
|
|
2867
|
+
output += `Freespins Wins: ${modeSummary.total.fsWins}
|
|
2868
|
+
`;
|
|
2869
|
+
output += `RTP (unoptimized): ${modeSummary.total.rtp}
|
|
2870
|
+
`;
|
|
2871
|
+
output += chalk2.bold("\n Result Set Summary:\n");
|
|
2872
|
+
for (const [criteria, criteriaSummary] of Object.entries(modeSummary.criteria)) {
|
|
2873
|
+
output += chalk2.gray(" " + "-".repeat(maxLineLength - 4)) + "\n";
|
|
2874
|
+
output += chalk2.bold(` Criteria: ${criteria}
|
|
2875
|
+
`);
|
|
2876
|
+
output += ` Simulations: ${criteriaSummary.numSims}
|
|
2877
|
+
`;
|
|
2878
|
+
output += ` Basegame Wins: ${criteriaSummary.bsWins}
|
|
2879
|
+
`;
|
|
2880
|
+
output += ` Freespins Wins: ${criteriaSummary.fsWins}
|
|
2881
|
+
`;
|
|
2882
|
+
output += ` RTP (unoptimized): ${criteriaSummary.rtp}
|
|
2883
|
+
`;
|
|
2884
|
+
}
|
|
2885
|
+
}
|
|
2886
|
+
console.log(output);
|
|
2887
|
+
writeFile(this.PATHS.simulationSummary, JSON.stringify(this.summary, null, 2));
|
|
2888
|
+
if (this.socket && this.panelActive) {
|
|
2889
|
+
this.socket.emit("simulationSummary", {
|
|
2890
|
+
summary: this.summary
|
|
2891
|
+
});
|
|
2892
|
+
}
|
|
2893
|
+
}
|
|
2894
|
+
sendSimulationStatus(message) {
|
|
2895
|
+
if (this.socket && this.panelActive) {
|
|
2896
|
+
this.socket.emit("simulationStatus", message);
|
|
2897
|
+
}
|
|
2898
|
+
}
|
|
2276
2899
|
};
|
|
2277
2900
|
|
|
2278
2901
|
// src/analysis/index.ts
|
|
2279
|
-
import
|
|
2280
|
-
import
|
|
2281
|
-
import assert8 from "assert";
|
|
2902
|
+
import fs4 from "fs";
|
|
2903
|
+
import assert6 from "assert";
|
|
2282
2904
|
|
|
2283
2905
|
// src/analysis/utils.ts
|
|
2284
2906
|
function parseLookupTable(content) {
|
|
2285
2907
|
const lines = content.trim().split("\n");
|
|
2286
2908
|
const lut = [];
|
|
2287
2909
|
for (const line of lines) {
|
|
2910
|
+
if (!line.trim()) continue;
|
|
2288
2911
|
const [indexStr, weightStr, payoutStr] = line.split(",");
|
|
2289
2912
|
const index = parseInt(indexStr.trim());
|
|
2290
2913
|
const weight = parseInt(weightStr.trim());
|
|
@@ -2297,11 +2920,12 @@ function parseLookupTableSegmented(content) {
|
|
|
2297
2920
|
const lines = content.trim().split("\n");
|
|
2298
2921
|
const lut = [];
|
|
2299
2922
|
for (const line of lines) {
|
|
2300
|
-
|
|
2923
|
+
if (!line.trim()) continue;
|
|
2924
|
+
const [indexStr, criteria, bsWinsStr, fsWinsStr] = line.split(",");
|
|
2301
2925
|
const index = parseInt(indexStr.trim());
|
|
2302
|
-
const
|
|
2303
|
-
const
|
|
2304
|
-
lut.push([index, criteria,
|
|
2926
|
+
const bsWins = parseFloat(bsWinsStr.trim());
|
|
2927
|
+
const fsWins = parseFloat(fsWinsStr.trim());
|
|
2928
|
+
lut.push([index, criteria, bsWins, fsWins]);
|
|
2305
2929
|
}
|
|
2306
2930
|
return lut;
|
|
2307
2931
|
}
|
|
@@ -2335,19 +2959,19 @@ function getPayoutWeights(lut, opts = {}) {
|
|
|
2335
2959
|
function getNonZeroHitrate(payoutWeights) {
|
|
2336
2960
|
const totalWeight = getTotalWeight(payoutWeights);
|
|
2337
2961
|
if (Math.min(...Object.keys(payoutWeights).map(Number)) == 0) {
|
|
2338
|
-
return totalWeight / (totalWeight - (payoutWeights[0] ?? 0) / totalWeight);
|
|
2962
|
+
return round(totalWeight / (totalWeight - (payoutWeights[0] ?? 0) / totalWeight), 4);
|
|
2339
2963
|
} else {
|
|
2340
2964
|
return 1;
|
|
2341
2965
|
}
|
|
2342
2966
|
}
|
|
2343
2967
|
function getNullHitrate(payoutWeights) {
|
|
2344
|
-
return payoutWeights[0] ?? 0;
|
|
2968
|
+
return round(payoutWeights[0] ?? 0, 4);
|
|
2345
2969
|
}
|
|
2346
2970
|
function getMaxwinHitrate(payoutWeights) {
|
|
2347
2971
|
const totalWeight = getTotalWeight(payoutWeights);
|
|
2348
2972
|
const maxWin = Math.max(...Object.keys(payoutWeights).map(Number));
|
|
2349
2973
|
const hitRate = (payoutWeights[maxWin] || 0) / totalWeight;
|
|
2350
|
-
return 1 / hitRate;
|
|
2974
|
+
return round(1 / hitRate, 4);
|
|
2351
2975
|
}
|
|
2352
2976
|
function getUniquePayouts(payoutWeights) {
|
|
2353
2977
|
return Object.keys(payoutWeights).length;
|
|
@@ -2366,15 +2990,15 @@ function getAvgWin(payoutWeights) {
|
|
|
2366
2990
|
const payout = parseFloat(payoutStr);
|
|
2367
2991
|
avgWin += payout * weight;
|
|
2368
2992
|
}
|
|
2369
|
-
return avgWin;
|
|
2993
|
+
return round(avgWin, 4);
|
|
2370
2994
|
}
|
|
2371
2995
|
function getRtp(payoutWeights, cost) {
|
|
2372
2996
|
const avgWin = getAvgWin(payoutWeights);
|
|
2373
|
-
return avgWin / cost;
|
|
2997
|
+
return round(avgWin / cost, 4);
|
|
2374
2998
|
}
|
|
2375
2999
|
function getStandardDeviation(payoutWeights) {
|
|
2376
3000
|
const variance = getVariance(payoutWeights);
|
|
2377
|
-
return Math.sqrt(variance);
|
|
3001
|
+
return round(Math.sqrt(variance), 4);
|
|
2378
3002
|
}
|
|
2379
3003
|
function getVariance(payoutWeights) {
|
|
2380
3004
|
const totalWeight = getTotalWeight(payoutWeights);
|
|
@@ -2384,7 +3008,7 @@ function getVariance(payoutWeights) {
|
|
|
2384
3008
|
const payout = parseFloat(payoutStr);
|
|
2385
3009
|
variance += Math.pow(payout - avgWin, 2) * (weight / totalWeight);
|
|
2386
3010
|
}
|
|
2387
|
-
return variance;
|
|
3011
|
+
return round(variance, 4);
|
|
2388
3012
|
}
|
|
2389
3013
|
function getLessBetHitrate(payoutWeights, cost) {
|
|
2390
3014
|
let lessBetWeight = 0;
|
|
@@ -2395,80 +3019,29 @@ function getLessBetHitrate(payoutWeights, cost) {
|
|
|
2395
3019
|
lessBetWeight += weight;
|
|
2396
3020
|
}
|
|
2397
3021
|
}
|
|
2398
|
-
return lessBetWeight / totalWeight;
|
|
3022
|
+
return round(lessBetWeight / totalWeight, 4);
|
|
2399
3023
|
}
|
|
2400
3024
|
|
|
2401
3025
|
// src/analysis/index.ts
|
|
2402
|
-
import { isMainThread as
|
|
3026
|
+
import { isMainThread as isMainThread3 } from "worker_threads";
|
|
2403
3027
|
var Analysis = class {
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
constructor(optimizer) {
|
|
2408
|
-
this.gameConfig = optimizer.getGameConfig();
|
|
2409
|
-
this.optimizerConfig = optimizer.getOptimizerGameModes();
|
|
2410
|
-
this.filePaths = {};
|
|
3028
|
+
game;
|
|
3029
|
+
constructor(game) {
|
|
3030
|
+
this.game = game;
|
|
2411
3031
|
}
|
|
2412
3032
|
async runAnalysis(gameModes) {
|
|
2413
|
-
if (!
|
|
2414
|
-
this.filePaths = this.getPathsForModes(gameModes);
|
|
3033
|
+
if (!isMainThread3) return;
|
|
2415
3034
|
this.getNumberStats(gameModes);
|
|
2416
3035
|
this.getWinRanges(gameModes);
|
|
2417
3036
|
console.log("Analysis complete. Files written to build directory.");
|
|
2418
3037
|
}
|
|
2419
|
-
getPathsForModes(gameModes) {
|
|
2420
|
-
const rootPath = this.gameConfig.rootDir;
|
|
2421
|
-
const paths = {};
|
|
2422
|
-
for (const modeStr of gameModes) {
|
|
2423
|
-
const lut = path2.join(
|
|
2424
|
-
rootPath,
|
|
2425
|
-
this.gameConfig.outputDir,
|
|
2426
|
-
`lookUpTable_${modeStr}.csv`
|
|
2427
|
-
);
|
|
2428
|
-
const lutSegmented = path2.join(
|
|
2429
|
-
rootPath,
|
|
2430
|
-
this.gameConfig.outputDir,
|
|
2431
|
-
`lookUpTableSegmented_${modeStr}.csv`
|
|
2432
|
-
);
|
|
2433
|
-
const lutOptimized = path2.join(
|
|
2434
|
-
rootPath,
|
|
2435
|
-
this.gameConfig.outputDir,
|
|
2436
|
-
"publish_files",
|
|
2437
|
-
`lookUpTable_${modeStr}_0.csv`
|
|
2438
|
-
);
|
|
2439
|
-
const booksJsonl = path2.join(
|
|
2440
|
-
rootPath,
|
|
2441
|
-
this.gameConfig.outputDir,
|
|
2442
|
-
`books_${modeStr}.jsonl`
|
|
2443
|
-
);
|
|
2444
|
-
const booksJsonlCompressed = path2.join(
|
|
2445
|
-
rootPath,
|
|
2446
|
-
this.gameConfig.outputDir,
|
|
2447
|
-
"publish_files",
|
|
2448
|
-
`books_${modeStr}.jsonl.zst`
|
|
2449
|
-
);
|
|
2450
|
-
paths[modeStr] = {
|
|
2451
|
-
lut,
|
|
2452
|
-
lutSegmented,
|
|
2453
|
-
lutOptimized,
|
|
2454
|
-
booksJsonl,
|
|
2455
|
-
booksJsonlCompressed
|
|
2456
|
-
};
|
|
2457
|
-
for (const p of Object.values(paths[modeStr])) {
|
|
2458
|
-
assert8(
|
|
2459
|
-
fs3.existsSync(p),
|
|
2460
|
-
`File "${p}" does not exist. Run optimization to auto-create it.`
|
|
2461
|
-
);
|
|
2462
|
-
}
|
|
2463
|
-
}
|
|
2464
|
-
return paths;
|
|
2465
|
-
}
|
|
2466
3038
|
getNumberStats(gameModes) {
|
|
3039
|
+
const meta = this.game.getMetadata();
|
|
2467
3040
|
const stats = [];
|
|
2468
3041
|
for (const modeStr of gameModes) {
|
|
2469
3042
|
const mode = this.getGameModeConfig(modeStr);
|
|
2470
3043
|
const lutOptimized = parseLookupTable(
|
|
2471
|
-
|
|
3044
|
+
fs4.readFileSync(meta.paths.lookupTablePublish(modeStr), "utf-8")
|
|
2472
3045
|
);
|
|
2473
3046
|
const totalWeight = getTotalLutWeight(lutOptimized);
|
|
2474
3047
|
const payoutWeights = getPayoutWeights(lutOptimized);
|
|
@@ -2488,10 +3061,7 @@ var Analysis = class {
|
|
|
2488
3061
|
uniquePayouts: getUniquePayouts(payoutWeights)
|
|
2489
3062
|
});
|
|
2490
3063
|
}
|
|
2491
|
-
writeJsonFile(
|
|
2492
|
-
path2.join(this.gameConfig.rootDir, this.gameConfig.outputDir, "stats_summary.json"),
|
|
2493
|
-
stats
|
|
2494
|
-
);
|
|
3064
|
+
writeJsonFile(meta.paths.statsSummary, stats);
|
|
2495
3065
|
}
|
|
2496
3066
|
getWinRanges(gameModes) {
|
|
2497
3067
|
const winRanges = [
|
|
@@ -2514,14 +3084,31 @@ var Analysis = class {
|
|
|
2514
3084
|
[7500, 9999.99],
|
|
2515
3085
|
[1e4, 14999.99],
|
|
2516
3086
|
[15e3, 19999.99],
|
|
2517
|
-
[2e4, 24999.99]
|
|
3087
|
+
[2e4, 24999.99],
|
|
3088
|
+
[25e3, 49999.99],
|
|
3089
|
+
[5e4, 74999.99],
|
|
3090
|
+
[75e3, 99999.99],
|
|
3091
|
+
[1e5, Infinity]
|
|
2518
3092
|
];
|
|
2519
|
-
const payoutRanges =
|
|
3093
|
+
const payoutRanges = [];
|
|
3094
|
+
const meta = this.game.getMetadata();
|
|
2520
3095
|
for (const modeStr of gameModes) {
|
|
2521
|
-
payoutRanges[modeStr] = { overall: {}, criteria: {} };
|
|
2522
3096
|
const lutSegmented = parseLookupTableSegmented(
|
|
2523
|
-
|
|
3097
|
+
fs4.readFileSync(meta.paths.lookupTableSegmented(modeStr), "utf-8")
|
|
2524
3098
|
);
|
|
3099
|
+
const range = {
|
|
3100
|
+
gameMode: modeStr,
|
|
3101
|
+
allPayouts: {
|
|
3102
|
+
overall: {},
|
|
3103
|
+
criteria: {}
|
|
3104
|
+
},
|
|
3105
|
+
uniquePayouts: {
|
|
3106
|
+
overall: {},
|
|
3107
|
+
criteria: {}
|
|
3108
|
+
}
|
|
3109
|
+
};
|
|
3110
|
+
const uniquePayoutsOverall = /* @__PURE__ */ new Map();
|
|
3111
|
+
const uniquePayoutsCriteria = /* @__PURE__ */ new Map();
|
|
2525
3112
|
lutSegmented.forEach(([, criteria, bp, fsp]) => {
|
|
2526
3113
|
const basePayout = bp;
|
|
2527
3114
|
const freeSpinPayout = fsp;
|
|
@@ -2529,32 +3116,54 @@ var Analysis = class {
|
|
|
2529
3116
|
for (const [min, max] of winRanges) {
|
|
2530
3117
|
if (payout >= min && payout <= max) {
|
|
2531
3118
|
const rangeKey = `${min}-${max}`;
|
|
2532
|
-
if (!
|
|
2533
|
-
|
|
3119
|
+
if (!range.allPayouts.overall[rangeKey]) {
|
|
3120
|
+
range.allPayouts.overall[rangeKey] = 0;
|
|
2534
3121
|
}
|
|
2535
|
-
|
|
2536
|
-
if (!
|
|
2537
|
-
|
|
3122
|
+
range.allPayouts.overall[rangeKey] += 1;
|
|
3123
|
+
if (!range.allPayouts.criteria[criteria]) {
|
|
3124
|
+
range.allPayouts.criteria[criteria] = {};
|
|
2538
3125
|
}
|
|
2539
|
-
if (!
|
|
2540
|
-
|
|
3126
|
+
if (!range.allPayouts.criteria[criteria][rangeKey]) {
|
|
3127
|
+
range.allPayouts.criteria[criteria][rangeKey] = 0;
|
|
2541
3128
|
}
|
|
2542
|
-
|
|
3129
|
+
range.allPayouts.criteria[criteria][rangeKey] += 1;
|
|
3130
|
+
if (!uniquePayoutsOverall.has(rangeKey)) {
|
|
3131
|
+
uniquePayoutsOverall.set(rangeKey, /* @__PURE__ */ new Set());
|
|
3132
|
+
}
|
|
3133
|
+
uniquePayoutsOverall.get(rangeKey).add(payout);
|
|
3134
|
+
if (!uniquePayoutsCriteria.has(criteria)) {
|
|
3135
|
+
uniquePayoutsCriteria.set(criteria, /* @__PURE__ */ new Map());
|
|
3136
|
+
}
|
|
3137
|
+
if (!uniquePayoutsCriteria.get(criteria).has(rangeKey)) {
|
|
3138
|
+
uniquePayoutsCriteria.get(criteria).set(rangeKey, /* @__PURE__ */ new Set());
|
|
3139
|
+
}
|
|
3140
|
+
uniquePayoutsCriteria.get(criteria).get(rangeKey).add(payout);
|
|
2543
3141
|
break;
|
|
2544
3142
|
}
|
|
2545
3143
|
}
|
|
2546
3144
|
});
|
|
2547
|
-
|
|
2548
|
-
|
|
3145
|
+
uniquePayoutsOverall.forEach((payoutSet, rangeKey) => {
|
|
3146
|
+
range.uniquePayouts.overall[rangeKey] = payoutSet.size;
|
|
3147
|
+
});
|
|
3148
|
+
uniquePayoutsCriteria.forEach((rangeMap, criteria) => {
|
|
3149
|
+
if (!range.uniquePayouts.criteria[criteria]) {
|
|
3150
|
+
range.uniquePayouts.criteria[criteria] = {};
|
|
3151
|
+
}
|
|
3152
|
+
rangeMap.forEach((payoutSet, rangeKey) => {
|
|
3153
|
+
range.uniquePayouts.criteria[criteria][rangeKey] = payoutSet.size;
|
|
3154
|
+
});
|
|
3155
|
+
});
|
|
3156
|
+
const orderedAllOverall = {};
|
|
3157
|
+
Object.keys(range.allPayouts.overall).sort((a, b) => {
|
|
2549
3158
|
const [aMin] = a.split("-").map(Number);
|
|
2550
3159
|
const [bMin] = b.split("-").map(Number);
|
|
2551
3160
|
return aMin - bMin;
|
|
2552
3161
|
}).forEach((key) => {
|
|
2553
|
-
|
|
3162
|
+
orderedAllOverall[key] = range.allPayouts.overall[key];
|
|
2554
3163
|
});
|
|
2555
|
-
const
|
|
2556
|
-
Object.keys(
|
|
2557
|
-
const critMap =
|
|
3164
|
+
const orderedAllCriteria = {};
|
|
3165
|
+
Object.keys(range.allPayouts.criteria).forEach((crit) => {
|
|
3166
|
+
const critMap = range.allPayouts.criteria[crit];
|
|
2558
3167
|
const orderedCritMap = {};
|
|
2559
3168
|
Object.keys(critMap).sort((a, b) => {
|
|
2560
3169
|
const [aMin] = a.split("-").map(Number);
|
|
@@ -2563,35 +3172,61 @@ var Analysis = class {
|
|
|
2563
3172
|
}).forEach((key) => {
|
|
2564
3173
|
orderedCritMap[key] = critMap[key];
|
|
2565
3174
|
});
|
|
2566
|
-
|
|
3175
|
+
orderedAllCriteria[crit] = orderedCritMap;
|
|
3176
|
+
});
|
|
3177
|
+
const orderedUniqueOverall = {};
|
|
3178
|
+
Object.keys(range.uniquePayouts.overall).sort((a, b) => {
|
|
3179
|
+
const [aMin] = a.split("-").map(Number);
|
|
3180
|
+
const [bMin] = b.split("-").map(Number);
|
|
3181
|
+
return aMin - bMin;
|
|
3182
|
+
}).forEach((key) => {
|
|
3183
|
+
orderedUniqueOverall[key] = range.uniquePayouts.overall[key];
|
|
3184
|
+
});
|
|
3185
|
+
const orderedUniqueCriteria = {};
|
|
3186
|
+
Object.keys(range.uniquePayouts.criteria).forEach((crit) => {
|
|
3187
|
+
const critMap = range.uniquePayouts.criteria[crit];
|
|
3188
|
+
const orderedCritMap = {};
|
|
3189
|
+
Object.keys(critMap).sort((a, b) => {
|
|
3190
|
+
const [aMin] = a.split("-").map(Number);
|
|
3191
|
+
const [bMin] = b.split("-").map(Number);
|
|
3192
|
+
return aMin - bMin;
|
|
3193
|
+
}).forEach((key) => {
|
|
3194
|
+
orderedCritMap[key] = critMap[key];
|
|
3195
|
+
});
|
|
3196
|
+
orderedUniqueCriteria[crit] = orderedCritMap;
|
|
3197
|
+
});
|
|
3198
|
+
payoutRanges.push({
|
|
3199
|
+
gameMode: modeStr,
|
|
3200
|
+
allPayouts: {
|
|
3201
|
+
overall: orderedAllOverall,
|
|
3202
|
+
criteria: orderedAllCriteria
|
|
3203
|
+
},
|
|
3204
|
+
uniquePayouts: {
|
|
3205
|
+
overall: orderedUniqueOverall,
|
|
3206
|
+
criteria: orderedUniqueCriteria
|
|
3207
|
+
}
|
|
2567
3208
|
});
|
|
2568
|
-
payoutRanges[modeStr] = {
|
|
2569
|
-
overall: orderedOverall,
|
|
2570
|
-
criteria: {}
|
|
2571
|
-
};
|
|
2572
3209
|
}
|
|
2573
|
-
writeJsonFile(
|
|
2574
|
-
path2.join(this.gameConfig.rootDir, this.gameConfig.outputDir, "stats_payouts.json"),
|
|
2575
|
-
payoutRanges
|
|
2576
|
-
);
|
|
3210
|
+
writeJsonFile(meta.paths.statsPayouts, payoutRanges);
|
|
2577
3211
|
}
|
|
2578
3212
|
getGameModeConfig(mode) {
|
|
2579
|
-
const config = this.
|
|
2580
|
-
|
|
3213
|
+
const config = this.game.getConfig().gameModes[mode];
|
|
3214
|
+
assert6(config, `Game mode "${mode}" not found in game config`);
|
|
2581
3215
|
return config;
|
|
2582
3216
|
}
|
|
2583
3217
|
};
|
|
2584
3218
|
|
|
2585
3219
|
// src/optimizer/index.ts
|
|
2586
|
-
import
|
|
2587
|
-
import
|
|
3220
|
+
import path6 from "path";
|
|
3221
|
+
import assert8 from "assert";
|
|
2588
3222
|
import { spawn } from "child_process";
|
|
2589
|
-
import { isMainThread as
|
|
3223
|
+
import { isMainThread as isMainThread4 } from "worker_threads";
|
|
2590
3224
|
|
|
2591
3225
|
// src/utils/math-config.ts
|
|
2592
|
-
import
|
|
3226
|
+
import path4 from "path";
|
|
2593
3227
|
function makeMathConfig(optimizer, opts = {}) {
|
|
2594
3228
|
const game = optimizer.getGameConfig();
|
|
3229
|
+
const meta = optimizer.getGameMeta();
|
|
2595
3230
|
const gameModesCfg = optimizer.getOptimizerGameModes();
|
|
2596
3231
|
const { writeToFile } = opts;
|
|
2597
3232
|
const isDefined = (v) => v !== void 0;
|
|
@@ -2633,16 +3268,17 @@ function makeMathConfig(optimizer, opts = {}) {
|
|
|
2633
3268
|
}))
|
|
2634
3269
|
};
|
|
2635
3270
|
if (writeToFile) {
|
|
2636
|
-
const outPath =
|
|
3271
|
+
const outPath = path4.join(meta.rootDir, meta.outputDir, "math_config.json");
|
|
2637
3272
|
writeJsonFile(outPath, config);
|
|
2638
3273
|
}
|
|
2639
3274
|
return config;
|
|
2640
3275
|
}
|
|
2641
3276
|
|
|
2642
3277
|
// src/utils/setup-file.ts
|
|
2643
|
-
import
|
|
3278
|
+
import path5 from "path";
|
|
2644
3279
|
function makeSetupFile(optimizer, gameMode) {
|
|
2645
3280
|
const gameConfig = optimizer.getGameConfig();
|
|
3281
|
+
const gameMeta = optimizer.getGameMeta();
|
|
2646
3282
|
const optimizerGameModes = optimizer.getOptimizerGameModes();
|
|
2647
3283
|
const modeConfig = optimizerGameModes[gameMode];
|
|
2648
3284
|
if (!modeConfig) {
|
|
@@ -2676,16 +3312,16 @@ function makeSetupFile(optimizer, gameMode) {
|
|
|
2676
3312
|
`;
|
|
2677
3313
|
content += `simulation_trials;${params.simulationTrials}
|
|
2678
3314
|
`;
|
|
2679
|
-
content += `user_game_build_path;${
|
|
3315
|
+
content += `user_game_build_path;${path5.join(gameMeta.rootDir, gameMeta.outputDir)}
|
|
2680
3316
|
`;
|
|
2681
3317
|
content += `pmb_rtp;${params.pmbRtp}
|
|
2682
3318
|
`;
|
|
2683
|
-
const outPath =
|
|
3319
|
+
const outPath = path5.join(__dirname, "./optimizer-rust/src", "setup.txt");
|
|
2684
3320
|
writeFile(outPath, content);
|
|
2685
3321
|
}
|
|
2686
3322
|
|
|
2687
3323
|
// src/optimizer/OptimizationConditions.ts
|
|
2688
|
-
import
|
|
3324
|
+
import assert7 from "assert";
|
|
2689
3325
|
var OptimizationConditions = class {
|
|
2690
3326
|
rtp;
|
|
2691
3327
|
avgWin;
|
|
@@ -2696,14 +3332,14 @@ var OptimizationConditions = class {
|
|
|
2696
3332
|
constructor(opts) {
|
|
2697
3333
|
let { rtp, avgWin, hitRate, searchConditions, priority } = opts;
|
|
2698
3334
|
if (rtp == void 0 || rtp === "x") {
|
|
2699
|
-
|
|
3335
|
+
assert7(avgWin !== void 0 && hitRate !== void 0, "If RTP is not specified, hit-rate (hr) and average win amount (av_win) must be given.");
|
|
2700
3336
|
rtp = Math.round(avgWin / Number(hitRate) * 1e5) / 1e5;
|
|
2701
3337
|
}
|
|
2702
3338
|
let noneCount = 0;
|
|
2703
3339
|
for (const val of [rtp, avgWin, hitRate]) {
|
|
2704
3340
|
if (val === void 0) noneCount++;
|
|
2705
3341
|
}
|
|
2706
|
-
|
|
3342
|
+
assert7(noneCount <= 1, "Invalid combination of optimization conditions.");
|
|
2707
3343
|
this.searchRange = [-1, -1];
|
|
2708
3344
|
this.forceSearch = {};
|
|
2709
3345
|
if (typeof searchConditions === "number") {
|
|
@@ -2784,9 +3420,11 @@ var OptimizationParameters = class _OptimizationParameters {
|
|
|
2784
3420
|
// src/optimizer/index.ts
|
|
2785
3421
|
var Optimizer = class {
|
|
2786
3422
|
gameConfig;
|
|
3423
|
+
gameMeta;
|
|
2787
3424
|
gameModes;
|
|
2788
3425
|
constructor(opts) {
|
|
2789
3426
|
this.gameConfig = opts.game.getConfig();
|
|
3427
|
+
this.gameMeta = opts.game.getMetadata();
|
|
2790
3428
|
this.gameModes = opts.gameModes;
|
|
2791
3429
|
this.verifyConfig();
|
|
2792
3430
|
}
|
|
@@ -2794,11 +3432,15 @@ var Optimizer = class {
|
|
|
2794
3432
|
* Runs the optimization process, and runs analysis after.
|
|
2795
3433
|
*/
|
|
2796
3434
|
async runOptimization({ gameModes }) {
|
|
2797
|
-
if (!
|
|
3435
|
+
if (!isMainThread4) return;
|
|
2798
3436
|
const mathConfig = makeMathConfig(this, { writeToFile: true });
|
|
2799
3437
|
for (const mode of gameModes) {
|
|
2800
3438
|
const setupFile = makeSetupFile(this, mode);
|
|
2801
3439
|
await this.runSingleOptimization();
|
|
3440
|
+
await makeLutIndexFromPublishLut(
|
|
3441
|
+
this.gameMeta.paths.lookupTablePublish(mode),
|
|
3442
|
+
this.gameMeta.paths.lookupTableIndex(mode)
|
|
3443
|
+
);
|
|
2802
3444
|
}
|
|
2803
3445
|
console.log("Optimization complete. Files written to build directory.");
|
|
2804
3446
|
}
|
|
@@ -2831,7 +3473,7 @@ var Optimizer = class {
|
|
|
2831
3473
|
}
|
|
2832
3474
|
gameModeRtp = Math.round(gameModeRtp * 1e3) / 1e3;
|
|
2833
3475
|
paramRtp = Math.round(paramRtp * 1e3) / 1e3;
|
|
2834
|
-
|
|
3476
|
+
assert8(
|
|
2835
3477
|
gameModeRtp === paramRtp,
|
|
2836
3478
|
`Sum of all RTP conditions (${paramRtp}) does not match the game mode RTP (${gameModeRtp}) in game mode "${k}".`
|
|
2837
3479
|
);
|
|
@@ -2840,6 +3482,9 @@ var Optimizer = class {
|
|
|
2840
3482
|
getGameConfig() {
|
|
2841
3483
|
return this.gameConfig;
|
|
2842
3484
|
}
|
|
3485
|
+
getGameMeta() {
|
|
3486
|
+
return this.gameMeta;
|
|
3487
|
+
}
|
|
2843
3488
|
getOptimizerGameModes() {
|
|
2844
3489
|
return this.gameModes;
|
|
2845
3490
|
}
|
|
@@ -2848,7 +3493,7 @@ async function rustProgram(...args) {
|
|
|
2848
3493
|
console.log("Starting Rust optimizer. This may take a while...");
|
|
2849
3494
|
return new Promise((resolve, reject) => {
|
|
2850
3495
|
const task = spawn("cargo", ["run", "-q", "--release", ...args], {
|
|
2851
|
-
cwd:
|
|
3496
|
+
cwd: path6.join(__dirname, "./optimizer-rust"),
|
|
2852
3497
|
stdio: "pipe"
|
|
2853
3498
|
});
|
|
2854
3499
|
task.on("error", (error) => {
|
|
@@ -2875,8 +3520,8 @@ async function rustProgram(...args) {
|
|
|
2875
3520
|
}
|
|
2876
3521
|
|
|
2877
3522
|
// src/slot-game/index.ts
|
|
2878
|
-
import { isMainThread as
|
|
2879
|
-
var SlotGame = class {
|
|
3523
|
+
import { isMainThread as isMainThread5, workerData as workerData2 } from "worker_threads";
|
|
3524
|
+
var SlotGame = class _SlotGame {
|
|
2880
3525
|
configOpts;
|
|
2881
3526
|
simulation;
|
|
2882
3527
|
optimizer;
|
|
@@ -2926,38 +3571,56 @@ var SlotGame = class {
|
|
|
2926
3571
|
/**
|
|
2927
3572
|
* Runs the analysis based on the configured settings.
|
|
2928
3573
|
*/
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
"Optimization must be configured to run analysis. Do so by calling configureOptimization() first."
|
|
2933
|
-
);
|
|
2934
|
-
}
|
|
2935
|
-
this.analyzer = new Analysis(this.optimizer);
|
|
2936
|
-
await this.analyzer.runAnalysis(opts.gameModes);
|
|
3574
|
+
runAnalysis(opts) {
|
|
3575
|
+
this.analyzer = new Analysis(this);
|
|
3576
|
+
this.analyzer.runAnalysis(opts.gameModes);
|
|
2937
3577
|
}
|
|
2938
3578
|
/**
|
|
2939
3579
|
* Runs the configured tasks: simulation, optimization, and/or analysis.
|
|
2940
3580
|
*/
|
|
2941
3581
|
async runTasks(opts = {}) {
|
|
3582
|
+
if (isMainThread5 && !opts._internal_ignore_args) {
|
|
3583
|
+
const [{ default: yargs }, { hideBin }] = await Promise.all([
|
|
3584
|
+
import("yargs"),
|
|
3585
|
+
import("yargs/helpers")
|
|
3586
|
+
]);
|
|
3587
|
+
const argvParser = yargs(hideBin(process.argv)).options({
|
|
3588
|
+
[CLI_ARGS.RUN]: { type: "boolean", default: false }
|
|
3589
|
+
});
|
|
3590
|
+
const argv = await argvParser.parse();
|
|
3591
|
+
if (!argv[CLI_ARGS.RUN]) return;
|
|
3592
|
+
}
|
|
3593
|
+
if (!isMainThread5 && workerData2 && typeof workerData2 === "object" && "simStart" in workerData2) {
|
|
3594
|
+
opts.doSimulation = true;
|
|
3595
|
+
}
|
|
2942
3596
|
if (!opts.doSimulation && !opts.doOptimization && !opts.doAnalysis) {
|
|
2943
3597
|
console.log("No tasks to run. Enable either simulation, optimization or analysis.");
|
|
2944
3598
|
}
|
|
2945
3599
|
if (opts.doSimulation) {
|
|
2946
3600
|
await this.runSimulation(opts.simulationOpts || {});
|
|
2947
3601
|
}
|
|
3602
|
+
if (opts.doAnalysis) {
|
|
3603
|
+
this.runAnalysis(opts.analysisOpts || { gameModes: [] });
|
|
3604
|
+
}
|
|
2948
3605
|
if (opts.doOptimization) {
|
|
2949
3606
|
await this.runOptimization(opts.optimizationOpts || { gameModes: [] });
|
|
3607
|
+
if (opts.doAnalysis) {
|
|
3608
|
+
this.runAnalysis(opts.analysisOpts || { gameModes: [] });
|
|
3609
|
+
}
|
|
2950
3610
|
}
|
|
2951
|
-
if (
|
|
2952
|
-
await this.runAnalysis(opts.analysisOpts || { gameModes: [] });
|
|
2953
|
-
}
|
|
2954
|
-
if (isMainThread4) console.log("Finishing up...");
|
|
3611
|
+
if (isMainThread5) console.log("Done!");
|
|
2955
3612
|
}
|
|
2956
3613
|
/**
|
|
2957
3614
|
* Gets the game configuration.
|
|
2958
3615
|
*/
|
|
2959
3616
|
getConfig() {
|
|
2960
|
-
return createGameConfig(this.configOpts);
|
|
3617
|
+
return createGameConfig(this.configOpts).config;
|
|
3618
|
+
}
|
|
3619
|
+
getMetadata() {
|
|
3620
|
+
return createGameConfig(this.configOpts).metadata;
|
|
3621
|
+
}
|
|
3622
|
+
clone() {
|
|
3623
|
+
return new _SlotGame(this.configOpts);
|
|
2961
3624
|
}
|
|
2962
3625
|
};
|
|
2963
3626
|
|
|
@@ -2970,7 +3633,7 @@ var defineSymbols = (symbols) => symbols;
|
|
|
2970
3633
|
var defineGameModes = (gameModes) => gameModes;
|
|
2971
3634
|
|
|
2972
3635
|
// src/game-mode/index.ts
|
|
2973
|
-
import
|
|
3636
|
+
import assert9 from "assert";
|
|
2974
3637
|
var GameMode = class {
|
|
2975
3638
|
name;
|
|
2976
3639
|
_reelsAmount;
|
|
@@ -2993,12 +3656,12 @@ var GameMode = class {
|
|
|
2993
3656
|
this.reelSets = opts.reelSets;
|
|
2994
3657
|
this.resultSets = opts.resultSets;
|
|
2995
3658
|
this.isBonusBuy = opts.isBonusBuy;
|
|
2996
|
-
|
|
2997
|
-
|
|
3659
|
+
assert9(this.rtp >= 0.9 && this.rtp <= 0.99, "RTP must be between 0.9 and 0.99");
|
|
3660
|
+
assert9(
|
|
2998
3661
|
this.symbolsPerReel.length === this.reelsAmount,
|
|
2999
3662
|
"symbolsPerReel length must match reelsAmount."
|
|
3000
3663
|
);
|
|
3001
|
-
|
|
3664
|
+
assert9(this.reelSets.length > 0, "GameMode must have at least one ReelSet defined.");
|
|
3002
3665
|
}
|
|
3003
3666
|
/**
|
|
3004
3667
|
* Intended for internal use only.
|
|
@@ -3011,7 +3674,7 @@ var GameMode = class {
|
|
|
3011
3674
|
* Intended for internal use only.
|
|
3012
3675
|
*/
|
|
3013
3676
|
_setSymbolsPerReel(symbolsPerReel) {
|
|
3014
|
-
|
|
3677
|
+
assert9(
|
|
3015
3678
|
symbolsPerReel.length === this._reelsAmount,
|
|
3016
3679
|
"symbolsPerReel length must match reelsAmount."
|
|
3017
3680
|
);
|
|
@@ -3077,7 +3740,7 @@ var WinType = class {
|
|
|
3077
3740
|
};
|
|
3078
3741
|
|
|
3079
3742
|
// src/win-types/LinesWinType.ts
|
|
3080
|
-
import
|
|
3743
|
+
import assert10 from "assert";
|
|
3081
3744
|
var LinesWinType = class extends WinType {
|
|
3082
3745
|
lines;
|
|
3083
3746
|
constructor(opts) {
|
|
@@ -3131,8 +3794,8 @@ var LinesWinType = class extends WinType {
|
|
|
3131
3794
|
if (!baseSymbol) {
|
|
3132
3795
|
baseSymbol = thisSymbol;
|
|
3133
3796
|
}
|
|
3134
|
-
|
|
3135
|
-
|
|
3797
|
+
assert10(baseSymbol, `No symbol found at line ${lineNum}, reel ${ridx}`);
|
|
3798
|
+
assert10(thisSymbol, `No symbol found at line ${lineNum}, reel ${ridx}`);
|
|
3136
3799
|
if (potentialWinLine.length == 0) {
|
|
3137
3800
|
if (this.isWild(thisSymbol)) {
|
|
3138
3801
|
potentialWildLine.push({ reel: ridx, row: sidx, symbol: thisSymbol });
|
|
@@ -3447,13 +4110,13 @@ var ManywaysWinType = class extends WinType {
|
|
|
3447
4110
|
};
|
|
3448
4111
|
|
|
3449
4112
|
// src/reel-set/GeneratedReelSet.ts
|
|
3450
|
-
import
|
|
3451
|
-
import
|
|
3452
|
-
import { isMainThread as
|
|
4113
|
+
import fs6 from "fs";
|
|
4114
|
+
import path8 from "path";
|
|
4115
|
+
import { isMainThread as isMainThread6 } from "worker_threads";
|
|
3453
4116
|
|
|
3454
4117
|
// src/reel-set/index.ts
|
|
3455
|
-
import
|
|
3456
|
-
import
|
|
4118
|
+
import fs5 from "fs";
|
|
4119
|
+
import path7 from "path";
|
|
3457
4120
|
var ReelSet = class {
|
|
3458
4121
|
id;
|
|
3459
4122
|
associatedGameModeName;
|
|
@@ -3473,11 +4136,11 @@ var ReelSet = class {
|
|
|
3473
4136
|
* Reads a reelset CSV file and returns the reels as arrays of GameSymbols.
|
|
3474
4137
|
*/
|
|
3475
4138
|
parseReelsetCSV(reelSetPath, config) {
|
|
3476
|
-
if (!
|
|
4139
|
+
if (!fs5.existsSync(reelSetPath)) {
|
|
3477
4140
|
throw new Error(`Reelset CSV file not found at path: ${reelSetPath}`);
|
|
3478
4141
|
}
|
|
3479
4142
|
const allowedExtensions = [".csv"];
|
|
3480
|
-
const ext =
|
|
4143
|
+
const ext = path7.extname(reelSetPath).toLowerCase();
|
|
3481
4144
|
if (!allowedExtensions.includes(ext)) {
|
|
3482
4145
|
throw new Error(
|
|
3483
4146
|
`Invalid file extension for reelset CSV: ${ext}. Allowed extensions are: ${allowedExtensions.join(
|
|
@@ -3485,7 +4148,7 @@ var ReelSet = class {
|
|
|
3485
4148
|
)}`
|
|
3486
4149
|
);
|
|
3487
4150
|
}
|
|
3488
|
-
const csvData =
|
|
4151
|
+
const csvData = fs5.readFileSync(reelSetPath, "utf8");
|
|
3489
4152
|
const rows = csvData.split("\n").filter((line) => line.trim() !== "");
|
|
3490
4153
|
const reels = Array.from(
|
|
3491
4154
|
{ length: config.gameModes[this.associatedGameModeName].reelsAmount },
|
|
@@ -3493,6 +4156,7 @@ var ReelSet = class {
|
|
|
3493
4156
|
);
|
|
3494
4157
|
rows.forEach((row) => {
|
|
3495
4158
|
const symsInRow = row.split(",").map((symbolId) => {
|
|
4159
|
+
if (!symbolId.trim()) return null;
|
|
3496
4160
|
const symbol = config.symbols.get(symbolId.trim());
|
|
3497
4161
|
if (!symbol) {
|
|
3498
4162
|
throw new Error(`Symbol with id "${symbolId}" not found in game config.`);
|
|
@@ -3505,18 +4169,9 @@ var ReelSet = class {
|
|
|
3505
4169
|
`Row in reelset CSV has more symbols than expected reels amount (${reels.length})`
|
|
3506
4170
|
);
|
|
3507
4171
|
}
|
|
3508
|
-
reels[ridx].push(symbol);
|
|
4172
|
+
if (symbol) reels[ridx].push(symbol);
|
|
3509
4173
|
});
|
|
3510
4174
|
});
|
|
3511
|
-
const reelLengths = reels.map((r) => r.length);
|
|
3512
|
-
const uniqueLengths = new Set(reelLengths);
|
|
3513
|
-
if (uniqueLengths.size > 1) {
|
|
3514
|
-
throw new Error(
|
|
3515
|
-
`Inconsistent reel lengths in reelset CSV at ${reelSetPath}: ${[
|
|
3516
|
-
...uniqueLengths
|
|
3517
|
-
].join(", ")}`
|
|
3518
|
-
);
|
|
3519
|
-
}
|
|
3520
4175
|
return reels;
|
|
3521
4176
|
}
|
|
3522
4177
|
};
|
|
@@ -3646,12 +4301,12 @@ var GeneratedReelSet = class extends ReelSet {
|
|
|
3646
4301
|
`Error generating reels for game mode "${this.associatedGameModeName}". It's not defined in the game config.`
|
|
3647
4302
|
);
|
|
3648
4303
|
}
|
|
3649
|
-
const outputDir = config.rootDir.endsWith(config.outputDir) ? config.rootDir :
|
|
3650
|
-
const filePath =
|
|
4304
|
+
const outputDir = config.rootDir.endsWith(config.outputDir) ? config.rootDir : path8.join(config.rootDir, config.outputDir);
|
|
4305
|
+
const filePath = path8.join(
|
|
3651
4306
|
outputDir,
|
|
3652
4307
|
`reels_${this.associatedGameModeName}-${this.id}.csv`
|
|
3653
4308
|
);
|
|
3654
|
-
const exists =
|
|
4309
|
+
const exists = fs6.existsSync(filePath);
|
|
3655
4310
|
if (exists && !this.overrideExisting) {
|
|
3656
4311
|
this.reels = this.parseReelsetCSV(filePath, config);
|
|
3657
4312
|
return this;
|
|
@@ -3788,9 +4443,9 @@ var GeneratedReelSet = class extends ReelSet {
|
|
|
3788
4443
|
}
|
|
3789
4444
|
}
|
|
3790
4445
|
const csvString = csvRows.map((row) => row.join(",")).join("\n");
|
|
3791
|
-
if (
|
|
4446
|
+
if (isMainThread6) {
|
|
3792
4447
|
createDirIfNotExists(outputDir);
|
|
3793
|
-
|
|
4448
|
+
fs6.writeFileSync(filePath, csvString);
|
|
3794
4449
|
console.log(
|
|
3795
4450
|
`Generated reelset ${this.id} for game mode ${this.associatedGameModeName}`
|
|
3796
4451
|
);
|
|
@@ -3801,7 +4456,7 @@ var GeneratedReelSet = class extends ReelSet {
|
|
|
3801
4456
|
};
|
|
3802
4457
|
|
|
3803
4458
|
// src/reel-set/StaticReelSet.ts
|
|
3804
|
-
import
|
|
4459
|
+
import assert11 from "assert";
|
|
3805
4460
|
var StaticReelSet = class extends ReelSet {
|
|
3806
4461
|
reels;
|
|
3807
4462
|
csvPath;
|
|
@@ -3811,7 +4466,7 @@ var StaticReelSet = class extends ReelSet {
|
|
|
3811
4466
|
this.reels = [];
|
|
3812
4467
|
this._strReels = opts.reels || [];
|
|
3813
4468
|
this.csvPath = opts.csvPath || "";
|
|
3814
|
-
|
|
4469
|
+
assert11(
|
|
3815
4470
|
opts.reels || opts.csvPath,
|
|
3816
4471
|
`Either 'reels' or 'csvPath' must be provided for StaticReelSet ${this.id}`
|
|
3817
4472
|
);
|
|
@@ -4064,6 +4719,7 @@ export {
|
|
|
4064
4719
|
OptimizationConditions,
|
|
4065
4720
|
OptimizationParameters,
|
|
4066
4721
|
OptimizationScaling,
|
|
4722
|
+
RandomNumberGenerator,
|
|
4067
4723
|
ResultSet,
|
|
4068
4724
|
SPIN_TYPE,
|
|
4069
4725
|
StandaloneBoard,
|
|
@@ -4071,6 +4727,8 @@ export {
|
|
|
4071
4727
|
createSlotGame,
|
|
4072
4728
|
defineGameModes,
|
|
4073
4729
|
defineSymbols,
|
|
4074
|
-
defineUserState
|
|
4730
|
+
defineUserState,
|
|
4731
|
+
parseLookupTable,
|
|
4732
|
+
parseLookupTableSegmented
|
|
4075
4733
|
};
|
|
4076
4734
|
//# sourceMappingURL=index.mjs.map
|