@slot-engine/core 0.1.10 → 0.1.12
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 +47 -18
- package/dist/index.d.ts +47 -18
- package/dist/index.js +345 -219
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +345 -219
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -32,7 +32,7 @@ function createGameConfig(opts) {
|
|
|
32
32
|
// src/simulation/index.ts
|
|
33
33
|
import fs2 from "fs";
|
|
34
34
|
import path from "path";
|
|
35
|
-
import
|
|
35
|
+
import assert7 from "assert";
|
|
36
36
|
import zlib from "zlib";
|
|
37
37
|
import readline2 from "readline";
|
|
38
38
|
import { buildSync } from "esbuild";
|
|
@@ -258,6 +258,9 @@ var JSONL = class {
|
|
|
258
258
|
});
|
|
259
259
|
}
|
|
260
260
|
};
|
|
261
|
+
function round(value, decimals) {
|
|
262
|
+
return Number(Math.round(Number(value + "e" + decimals)) + "e-" + decimals);
|
|
263
|
+
}
|
|
261
264
|
|
|
262
265
|
// src/result-set/index.ts
|
|
263
266
|
var ResultSet = class {
|
|
@@ -279,7 +282,7 @@ var ResultSet = class {
|
|
|
279
282
|
this.forceFreespins = opts.forceFreespins;
|
|
280
283
|
this.evaluate = opts.evaluate;
|
|
281
284
|
}
|
|
282
|
-
static
|
|
285
|
+
static getNumberOfSimsForCriteria(ctx, gameModeName) {
|
|
283
286
|
const rng = new RandomNumberGenerator();
|
|
284
287
|
rng.setSeed(0);
|
|
285
288
|
assert2(ctx.simRunsAmount, "Simulation configuration is not set.");
|
|
@@ -306,7 +309,7 @@ var ResultSet = class {
|
|
|
306
309
|
const criteriaToWeights = Object.fromEntries(
|
|
307
310
|
resultSets.map((rs) => [rs.criteria, rs.quota])
|
|
308
311
|
);
|
|
309
|
-
while (totalSims
|
|
312
|
+
while (totalSims !== simNums) {
|
|
310
313
|
const rs = rng.weightedRandom(criteriaToWeights);
|
|
311
314
|
if (reduceSims && numberOfSimsForCriteria[rs] > 1) {
|
|
312
315
|
numberOfSimsForCriteria[rs] -= 1;
|
|
@@ -319,18 +322,7 @@ var ResultSet = class {
|
|
|
319
322
|
);
|
|
320
323
|
reduceSims = totalSims > simNums;
|
|
321
324
|
}
|
|
322
|
-
|
|
323
|
-
const simNumsToCriteria = {};
|
|
324
|
-
Object.entries(numberOfSimsForCriteria).forEach(([criteria, num]) => {
|
|
325
|
-
for (let i = 0; i <= num; i++) {
|
|
326
|
-
allCriteria.push(criteria);
|
|
327
|
-
}
|
|
328
|
-
});
|
|
329
|
-
allCriteria = rng.shuffle(allCriteria);
|
|
330
|
-
for (let i = 1; i <= Math.min(simNums, allCriteria.length); i++) {
|
|
331
|
-
simNumsToCriteria[i] = allCriteria[i];
|
|
332
|
-
}
|
|
333
|
-
return simNumsToCriteria;
|
|
325
|
+
return numberOfSimsForCriteria;
|
|
334
326
|
}
|
|
335
327
|
/**
|
|
336
328
|
* Checks if core criteria is met, e.g. target multiplier or max win.
|
|
@@ -1287,7 +1279,7 @@ function createGameContext(opts) {
|
|
|
1287
1279
|
}
|
|
1288
1280
|
|
|
1289
1281
|
// src/book/index.ts
|
|
1290
|
-
var Book = class
|
|
1282
|
+
var Book = class {
|
|
1291
1283
|
id;
|
|
1292
1284
|
criteria = "N/A";
|
|
1293
1285
|
events = [];
|
|
@@ -1309,7 +1301,11 @@ var Book = class _Book {
|
|
|
1309
1301
|
*/
|
|
1310
1302
|
addEvent(event) {
|
|
1311
1303
|
const index = this.events.length + 1;
|
|
1312
|
-
this.events.push({
|
|
1304
|
+
this.events.push({
|
|
1305
|
+
index,
|
|
1306
|
+
type: event.type,
|
|
1307
|
+
data: copy(event.data)
|
|
1308
|
+
});
|
|
1313
1309
|
}
|
|
1314
1310
|
/**
|
|
1315
1311
|
* Intended for internal use only.
|
|
@@ -1324,17 +1320,6 @@ var Book = class _Book {
|
|
|
1324
1320
|
freespinsWins: this.freespinsWins
|
|
1325
1321
|
};
|
|
1326
1322
|
}
|
|
1327
|
-
/**
|
|
1328
|
-
* Intended for internal use only.
|
|
1329
|
-
*/
|
|
1330
|
-
static fromSerialized(data) {
|
|
1331
|
-
const book = new _Book({ id: data.id, criteria: data.criteria });
|
|
1332
|
-
book.events = data.events;
|
|
1333
|
-
book.payout = data.payout;
|
|
1334
|
-
book.basegameWins = data.basegameWins;
|
|
1335
|
-
book.freespinsWins = data.freespinsWins;
|
|
1336
|
-
return book;
|
|
1337
|
-
}
|
|
1338
1323
|
};
|
|
1339
1324
|
|
|
1340
1325
|
// src/wallet/index.ts
|
|
@@ -1560,6 +1545,107 @@ var Wallet = class {
|
|
|
1560
1545
|
|
|
1561
1546
|
// src/simulation/index.ts
|
|
1562
1547
|
import { pipeline } from "stream/promises";
|
|
1548
|
+
|
|
1549
|
+
// src/simulation/utils.ts
|
|
1550
|
+
import assert6 from "assert";
|
|
1551
|
+
function hashStringToInt(input) {
|
|
1552
|
+
let h = 2166136261;
|
|
1553
|
+
for (let i = 0; i < input.length; i++) {
|
|
1554
|
+
h ^= input.charCodeAt(i);
|
|
1555
|
+
h = Math.imul(h, 16777619);
|
|
1556
|
+
}
|
|
1557
|
+
return h >>> 0;
|
|
1558
|
+
}
|
|
1559
|
+
function splitCountsAcrossChunks(totalCounts, chunkSizes) {
|
|
1560
|
+
const total = chunkSizes.reduce((a, b) => a + b, 0);
|
|
1561
|
+
const allCriteria = Object.keys(totalCounts);
|
|
1562
|
+
const totalCountsSum = allCriteria.reduce((s, c) => s + (totalCounts[c] ?? 0), 0);
|
|
1563
|
+
assert6(
|
|
1564
|
+
totalCountsSum === total,
|
|
1565
|
+
`Counts (${totalCountsSum}) must match chunk total (${total}).`
|
|
1566
|
+
);
|
|
1567
|
+
const perChunk = chunkSizes.map(() => ({}));
|
|
1568
|
+
for (const criteria of allCriteria) {
|
|
1569
|
+
const count = totalCounts[criteria] ?? 0;
|
|
1570
|
+
if (count <= 0) {
|
|
1571
|
+
for (let i = 0; i < chunkSizes.length; i++) perChunk[i][criteria] = 0;
|
|
1572
|
+
continue;
|
|
1573
|
+
}
|
|
1574
|
+
let chunks = chunkSizes.map((size) => count * size / total);
|
|
1575
|
+
chunks = chunks.map((x) => Math.floor(x));
|
|
1576
|
+
let assigned = chunks.reduce((a, b) => a + b, 0);
|
|
1577
|
+
let remaining = count - assigned;
|
|
1578
|
+
const remainders = chunks.map((x, i) => ({ i, r: x - Math.floor(x) })).sort((a, b) => b.r - a.r);
|
|
1579
|
+
for (let i = 0; i < chunkSizes.length; i++) {
|
|
1580
|
+
perChunk[i][criteria] = chunks[i];
|
|
1581
|
+
}
|
|
1582
|
+
let idx = 0;
|
|
1583
|
+
while (remaining > 0) {
|
|
1584
|
+
perChunk[remainders[idx].i][criteria] += 1;
|
|
1585
|
+
remaining--;
|
|
1586
|
+
idx = (idx + 1) % remainders.length;
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
const chunkTotals = () => perChunk.map((m) => Object.values(m).reduce((s, v) => s + v, 0));
|
|
1590
|
+
let totals = chunkTotals();
|
|
1591
|
+
const getDeficits = () => totals.map((t, i) => chunkSizes[i] - t);
|
|
1592
|
+
let deficits = getDeficits();
|
|
1593
|
+
for (let target = 0; target < chunkSizes.length; target++) {
|
|
1594
|
+
while (deficits[target] > 0) {
|
|
1595
|
+
const src = deficits.findIndex((d) => d < 0);
|
|
1596
|
+
assert6(src !== -1, "No surplus chunk found, but deficits remain.");
|
|
1597
|
+
const crit = allCriteria.find((c) => (perChunk[src][c] ?? 0) > 0);
|
|
1598
|
+
assert6(crit, `No movable criteria found from surplus chunk ${src}.`);
|
|
1599
|
+
perChunk[src][crit] -= 1;
|
|
1600
|
+
perChunk[target][crit] = (perChunk[target][crit] ?? 0) + 1;
|
|
1601
|
+
totals[src] -= 1;
|
|
1602
|
+
totals[target] += 1;
|
|
1603
|
+
deficits[src] += 1;
|
|
1604
|
+
deficits[target] -= 1;
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
totals = chunkTotals();
|
|
1608
|
+
for (let i = 0; i < chunkSizes.length; i++) {
|
|
1609
|
+
assert6(
|
|
1610
|
+
totals[i] === chunkSizes[i],
|
|
1611
|
+
`Chunk ${i} size mismatch. Expected ${chunkSizes[i]}, got ${totals[i]}`
|
|
1612
|
+
);
|
|
1613
|
+
}
|
|
1614
|
+
for (const c of allCriteria) {
|
|
1615
|
+
const sum = perChunk.reduce((s, m) => s + (m[c] ?? 0), 0);
|
|
1616
|
+
assert6(sum === (totalCounts[c] ?? 0), `Chunk split mismatch for criteria "${c}"`);
|
|
1617
|
+
}
|
|
1618
|
+
return perChunk;
|
|
1619
|
+
}
|
|
1620
|
+
function createCriteriaSampler(counts, seed) {
|
|
1621
|
+
const rng = new RandomNumberGenerator();
|
|
1622
|
+
rng.setSeed(seed);
|
|
1623
|
+
const keys = Object.keys(counts).filter((k) => (counts[k] ?? 0) > 0);
|
|
1624
|
+
const remaining = Object.fromEntries(keys.map((k) => [k, counts[k] ?? 0]));
|
|
1625
|
+
let remainingTotal = Object.values(remaining).reduce((a, b) => a + b, 0);
|
|
1626
|
+
return () => {
|
|
1627
|
+
if (remainingTotal <= 0) return "N/A";
|
|
1628
|
+
const roll = Math.min(
|
|
1629
|
+
remainingTotal - Number.EPSILON,
|
|
1630
|
+
rng.randomFloat(0, remainingTotal)
|
|
1631
|
+
);
|
|
1632
|
+
let acc = 0;
|
|
1633
|
+
for (const k of keys) {
|
|
1634
|
+
const w = remaining[k] ?? 0;
|
|
1635
|
+
if (w <= 0) continue;
|
|
1636
|
+
acc += w;
|
|
1637
|
+
if (roll < acc) {
|
|
1638
|
+
remaining[k] = w - 1;
|
|
1639
|
+
remainingTotal--;
|
|
1640
|
+
return k;
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
remainingTotal--;
|
|
1644
|
+
return keys.find((k) => (remaining[k] ?? 0) > 0) ?? "N/A";
|
|
1645
|
+
};
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
// src/simulation/index.ts
|
|
1563
1649
|
var completedSimulations = 0;
|
|
1564
1650
|
var TEMP_FILENAME = "__temp_compiled_src_IGNORE.js";
|
|
1565
1651
|
var TEMP_FOLDER = "temp_files";
|
|
@@ -1570,25 +1656,46 @@ var Simulation = class {
|
|
|
1570
1656
|
concurrency;
|
|
1571
1657
|
debug = false;
|
|
1572
1658
|
actualSims = 0;
|
|
1573
|
-
library;
|
|
1574
1659
|
wallet;
|
|
1575
1660
|
recordsWriteStream;
|
|
1576
1661
|
hasWrittenRecord = false;
|
|
1662
|
+
maxPendingSims;
|
|
1663
|
+
maxHighWaterMark;
|
|
1664
|
+
PATHS = {};
|
|
1665
|
+
// Worker related
|
|
1666
|
+
credits = 0;
|
|
1667
|
+
creditWaiters = [];
|
|
1668
|
+
creditListenerInit = false;
|
|
1577
1669
|
constructor(opts, gameConfigOpts) {
|
|
1578
1670
|
this.gameConfig = createGameConfig(gameConfigOpts);
|
|
1579
1671
|
this.gameConfigOpts = gameConfigOpts;
|
|
1580
1672
|
this.simRunsAmount = opts.simRunsAmount || {};
|
|
1581
1673
|
this.concurrency = (opts.concurrency || 6) >= 2 ? opts.concurrency || 6 : 2;
|
|
1582
|
-
this.library = /* @__PURE__ */ new Map();
|
|
1583
1674
|
this.wallet = new Wallet();
|
|
1675
|
+
this.maxPendingSims = Math.max(10, opts.maxPendingSims ?? 250);
|
|
1676
|
+
this.maxHighWaterMark = (opts.maxDiskBuffer ?? 50) * 1024 * 1024;
|
|
1584
1677
|
const gameModeKeys = Object.keys(this.gameConfig.gameModes);
|
|
1585
|
-
|
|
1678
|
+
assert7(
|
|
1586
1679
|
Object.values(this.gameConfig.gameModes).map((m) => gameModeKeys.includes(m.name)).every((v) => v === true),
|
|
1587
1680
|
"Game mode name must match its key in the gameModes object."
|
|
1588
1681
|
);
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1682
|
+
this.PATHS.base = path.join(this.gameConfig.rootDir, this.gameConfig.outputDir);
|
|
1683
|
+
this.PATHS = {
|
|
1684
|
+
...this.PATHS,
|
|
1685
|
+
books: (mode) => path.join(this.PATHS.base, `books_${mode}.jsonl`),
|
|
1686
|
+
booksCompressed: (mode) => path.join(this.PATHS.base, "publish_files", `books_${mode}.jsonl.zst`),
|
|
1687
|
+
tempBooks: (mode, i) => path.join(this.PATHS.base, TEMP_FOLDER, `temp_books_${mode}_${i}.jsonl`),
|
|
1688
|
+
lookupTable: (mode) => path.join(this.PATHS.base, `lookUpTable_${mode}.csv`),
|
|
1689
|
+
tempLookupTable: (mode, i) => path.join(this.PATHS.base, TEMP_FOLDER, `temp_lookup_${mode}_${i}.csv`),
|
|
1690
|
+
lookupTableSegmented: (mode) => path.join(this.PATHS.base, `lookUpTableSegmented_${mode}.csv`),
|
|
1691
|
+
tempLookupTableSegmented: (mode, i) => path.join(this.PATHS.base, TEMP_FOLDER, `temp_lookup_segmented_${mode}_${i}.csv`),
|
|
1692
|
+
lookupTablePublish: (mode) => path.join(this.PATHS.base, "publish_files", `lookUpTable_${mode}_0.csv`),
|
|
1693
|
+
tempRecords: (mode) => path.join(this.PATHS.base, TEMP_FOLDER, `temp_records_${mode}.jsonl`),
|
|
1694
|
+
forceRecords: (mode) => path.join(this.PATHS.base, `force_record_${mode}.json`),
|
|
1695
|
+
indexJson: path.join(this.PATHS.base, "publish_files", "index.json"),
|
|
1696
|
+
optimizationFiles: path.join(this.PATHS.base, "optimization_files"),
|
|
1697
|
+
publishFiles: path.join(this.PATHS.base, "publish_files")
|
|
1698
|
+
};
|
|
1592
1699
|
}
|
|
1593
1700
|
async runSimulation(opts) {
|
|
1594
1701
|
const debug = opts.debug || false;
|
|
@@ -1600,11 +1707,11 @@ var Simulation = class {
|
|
|
1600
1707
|
}
|
|
1601
1708
|
this.generateReelsetFiles();
|
|
1602
1709
|
if (isMainThread) {
|
|
1710
|
+
this.preprocessFiles();
|
|
1603
1711
|
const debugDetails = {};
|
|
1604
1712
|
for (const mode of gameModesToSimulate) {
|
|
1605
1713
|
completedSimulations = 0;
|
|
1606
1714
|
this.wallet = new Wallet();
|
|
1607
|
-
this.library = /* @__PURE__ */ new Map();
|
|
1608
1715
|
this.hasWrittenRecord = false;
|
|
1609
1716
|
debugDetails[mode] = {};
|
|
1610
1717
|
console.log(`
|
|
@@ -1617,37 +1724,37 @@ Simulating game mode: ${mode}`);
|
|
|
1617
1724
|
`Tried to simulate game mode "${mode}", but it's not configured in the game config.`
|
|
1618
1725
|
);
|
|
1619
1726
|
}
|
|
1620
|
-
const booksPath =
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
path.join(this.gameConfig.rootDir, this.gameConfig.outputDir)
|
|
1727
|
+
const booksPath = this.PATHS.books(mode);
|
|
1728
|
+
const tempRecordsPath = this.PATHS.tempRecords(mode);
|
|
1729
|
+
createDirIfNotExists(this.PATHS.base);
|
|
1730
|
+
createDirIfNotExists(path.join(this.PATHS.base, TEMP_FOLDER));
|
|
1731
|
+
this.recordsWriteStream = fs2.createWriteStream(tempRecordsPath, {
|
|
1732
|
+
highWaterMark: this.maxHighWaterMark
|
|
1733
|
+
}).setMaxListeners(30);
|
|
1734
|
+
const criteriaCounts = ResultSet.getNumberOfSimsForCriteria(this, mode);
|
|
1735
|
+
const totalSims = Object.values(criteriaCounts).reduce((a, b) => a + b, 0);
|
|
1736
|
+
assert7(
|
|
1737
|
+
totalSims === runs,
|
|
1738
|
+
`Criteria mismatch for mode "${mode}". Expected ${runs}, got ${totalSims}`
|
|
1633
1739
|
);
|
|
1634
|
-
|
|
1635
|
-
|
|
1740
|
+
const chunks = this.getSimRangesForChunks(totalSims, this.concurrency);
|
|
1741
|
+
const chunkSizes = chunks.map(([s, e]) => Math.max(0, e - s + 1));
|
|
1742
|
+
const chunkCriteriaCounts = splitCountsAcrossChunks(criteriaCounts, chunkSizes);
|
|
1743
|
+
await this.spawnWorkersForGameMode({
|
|
1744
|
+
mode,
|
|
1745
|
+
chunks,
|
|
1746
|
+
chunkCriteriaCounts,
|
|
1747
|
+
totalSims
|
|
1748
|
+
});
|
|
1749
|
+
createDirIfNotExists(this.PATHS.optimizationFiles);
|
|
1750
|
+
createDirIfNotExists(this.PATHS.publishFiles);
|
|
1751
|
+
console.log(
|
|
1752
|
+
`Writing final files for game mode "${mode}". This may take a while...`
|
|
1636
1753
|
);
|
|
1637
|
-
this.recordsWriteStream = fs2.createWriteStream(tempRecordsPath);
|
|
1638
|
-
const simNumsToCriteria = ResultSet.assignCriteriaToSimulations(this, mode);
|
|
1639
|
-
await this.spawnWorkersForGameMode({ mode, simNumsToCriteria });
|
|
1640
1754
|
const finalBookStream = fs2.createWriteStream(booksPath);
|
|
1641
|
-
const numSims = Object.keys(simNumsToCriteria).length;
|
|
1642
|
-
const chunks = this.getSimRangesForChunks(numSims, this.concurrency);
|
|
1643
1755
|
let isFirstChunk = true;
|
|
1644
1756
|
for (let i = 0; i < chunks.length; i++) {
|
|
1645
|
-
const tempBookPath =
|
|
1646
|
-
this.gameConfig.rootDir,
|
|
1647
|
-
this.gameConfig.outputDir,
|
|
1648
|
-
TEMP_FOLDER,
|
|
1649
|
-
`temp_books_${mode}_${i}.jsonl`
|
|
1650
|
-
);
|
|
1757
|
+
const tempBookPath = this.PATHS.tempBooks(mode, i);
|
|
1651
1758
|
if (fs2.existsSync(tempBookPath)) {
|
|
1652
1759
|
if (!isFirstChunk) {
|
|
1653
1760
|
finalBookStream.write("\n");
|
|
@@ -1662,6 +1769,16 @@ Simulating game mode: ${mode}`);
|
|
|
1662
1769
|
}
|
|
1663
1770
|
finalBookStream.end();
|
|
1664
1771
|
await new Promise((resolve) => finalBookStream.on("finish", resolve));
|
|
1772
|
+
const lutPath = this.PATHS.lookupTable(mode);
|
|
1773
|
+
const lutPathPublish = this.PATHS.lookupTablePublish(mode);
|
|
1774
|
+
const lutSegmentedPath = this.PATHS.lookupTableSegmented(mode);
|
|
1775
|
+
await this.mergeCsv(chunks, lutPath, (i) => `temp_lookup_${mode}_${i}.csv`);
|
|
1776
|
+
fs2.copyFileSync(lutPath, lutPathPublish);
|
|
1777
|
+
await this.mergeCsv(
|
|
1778
|
+
chunks,
|
|
1779
|
+
lutSegmentedPath,
|
|
1780
|
+
(i) => `temp_lookup_segmented_${mode}_${i}.csv`
|
|
1781
|
+
);
|
|
1665
1782
|
if (this.recordsWriteStream) {
|
|
1666
1783
|
await new Promise((resolve) => {
|
|
1667
1784
|
this.recordsWriteStream.end(() => {
|
|
@@ -1670,26 +1787,21 @@ Simulating game mode: ${mode}`);
|
|
|
1670
1787
|
});
|
|
1671
1788
|
this.recordsWriteStream = void 0;
|
|
1672
1789
|
}
|
|
1673
|
-
|
|
1674
|
-
path.join(
|
|
1675
|
-
this.gameConfig.rootDir,
|
|
1676
|
-
this.gameConfig.outputDir,
|
|
1677
|
-
"optimization_files"
|
|
1678
|
-
)
|
|
1679
|
-
);
|
|
1680
|
-
createDirIfNotExists(
|
|
1681
|
-
path.join(this.gameConfig.rootDir, this.gameConfig.outputDir, "publish_files")
|
|
1682
|
-
);
|
|
1683
|
-
console.log(`Writing final files for game mode: ${mode} ...`);
|
|
1684
|
-
this.writeLookupTableCSV(mode);
|
|
1685
|
-
this.writeLookupTableSegmentedCSV(mode);
|
|
1686
|
-
this.writeRecords(mode);
|
|
1790
|
+
await this.writeRecords(mode);
|
|
1687
1791
|
await this.writeBooksJson(mode);
|
|
1688
1792
|
this.writeIndexJson();
|
|
1689
1793
|
console.log(`Mode ${mode} done!`);
|
|
1690
|
-
debugDetails[mode].rtp =
|
|
1691
|
-
|
|
1692
|
-
|
|
1794
|
+
debugDetails[mode].rtp = round(
|
|
1795
|
+
this.wallet.getCumulativeWins() / (runs * this.gameConfig.gameModes[mode].cost),
|
|
1796
|
+
3
|
|
1797
|
+
);
|
|
1798
|
+
debugDetails[mode].wins = round(this.wallet.getCumulativeWins(), 3);
|
|
1799
|
+
debugDetails[mode].winsPerSpinType = Object.fromEntries(
|
|
1800
|
+
Object.entries(this.wallet.getCumulativeWinsPerSpinType()).map(([k, v]) => [
|
|
1801
|
+
k,
|
|
1802
|
+
round(v, 3)
|
|
1803
|
+
])
|
|
1804
|
+
);
|
|
1693
1805
|
console.timeEnd(mode);
|
|
1694
1806
|
}
|
|
1695
1807
|
console.log("\n=== SIMULATION SUMMARY ===");
|
|
@@ -1699,14 +1811,14 @@ Simulating game mode: ${mode}`);
|
|
|
1699
1811
|
let actualSims = 0;
|
|
1700
1812
|
const criteriaToRetries = {};
|
|
1701
1813
|
if (!isMainThread) {
|
|
1702
|
-
const { mode, simStart, simEnd, index } = workerData;
|
|
1703
|
-
const
|
|
1814
|
+
const { mode, simStart, simEnd, index, criteriaCounts } = workerData;
|
|
1815
|
+
const seed = hashStringToInt(mode) + index >>> 0;
|
|
1816
|
+
const nextCriteria = createCriteriaSampler(criteriaCounts, seed);
|
|
1704
1817
|
for (let simId = simStart; simId <= simEnd; simId++) {
|
|
1705
1818
|
if (this.debug) desiredSims++;
|
|
1706
|
-
const criteria =
|
|
1707
|
-
if (!criteriaToRetries[criteria])
|
|
1708
|
-
|
|
1709
|
-
}
|
|
1819
|
+
const criteria = nextCriteria();
|
|
1820
|
+
if (!criteriaToRetries[criteria]) criteriaToRetries[criteria] = 0;
|
|
1821
|
+
await this.acquireCredit();
|
|
1710
1822
|
this.runSingleSimulation({ simId, mode, criteria, index });
|
|
1711
1823
|
if (this.debug) {
|
|
1712
1824
|
criteriaToRetries[criteria] += this.actualSims - 1;
|
|
@@ -1721,30 +1833,31 @@ Simulating game mode: ${mode}`);
|
|
|
1721
1833
|
type: "done",
|
|
1722
1834
|
workerNum: index
|
|
1723
1835
|
});
|
|
1836
|
+
parentPort?.removeAllListeners();
|
|
1837
|
+
parentPort?.close();
|
|
1724
1838
|
}
|
|
1725
1839
|
}
|
|
1726
1840
|
/**
|
|
1727
1841
|
* Runs all simulations for a specific game mode.
|
|
1728
1842
|
*/
|
|
1729
1843
|
async spawnWorkersForGameMode(opts) {
|
|
1730
|
-
const { mode,
|
|
1731
|
-
const numSims = Object.keys(simNumsToCriteria).length;
|
|
1732
|
-
const simRangesPerChunk = this.getSimRangesForChunks(numSims, this.concurrency);
|
|
1844
|
+
const { mode, chunks, chunkCriteriaCounts, totalSims } = opts;
|
|
1733
1845
|
await Promise.all(
|
|
1734
|
-
|
|
1846
|
+
chunks.map(([simStart, simEnd], index) => {
|
|
1735
1847
|
return this.callWorker({
|
|
1736
|
-
basePath:
|
|
1848
|
+
basePath: this.PATHS.base,
|
|
1737
1849
|
mode,
|
|
1738
1850
|
simStart,
|
|
1739
1851
|
simEnd,
|
|
1740
1852
|
index,
|
|
1741
|
-
totalSims
|
|
1853
|
+
totalSims,
|
|
1854
|
+
criteriaCounts: chunkCriteriaCounts[index]
|
|
1742
1855
|
});
|
|
1743
1856
|
})
|
|
1744
1857
|
);
|
|
1745
1858
|
}
|
|
1746
1859
|
async callWorker(opts) {
|
|
1747
|
-
const { mode, simEnd, simStart, basePath, index, totalSims } = opts;
|
|
1860
|
+
const { mode, simEnd, simStart, basePath, index, totalSims, criteriaCounts } = opts;
|
|
1748
1861
|
function logArrowProgress(current, total) {
|
|
1749
1862
|
const percentage = current / total * 100;
|
|
1750
1863
|
const progressBarLength = 50;
|
|
@@ -1755,6 +1868,11 @@ Simulating game mode: ${mode}`);
|
|
|
1755
1868
|
process.stdout.write("\n");
|
|
1756
1869
|
}
|
|
1757
1870
|
}
|
|
1871
|
+
const write = async (stream, chunk) => {
|
|
1872
|
+
if (!stream.write(chunk)) {
|
|
1873
|
+
await new Promise((resolve) => stream.once("drain", resolve));
|
|
1874
|
+
}
|
|
1875
|
+
};
|
|
1758
1876
|
return new Promise((resolve, reject) => {
|
|
1759
1877
|
const scriptPath = path.join(basePath, TEMP_FILENAME);
|
|
1760
1878
|
const worker = new Worker(scriptPath, {
|
|
@@ -1762,42 +1880,77 @@ Simulating game mode: ${mode}`);
|
|
|
1762
1880
|
mode,
|
|
1763
1881
|
simStart,
|
|
1764
1882
|
simEnd,
|
|
1765
|
-
index
|
|
1883
|
+
index,
|
|
1884
|
+
criteriaCounts
|
|
1766
1885
|
}
|
|
1767
1886
|
});
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
);
|
|
1773
|
-
const
|
|
1887
|
+
worker.postMessage({ type: "credit", amount: this.maxPendingSims });
|
|
1888
|
+
const tempBookPath = this.PATHS.tempBooks(mode, index);
|
|
1889
|
+
const bookStream = fs2.createWriteStream(tempBookPath, {
|
|
1890
|
+
highWaterMark: this.maxHighWaterMark
|
|
1891
|
+
});
|
|
1892
|
+
const tempLookupPath = this.PATHS.tempLookupTable(mode, index);
|
|
1893
|
+
const lookupStream = fs2.createWriteStream(tempLookupPath, {
|
|
1894
|
+
highWaterMark: this.maxHighWaterMark
|
|
1895
|
+
});
|
|
1896
|
+
const tempLookupSegPath = this.PATHS.tempLookupTableSegmented(mode, index);
|
|
1897
|
+
const lookupSegmentedStream = fs2.createWriteStream(tempLookupSegPath, {
|
|
1898
|
+
highWaterMark: this.maxHighWaterMark
|
|
1899
|
+
});
|
|
1900
|
+
let writeChain = Promise.resolve();
|
|
1774
1901
|
worker.on("message", (msg) => {
|
|
1775
1902
|
if (msg.type === "log") {
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
id: book.id,
|
|
1784
|
-
payoutMultiplier: book.payout,
|
|
1785
|
-
events: book.events
|
|
1786
|
-
};
|
|
1787
|
-
const prefix = book.id === simStart ? "" : "\n";
|
|
1788
|
-
bookStream.write(prefix + JSONL.stringify([bookData]));
|
|
1789
|
-
book.events = [];
|
|
1790
|
-
this.library.set(book.id, book);
|
|
1791
|
-
if (this.recordsWriteStream) {
|
|
1792
|
-
for (const record of msg.records) {
|
|
1793
|
-
const recordPrefix = this.hasWrittenRecord ? "\n" : "";
|
|
1794
|
-
this.recordsWriteStream.write(recordPrefix + JSONL.stringify([record]));
|
|
1795
|
-
this.hasWrittenRecord = true;
|
|
1903
|
+
return;
|
|
1904
|
+
}
|
|
1905
|
+
if (msg.type === "complete") {
|
|
1906
|
+
writeChain = writeChain.then(async () => {
|
|
1907
|
+
completedSimulations++;
|
|
1908
|
+
if (completedSimulations % 250 === 0) {
|
|
1909
|
+
logArrowProgress(completedSimulations, totalSims);
|
|
1796
1910
|
}
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1911
|
+
const book = msg.book;
|
|
1912
|
+
const bookData = {
|
|
1913
|
+
id: book.id,
|
|
1914
|
+
payoutMultiplier: book.payout,
|
|
1915
|
+
events: book.events
|
|
1916
|
+
};
|
|
1917
|
+
const prefix = book.id === simStart ? "" : "\n";
|
|
1918
|
+
await write(bookStream, prefix + JSONL.stringify([bookData]));
|
|
1919
|
+
await write(lookupStream, `${book.id},1,${Math.round(book.payout)}
|
|
1920
|
+
`);
|
|
1921
|
+
await write(
|
|
1922
|
+
lookupSegmentedStream,
|
|
1923
|
+
`${book.id},${book.criteria},${book.basegameWins},${book.freespinsWins}
|
|
1924
|
+
`
|
|
1925
|
+
);
|
|
1926
|
+
if (this.recordsWriteStream) {
|
|
1927
|
+
for (const record of msg.records) {
|
|
1928
|
+
const recordPrefix = this.hasWrittenRecord ? "\n" : "";
|
|
1929
|
+
await write(
|
|
1930
|
+
this.recordsWriteStream,
|
|
1931
|
+
recordPrefix + JSONL.stringify([record])
|
|
1932
|
+
);
|
|
1933
|
+
this.hasWrittenRecord = true;
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
this.wallet.mergeSerialized(msg.wallet);
|
|
1937
|
+
worker.postMessage({ type: "credit", amount: 1 });
|
|
1938
|
+
}).catch(reject);
|
|
1939
|
+
return;
|
|
1940
|
+
}
|
|
1941
|
+
if (msg.type === "done") {
|
|
1942
|
+
writeChain.then(async () => {
|
|
1943
|
+
bookStream.end();
|
|
1944
|
+
lookupStream.end();
|
|
1945
|
+
lookupSegmentedStream.end();
|
|
1946
|
+
await Promise.all([
|
|
1947
|
+
new Promise((r) => bookStream.on("finish", () => r())),
|
|
1948
|
+
new Promise((r) => lookupStream.on("finish", () => r())),
|
|
1949
|
+
new Promise((r) => lookupSegmentedStream.on("finish", () => r()))
|
|
1950
|
+
]);
|
|
1951
|
+
resolve(true);
|
|
1952
|
+
}).catch(reject);
|
|
1953
|
+
return;
|
|
1801
1954
|
}
|
|
1802
1955
|
});
|
|
1803
1956
|
worker.on("error", (error) => {
|
|
@@ -1858,6 +2011,31 @@ ${error.stack}
|
|
|
1858
2011
|
records: ctx.services.data._getRecords()
|
|
1859
2012
|
});
|
|
1860
2013
|
}
|
|
2014
|
+
initCreditListener() {
|
|
2015
|
+
if (this.creditListenerInit) return;
|
|
2016
|
+
this.creditListenerInit = true;
|
|
2017
|
+
parentPort?.on("message", (msg) => {
|
|
2018
|
+
if (msg?.type !== "credit") return;
|
|
2019
|
+
const amount = Number(msg?.amount ?? 0);
|
|
2020
|
+
if (!Number.isFinite(amount) || amount <= 0) return;
|
|
2021
|
+
this.credits += amount;
|
|
2022
|
+
while (this.credits > 0 && this.creditWaiters.length > 0) {
|
|
2023
|
+
this.credits -= 1;
|
|
2024
|
+
const resolve = this.creditWaiters.shift();
|
|
2025
|
+
resolve();
|
|
2026
|
+
}
|
|
2027
|
+
});
|
|
2028
|
+
}
|
|
2029
|
+
acquireCredit() {
|
|
2030
|
+
this.initCreditListener();
|
|
2031
|
+
if (this.credits > 0) {
|
|
2032
|
+
this.credits -= 1;
|
|
2033
|
+
return Promise.resolve();
|
|
2034
|
+
}
|
|
2035
|
+
return new Promise((resolve) => {
|
|
2036
|
+
this.creditWaiters.push(resolve);
|
|
2037
|
+
});
|
|
2038
|
+
}
|
|
1861
2039
|
/**
|
|
1862
2040
|
* If a simulation does not meet the required criteria, reset the state to run it again.
|
|
1863
2041
|
*
|
|
@@ -1885,7 +2063,7 @@ ${error.stack}
|
|
|
1885
2063
|
ctx.state.totalFreespinAmount = 0;
|
|
1886
2064
|
ctx.state.triggeredMaxWin = false;
|
|
1887
2065
|
ctx.state.triggeredFreespins = false;
|
|
1888
|
-
ctx.state.userData = ctx.config.userState
|
|
2066
|
+
ctx.state.userData = copy(ctx.config.userState);
|
|
1889
2067
|
}
|
|
1890
2068
|
/**
|
|
1891
2069
|
* Contains and executes the entire game logic:
|
|
@@ -1900,64 +2078,9 @@ ${error.stack}
|
|
|
1900
2078
|
handleGameFlow(ctx) {
|
|
1901
2079
|
this.gameConfig.hooks.onHandleGameFlow(ctx);
|
|
1902
2080
|
}
|
|
1903
|
-
/**
|
|
1904
|
-
* Creates a CSV file in the format "simulationId,weight,payout".
|
|
1905
|
-
*
|
|
1906
|
-
* `weight` defaults to 1.
|
|
1907
|
-
*/
|
|
1908
|
-
writeLookupTableCSV(gameMode) {
|
|
1909
|
-
const rows = [];
|
|
1910
|
-
for (const [bookId, book] of this.library.entries()) {
|
|
1911
|
-
rows.push(`${book.id},1,${Math.round(book.payout)}`);
|
|
1912
|
-
}
|
|
1913
|
-
rows.sort((a, b) => Number(a.split(",")[0]) - Number(b.split(",")[0]));
|
|
1914
|
-
let outputFileName = `lookUpTable_${gameMode}.csv`;
|
|
1915
|
-
let outputFilePath = path.join(
|
|
1916
|
-
this.gameConfig.rootDir,
|
|
1917
|
-
this.gameConfig.outputDir,
|
|
1918
|
-
outputFileName
|
|
1919
|
-
);
|
|
1920
|
-
writeFile(outputFilePath, rows.join("\n"));
|
|
1921
|
-
outputFileName = `lookUpTable_${gameMode}_0.csv`;
|
|
1922
|
-
outputFilePath = path.join(
|
|
1923
|
-
this.gameConfig.rootDir,
|
|
1924
|
-
this.gameConfig.outputDir,
|
|
1925
|
-
"publish_files",
|
|
1926
|
-
outputFileName
|
|
1927
|
-
);
|
|
1928
|
-
writeFile(outputFilePath, rows.join("\n"));
|
|
1929
|
-
return outputFilePath;
|
|
1930
|
-
}
|
|
1931
|
-
/**
|
|
1932
|
-
* Creates a CSV file in the format "simulationId,criteria,payoutBase,payoutFreespins".
|
|
1933
|
-
*/
|
|
1934
|
-
writeLookupTableSegmentedCSV(gameMode) {
|
|
1935
|
-
const rows = [];
|
|
1936
|
-
for (const [bookId, book] of this.library.entries()) {
|
|
1937
|
-
rows.push(`${book.id},${book.criteria},${book.basegameWins},${book.freespinsWins}`);
|
|
1938
|
-
}
|
|
1939
|
-
rows.sort((a, b) => Number(a.split(",")[0]) - Number(b.split(",")[0]));
|
|
1940
|
-
const outputFileName = `lookUpTableSegmented_${gameMode}.csv`;
|
|
1941
|
-
const outputFilePath = path.join(
|
|
1942
|
-
this.gameConfig.rootDir,
|
|
1943
|
-
this.gameConfig.outputDir,
|
|
1944
|
-
outputFileName
|
|
1945
|
-
);
|
|
1946
|
-
writeFile(outputFilePath, rows.join("\n"));
|
|
1947
|
-
return outputFilePath;
|
|
1948
|
-
}
|
|
1949
2081
|
async writeRecords(mode) {
|
|
1950
|
-
const tempRecordsPath =
|
|
1951
|
-
|
|
1952
|
-
this.gameConfig.outputDir,
|
|
1953
|
-
TEMP_FOLDER,
|
|
1954
|
-
`temp_records_${mode}.jsonl`
|
|
1955
|
-
);
|
|
1956
|
-
const forceRecordsPath = path.join(
|
|
1957
|
-
this.gameConfig.rootDir,
|
|
1958
|
-
this.gameConfig.outputDir,
|
|
1959
|
-
`force_record_${mode}.json`
|
|
1960
|
-
);
|
|
2082
|
+
const tempRecordsPath = this.PATHS.tempRecords(mode);
|
|
2083
|
+
const forceRecordsPath = this.PATHS.forceRecords(mode);
|
|
1961
2084
|
const aggregatedRecords = /* @__PURE__ */ new Map();
|
|
1962
2085
|
if (fs2.existsSync(tempRecordsPath)) {
|
|
1963
2086
|
const fileStream = fs2.createReadStream(tempRecordsPath);
|
|
@@ -2003,15 +2126,10 @@ ${error.stack}
|
|
|
2003
2126
|
fs2.rmSync(tempRecordsPath, { force: true });
|
|
2004
2127
|
}
|
|
2005
2128
|
writeIndexJson() {
|
|
2006
|
-
const outputFilePath =
|
|
2007
|
-
this.gameConfig.rootDir,
|
|
2008
|
-
this.gameConfig.outputDir,
|
|
2009
|
-
"publish_files",
|
|
2010
|
-
"index.json"
|
|
2011
|
-
);
|
|
2129
|
+
const outputFilePath = this.PATHS.indexJson;
|
|
2012
2130
|
const modes = Object.keys(this.simRunsAmount).map((id) => {
|
|
2013
2131
|
const mode = this.gameConfig.gameModes[id];
|
|
2014
|
-
|
|
2132
|
+
assert7(mode, `Game mode "${id}" not found in game config.`);
|
|
2015
2133
|
return {
|
|
2016
2134
|
name: mode.name,
|
|
2017
2135
|
cost: mode.cost,
|
|
@@ -2022,17 +2140,8 @@ ${error.stack}
|
|
|
2022
2140
|
writeFile(outputFilePath, JSON.stringify({ modes }, null, 2));
|
|
2023
2141
|
}
|
|
2024
2142
|
async writeBooksJson(gameMode) {
|
|
2025
|
-
const outputFilePath =
|
|
2026
|
-
|
|
2027
|
-
this.gameConfig.outputDir,
|
|
2028
|
-
`books_${gameMode}.jsonl`
|
|
2029
|
-
);
|
|
2030
|
-
const compressedFilePath = path.join(
|
|
2031
|
-
this.gameConfig.rootDir,
|
|
2032
|
-
this.gameConfig.outputDir,
|
|
2033
|
-
"publish_files",
|
|
2034
|
-
`books_${gameMode}.jsonl.zst`
|
|
2035
|
-
);
|
|
2143
|
+
const outputFilePath = this.PATHS.books(gameMode);
|
|
2144
|
+
const compressedFilePath = this.PATHS.booksCompressed(gameMode);
|
|
2036
2145
|
fs2.rmSync(compressedFilePath, { force: true });
|
|
2037
2146
|
if (fs2.existsSync(outputFilePath)) {
|
|
2038
2147
|
await pipeline(
|
|
@@ -2065,11 +2174,12 @@ ${error.stack}
|
|
|
2065
2174
|
});
|
|
2066
2175
|
}
|
|
2067
2176
|
getSimRangesForChunks(total, chunks) {
|
|
2068
|
-
const
|
|
2069
|
-
const
|
|
2177
|
+
const realChunks = Math.min(chunks, Math.max(total, 1));
|
|
2178
|
+
const base = Math.floor(total / realChunks);
|
|
2179
|
+
const remainder = total % realChunks;
|
|
2070
2180
|
const result = [];
|
|
2071
2181
|
let current = 1;
|
|
2072
|
-
for (let i = 0; i <
|
|
2182
|
+
for (let i = 0; i < realChunks; i++) {
|
|
2073
2183
|
const size = base + (i < remainder ? 1 : 0);
|
|
2074
2184
|
const start = current;
|
|
2075
2185
|
const end = current + size - 1;
|
|
@@ -2095,6 +2205,22 @@ ${error.stack}
|
|
|
2095
2205
|
}
|
|
2096
2206
|
}
|
|
2097
2207
|
}
|
|
2208
|
+
async mergeCsv(chunks, outPath, tempName) {
|
|
2209
|
+
fs2.rmSync(outPath, { force: true });
|
|
2210
|
+
const out = fs2.createWriteStream(outPath);
|
|
2211
|
+
let wroteAny = false;
|
|
2212
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
2213
|
+
const p = path.join(this.PATHS.base, TEMP_FOLDER, tempName(i));
|
|
2214
|
+
if (!fs2.existsSync(p)) continue;
|
|
2215
|
+
if (wroteAny) out.write("");
|
|
2216
|
+
const rs = fs2.createReadStream(p);
|
|
2217
|
+
for await (const buf of rs) out.write(buf);
|
|
2218
|
+
fs2.rmSync(p);
|
|
2219
|
+
wroteAny = true;
|
|
2220
|
+
}
|
|
2221
|
+
out.end();
|
|
2222
|
+
await new Promise((resolve) => out.on("finish", resolve));
|
|
2223
|
+
}
|
|
2098
2224
|
/**
|
|
2099
2225
|
* Confirms all pending records and adds them to the main records list.
|
|
2100
2226
|
*/
|
|
@@ -2130,7 +2256,7 @@ ${error.stack}
|
|
|
2130
2256
|
// src/analysis/index.ts
|
|
2131
2257
|
import fs3 from "fs";
|
|
2132
2258
|
import path2 from "path";
|
|
2133
|
-
import
|
|
2259
|
+
import assert8 from "assert";
|
|
2134
2260
|
|
|
2135
2261
|
// src/analysis/utils.ts
|
|
2136
2262
|
function parseLookupTable(content) {
|
|
@@ -2307,7 +2433,7 @@ var Analysis = class {
|
|
|
2307
2433
|
booksJsonlCompressed
|
|
2308
2434
|
};
|
|
2309
2435
|
for (const p of Object.values(paths[modeStr])) {
|
|
2310
|
-
|
|
2436
|
+
assert8(
|
|
2311
2437
|
fs3.existsSync(p),
|
|
2312
2438
|
`File "${p}" does not exist. Run optimization to auto-create it.`
|
|
2313
2439
|
);
|
|
@@ -2429,14 +2555,14 @@ var Analysis = class {
|
|
|
2429
2555
|
}
|
|
2430
2556
|
getGameModeConfig(mode) {
|
|
2431
2557
|
const config = this.gameConfig.gameModes[mode];
|
|
2432
|
-
|
|
2558
|
+
assert8(config, `Game mode "${mode}" not found in game config`);
|
|
2433
2559
|
return config;
|
|
2434
2560
|
}
|
|
2435
2561
|
};
|
|
2436
2562
|
|
|
2437
2563
|
// src/optimizer/index.ts
|
|
2438
2564
|
import path5 from "path";
|
|
2439
|
-
import
|
|
2565
|
+
import assert10 from "assert";
|
|
2440
2566
|
import { spawn } from "child_process";
|
|
2441
2567
|
import { isMainThread as isMainThread3 } from "worker_threads";
|
|
2442
2568
|
|
|
@@ -2537,7 +2663,7 @@ function makeSetupFile(optimizer, gameMode) {
|
|
|
2537
2663
|
}
|
|
2538
2664
|
|
|
2539
2665
|
// src/optimizer/OptimizationConditions.ts
|
|
2540
|
-
import
|
|
2666
|
+
import assert9 from "assert";
|
|
2541
2667
|
var OptimizationConditions = class {
|
|
2542
2668
|
rtp;
|
|
2543
2669
|
avgWin;
|
|
@@ -2548,14 +2674,14 @@ var OptimizationConditions = class {
|
|
|
2548
2674
|
constructor(opts) {
|
|
2549
2675
|
let { rtp, avgWin, hitRate, searchConditions, priority } = opts;
|
|
2550
2676
|
if (rtp == void 0 || rtp === "x") {
|
|
2551
|
-
|
|
2677
|
+
assert9(avgWin !== void 0 && hitRate !== void 0, "If RTP is not specified, hit-rate (hr) and average win amount (av_win) must be given.");
|
|
2552
2678
|
rtp = Math.round(avgWin / Number(hitRate) * 1e5) / 1e5;
|
|
2553
2679
|
}
|
|
2554
2680
|
let noneCount = 0;
|
|
2555
2681
|
for (const val of [rtp, avgWin, hitRate]) {
|
|
2556
2682
|
if (val === void 0) noneCount++;
|
|
2557
2683
|
}
|
|
2558
|
-
|
|
2684
|
+
assert9(noneCount <= 1, "Invalid combination of optimization conditions.");
|
|
2559
2685
|
this.searchRange = [-1, -1];
|
|
2560
2686
|
this.forceSearch = {};
|
|
2561
2687
|
if (typeof searchConditions === "number") {
|
|
@@ -2683,7 +2809,7 @@ var Optimizer = class {
|
|
|
2683
2809
|
}
|
|
2684
2810
|
gameModeRtp = Math.round(gameModeRtp * 1e3) / 1e3;
|
|
2685
2811
|
paramRtp = Math.round(paramRtp * 1e3) / 1e3;
|
|
2686
|
-
|
|
2812
|
+
assert10(
|
|
2687
2813
|
gameModeRtp === paramRtp,
|
|
2688
2814
|
`Sum of all RTP conditions (${paramRtp}) does not match the game mode RTP (${gameModeRtp}) in game mode "${k}".`
|
|
2689
2815
|
);
|
|
@@ -2822,7 +2948,7 @@ var defineSymbols = (symbols) => symbols;
|
|
|
2822
2948
|
var defineGameModes = (gameModes) => gameModes;
|
|
2823
2949
|
|
|
2824
2950
|
// src/game-mode/index.ts
|
|
2825
|
-
import
|
|
2951
|
+
import assert11 from "assert";
|
|
2826
2952
|
var GameMode = class {
|
|
2827
2953
|
name;
|
|
2828
2954
|
_reelsAmount;
|
|
@@ -2845,12 +2971,12 @@ var GameMode = class {
|
|
|
2845
2971
|
this.reelSets = opts.reelSets;
|
|
2846
2972
|
this.resultSets = opts.resultSets;
|
|
2847
2973
|
this.isBonusBuy = opts.isBonusBuy;
|
|
2848
|
-
|
|
2849
|
-
|
|
2974
|
+
assert11(this.rtp >= 0.9 && this.rtp <= 0.99, "RTP must be between 0.9 and 0.99");
|
|
2975
|
+
assert11(
|
|
2850
2976
|
this.symbolsPerReel.length === this.reelsAmount,
|
|
2851
2977
|
"symbolsPerReel length must match reelsAmount."
|
|
2852
2978
|
);
|
|
2853
|
-
|
|
2979
|
+
assert11(this.reelSets.length > 0, "GameMode must have at least one ReelSet defined.");
|
|
2854
2980
|
}
|
|
2855
2981
|
/**
|
|
2856
2982
|
* Intended for internal use only.
|
|
@@ -2863,7 +2989,7 @@ var GameMode = class {
|
|
|
2863
2989
|
* Intended for internal use only.
|
|
2864
2990
|
*/
|
|
2865
2991
|
_setSymbolsPerReel(symbolsPerReel) {
|
|
2866
|
-
|
|
2992
|
+
assert11(
|
|
2867
2993
|
symbolsPerReel.length === this._reelsAmount,
|
|
2868
2994
|
"symbolsPerReel length must match reelsAmount."
|
|
2869
2995
|
);
|
|
@@ -2929,7 +3055,7 @@ var WinType = class {
|
|
|
2929
3055
|
};
|
|
2930
3056
|
|
|
2931
3057
|
// src/win-types/LinesWinType.ts
|
|
2932
|
-
import
|
|
3058
|
+
import assert12 from "assert";
|
|
2933
3059
|
var LinesWinType = class extends WinType {
|
|
2934
3060
|
lines;
|
|
2935
3061
|
constructor(opts) {
|
|
@@ -2983,8 +3109,8 @@ var LinesWinType = class extends WinType {
|
|
|
2983
3109
|
if (!baseSymbol) {
|
|
2984
3110
|
baseSymbol = thisSymbol;
|
|
2985
3111
|
}
|
|
2986
|
-
|
|
2987
|
-
|
|
3112
|
+
assert12(baseSymbol, `No symbol found at line ${lineNum}, reel ${ridx}`);
|
|
3113
|
+
assert12(thisSymbol, `No symbol found at line ${lineNum}, reel ${ridx}`);
|
|
2988
3114
|
if (potentialWinLine.length == 0) {
|
|
2989
3115
|
if (this.isWild(thisSymbol)) {
|
|
2990
3116
|
potentialWildLine.push({ reel: ridx, row: sidx, symbol: thisSymbol });
|
|
@@ -3653,7 +3779,7 @@ var GeneratedReelSet = class extends ReelSet {
|
|
|
3653
3779
|
};
|
|
3654
3780
|
|
|
3655
3781
|
// src/reel-set/StaticReelSet.ts
|
|
3656
|
-
import
|
|
3782
|
+
import assert13 from "assert";
|
|
3657
3783
|
var StaticReelSet = class extends ReelSet {
|
|
3658
3784
|
reels;
|
|
3659
3785
|
csvPath;
|
|
@@ -3663,7 +3789,7 @@ var StaticReelSet = class extends ReelSet {
|
|
|
3663
3789
|
this.reels = [];
|
|
3664
3790
|
this._strReels = opts.reels || [];
|
|
3665
3791
|
this.csvPath = opts.csvPath || "";
|
|
3666
|
-
|
|
3792
|
+
assert13(
|
|
3667
3793
|
opts.reels || opts.csvPath,
|
|
3668
3794
|
`Either 'reels' or 'csvPath' must be provided for StaticReelSet ${this.id}`
|
|
3669
3795
|
);
|