@slot-engine/core 0.1.14-test.0 → 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 +1093 -482
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1094 -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,16 +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 =
|
|
2098
|
-
|
|
2592
|
+
if (fs3.existsSync(tempRecordsPath)) {
|
|
2593
|
+
const fileStream = fs3.createReadStream(tempRecordsPath, {
|
|
2594
|
+
highWaterMark: this.streamHighWaterMark
|
|
2595
|
+
});
|
|
2596
|
+
const rl = readline.createInterface({
|
|
2099
2597
|
input: fileStream,
|
|
2100
2598
|
crlfDelay: Infinity
|
|
2101
2599
|
});
|
|
2102
2600
|
for await (const line of rl) {
|
|
2103
2601
|
if (line.trim() === "") continue;
|
|
2104
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
|
+
}
|
|
2105
2609
|
const key = JSON.stringify(record.search);
|
|
2106
2610
|
let existing = aggregatedRecords.get(key);
|
|
2107
2611
|
if (!existing) {
|
|
@@ -2118,8 +2622,9 @@ ${error.stack}
|
|
|
2118
2622
|
}
|
|
2119
2623
|
}
|
|
2120
2624
|
}
|
|
2121
|
-
|
|
2122
|
-
|
|
2625
|
+
fs3.rmSync(forceRecordsPath, { force: true });
|
|
2626
|
+
fs3.rmSync(this.PATHS.forceKeys(mode), { force: true });
|
|
2627
|
+
const writeStream = fs3.createWriteStream(forceRecordsPath, { encoding: "utf-8" });
|
|
2123
2628
|
writeStream.write("[\n");
|
|
2124
2629
|
let isFirst = true;
|
|
2125
2630
|
for (const record of aggregatedRecords.values()) {
|
|
@@ -2134,13 +2639,17 @@ ${error.stack}
|
|
|
2134
2639
|
await new Promise((resolve) => {
|
|
2135
2640
|
writeStream.on("finish", () => resolve());
|
|
2136
2641
|
});
|
|
2137
|
-
|
|
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 });
|
|
2138
2647
|
}
|
|
2139
2648
|
writeIndexJson() {
|
|
2140
2649
|
const outputFilePath = this.PATHS.indexJson;
|
|
2141
2650
|
const modes = Object.keys(this.simRunsAmount).map((id) => {
|
|
2142
2651
|
const mode = this.gameConfig.gameModes[id];
|
|
2143
|
-
|
|
2652
|
+
assert5(mode, `Game mode "${id}" not found in game config.`);
|
|
2144
2653
|
return {
|
|
2145
2654
|
name: mode.name,
|
|
2146
2655
|
cost: mode.cost,
|
|
@@ -2150,38 +2659,26 @@ ${error.stack}
|
|
|
2150
2659
|
});
|
|
2151
2660
|
writeFile(outputFilePath, JSON.stringify({ modes }, null, 2));
|
|
2152
2661
|
}
|
|
2153
|
-
async writeBooksJson(gameMode) {
|
|
2154
|
-
const outputFilePath = this.PATHS.books(gameMode);
|
|
2155
|
-
const compressedFilePath = this.PATHS.booksCompressed(gameMode);
|
|
2156
|
-
fs2.rmSync(compressedFilePath, { force: true });
|
|
2157
|
-
if (fs2.existsSync(outputFilePath)) {
|
|
2158
|
-
await pipeline(
|
|
2159
|
-
fs2.createReadStream(outputFilePath),
|
|
2160
|
-
zlib.createZstdCompress(),
|
|
2161
|
-
fs2.createWriteStream(compressedFilePath)
|
|
2162
|
-
);
|
|
2163
|
-
}
|
|
2164
|
-
}
|
|
2165
2662
|
/**
|
|
2166
2663
|
* Compiles user configured game to JS for use in different Node processes
|
|
2167
2664
|
*/
|
|
2168
2665
|
preprocessFiles() {
|
|
2169
|
-
const builtFilePath =
|
|
2666
|
+
const builtFilePath = path3.join(
|
|
2170
2667
|
this.gameConfig.rootDir,
|
|
2171
2668
|
this.gameConfig.outputDir,
|
|
2172
2669
|
TEMP_FILENAME
|
|
2173
2670
|
);
|
|
2174
|
-
|
|
2671
|
+
fs3.rmSync(builtFilePath, { force: true });
|
|
2175
2672
|
buildSync({
|
|
2176
2673
|
entryPoints: [this.gameConfig.rootDir],
|
|
2177
2674
|
bundle: true,
|
|
2178
2675
|
platform: "node",
|
|
2179
|
-
outfile:
|
|
2676
|
+
outfile: path3.join(
|
|
2180
2677
|
this.gameConfig.rootDir,
|
|
2181
2678
|
this.gameConfig.outputDir,
|
|
2182
2679
|
TEMP_FILENAME
|
|
2183
2680
|
),
|
|
2184
|
-
external: ["esbuild"]
|
|
2681
|
+
external: ["esbuild", "yargs"]
|
|
2185
2682
|
});
|
|
2186
2683
|
}
|
|
2187
2684
|
getSimRangesForChunks(total, chunks) {
|
|
@@ -2216,27 +2713,62 @@ ${error.stack}
|
|
|
2216
2713
|
}
|
|
2217
2714
|
}
|
|
2218
2715
|
}
|
|
2219
|
-
|
|
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) {
|
|
2220
2724
|
try {
|
|
2221
|
-
|
|
2222
|
-
const
|
|
2725
|
+
fs3.rmSync(outPath, { force: true });
|
|
2726
|
+
const lutStream = fs3.createWriteStream(outPath, {
|
|
2223
2727
|
highWaterMark: this.streamHighWaterMark
|
|
2224
2728
|
});
|
|
2729
|
+
const lutIndexStream = lutIndexPath ? fs3.createWriteStream(lutIndexPath, {
|
|
2730
|
+
highWaterMark: this.streamHighWaterMark
|
|
2731
|
+
}) : void 0;
|
|
2732
|
+
let offset = 0n;
|
|
2225
2733
|
for (let i = 0; i < chunks.length; i++) {
|
|
2226
|
-
const
|
|
2227
|
-
if (!
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
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
|
+
}
|
|
2234
2762
|
}
|
|
2235
2763
|
}
|
|
2236
|
-
|
|
2764
|
+
fs3.rmSync(tempLutChunk);
|
|
2237
2765
|
}
|
|
2238
|
-
|
|
2239
|
-
|
|
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
|
+
]);
|
|
2240
2772
|
} catch (error) {
|
|
2241
2773
|
throw new Error(`Error merging CSV files: ${error.message}`);
|
|
2242
2774
|
}
|
|
@@ -2247,21 +2779,16 @@ ${error.stack}
|
|
|
2247
2779
|
confirmRecords(ctx) {
|
|
2248
2780
|
const recorder = ctx.services.data._getRecorder();
|
|
2249
2781
|
for (const pendingRecord of recorder.pendingRecords) {
|
|
2250
|
-
const
|
|
2251
|
-
let record = recorder.
|
|
2252
|
-
if (r.search.length !== search.length) return false;
|
|
2253
|
-
for (let i = 0; i < r.search.length; i++) {
|
|
2254
|
-
if (r.search[i].name !== search[i].name) return false;
|
|
2255
|
-
if (r.search[i].value !== search[i].value) return false;
|
|
2256
|
-
}
|
|
2257
|
-
return true;
|
|
2258
|
-
});
|
|
2782
|
+
const key = Object.keys(pendingRecord.properties).sort().map((k) => `${k}:${pendingRecord.properties[k]}`).join("|");
|
|
2783
|
+
let record = recorder.recordsMap.get(key);
|
|
2259
2784
|
if (!record) {
|
|
2785
|
+
const search = Object.entries(pendingRecord.properties).map(([name, value]) => ({ name, value })).sort((a, b) => a.name.localeCompare(b.name));
|
|
2260
2786
|
record = {
|
|
2261
2787
|
search,
|
|
2262
2788
|
timesTriggered: 0,
|
|
2263
2789
|
bookIds: []
|
|
2264
2790
|
};
|
|
2791
|
+
recorder.recordsMap.set(key, record);
|
|
2265
2792
|
recorder.records.push(record);
|
|
2266
2793
|
}
|
|
2267
2794
|
record.timesTriggered++;
|
|
@@ -2271,18 +2798,71 @@ ${error.stack}
|
|
|
2271
2798
|
}
|
|
2272
2799
|
recorder.pendingRecords = [];
|
|
2273
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
|
+
}
|
|
2274
2854
|
};
|
|
2275
2855
|
|
|
2276
2856
|
// src/analysis/index.ts
|
|
2277
|
-
import
|
|
2278
|
-
import
|
|
2279
|
-
import assert8 from "assert";
|
|
2857
|
+
import fs4 from "fs";
|
|
2858
|
+
import assert6 from "assert";
|
|
2280
2859
|
|
|
2281
2860
|
// src/analysis/utils.ts
|
|
2282
2861
|
function parseLookupTable(content) {
|
|
2283
2862
|
const lines = content.trim().split("\n");
|
|
2284
2863
|
const lut = [];
|
|
2285
2864
|
for (const line of lines) {
|
|
2865
|
+
if (!line.trim()) continue;
|
|
2286
2866
|
const [indexStr, weightStr, payoutStr] = line.split(",");
|
|
2287
2867
|
const index = parseInt(indexStr.trim());
|
|
2288
2868
|
const weight = parseInt(weightStr.trim());
|
|
@@ -2295,11 +2875,12 @@ function parseLookupTableSegmented(content) {
|
|
|
2295
2875
|
const lines = content.trim().split("\n");
|
|
2296
2876
|
const lut = [];
|
|
2297
2877
|
for (const line of lines) {
|
|
2298
|
-
|
|
2878
|
+
if (!line.trim()) continue;
|
|
2879
|
+
const [indexStr, criteria, bsWinsStr, fsWinsStr] = line.split(",");
|
|
2299
2880
|
const index = parseInt(indexStr.trim());
|
|
2300
|
-
const
|
|
2301
|
-
const
|
|
2302
|
-
lut.push([index, criteria,
|
|
2881
|
+
const bsWins = parseFloat(bsWinsStr.trim());
|
|
2882
|
+
const fsWins = parseFloat(fsWinsStr.trim());
|
|
2883
|
+
lut.push([index, criteria, bsWins, fsWins]);
|
|
2303
2884
|
}
|
|
2304
2885
|
return lut;
|
|
2305
2886
|
}
|
|
@@ -2333,19 +2914,19 @@ function getPayoutWeights(lut, opts = {}) {
|
|
|
2333
2914
|
function getNonZeroHitrate(payoutWeights) {
|
|
2334
2915
|
const totalWeight = getTotalWeight(payoutWeights);
|
|
2335
2916
|
if (Math.min(...Object.keys(payoutWeights).map(Number)) == 0) {
|
|
2336
|
-
return totalWeight / (totalWeight - (payoutWeights[0] ?? 0) / totalWeight);
|
|
2917
|
+
return round(totalWeight / (totalWeight - (payoutWeights[0] ?? 0) / totalWeight), 4);
|
|
2337
2918
|
} else {
|
|
2338
2919
|
return 1;
|
|
2339
2920
|
}
|
|
2340
2921
|
}
|
|
2341
2922
|
function getNullHitrate(payoutWeights) {
|
|
2342
|
-
return payoutWeights[0] ?? 0;
|
|
2923
|
+
return round(payoutWeights[0] ?? 0, 4);
|
|
2343
2924
|
}
|
|
2344
2925
|
function getMaxwinHitrate(payoutWeights) {
|
|
2345
2926
|
const totalWeight = getTotalWeight(payoutWeights);
|
|
2346
2927
|
const maxWin = Math.max(...Object.keys(payoutWeights).map(Number));
|
|
2347
2928
|
const hitRate = (payoutWeights[maxWin] || 0) / totalWeight;
|
|
2348
|
-
return 1 / hitRate;
|
|
2929
|
+
return round(1 / hitRate, 4);
|
|
2349
2930
|
}
|
|
2350
2931
|
function getUniquePayouts(payoutWeights) {
|
|
2351
2932
|
return Object.keys(payoutWeights).length;
|
|
@@ -2364,15 +2945,15 @@ function getAvgWin(payoutWeights) {
|
|
|
2364
2945
|
const payout = parseFloat(payoutStr);
|
|
2365
2946
|
avgWin += payout * weight;
|
|
2366
2947
|
}
|
|
2367
|
-
return avgWin;
|
|
2948
|
+
return round(avgWin, 4);
|
|
2368
2949
|
}
|
|
2369
2950
|
function getRtp(payoutWeights, cost) {
|
|
2370
2951
|
const avgWin = getAvgWin(payoutWeights);
|
|
2371
|
-
return avgWin / cost;
|
|
2952
|
+
return round(avgWin / cost, 4);
|
|
2372
2953
|
}
|
|
2373
2954
|
function getStandardDeviation(payoutWeights) {
|
|
2374
2955
|
const variance = getVariance(payoutWeights);
|
|
2375
|
-
return Math.sqrt(variance);
|
|
2956
|
+
return round(Math.sqrt(variance), 4);
|
|
2376
2957
|
}
|
|
2377
2958
|
function getVariance(payoutWeights) {
|
|
2378
2959
|
const totalWeight = getTotalWeight(payoutWeights);
|
|
@@ -2382,7 +2963,7 @@ function getVariance(payoutWeights) {
|
|
|
2382
2963
|
const payout = parseFloat(payoutStr);
|
|
2383
2964
|
variance += Math.pow(payout - avgWin, 2) * (weight / totalWeight);
|
|
2384
2965
|
}
|
|
2385
|
-
return variance;
|
|
2966
|
+
return round(variance, 4);
|
|
2386
2967
|
}
|
|
2387
2968
|
function getLessBetHitrate(payoutWeights, cost) {
|
|
2388
2969
|
let lessBetWeight = 0;
|
|
@@ -2393,80 +2974,29 @@ function getLessBetHitrate(payoutWeights, cost) {
|
|
|
2393
2974
|
lessBetWeight += weight;
|
|
2394
2975
|
}
|
|
2395
2976
|
}
|
|
2396
|
-
return lessBetWeight / totalWeight;
|
|
2977
|
+
return round(lessBetWeight / totalWeight, 4);
|
|
2397
2978
|
}
|
|
2398
2979
|
|
|
2399
2980
|
// src/analysis/index.ts
|
|
2400
|
-
import { isMainThread as
|
|
2981
|
+
import { isMainThread as isMainThread3 } from "worker_threads";
|
|
2401
2982
|
var Analysis = class {
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
constructor(optimizer) {
|
|
2406
|
-
this.gameConfig = optimizer.getGameConfig();
|
|
2407
|
-
this.optimizerConfig = optimizer.getOptimizerGameModes();
|
|
2408
|
-
this.filePaths = {};
|
|
2983
|
+
game;
|
|
2984
|
+
constructor(game) {
|
|
2985
|
+
this.game = game;
|
|
2409
2986
|
}
|
|
2410
2987
|
async runAnalysis(gameModes) {
|
|
2411
|
-
if (!
|
|
2412
|
-
this.filePaths = this.getPathsForModes(gameModes);
|
|
2988
|
+
if (!isMainThread3) return;
|
|
2413
2989
|
this.getNumberStats(gameModes);
|
|
2414
2990
|
this.getWinRanges(gameModes);
|
|
2415
2991
|
console.log("Analysis complete. Files written to build directory.");
|
|
2416
2992
|
}
|
|
2417
|
-
getPathsForModes(gameModes) {
|
|
2418
|
-
const rootPath = this.gameConfig.rootDir;
|
|
2419
|
-
const paths = {};
|
|
2420
|
-
for (const modeStr of gameModes) {
|
|
2421
|
-
const lut = path2.join(
|
|
2422
|
-
rootPath,
|
|
2423
|
-
this.gameConfig.outputDir,
|
|
2424
|
-
`lookUpTable_${modeStr}.csv`
|
|
2425
|
-
);
|
|
2426
|
-
const lutSegmented = path2.join(
|
|
2427
|
-
rootPath,
|
|
2428
|
-
this.gameConfig.outputDir,
|
|
2429
|
-
`lookUpTableSegmented_${modeStr}.csv`
|
|
2430
|
-
);
|
|
2431
|
-
const lutOptimized = path2.join(
|
|
2432
|
-
rootPath,
|
|
2433
|
-
this.gameConfig.outputDir,
|
|
2434
|
-
"publish_files",
|
|
2435
|
-
`lookUpTable_${modeStr}_0.csv`
|
|
2436
|
-
);
|
|
2437
|
-
const booksJsonl = path2.join(
|
|
2438
|
-
rootPath,
|
|
2439
|
-
this.gameConfig.outputDir,
|
|
2440
|
-
`books_${modeStr}.jsonl`
|
|
2441
|
-
);
|
|
2442
|
-
const booksJsonlCompressed = path2.join(
|
|
2443
|
-
rootPath,
|
|
2444
|
-
this.gameConfig.outputDir,
|
|
2445
|
-
"publish_files",
|
|
2446
|
-
`books_${modeStr}.jsonl.zst`
|
|
2447
|
-
);
|
|
2448
|
-
paths[modeStr] = {
|
|
2449
|
-
lut,
|
|
2450
|
-
lutSegmented,
|
|
2451
|
-
lutOptimized,
|
|
2452
|
-
booksJsonl,
|
|
2453
|
-
booksJsonlCompressed
|
|
2454
|
-
};
|
|
2455
|
-
for (const p of Object.values(paths[modeStr])) {
|
|
2456
|
-
assert8(
|
|
2457
|
-
fs3.existsSync(p),
|
|
2458
|
-
`File "${p}" does not exist. Run optimization to auto-create it.`
|
|
2459
|
-
);
|
|
2460
|
-
}
|
|
2461
|
-
}
|
|
2462
|
-
return paths;
|
|
2463
|
-
}
|
|
2464
2993
|
getNumberStats(gameModes) {
|
|
2994
|
+
const meta = this.game.getMetadata();
|
|
2465
2995
|
const stats = [];
|
|
2466
2996
|
for (const modeStr of gameModes) {
|
|
2467
2997
|
const mode = this.getGameModeConfig(modeStr);
|
|
2468
2998
|
const lutOptimized = parseLookupTable(
|
|
2469
|
-
|
|
2999
|
+
fs4.readFileSync(meta.paths.lookupTablePublish(modeStr), "utf-8")
|
|
2470
3000
|
);
|
|
2471
3001
|
const totalWeight = getTotalLutWeight(lutOptimized);
|
|
2472
3002
|
const payoutWeights = getPayoutWeights(lutOptimized);
|
|
@@ -2486,10 +3016,7 @@ var Analysis = class {
|
|
|
2486
3016
|
uniquePayouts: getUniquePayouts(payoutWeights)
|
|
2487
3017
|
});
|
|
2488
3018
|
}
|
|
2489
|
-
writeJsonFile(
|
|
2490
|
-
path2.join(this.gameConfig.rootDir, this.gameConfig.outputDir, "stats_summary.json"),
|
|
2491
|
-
stats
|
|
2492
|
-
);
|
|
3019
|
+
writeJsonFile(meta.paths.statsSummary, stats);
|
|
2493
3020
|
}
|
|
2494
3021
|
getWinRanges(gameModes) {
|
|
2495
3022
|
const winRanges = [
|
|
@@ -2512,14 +3039,31 @@ var Analysis = class {
|
|
|
2512
3039
|
[7500, 9999.99],
|
|
2513
3040
|
[1e4, 14999.99],
|
|
2514
3041
|
[15e3, 19999.99],
|
|
2515
|
-
[2e4, 24999.99]
|
|
3042
|
+
[2e4, 24999.99],
|
|
3043
|
+
[25e3, 49999.99],
|
|
3044
|
+
[5e4, 74999.99],
|
|
3045
|
+
[75e3, 99999.99],
|
|
3046
|
+
[1e5, Infinity]
|
|
2516
3047
|
];
|
|
2517
|
-
const payoutRanges =
|
|
3048
|
+
const payoutRanges = [];
|
|
3049
|
+
const meta = this.game.getMetadata();
|
|
2518
3050
|
for (const modeStr of gameModes) {
|
|
2519
|
-
payoutRanges[modeStr] = { overall: {}, criteria: {} };
|
|
2520
3051
|
const lutSegmented = parseLookupTableSegmented(
|
|
2521
|
-
|
|
3052
|
+
fs4.readFileSync(meta.paths.lookupTableSegmented(modeStr), "utf-8")
|
|
2522
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();
|
|
2523
3067
|
lutSegmented.forEach(([, criteria, bp, fsp]) => {
|
|
2524
3068
|
const basePayout = bp;
|
|
2525
3069
|
const freeSpinPayout = fsp;
|
|
@@ -2527,32 +3071,54 @@ var Analysis = class {
|
|
|
2527
3071
|
for (const [min, max] of winRanges) {
|
|
2528
3072
|
if (payout >= min && payout <= max) {
|
|
2529
3073
|
const rangeKey = `${min}-${max}`;
|
|
2530
|
-
if (!
|
|
2531
|
-
|
|
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;
|
|
2532
3083
|
}
|
|
2533
|
-
|
|
2534
|
-
if (!
|
|
2535
|
-
|
|
3084
|
+
range.allPayouts.criteria[criteria][rangeKey] += 1;
|
|
3085
|
+
if (!uniquePayoutsOverall.has(rangeKey)) {
|
|
3086
|
+
uniquePayoutsOverall.set(rangeKey, /* @__PURE__ */ new Set());
|
|
2536
3087
|
}
|
|
2537
|
-
|
|
2538
|
-
|
|
3088
|
+
uniquePayoutsOverall.get(rangeKey).add(payout);
|
|
3089
|
+
if (!uniquePayoutsCriteria.has(criteria)) {
|
|
3090
|
+
uniquePayoutsCriteria.set(criteria, /* @__PURE__ */ new Map());
|
|
2539
3091
|
}
|
|
2540
|
-
|
|
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);
|
|
2541
3096
|
break;
|
|
2542
3097
|
}
|
|
2543
3098
|
}
|
|
2544
3099
|
});
|
|
2545
|
-
|
|
2546
|
-
|
|
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) => {
|
|
2547
3113
|
const [aMin] = a.split("-").map(Number);
|
|
2548
3114
|
const [bMin] = b.split("-").map(Number);
|
|
2549
3115
|
return aMin - bMin;
|
|
2550
3116
|
}).forEach((key) => {
|
|
2551
|
-
|
|
3117
|
+
orderedAllOverall[key] = range.allPayouts.overall[key];
|
|
2552
3118
|
});
|
|
2553
|
-
const
|
|
2554
|
-
Object.keys(
|
|
2555
|
-
const critMap =
|
|
3119
|
+
const orderedAllCriteria = {};
|
|
3120
|
+
Object.keys(range.allPayouts.criteria).forEach((crit) => {
|
|
3121
|
+
const critMap = range.allPayouts.criteria[crit];
|
|
2556
3122
|
const orderedCritMap = {};
|
|
2557
3123
|
Object.keys(critMap).sort((a, b) => {
|
|
2558
3124
|
const [aMin] = a.split("-").map(Number);
|
|
@@ -2561,35 +3127,61 @@ var Analysis = class {
|
|
|
2561
3127
|
}).forEach((key) => {
|
|
2562
3128
|
orderedCritMap[key] = critMap[key];
|
|
2563
3129
|
});
|
|
2564
|
-
|
|
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
|
+
}
|
|
2565
3163
|
});
|
|
2566
|
-
payoutRanges[modeStr] = {
|
|
2567
|
-
overall: orderedOverall,
|
|
2568
|
-
criteria: {}
|
|
2569
|
-
};
|
|
2570
3164
|
}
|
|
2571
|
-
writeJsonFile(
|
|
2572
|
-
path2.join(this.gameConfig.rootDir, this.gameConfig.outputDir, "stats_payouts.json"),
|
|
2573
|
-
payoutRanges
|
|
2574
|
-
);
|
|
3165
|
+
writeJsonFile(meta.paths.statsPayouts, payoutRanges);
|
|
2575
3166
|
}
|
|
2576
3167
|
getGameModeConfig(mode) {
|
|
2577
|
-
const config = this.
|
|
2578
|
-
|
|
3168
|
+
const config = this.game.getConfig().gameModes[mode];
|
|
3169
|
+
assert6(config, `Game mode "${mode}" not found in game config`);
|
|
2579
3170
|
return config;
|
|
2580
3171
|
}
|
|
2581
3172
|
};
|
|
2582
3173
|
|
|
2583
3174
|
// src/optimizer/index.ts
|
|
2584
|
-
import
|
|
2585
|
-
import
|
|
3175
|
+
import path6 from "path";
|
|
3176
|
+
import assert8 from "assert";
|
|
2586
3177
|
import { spawn } from "child_process";
|
|
2587
|
-
import { isMainThread as
|
|
3178
|
+
import { isMainThread as isMainThread4 } from "worker_threads";
|
|
2588
3179
|
|
|
2589
3180
|
// src/utils/math-config.ts
|
|
2590
|
-
import
|
|
3181
|
+
import path4 from "path";
|
|
2591
3182
|
function makeMathConfig(optimizer, opts = {}) {
|
|
2592
3183
|
const game = optimizer.getGameConfig();
|
|
3184
|
+
const meta = optimizer.getGameMeta();
|
|
2593
3185
|
const gameModesCfg = optimizer.getOptimizerGameModes();
|
|
2594
3186
|
const { writeToFile } = opts;
|
|
2595
3187
|
const isDefined = (v) => v !== void 0;
|
|
@@ -2631,16 +3223,17 @@ function makeMathConfig(optimizer, opts = {}) {
|
|
|
2631
3223
|
}))
|
|
2632
3224
|
};
|
|
2633
3225
|
if (writeToFile) {
|
|
2634
|
-
const outPath =
|
|
3226
|
+
const outPath = path4.join(meta.rootDir, meta.outputDir, "math_config.json");
|
|
2635
3227
|
writeJsonFile(outPath, config);
|
|
2636
3228
|
}
|
|
2637
3229
|
return config;
|
|
2638
3230
|
}
|
|
2639
3231
|
|
|
2640
3232
|
// src/utils/setup-file.ts
|
|
2641
|
-
import
|
|
3233
|
+
import path5 from "path";
|
|
2642
3234
|
function makeSetupFile(optimizer, gameMode) {
|
|
2643
3235
|
const gameConfig = optimizer.getGameConfig();
|
|
3236
|
+
const gameMeta = optimizer.getGameMeta();
|
|
2644
3237
|
const optimizerGameModes = optimizer.getOptimizerGameModes();
|
|
2645
3238
|
const modeConfig = optimizerGameModes[gameMode];
|
|
2646
3239
|
if (!modeConfig) {
|
|
@@ -2674,16 +3267,16 @@ function makeSetupFile(optimizer, gameMode) {
|
|
|
2674
3267
|
`;
|
|
2675
3268
|
content += `simulation_trials;${params.simulationTrials}
|
|
2676
3269
|
`;
|
|
2677
|
-
content += `user_game_build_path;${
|
|
3270
|
+
content += `user_game_build_path;${path5.join(gameMeta.rootDir, gameMeta.outputDir)}
|
|
2678
3271
|
`;
|
|
2679
3272
|
content += `pmb_rtp;${params.pmbRtp}
|
|
2680
3273
|
`;
|
|
2681
|
-
const outPath =
|
|
3274
|
+
const outPath = path5.join(__dirname, "./optimizer-rust/src", "setup.txt");
|
|
2682
3275
|
writeFile(outPath, content);
|
|
2683
3276
|
}
|
|
2684
3277
|
|
|
2685
3278
|
// src/optimizer/OptimizationConditions.ts
|
|
2686
|
-
import
|
|
3279
|
+
import assert7 from "assert";
|
|
2687
3280
|
var OptimizationConditions = class {
|
|
2688
3281
|
rtp;
|
|
2689
3282
|
avgWin;
|
|
@@ -2694,14 +3287,14 @@ var OptimizationConditions = class {
|
|
|
2694
3287
|
constructor(opts) {
|
|
2695
3288
|
let { rtp, avgWin, hitRate, searchConditions, priority } = opts;
|
|
2696
3289
|
if (rtp == void 0 || rtp === "x") {
|
|
2697
|
-
|
|
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.");
|
|
2698
3291
|
rtp = Math.round(avgWin / Number(hitRate) * 1e5) / 1e5;
|
|
2699
3292
|
}
|
|
2700
3293
|
let noneCount = 0;
|
|
2701
3294
|
for (const val of [rtp, avgWin, hitRate]) {
|
|
2702
3295
|
if (val === void 0) noneCount++;
|
|
2703
3296
|
}
|
|
2704
|
-
|
|
3297
|
+
assert7(noneCount <= 1, "Invalid combination of optimization conditions.");
|
|
2705
3298
|
this.searchRange = [-1, -1];
|
|
2706
3299
|
this.forceSearch = {};
|
|
2707
3300
|
if (typeof searchConditions === "number") {
|
|
@@ -2782,9 +3375,11 @@ var OptimizationParameters = class _OptimizationParameters {
|
|
|
2782
3375
|
// src/optimizer/index.ts
|
|
2783
3376
|
var Optimizer = class {
|
|
2784
3377
|
gameConfig;
|
|
3378
|
+
gameMeta;
|
|
2785
3379
|
gameModes;
|
|
2786
3380
|
constructor(opts) {
|
|
2787
3381
|
this.gameConfig = opts.game.getConfig();
|
|
3382
|
+
this.gameMeta = opts.game.getMetadata();
|
|
2788
3383
|
this.gameModes = opts.gameModes;
|
|
2789
3384
|
this.verifyConfig();
|
|
2790
3385
|
}
|
|
@@ -2792,11 +3387,15 @@ var Optimizer = class {
|
|
|
2792
3387
|
* Runs the optimization process, and runs analysis after.
|
|
2793
3388
|
*/
|
|
2794
3389
|
async runOptimization({ gameModes }) {
|
|
2795
|
-
if (!
|
|
3390
|
+
if (!isMainThread4) return;
|
|
2796
3391
|
const mathConfig = makeMathConfig(this, { writeToFile: true });
|
|
2797
3392
|
for (const mode of gameModes) {
|
|
2798
3393
|
const setupFile = makeSetupFile(this, mode);
|
|
2799
3394
|
await this.runSingleOptimization();
|
|
3395
|
+
await makeLutIndexFromPublishLut(
|
|
3396
|
+
this.gameMeta.paths.lookupTablePublish(mode),
|
|
3397
|
+
this.gameMeta.paths.lookupTableIndex(mode)
|
|
3398
|
+
);
|
|
2800
3399
|
}
|
|
2801
3400
|
console.log("Optimization complete. Files written to build directory.");
|
|
2802
3401
|
}
|
|
@@ -2829,7 +3428,7 @@ var Optimizer = class {
|
|
|
2829
3428
|
}
|
|
2830
3429
|
gameModeRtp = Math.round(gameModeRtp * 1e3) / 1e3;
|
|
2831
3430
|
paramRtp = Math.round(paramRtp * 1e3) / 1e3;
|
|
2832
|
-
|
|
3431
|
+
assert8(
|
|
2833
3432
|
gameModeRtp === paramRtp,
|
|
2834
3433
|
`Sum of all RTP conditions (${paramRtp}) does not match the game mode RTP (${gameModeRtp}) in game mode "${k}".`
|
|
2835
3434
|
);
|
|
@@ -2838,6 +3437,9 @@ var Optimizer = class {
|
|
|
2838
3437
|
getGameConfig() {
|
|
2839
3438
|
return this.gameConfig;
|
|
2840
3439
|
}
|
|
3440
|
+
getGameMeta() {
|
|
3441
|
+
return this.gameMeta;
|
|
3442
|
+
}
|
|
2841
3443
|
getOptimizerGameModes() {
|
|
2842
3444
|
return this.gameModes;
|
|
2843
3445
|
}
|
|
@@ -2846,7 +3448,7 @@ async function rustProgram(...args) {
|
|
|
2846
3448
|
console.log("Starting Rust optimizer. This may take a while...");
|
|
2847
3449
|
return new Promise((resolve, reject) => {
|
|
2848
3450
|
const task = spawn("cargo", ["run", "-q", "--release", ...args], {
|
|
2849
|
-
cwd:
|
|
3451
|
+
cwd: path6.join(__dirname, "./optimizer-rust"),
|
|
2850
3452
|
stdio: "pipe"
|
|
2851
3453
|
});
|
|
2852
3454
|
task.on("error", (error) => {
|
|
@@ -2873,8 +3475,8 @@ async function rustProgram(...args) {
|
|
|
2873
3475
|
}
|
|
2874
3476
|
|
|
2875
3477
|
// src/slot-game/index.ts
|
|
2876
|
-
import { isMainThread as
|
|
2877
|
-
var SlotGame = class {
|
|
3478
|
+
import { isMainThread as isMainThread5, workerData as workerData2 } from "worker_threads";
|
|
3479
|
+
var SlotGame = class _SlotGame {
|
|
2878
3480
|
configOpts;
|
|
2879
3481
|
simulation;
|
|
2880
3482
|
optimizer;
|
|
@@ -2924,38 +3526,56 @@ var SlotGame = class {
|
|
|
2924
3526
|
/**
|
|
2925
3527
|
* Runs the analysis based on the configured settings.
|
|
2926
3528
|
*/
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
"Optimization must be configured to run analysis. Do so by calling configureOptimization() first."
|
|
2931
|
-
);
|
|
2932
|
-
}
|
|
2933
|
-
this.analyzer = new Analysis(this.optimizer);
|
|
2934
|
-
await this.analyzer.runAnalysis(opts.gameModes);
|
|
3529
|
+
runAnalysis(opts) {
|
|
3530
|
+
this.analyzer = new Analysis(this);
|
|
3531
|
+
this.analyzer.runAnalysis(opts.gameModes);
|
|
2935
3532
|
}
|
|
2936
3533
|
/**
|
|
2937
3534
|
* Runs the configured tasks: simulation, optimization, and/or analysis.
|
|
2938
3535
|
*/
|
|
2939
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
|
+
}
|
|
2940
3551
|
if (!opts.doSimulation && !opts.doOptimization && !opts.doAnalysis) {
|
|
2941
3552
|
console.log("No tasks to run. Enable either simulation, optimization or analysis.");
|
|
2942
3553
|
}
|
|
2943
3554
|
if (opts.doSimulation) {
|
|
2944
3555
|
await this.runSimulation(opts.simulationOpts || {});
|
|
2945
3556
|
}
|
|
3557
|
+
if (opts.doAnalysis) {
|
|
3558
|
+
this.runAnalysis(opts.analysisOpts || { gameModes: [] });
|
|
3559
|
+
}
|
|
2946
3560
|
if (opts.doOptimization) {
|
|
2947
3561
|
await this.runOptimization(opts.optimizationOpts || { gameModes: [] });
|
|
3562
|
+
if (opts.doAnalysis) {
|
|
3563
|
+
this.runAnalysis(opts.analysisOpts || { gameModes: [] });
|
|
3564
|
+
}
|
|
2948
3565
|
}
|
|
2949
|
-
if (
|
|
2950
|
-
await this.runAnalysis(opts.analysisOpts || { gameModes: [] });
|
|
2951
|
-
}
|
|
2952
|
-
if (isMainThread4) console.log("Finishing up...");
|
|
3566
|
+
if (isMainThread5) console.log("Done!");
|
|
2953
3567
|
}
|
|
2954
3568
|
/**
|
|
2955
3569
|
* Gets the game configuration.
|
|
2956
3570
|
*/
|
|
2957
3571
|
getConfig() {
|
|
2958
|
-
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);
|
|
2959
3579
|
}
|
|
2960
3580
|
};
|
|
2961
3581
|
|
|
@@ -2968,7 +3588,7 @@ var defineSymbols = (symbols) => symbols;
|
|
|
2968
3588
|
var defineGameModes = (gameModes) => gameModes;
|
|
2969
3589
|
|
|
2970
3590
|
// src/game-mode/index.ts
|
|
2971
|
-
import
|
|
3591
|
+
import assert9 from "assert";
|
|
2972
3592
|
var GameMode = class {
|
|
2973
3593
|
name;
|
|
2974
3594
|
_reelsAmount;
|
|
@@ -2991,12 +3611,12 @@ var GameMode = class {
|
|
|
2991
3611
|
this.reelSets = opts.reelSets;
|
|
2992
3612
|
this.resultSets = opts.resultSets;
|
|
2993
3613
|
this.isBonusBuy = opts.isBonusBuy;
|
|
2994
|
-
|
|
2995
|
-
|
|
3614
|
+
assert9(this.rtp >= 0.9 && this.rtp <= 0.99, "RTP must be between 0.9 and 0.99");
|
|
3615
|
+
assert9(
|
|
2996
3616
|
this.symbolsPerReel.length === this.reelsAmount,
|
|
2997
3617
|
"symbolsPerReel length must match reelsAmount."
|
|
2998
3618
|
);
|
|
2999
|
-
|
|
3619
|
+
assert9(this.reelSets.length > 0, "GameMode must have at least one ReelSet defined.");
|
|
3000
3620
|
}
|
|
3001
3621
|
/**
|
|
3002
3622
|
* Intended for internal use only.
|
|
@@ -3009,7 +3629,7 @@ var GameMode = class {
|
|
|
3009
3629
|
* Intended for internal use only.
|
|
3010
3630
|
*/
|
|
3011
3631
|
_setSymbolsPerReel(symbolsPerReel) {
|
|
3012
|
-
|
|
3632
|
+
assert9(
|
|
3013
3633
|
symbolsPerReel.length === this._reelsAmount,
|
|
3014
3634
|
"symbolsPerReel length must match reelsAmount."
|
|
3015
3635
|
);
|
|
@@ -3075,7 +3695,7 @@ var WinType = class {
|
|
|
3075
3695
|
};
|
|
3076
3696
|
|
|
3077
3697
|
// src/win-types/LinesWinType.ts
|
|
3078
|
-
import
|
|
3698
|
+
import assert10 from "assert";
|
|
3079
3699
|
var LinesWinType = class extends WinType {
|
|
3080
3700
|
lines;
|
|
3081
3701
|
constructor(opts) {
|
|
@@ -3129,8 +3749,8 @@ var LinesWinType = class extends WinType {
|
|
|
3129
3749
|
if (!baseSymbol) {
|
|
3130
3750
|
baseSymbol = thisSymbol;
|
|
3131
3751
|
}
|
|
3132
|
-
|
|
3133
|
-
|
|
3752
|
+
assert10(baseSymbol, `No symbol found at line ${lineNum}, reel ${ridx}`);
|
|
3753
|
+
assert10(thisSymbol, `No symbol found at line ${lineNum}, reel ${ridx}`);
|
|
3134
3754
|
if (potentialWinLine.length == 0) {
|
|
3135
3755
|
if (this.isWild(thisSymbol)) {
|
|
3136
3756
|
potentialWildLine.push({ reel: ridx, row: sidx, symbol: thisSymbol });
|
|
@@ -3445,13 +4065,13 @@ var ManywaysWinType = class extends WinType {
|
|
|
3445
4065
|
};
|
|
3446
4066
|
|
|
3447
4067
|
// src/reel-set/GeneratedReelSet.ts
|
|
3448
|
-
import
|
|
3449
|
-
import
|
|
3450
|
-
import { isMainThread as
|
|
4068
|
+
import fs6 from "fs";
|
|
4069
|
+
import path8 from "path";
|
|
4070
|
+
import { isMainThread as isMainThread6 } from "worker_threads";
|
|
3451
4071
|
|
|
3452
4072
|
// src/reel-set/index.ts
|
|
3453
|
-
import
|
|
3454
|
-
import
|
|
4073
|
+
import fs5 from "fs";
|
|
4074
|
+
import path7 from "path";
|
|
3455
4075
|
var ReelSet = class {
|
|
3456
4076
|
id;
|
|
3457
4077
|
associatedGameModeName;
|
|
@@ -3471,11 +4091,11 @@ var ReelSet = class {
|
|
|
3471
4091
|
* Reads a reelset CSV file and returns the reels as arrays of GameSymbols.
|
|
3472
4092
|
*/
|
|
3473
4093
|
parseReelsetCSV(reelSetPath, config) {
|
|
3474
|
-
if (!
|
|
4094
|
+
if (!fs5.existsSync(reelSetPath)) {
|
|
3475
4095
|
throw new Error(`Reelset CSV file not found at path: ${reelSetPath}`);
|
|
3476
4096
|
}
|
|
3477
4097
|
const allowedExtensions = [".csv"];
|
|
3478
|
-
const ext =
|
|
4098
|
+
const ext = path7.extname(reelSetPath).toLowerCase();
|
|
3479
4099
|
if (!allowedExtensions.includes(ext)) {
|
|
3480
4100
|
throw new Error(
|
|
3481
4101
|
`Invalid file extension for reelset CSV: ${ext}. Allowed extensions are: ${allowedExtensions.join(
|
|
@@ -3483,7 +4103,7 @@ var ReelSet = class {
|
|
|
3483
4103
|
)}`
|
|
3484
4104
|
);
|
|
3485
4105
|
}
|
|
3486
|
-
const csvData =
|
|
4106
|
+
const csvData = fs5.readFileSync(reelSetPath, "utf8");
|
|
3487
4107
|
const rows = csvData.split("\n").filter((line) => line.trim() !== "");
|
|
3488
4108
|
const reels = Array.from(
|
|
3489
4109
|
{ length: config.gameModes[this.associatedGameModeName].reelsAmount },
|
|
@@ -3491,6 +4111,7 @@ var ReelSet = class {
|
|
|
3491
4111
|
);
|
|
3492
4112
|
rows.forEach((row) => {
|
|
3493
4113
|
const symsInRow = row.split(",").map((symbolId) => {
|
|
4114
|
+
if (!symbolId.trim()) return null;
|
|
3494
4115
|
const symbol = config.symbols.get(symbolId.trim());
|
|
3495
4116
|
if (!symbol) {
|
|
3496
4117
|
throw new Error(`Symbol with id "${symbolId}" not found in game config.`);
|
|
@@ -3503,18 +4124,9 @@ var ReelSet = class {
|
|
|
3503
4124
|
`Row in reelset CSV has more symbols than expected reels amount (${reels.length})`
|
|
3504
4125
|
);
|
|
3505
4126
|
}
|
|
3506
|
-
reels[ridx].push(symbol);
|
|
4127
|
+
if (symbol) reels[ridx].push(symbol);
|
|
3507
4128
|
});
|
|
3508
4129
|
});
|
|
3509
|
-
const reelLengths = reels.map((r) => r.length);
|
|
3510
|
-
const uniqueLengths = new Set(reelLengths);
|
|
3511
|
-
if (uniqueLengths.size > 1) {
|
|
3512
|
-
throw new Error(
|
|
3513
|
-
`Inconsistent reel lengths in reelset CSV at ${reelSetPath}: ${[
|
|
3514
|
-
...uniqueLengths
|
|
3515
|
-
].join(", ")}`
|
|
3516
|
-
);
|
|
3517
|
-
}
|
|
3518
4130
|
return reels;
|
|
3519
4131
|
}
|
|
3520
4132
|
};
|
|
@@ -3644,12 +4256,12 @@ var GeneratedReelSet = class extends ReelSet {
|
|
|
3644
4256
|
`Error generating reels for game mode "${this.associatedGameModeName}". It's not defined in the game config.`
|
|
3645
4257
|
);
|
|
3646
4258
|
}
|
|
3647
|
-
const outputDir = config.rootDir.endsWith(config.outputDir) ? config.rootDir :
|
|
3648
|
-
const filePath =
|
|
4259
|
+
const outputDir = config.rootDir.endsWith(config.outputDir) ? config.rootDir : path8.join(config.rootDir, config.outputDir);
|
|
4260
|
+
const filePath = path8.join(
|
|
3649
4261
|
outputDir,
|
|
3650
4262
|
`reels_${this.associatedGameModeName}-${this.id}.csv`
|
|
3651
4263
|
);
|
|
3652
|
-
const exists =
|
|
4264
|
+
const exists = fs6.existsSync(filePath);
|
|
3653
4265
|
if (exists && !this.overrideExisting) {
|
|
3654
4266
|
this.reels = this.parseReelsetCSV(filePath, config);
|
|
3655
4267
|
return this;
|
|
@@ -3786,9 +4398,9 @@ var GeneratedReelSet = class extends ReelSet {
|
|
|
3786
4398
|
}
|
|
3787
4399
|
}
|
|
3788
4400
|
const csvString = csvRows.map((row) => row.join(",")).join("\n");
|
|
3789
|
-
if (
|
|
4401
|
+
if (isMainThread6) {
|
|
3790
4402
|
createDirIfNotExists(outputDir);
|
|
3791
|
-
|
|
4403
|
+
fs6.writeFileSync(filePath, csvString);
|
|
3792
4404
|
console.log(
|
|
3793
4405
|
`Generated reelset ${this.id} for game mode ${this.associatedGameModeName}`
|
|
3794
4406
|
);
|
|
@@ -3799,7 +4411,7 @@ var GeneratedReelSet = class extends ReelSet {
|
|
|
3799
4411
|
};
|
|
3800
4412
|
|
|
3801
4413
|
// src/reel-set/StaticReelSet.ts
|
|
3802
|
-
import
|
|
4414
|
+
import assert11 from "assert";
|
|
3803
4415
|
var StaticReelSet = class extends ReelSet {
|
|
3804
4416
|
reels;
|
|
3805
4417
|
csvPath;
|
|
@@ -3809,7 +4421,7 @@ var StaticReelSet = class extends ReelSet {
|
|
|
3809
4421
|
this.reels = [];
|
|
3810
4422
|
this._strReels = opts.reels || [];
|
|
3811
4423
|
this.csvPath = opts.csvPath || "";
|
|
3812
|
-
|
|
4424
|
+
assert11(
|
|
3813
4425
|
opts.reels || opts.csvPath,
|
|
3814
4426
|
`Either 'reels' or 'csvPath' must be provided for StaticReelSet ${this.id}`
|
|
3815
4427
|
);
|
|
@@ -4062,6 +4674,7 @@ export {
|
|
|
4062
4674
|
OptimizationConditions,
|
|
4063
4675
|
OptimizationParameters,
|
|
4064
4676
|
OptimizationScaling,
|
|
4677
|
+
RandomNumberGenerator,
|
|
4065
4678
|
ResultSet,
|
|
4066
4679
|
SPIN_TYPE,
|
|
4067
4680
|
StandaloneBoard,
|
|
@@ -4069,6 +4682,8 @@ export {
|
|
|
4069
4682
|
createSlotGame,
|
|
4070
4683
|
defineGameModes,
|
|
4071
4684
|
defineSymbols,
|
|
4072
|
-
defineUserState
|
|
4685
|
+
defineUserState,
|
|
4686
|
+
parseLookupTable,
|
|
4687
|
+
parseLookupTableSegmented
|
|
4073
4688
|
};
|
|
4074
4689
|
//# sourceMappingURL=index.mjs.map
|