@slot-engine/core 0.1.10 → 0.1.11
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 +338 -217
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +338 -217
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -84,7 +84,7 @@ function createGameConfig(opts) {
|
|
|
84
84
|
// src/simulation/index.ts
|
|
85
85
|
var import_fs2 = __toESM(require("fs"));
|
|
86
86
|
var import_path = __toESM(require("path"));
|
|
87
|
-
var
|
|
87
|
+
var import_assert7 = __toESM(require("assert"));
|
|
88
88
|
var import_zlib = __toESM(require("zlib"));
|
|
89
89
|
var import_readline2 = __toESM(require("readline"));
|
|
90
90
|
var import_esbuild = require("esbuild");
|
|
@@ -310,6 +310,9 @@ var JSONL = class {
|
|
|
310
310
|
});
|
|
311
311
|
}
|
|
312
312
|
};
|
|
313
|
+
function round(value, decimals) {
|
|
314
|
+
return Number(Math.round(Number(value + "e" + decimals)) + "e-" + decimals);
|
|
315
|
+
}
|
|
313
316
|
|
|
314
317
|
// src/result-set/index.ts
|
|
315
318
|
var ResultSet = class {
|
|
@@ -331,7 +334,7 @@ var ResultSet = class {
|
|
|
331
334
|
this.forceFreespins = opts.forceFreespins;
|
|
332
335
|
this.evaluate = opts.evaluate;
|
|
333
336
|
}
|
|
334
|
-
static
|
|
337
|
+
static getNumberOfSimsForCriteria(ctx, gameModeName) {
|
|
335
338
|
const rng = new RandomNumberGenerator();
|
|
336
339
|
rng.setSeed(0);
|
|
337
340
|
(0, import_assert2.default)(ctx.simRunsAmount, "Simulation configuration is not set.");
|
|
@@ -358,7 +361,7 @@ var ResultSet = class {
|
|
|
358
361
|
const criteriaToWeights = Object.fromEntries(
|
|
359
362
|
resultSets.map((rs) => [rs.criteria, rs.quota])
|
|
360
363
|
);
|
|
361
|
-
while (totalSims
|
|
364
|
+
while (totalSims !== simNums) {
|
|
362
365
|
const rs = rng.weightedRandom(criteriaToWeights);
|
|
363
366
|
if (reduceSims && numberOfSimsForCriteria[rs] > 1) {
|
|
364
367
|
numberOfSimsForCriteria[rs] -= 1;
|
|
@@ -371,18 +374,7 @@ var ResultSet = class {
|
|
|
371
374
|
);
|
|
372
375
|
reduceSims = totalSims > simNums;
|
|
373
376
|
}
|
|
374
|
-
|
|
375
|
-
const simNumsToCriteria = {};
|
|
376
|
-
Object.entries(numberOfSimsForCriteria).forEach(([criteria, num]) => {
|
|
377
|
-
for (let i = 0; i <= num; i++) {
|
|
378
|
-
allCriteria.push(criteria);
|
|
379
|
-
}
|
|
380
|
-
});
|
|
381
|
-
allCriteria = rng.shuffle(allCriteria);
|
|
382
|
-
for (let i = 1; i <= Math.min(simNums, allCriteria.length); i++) {
|
|
383
|
-
simNumsToCriteria[i] = allCriteria[i];
|
|
384
|
-
}
|
|
385
|
-
return simNumsToCriteria;
|
|
377
|
+
return numberOfSimsForCriteria;
|
|
386
378
|
}
|
|
387
379
|
/**
|
|
388
380
|
* Checks if core criteria is met, e.g. target multiplier or max win.
|
|
@@ -1339,7 +1331,7 @@ function createGameContext(opts) {
|
|
|
1339
1331
|
}
|
|
1340
1332
|
|
|
1341
1333
|
// src/book/index.ts
|
|
1342
|
-
var Book = class
|
|
1334
|
+
var Book = class {
|
|
1343
1335
|
id;
|
|
1344
1336
|
criteria = "N/A";
|
|
1345
1337
|
events = [];
|
|
@@ -1376,17 +1368,6 @@ var Book = class _Book {
|
|
|
1376
1368
|
freespinsWins: this.freespinsWins
|
|
1377
1369
|
};
|
|
1378
1370
|
}
|
|
1379
|
-
/**
|
|
1380
|
-
* Intended for internal use only.
|
|
1381
|
-
*/
|
|
1382
|
-
static fromSerialized(data) {
|
|
1383
|
-
const book = new _Book({ id: data.id, criteria: data.criteria });
|
|
1384
|
-
book.events = data.events;
|
|
1385
|
-
book.payout = data.payout;
|
|
1386
|
-
book.basegameWins = data.basegameWins;
|
|
1387
|
-
book.freespinsWins = data.freespinsWins;
|
|
1388
|
-
return book;
|
|
1389
|
-
}
|
|
1390
1371
|
};
|
|
1391
1372
|
|
|
1392
1373
|
// src/wallet/index.ts
|
|
@@ -1612,6 +1593,107 @@ var Wallet = class {
|
|
|
1612
1593
|
|
|
1613
1594
|
// src/simulation/index.ts
|
|
1614
1595
|
var import_promises = require("stream/promises");
|
|
1596
|
+
|
|
1597
|
+
// src/simulation/utils.ts
|
|
1598
|
+
var import_assert6 = __toESM(require("assert"));
|
|
1599
|
+
function hashStringToInt(input) {
|
|
1600
|
+
let h = 2166136261;
|
|
1601
|
+
for (let i = 0; i < input.length; i++) {
|
|
1602
|
+
h ^= input.charCodeAt(i);
|
|
1603
|
+
h = Math.imul(h, 16777619);
|
|
1604
|
+
}
|
|
1605
|
+
return h >>> 0;
|
|
1606
|
+
}
|
|
1607
|
+
function splitCountsAcrossChunks(totalCounts, chunkSizes) {
|
|
1608
|
+
const total = chunkSizes.reduce((a, b) => a + b, 0);
|
|
1609
|
+
const allCriteria = Object.keys(totalCounts);
|
|
1610
|
+
const totalCountsSum = allCriteria.reduce((s, c) => s + (totalCounts[c] ?? 0), 0);
|
|
1611
|
+
(0, import_assert6.default)(
|
|
1612
|
+
totalCountsSum === total,
|
|
1613
|
+
`Counts (${totalCountsSum}) must match chunk total (${total}).`
|
|
1614
|
+
);
|
|
1615
|
+
const perChunk = chunkSizes.map(() => ({}));
|
|
1616
|
+
for (const criteria of allCriteria) {
|
|
1617
|
+
const count = totalCounts[criteria] ?? 0;
|
|
1618
|
+
if (count <= 0) {
|
|
1619
|
+
for (let i = 0; i < chunkSizes.length; i++) perChunk[i][criteria] = 0;
|
|
1620
|
+
continue;
|
|
1621
|
+
}
|
|
1622
|
+
let chunks = chunkSizes.map((size) => count * size / total);
|
|
1623
|
+
chunks = chunks.map((x) => Math.floor(x));
|
|
1624
|
+
let assigned = chunks.reduce((a, b) => a + b, 0);
|
|
1625
|
+
let remaining = count - assigned;
|
|
1626
|
+
const remainders = chunks.map((x, i) => ({ i, r: x - Math.floor(x) })).sort((a, b) => b.r - a.r);
|
|
1627
|
+
for (let i = 0; i < chunkSizes.length; i++) {
|
|
1628
|
+
perChunk[i][criteria] = chunks[i];
|
|
1629
|
+
}
|
|
1630
|
+
let idx = 0;
|
|
1631
|
+
while (remaining > 0) {
|
|
1632
|
+
perChunk[remainders[idx].i][criteria] += 1;
|
|
1633
|
+
remaining--;
|
|
1634
|
+
idx = (idx + 1) % remainders.length;
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
const chunkTotals = () => perChunk.map((m) => Object.values(m).reduce((s, v) => s + v, 0));
|
|
1638
|
+
let totals = chunkTotals();
|
|
1639
|
+
const getDeficits = () => totals.map((t, i) => chunkSizes[i] - t);
|
|
1640
|
+
let deficits = getDeficits();
|
|
1641
|
+
for (let target = 0; target < chunkSizes.length; target++) {
|
|
1642
|
+
while (deficits[target] > 0) {
|
|
1643
|
+
const src = deficits.findIndex((d) => d < 0);
|
|
1644
|
+
(0, import_assert6.default)(src !== -1, "No surplus chunk found, but deficits remain.");
|
|
1645
|
+
const crit = allCriteria.find((c) => (perChunk[src][c] ?? 0) > 0);
|
|
1646
|
+
(0, import_assert6.default)(crit, `No movable criteria found from surplus chunk ${src}.`);
|
|
1647
|
+
perChunk[src][crit] -= 1;
|
|
1648
|
+
perChunk[target][crit] = (perChunk[target][crit] ?? 0) + 1;
|
|
1649
|
+
totals[src] -= 1;
|
|
1650
|
+
totals[target] += 1;
|
|
1651
|
+
deficits[src] += 1;
|
|
1652
|
+
deficits[target] -= 1;
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
totals = chunkTotals();
|
|
1656
|
+
for (let i = 0; i < chunkSizes.length; i++) {
|
|
1657
|
+
(0, import_assert6.default)(
|
|
1658
|
+
totals[i] === chunkSizes[i],
|
|
1659
|
+
`Chunk ${i} size mismatch. Expected ${chunkSizes[i]}, got ${totals[i]}`
|
|
1660
|
+
);
|
|
1661
|
+
}
|
|
1662
|
+
for (const c of allCriteria) {
|
|
1663
|
+
const sum = perChunk.reduce((s, m) => s + (m[c] ?? 0), 0);
|
|
1664
|
+
(0, import_assert6.default)(sum === (totalCounts[c] ?? 0), `Chunk split mismatch for criteria "${c}"`);
|
|
1665
|
+
}
|
|
1666
|
+
return perChunk;
|
|
1667
|
+
}
|
|
1668
|
+
function createCriteriaSampler(counts, seed) {
|
|
1669
|
+
const rng = new RandomNumberGenerator();
|
|
1670
|
+
rng.setSeed(seed);
|
|
1671
|
+
const keys = Object.keys(counts).filter((k) => (counts[k] ?? 0) > 0);
|
|
1672
|
+
const remaining = Object.fromEntries(keys.map((k) => [k, counts[k] ?? 0]));
|
|
1673
|
+
let remainingTotal = Object.values(remaining).reduce((a, b) => a + b, 0);
|
|
1674
|
+
return () => {
|
|
1675
|
+
if (remainingTotal <= 0) return "N/A";
|
|
1676
|
+
const roll = Math.min(
|
|
1677
|
+
remainingTotal - Number.EPSILON,
|
|
1678
|
+
rng.randomFloat(0, remainingTotal)
|
|
1679
|
+
);
|
|
1680
|
+
let acc = 0;
|
|
1681
|
+
for (const k of keys) {
|
|
1682
|
+
const w = remaining[k] ?? 0;
|
|
1683
|
+
if (w <= 0) continue;
|
|
1684
|
+
acc += w;
|
|
1685
|
+
if (roll < acc) {
|
|
1686
|
+
remaining[k] = w - 1;
|
|
1687
|
+
remainingTotal--;
|
|
1688
|
+
return k;
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
remainingTotal--;
|
|
1692
|
+
return keys.find((k) => (remaining[k] ?? 0) > 0) ?? "N/A";
|
|
1693
|
+
};
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
// src/simulation/index.ts
|
|
1615
1697
|
var completedSimulations = 0;
|
|
1616
1698
|
var TEMP_FILENAME = "__temp_compiled_src_IGNORE.js";
|
|
1617
1699
|
var TEMP_FOLDER = "temp_files";
|
|
@@ -1622,25 +1704,46 @@ var Simulation = class {
|
|
|
1622
1704
|
concurrency;
|
|
1623
1705
|
debug = false;
|
|
1624
1706
|
actualSims = 0;
|
|
1625
|
-
library;
|
|
1626
1707
|
wallet;
|
|
1627
1708
|
recordsWriteStream;
|
|
1628
1709
|
hasWrittenRecord = false;
|
|
1710
|
+
maxPendingSims;
|
|
1711
|
+
maxHighWaterMark;
|
|
1712
|
+
PATHS = {};
|
|
1713
|
+
// Worker related
|
|
1714
|
+
credits = 0;
|
|
1715
|
+
creditWaiters = [];
|
|
1716
|
+
creditListenerInit = false;
|
|
1629
1717
|
constructor(opts, gameConfigOpts) {
|
|
1630
1718
|
this.gameConfig = createGameConfig(gameConfigOpts);
|
|
1631
1719
|
this.gameConfigOpts = gameConfigOpts;
|
|
1632
1720
|
this.simRunsAmount = opts.simRunsAmount || {};
|
|
1633
1721
|
this.concurrency = (opts.concurrency || 6) >= 2 ? opts.concurrency || 6 : 2;
|
|
1634
|
-
this.library = /* @__PURE__ */ new Map();
|
|
1635
1722
|
this.wallet = new Wallet();
|
|
1723
|
+
this.maxPendingSims = Math.max(10, opts.maxPendingSims ?? 250);
|
|
1724
|
+
this.maxHighWaterMark = (opts.maxDiskBuffer ?? 50) * 1024 * 1024;
|
|
1636
1725
|
const gameModeKeys = Object.keys(this.gameConfig.gameModes);
|
|
1637
|
-
(0,
|
|
1726
|
+
(0, import_assert7.default)(
|
|
1638
1727
|
Object.values(this.gameConfig.gameModes).map((m) => gameModeKeys.includes(m.name)).every((v) => v === true),
|
|
1639
1728
|
"Game mode name must match its key in the gameModes object."
|
|
1640
1729
|
);
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1730
|
+
this.PATHS.base = import_path.default.join(this.gameConfig.rootDir, this.gameConfig.outputDir);
|
|
1731
|
+
this.PATHS = {
|
|
1732
|
+
...this.PATHS,
|
|
1733
|
+
books: (mode) => import_path.default.join(this.PATHS.base, `books_${mode}.jsonl`),
|
|
1734
|
+
booksCompressed: (mode) => import_path.default.join(this.PATHS.base, "publish_files", `books_${mode}.jsonl.zst`),
|
|
1735
|
+
tempBooks: (mode, i) => import_path.default.join(this.PATHS.base, TEMP_FOLDER, `temp_books_${mode}_${i}.jsonl`),
|
|
1736
|
+
lookupTable: (mode) => import_path.default.join(this.PATHS.base, `lookUpTable_${mode}.csv`),
|
|
1737
|
+
tempLookupTable: (mode, i) => import_path.default.join(this.PATHS.base, TEMP_FOLDER, `temp_lookup_${mode}_${i}.csv`),
|
|
1738
|
+
lookupTableSegmented: (mode) => import_path.default.join(this.PATHS.base, `lookUpTableSegmented_${mode}.csv`),
|
|
1739
|
+
tempLookupTableSegmented: (mode, i) => import_path.default.join(this.PATHS.base, TEMP_FOLDER, `temp_lookup_segmented_${mode}_${i}.csv`),
|
|
1740
|
+
lookupTablePublish: (mode) => import_path.default.join(this.PATHS.base, "publish_files", `lookUpTable_${mode}_0.csv`),
|
|
1741
|
+
tempRecords: (mode) => import_path.default.join(this.PATHS.base, TEMP_FOLDER, `temp_records_${mode}.jsonl`),
|
|
1742
|
+
forceRecords: (mode) => import_path.default.join(this.PATHS.base, `force_record_${mode}.json`),
|
|
1743
|
+
indexJson: import_path.default.join(this.PATHS.base, "publish_files", "index.json"),
|
|
1744
|
+
optimizationFiles: import_path.default.join(this.PATHS.base, "optimization_files"),
|
|
1745
|
+
publishFiles: import_path.default.join(this.PATHS.base, "publish_files")
|
|
1746
|
+
};
|
|
1644
1747
|
}
|
|
1645
1748
|
async runSimulation(opts) {
|
|
1646
1749
|
const debug = opts.debug || false;
|
|
@@ -1652,11 +1755,11 @@ var Simulation = class {
|
|
|
1652
1755
|
}
|
|
1653
1756
|
this.generateReelsetFiles();
|
|
1654
1757
|
if (import_worker_threads.isMainThread) {
|
|
1758
|
+
this.preprocessFiles();
|
|
1655
1759
|
const debugDetails = {};
|
|
1656
1760
|
for (const mode of gameModesToSimulate) {
|
|
1657
1761
|
completedSimulations = 0;
|
|
1658
1762
|
this.wallet = new Wallet();
|
|
1659
|
-
this.library = /* @__PURE__ */ new Map();
|
|
1660
1763
|
this.hasWrittenRecord = false;
|
|
1661
1764
|
debugDetails[mode] = {};
|
|
1662
1765
|
console.log(`
|
|
@@ -1669,37 +1772,37 @@ Simulating game mode: ${mode}`);
|
|
|
1669
1772
|
`Tried to simulate game mode "${mode}", but it's not configured in the game config.`
|
|
1670
1773
|
);
|
|
1671
1774
|
}
|
|
1672
|
-
const booksPath =
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
import_path.default.join(this.gameConfig.rootDir, this.gameConfig.outputDir)
|
|
1775
|
+
const booksPath = this.PATHS.books(mode);
|
|
1776
|
+
const tempRecordsPath = this.PATHS.tempRecords(mode);
|
|
1777
|
+
createDirIfNotExists(this.PATHS.base);
|
|
1778
|
+
createDirIfNotExists(import_path.default.join(this.PATHS.base, TEMP_FOLDER));
|
|
1779
|
+
this.recordsWriteStream = import_fs2.default.createWriteStream(tempRecordsPath, {
|
|
1780
|
+
highWaterMark: this.maxHighWaterMark
|
|
1781
|
+
});
|
|
1782
|
+
const criteriaCounts = ResultSet.getNumberOfSimsForCriteria(this, mode);
|
|
1783
|
+
const totalSims = Object.values(criteriaCounts).reduce((a, b) => a + b, 0);
|
|
1784
|
+
(0, import_assert7.default)(
|
|
1785
|
+
totalSims === runs,
|
|
1786
|
+
`Criteria mismatch for mode "${mode}". Expected ${runs}, got ${totalSims}`
|
|
1685
1787
|
);
|
|
1686
|
-
|
|
1687
|
-
|
|
1788
|
+
const chunks = this.getSimRangesForChunks(totalSims, this.concurrency);
|
|
1789
|
+
const chunkSizes = chunks.map(([s, e]) => Math.max(0, e - s + 1));
|
|
1790
|
+
const chunkCriteriaCounts = splitCountsAcrossChunks(criteriaCounts, chunkSizes);
|
|
1791
|
+
await this.spawnWorkersForGameMode({
|
|
1792
|
+
mode,
|
|
1793
|
+
chunks,
|
|
1794
|
+
chunkCriteriaCounts,
|
|
1795
|
+
totalSims
|
|
1796
|
+
});
|
|
1797
|
+
createDirIfNotExists(this.PATHS.optimizationFiles);
|
|
1798
|
+
createDirIfNotExists(this.PATHS.publishFiles);
|
|
1799
|
+
console.log(
|
|
1800
|
+
`Writing final files for game mode "${mode}". This may take a while...`
|
|
1688
1801
|
);
|
|
1689
|
-
this.recordsWriteStream = import_fs2.default.createWriteStream(tempRecordsPath);
|
|
1690
|
-
const simNumsToCriteria = ResultSet.assignCriteriaToSimulations(this, mode);
|
|
1691
|
-
await this.spawnWorkersForGameMode({ mode, simNumsToCriteria });
|
|
1692
1802
|
const finalBookStream = import_fs2.default.createWriteStream(booksPath);
|
|
1693
|
-
const numSims = Object.keys(simNumsToCriteria).length;
|
|
1694
|
-
const chunks = this.getSimRangesForChunks(numSims, this.concurrency);
|
|
1695
1803
|
let isFirstChunk = true;
|
|
1696
1804
|
for (let i = 0; i < chunks.length; i++) {
|
|
1697
|
-
const tempBookPath =
|
|
1698
|
-
this.gameConfig.rootDir,
|
|
1699
|
-
this.gameConfig.outputDir,
|
|
1700
|
-
TEMP_FOLDER,
|
|
1701
|
-
`temp_books_${mode}_${i}.jsonl`
|
|
1702
|
-
);
|
|
1805
|
+
const tempBookPath = this.PATHS.tempBooks(mode, i);
|
|
1703
1806
|
if (import_fs2.default.existsSync(tempBookPath)) {
|
|
1704
1807
|
if (!isFirstChunk) {
|
|
1705
1808
|
finalBookStream.write("\n");
|
|
@@ -1714,6 +1817,16 @@ Simulating game mode: ${mode}`);
|
|
|
1714
1817
|
}
|
|
1715
1818
|
finalBookStream.end();
|
|
1716
1819
|
await new Promise((resolve) => finalBookStream.on("finish", resolve));
|
|
1820
|
+
const lutPath = this.PATHS.lookupTable(mode);
|
|
1821
|
+
const lutPathPublish = this.PATHS.lookupTablePublish(mode);
|
|
1822
|
+
const lutSegmentedPath = this.PATHS.lookupTableSegmented(mode);
|
|
1823
|
+
await this.mergeCsv(chunks, lutPath, (i) => `temp_lookup_${mode}_${i}.csv`);
|
|
1824
|
+
import_fs2.default.copyFileSync(lutPath, lutPathPublish);
|
|
1825
|
+
await this.mergeCsv(
|
|
1826
|
+
chunks,
|
|
1827
|
+
lutSegmentedPath,
|
|
1828
|
+
(i) => `temp_lookup_segmented_${mode}_${i}.csv`
|
|
1829
|
+
);
|
|
1717
1830
|
if (this.recordsWriteStream) {
|
|
1718
1831
|
await new Promise((resolve) => {
|
|
1719
1832
|
this.recordsWriteStream.end(() => {
|
|
@@ -1722,26 +1835,21 @@ Simulating game mode: ${mode}`);
|
|
|
1722
1835
|
});
|
|
1723
1836
|
this.recordsWriteStream = void 0;
|
|
1724
1837
|
}
|
|
1725
|
-
|
|
1726
|
-
import_path.default.join(
|
|
1727
|
-
this.gameConfig.rootDir,
|
|
1728
|
-
this.gameConfig.outputDir,
|
|
1729
|
-
"optimization_files"
|
|
1730
|
-
)
|
|
1731
|
-
);
|
|
1732
|
-
createDirIfNotExists(
|
|
1733
|
-
import_path.default.join(this.gameConfig.rootDir, this.gameConfig.outputDir, "publish_files")
|
|
1734
|
-
);
|
|
1735
|
-
console.log(`Writing final files for game mode: ${mode} ...`);
|
|
1736
|
-
this.writeLookupTableCSV(mode);
|
|
1737
|
-
this.writeLookupTableSegmentedCSV(mode);
|
|
1738
|
-
this.writeRecords(mode);
|
|
1838
|
+
await this.writeRecords(mode);
|
|
1739
1839
|
await this.writeBooksJson(mode);
|
|
1740
1840
|
this.writeIndexJson();
|
|
1741
1841
|
console.log(`Mode ${mode} done!`);
|
|
1742
|
-
debugDetails[mode].rtp =
|
|
1743
|
-
|
|
1744
|
-
|
|
1842
|
+
debugDetails[mode].rtp = round(
|
|
1843
|
+
this.wallet.getCumulativeWins() / (runs * this.gameConfig.gameModes[mode].cost),
|
|
1844
|
+
3
|
|
1845
|
+
);
|
|
1846
|
+
debugDetails[mode].wins = round(this.wallet.getCumulativeWins(), 3);
|
|
1847
|
+
debugDetails[mode].winsPerSpinType = Object.fromEntries(
|
|
1848
|
+
Object.entries(this.wallet.getCumulativeWinsPerSpinType()).map(([k, v]) => [
|
|
1849
|
+
k,
|
|
1850
|
+
round(v, 3)
|
|
1851
|
+
])
|
|
1852
|
+
);
|
|
1745
1853
|
console.timeEnd(mode);
|
|
1746
1854
|
}
|
|
1747
1855
|
console.log("\n=== SIMULATION SUMMARY ===");
|
|
@@ -1751,14 +1859,14 @@ Simulating game mode: ${mode}`);
|
|
|
1751
1859
|
let actualSims = 0;
|
|
1752
1860
|
const criteriaToRetries = {};
|
|
1753
1861
|
if (!import_worker_threads.isMainThread) {
|
|
1754
|
-
const { mode, simStart, simEnd, index } = import_worker_threads.workerData;
|
|
1755
|
-
const
|
|
1862
|
+
const { mode, simStart, simEnd, index, criteriaCounts } = import_worker_threads.workerData;
|
|
1863
|
+
const seed = hashStringToInt(mode) + index >>> 0;
|
|
1864
|
+
const nextCriteria = createCriteriaSampler(criteriaCounts, seed);
|
|
1756
1865
|
for (let simId = simStart; simId <= simEnd; simId++) {
|
|
1757
1866
|
if (this.debug) desiredSims++;
|
|
1758
|
-
const criteria =
|
|
1759
|
-
if (!criteriaToRetries[criteria])
|
|
1760
|
-
|
|
1761
|
-
}
|
|
1867
|
+
const criteria = nextCriteria();
|
|
1868
|
+
if (!criteriaToRetries[criteria]) criteriaToRetries[criteria] = 0;
|
|
1869
|
+
await this.acquireCredit();
|
|
1762
1870
|
this.runSingleSimulation({ simId, mode, criteria, index });
|
|
1763
1871
|
if (this.debug) {
|
|
1764
1872
|
criteriaToRetries[criteria] += this.actualSims - 1;
|
|
@@ -1773,30 +1881,30 @@ Simulating game mode: ${mode}`);
|
|
|
1773
1881
|
type: "done",
|
|
1774
1882
|
workerNum: index
|
|
1775
1883
|
});
|
|
1884
|
+
import_worker_threads.parentPort?.close();
|
|
1776
1885
|
}
|
|
1777
1886
|
}
|
|
1778
1887
|
/**
|
|
1779
1888
|
* Runs all simulations for a specific game mode.
|
|
1780
1889
|
*/
|
|
1781
1890
|
async spawnWorkersForGameMode(opts) {
|
|
1782
|
-
const { mode,
|
|
1783
|
-
const numSims = Object.keys(simNumsToCriteria).length;
|
|
1784
|
-
const simRangesPerChunk = this.getSimRangesForChunks(numSims, this.concurrency);
|
|
1891
|
+
const { mode, chunks, chunkCriteriaCounts, totalSims } = opts;
|
|
1785
1892
|
await Promise.all(
|
|
1786
|
-
|
|
1893
|
+
chunks.map(([simStart, simEnd], index) => {
|
|
1787
1894
|
return this.callWorker({
|
|
1788
|
-
basePath:
|
|
1895
|
+
basePath: this.PATHS.base,
|
|
1789
1896
|
mode,
|
|
1790
1897
|
simStart,
|
|
1791
1898
|
simEnd,
|
|
1792
1899
|
index,
|
|
1793
|
-
totalSims
|
|
1900
|
+
totalSims,
|
|
1901
|
+
criteriaCounts: chunkCriteriaCounts[index]
|
|
1794
1902
|
});
|
|
1795
1903
|
})
|
|
1796
1904
|
);
|
|
1797
1905
|
}
|
|
1798
1906
|
async callWorker(opts) {
|
|
1799
|
-
const { mode, simEnd, simStart, basePath, index, totalSims } = opts;
|
|
1907
|
+
const { mode, simEnd, simStart, basePath, index, totalSims, criteriaCounts } = opts;
|
|
1800
1908
|
function logArrowProgress(current, total) {
|
|
1801
1909
|
const percentage = current / total * 100;
|
|
1802
1910
|
const progressBarLength = 50;
|
|
@@ -1807,6 +1915,11 @@ Simulating game mode: ${mode}`);
|
|
|
1807
1915
|
process.stdout.write("\n");
|
|
1808
1916
|
}
|
|
1809
1917
|
}
|
|
1918
|
+
const write = async (stream, chunk) => {
|
|
1919
|
+
if (!stream.write(chunk)) {
|
|
1920
|
+
await new Promise((resolve) => stream.once("drain", resolve));
|
|
1921
|
+
}
|
|
1922
|
+
};
|
|
1810
1923
|
return new Promise((resolve, reject) => {
|
|
1811
1924
|
const scriptPath = import_path.default.join(basePath, TEMP_FILENAME);
|
|
1812
1925
|
const worker = new import_worker_threads.Worker(scriptPath, {
|
|
@@ -1814,42 +1927,77 @@ Simulating game mode: ${mode}`);
|
|
|
1814
1927
|
mode,
|
|
1815
1928
|
simStart,
|
|
1816
1929
|
simEnd,
|
|
1817
|
-
index
|
|
1930
|
+
index,
|
|
1931
|
+
criteriaCounts
|
|
1818
1932
|
}
|
|
1819
1933
|
});
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
);
|
|
1825
|
-
const
|
|
1934
|
+
worker.postMessage({ type: "credit", amount: this.maxPendingSims });
|
|
1935
|
+
const tempBookPath = this.PATHS.tempBooks(mode, index);
|
|
1936
|
+
const bookStream = import_fs2.default.createWriteStream(tempBookPath, {
|
|
1937
|
+
highWaterMark: this.maxHighWaterMark
|
|
1938
|
+
});
|
|
1939
|
+
const tempLookupPath = this.PATHS.tempLookupTable(mode, index);
|
|
1940
|
+
const lookupStream = import_fs2.default.createWriteStream(tempLookupPath, {
|
|
1941
|
+
highWaterMark: this.maxHighWaterMark
|
|
1942
|
+
});
|
|
1943
|
+
const tempLookupSegPath = this.PATHS.tempLookupTableSegmented(mode, index);
|
|
1944
|
+
const lookupSegmentedStream = import_fs2.default.createWriteStream(tempLookupSegPath, {
|
|
1945
|
+
highWaterMark: this.maxHighWaterMark
|
|
1946
|
+
});
|
|
1947
|
+
let writeChain = Promise.resolve();
|
|
1826
1948
|
worker.on("message", (msg) => {
|
|
1827
1949
|
if (msg.type === "log") {
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
id: book.id,
|
|
1836
|
-
payoutMultiplier: book.payout,
|
|
1837
|
-
events: book.events
|
|
1838
|
-
};
|
|
1839
|
-
const prefix = book.id === simStart ? "" : "\n";
|
|
1840
|
-
bookStream.write(prefix + JSONL.stringify([bookData]));
|
|
1841
|
-
book.events = [];
|
|
1842
|
-
this.library.set(book.id, book);
|
|
1843
|
-
if (this.recordsWriteStream) {
|
|
1844
|
-
for (const record of msg.records) {
|
|
1845
|
-
const recordPrefix = this.hasWrittenRecord ? "\n" : "";
|
|
1846
|
-
this.recordsWriteStream.write(recordPrefix + JSONL.stringify([record]));
|
|
1847
|
-
this.hasWrittenRecord = true;
|
|
1950
|
+
return;
|
|
1951
|
+
}
|
|
1952
|
+
if (msg.type === "complete") {
|
|
1953
|
+
writeChain = writeChain.then(async () => {
|
|
1954
|
+
completedSimulations++;
|
|
1955
|
+
if (completedSimulations % 250 === 0) {
|
|
1956
|
+
logArrowProgress(completedSimulations, totalSims);
|
|
1848
1957
|
}
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1958
|
+
const book = msg.book;
|
|
1959
|
+
const bookData = {
|
|
1960
|
+
id: book.id,
|
|
1961
|
+
payoutMultiplier: book.payout,
|
|
1962
|
+
events: book.events
|
|
1963
|
+
};
|
|
1964
|
+
const prefix = book.id === simStart ? "" : "\n";
|
|
1965
|
+
await write(bookStream, prefix + JSONL.stringify([bookData]));
|
|
1966
|
+
await write(lookupStream, `${book.id},1,${Math.round(book.payout)}
|
|
1967
|
+
`);
|
|
1968
|
+
await write(
|
|
1969
|
+
lookupSegmentedStream,
|
|
1970
|
+
`${book.id},${book.criteria},${book.basegameWins},${book.freespinsWins}
|
|
1971
|
+
`
|
|
1972
|
+
);
|
|
1973
|
+
if (this.recordsWriteStream) {
|
|
1974
|
+
for (const record of msg.records) {
|
|
1975
|
+
const recordPrefix = this.hasWrittenRecord ? "\n" : "";
|
|
1976
|
+
await write(
|
|
1977
|
+
this.recordsWriteStream,
|
|
1978
|
+
recordPrefix + JSONL.stringify([record])
|
|
1979
|
+
);
|
|
1980
|
+
this.hasWrittenRecord = true;
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1983
|
+
this.wallet.mergeSerialized(msg.wallet);
|
|
1984
|
+
worker.postMessage({ type: "credit", amount: 1 });
|
|
1985
|
+
}).catch(reject);
|
|
1986
|
+
return;
|
|
1987
|
+
}
|
|
1988
|
+
if (msg.type === "done") {
|
|
1989
|
+
writeChain.then(async () => {
|
|
1990
|
+
bookStream.end();
|
|
1991
|
+
lookupStream.end();
|
|
1992
|
+
lookupSegmentedStream.end();
|
|
1993
|
+
await Promise.all([
|
|
1994
|
+
new Promise((r) => bookStream.on("finish", () => r())),
|
|
1995
|
+
new Promise((r) => lookupStream.on("finish", () => r())),
|
|
1996
|
+
new Promise((r) => lookupSegmentedStream.on("finish", () => r()))
|
|
1997
|
+
]);
|
|
1998
|
+
resolve(true);
|
|
1999
|
+
}).catch(reject);
|
|
2000
|
+
return;
|
|
1853
2001
|
}
|
|
1854
2002
|
});
|
|
1855
2003
|
worker.on("error", (error) => {
|
|
@@ -1910,6 +2058,31 @@ ${error.stack}
|
|
|
1910
2058
|
records: ctx.services.data._getRecords()
|
|
1911
2059
|
});
|
|
1912
2060
|
}
|
|
2061
|
+
initCreditListener() {
|
|
2062
|
+
if (this.creditListenerInit) return;
|
|
2063
|
+
this.creditListenerInit = true;
|
|
2064
|
+
import_worker_threads.parentPort?.on("message", (msg) => {
|
|
2065
|
+
if (msg?.type !== "credit") return;
|
|
2066
|
+
const amount = Number(msg?.amount ?? 0);
|
|
2067
|
+
if (!Number.isFinite(amount) || amount <= 0) return;
|
|
2068
|
+
this.credits += amount;
|
|
2069
|
+
while (this.credits > 0 && this.creditWaiters.length > 0) {
|
|
2070
|
+
this.credits -= 1;
|
|
2071
|
+
const resolve = this.creditWaiters.shift();
|
|
2072
|
+
resolve();
|
|
2073
|
+
}
|
|
2074
|
+
});
|
|
2075
|
+
}
|
|
2076
|
+
acquireCredit() {
|
|
2077
|
+
this.initCreditListener();
|
|
2078
|
+
if (this.credits > 0) {
|
|
2079
|
+
this.credits -= 1;
|
|
2080
|
+
return Promise.resolve();
|
|
2081
|
+
}
|
|
2082
|
+
return new Promise((resolve) => {
|
|
2083
|
+
this.creditWaiters.push(resolve);
|
|
2084
|
+
});
|
|
2085
|
+
}
|
|
1913
2086
|
/**
|
|
1914
2087
|
* If a simulation does not meet the required criteria, reset the state to run it again.
|
|
1915
2088
|
*
|
|
@@ -1952,64 +2125,9 @@ ${error.stack}
|
|
|
1952
2125
|
handleGameFlow(ctx) {
|
|
1953
2126
|
this.gameConfig.hooks.onHandleGameFlow(ctx);
|
|
1954
2127
|
}
|
|
1955
|
-
/**
|
|
1956
|
-
* Creates a CSV file in the format "simulationId,weight,payout".
|
|
1957
|
-
*
|
|
1958
|
-
* `weight` defaults to 1.
|
|
1959
|
-
*/
|
|
1960
|
-
writeLookupTableCSV(gameMode) {
|
|
1961
|
-
const rows = [];
|
|
1962
|
-
for (const [bookId, book] of this.library.entries()) {
|
|
1963
|
-
rows.push(`${book.id},1,${Math.round(book.payout)}`);
|
|
1964
|
-
}
|
|
1965
|
-
rows.sort((a, b) => Number(a.split(",")[0]) - Number(b.split(",")[0]));
|
|
1966
|
-
let outputFileName = `lookUpTable_${gameMode}.csv`;
|
|
1967
|
-
let outputFilePath = import_path.default.join(
|
|
1968
|
-
this.gameConfig.rootDir,
|
|
1969
|
-
this.gameConfig.outputDir,
|
|
1970
|
-
outputFileName
|
|
1971
|
-
);
|
|
1972
|
-
writeFile(outputFilePath, rows.join("\n"));
|
|
1973
|
-
outputFileName = `lookUpTable_${gameMode}_0.csv`;
|
|
1974
|
-
outputFilePath = import_path.default.join(
|
|
1975
|
-
this.gameConfig.rootDir,
|
|
1976
|
-
this.gameConfig.outputDir,
|
|
1977
|
-
"publish_files",
|
|
1978
|
-
outputFileName
|
|
1979
|
-
);
|
|
1980
|
-
writeFile(outputFilePath, rows.join("\n"));
|
|
1981
|
-
return outputFilePath;
|
|
1982
|
-
}
|
|
1983
|
-
/**
|
|
1984
|
-
* Creates a CSV file in the format "simulationId,criteria,payoutBase,payoutFreespins".
|
|
1985
|
-
*/
|
|
1986
|
-
writeLookupTableSegmentedCSV(gameMode) {
|
|
1987
|
-
const rows = [];
|
|
1988
|
-
for (const [bookId, book] of this.library.entries()) {
|
|
1989
|
-
rows.push(`${book.id},${book.criteria},${book.basegameWins},${book.freespinsWins}`);
|
|
1990
|
-
}
|
|
1991
|
-
rows.sort((a, b) => Number(a.split(",")[0]) - Number(b.split(",")[0]));
|
|
1992
|
-
const outputFileName = `lookUpTableSegmented_${gameMode}.csv`;
|
|
1993
|
-
const outputFilePath = import_path.default.join(
|
|
1994
|
-
this.gameConfig.rootDir,
|
|
1995
|
-
this.gameConfig.outputDir,
|
|
1996
|
-
outputFileName
|
|
1997
|
-
);
|
|
1998
|
-
writeFile(outputFilePath, rows.join("\n"));
|
|
1999
|
-
return outputFilePath;
|
|
2000
|
-
}
|
|
2001
2128
|
async writeRecords(mode) {
|
|
2002
|
-
const tempRecordsPath =
|
|
2003
|
-
|
|
2004
|
-
this.gameConfig.outputDir,
|
|
2005
|
-
TEMP_FOLDER,
|
|
2006
|
-
`temp_records_${mode}.jsonl`
|
|
2007
|
-
);
|
|
2008
|
-
const forceRecordsPath = import_path.default.join(
|
|
2009
|
-
this.gameConfig.rootDir,
|
|
2010
|
-
this.gameConfig.outputDir,
|
|
2011
|
-
`force_record_${mode}.json`
|
|
2012
|
-
);
|
|
2129
|
+
const tempRecordsPath = this.PATHS.tempRecords(mode);
|
|
2130
|
+
const forceRecordsPath = this.PATHS.forceRecords(mode);
|
|
2013
2131
|
const aggregatedRecords = /* @__PURE__ */ new Map();
|
|
2014
2132
|
if (import_fs2.default.existsSync(tempRecordsPath)) {
|
|
2015
2133
|
const fileStream = import_fs2.default.createReadStream(tempRecordsPath);
|
|
@@ -2055,15 +2173,10 @@ ${error.stack}
|
|
|
2055
2173
|
import_fs2.default.rmSync(tempRecordsPath, { force: true });
|
|
2056
2174
|
}
|
|
2057
2175
|
writeIndexJson() {
|
|
2058
|
-
const outputFilePath =
|
|
2059
|
-
this.gameConfig.rootDir,
|
|
2060
|
-
this.gameConfig.outputDir,
|
|
2061
|
-
"publish_files",
|
|
2062
|
-
"index.json"
|
|
2063
|
-
);
|
|
2176
|
+
const outputFilePath = this.PATHS.indexJson;
|
|
2064
2177
|
const modes = Object.keys(this.simRunsAmount).map((id) => {
|
|
2065
2178
|
const mode = this.gameConfig.gameModes[id];
|
|
2066
|
-
(0,
|
|
2179
|
+
(0, import_assert7.default)(mode, `Game mode "${id}" not found in game config.`);
|
|
2067
2180
|
return {
|
|
2068
2181
|
name: mode.name,
|
|
2069
2182
|
cost: mode.cost,
|
|
@@ -2074,17 +2187,8 @@ ${error.stack}
|
|
|
2074
2187
|
writeFile(outputFilePath, JSON.stringify({ modes }, null, 2));
|
|
2075
2188
|
}
|
|
2076
2189
|
async writeBooksJson(gameMode) {
|
|
2077
|
-
const outputFilePath =
|
|
2078
|
-
|
|
2079
|
-
this.gameConfig.outputDir,
|
|
2080
|
-
`books_${gameMode}.jsonl`
|
|
2081
|
-
);
|
|
2082
|
-
const compressedFilePath = import_path.default.join(
|
|
2083
|
-
this.gameConfig.rootDir,
|
|
2084
|
-
this.gameConfig.outputDir,
|
|
2085
|
-
"publish_files",
|
|
2086
|
-
`books_${gameMode}.jsonl.zst`
|
|
2087
|
-
);
|
|
2190
|
+
const outputFilePath = this.PATHS.books(gameMode);
|
|
2191
|
+
const compressedFilePath = this.PATHS.booksCompressed(gameMode);
|
|
2088
2192
|
import_fs2.default.rmSync(compressedFilePath, { force: true });
|
|
2089
2193
|
if (import_fs2.default.existsSync(outputFilePath)) {
|
|
2090
2194
|
await (0, import_promises.pipeline)(
|
|
@@ -2117,11 +2221,12 @@ ${error.stack}
|
|
|
2117
2221
|
});
|
|
2118
2222
|
}
|
|
2119
2223
|
getSimRangesForChunks(total, chunks) {
|
|
2120
|
-
const
|
|
2121
|
-
const
|
|
2224
|
+
const realChunks = Math.min(chunks, Math.max(total, 1));
|
|
2225
|
+
const base = Math.floor(total / realChunks);
|
|
2226
|
+
const remainder = total % realChunks;
|
|
2122
2227
|
const result = [];
|
|
2123
2228
|
let current = 1;
|
|
2124
|
-
for (let i = 0; i <
|
|
2229
|
+
for (let i = 0; i < realChunks; i++) {
|
|
2125
2230
|
const size = base + (i < remainder ? 1 : 0);
|
|
2126
2231
|
const start = current;
|
|
2127
2232
|
const end = current + size - 1;
|
|
@@ -2147,6 +2252,22 @@ ${error.stack}
|
|
|
2147
2252
|
}
|
|
2148
2253
|
}
|
|
2149
2254
|
}
|
|
2255
|
+
async mergeCsv(chunks, outPath, tempName) {
|
|
2256
|
+
import_fs2.default.rmSync(outPath, { force: true });
|
|
2257
|
+
const out = import_fs2.default.createWriteStream(outPath);
|
|
2258
|
+
let wroteAny = false;
|
|
2259
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
2260
|
+
const p = import_path.default.join(this.PATHS.base, TEMP_FOLDER, tempName(i));
|
|
2261
|
+
if (!import_fs2.default.existsSync(p)) continue;
|
|
2262
|
+
if (wroteAny) out.write("");
|
|
2263
|
+
const rs = import_fs2.default.createReadStream(p);
|
|
2264
|
+
for await (const buf of rs) out.write(buf);
|
|
2265
|
+
import_fs2.default.rmSync(p);
|
|
2266
|
+
wroteAny = true;
|
|
2267
|
+
}
|
|
2268
|
+
out.end();
|
|
2269
|
+
await new Promise((resolve) => out.on("finish", resolve));
|
|
2270
|
+
}
|
|
2150
2271
|
/**
|
|
2151
2272
|
* Confirms all pending records and adds them to the main records list.
|
|
2152
2273
|
*/
|
|
@@ -2182,7 +2303,7 @@ ${error.stack}
|
|
|
2182
2303
|
// src/analysis/index.ts
|
|
2183
2304
|
var import_fs3 = __toESM(require("fs"));
|
|
2184
2305
|
var import_path2 = __toESM(require("path"));
|
|
2185
|
-
var
|
|
2306
|
+
var import_assert8 = __toESM(require("assert"));
|
|
2186
2307
|
|
|
2187
2308
|
// src/analysis/utils.ts
|
|
2188
2309
|
function parseLookupTable(content) {
|
|
@@ -2359,7 +2480,7 @@ var Analysis = class {
|
|
|
2359
2480
|
booksJsonlCompressed
|
|
2360
2481
|
};
|
|
2361
2482
|
for (const p of Object.values(paths[modeStr])) {
|
|
2362
|
-
(0,
|
|
2483
|
+
(0, import_assert8.default)(
|
|
2363
2484
|
import_fs3.default.existsSync(p),
|
|
2364
2485
|
`File "${p}" does not exist. Run optimization to auto-create it.`
|
|
2365
2486
|
);
|
|
@@ -2481,14 +2602,14 @@ var Analysis = class {
|
|
|
2481
2602
|
}
|
|
2482
2603
|
getGameModeConfig(mode) {
|
|
2483
2604
|
const config = this.gameConfig.gameModes[mode];
|
|
2484
|
-
(0,
|
|
2605
|
+
(0, import_assert8.default)(config, `Game mode "${mode}" not found in game config`);
|
|
2485
2606
|
return config;
|
|
2486
2607
|
}
|
|
2487
2608
|
};
|
|
2488
2609
|
|
|
2489
2610
|
// src/optimizer/index.ts
|
|
2490
2611
|
var import_path5 = __toESM(require("path"));
|
|
2491
|
-
var
|
|
2612
|
+
var import_assert10 = __toESM(require("assert"));
|
|
2492
2613
|
var import_child_process = require("child_process");
|
|
2493
2614
|
var import_worker_threads3 = require("worker_threads");
|
|
2494
2615
|
|
|
@@ -2589,7 +2710,7 @@ function makeSetupFile(optimizer, gameMode) {
|
|
|
2589
2710
|
}
|
|
2590
2711
|
|
|
2591
2712
|
// src/optimizer/OptimizationConditions.ts
|
|
2592
|
-
var
|
|
2713
|
+
var import_assert9 = __toESM(require("assert"));
|
|
2593
2714
|
var OptimizationConditions = class {
|
|
2594
2715
|
rtp;
|
|
2595
2716
|
avgWin;
|
|
@@ -2600,14 +2721,14 @@ var OptimizationConditions = class {
|
|
|
2600
2721
|
constructor(opts) {
|
|
2601
2722
|
let { rtp, avgWin, hitRate, searchConditions, priority } = opts;
|
|
2602
2723
|
if (rtp == void 0 || rtp === "x") {
|
|
2603
|
-
(0,
|
|
2724
|
+
(0, import_assert9.default)(avgWin !== void 0 && hitRate !== void 0, "If RTP is not specified, hit-rate (hr) and average win amount (av_win) must be given.");
|
|
2604
2725
|
rtp = Math.round(avgWin / Number(hitRate) * 1e5) / 1e5;
|
|
2605
2726
|
}
|
|
2606
2727
|
let noneCount = 0;
|
|
2607
2728
|
for (const val of [rtp, avgWin, hitRate]) {
|
|
2608
2729
|
if (val === void 0) noneCount++;
|
|
2609
2730
|
}
|
|
2610
|
-
(0,
|
|
2731
|
+
(0, import_assert9.default)(noneCount <= 1, "Invalid combination of optimization conditions.");
|
|
2611
2732
|
this.searchRange = [-1, -1];
|
|
2612
2733
|
this.forceSearch = {};
|
|
2613
2734
|
if (typeof searchConditions === "number") {
|
|
@@ -2735,7 +2856,7 @@ var Optimizer = class {
|
|
|
2735
2856
|
}
|
|
2736
2857
|
gameModeRtp = Math.round(gameModeRtp * 1e3) / 1e3;
|
|
2737
2858
|
paramRtp = Math.round(paramRtp * 1e3) / 1e3;
|
|
2738
|
-
(0,
|
|
2859
|
+
(0, import_assert10.default)(
|
|
2739
2860
|
gameModeRtp === paramRtp,
|
|
2740
2861
|
`Sum of all RTP conditions (${paramRtp}) does not match the game mode RTP (${gameModeRtp}) in game mode "${k}".`
|
|
2741
2862
|
);
|
|
@@ -2874,7 +2995,7 @@ var defineSymbols = (symbols) => symbols;
|
|
|
2874
2995
|
var defineGameModes = (gameModes) => gameModes;
|
|
2875
2996
|
|
|
2876
2997
|
// src/game-mode/index.ts
|
|
2877
|
-
var
|
|
2998
|
+
var import_assert11 = __toESM(require("assert"));
|
|
2878
2999
|
var GameMode = class {
|
|
2879
3000
|
name;
|
|
2880
3001
|
_reelsAmount;
|
|
@@ -2897,12 +3018,12 @@ var GameMode = class {
|
|
|
2897
3018
|
this.reelSets = opts.reelSets;
|
|
2898
3019
|
this.resultSets = opts.resultSets;
|
|
2899
3020
|
this.isBonusBuy = opts.isBonusBuy;
|
|
2900
|
-
(0,
|
|
2901
|
-
(0,
|
|
3021
|
+
(0, import_assert11.default)(this.rtp >= 0.9 && this.rtp <= 0.99, "RTP must be between 0.9 and 0.99");
|
|
3022
|
+
(0, import_assert11.default)(
|
|
2902
3023
|
this.symbolsPerReel.length === this.reelsAmount,
|
|
2903
3024
|
"symbolsPerReel length must match reelsAmount."
|
|
2904
3025
|
);
|
|
2905
|
-
(0,
|
|
3026
|
+
(0, import_assert11.default)(this.reelSets.length > 0, "GameMode must have at least one ReelSet defined.");
|
|
2906
3027
|
}
|
|
2907
3028
|
/**
|
|
2908
3029
|
* Intended for internal use only.
|
|
@@ -2915,7 +3036,7 @@ var GameMode = class {
|
|
|
2915
3036
|
* Intended for internal use only.
|
|
2916
3037
|
*/
|
|
2917
3038
|
_setSymbolsPerReel(symbolsPerReel) {
|
|
2918
|
-
(0,
|
|
3039
|
+
(0, import_assert11.default)(
|
|
2919
3040
|
symbolsPerReel.length === this._reelsAmount,
|
|
2920
3041
|
"symbolsPerReel length must match reelsAmount."
|
|
2921
3042
|
);
|
|
@@ -2981,7 +3102,7 @@ var WinType = class {
|
|
|
2981
3102
|
};
|
|
2982
3103
|
|
|
2983
3104
|
// src/win-types/LinesWinType.ts
|
|
2984
|
-
var
|
|
3105
|
+
var import_assert12 = __toESM(require("assert"));
|
|
2985
3106
|
var LinesWinType = class extends WinType {
|
|
2986
3107
|
lines;
|
|
2987
3108
|
constructor(opts) {
|
|
@@ -3035,8 +3156,8 @@ var LinesWinType = class extends WinType {
|
|
|
3035
3156
|
if (!baseSymbol) {
|
|
3036
3157
|
baseSymbol = thisSymbol;
|
|
3037
3158
|
}
|
|
3038
|
-
(0,
|
|
3039
|
-
(0,
|
|
3159
|
+
(0, import_assert12.default)(baseSymbol, `No symbol found at line ${lineNum}, reel ${ridx}`);
|
|
3160
|
+
(0, import_assert12.default)(thisSymbol, `No symbol found at line ${lineNum}, reel ${ridx}`);
|
|
3040
3161
|
if (potentialWinLine.length == 0) {
|
|
3041
3162
|
if (this.isWild(thisSymbol)) {
|
|
3042
3163
|
potentialWildLine.push({ reel: ridx, row: sidx, symbol: thisSymbol });
|
|
@@ -3705,7 +3826,7 @@ var GeneratedReelSet = class extends ReelSet {
|
|
|
3705
3826
|
};
|
|
3706
3827
|
|
|
3707
3828
|
// src/reel-set/StaticReelSet.ts
|
|
3708
|
-
var
|
|
3829
|
+
var import_assert13 = __toESM(require("assert"));
|
|
3709
3830
|
var StaticReelSet = class extends ReelSet {
|
|
3710
3831
|
reels;
|
|
3711
3832
|
csvPath;
|
|
@@ -3715,7 +3836,7 @@ var StaticReelSet = class extends ReelSet {
|
|
|
3715
3836
|
this.reels = [];
|
|
3716
3837
|
this._strReels = opts.reels || [];
|
|
3717
3838
|
this.csvPath = opts.csvPath || "";
|
|
3718
|
-
(0,
|
|
3839
|
+
(0, import_assert13.default)(
|
|
3719
3840
|
opts.reels || opts.csvPath,
|
|
3720
3841
|
`Either 'reels' or 'csvPath' must be provided for StaticReelSet ${this.id}`
|
|
3721
3842
|
);
|