@slot-engine/core 0.1.14 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +212 -92
- package/dist/index.d.ts +212 -92
- package/dist/index.js +1091 -482
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1092 -479
- package/dist/index.mjs.map +1 -1
- package/package.json +11 -2
package/dist/index.mjs
CHANGED
|
@@ -1,11 +1,62 @@
|
|
|
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
|
+
lookupTable: (mode) => path.join(basePath, `lookUpTable_${mode}.csv`),
|
|
35
|
+
lookupTableIndex: (mode) => path.join(basePath, `lookUpTable_${mode}.index`),
|
|
36
|
+
lookupTableSegmented: (mode) => path.join(basePath, `lookUpTableSegmented_${mode}.csv`),
|
|
37
|
+
lookupTableSegmentedIndex: (mode) => path.join(basePath, `lookUpTableSegmented_${mode}.index`),
|
|
38
|
+
lookupTablePublish: (mode) => path.join(basePath, "publish_files", `lookUpTable_${mode}_0.csv`),
|
|
39
|
+
forceRecords: (mode) => path.join(basePath, `force_record_${mode}.json`),
|
|
40
|
+
forceKeys: (mode) => path.join(basePath, `force_keys_${mode}.json`),
|
|
41
|
+
indexJson: path.join(basePath, "publish_files", "index.json"),
|
|
42
|
+
optimizationFiles: path.join(basePath, "optimization_files"),
|
|
43
|
+
publishFiles: path.join(basePath, "publish_files"),
|
|
44
|
+
simulationSummary: path.join(basePath, "simulation_summary.json"),
|
|
45
|
+
statsPayouts: path.join(basePath, "stats_payouts.json"),
|
|
46
|
+
statsSummary: path.join(basePath, "stats_summary.json")
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function createTemporaryFilePaths(basePath, tempFolder) {
|
|
50
|
+
return {
|
|
51
|
+
tempBooks: (mode, i) => path.join(basePath, tempFolder, `temp_books_${mode}_${i}.jsonl`),
|
|
52
|
+
tempLookupTable: (mode, i) => path.join(basePath, tempFolder, `temp_lookup_${mode}_${i}.csv`),
|
|
53
|
+
tempLookupTableSegmented: (mode, i) => path.join(basePath, tempFolder, `temp_lookup_segmented_${mode}_${i}.csv`),
|
|
54
|
+
tempRecords: (mode) => path.join(basePath, tempFolder, `temp_records_${mode}.jsonl`)
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// src/game-config/index.ts
|
|
59
|
+
import path2 from "path";
|
|
9
60
|
function createGameConfig(opts) {
|
|
10
61
|
const symbols = /* @__PURE__ */ new Map();
|
|
11
62
|
for (const [key, value] of Object.entries(opts.symbols)) {
|
|
@@ -15,70 +66,42 @@ function createGameConfig(opts) {
|
|
|
15
66
|
const getAnticipationTrigger = (spinType) => {
|
|
16
67
|
return Math.min(...Object.keys(opts.scatterToFreespins[spinType] || {}).map(Number)) - 1;
|
|
17
68
|
};
|
|
69
|
+
const rootDir = opts.rootDir || process.cwd();
|
|
70
|
+
const outputDir = "__build__";
|
|
71
|
+
const basePath = path2.join(rootDir, outputDir);
|
|
18
72
|
return {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
73
|
+
config: {
|
|
74
|
+
padSymbols: opts.padSymbols || 1,
|
|
75
|
+
userState: opts.userState || {},
|
|
76
|
+
...opts,
|
|
77
|
+
symbols,
|
|
78
|
+
anticipationTriggers: {
|
|
79
|
+
[SPIN_TYPE.BASE_GAME]: getAnticipationTrigger(SPIN_TYPE.BASE_GAME),
|
|
80
|
+
[SPIN_TYPE.FREE_SPINS]: getAnticipationTrigger(SPIN_TYPE.FREE_SPINS)
|
|
81
|
+
}
|
|
26
82
|
},
|
|
27
|
-
|
|
28
|
-
|
|
83
|
+
metadata: {
|
|
84
|
+
outputDir,
|
|
85
|
+
rootDir,
|
|
86
|
+
isCustomRoot: !!opts.rootDir,
|
|
87
|
+
paths: createPermanentFilePaths(basePath)
|
|
88
|
+
}
|
|
29
89
|
};
|
|
30
90
|
}
|
|
31
91
|
|
|
32
92
|
// src/simulation/index.ts
|
|
33
|
-
import
|
|
34
|
-
import
|
|
35
|
-
import
|
|
93
|
+
import fs3 from "fs";
|
|
94
|
+
import path3 from "path";
|
|
95
|
+
import assert5 from "assert";
|
|
36
96
|
import zlib from "zlib";
|
|
37
|
-
import
|
|
97
|
+
import readline from "readline";
|
|
38
98
|
import { buildSync } from "esbuild";
|
|
39
|
-
import { Worker, isMainThread, parentPort, workerData } from "worker_threads";
|
|
99
|
+
import { Worker, isMainThread as isMainThread2, parentPort as parentPort2, workerData } from "worker_threads";
|
|
40
100
|
|
|
41
101
|
// src/result-set/index.ts
|
|
42
102
|
import assert2 from "assert";
|
|
43
103
|
|
|
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
|
-
};
|
|
104
|
+
// src/rng/index.ts
|
|
82
105
|
var RandomNumberGenerator = class {
|
|
83
106
|
mIdum;
|
|
84
107
|
mIy;
|
|
@@ -270,10 +293,10 @@ var ResultSet = class {
|
|
|
270
293
|
const freespinsMet = this.forceFreespins ? ctx.state.triggeredFreespins : true;
|
|
271
294
|
const wallet = ctx.services.wallet._getWallet();
|
|
272
295
|
const multiplierMet = this.forceMaxWin ? true : this.multiplier !== void 0 ? wallet.getCurrentWin() === this.multiplier : wallet.getCurrentWin() > 0;
|
|
273
|
-
const
|
|
274
|
-
const coreCriteriaMet = freespinsMet && multiplierMet &&
|
|
296
|
+
const respectsMaxWin = this.forceMaxWin ? wallet.getCurrentWin() >= ctx.config.maxWinX : wallet.getCurrentWin() < ctx.config.maxWinX;
|
|
297
|
+
const coreCriteriaMet = freespinsMet && multiplierMet && respectsMaxWin;
|
|
275
298
|
const finalResult = customEval !== void 0 ? coreCriteriaMet && customEval === true : coreCriteriaMet;
|
|
276
|
-
if (this.forceMaxWin &&
|
|
299
|
+
if (this.forceMaxWin && respectsMaxWin) {
|
|
277
300
|
ctx.services.data.record({
|
|
278
301
|
maxwin: true
|
|
279
302
|
});
|
|
@@ -308,13 +331,34 @@ function createGameState(opts) {
|
|
|
308
331
|
// src/recorder/index.ts
|
|
309
332
|
var Recorder = class {
|
|
310
333
|
records;
|
|
334
|
+
recordsMap;
|
|
311
335
|
pendingRecords;
|
|
312
336
|
constructor() {
|
|
313
337
|
this.records = [];
|
|
338
|
+
this.recordsMap = /* @__PURE__ */ new Map();
|
|
339
|
+
this.pendingRecords = [];
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Intended for internal use only.
|
|
343
|
+
*/
|
|
344
|
+
_reset() {
|
|
345
|
+
this.records = [];
|
|
346
|
+
this.recordsMap.clear();
|
|
314
347
|
this.pendingRecords = [];
|
|
315
348
|
}
|
|
316
349
|
};
|
|
317
350
|
|
|
351
|
+
// src/service/index.ts
|
|
352
|
+
var AbstractService = class {
|
|
353
|
+
/**
|
|
354
|
+
* Function that returns the current game context.
|
|
355
|
+
*/
|
|
356
|
+
ctx;
|
|
357
|
+
constructor(ctx) {
|
|
358
|
+
this.ctx = ctx;
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
|
|
318
362
|
// src/board/index.ts
|
|
319
363
|
import assert3 from "assert";
|
|
320
364
|
|
|
@@ -903,19 +947,13 @@ var BoardService = class extends AbstractService {
|
|
|
903
947
|
};
|
|
904
948
|
|
|
905
949
|
// src/service/data.ts
|
|
906
|
-
import
|
|
950
|
+
import { isMainThread, parentPort } from "worker_threads";
|
|
907
951
|
var DataService = class extends AbstractService {
|
|
908
952
|
recorder;
|
|
909
953
|
book;
|
|
910
954
|
constructor(ctx) {
|
|
911
955
|
super(ctx);
|
|
912
956
|
}
|
|
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
957
|
/**
|
|
920
958
|
* Intended for internal use only.
|
|
921
959
|
*/
|
|
@@ -944,14 +982,12 @@ var DataService = class extends AbstractService {
|
|
|
944
982
|
* Intended for internal use only.
|
|
945
983
|
*/
|
|
946
984
|
_getRecords() {
|
|
947
|
-
this.ensureRecorder();
|
|
948
985
|
return this.recorder.records;
|
|
949
986
|
}
|
|
950
987
|
/**
|
|
951
988
|
* Record data for statistical analysis.
|
|
952
989
|
*/
|
|
953
990
|
record(data) {
|
|
954
|
-
this.ensureRecorder();
|
|
955
991
|
this.recorder.pendingRecords.push({
|
|
956
992
|
bookId: this.ctx().state.currentSimulationId,
|
|
957
993
|
properties: Object.fromEntries(
|
|
@@ -971,14 +1007,19 @@ var DataService = class extends AbstractService {
|
|
|
971
1007
|
* Adds an event to the book.
|
|
972
1008
|
*/
|
|
973
1009
|
addBookEvent(event) {
|
|
974
|
-
this.ensureBook();
|
|
975
1010
|
this.book.addEvent(event);
|
|
976
1011
|
}
|
|
1012
|
+
/**
|
|
1013
|
+
* Write a log message to the terminal UI.
|
|
1014
|
+
*/
|
|
1015
|
+
log(message) {
|
|
1016
|
+
if (isMainThread) return;
|
|
1017
|
+
parentPort?.postMessage({ type: "user-log", message });
|
|
1018
|
+
}
|
|
977
1019
|
/**
|
|
978
1020
|
* Intended for internal use only.
|
|
979
1021
|
*/
|
|
980
1022
|
_clearPendingRecords() {
|
|
981
|
-
this.ensureRecorder();
|
|
982
1023
|
this.recorder.pendingRecords = [];
|
|
983
1024
|
}
|
|
984
1025
|
};
|
|
@@ -1119,21 +1160,44 @@ var GameService = class extends AbstractService {
|
|
|
1119
1160
|
}
|
|
1120
1161
|
};
|
|
1121
1162
|
|
|
1163
|
+
// src/service/rng.ts
|
|
1164
|
+
var RngService = class extends AbstractService {
|
|
1165
|
+
rng = new RandomNumberGenerator();
|
|
1166
|
+
constructor(ctx) {
|
|
1167
|
+
super(ctx);
|
|
1168
|
+
}
|
|
1169
|
+
/**
|
|
1170
|
+
* Random weighted selection from a set of items.
|
|
1171
|
+
*/
|
|
1172
|
+
weightedRandom = this.rng.weightedRandom.bind(this.rng);
|
|
1173
|
+
/**
|
|
1174
|
+
* Selects a random item from an array.
|
|
1175
|
+
*/
|
|
1176
|
+
randomItem = this.rng.randomItem.bind(this.rng);
|
|
1177
|
+
/**
|
|
1178
|
+
* Shuffles an array.
|
|
1179
|
+
*/
|
|
1180
|
+
shuffle = this.rng.shuffle.bind(this.rng);
|
|
1181
|
+
/**
|
|
1182
|
+
* Generates a random float between two values.
|
|
1183
|
+
*/
|
|
1184
|
+
randomFloat = this.rng.randomFloat.bind(this.rng);
|
|
1185
|
+
/**
|
|
1186
|
+
* Sets the seed for the RNG.
|
|
1187
|
+
*/
|
|
1188
|
+
setSeedIfDifferent = this.rng.setSeedIfDifferent.bind(this.rng);
|
|
1189
|
+
};
|
|
1190
|
+
|
|
1122
1191
|
// src/service/wallet.ts
|
|
1123
|
-
import assert5 from "assert";
|
|
1124
1192
|
var WalletService = class extends AbstractService {
|
|
1125
1193
|
wallet;
|
|
1126
1194
|
constructor(ctx) {
|
|
1127
1195
|
super(ctx);
|
|
1128
1196
|
}
|
|
1129
|
-
ensureWallet() {
|
|
1130
|
-
assert5(this.wallet, "Wallet not set in WalletService. Call setWallet() first.");
|
|
1131
|
-
}
|
|
1132
1197
|
/**
|
|
1133
1198
|
* Intended for internal use only.
|
|
1134
1199
|
*/
|
|
1135
1200
|
_getWallet() {
|
|
1136
|
-
this.ensureWallet();
|
|
1137
1201
|
return this.wallet;
|
|
1138
1202
|
}
|
|
1139
1203
|
/**
|
|
@@ -1149,7 +1213,6 @@ var WalletService = class extends AbstractService {
|
|
|
1149
1213
|
* If your game has tumbling mechanics, you should call this method again after every new tumble and win calculation.
|
|
1150
1214
|
*/
|
|
1151
1215
|
addSpinWin(amount) {
|
|
1152
|
-
this.ensureWallet();
|
|
1153
1216
|
this.wallet.addSpinWin(amount);
|
|
1154
1217
|
}
|
|
1155
1218
|
/**
|
|
@@ -1158,7 +1221,6 @@ var WalletService = class extends AbstractService {
|
|
|
1158
1221
|
* This also calls `addSpinWin()` internally, to add the tumble win to the overall spin win.
|
|
1159
1222
|
*/
|
|
1160
1223
|
addTumbleWin(amount) {
|
|
1161
|
-
this.ensureWallet();
|
|
1162
1224
|
this.wallet.addTumbleWin(amount);
|
|
1163
1225
|
}
|
|
1164
1226
|
/**
|
|
@@ -1168,28 +1230,24 @@ var WalletService = class extends AbstractService {
|
|
|
1168
1230
|
* and after a (free) spin is played out to finalize the win.
|
|
1169
1231
|
*/
|
|
1170
1232
|
confirmSpinWin() {
|
|
1171
|
-
this.ensureWallet();
|
|
1172
1233
|
this.wallet.confirmSpinWin(this.ctx().state.currentSpinType);
|
|
1173
1234
|
}
|
|
1174
1235
|
/**
|
|
1175
1236
|
* Gets the total win amount of the current simulation.
|
|
1176
1237
|
*/
|
|
1177
1238
|
getCurrentWin() {
|
|
1178
|
-
this.ensureWallet();
|
|
1179
1239
|
return this.wallet.getCurrentWin();
|
|
1180
1240
|
}
|
|
1181
1241
|
/**
|
|
1182
1242
|
* Gets the current spin win amount of the ongoing spin.
|
|
1183
1243
|
*/
|
|
1184
1244
|
getCurrentSpinWin() {
|
|
1185
|
-
this.ensureWallet();
|
|
1186
1245
|
return this.wallet.getCurrentSpinWin();
|
|
1187
1246
|
}
|
|
1188
1247
|
/**
|
|
1189
1248
|
* Gets the current tumble win amount of the ongoing spin.
|
|
1190
1249
|
*/
|
|
1191
1250
|
getCurrentTumbleWin() {
|
|
1192
|
-
this.ensureWallet();
|
|
1193
1251
|
return this.wallet.getCurrentTumbleWin();
|
|
1194
1252
|
}
|
|
1195
1253
|
};
|
|
@@ -1218,7 +1276,6 @@ function createGameContext(opts) {
|
|
|
1218
1276
|
|
|
1219
1277
|
// utils.ts
|
|
1220
1278
|
import fs from "fs";
|
|
1221
|
-
import readline from "readline";
|
|
1222
1279
|
function createDirIfNotExists(dirPath) {
|
|
1223
1280
|
if (!fs.existsSync(dirPath)) {
|
|
1224
1281
|
fs.mkdirSync(dirPath, { recursive: true });
|
|
@@ -1243,37 +1300,6 @@ function writeFile(filePath, data) {
|
|
|
1243
1300
|
function copy(obj) {
|
|
1244
1301
|
return JSON.parse(JSON.stringify(obj));
|
|
1245
1302
|
}
|
|
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
1303
|
function round(value, decimals) {
|
|
1278
1304
|
return Number(Math.round(Number(value + "e" + decimals)) + "e-" + decimals);
|
|
1279
1305
|
}
|
|
@@ -1293,16 +1319,26 @@ var Book = class {
|
|
|
1293
1319
|
/**
|
|
1294
1320
|
* Intended for internal use only.
|
|
1295
1321
|
*/
|
|
1296
|
-
|
|
1322
|
+
_reset(id, criteria) {
|
|
1323
|
+
this.id = id;
|
|
1324
|
+
this.criteria = criteria;
|
|
1325
|
+
this.events = [];
|
|
1326
|
+
this.payout = 0;
|
|
1327
|
+
this.basegameWins = 0;
|
|
1328
|
+
this.freespinsWins = 0;
|
|
1329
|
+
}
|
|
1330
|
+
/**
|
|
1331
|
+
* Intended for internal use only.
|
|
1332
|
+
*/
|
|
1333
|
+
_setCriteria(criteria) {
|
|
1297
1334
|
this.criteria = criteria;
|
|
1298
1335
|
}
|
|
1299
1336
|
/**
|
|
1300
1337
|
* Adds an event to the book.
|
|
1301
1338
|
*/
|
|
1302
1339
|
addEvent(event) {
|
|
1303
|
-
const index = this.events.length + 1;
|
|
1304
1340
|
this.events.push({
|
|
1305
|
-
index,
|
|
1341
|
+
index: this.events.length + 1,
|
|
1306
1342
|
type: event.type,
|
|
1307
1343
|
data: copy(event.data)
|
|
1308
1344
|
});
|
|
@@ -1310,7 +1346,7 @@ var Book = class {
|
|
|
1310
1346
|
/**
|
|
1311
1347
|
* Intended for internal use only.
|
|
1312
1348
|
*/
|
|
1313
|
-
|
|
1349
|
+
_serialize() {
|
|
1314
1350
|
return {
|
|
1315
1351
|
id: this.id,
|
|
1316
1352
|
criteria: this.criteria,
|
|
@@ -1375,6 +1411,23 @@ var Wallet = class {
|
|
|
1375
1411
|
currentTumbleWin = 0;
|
|
1376
1412
|
constructor() {
|
|
1377
1413
|
}
|
|
1414
|
+
/**
|
|
1415
|
+
* Intended for internal use only.
|
|
1416
|
+
*/
|
|
1417
|
+
_reset() {
|
|
1418
|
+
this.cumulativeWins = 0;
|
|
1419
|
+
this.cumulativeWinsPerSpinType = {
|
|
1420
|
+
[SPIN_TYPE.BASE_GAME]: 0,
|
|
1421
|
+
[SPIN_TYPE.FREE_SPINS]: 0
|
|
1422
|
+
};
|
|
1423
|
+
this.currentWin = 0;
|
|
1424
|
+
this.currentWinPerSpinType = {
|
|
1425
|
+
[SPIN_TYPE.BASE_GAME]: 0,
|
|
1426
|
+
[SPIN_TYPE.FREE_SPINS]: 0
|
|
1427
|
+
};
|
|
1428
|
+
this.currentSpinWin = 0;
|
|
1429
|
+
this.currentTumbleWin = 0;
|
|
1430
|
+
}
|
|
1378
1431
|
/**
|
|
1379
1432
|
* Updates the win for the current spin.
|
|
1380
1433
|
*
|
|
@@ -1547,7 +1600,9 @@ var Wallet = class {
|
|
|
1547
1600
|
import { pipeline } from "stream/promises";
|
|
1548
1601
|
|
|
1549
1602
|
// src/simulation/utils.ts
|
|
1550
|
-
import
|
|
1603
|
+
import fs2 from "fs";
|
|
1604
|
+
import assert4 from "assert";
|
|
1605
|
+
import chalk from "chalk";
|
|
1551
1606
|
function hashStringToInt(input) {
|
|
1552
1607
|
let h = 2166136261;
|
|
1553
1608
|
for (let i = 0; i < input.length; i++) {
|
|
@@ -1560,7 +1615,7 @@ function splitCountsAcrossChunks(totalCounts, chunkSizes) {
|
|
|
1560
1615
|
const total = chunkSizes.reduce((a, b) => a + b, 0);
|
|
1561
1616
|
const allCriteria = Object.keys(totalCounts);
|
|
1562
1617
|
const totalCountsSum = allCriteria.reduce((s, c) => s + (totalCounts[c] ?? 0), 0);
|
|
1563
|
-
|
|
1618
|
+
assert4(
|
|
1564
1619
|
totalCountsSum === total,
|
|
1565
1620
|
`Counts (${totalCountsSum}) must match chunk total (${total}).`
|
|
1566
1621
|
);
|
|
@@ -1593,9 +1648,9 @@ function splitCountsAcrossChunks(totalCounts, chunkSizes) {
|
|
|
1593
1648
|
for (let target = 0; target < chunkSizes.length; target++) {
|
|
1594
1649
|
while (deficits[target] > 0) {
|
|
1595
1650
|
const src = deficits.findIndex((d) => d < 0);
|
|
1596
|
-
|
|
1651
|
+
assert4(src !== -1, "No surplus chunk found, but deficits remain.");
|
|
1597
1652
|
const crit = allCriteria.find((c) => (perChunk[src][c] ?? 0) > 0);
|
|
1598
|
-
|
|
1653
|
+
assert4(crit, `No movable criteria found from surplus chunk ${src}.`);
|
|
1599
1654
|
perChunk[src][crit] -= 1;
|
|
1600
1655
|
perChunk[target][crit] = (perChunk[target][crit] ?? 0) + 1;
|
|
1601
1656
|
totals[src] -= 1;
|
|
@@ -1606,14 +1661,14 @@ function splitCountsAcrossChunks(totalCounts, chunkSizes) {
|
|
|
1606
1661
|
}
|
|
1607
1662
|
totals = chunkTotals();
|
|
1608
1663
|
for (let i = 0; i < chunkSizes.length; i++) {
|
|
1609
|
-
|
|
1664
|
+
assert4(
|
|
1610
1665
|
totals[i] === chunkSizes[i],
|
|
1611
1666
|
`Chunk ${i} size mismatch. Expected ${chunkSizes[i]}, got ${totals[i]}`
|
|
1612
1667
|
);
|
|
1613
1668
|
}
|
|
1614
1669
|
for (const c of allCriteria) {
|
|
1615
1670
|
const sum = perChunk.reduce((s, m) => s + (m[c] ?? 0), 0);
|
|
1616
|
-
|
|
1671
|
+
assert4(sum === (totalCounts[c] ?? 0), `Chunk split mismatch for criteria "${c}"`);
|
|
1617
1672
|
}
|
|
1618
1673
|
return perChunk;
|
|
1619
1674
|
}
|
|
@@ -1644,8 +1699,295 @@ function createCriteriaSampler(counts, seed) {
|
|
|
1644
1699
|
return keys.find((k) => (remaining[k] ?? 0) > 0) ?? "N/A";
|
|
1645
1700
|
};
|
|
1646
1701
|
}
|
|
1702
|
+
async function makeLutIndexFromPublishLut(lutPublishPath, lutIndexPath) {
|
|
1703
|
+
console.log(chalk.gray(`Regenerating LUT index file...`));
|
|
1704
|
+
if (!fs2.existsSync(lutPublishPath)) {
|
|
1705
|
+
console.warn(
|
|
1706
|
+
chalk.yellow(
|
|
1707
|
+
`LUT publish file does not exist when regenerating index file: ${lutPublishPath}`
|
|
1708
|
+
)
|
|
1709
|
+
);
|
|
1710
|
+
return;
|
|
1711
|
+
}
|
|
1712
|
+
try {
|
|
1713
|
+
const lutPublishStream = fs2.createReadStream(lutPublishPath, {
|
|
1714
|
+
highWaterMark: 500 * 1024 * 1024
|
|
1715
|
+
});
|
|
1716
|
+
const rl = __require("readline").createInterface({
|
|
1717
|
+
input: lutPublishStream,
|
|
1718
|
+
crlfDelay: Infinity
|
|
1719
|
+
});
|
|
1720
|
+
const lutIndexStream = fs2.createWriteStream(lutIndexPath, {
|
|
1721
|
+
highWaterMark: 500 * 1024 * 1024
|
|
1722
|
+
});
|
|
1723
|
+
let offset = 0n;
|
|
1724
|
+
for await (const line of rl) {
|
|
1725
|
+
if (!line.trim()) continue;
|
|
1726
|
+
const indexBuffer = Buffer.alloc(8);
|
|
1727
|
+
indexBuffer.writeBigUInt64LE(offset);
|
|
1728
|
+
if (!lutIndexStream.write(indexBuffer)) {
|
|
1729
|
+
await new Promise((resolve) => lutIndexStream.once("drain", resolve));
|
|
1730
|
+
}
|
|
1731
|
+
offset += BigInt(Buffer.byteLength(line + "\n", "utf8"));
|
|
1732
|
+
}
|
|
1733
|
+
lutIndexStream.end();
|
|
1734
|
+
await new Promise((resolve) => lutIndexStream.on("finish", resolve));
|
|
1735
|
+
} catch (error) {
|
|
1736
|
+
throw new Error(`Error generating LUT index from publish LUT: ${error}`);
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
// src/simulation/index.ts
|
|
1741
|
+
import { io } from "socket.io-client";
|
|
1742
|
+
import chalk2 from "chalk";
|
|
1743
|
+
|
|
1744
|
+
// src/tui/index.ts
|
|
1745
|
+
var TerminalUi = class {
|
|
1746
|
+
progress = 0;
|
|
1747
|
+
timeRemaining = 0;
|
|
1748
|
+
gameMode;
|
|
1749
|
+
currentSims = 0;
|
|
1750
|
+
totalSims = 0;
|
|
1751
|
+
logs = [];
|
|
1752
|
+
logScrollOffset = 0;
|
|
1753
|
+
isScrolled = false;
|
|
1754
|
+
minWidth = 50;
|
|
1755
|
+
minHeight = 12;
|
|
1756
|
+
isRendering = false;
|
|
1757
|
+
renderInterval = null;
|
|
1758
|
+
resizeHandler;
|
|
1759
|
+
sigintHandler;
|
|
1760
|
+
keyHandler;
|
|
1761
|
+
constructor(opts) {
|
|
1762
|
+
this.gameMode = opts.gameMode;
|
|
1763
|
+
this.resizeHandler = () => {
|
|
1764
|
+
this.clearScreen();
|
|
1765
|
+
this.render();
|
|
1766
|
+
};
|
|
1767
|
+
this.sigintHandler = () => {
|
|
1768
|
+
this.stop();
|
|
1769
|
+
process.exit(0);
|
|
1770
|
+
};
|
|
1771
|
+
this.keyHandler = (data) => {
|
|
1772
|
+
const key = data.toString();
|
|
1773
|
+
if (key === "j" || key === "\x1B[A") {
|
|
1774
|
+
this.scrollUp();
|
|
1775
|
+
} else if (key === "k" || key === "\x1B[B") {
|
|
1776
|
+
this.scrollDown();
|
|
1777
|
+
} else if (key === "l") {
|
|
1778
|
+
this.scrollToBottom();
|
|
1779
|
+
} else if (key === "") {
|
|
1780
|
+
this.stop();
|
|
1781
|
+
process.exit(0);
|
|
1782
|
+
}
|
|
1783
|
+
};
|
|
1784
|
+
process.stdout.on("resize", this.resizeHandler);
|
|
1785
|
+
}
|
|
1786
|
+
get terminalWidth() {
|
|
1787
|
+
return process.stdout.columns || 80;
|
|
1788
|
+
}
|
|
1789
|
+
get terminalHeight() {
|
|
1790
|
+
return process.stdout.rows || 24;
|
|
1791
|
+
}
|
|
1792
|
+
get isTooSmall() {
|
|
1793
|
+
return this.terminalWidth < this.minWidth || this.terminalHeight < this.minHeight;
|
|
1794
|
+
}
|
|
1795
|
+
start() {
|
|
1796
|
+
this.enterAltScreen();
|
|
1797
|
+
this.hideCursor();
|
|
1798
|
+
this.clearScreen();
|
|
1799
|
+
if (process.stdin.isTTY) {
|
|
1800
|
+
process.stdin.setRawMode(true);
|
|
1801
|
+
process.stdin.resume();
|
|
1802
|
+
process.stdin.on("data", this.keyHandler);
|
|
1803
|
+
}
|
|
1804
|
+
this.render();
|
|
1805
|
+
this.renderInterval = setInterval(() => this.render(), 100);
|
|
1806
|
+
process.on("SIGINT", this.sigintHandler);
|
|
1807
|
+
}
|
|
1808
|
+
stop() {
|
|
1809
|
+
if (this.renderInterval) {
|
|
1810
|
+
clearInterval(this.renderInterval);
|
|
1811
|
+
this.renderInterval = null;
|
|
1812
|
+
}
|
|
1813
|
+
if (process.stdin.isTTY) {
|
|
1814
|
+
process.stdin.off("data", this.keyHandler);
|
|
1815
|
+
process.stdin.setRawMode(false);
|
|
1816
|
+
process.stdin.pause();
|
|
1817
|
+
}
|
|
1818
|
+
this.showCursor();
|
|
1819
|
+
this.clearScreen();
|
|
1820
|
+
this.exitAltScreen();
|
|
1821
|
+
process.stdout.off("resize", this.resizeHandler);
|
|
1822
|
+
process.off("SIGINT", this.sigintHandler);
|
|
1823
|
+
}
|
|
1824
|
+
setProgress(progress, timeRemaining, completedSims) {
|
|
1825
|
+
this.progress = Math.max(0, Math.min(100, progress));
|
|
1826
|
+
this.timeRemaining = Math.max(0, timeRemaining);
|
|
1827
|
+
this.currentSims = completedSims;
|
|
1828
|
+
}
|
|
1829
|
+
setDetails(opts) {
|
|
1830
|
+
this.gameMode = opts.gameMode;
|
|
1831
|
+
this.totalSims = opts.totalSims;
|
|
1832
|
+
}
|
|
1833
|
+
log(message) {
|
|
1834
|
+
this.logs.push({ i: this.logs.length, m: message });
|
|
1835
|
+
if (!this.isScrolled) this.scrollToBottom();
|
|
1836
|
+
}
|
|
1837
|
+
scrollUp(lines = 1) {
|
|
1838
|
+
this.logScrollOffset = Math.max(0, this.logScrollOffset - lines);
|
|
1839
|
+
this.isScrolled = true;
|
|
1840
|
+
}
|
|
1841
|
+
scrollDown(lines = 1) {
|
|
1842
|
+
const maxOffset = Math.max(0, this.logs.length - this.getLogAreaHeight());
|
|
1843
|
+
this.logScrollOffset = Math.min(maxOffset, this.logScrollOffset + lines);
|
|
1844
|
+
if (this.logScrollOffset >= maxOffset) {
|
|
1845
|
+
this.isScrolled = false;
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
scrollToBottom() {
|
|
1849
|
+
const maxOffset = Math.max(0, this.logs.length - this.getLogAreaHeight());
|
|
1850
|
+
this.logScrollOffset = maxOffset;
|
|
1851
|
+
this.isScrolled = false;
|
|
1852
|
+
}
|
|
1853
|
+
clearLogs() {
|
|
1854
|
+
this.logs = [];
|
|
1855
|
+
this.logScrollOffset = 0;
|
|
1856
|
+
}
|
|
1857
|
+
getLogAreaHeight() {
|
|
1858
|
+
return Math.max(1, this.terminalHeight - 8);
|
|
1859
|
+
}
|
|
1860
|
+
enterAltScreen() {
|
|
1861
|
+
process.stdout.write("\x1B[?1049h");
|
|
1862
|
+
}
|
|
1863
|
+
exitAltScreen() {
|
|
1864
|
+
process.stdout.write("\x1B[?1049l");
|
|
1865
|
+
}
|
|
1866
|
+
hideCursor() {
|
|
1867
|
+
process.stdout.write("\x1B[?25l");
|
|
1868
|
+
}
|
|
1869
|
+
showCursor() {
|
|
1870
|
+
process.stdout.write("\x1B[?25h");
|
|
1871
|
+
}
|
|
1872
|
+
clearScreen() {
|
|
1873
|
+
process.stdout.write("\x1B[2J\x1B[H");
|
|
1874
|
+
}
|
|
1875
|
+
moveTo(row, col) {
|
|
1876
|
+
process.stdout.write(`\x1B[${row};${col}H`);
|
|
1877
|
+
}
|
|
1878
|
+
render() {
|
|
1879
|
+
if (this.isRendering) return;
|
|
1880
|
+
this.isRendering = true;
|
|
1881
|
+
try {
|
|
1882
|
+
this.moveTo(1, 1);
|
|
1883
|
+
if (this.isTooSmall) {
|
|
1884
|
+
this.clearScreen();
|
|
1885
|
+
let msg = "Terminal too small.";
|
|
1886
|
+
let row = Math.floor(this.terminalHeight / 2);
|
|
1887
|
+
let col = Math.max(1, Math.floor((this.terminalWidth - msg.length) / 2));
|
|
1888
|
+
this.moveTo(row, col);
|
|
1889
|
+
process.stdout.write(msg);
|
|
1890
|
+
msg = "Try resizing or restarting the terminal.";
|
|
1891
|
+
row += 1;
|
|
1892
|
+
col = Math.max(1, Math.floor((this.terminalWidth - msg.length) / 2));
|
|
1893
|
+
this.moveTo(row, col);
|
|
1894
|
+
process.stdout.write(msg);
|
|
1895
|
+
return;
|
|
1896
|
+
}
|
|
1897
|
+
const lines = [];
|
|
1898
|
+
const width = this.terminalWidth;
|
|
1899
|
+
lines.push(this.boxLine("top", width));
|
|
1900
|
+
const canScrollUp = this.logScrollOffset > 0;
|
|
1901
|
+
const topHint = canScrollUp ? "\u2191 scroll up (j)" : "";
|
|
1902
|
+
lines.push(this.contentLine(this.centerText(topHint, width - 2), width));
|
|
1903
|
+
const logAreaHeight = this.getLogAreaHeight();
|
|
1904
|
+
const visibleLogs = this.getVisibleLogs(logAreaHeight);
|
|
1905
|
+
for (const log of visibleLogs) {
|
|
1906
|
+
lines.push(
|
|
1907
|
+
this.contentLine(` (${log.i}) ` + this.truncate(log.m, width - 4), width)
|
|
1908
|
+
);
|
|
1909
|
+
}
|
|
1910
|
+
for (let i = visibleLogs.length; i < logAreaHeight; i++) {
|
|
1911
|
+
lines.push(this.contentLine("", width));
|
|
1912
|
+
}
|
|
1913
|
+
const canScrollDown = this.logScrollOffset < Math.max(0, this.logs.length - logAreaHeight);
|
|
1914
|
+
const bottomHint = canScrollDown ? "\u2193 scroll down (k) \u2193 jump to newest (l)" : "";
|
|
1915
|
+
lines.push(this.contentLine(this.centerText(bottomHint, width - 2), width));
|
|
1916
|
+
lines.push(this.boxLine("middle", width));
|
|
1917
|
+
const modeText = `Mode: ${this.gameMode}`;
|
|
1918
|
+
const simsText = `${this.currentSims}/${this.totalSims}`;
|
|
1919
|
+
const infoLine = this.createInfoLine(modeText, simsText, width);
|
|
1920
|
+
lines.push(infoLine);
|
|
1921
|
+
lines.push(this.boxLine("middle", width));
|
|
1922
|
+
lines.push(this.createProgressLine(width));
|
|
1923
|
+
lines.push(this.boxLine("bottom", width));
|
|
1924
|
+
this.moveTo(1, 1);
|
|
1925
|
+
process.stdout.write(lines.join("\n"));
|
|
1926
|
+
} finally {
|
|
1927
|
+
this.isRendering = false;
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
getVisibleLogs(height) {
|
|
1931
|
+
const start = this.logScrollOffset;
|
|
1932
|
+
const end = start + height;
|
|
1933
|
+
return this.logs.slice(start, end);
|
|
1934
|
+
}
|
|
1935
|
+
boxLine(type, width) {
|
|
1936
|
+
const chars = {
|
|
1937
|
+
top: { left: "\u250C", right: "\u2510", fill: "\u2500" },
|
|
1938
|
+
middle: { left: "\u251C", right: "\u2524", fill: "\u2500" },
|
|
1939
|
+
bottom: { left: "\u2514", right: "\u2518", fill: "\u2500" }
|
|
1940
|
+
};
|
|
1941
|
+
const c = chars[type];
|
|
1942
|
+
return c.left + c.fill.repeat(width - 2) + c.right;
|
|
1943
|
+
}
|
|
1944
|
+
contentLine(content, width) {
|
|
1945
|
+
const innerWidth = width - 2;
|
|
1946
|
+
const paddedContent = content.padEnd(innerWidth).slice(0, innerWidth);
|
|
1947
|
+
return "\u2502" + paddedContent + "\u2502";
|
|
1948
|
+
}
|
|
1949
|
+
createInfoLine(left, right, width) {
|
|
1950
|
+
const innerWidth = width - 2;
|
|
1951
|
+
const separator = " \u2502 ";
|
|
1952
|
+
const availableForText = innerWidth - separator.length;
|
|
1953
|
+
const leftWidth = Math.floor(availableForText / 2);
|
|
1954
|
+
const rightWidth = availableForText - leftWidth;
|
|
1955
|
+
const leftTruncated = this.truncate(left, leftWidth);
|
|
1956
|
+
const rightTruncated = this.truncate(right, rightWidth);
|
|
1957
|
+
const leftPadded = this.centerText(leftTruncated, leftWidth);
|
|
1958
|
+
const rightPadded = this.centerText(rightTruncated, rightWidth);
|
|
1959
|
+
return "\u2502" + leftPadded + separator + rightPadded + "\u2502";
|
|
1960
|
+
}
|
|
1961
|
+
createProgressLine(width) {
|
|
1962
|
+
const innerWidth = width - 2;
|
|
1963
|
+
const timeStr = this.formatTime(this.timeRemaining);
|
|
1964
|
+
const percentStr = `${this.progress.toFixed(2)}%`;
|
|
1965
|
+
const rightInfo = `${timeStr} ${percentStr}`;
|
|
1966
|
+
const barWidth = Math.max(10, innerWidth - rightInfo.length - 3);
|
|
1967
|
+
const filledWidth = Math.round(this.progress / 100 * barWidth);
|
|
1968
|
+
const emptyWidth = barWidth - filledWidth;
|
|
1969
|
+
const bar = "\u2588".repeat(filledWidth) + "-".repeat(emptyWidth);
|
|
1970
|
+
const content = ` ${bar} ${rightInfo}`;
|
|
1971
|
+
return this.contentLine(content, width);
|
|
1972
|
+
}
|
|
1973
|
+
formatTime(seconds) {
|
|
1974
|
+
return new Date(seconds * 1e3).toISOString().substr(11, 8);
|
|
1975
|
+
}
|
|
1976
|
+
centerText(text, width) {
|
|
1977
|
+
if (text.length >= width) return text.slice(0, width);
|
|
1978
|
+
const padding = width - text.length;
|
|
1979
|
+
const leftPad = Math.floor(padding / 2);
|
|
1980
|
+
const rightPad = padding - leftPad;
|
|
1981
|
+
return " ".repeat(leftPad) + text + " ".repeat(rightPad);
|
|
1982
|
+
}
|
|
1983
|
+
truncate(text, maxLength) {
|
|
1984
|
+
if (text.length <= maxLength) return text;
|
|
1985
|
+
return text.slice(0, maxLength - 3) + "...";
|
|
1986
|
+
}
|
|
1987
|
+
};
|
|
1647
1988
|
|
|
1648
1989
|
// src/simulation/index.ts
|
|
1990
|
+
import { Readable } from "stream";
|
|
1649
1991
|
var completedSimulations = 0;
|
|
1650
1992
|
var TEMP_FILENAME = "__temp_compiled_src_IGNORE.js";
|
|
1651
1993
|
var TEMP_FOLDER = "temp_files";
|
|
@@ -1656,68 +1998,112 @@ var Simulation = class {
|
|
|
1656
1998
|
concurrency;
|
|
1657
1999
|
debug = false;
|
|
1658
2000
|
actualSims = 0;
|
|
1659
|
-
wallet;
|
|
2001
|
+
wallet = new Wallet();
|
|
2002
|
+
summary = {};
|
|
1660
2003
|
recordsWriteStream;
|
|
1661
2004
|
hasWrittenRecord = false;
|
|
1662
2005
|
streamHighWaterMark = 500 * 1024 * 1024;
|
|
1663
2006
|
maxPendingSims;
|
|
1664
2007
|
maxHighWaterMark;
|
|
2008
|
+
panelPort = 7770;
|
|
2009
|
+
panelActive = false;
|
|
2010
|
+
panelWsUrl;
|
|
2011
|
+
socket;
|
|
2012
|
+
tui;
|
|
2013
|
+
tempBookIndexPaths = [];
|
|
2014
|
+
bookIndexMetas = [];
|
|
1665
2015
|
PATHS = {};
|
|
1666
2016
|
// Worker related
|
|
1667
2017
|
credits = 0;
|
|
1668
2018
|
creditWaiters = [];
|
|
1669
2019
|
creditListenerInit = false;
|
|
2020
|
+
bookBuffers = /* @__PURE__ */ new Map();
|
|
2021
|
+
bookBufferSizes = /* @__PURE__ */ new Map();
|
|
2022
|
+
bookChunkIndexes = /* @__PURE__ */ new Map();
|
|
1670
2023
|
constructor(opts, gameConfigOpts) {
|
|
1671
|
-
|
|
2024
|
+
const { config, metadata } = createGameConfig(gameConfigOpts);
|
|
2025
|
+
this.gameConfig = { ...config, ...metadata };
|
|
1672
2026
|
this.gameConfigOpts = gameConfigOpts;
|
|
1673
2027
|
this.simRunsAmount = opts.simRunsAmount || {};
|
|
1674
2028
|
this.concurrency = (opts.concurrency || 6) >= 2 ? opts.concurrency || 6 : 2;
|
|
1675
|
-
this.
|
|
1676
|
-
this.
|
|
1677
|
-
this.maxHighWaterMark = (opts.maxDiskBuffer ?? 50) * 1024 * 1024;
|
|
2029
|
+
this.maxPendingSims = opts.maxPendingSims ?? 25;
|
|
2030
|
+
this.maxHighWaterMark = (opts.maxDiskBuffer ?? 150) * 1024 * 1024;
|
|
1678
2031
|
const gameModeKeys = Object.keys(this.gameConfig.gameModes);
|
|
1679
|
-
|
|
2032
|
+
assert5(
|
|
1680
2033
|
Object.values(this.gameConfig.gameModes).map((m) => gameModeKeys.includes(m.name)).every((v) => v === true),
|
|
1681
2034
|
"Game mode name must match its key in the gameModes object."
|
|
1682
2035
|
);
|
|
1683
|
-
|
|
2036
|
+
const basePath = path3.join(this.gameConfig.rootDir, this.gameConfig.outputDir);
|
|
1684
2037
|
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")
|
|
2038
|
+
...createPermanentFilePaths(basePath),
|
|
2039
|
+
...createTemporaryFilePaths(basePath, TEMP_FOLDER)
|
|
1699
2040
|
};
|
|
2041
|
+
this.tui = new TerminalUi({
|
|
2042
|
+
gameMode: "N/A"
|
|
2043
|
+
});
|
|
1700
2044
|
}
|
|
1701
2045
|
async runSimulation(opts) {
|
|
1702
2046
|
const debug = opts.debug || false;
|
|
1703
2047
|
this.debug = debug;
|
|
2048
|
+
let statusMessage = "";
|
|
1704
2049
|
const gameModesToSimulate = Object.keys(this.simRunsAmount);
|
|
1705
2050
|
const configuredGameModes = Object.keys(this.gameConfig.gameModes);
|
|
1706
2051
|
if (gameModesToSimulate.length === 0) {
|
|
1707
2052
|
throw new Error("No game modes configured for simulation.");
|
|
1708
2053
|
}
|
|
1709
2054
|
this.generateReelsetFiles();
|
|
1710
|
-
if (
|
|
2055
|
+
if (isMainThread2) {
|
|
1711
2056
|
this.preprocessFiles();
|
|
1712
|
-
|
|
2057
|
+
this.panelPort = opts.panelPort || 7770;
|
|
2058
|
+
this.panelWsUrl = `http://localhost:${this.panelPort}`;
|
|
2059
|
+
await new Promise((resolve) => {
|
|
2060
|
+
try {
|
|
2061
|
+
this.socket = io(this.panelWsUrl, {
|
|
2062
|
+
path: "/ws",
|
|
2063
|
+
transports: ["websocket", "polling"],
|
|
2064
|
+
withCredentials: true,
|
|
2065
|
+
autoConnect: false,
|
|
2066
|
+
reconnection: false
|
|
2067
|
+
});
|
|
2068
|
+
this.socket.connect();
|
|
2069
|
+
this.socket.once("connect", () => {
|
|
2070
|
+
this.panelActive = true;
|
|
2071
|
+
resolve();
|
|
2072
|
+
});
|
|
2073
|
+
this.socket.once("connect_error", () => {
|
|
2074
|
+
this.socket?.close();
|
|
2075
|
+
this.socket = void 0;
|
|
2076
|
+
resolve();
|
|
2077
|
+
});
|
|
2078
|
+
} catch (error) {
|
|
2079
|
+
this.socket = void 0;
|
|
2080
|
+
resolve();
|
|
2081
|
+
}
|
|
2082
|
+
});
|
|
2083
|
+
this.tui?.start();
|
|
2084
|
+
fs3.rmSync(path3.join(this.PATHS.base, "books_chunks"), {
|
|
2085
|
+
recursive: true,
|
|
2086
|
+
force: true
|
|
2087
|
+
});
|
|
1713
2088
|
for (const mode of gameModesToSimulate) {
|
|
1714
2089
|
completedSimulations = 0;
|
|
1715
2090
|
this.wallet = new Wallet();
|
|
2091
|
+
this.tui?.setDetails({
|
|
2092
|
+
gameMode: mode,
|
|
2093
|
+
totalSims: this.simRunsAmount[mode] || 0
|
|
2094
|
+
});
|
|
1716
2095
|
this.hasWrittenRecord = false;
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
2096
|
+
this.bookIndexMetas = [];
|
|
2097
|
+
this.tempBookIndexPaths = [];
|
|
2098
|
+
this.bookChunkIndexes = /* @__PURE__ */ new Map();
|
|
2099
|
+
this.bookBuffers = /* @__PURE__ */ new Map();
|
|
2100
|
+
this.bookBufferSizes = /* @__PURE__ */ new Map();
|
|
2101
|
+
const startTime = Date.now();
|
|
2102
|
+
statusMessage = `Simulating mode "${mode}" with ${this.simRunsAmount[mode]} runs.`;
|
|
2103
|
+
this.tui?.log(statusMessage);
|
|
2104
|
+
if (this.socket && this.panelActive) {
|
|
2105
|
+
this.socket.emit("simulationStatus", statusMessage);
|
|
2106
|
+
}
|
|
1721
2107
|
const runs = this.simRunsAmount[mode] || 0;
|
|
1722
2108
|
if (runs <= 0) continue;
|
|
1723
2109
|
if (!configuredGameModes.includes(mode)) {
|
|
@@ -1725,16 +2111,19 @@ Simulating game mode: ${mode}`);
|
|
|
1725
2111
|
`Tried to simulate game mode "${mode}", but it's not configured in the game config.`
|
|
1726
2112
|
);
|
|
1727
2113
|
}
|
|
1728
|
-
|
|
2114
|
+
this.summary[mode] = {
|
|
2115
|
+
total: { numSims: runs, bsWins: 0, fsWins: 0, rtp: 0 },
|
|
2116
|
+
criteria: {}
|
|
2117
|
+
};
|
|
1729
2118
|
const tempRecordsPath = this.PATHS.tempRecords(mode);
|
|
1730
2119
|
createDirIfNotExists(this.PATHS.base);
|
|
1731
|
-
createDirIfNotExists(
|
|
1732
|
-
this.recordsWriteStream =
|
|
2120
|
+
createDirIfNotExists(path3.join(this.PATHS.base, TEMP_FOLDER));
|
|
2121
|
+
this.recordsWriteStream = fs3.createWriteStream(tempRecordsPath, {
|
|
1733
2122
|
highWaterMark: this.maxHighWaterMark
|
|
1734
2123
|
}).setMaxListeners(30);
|
|
1735
2124
|
const criteriaCounts = ResultSet.getNumberOfSimsForCriteria(this, mode);
|
|
1736
2125
|
const totalSims = Object.values(criteriaCounts).reduce((a, b) => a + b, 0);
|
|
1737
|
-
|
|
2126
|
+
assert5(
|
|
1738
2127
|
totalSims === runs,
|
|
1739
2128
|
`Criteria mismatch for mode "${mode}". Expected ${runs}, got ${totalSims}`
|
|
1740
2129
|
);
|
|
@@ -1749,46 +2138,54 @@ Simulating game mode: ${mode}`);
|
|
|
1749
2138
|
});
|
|
1750
2139
|
createDirIfNotExists(this.PATHS.optimizationFiles);
|
|
1751
2140
|
createDirIfNotExists(this.PATHS.publishFiles);
|
|
1752
|
-
|
|
1753
|
-
|
|
2141
|
+
statusMessage = `Writing final files for game mode "${mode}". This may take a while...`;
|
|
2142
|
+
this.tui?.log(statusMessage);
|
|
2143
|
+
if (this.socket && this.panelActive) {
|
|
2144
|
+
this.socket.emit("simulationStatus", statusMessage);
|
|
2145
|
+
}
|
|
2146
|
+
writeFile(
|
|
2147
|
+
this.PATHS.booksIndexMeta(mode),
|
|
2148
|
+
JSON.stringify(
|
|
2149
|
+
this.bookIndexMetas.sort((a, b) => a.worker - b.worker),
|
|
2150
|
+
null,
|
|
2151
|
+
2
|
|
2152
|
+
)
|
|
1754
2153
|
);
|
|
2154
|
+
const booksPath = this.PATHS.booksCompressed(mode);
|
|
1755
2155
|
try {
|
|
1756
|
-
const finalBookStream =
|
|
2156
|
+
const finalBookStream = fs3.createWriteStream(booksPath, {
|
|
1757
2157
|
highWaterMark: this.streamHighWaterMark
|
|
1758
2158
|
});
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
}
|
|
1767
|
-
}
|
|
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
|
-
}
|
|
2159
|
+
for (const { worker, chunks: chunks2 } of this.bookIndexMetas) {
|
|
2160
|
+
for (let chunk = 0; chunk < chunks2; chunk++) {
|
|
2161
|
+
const bookChunkPath = this.PATHS.booksChunk(mode, worker, chunk);
|
|
2162
|
+
if (!fs3.existsSync(bookChunkPath)) continue;
|
|
2163
|
+
const chunkData = fs3.readFileSync(bookChunkPath);
|
|
2164
|
+
if (!finalBookStream.write(chunkData)) {
|
|
2165
|
+
await new Promise((r) => finalBookStream.once("drain", () => r()));
|
|
1773
2166
|
}
|
|
1774
|
-
fs2.rmSync(tempBookPath);
|
|
1775
|
-
isFirstChunk = false;
|
|
1776
2167
|
}
|
|
1777
2168
|
}
|
|
1778
2169
|
finalBookStream.end();
|
|
1779
|
-
await new Promise((
|
|
2170
|
+
await new Promise((r) => finalBookStream.on("finish", () => r()));
|
|
1780
2171
|
} catch (error) {
|
|
1781
2172
|
throw new Error(`Error merging book files: ${error.message}`);
|
|
1782
2173
|
}
|
|
1783
2174
|
const lutPath = this.PATHS.lookupTable(mode);
|
|
1784
2175
|
const lutPathPublish = this.PATHS.lookupTablePublish(mode);
|
|
1785
2176
|
const lutSegmentedPath = this.PATHS.lookupTableSegmented(mode);
|
|
1786
|
-
await this.mergeCsv(
|
|
1787
|
-
|
|
2177
|
+
await this.mergeCsv(
|
|
2178
|
+
chunks,
|
|
2179
|
+
lutPath,
|
|
2180
|
+
(i) => `temp_lookup_${mode}_${i}.csv`,
|
|
2181
|
+
this.PATHS.lookupTableIndex(mode)
|
|
2182
|
+
);
|
|
2183
|
+
fs3.copyFileSync(lutPath, lutPathPublish);
|
|
1788
2184
|
await this.mergeCsv(
|
|
1789
2185
|
chunks,
|
|
1790
2186
|
lutSegmentedPath,
|
|
1791
|
-
(i) => `temp_lookup_segmented_${mode}_${i}.csv
|
|
2187
|
+
(i) => `temp_lookup_segmented_${mode}_${i}.csv`,
|
|
2188
|
+
this.PATHS.lookupTableSegmentedIndex(mode)
|
|
1792
2189
|
);
|
|
1793
2190
|
if (this.recordsWriteStream) {
|
|
1794
2191
|
await new Promise((resolve) => {
|
|
@@ -1799,29 +2196,22 @@ Simulating game mode: ${mode}`);
|
|
|
1799
2196
|
this.recordsWriteStream = void 0;
|
|
1800
2197
|
}
|
|
1801
2198
|
await this.writeRecords(mode);
|
|
1802
|
-
await this.writeBooksJson(mode);
|
|
1803
2199
|
this.writeIndexJson();
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
)
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
Object.entries(this.wallet.getCumulativeWinsPerSpinType()).map(([k, v]) => [
|
|
1812
|
-
k,
|
|
1813
|
-
round(v, 3)
|
|
1814
|
-
])
|
|
1815
|
-
);
|
|
1816
|
-
console.timeEnd(mode);
|
|
2200
|
+
const endTime = Date.now();
|
|
2201
|
+
const prettyTime = new Date(endTime - startTime).toISOString().slice(11, -1);
|
|
2202
|
+
statusMessage = `Mode ${mode} done! Time taken: ${prettyTime}`;
|
|
2203
|
+
this.tui?.log(statusMessage);
|
|
2204
|
+
if (this.socket && this.panelActive) {
|
|
2205
|
+
this.socket.emit("simulationStatus", statusMessage);
|
|
2206
|
+
}
|
|
1817
2207
|
}
|
|
1818
|
-
|
|
1819
|
-
|
|
2208
|
+
this.tui?.stop();
|
|
2209
|
+
await this.printSimulationSummary();
|
|
1820
2210
|
}
|
|
1821
2211
|
let desiredSims = 0;
|
|
1822
2212
|
let actualSims = 0;
|
|
1823
2213
|
const criteriaToRetries = {};
|
|
1824
|
-
if (!
|
|
2214
|
+
if (!isMainThread2) {
|
|
1825
2215
|
const { mode, simStart, simEnd, index, criteriaCounts } = workerData;
|
|
1826
2216
|
const seed = hashStringToInt(mode) + index >>> 0;
|
|
1827
2217
|
const nextCriteria = createCriteriaSampler(criteriaCounts, seed);
|
|
@@ -1836,16 +2226,16 @@ Simulating game mode: ${mode}`);
|
|
|
1836
2226
|
actualSims += this.actualSims;
|
|
1837
2227
|
}
|
|
1838
2228
|
}
|
|
1839
|
-
|
|
1840
|
-
console.log(`Desired ${desiredSims}, Actual ${actualSims}`);
|
|
1841
|
-
console.log(`Retries per criteria:`, criteriaToRetries);
|
|
1842
|
-
}
|
|
1843
|
-
parentPort?.postMessage({
|
|
2229
|
+
parentPort2?.postMessage({
|
|
1844
2230
|
type: "done",
|
|
1845
2231
|
workerNum: index
|
|
1846
2232
|
});
|
|
1847
|
-
|
|
1848
|
-
|
|
2233
|
+
parentPort2?.removeAllListeners();
|
|
2234
|
+
parentPort2?.close();
|
|
2235
|
+
}
|
|
2236
|
+
if (this.socket && this.panelActive) {
|
|
2237
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
2238
|
+
this.socket?.close();
|
|
1849
2239
|
}
|
|
1850
2240
|
}
|
|
1851
2241
|
/**
|
|
@@ -1853,39 +2243,36 @@ Simulating game mode: ${mode}`);
|
|
|
1853
2243
|
*/
|
|
1854
2244
|
async spawnWorkersForGameMode(opts) {
|
|
1855
2245
|
const { mode, chunks, chunkCriteriaCounts, totalSims } = opts;
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
2246
|
+
try {
|
|
2247
|
+
await Promise.all(
|
|
2248
|
+
chunks.map(([simStart, simEnd], index) => {
|
|
2249
|
+
return this.callWorker({
|
|
2250
|
+
basePath: this.PATHS.base,
|
|
2251
|
+
mode,
|
|
2252
|
+
simStart,
|
|
2253
|
+
simEnd,
|
|
2254
|
+
index,
|
|
2255
|
+
totalSims,
|
|
2256
|
+
criteriaCounts: chunkCriteriaCounts[index]
|
|
2257
|
+
});
|
|
2258
|
+
})
|
|
2259
|
+
);
|
|
2260
|
+
} catch (error) {
|
|
2261
|
+
this.tui?.stop();
|
|
2262
|
+
throw error;
|
|
2263
|
+
}
|
|
1869
2264
|
}
|
|
1870
2265
|
async callWorker(opts) {
|
|
1871
2266
|
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
2267
|
const write = async (stream, chunk) => {
|
|
1883
2268
|
if (!stream.write(chunk)) {
|
|
1884
2269
|
await new Promise((resolve) => stream.once("drain", resolve));
|
|
1885
2270
|
}
|
|
1886
2271
|
};
|
|
1887
2272
|
return new Promise((resolve, reject) => {
|
|
1888
|
-
const scriptPath =
|
|
2273
|
+
const scriptPath = path3.join(basePath, TEMP_FILENAME);
|
|
2274
|
+
createDirIfNotExists(path3.join(this.PATHS.base, "books_chunks"));
|
|
2275
|
+
const startTime = Date.now();
|
|
1889
2276
|
const worker = new Worker(scriptPath, {
|
|
1890
2277
|
workerData: {
|
|
1891
2278
|
mode,
|
|
@@ -1896,50 +2283,137 @@ Simulating game mode: ${mode}`);
|
|
|
1896
2283
|
}
|
|
1897
2284
|
});
|
|
1898
2285
|
worker.postMessage({ type: "credit", amount: this.maxPendingSims });
|
|
1899
|
-
const
|
|
1900
|
-
|
|
2286
|
+
const flushBookChunk = async () => {
|
|
2287
|
+
if (this.bookBuffers.get(index)?.length === 0) return;
|
|
2288
|
+
if (!this.bookChunkIndexes.has(index)) {
|
|
2289
|
+
this.bookChunkIndexes.set(index, 0);
|
|
2290
|
+
}
|
|
2291
|
+
const chunkIndex = this.bookChunkIndexes.get(index);
|
|
2292
|
+
const bookChunkPath = this.PATHS.booksChunk(mode, index, chunkIndex);
|
|
2293
|
+
const data = this.bookBuffers.get(index).join("\n") + "\n";
|
|
2294
|
+
await pipeline(
|
|
2295
|
+
Readable.from([Buffer.from(data, "utf8")]),
|
|
2296
|
+
zlib.createZstdCompress(),
|
|
2297
|
+
fs3.createWriteStream(bookChunkPath)
|
|
2298
|
+
);
|
|
2299
|
+
this.bookBuffers.set(index, []);
|
|
2300
|
+
this.bookBufferSizes.set(index, 0);
|
|
2301
|
+
this.bookChunkIndexes.set(index, chunkIndex + 1);
|
|
2302
|
+
};
|
|
2303
|
+
const booksIndexPath = this.PATHS.booksIndex(mode, index);
|
|
2304
|
+
const booksIndexStream = fs3.createWriteStream(booksIndexPath, {
|
|
1901
2305
|
highWaterMark: this.maxHighWaterMark
|
|
1902
2306
|
});
|
|
1903
2307
|
const tempLookupPath = this.PATHS.tempLookupTable(mode, index);
|
|
1904
|
-
const lookupStream =
|
|
2308
|
+
const lookupStream = fs3.createWriteStream(tempLookupPath, {
|
|
1905
2309
|
highWaterMark: this.maxHighWaterMark
|
|
1906
2310
|
});
|
|
1907
2311
|
const tempLookupSegPath = this.PATHS.tempLookupTableSegmented(mode, index);
|
|
1908
|
-
const lookupSegmentedStream =
|
|
2312
|
+
const lookupSegmentedStream = fs3.createWriteStream(tempLookupSegPath, {
|
|
1909
2313
|
highWaterMark: this.maxHighWaterMark
|
|
1910
2314
|
});
|
|
1911
2315
|
let writeChain = Promise.resolve();
|
|
1912
2316
|
worker.on("message", (msg) => {
|
|
1913
|
-
if (msg.type === "log") {
|
|
2317
|
+
if (msg.type === "log" || msg.type === "user-log") {
|
|
2318
|
+
this.tui?.log(msg.message);
|
|
1914
2319
|
return;
|
|
1915
2320
|
}
|
|
2321
|
+
if (msg.type === "log-exit") {
|
|
2322
|
+
this.tui?.log(msg.message);
|
|
2323
|
+
this.tui?.stop();
|
|
2324
|
+
console.log(msg.message);
|
|
2325
|
+
process.exit(1);
|
|
2326
|
+
}
|
|
1916
2327
|
if (msg.type === "complete") {
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
2328
|
+
completedSimulations++;
|
|
2329
|
+
if (completedSimulations % 250 === 0 || completedSimulations === totalSims) {
|
|
2330
|
+
const percentage = completedSimulations / totalSims * 100;
|
|
2331
|
+
this.tui?.setProgress(
|
|
2332
|
+
percentage,
|
|
2333
|
+
this.getTimeRemaining(startTime, totalSims),
|
|
2334
|
+
completedSimulations
|
|
2335
|
+
);
|
|
2336
|
+
}
|
|
2337
|
+
if (this.socket && this.panelActive) {
|
|
2338
|
+
if (completedSimulations % 1e3 === 0 || completedSimulations === totalSims) {
|
|
2339
|
+
this.socket.emit("simulationProgress", {
|
|
2340
|
+
mode,
|
|
2341
|
+
percentage: completedSimulations / totalSims * 100,
|
|
2342
|
+
current: completedSimulations,
|
|
2343
|
+
total: totalSims,
|
|
2344
|
+
timeRemaining: this.getTimeRemaining(startTime, totalSims)
|
|
2345
|
+
});
|
|
2346
|
+
this.socket.emit(
|
|
2347
|
+
"simulationShouldStop",
|
|
2348
|
+
this.gameConfig.id,
|
|
2349
|
+
(shouldStop) => {
|
|
2350
|
+
if (shouldStop) {
|
|
2351
|
+
worker.terminate();
|
|
2352
|
+
}
|
|
2353
|
+
}
|
|
2354
|
+
);
|
|
1921
2355
|
}
|
|
2356
|
+
}
|
|
2357
|
+
writeChain = writeChain.then(async () => {
|
|
1922
2358
|
const book = msg.book;
|
|
1923
2359
|
const bookData = {
|
|
1924
2360
|
id: book.id,
|
|
1925
2361
|
payoutMultiplier: book.payout,
|
|
1926
2362
|
events: book.events
|
|
1927
2363
|
};
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
2364
|
+
if (!this.summary[mode]?.criteria[book.criteria]) {
|
|
2365
|
+
this.summary[mode].criteria[book.criteria] = {
|
|
2366
|
+
numSims: 0,
|
|
2367
|
+
bsWins: 0,
|
|
2368
|
+
fsWins: 0,
|
|
2369
|
+
rtp: 0
|
|
2370
|
+
};
|
|
2371
|
+
}
|
|
2372
|
+
const bsWins = round(book.basegameWins, 4);
|
|
2373
|
+
const fsWins = round(book.freespinsWins, 4);
|
|
2374
|
+
this.summary[mode].criteria[book.criteria].numSims += 1;
|
|
2375
|
+
this.summary[mode].total.bsWins += bsWins;
|
|
2376
|
+
this.summary[mode].total.fsWins += fsWins;
|
|
2377
|
+
this.summary[mode].criteria[book.criteria].bsWins += bsWins;
|
|
2378
|
+
this.summary[mode].criteria[book.criteria].fsWins += fsWins;
|
|
2379
|
+
const bookLine = JSON.stringify(bookData);
|
|
2380
|
+
const lineSize = Buffer.byteLength(bookLine + "\n", "utf8");
|
|
2381
|
+
if (this.bookBuffers.has(index)) {
|
|
2382
|
+
this.bookBuffers.get(index).push(bookLine);
|
|
2383
|
+
this.bookBufferSizes.set(
|
|
2384
|
+
index,
|
|
2385
|
+
this.bookBufferSizes.get(index) + lineSize
|
|
2386
|
+
);
|
|
2387
|
+
} else {
|
|
2388
|
+
this.bookBuffers.set(index, [bookLine]);
|
|
2389
|
+
this.bookBufferSizes.set(index, lineSize);
|
|
2390
|
+
}
|
|
2391
|
+
if (!this.tempBookIndexPaths.includes(booksIndexPath)) {
|
|
2392
|
+
this.tempBookIndexPaths.push(booksIndexPath);
|
|
2393
|
+
}
|
|
2394
|
+
await Promise.all([
|
|
2395
|
+
write(
|
|
2396
|
+
booksIndexStream,
|
|
2397
|
+
`${book.id},${index},${this.bookChunkIndexes.get(index) || 0}
|
|
1935
2398
|
`
|
|
1936
|
-
|
|
2399
|
+
),
|
|
2400
|
+
write(lookupStream, `${book.id},1,${Math.round(book.payout)}
|
|
2401
|
+
`),
|
|
2402
|
+
write(
|
|
2403
|
+
lookupSegmentedStream,
|
|
2404
|
+
`${book.id},${book.criteria},${book.basegameWins},${book.freespinsWins}
|
|
2405
|
+
`
|
|
2406
|
+
)
|
|
2407
|
+
]);
|
|
2408
|
+
if (this.bookBufferSizes.get(index) >= 12 * 1024 * 1024) {
|
|
2409
|
+
await flushBookChunk();
|
|
2410
|
+
}
|
|
1937
2411
|
if (this.recordsWriteStream) {
|
|
1938
2412
|
for (const record of msg.records) {
|
|
1939
2413
|
const recordPrefix = this.hasWrittenRecord ? "\n" : "";
|
|
1940
2414
|
await write(
|
|
1941
2415
|
this.recordsWriteStream,
|
|
1942
|
-
recordPrefix +
|
|
2416
|
+
recordPrefix + JSON.stringify(record)
|
|
1943
2417
|
);
|
|
1944
2418
|
this.hasWrittenRecord = true;
|
|
1945
2419
|
}
|
|
@@ -1951,31 +2425,35 @@ Simulating game mode: ${mode}`);
|
|
|
1951
2425
|
}
|
|
1952
2426
|
if (msg.type === "done") {
|
|
1953
2427
|
writeChain.then(async () => {
|
|
1954
|
-
|
|
2428
|
+
await flushBookChunk();
|
|
1955
2429
|
lookupStream.end();
|
|
1956
2430
|
lookupSegmentedStream.end();
|
|
2431
|
+
booksIndexStream.end();
|
|
1957
2432
|
await Promise.all([
|
|
1958
|
-
new Promise((r) => bookStream.on("finish", () => r())),
|
|
1959
2433
|
new Promise((r) => lookupStream.on("finish", () => r())),
|
|
1960
|
-
new Promise((r) => lookupSegmentedStream.on("finish", () => r()))
|
|
2434
|
+
new Promise((r) => lookupSegmentedStream.on("finish", () => r())),
|
|
2435
|
+
new Promise((r) => booksIndexStream.on("finish", () => r()))
|
|
1961
2436
|
]);
|
|
2437
|
+
const bookIndexMeta = {
|
|
2438
|
+
worker: index,
|
|
2439
|
+
chunks: this.bookChunkIndexes.get(index) + 2,
|
|
2440
|
+
simStart,
|
|
2441
|
+
simEnd
|
|
2442
|
+
};
|
|
2443
|
+
this.bookIndexMetas.push(bookIndexMeta);
|
|
1962
2444
|
resolve(true);
|
|
1963
2445
|
}).catch(reject);
|
|
1964
2446
|
return;
|
|
1965
2447
|
}
|
|
1966
2448
|
});
|
|
1967
2449
|
worker.on("error", (error) => {
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
`);
|
|
1971
|
-
process.stdout.write(`
|
|
1972
|
-
${error.stack}
|
|
1973
|
-
`);
|
|
1974
|
-
reject(error);
|
|
2450
|
+
this.tui?.log(error.message);
|
|
2451
|
+
resolve(error);
|
|
1975
2452
|
});
|
|
1976
2453
|
worker.on("exit", (code) => {
|
|
1977
2454
|
if (code !== 0) {
|
|
1978
|
-
|
|
2455
|
+
this.tui?.log(chalk2.yellow(`Worker stopped with exit code ${code}`));
|
|
2456
|
+
resolve(false);
|
|
1979
2457
|
}
|
|
1980
2458
|
});
|
|
1981
2459
|
});
|
|
@@ -1985,12 +2463,21 @@ ${error.stack}
|
|
|
1985
2463
|
*/
|
|
1986
2464
|
runSingleSimulation(opts) {
|
|
1987
2465
|
const { simId, mode, criteria } = opts;
|
|
2466
|
+
let retries = 0;
|
|
1988
2467
|
const ctx = createGameContext({
|
|
1989
2468
|
config: this.gameConfig
|
|
1990
2469
|
});
|
|
1991
2470
|
ctx.state.currentGameMode = mode;
|
|
1992
2471
|
ctx.state.currentSimulationId = simId;
|
|
1993
2472
|
ctx.state.isCriteriaMet = false;
|
|
2473
|
+
ctx.services.data._setBook(
|
|
2474
|
+
new Book({
|
|
2475
|
+
id: simId,
|
|
2476
|
+
criteria
|
|
2477
|
+
})
|
|
2478
|
+
);
|
|
2479
|
+
ctx.services.wallet._setWallet(new Wallet());
|
|
2480
|
+
ctx.services.data._setRecorder(new Recorder());
|
|
1994
2481
|
const resultSet = ctx.services.game.getResultSetByCriteria(
|
|
1995
2482
|
ctx.state.currentGameMode,
|
|
1996
2483
|
criteria
|
|
@@ -2003,6 +2490,21 @@ ${error.stack}
|
|
|
2003
2490
|
if (resultSet.meetsCriteria(ctx)) {
|
|
2004
2491
|
ctx.state.isCriteriaMet = true;
|
|
2005
2492
|
}
|
|
2493
|
+
retries++;
|
|
2494
|
+
if (!ctx.state.isCriteriaMet && retries % 1e4 === 0) {
|
|
2495
|
+
parentPort2?.postMessage({
|
|
2496
|
+
type: "log",
|
|
2497
|
+
message: chalk2.yellow(
|
|
2498
|
+
`Excessive retries @ #${simId} @ criteria "${criteria}": ${retries} retries`
|
|
2499
|
+
)
|
|
2500
|
+
});
|
|
2501
|
+
}
|
|
2502
|
+
if (!ctx.state.isCriteriaMet && retries % 5e4 === 0) {
|
|
2503
|
+
parentPort2?.postMessage({
|
|
2504
|
+
type: "log-exit",
|
|
2505
|
+
message: chalk2.red("Possible infinite loop detected, exiting simulation.")
|
|
2506
|
+
});
|
|
2507
|
+
}
|
|
2006
2508
|
}
|
|
2007
2509
|
ctx.services.wallet._getWallet().writePayoutToBook(ctx);
|
|
2008
2510
|
ctx.services.wallet._getWallet().confirmWins(ctx);
|
|
@@ -2014,10 +2516,10 @@ ${error.stack}
|
|
|
2014
2516
|
});
|
|
2015
2517
|
ctx.config.hooks.onSimulationAccepted?.(ctx);
|
|
2016
2518
|
this.confirmRecords(ctx);
|
|
2017
|
-
|
|
2519
|
+
parentPort2?.postMessage({
|
|
2018
2520
|
type: "complete",
|
|
2019
2521
|
simId,
|
|
2020
|
-
book: ctx.services.data._getBook().
|
|
2522
|
+
book: ctx.services.data._getBook()._serialize(),
|
|
2021
2523
|
wallet: ctx.services.wallet._getWallet().serialize(),
|
|
2022
2524
|
records: ctx.services.data._getRecords()
|
|
2023
2525
|
});
|
|
@@ -2025,7 +2527,7 @@ ${error.stack}
|
|
|
2025
2527
|
initCreditListener() {
|
|
2026
2528
|
if (this.creditListenerInit) return;
|
|
2027
2529
|
this.creditListenerInit = true;
|
|
2028
|
-
|
|
2530
|
+
parentPort2?.on("message", (msg) => {
|
|
2029
2531
|
if (msg?.type !== "credit") return;
|
|
2030
2532
|
const amount = Number(msg?.amount ?? 0);
|
|
2031
2533
|
if (!Number.isFinite(amount) || amount <= 0) return;
|
|
@@ -2055,17 +2557,10 @@ ${error.stack}
|
|
|
2055
2557
|
resetSimulation(ctx) {
|
|
2056
2558
|
this.resetState(ctx);
|
|
2057
2559
|
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
|
-
});
|
|
2560
|
+
ctx.services.data._getRecorder()._reset();
|
|
2561
|
+
ctx.services.wallet._getWallet()._reset();
|
|
2562
|
+
ctx.services.data._getBook()._reset(ctx.state.currentSimulationId, ctx.state.currentResultSet.criteria);
|
|
2563
|
+
ctx.services.game.getCurrentGameMode()._resetTempValues();
|
|
2069
2564
|
}
|
|
2070
2565
|
resetState(ctx) {
|
|
2071
2566
|
ctx.services.rng.setSeedIfDifferent(ctx.state.currentSimulationId);
|
|
@@ -2092,18 +2587,25 @@ ${error.stack}
|
|
|
2092
2587
|
async writeRecords(mode) {
|
|
2093
2588
|
const tempRecordsPath = this.PATHS.tempRecords(mode);
|
|
2094
2589
|
const forceRecordsPath = this.PATHS.forceRecords(mode);
|
|
2590
|
+
const allSearchKeysAndValues = /* @__PURE__ */ new Map();
|
|
2095
2591
|
const aggregatedRecords = /* @__PURE__ */ new Map();
|
|
2096
|
-
if (
|
|
2097
|
-
const fileStream =
|
|
2592
|
+
if (fs3.existsSync(tempRecordsPath)) {
|
|
2593
|
+
const fileStream = fs3.createReadStream(tempRecordsPath, {
|
|
2098
2594
|
highWaterMark: this.streamHighWaterMark
|
|
2099
2595
|
});
|
|
2100
|
-
const rl =
|
|
2596
|
+
const rl = readline.createInterface({
|
|
2101
2597
|
input: fileStream,
|
|
2102
2598
|
crlfDelay: Infinity
|
|
2103
2599
|
});
|
|
2104
2600
|
for await (const line of rl) {
|
|
2105
2601
|
if (line.trim() === "") continue;
|
|
2106
2602
|
const record = JSON.parse(line);
|
|
2603
|
+
for (const entry of record.search) {
|
|
2604
|
+
if (!allSearchKeysAndValues.has(entry.name)) {
|
|
2605
|
+
allSearchKeysAndValues.set(entry.name, /* @__PURE__ */ new Set());
|
|
2606
|
+
}
|
|
2607
|
+
allSearchKeysAndValues.get(entry.name).add(String(entry.value));
|
|
2608
|
+
}
|
|
2107
2609
|
const key = JSON.stringify(record.search);
|
|
2108
2610
|
let existing = aggregatedRecords.get(key);
|
|
2109
2611
|
if (!existing) {
|
|
@@ -2120,8 +2622,9 @@ ${error.stack}
|
|
|
2120
2622
|
}
|
|
2121
2623
|
}
|
|
2122
2624
|
}
|
|
2123
|
-
|
|
2124
|
-
|
|
2625
|
+
fs3.rmSync(forceRecordsPath, { force: true });
|
|
2626
|
+
fs3.rmSync(this.PATHS.forceKeys(mode), { force: true });
|
|
2627
|
+
const writeStream = fs3.createWriteStream(forceRecordsPath, { encoding: "utf-8" });
|
|
2125
2628
|
writeStream.write("[\n");
|
|
2126
2629
|
let isFirst = true;
|
|
2127
2630
|
for (const record of aggregatedRecords.values()) {
|
|
@@ -2136,13 +2639,17 @@ ${error.stack}
|
|
|
2136
2639
|
await new Promise((resolve) => {
|
|
2137
2640
|
writeStream.on("finish", () => resolve());
|
|
2138
2641
|
});
|
|
2139
|
-
|
|
2642
|
+
const forceJson = Object.fromEntries(
|
|
2643
|
+
Array.from(allSearchKeysAndValues.entries()).sort(([a], [b]) => a.localeCompare(b)).map(([key, values]) => [key, Array.from(values)])
|
|
2644
|
+
);
|
|
2645
|
+
writeFile(this.PATHS.forceKeys(mode), JSON.stringify(forceJson, null, 2));
|
|
2646
|
+
fs3.rmSync(tempRecordsPath, { force: true });
|
|
2140
2647
|
}
|
|
2141
2648
|
writeIndexJson() {
|
|
2142
2649
|
const outputFilePath = this.PATHS.indexJson;
|
|
2143
2650
|
const modes = Object.keys(this.simRunsAmount).map((id) => {
|
|
2144
2651
|
const mode = this.gameConfig.gameModes[id];
|
|
2145
|
-
|
|
2652
|
+
assert5(mode, `Game mode "${id}" not found in game config.`);
|
|
2146
2653
|
return {
|
|
2147
2654
|
name: mode.name,
|
|
2148
2655
|
cost: mode.cost,
|
|
@@ -2152,38 +2659,26 @@ ${error.stack}
|
|
|
2152
2659
|
});
|
|
2153
2660
|
writeFile(outputFilePath, JSON.stringify({ modes }, null, 2));
|
|
2154
2661
|
}
|
|
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
2662
|
/**
|
|
2168
2663
|
* Compiles user configured game to JS for use in different Node processes
|
|
2169
2664
|
*/
|
|
2170
2665
|
preprocessFiles() {
|
|
2171
|
-
const builtFilePath =
|
|
2666
|
+
const builtFilePath = path3.join(
|
|
2172
2667
|
this.gameConfig.rootDir,
|
|
2173
2668
|
this.gameConfig.outputDir,
|
|
2174
2669
|
TEMP_FILENAME
|
|
2175
2670
|
);
|
|
2176
|
-
|
|
2671
|
+
fs3.rmSync(builtFilePath, { force: true });
|
|
2177
2672
|
buildSync({
|
|
2178
2673
|
entryPoints: [this.gameConfig.rootDir],
|
|
2179
2674
|
bundle: true,
|
|
2180
2675
|
platform: "node",
|
|
2181
|
-
outfile:
|
|
2676
|
+
outfile: path3.join(
|
|
2182
2677
|
this.gameConfig.rootDir,
|
|
2183
2678
|
this.gameConfig.outputDir,
|
|
2184
2679
|
TEMP_FILENAME
|
|
2185
2680
|
),
|
|
2186
|
-
external: ["esbuild"]
|
|
2681
|
+
external: ["esbuild", "yargs"]
|
|
2187
2682
|
});
|
|
2188
2683
|
}
|
|
2189
2684
|
getSimRangesForChunks(total, chunks) {
|
|
@@ -2218,27 +2713,62 @@ ${error.stack}
|
|
|
2218
2713
|
}
|
|
2219
2714
|
}
|
|
2220
2715
|
}
|
|
2221
|
-
|
|
2716
|
+
getTimeRemaining(startTime, totalSims) {
|
|
2717
|
+
const elapsedTime = Date.now() - startTime;
|
|
2718
|
+
const simsLeft = totalSims - completedSimulations;
|
|
2719
|
+
const timePerSim = elapsedTime / completedSimulations;
|
|
2720
|
+
const timeRemaining = Math.round(simsLeft * timePerSim / 1e3);
|
|
2721
|
+
return timeRemaining;
|
|
2722
|
+
}
|
|
2723
|
+
async mergeCsv(chunks, outPath, tempName, lutIndexPath) {
|
|
2222
2724
|
try {
|
|
2223
|
-
|
|
2224
|
-
const
|
|
2725
|
+
fs3.rmSync(outPath, { force: true });
|
|
2726
|
+
const lutStream = fs3.createWriteStream(outPath, {
|
|
2225
2727
|
highWaterMark: this.streamHighWaterMark
|
|
2226
2728
|
});
|
|
2729
|
+
const lutIndexStream = lutIndexPath ? fs3.createWriteStream(lutIndexPath, {
|
|
2730
|
+
highWaterMark: this.streamHighWaterMark
|
|
2731
|
+
}) : void 0;
|
|
2732
|
+
let offset = 0n;
|
|
2227
2733
|
for (let i = 0; i < chunks.length; i++) {
|
|
2228
|
-
const
|
|
2229
|
-
if (!
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2734
|
+
const tempLutChunk = path3.join(this.PATHS.base, TEMP_FOLDER, tempName(i));
|
|
2735
|
+
if (!fs3.existsSync(tempLutChunk)) continue;
|
|
2736
|
+
if (lutIndexStream) {
|
|
2737
|
+
const rl = readline.createInterface({
|
|
2738
|
+
input: fs3.createReadStream(tempLutChunk),
|
|
2739
|
+
crlfDelay: Infinity
|
|
2740
|
+
});
|
|
2741
|
+
for await (const line of rl) {
|
|
2742
|
+
if (!line.trim()) continue;
|
|
2743
|
+
const indexBuffer = Buffer.alloc(8);
|
|
2744
|
+
indexBuffer.writeBigUInt64LE(offset);
|
|
2745
|
+
if (!lutIndexStream.write(indexBuffer)) {
|
|
2746
|
+
await new Promise((resolve) => lutIndexStream.once("drain", resolve));
|
|
2747
|
+
}
|
|
2748
|
+
const lineWithNewline = line + "\n";
|
|
2749
|
+
if (!lutStream.write(lineWithNewline)) {
|
|
2750
|
+
await new Promise((resolve) => lutStream.once("drain", resolve));
|
|
2751
|
+
}
|
|
2752
|
+
offset += BigInt(Buffer.byteLength(lineWithNewline, "utf8"));
|
|
2753
|
+
}
|
|
2754
|
+
} else {
|
|
2755
|
+
const tempChunkStream = fs3.createReadStream(tempLutChunk, {
|
|
2756
|
+
highWaterMark: this.streamHighWaterMark
|
|
2757
|
+
});
|
|
2758
|
+
for await (const buf of tempChunkStream) {
|
|
2759
|
+
if (!lutStream.write(buf)) {
|
|
2760
|
+
await new Promise((resolve) => lutStream.once("drain", resolve));
|
|
2761
|
+
}
|
|
2236
2762
|
}
|
|
2237
2763
|
}
|
|
2238
|
-
|
|
2764
|
+
fs3.rmSync(tempLutChunk);
|
|
2239
2765
|
}
|
|
2240
|
-
|
|
2241
|
-
|
|
2766
|
+
lutStream.end();
|
|
2767
|
+
lutIndexStream?.end();
|
|
2768
|
+
await Promise.all([
|
|
2769
|
+
new Promise((resolve) => lutStream.on("finish", resolve)),
|
|
2770
|
+
lutIndexStream ? new Promise((resolve) => lutIndexStream.on("finish", resolve)) : Promise.resolve()
|
|
2771
|
+
]);
|
|
2242
2772
|
} catch (error) {
|
|
2243
2773
|
throw new Error(`Error merging CSV files: ${error.message}`);
|
|
2244
2774
|
}
|
|
@@ -2249,21 +2779,16 @@ ${error.stack}
|
|
|
2249
2779
|
confirmRecords(ctx) {
|
|
2250
2780
|
const recorder = ctx.services.data._getRecorder();
|
|
2251
2781
|
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
|
-
});
|
|
2782
|
+
const key = Object.keys(pendingRecord.properties).sort().map((k) => `${k}:${pendingRecord.properties[k]}`).join("|");
|
|
2783
|
+
let record = recorder.recordsMap.get(key);
|
|
2261
2784
|
if (!record) {
|
|
2785
|
+
const search = Object.entries(pendingRecord.properties).map(([name, value]) => ({ name, value })).sort((a, b) => a.name.localeCompare(b.name));
|
|
2262
2786
|
record = {
|
|
2263
2787
|
search,
|
|
2264
2788
|
timesTriggered: 0,
|
|
2265
2789
|
bookIds: []
|
|
2266
2790
|
};
|
|
2791
|
+
recorder.recordsMap.set(key, record);
|
|
2267
2792
|
recorder.records.push(record);
|
|
2268
2793
|
}
|
|
2269
2794
|
record.timesTriggered++;
|
|
@@ -2273,18 +2798,71 @@ ${error.stack}
|
|
|
2273
2798
|
}
|
|
2274
2799
|
recorder.pendingRecords = [];
|
|
2275
2800
|
}
|
|
2801
|
+
async printSimulationSummary() {
|
|
2802
|
+
Object.entries(this.summary).forEach(([mode, modeSummary]) => {
|
|
2803
|
+
const modeCost = this.gameConfig.gameModes[mode].cost;
|
|
2804
|
+
Object.entries(modeSummary.criteria).forEach(([criteria, criteriaSummary]) => {
|
|
2805
|
+
const totalWins2 = criteriaSummary.bsWins + criteriaSummary.fsWins;
|
|
2806
|
+
const rtp2 = totalWins2 / (criteriaSummary.numSims * modeCost);
|
|
2807
|
+
this.summary[mode].criteria[criteria].rtp = round(rtp2, 4);
|
|
2808
|
+
this.summary[mode].criteria[criteria].bsWins = round(criteriaSummary.bsWins, 4);
|
|
2809
|
+
this.summary[mode].criteria[criteria].fsWins = round(criteriaSummary.fsWins, 4);
|
|
2810
|
+
});
|
|
2811
|
+
const totalWins = modeSummary.total.bsWins + modeSummary.total.fsWins;
|
|
2812
|
+
const rtp = totalWins / (modeSummary.total.numSims * modeCost);
|
|
2813
|
+
this.summary[mode].total.rtp = round(rtp, 4);
|
|
2814
|
+
this.summary[mode].total.bsWins = round(modeSummary.total.bsWins, 4);
|
|
2815
|
+
this.summary[mode].total.fsWins = round(modeSummary.total.fsWins, 4);
|
|
2816
|
+
});
|
|
2817
|
+
const maxLineLength = 50;
|
|
2818
|
+
let output = chalk2.green.bold("\nSimulation Summary\n");
|
|
2819
|
+
for (const [mode, modeSummary] of Object.entries(this.summary)) {
|
|
2820
|
+
output += "-".repeat(maxLineLength) + "\n\n";
|
|
2821
|
+
output += chalk2.bold.bgWhite(`Mode: ${mode}
|
|
2822
|
+
`);
|
|
2823
|
+
output += `Simulations: ${modeSummary.total.numSims}
|
|
2824
|
+
`;
|
|
2825
|
+
output += `Basegame Wins: ${modeSummary.total.bsWins}
|
|
2826
|
+
`;
|
|
2827
|
+
output += `Freespins Wins: ${modeSummary.total.fsWins}
|
|
2828
|
+
`;
|
|
2829
|
+
output += `RTP (unoptimized): ${modeSummary.total.rtp}
|
|
2830
|
+
`;
|
|
2831
|
+
output += chalk2.bold("\n Result Set Summary:\n");
|
|
2832
|
+
for (const [criteria, criteriaSummary] of Object.entries(modeSummary.criteria)) {
|
|
2833
|
+
output += chalk2.gray(" " + "-".repeat(maxLineLength - 4)) + "\n";
|
|
2834
|
+
output += chalk2.bold(` Criteria: ${criteria}
|
|
2835
|
+
`);
|
|
2836
|
+
output += ` Simulations: ${criteriaSummary.numSims}
|
|
2837
|
+
`;
|
|
2838
|
+
output += ` Basegame Wins: ${criteriaSummary.bsWins}
|
|
2839
|
+
`;
|
|
2840
|
+
output += ` Freespins Wins: ${criteriaSummary.fsWins}
|
|
2841
|
+
`;
|
|
2842
|
+
output += ` RTP (unoptimized): ${criteriaSummary.rtp}
|
|
2843
|
+
`;
|
|
2844
|
+
}
|
|
2845
|
+
}
|
|
2846
|
+
console.log(output);
|
|
2847
|
+
writeFile(this.PATHS.simulationSummary, JSON.stringify(this.summary, null, 2));
|
|
2848
|
+
if (this.socket && this.panelActive) {
|
|
2849
|
+
this.socket.emit("simulationSummary", {
|
|
2850
|
+
summary: this.summary
|
|
2851
|
+
});
|
|
2852
|
+
}
|
|
2853
|
+
}
|
|
2276
2854
|
};
|
|
2277
2855
|
|
|
2278
2856
|
// src/analysis/index.ts
|
|
2279
|
-
import
|
|
2280
|
-
import
|
|
2281
|
-
import assert8 from "assert";
|
|
2857
|
+
import fs4 from "fs";
|
|
2858
|
+
import assert6 from "assert";
|
|
2282
2859
|
|
|
2283
2860
|
// src/analysis/utils.ts
|
|
2284
2861
|
function parseLookupTable(content) {
|
|
2285
2862
|
const lines = content.trim().split("\n");
|
|
2286
2863
|
const lut = [];
|
|
2287
2864
|
for (const line of lines) {
|
|
2865
|
+
if (!line.trim()) continue;
|
|
2288
2866
|
const [indexStr, weightStr, payoutStr] = line.split(",");
|
|
2289
2867
|
const index = parseInt(indexStr.trim());
|
|
2290
2868
|
const weight = parseInt(weightStr.trim());
|
|
@@ -2297,11 +2875,12 @@ function parseLookupTableSegmented(content) {
|
|
|
2297
2875
|
const lines = content.trim().split("\n");
|
|
2298
2876
|
const lut = [];
|
|
2299
2877
|
for (const line of lines) {
|
|
2300
|
-
|
|
2878
|
+
if (!line.trim()) continue;
|
|
2879
|
+
const [indexStr, criteria, bsWinsStr, fsWinsStr] = line.split(",");
|
|
2301
2880
|
const index = parseInt(indexStr.trim());
|
|
2302
|
-
const
|
|
2303
|
-
const
|
|
2304
|
-
lut.push([index, criteria,
|
|
2881
|
+
const bsWins = parseFloat(bsWinsStr.trim());
|
|
2882
|
+
const fsWins = parseFloat(fsWinsStr.trim());
|
|
2883
|
+
lut.push([index, criteria, bsWins, fsWins]);
|
|
2305
2884
|
}
|
|
2306
2885
|
return lut;
|
|
2307
2886
|
}
|
|
@@ -2335,19 +2914,19 @@ function getPayoutWeights(lut, opts = {}) {
|
|
|
2335
2914
|
function getNonZeroHitrate(payoutWeights) {
|
|
2336
2915
|
const totalWeight = getTotalWeight(payoutWeights);
|
|
2337
2916
|
if (Math.min(...Object.keys(payoutWeights).map(Number)) == 0) {
|
|
2338
|
-
return totalWeight / (totalWeight - (payoutWeights[0] ?? 0) / totalWeight);
|
|
2917
|
+
return round(totalWeight / (totalWeight - (payoutWeights[0] ?? 0) / totalWeight), 4);
|
|
2339
2918
|
} else {
|
|
2340
2919
|
return 1;
|
|
2341
2920
|
}
|
|
2342
2921
|
}
|
|
2343
2922
|
function getNullHitrate(payoutWeights) {
|
|
2344
|
-
return payoutWeights[0] ?? 0;
|
|
2923
|
+
return round(payoutWeights[0] ?? 0, 4);
|
|
2345
2924
|
}
|
|
2346
2925
|
function getMaxwinHitrate(payoutWeights) {
|
|
2347
2926
|
const totalWeight = getTotalWeight(payoutWeights);
|
|
2348
2927
|
const maxWin = Math.max(...Object.keys(payoutWeights).map(Number));
|
|
2349
2928
|
const hitRate = (payoutWeights[maxWin] || 0) / totalWeight;
|
|
2350
|
-
return 1 / hitRate;
|
|
2929
|
+
return round(1 / hitRate, 4);
|
|
2351
2930
|
}
|
|
2352
2931
|
function getUniquePayouts(payoutWeights) {
|
|
2353
2932
|
return Object.keys(payoutWeights).length;
|
|
@@ -2366,15 +2945,15 @@ function getAvgWin(payoutWeights) {
|
|
|
2366
2945
|
const payout = parseFloat(payoutStr);
|
|
2367
2946
|
avgWin += payout * weight;
|
|
2368
2947
|
}
|
|
2369
|
-
return avgWin;
|
|
2948
|
+
return round(avgWin, 4);
|
|
2370
2949
|
}
|
|
2371
2950
|
function getRtp(payoutWeights, cost) {
|
|
2372
2951
|
const avgWin = getAvgWin(payoutWeights);
|
|
2373
|
-
return avgWin / cost;
|
|
2952
|
+
return round(avgWin / cost, 4);
|
|
2374
2953
|
}
|
|
2375
2954
|
function getStandardDeviation(payoutWeights) {
|
|
2376
2955
|
const variance = getVariance(payoutWeights);
|
|
2377
|
-
return Math.sqrt(variance);
|
|
2956
|
+
return round(Math.sqrt(variance), 4);
|
|
2378
2957
|
}
|
|
2379
2958
|
function getVariance(payoutWeights) {
|
|
2380
2959
|
const totalWeight = getTotalWeight(payoutWeights);
|
|
@@ -2384,7 +2963,7 @@ function getVariance(payoutWeights) {
|
|
|
2384
2963
|
const payout = parseFloat(payoutStr);
|
|
2385
2964
|
variance += Math.pow(payout - avgWin, 2) * (weight / totalWeight);
|
|
2386
2965
|
}
|
|
2387
|
-
return variance;
|
|
2966
|
+
return round(variance, 4);
|
|
2388
2967
|
}
|
|
2389
2968
|
function getLessBetHitrate(payoutWeights, cost) {
|
|
2390
2969
|
let lessBetWeight = 0;
|
|
@@ -2395,80 +2974,29 @@ function getLessBetHitrate(payoutWeights, cost) {
|
|
|
2395
2974
|
lessBetWeight += weight;
|
|
2396
2975
|
}
|
|
2397
2976
|
}
|
|
2398
|
-
return lessBetWeight / totalWeight;
|
|
2977
|
+
return round(lessBetWeight / totalWeight, 4);
|
|
2399
2978
|
}
|
|
2400
2979
|
|
|
2401
2980
|
// src/analysis/index.ts
|
|
2402
|
-
import { isMainThread as
|
|
2981
|
+
import { isMainThread as isMainThread3 } from "worker_threads";
|
|
2403
2982
|
var Analysis = class {
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
constructor(optimizer) {
|
|
2408
|
-
this.gameConfig = optimizer.getGameConfig();
|
|
2409
|
-
this.optimizerConfig = optimizer.getOptimizerGameModes();
|
|
2410
|
-
this.filePaths = {};
|
|
2983
|
+
game;
|
|
2984
|
+
constructor(game) {
|
|
2985
|
+
this.game = game;
|
|
2411
2986
|
}
|
|
2412
2987
|
async runAnalysis(gameModes) {
|
|
2413
|
-
if (!
|
|
2414
|
-
this.filePaths = this.getPathsForModes(gameModes);
|
|
2988
|
+
if (!isMainThread3) return;
|
|
2415
2989
|
this.getNumberStats(gameModes);
|
|
2416
2990
|
this.getWinRanges(gameModes);
|
|
2417
2991
|
console.log("Analysis complete. Files written to build directory.");
|
|
2418
2992
|
}
|
|
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
2993
|
getNumberStats(gameModes) {
|
|
2994
|
+
const meta = this.game.getMetadata();
|
|
2467
2995
|
const stats = [];
|
|
2468
2996
|
for (const modeStr of gameModes) {
|
|
2469
2997
|
const mode = this.getGameModeConfig(modeStr);
|
|
2470
2998
|
const lutOptimized = parseLookupTable(
|
|
2471
|
-
|
|
2999
|
+
fs4.readFileSync(meta.paths.lookupTablePublish(modeStr), "utf-8")
|
|
2472
3000
|
);
|
|
2473
3001
|
const totalWeight = getTotalLutWeight(lutOptimized);
|
|
2474
3002
|
const payoutWeights = getPayoutWeights(lutOptimized);
|
|
@@ -2488,10 +3016,7 @@ var Analysis = class {
|
|
|
2488
3016
|
uniquePayouts: getUniquePayouts(payoutWeights)
|
|
2489
3017
|
});
|
|
2490
3018
|
}
|
|
2491
|
-
writeJsonFile(
|
|
2492
|
-
path2.join(this.gameConfig.rootDir, this.gameConfig.outputDir, "stats_summary.json"),
|
|
2493
|
-
stats
|
|
2494
|
-
);
|
|
3019
|
+
writeJsonFile(meta.paths.statsSummary, stats);
|
|
2495
3020
|
}
|
|
2496
3021
|
getWinRanges(gameModes) {
|
|
2497
3022
|
const winRanges = [
|
|
@@ -2514,14 +3039,31 @@ var Analysis = class {
|
|
|
2514
3039
|
[7500, 9999.99],
|
|
2515
3040
|
[1e4, 14999.99],
|
|
2516
3041
|
[15e3, 19999.99],
|
|
2517
|
-
[2e4, 24999.99]
|
|
3042
|
+
[2e4, 24999.99],
|
|
3043
|
+
[25e3, 49999.99],
|
|
3044
|
+
[5e4, 74999.99],
|
|
3045
|
+
[75e3, 99999.99],
|
|
3046
|
+
[1e5, Infinity]
|
|
2518
3047
|
];
|
|
2519
|
-
const payoutRanges =
|
|
3048
|
+
const payoutRanges = [];
|
|
3049
|
+
const meta = this.game.getMetadata();
|
|
2520
3050
|
for (const modeStr of gameModes) {
|
|
2521
|
-
payoutRanges[modeStr] = { overall: {}, criteria: {} };
|
|
2522
3051
|
const lutSegmented = parseLookupTableSegmented(
|
|
2523
|
-
|
|
3052
|
+
fs4.readFileSync(meta.paths.lookupTableSegmented(modeStr), "utf-8")
|
|
2524
3053
|
);
|
|
3054
|
+
const range = {
|
|
3055
|
+
gameMode: modeStr,
|
|
3056
|
+
allPayouts: {
|
|
3057
|
+
overall: {},
|
|
3058
|
+
criteria: {}
|
|
3059
|
+
},
|
|
3060
|
+
uniquePayouts: {
|
|
3061
|
+
overall: {},
|
|
3062
|
+
criteria: {}
|
|
3063
|
+
}
|
|
3064
|
+
};
|
|
3065
|
+
const uniquePayoutsOverall = /* @__PURE__ */ new Map();
|
|
3066
|
+
const uniquePayoutsCriteria = /* @__PURE__ */ new Map();
|
|
2525
3067
|
lutSegmented.forEach(([, criteria, bp, fsp]) => {
|
|
2526
3068
|
const basePayout = bp;
|
|
2527
3069
|
const freeSpinPayout = fsp;
|
|
@@ -2529,32 +3071,54 @@ var Analysis = class {
|
|
|
2529
3071
|
for (const [min, max] of winRanges) {
|
|
2530
3072
|
if (payout >= min && payout <= max) {
|
|
2531
3073
|
const rangeKey = `${min}-${max}`;
|
|
2532
|
-
if (!
|
|
2533
|
-
|
|
3074
|
+
if (!range.allPayouts.overall[rangeKey]) {
|
|
3075
|
+
range.allPayouts.overall[rangeKey] = 0;
|
|
3076
|
+
}
|
|
3077
|
+
range.allPayouts.overall[rangeKey] += 1;
|
|
3078
|
+
if (!range.allPayouts.criteria[criteria]) {
|
|
3079
|
+
range.allPayouts.criteria[criteria] = {};
|
|
3080
|
+
}
|
|
3081
|
+
if (!range.allPayouts.criteria[criteria][rangeKey]) {
|
|
3082
|
+
range.allPayouts.criteria[criteria][rangeKey] = 0;
|
|
2534
3083
|
}
|
|
2535
|
-
|
|
2536
|
-
if (!
|
|
2537
|
-
|
|
3084
|
+
range.allPayouts.criteria[criteria][rangeKey] += 1;
|
|
3085
|
+
if (!uniquePayoutsOverall.has(rangeKey)) {
|
|
3086
|
+
uniquePayoutsOverall.set(rangeKey, /* @__PURE__ */ new Set());
|
|
2538
3087
|
}
|
|
2539
|
-
|
|
2540
|
-
|
|
3088
|
+
uniquePayoutsOverall.get(rangeKey).add(payout);
|
|
3089
|
+
if (!uniquePayoutsCriteria.has(criteria)) {
|
|
3090
|
+
uniquePayoutsCriteria.set(criteria, /* @__PURE__ */ new Map());
|
|
2541
3091
|
}
|
|
2542
|
-
|
|
3092
|
+
if (!uniquePayoutsCriteria.get(criteria).has(rangeKey)) {
|
|
3093
|
+
uniquePayoutsCriteria.get(criteria).set(rangeKey, /* @__PURE__ */ new Set());
|
|
3094
|
+
}
|
|
3095
|
+
uniquePayoutsCriteria.get(criteria).get(rangeKey).add(payout);
|
|
2543
3096
|
break;
|
|
2544
3097
|
}
|
|
2545
3098
|
}
|
|
2546
3099
|
});
|
|
2547
|
-
|
|
2548
|
-
|
|
3100
|
+
uniquePayoutsOverall.forEach((payoutSet, rangeKey) => {
|
|
3101
|
+
range.uniquePayouts.overall[rangeKey] = payoutSet.size;
|
|
3102
|
+
});
|
|
3103
|
+
uniquePayoutsCriteria.forEach((rangeMap, criteria) => {
|
|
3104
|
+
if (!range.uniquePayouts.criteria[criteria]) {
|
|
3105
|
+
range.uniquePayouts.criteria[criteria] = {};
|
|
3106
|
+
}
|
|
3107
|
+
rangeMap.forEach((payoutSet, rangeKey) => {
|
|
3108
|
+
range.uniquePayouts.criteria[criteria][rangeKey] = payoutSet.size;
|
|
3109
|
+
});
|
|
3110
|
+
});
|
|
3111
|
+
const orderedAllOverall = {};
|
|
3112
|
+
Object.keys(range.allPayouts.overall).sort((a, b) => {
|
|
2549
3113
|
const [aMin] = a.split("-").map(Number);
|
|
2550
3114
|
const [bMin] = b.split("-").map(Number);
|
|
2551
3115
|
return aMin - bMin;
|
|
2552
3116
|
}).forEach((key) => {
|
|
2553
|
-
|
|
3117
|
+
orderedAllOverall[key] = range.allPayouts.overall[key];
|
|
2554
3118
|
});
|
|
2555
|
-
const
|
|
2556
|
-
Object.keys(
|
|
2557
|
-
const critMap =
|
|
3119
|
+
const orderedAllCriteria = {};
|
|
3120
|
+
Object.keys(range.allPayouts.criteria).forEach((crit) => {
|
|
3121
|
+
const critMap = range.allPayouts.criteria[crit];
|
|
2558
3122
|
const orderedCritMap = {};
|
|
2559
3123
|
Object.keys(critMap).sort((a, b) => {
|
|
2560
3124
|
const [aMin] = a.split("-").map(Number);
|
|
@@ -2563,35 +3127,61 @@ var Analysis = class {
|
|
|
2563
3127
|
}).forEach((key) => {
|
|
2564
3128
|
orderedCritMap[key] = critMap[key];
|
|
2565
3129
|
});
|
|
2566
|
-
|
|
3130
|
+
orderedAllCriteria[crit] = orderedCritMap;
|
|
3131
|
+
});
|
|
3132
|
+
const orderedUniqueOverall = {};
|
|
3133
|
+
Object.keys(range.uniquePayouts.overall).sort((a, b) => {
|
|
3134
|
+
const [aMin] = a.split("-").map(Number);
|
|
3135
|
+
const [bMin] = b.split("-").map(Number);
|
|
3136
|
+
return aMin - bMin;
|
|
3137
|
+
}).forEach((key) => {
|
|
3138
|
+
orderedUniqueOverall[key] = range.uniquePayouts.overall[key];
|
|
3139
|
+
});
|
|
3140
|
+
const orderedUniqueCriteria = {};
|
|
3141
|
+
Object.keys(range.uniquePayouts.criteria).forEach((crit) => {
|
|
3142
|
+
const critMap = range.uniquePayouts.criteria[crit];
|
|
3143
|
+
const orderedCritMap = {};
|
|
3144
|
+
Object.keys(critMap).sort((a, b) => {
|
|
3145
|
+
const [aMin] = a.split("-").map(Number);
|
|
3146
|
+
const [bMin] = b.split("-").map(Number);
|
|
3147
|
+
return aMin - bMin;
|
|
3148
|
+
}).forEach((key) => {
|
|
3149
|
+
orderedCritMap[key] = critMap[key];
|
|
3150
|
+
});
|
|
3151
|
+
orderedUniqueCriteria[crit] = orderedCritMap;
|
|
3152
|
+
});
|
|
3153
|
+
payoutRanges.push({
|
|
3154
|
+
gameMode: modeStr,
|
|
3155
|
+
allPayouts: {
|
|
3156
|
+
overall: orderedAllOverall,
|
|
3157
|
+
criteria: orderedAllCriteria
|
|
3158
|
+
},
|
|
3159
|
+
uniquePayouts: {
|
|
3160
|
+
overall: orderedUniqueOverall,
|
|
3161
|
+
criteria: orderedUniqueCriteria
|
|
3162
|
+
}
|
|
2567
3163
|
});
|
|
2568
|
-
payoutRanges[modeStr] = {
|
|
2569
|
-
overall: orderedOverall,
|
|
2570
|
-
criteria: {}
|
|
2571
|
-
};
|
|
2572
3164
|
}
|
|
2573
|
-
writeJsonFile(
|
|
2574
|
-
path2.join(this.gameConfig.rootDir, this.gameConfig.outputDir, "stats_payouts.json"),
|
|
2575
|
-
payoutRanges
|
|
2576
|
-
);
|
|
3165
|
+
writeJsonFile(meta.paths.statsPayouts, payoutRanges);
|
|
2577
3166
|
}
|
|
2578
3167
|
getGameModeConfig(mode) {
|
|
2579
|
-
const config = this.
|
|
2580
|
-
|
|
3168
|
+
const config = this.game.getConfig().gameModes[mode];
|
|
3169
|
+
assert6(config, `Game mode "${mode}" not found in game config`);
|
|
2581
3170
|
return config;
|
|
2582
3171
|
}
|
|
2583
3172
|
};
|
|
2584
3173
|
|
|
2585
3174
|
// src/optimizer/index.ts
|
|
2586
|
-
import
|
|
2587
|
-
import
|
|
3175
|
+
import path6 from "path";
|
|
3176
|
+
import assert8 from "assert";
|
|
2588
3177
|
import { spawn } from "child_process";
|
|
2589
|
-
import { isMainThread as
|
|
3178
|
+
import { isMainThread as isMainThread4 } from "worker_threads";
|
|
2590
3179
|
|
|
2591
3180
|
// src/utils/math-config.ts
|
|
2592
|
-
import
|
|
3181
|
+
import path4 from "path";
|
|
2593
3182
|
function makeMathConfig(optimizer, opts = {}) {
|
|
2594
3183
|
const game = optimizer.getGameConfig();
|
|
3184
|
+
const meta = optimizer.getGameMeta();
|
|
2595
3185
|
const gameModesCfg = optimizer.getOptimizerGameModes();
|
|
2596
3186
|
const { writeToFile } = opts;
|
|
2597
3187
|
const isDefined = (v) => v !== void 0;
|
|
@@ -2633,16 +3223,17 @@ function makeMathConfig(optimizer, opts = {}) {
|
|
|
2633
3223
|
}))
|
|
2634
3224
|
};
|
|
2635
3225
|
if (writeToFile) {
|
|
2636
|
-
const outPath =
|
|
3226
|
+
const outPath = path4.join(meta.rootDir, meta.outputDir, "math_config.json");
|
|
2637
3227
|
writeJsonFile(outPath, config);
|
|
2638
3228
|
}
|
|
2639
3229
|
return config;
|
|
2640
3230
|
}
|
|
2641
3231
|
|
|
2642
3232
|
// src/utils/setup-file.ts
|
|
2643
|
-
import
|
|
3233
|
+
import path5 from "path";
|
|
2644
3234
|
function makeSetupFile(optimizer, gameMode) {
|
|
2645
3235
|
const gameConfig = optimizer.getGameConfig();
|
|
3236
|
+
const gameMeta = optimizer.getGameMeta();
|
|
2646
3237
|
const optimizerGameModes = optimizer.getOptimizerGameModes();
|
|
2647
3238
|
const modeConfig = optimizerGameModes[gameMode];
|
|
2648
3239
|
if (!modeConfig) {
|
|
@@ -2676,16 +3267,16 @@ function makeSetupFile(optimizer, gameMode) {
|
|
|
2676
3267
|
`;
|
|
2677
3268
|
content += `simulation_trials;${params.simulationTrials}
|
|
2678
3269
|
`;
|
|
2679
|
-
content += `user_game_build_path;${
|
|
3270
|
+
content += `user_game_build_path;${path5.join(gameMeta.rootDir, gameMeta.outputDir)}
|
|
2680
3271
|
`;
|
|
2681
3272
|
content += `pmb_rtp;${params.pmbRtp}
|
|
2682
3273
|
`;
|
|
2683
|
-
const outPath =
|
|
3274
|
+
const outPath = path5.join(__dirname, "./optimizer-rust/src", "setup.txt");
|
|
2684
3275
|
writeFile(outPath, content);
|
|
2685
3276
|
}
|
|
2686
3277
|
|
|
2687
3278
|
// src/optimizer/OptimizationConditions.ts
|
|
2688
|
-
import
|
|
3279
|
+
import assert7 from "assert";
|
|
2689
3280
|
var OptimizationConditions = class {
|
|
2690
3281
|
rtp;
|
|
2691
3282
|
avgWin;
|
|
@@ -2696,14 +3287,14 @@ var OptimizationConditions = class {
|
|
|
2696
3287
|
constructor(opts) {
|
|
2697
3288
|
let { rtp, avgWin, hitRate, searchConditions, priority } = opts;
|
|
2698
3289
|
if (rtp == void 0 || rtp === "x") {
|
|
2699
|
-
|
|
3290
|
+
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
3291
|
rtp = Math.round(avgWin / Number(hitRate) * 1e5) / 1e5;
|
|
2701
3292
|
}
|
|
2702
3293
|
let noneCount = 0;
|
|
2703
3294
|
for (const val of [rtp, avgWin, hitRate]) {
|
|
2704
3295
|
if (val === void 0) noneCount++;
|
|
2705
3296
|
}
|
|
2706
|
-
|
|
3297
|
+
assert7(noneCount <= 1, "Invalid combination of optimization conditions.");
|
|
2707
3298
|
this.searchRange = [-1, -1];
|
|
2708
3299
|
this.forceSearch = {};
|
|
2709
3300
|
if (typeof searchConditions === "number") {
|
|
@@ -2784,9 +3375,11 @@ var OptimizationParameters = class _OptimizationParameters {
|
|
|
2784
3375
|
// src/optimizer/index.ts
|
|
2785
3376
|
var Optimizer = class {
|
|
2786
3377
|
gameConfig;
|
|
3378
|
+
gameMeta;
|
|
2787
3379
|
gameModes;
|
|
2788
3380
|
constructor(opts) {
|
|
2789
3381
|
this.gameConfig = opts.game.getConfig();
|
|
3382
|
+
this.gameMeta = opts.game.getMetadata();
|
|
2790
3383
|
this.gameModes = opts.gameModes;
|
|
2791
3384
|
this.verifyConfig();
|
|
2792
3385
|
}
|
|
@@ -2794,11 +3387,15 @@ var Optimizer = class {
|
|
|
2794
3387
|
* Runs the optimization process, and runs analysis after.
|
|
2795
3388
|
*/
|
|
2796
3389
|
async runOptimization({ gameModes }) {
|
|
2797
|
-
if (!
|
|
3390
|
+
if (!isMainThread4) return;
|
|
2798
3391
|
const mathConfig = makeMathConfig(this, { writeToFile: true });
|
|
2799
3392
|
for (const mode of gameModes) {
|
|
2800
3393
|
const setupFile = makeSetupFile(this, mode);
|
|
2801
3394
|
await this.runSingleOptimization();
|
|
3395
|
+
await makeLutIndexFromPublishLut(
|
|
3396
|
+
this.gameMeta.paths.lookupTablePublish(mode),
|
|
3397
|
+
this.gameMeta.paths.lookupTableIndex(mode)
|
|
3398
|
+
);
|
|
2802
3399
|
}
|
|
2803
3400
|
console.log("Optimization complete. Files written to build directory.");
|
|
2804
3401
|
}
|
|
@@ -2831,7 +3428,7 @@ var Optimizer = class {
|
|
|
2831
3428
|
}
|
|
2832
3429
|
gameModeRtp = Math.round(gameModeRtp * 1e3) / 1e3;
|
|
2833
3430
|
paramRtp = Math.round(paramRtp * 1e3) / 1e3;
|
|
2834
|
-
|
|
3431
|
+
assert8(
|
|
2835
3432
|
gameModeRtp === paramRtp,
|
|
2836
3433
|
`Sum of all RTP conditions (${paramRtp}) does not match the game mode RTP (${gameModeRtp}) in game mode "${k}".`
|
|
2837
3434
|
);
|
|
@@ -2840,6 +3437,9 @@ var Optimizer = class {
|
|
|
2840
3437
|
getGameConfig() {
|
|
2841
3438
|
return this.gameConfig;
|
|
2842
3439
|
}
|
|
3440
|
+
getGameMeta() {
|
|
3441
|
+
return this.gameMeta;
|
|
3442
|
+
}
|
|
2843
3443
|
getOptimizerGameModes() {
|
|
2844
3444
|
return this.gameModes;
|
|
2845
3445
|
}
|
|
@@ -2848,7 +3448,7 @@ async function rustProgram(...args) {
|
|
|
2848
3448
|
console.log("Starting Rust optimizer. This may take a while...");
|
|
2849
3449
|
return new Promise((resolve, reject) => {
|
|
2850
3450
|
const task = spawn("cargo", ["run", "-q", "--release", ...args], {
|
|
2851
|
-
cwd:
|
|
3451
|
+
cwd: path6.join(__dirname, "./optimizer-rust"),
|
|
2852
3452
|
stdio: "pipe"
|
|
2853
3453
|
});
|
|
2854
3454
|
task.on("error", (error) => {
|
|
@@ -2875,8 +3475,8 @@ async function rustProgram(...args) {
|
|
|
2875
3475
|
}
|
|
2876
3476
|
|
|
2877
3477
|
// src/slot-game/index.ts
|
|
2878
|
-
import { isMainThread as
|
|
2879
|
-
var SlotGame = class {
|
|
3478
|
+
import { isMainThread as isMainThread5, workerData as workerData2 } from "worker_threads";
|
|
3479
|
+
var SlotGame = class _SlotGame {
|
|
2880
3480
|
configOpts;
|
|
2881
3481
|
simulation;
|
|
2882
3482
|
optimizer;
|
|
@@ -2926,38 +3526,56 @@ var SlotGame = class {
|
|
|
2926
3526
|
/**
|
|
2927
3527
|
* Runs the analysis based on the configured settings.
|
|
2928
3528
|
*/
|
|
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);
|
|
3529
|
+
runAnalysis(opts) {
|
|
3530
|
+
this.analyzer = new Analysis(this);
|
|
3531
|
+
this.analyzer.runAnalysis(opts.gameModes);
|
|
2937
3532
|
}
|
|
2938
3533
|
/**
|
|
2939
3534
|
* Runs the configured tasks: simulation, optimization, and/or analysis.
|
|
2940
3535
|
*/
|
|
2941
3536
|
async runTasks(opts = {}) {
|
|
3537
|
+
if (isMainThread5 && !opts._internal_ignore_args) {
|
|
3538
|
+
const [{ default: yargs }, { hideBin }] = await Promise.all([
|
|
3539
|
+
import("yargs"),
|
|
3540
|
+
import("yargs/helpers")
|
|
3541
|
+
]);
|
|
3542
|
+
const argvParser = yargs(hideBin(process.argv)).options({
|
|
3543
|
+
[CLI_ARGS.RUN]: { type: "boolean", default: false }
|
|
3544
|
+
});
|
|
3545
|
+
const argv = await argvParser.parse();
|
|
3546
|
+
if (!argv[CLI_ARGS.RUN]) return;
|
|
3547
|
+
}
|
|
3548
|
+
if (!isMainThread5 && workerData2 && typeof workerData2 === "object" && "simStart" in workerData2) {
|
|
3549
|
+
opts.doSimulation = true;
|
|
3550
|
+
}
|
|
2942
3551
|
if (!opts.doSimulation && !opts.doOptimization && !opts.doAnalysis) {
|
|
2943
3552
|
console.log("No tasks to run. Enable either simulation, optimization or analysis.");
|
|
2944
3553
|
}
|
|
2945
3554
|
if (opts.doSimulation) {
|
|
2946
3555
|
await this.runSimulation(opts.simulationOpts || {});
|
|
2947
3556
|
}
|
|
3557
|
+
if (opts.doAnalysis) {
|
|
3558
|
+
this.runAnalysis(opts.analysisOpts || { gameModes: [] });
|
|
3559
|
+
}
|
|
2948
3560
|
if (opts.doOptimization) {
|
|
2949
3561
|
await this.runOptimization(opts.optimizationOpts || { gameModes: [] });
|
|
3562
|
+
if (opts.doAnalysis) {
|
|
3563
|
+
this.runAnalysis(opts.analysisOpts || { gameModes: [] });
|
|
3564
|
+
}
|
|
2950
3565
|
}
|
|
2951
|
-
if (
|
|
2952
|
-
await this.runAnalysis(opts.analysisOpts || { gameModes: [] });
|
|
2953
|
-
}
|
|
2954
|
-
if (isMainThread4) console.log("Finishing up...");
|
|
3566
|
+
if (isMainThread5) console.log("Done!");
|
|
2955
3567
|
}
|
|
2956
3568
|
/**
|
|
2957
3569
|
* Gets the game configuration.
|
|
2958
3570
|
*/
|
|
2959
3571
|
getConfig() {
|
|
2960
|
-
return createGameConfig(this.configOpts);
|
|
3572
|
+
return createGameConfig(this.configOpts).config;
|
|
3573
|
+
}
|
|
3574
|
+
getMetadata() {
|
|
3575
|
+
return createGameConfig(this.configOpts).metadata;
|
|
3576
|
+
}
|
|
3577
|
+
clone() {
|
|
3578
|
+
return new _SlotGame(this.configOpts);
|
|
2961
3579
|
}
|
|
2962
3580
|
};
|
|
2963
3581
|
|
|
@@ -2970,7 +3588,7 @@ var defineSymbols = (symbols) => symbols;
|
|
|
2970
3588
|
var defineGameModes = (gameModes) => gameModes;
|
|
2971
3589
|
|
|
2972
3590
|
// src/game-mode/index.ts
|
|
2973
|
-
import
|
|
3591
|
+
import assert9 from "assert";
|
|
2974
3592
|
var GameMode = class {
|
|
2975
3593
|
name;
|
|
2976
3594
|
_reelsAmount;
|
|
@@ -2993,12 +3611,12 @@ var GameMode = class {
|
|
|
2993
3611
|
this.reelSets = opts.reelSets;
|
|
2994
3612
|
this.resultSets = opts.resultSets;
|
|
2995
3613
|
this.isBonusBuy = opts.isBonusBuy;
|
|
2996
|
-
|
|
2997
|
-
|
|
3614
|
+
assert9(this.rtp >= 0.9 && this.rtp <= 0.99, "RTP must be between 0.9 and 0.99");
|
|
3615
|
+
assert9(
|
|
2998
3616
|
this.symbolsPerReel.length === this.reelsAmount,
|
|
2999
3617
|
"symbolsPerReel length must match reelsAmount."
|
|
3000
3618
|
);
|
|
3001
|
-
|
|
3619
|
+
assert9(this.reelSets.length > 0, "GameMode must have at least one ReelSet defined.");
|
|
3002
3620
|
}
|
|
3003
3621
|
/**
|
|
3004
3622
|
* Intended for internal use only.
|
|
@@ -3011,7 +3629,7 @@ var GameMode = class {
|
|
|
3011
3629
|
* Intended for internal use only.
|
|
3012
3630
|
*/
|
|
3013
3631
|
_setSymbolsPerReel(symbolsPerReel) {
|
|
3014
|
-
|
|
3632
|
+
assert9(
|
|
3015
3633
|
symbolsPerReel.length === this._reelsAmount,
|
|
3016
3634
|
"symbolsPerReel length must match reelsAmount."
|
|
3017
3635
|
);
|
|
@@ -3077,7 +3695,7 @@ var WinType = class {
|
|
|
3077
3695
|
};
|
|
3078
3696
|
|
|
3079
3697
|
// src/win-types/LinesWinType.ts
|
|
3080
|
-
import
|
|
3698
|
+
import assert10 from "assert";
|
|
3081
3699
|
var LinesWinType = class extends WinType {
|
|
3082
3700
|
lines;
|
|
3083
3701
|
constructor(opts) {
|
|
@@ -3131,8 +3749,8 @@ var LinesWinType = class extends WinType {
|
|
|
3131
3749
|
if (!baseSymbol) {
|
|
3132
3750
|
baseSymbol = thisSymbol;
|
|
3133
3751
|
}
|
|
3134
|
-
|
|
3135
|
-
|
|
3752
|
+
assert10(baseSymbol, `No symbol found at line ${lineNum}, reel ${ridx}`);
|
|
3753
|
+
assert10(thisSymbol, `No symbol found at line ${lineNum}, reel ${ridx}`);
|
|
3136
3754
|
if (potentialWinLine.length == 0) {
|
|
3137
3755
|
if (this.isWild(thisSymbol)) {
|
|
3138
3756
|
potentialWildLine.push({ reel: ridx, row: sidx, symbol: thisSymbol });
|
|
@@ -3447,13 +4065,13 @@ var ManywaysWinType = class extends WinType {
|
|
|
3447
4065
|
};
|
|
3448
4066
|
|
|
3449
4067
|
// src/reel-set/GeneratedReelSet.ts
|
|
3450
|
-
import
|
|
3451
|
-
import
|
|
3452
|
-
import { isMainThread as
|
|
4068
|
+
import fs6 from "fs";
|
|
4069
|
+
import path8 from "path";
|
|
4070
|
+
import { isMainThread as isMainThread6 } from "worker_threads";
|
|
3453
4071
|
|
|
3454
4072
|
// src/reel-set/index.ts
|
|
3455
|
-
import
|
|
3456
|
-
import
|
|
4073
|
+
import fs5 from "fs";
|
|
4074
|
+
import path7 from "path";
|
|
3457
4075
|
var ReelSet = class {
|
|
3458
4076
|
id;
|
|
3459
4077
|
associatedGameModeName;
|
|
@@ -3473,11 +4091,11 @@ var ReelSet = class {
|
|
|
3473
4091
|
* Reads a reelset CSV file and returns the reels as arrays of GameSymbols.
|
|
3474
4092
|
*/
|
|
3475
4093
|
parseReelsetCSV(reelSetPath, config) {
|
|
3476
|
-
if (!
|
|
4094
|
+
if (!fs5.existsSync(reelSetPath)) {
|
|
3477
4095
|
throw new Error(`Reelset CSV file not found at path: ${reelSetPath}`);
|
|
3478
4096
|
}
|
|
3479
4097
|
const allowedExtensions = [".csv"];
|
|
3480
|
-
const ext =
|
|
4098
|
+
const ext = path7.extname(reelSetPath).toLowerCase();
|
|
3481
4099
|
if (!allowedExtensions.includes(ext)) {
|
|
3482
4100
|
throw new Error(
|
|
3483
4101
|
`Invalid file extension for reelset CSV: ${ext}. Allowed extensions are: ${allowedExtensions.join(
|
|
@@ -3485,7 +4103,7 @@ var ReelSet = class {
|
|
|
3485
4103
|
)}`
|
|
3486
4104
|
);
|
|
3487
4105
|
}
|
|
3488
|
-
const csvData =
|
|
4106
|
+
const csvData = fs5.readFileSync(reelSetPath, "utf8");
|
|
3489
4107
|
const rows = csvData.split("\n").filter((line) => line.trim() !== "");
|
|
3490
4108
|
const reels = Array.from(
|
|
3491
4109
|
{ length: config.gameModes[this.associatedGameModeName].reelsAmount },
|
|
@@ -3493,6 +4111,7 @@ var ReelSet = class {
|
|
|
3493
4111
|
);
|
|
3494
4112
|
rows.forEach((row) => {
|
|
3495
4113
|
const symsInRow = row.split(",").map((symbolId) => {
|
|
4114
|
+
if (!symbolId.trim()) return null;
|
|
3496
4115
|
const symbol = config.symbols.get(symbolId.trim());
|
|
3497
4116
|
if (!symbol) {
|
|
3498
4117
|
throw new Error(`Symbol with id "${symbolId}" not found in game config.`);
|
|
@@ -3505,18 +4124,9 @@ var ReelSet = class {
|
|
|
3505
4124
|
`Row in reelset CSV has more symbols than expected reels amount (${reels.length})`
|
|
3506
4125
|
);
|
|
3507
4126
|
}
|
|
3508
|
-
reels[ridx].push(symbol);
|
|
4127
|
+
if (symbol) reels[ridx].push(symbol);
|
|
3509
4128
|
});
|
|
3510
4129
|
});
|
|
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
4130
|
return reels;
|
|
3521
4131
|
}
|
|
3522
4132
|
};
|
|
@@ -3646,12 +4256,12 @@ var GeneratedReelSet = class extends ReelSet {
|
|
|
3646
4256
|
`Error generating reels for game mode "${this.associatedGameModeName}". It's not defined in the game config.`
|
|
3647
4257
|
);
|
|
3648
4258
|
}
|
|
3649
|
-
const outputDir = config.rootDir.endsWith(config.outputDir) ? config.rootDir :
|
|
3650
|
-
const filePath =
|
|
4259
|
+
const outputDir = config.rootDir.endsWith(config.outputDir) ? config.rootDir : path8.join(config.rootDir, config.outputDir);
|
|
4260
|
+
const filePath = path8.join(
|
|
3651
4261
|
outputDir,
|
|
3652
4262
|
`reels_${this.associatedGameModeName}-${this.id}.csv`
|
|
3653
4263
|
);
|
|
3654
|
-
const exists =
|
|
4264
|
+
const exists = fs6.existsSync(filePath);
|
|
3655
4265
|
if (exists && !this.overrideExisting) {
|
|
3656
4266
|
this.reels = this.parseReelsetCSV(filePath, config);
|
|
3657
4267
|
return this;
|
|
@@ -3788,9 +4398,9 @@ var GeneratedReelSet = class extends ReelSet {
|
|
|
3788
4398
|
}
|
|
3789
4399
|
}
|
|
3790
4400
|
const csvString = csvRows.map((row) => row.join(",")).join("\n");
|
|
3791
|
-
if (
|
|
4401
|
+
if (isMainThread6) {
|
|
3792
4402
|
createDirIfNotExists(outputDir);
|
|
3793
|
-
|
|
4403
|
+
fs6.writeFileSync(filePath, csvString);
|
|
3794
4404
|
console.log(
|
|
3795
4405
|
`Generated reelset ${this.id} for game mode ${this.associatedGameModeName}`
|
|
3796
4406
|
);
|
|
@@ -3801,7 +4411,7 @@ var GeneratedReelSet = class extends ReelSet {
|
|
|
3801
4411
|
};
|
|
3802
4412
|
|
|
3803
4413
|
// src/reel-set/StaticReelSet.ts
|
|
3804
|
-
import
|
|
4414
|
+
import assert11 from "assert";
|
|
3805
4415
|
var StaticReelSet = class extends ReelSet {
|
|
3806
4416
|
reels;
|
|
3807
4417
|
csvPath;
|
|
@@ -3811,7 +4421,7 @@ var StaticReelSet = class extends ReelSet {
|
|
|
3811
4421
|
this.reels = [];
|
|
3812
4422
|
this._strReels = opts.reels || [];
|
|
3813
4423
|
this.csvPath = opts.csvPath || "";
|
|
3814
|
-
|
|
4424
|
+
assert11(
|
|
3815
4425
|
opts.reels || opts.csvPath,
|
|
3816
4426
|
`Either 'reels' or 'csvPath' must be provided for StaticReelSet ${this.id}`
|
|
3817
4427
|
);
|
|
@@ -4064,6 +4674,7 @@ export {
|
|
|
4064
4674
|
OptimizationConditions,
|
|
4065
4675
|
OptimizationParameters,
|
|
4066
4676
|
OptimizationScaling,
|
|
4677
|
+
RandomNumberGenerator,
|
|
4067
4678
|
ResultSet,
|
|
4068
4679
|
SPIN_TYPE,
|
|
4069
4680
|
StandaloneBoard,
|
|
@@ -4071,6 +4682,8 @@ export {
|
|
|
4071
4682
|
createSlotGame,
|
|
4072
4683
|
defineGameModes,
|
|
4073
4684
|
defineSymbols,
|
|
4074
|
-
defineUserState
|
|
4685
|
+
defineUserState,
|
|
4686
|
+
parseLookupTable,
|
|
4687
|
+
parseLookupTableSegmented
|
|
4075
4688
|
};
|
|
4076
4689
|
//# sourceMappingURL=index.mjs.map
|