@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.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 = [];
|
|
@@ -1361,7 +1353,11 @@ var Book = class _Book {
|
|
|
1361
1353
|
*/
|
|
1362
1354
|
addEvent(event) {
|
|
1363
1355
|
const index = this.events.length + 1;
|
|
1364
|
-
this.events.push({
|
|
1356
|
+
this.events.push({
|
|
1357
|
+
index,
|
|
1358
|
+
type: event.type,
|
|
1359
|
+
data: copy(event.data)
|
|
1360
|
+
});
|
|
1365
1361
|
}
|
|
1366
1362
|
/**
|
|
1367
1363
|
* Intended for internal use only.
|
|
@@ -1376,17 +1372,6 @@ var Book = class _Book {
|
|
|
1376
1372
|
freespinsWins: this.freespinsWins
|
|
1377
1373
|
};
|
|
1378
1374
|
}
|
|
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
1375
|
};
|
|
1391
1376
|
|
|
1392
1377
|
// src/wallet/index.ts
|
|
@@ -1612,6 +1597,107 @@ var Wallet = class {
|
|
|
1612
1597
|
|
|
1613
1598
|
// src/simulation/index.ts
|
|
1614
1599
|
var import_promises = require("stream/promises");
|
|
1600
|
+
|
|
1601
|
+
// src/simulation/utils.ts
|
|
1602
|
+
var import_assert6 = __toESM(require("assert"));
|
|
1603
|
+
function hashStringToInt(input) {
|
|
1604
|
+
let h = 2166136261;
|
|
1605
|
+
for (let i = 0; i < input.length; i++) {
|
|
1606
|
+
h ^= input.charCodeAt(i);
|
|
1607
|
+
h = Math.imul(h, 16777619);
|
|
1608
|
+
}
|
|
1609
|
+
return h >>> 0;
|
|
1610
|
+
}
|
|
1611
|
+
function splitCountsAcrossChunks(totalCounts, chunkSizes) {
|
|
1612
|
+
const total = chunkSizes.reduce((a, b) => a + b, 0);
|
|
1613
|
+
const allCriteria = Object.keys(totalCounts);
|
|
1614
|
+
const totalCountsSum = allCriteria.reduce((s, c) => s + (totalCounts[c] ?? 0), 0);
|
|
1615
|
+
(0, import_assert6.default)(
|
|
1616
|
+
totalCountsSum === total,
|
|
1617
|
+
`Counts (${totalCountsSum}) must match chunk total (${total}).`
|
|
1618
|
+
);
|
|
1619
|
+
const perChunk = chunkSizes.map(() => ({}));
|
|
1620
|
+
for (const criteria of allCriteria) {
|
|
1621
|
+
const count = totalCounts[criteria] ?? 0;
|
|
1622
|
+
if (count <= 0) {
|
|
1623
|
+
for (let i = 0; i < chunkSizes.length; i++) perChunk[i][criteria] = 0;
|
|
1624
|
+
continue;
|
|
1625
|
+
}
|
|
1626
|
+
let chunks = chunkSizes.map((size) => count * size / total);
|
|
1627
|
+
chunks = chunks.map((x) => Math.floor(x));
|
|
1628
|
+
let assigned = chunks.reduce((a, b) => a + b, 0);
|
|
1629
|
+
let remaining = count - assigned;
|
|
1630
|
+
const remainders = chunks.map((x, i) => ({ i, r: x - Math.floor(x) })).sort((a, b) => b.r - a.r);
|
|
1631
|
+
for (let i = 0; i < chunkSizes.length; i++) {
|
|
1632
|
+
perChunk[i][criteria] = chunks[i];
|
|
1633
|
+
}
|
|
1634
|
+
let idx = 0;
|
|
1635
|
+
while (remaining > 0) {
|
|
1636
|
+
perChunk[remainders[idx].i][criteria] += 1;
|
|
1637
|
+
remaining--;
|
|
1638
|
+
idx = (idx + 1) % remainders.length;
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
const chunkTotals = () => perChunk.map((m) => Object.values(m).reduce((s, v) => s + v, 0));
|
|
1642
|
+
let totals = chunkTotals();
|
|
1643
|
+
const getDeficits = () => totals.map((t, i) => chunkSizes[i] - t);
|
|
1644
|
+
let deficits = getDeficits();
|
|
1645
|
+
for (let target = 0; target < chunkSizes.length; target++) {
|
|
1646
|
+
while (deficits[target] > 0) {
|
|
1647
|
+
const src = deficits.findIndex((d) => d < 0);
|
|
1648
|
+
(0, import_assert6.default)(src !== -1, "No surplus chunk found, but deficits remain.");
|
|
1649
|
+
const crit = allCriteria.find((c) => (perChunk[src][c] ?? 0) > 0);
|
|
1650
|
+
(0, import_assert6.default)(crit, `No movable criteria found from surplus chunk ${src}.`);
|
|
1651
|
+
perChunk[src][crit] -= 1;
|
|
1652
|
+
perChunk[target][crit] = (perChunk[target][crit] ?? 0) + 1;
|
|
1653
|
+
totals[src] -= 1;
|
|
1654
|
+
totals[target] += 1;
|
|
1655
|
+
deficits[src] += 1;
|
|
1656
|
+
deficits[target] -= 1;
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
totals = chunkTotals();
|
|
1660
|
+
for (let i = 0; i < chunkSizes.length; i++) {
|
|
1661
|
+
(0, import_assert6.default)(
|
|
1662
|
+
totals[i] === chunkSizes[i],
|
|
1663
|
+
`Chunk ${i} size mismatch. Expected ${chunkSizes[i]}, got ${totals[i]}`
|
|
1664
|
+
);
|
|
1665
|
+
}
|
|
1666
|
+
for (const c of allCriteria) {
|
|
1667
|
+
const sum = perChunk.reduce((s, m) => s + (m[c] ?? 0), 0);
|
|
1668
|
+
(0, import_assert6.default)(sum === (totalCounts[c] ?? 0), `Chunk split mismatch for criteria "${c}"`);
|
|
1669
|
+
}
|
|
1670
|
+
return perChunk;
|
|
1671
|
+
}
|
|
1672
|
+
function createCriteriaSampler(counts, seed) {
|
|
1673
|
+
const rng = new RandomNumberGenerator();
|
|
1674
|
+
rng.setSeed(seed);
|
|
1675
|
+
const keys = Object.keys(counts).filter((k) => (counts[k] ?? 0) > 0);
|
|
1676
|
+
const remaining = Object.fromEntries(keys.map((k) => [k, counts[k] ?? 0]));
|
|
1677
|
+
let remainingTotal = Object.values(remaining).reduce((a, b) => a + b, 0);
|
|
1678
|
+
return () => {
|
|
1679
|
+
if (remainingTotal <= 0) return "N/A";
|
|
1680
|
+
const roll = Math.min(
|
|
1681
|
+
remainingTotal - Number.EPSILON,
|
|
1682
|
+
rng.randomFloat(0, remainingTotal)
|
|
1683
|
+
);
|
|
1684
|
+
let acc = 0;
|
|
1685
|
+
for (const k of keys) {
|
|
1686
|
+
const w = remaining[k] ?? 0;
|
|
1687
|
+
if (w <= 0) continue;
|
|
1688
|
+
acc += w;
|
|
1689
|
+
if (roll < acc) {
|
|
1690
|
+
remaining[k] = w - 1;
|
|
1691
|
+
remainingTotal--;
|
|
1692
|
+
return k;
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
remainingTotal--;
|
|
1696
|
+
return keys.find((k) => (remaining[k] ?? 0) > 0) ?? "N/A";
|
|
1697
|
+
};
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
// src/simulation/index.ts
|
|
1615
1701
|
var completedSimulations = 0;
|
|
1616
1702
|
var TEMP_FILENAME = "__temp_compiled_src_IGNORE.js";
|
|
1617
1703
|
var TEMP_FOLDER = "temp_files";
|
|
@@ -1622,25 +1708,46 @@ var Simulation = class {
|
|
|
1622
1708
|
concurrency;
|
|
1623
1709
|
debug = false;
|
|
1624
1710
|
actualSims = 0;
|
|
1625
|
-
library;
|
|
1626
1711
|
wallet;
|
|
1627
1712
|
recordsWriteStream;
|
|
1628
1713
|
hasWrittenRecord = false;
|
|
1714
|
+
maxPendingSims;
|
|
1715
|
+
maxHighWaterMark;
|
|
1716
|
+
PATHS = {};
|
|
1717
|
+
// Worker related
|
|
1718
|
+
credits = 0;
|
|
1719
|
+
creditWaiters = [];
|
|
1720
|
+
creditListenerInit = false;
|
|
1629
1721
|
constructor(opts, gameConfigOpts) {
|
|
1630
1722
|
this.gameConfig = createGameConfig(gameConfigOpts);
|
|
1631
1723
|
this.gameConfigOpts = gameConfigOpts;
|
|
1632
1724
|
this.simRunsAmount = opts.simRunsAmount || {};
|
|
1633
1725
|
this.concurrency = (opts.concurrency || 6) >= 2 ? opts.concurrency || 6 : 2;
|
|
1634
|
-
this.library = /* @__PURE__ */ new Map();
|
|
1635
1726
|
this.wallet = new Wallet();
|
|
1727
|
+
this.maxPendingSims = Math.max(10, opts.maxPendingSims ?? 250);
|
|
1728
|
+
this.maxHighWaterMark = (opts.maxDiskBuffer ?? 50) * 1024 * 1024;
|
|
1636
1729
|
const gameModeKeys = Object.keys(this.gameConfig.gameModes);
|
|
1637
|
-
(0,
|
|
1730
|
+
(0, import_assert7.default)(
|
|
1638
1731
|
Object.values(this.gameConfig.gameModes).map((m) => gameModeKeys.includes(m.name)).every((v) => v === true),
|
|
1639
1732
|
"Game mode name must match its key in the gameModes object."
|
|
1640
1733
|
);
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1734
|
+
this.PATHS.base = import_path.default.join(this.gameConfig.rootDir, this.gameConfig.outputDir);
|
|
1735
|
+
this.PATHS = {
|
|
1736
|
+
...this.PATHS,
|
|
1737
|
+
books: (mode) => import_path.default.join(this.PATHS.base, `books_${mode}.jsonl`),
|
|
1738
|
+
booksCompressed: (mode) => import_path.default.join(this.PATHS.base, "publish_files", `books_${mode}.jsonl.zst`),
|
|
1739
|
+
tempBooks: (mode, i) => import_path.default.join(this.PATHS.base, TEMP_FOLDER, `temp_books_${mode}_${i}.jsonl`),
|
|
1740
|
+
lookupTable: (mode) => import_path.default.join(this.PATHS.base, `lookUpTable_${mode}.csv`),
|
|
1741
|
+
tempLookupTable: (mode, i) => import_path.default.join(this.PATHS.base, TEMP_FOLDER, `temp_lookup_${mode}_${i}.csv`),
|
|
1742
|
+
lookupTableSegmented: (mode) => import_path.default.join(this.PATHS.base, `lookUpTableSegmented_${mode}.csv`),
|
|
1743
|
+
tempLookupTableSegmented: (mode, i) => import_path.default.join(this.PATHS.base, TEMP_FOLDER, `temp_lookup_segmented_${mode}_${i}.csv`),
|
|
1744
|
+
lookupTablePublish: (mode) => import_path.default.join(this.PATHS.base, "publish_files", `lookUpTable_${mode}_0.csv`),
|
|
1745
|
+
tempRecords: (mode) => import_path.default.join(this.PATHS.base, TEMP_FOLDER, `temp_records_${mode}.jsonl`),
|
|
1746
|
+
forceRecords: (mode) => import_path.default.join(this.PATHS.base, `force_record_${mode}.json`),
|
|
1747
|
+
indexJson: import_path.default.join(this.PATHS.base, "publish_files", "index.json"),
|
|
1748
|
+
optimizationFiles: import_path.default.join(this.PATHS.base, "optimization_files"),
|
|
1749
|
+
publishFiles: import_path.default.join(this.PATHS.base, "publish_files")
|
|
1750
|
+
};
|
|
1644
1751
|
}
|
|
1645
1752
|
async runSimulation(opts) {
|
|
1646
1753
|
const debug = opts.debug || false;
|
|
@@ -1652,11 +1759,11 @@ var Simulation = class {
|
|
|
1652
1759
|
}
|
|
1653
1760
|
this.generateReelsetFiles();
|
|
1654
1761
|
if (import_worker_threads.isMainThread) {
|
|
1762
|
+
this.preprocessFiles();
|
|
1655
1763
|
const debugDetails = {};
|
|
1656
1764
|
for (const mode of gameModesToSimulate) {
|
|
1657
1765
|
completedSimulations = 0;
|
|
1658
1766
|
this.wallet = new Wallet();
|
|
1659
|
-
this.library = /* @__PURE__ */ new Map();
|
|
1660
1767
|
this.hasWrittenRecord = false;
|
|
1661
1768
|
debugDetails[mode] = {};
|
|
1662
1769
|
console.log(`
|
|
@@ -1669,37 +1776,37 @@ Simulating game mode: ${mode}`);
|
|
|
1669
1776
|
`Tried to simulate game mode "${mode}", but it's not configured in the game config.`
|
|
1670
1777
|
);
|
|
1671
1778
|
}
|
|
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)
|
|
1779
|
+
const booksPath = this.PATHS.books(mode);
|
|
1780
|
+
const tempRecordsPath = this.PATHS.tempRecords(mode);
|
|
1781
|
+
createDirIfNotExists(this.PATHS.base);
|
|
1782
|
+
createDirIfNotExists(import_path.default.join(this.PATHS.base, TEMP_FOLDER));
|
|
1783
|
+
this.recordsWriteStream = import_fs2.default.createWriteStream(tempRecordsPath, {
|
|
1784
|
+
highWaterMark: this.maxHighWaterMark
|
|
1785
|
+
}).setMaxListeners(30);
|
|
1786
|
+
const criteriaCounts = ResultSet.getNumberOfSimsForCriteria(this, mode);
|
|
1787
|
+
const totalSims = Object.values(criteriaCounts).reduce((a, b) => a + b, 0);
|
|
1788
|
+
(0, import_assert7.default)(
|
|
1789
|
+
totalSims === runs,
|
|
1790
|
+
`Criteria mismatch for mode "${mode}". Expected ${runs}, got ${totalSims}`
|
|
1685
1791
|
);
|
|
1686
|
-
|
|
1687
|
-
|
|
1792
|
+
const chunks = this.getSimRangesForChunks(totalSims, this.concurrency);
|
|
1793
|
+
const chunkSizes = chunks.map(([s, e]) => Math.max(0, e - s + 1));
|
|
1794
|
+
const chunkCriteriaCounts = splitCountsAcrossChunks(criteriaCounts, chunkSizes);
|
|
1795
|
+
await this.spawnWorkersForGameMode({
|
|
1796
|
+
mode,
|
|
1797
|
+
chunks,
|
|
1798
|
+
chunkCriteriaCounts,
|
|
1799
|
+
totalSims
|
|
1800
|
+
});
|
|
1801
|
+
createDirIfNotExists(this.PATHS.optimizationFiles);
|
|
1802
|
+
createDirIfNotExists(this.PATHS.publishFiles);
|
|
1803
|
+
console.log(
|
|
1804
|
+
`Writing final files for game mode "${mode}". This may take a while...`
|
|
1688
1805
|
);
|
|
1689
|
-
this.recordsWriteStream = import_fs2.default.createWriteStream(tempRecordsPath);
|
|
1690
|
-
const simNumsToCriteria = ResultSet.assignCriteriaToSimulations(this, mode);
|
|
1691
|
-
await this.spawnWorkersForGameMode({ mode, simNumsToCriteria });
|
|
1692
1806
|
const finalBookStream = import_fs2.default.createWriteStream(booksPath);
|
|
1693
|
-
const numSims = Object.keys(simNumsToCriteria).length;
|
|
1694
|
-
const chunks = this.getSimRangesForChunks(numSims, this.concurrency);
|
|
1695
1807
|
let isFirstChunk = true;
|
|
1696
1808
|
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
|
-
);
|
|
1809
|
+
const tempBookPath = this.PATHS.tempBooks(mode, i);
|
|
1703
1810
|
if (import_fs2.default.existsSync(tempBookPath)) {
|
|
1704
1811
|
if (!isFirstChunk) {
|
|
1705
1812
|
finalBookStream.write("\n");
|
|
@@ -1714,6 +1821,16 @@ Simulating game mode: ${mode}`);
|
|
|
1714
1821
|
}
|
|
1715
1822
|
finalBookStream.end();
|
|
1716
1823
|
await new Promise((resolve) => finalBookStream.on("finish", resolve));
|
|
1824
|
+
const lutPath = this.PATHS.lookupTable(mode);
|
|
1825
|
+
const lutPathPublish = this.PATHS.lookupTablePublish(mode);
|
|
1826
|
+
const lutSegmentedPath = this.PATHS.lookupTableSegmented(mode);
|
|
1827
|
+
await this.mergeCsv(chunks, lutPath, (i) => `temp_lookup_${mode}_${i}.csv`);
|
|
1828
|
+
import_fs2.default.copyFileSync(lutPath, lutPathPublish);
|
|
1829
|
+
await this.mergeCsv(
|
|
1830
|
+
chunks,
|
|
1831
|
+
lutSegmentedPath,
|
|
1832
|
+
(i) => `temp_lookup_segmented_${mode}_${i}.csv`
|
|
1833
|
+
);
|
|
1717
1834
|
if (this.recordsWriteStream) {
|
|
1718
1835
|
await new Promise((resolve) => {
|
|
1719
1836
|
this.recordsWriteStream.end(() => {
|
|
@@ -1722,26 +1839,21 @@ Simulating game mode: ${mode}`);
|
|
|
1722
1839
|
});
|
|
1723
1840
|
this.recordsWriteStream = void 0;
|
|
1724
1841
|
}
|
|
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);
|
|
1842
|
+
await this.writeRecords(mode);
|
|
1739
1843
|
await this.writeBooksJson(mode);
|
|
1740
1844
|
this.writeIndexJson();
|
|
1741
1845
|
console.log(`Mode ${mode} done!`);
|
|
1742
|
-
debugDetails[mode].rtp =
|
|
1743
|
-
|
|
1744
|
-
|
|
1846
|
+
debugDetails[mode].rtp = round(
|
|
1847
|
+
this.wallet.getCumulativeWins() / (runs * this.gameConfig.gameModes[mode].cost),
|
|
1848
|
+
3
|
|
1849
|
+
);
|
|
1850
|
+
debugDetails[mode].wins = round(this.wallet.getCumulativeWins(), 3);
|
|
1851
|
+
debugDetails[mode].winsPerSpinType = Object.fromEntries(
|
|
1852
|
+
Object.entries(this.wallet.getCumulativeWinsPerSpinType()).map(([k, v]) => [
|
|
1853
|
+
k,
|
|
1854
|
+
round(v, 3)
|
|
1855
|
+
])
|
|
1856
|
+
);
|
|
1745
1857
|
console.timeEnd(mode);
|
|
1746
1858
|
}
|
|
1747
1859
|
console.log("\n=== SIMULATION SUMMARY ===");
|
|
@@ -1751,14 +1863,14 @@ Simulating game mode: ${mode}`);
|
|
|
1751
1863
|
let actualSims = 0;
|
|
1752
1864
|
const criteriaToRetries = {};
|
|
1753
1865
|
if (!import_worker_threads.isMainThread) {
|
|
1754
|
-
const { mode, simStart, simEnd, index } = import_worker_threads.workerData;
|
|
1755
|
-
const
|
|
1866
|
+
const { mode, simStart, simEnd, index, criteriaCounts } = import_worker_threads.workerData;
|
|
1867
|
+
const seed = hashStringToInt(mode) + index >>> 0;
|
|
1868
|
+
const nextCriteria = createCriteriaSampler(criteriaCounts, seed);
|
|
1756
1869
|
for (let simId = simStart; simId <= simEnd; simId++) {
|
|
1757
1870
|
if (this.debug) desiredSims++;
|
|
1758
|
-
const criteria =
|
|
1759
|
-
if (!criteriaToRetries[criteria])
|
|
1760
|
-
|
|
1761
|
-
}
|
|
1871
|
+
const criteria = nextCriteria();
|
|
1872
|
+
if (!criteriaToRetries[criteria]) criteriaToRetries[criteria] = 0;
|
|
1873
|
+
await this.acquireCredit();
|
|
1762
1874
|
this.runSingleSimulation({ simId, mode, criteria, index });
|
|
1763
1875
|
if (this.debug) {
|
|
1764
1876
|
criteriaToRetries[criteria] += this.actualSims - 1;
|
|
@@ -1773,30 +1885,31 @@ Simulating game mode: ${mode}`);
|
|
|
1773
1885
|
type: "done",
|
|
1774
1886
|
workerNum: index
|
|
1775
1887
|
});
|
|
1888
|
+
import_worker_threads.parentPort?.removeAllListeners();
|
|
1889
|
+
import_worker_threads.parentPort?.close();
|
|
1776
1890
|
}
|
|
1777
1891
|
}
|
|
1778
1892
|
/**
|
|
1779
1893
|
* Runs all simulations for a specific game mode.
|
|
1780
1894
|
*/
|
|
1781
1895
|
async spawnWorkersForGameMode(opts) {
|
|
1782
|
-
const { mode,
|
|
1783
|
-
const numSims = Object.keys(simNumsToCriteria).length;
|
|
1784
|
-
const simRangesPerChunk = this.getSimRangesForChunks(numSims, this.concurrency);
|
|
1896
|
+
const { mode, chunks, chunkCriteriaCounts, totalSims } = opts;
|
|
1785
1897
|
await Promise.all(
|
|
1786
|
-
|
|
1898
|
+
chunks.map(([simStart, simEnd], index) => {
|
|
1787
1899
|
return this.callWorker({
|
|
1788
|
-
basePath:
|
|
1900
|
+
basePath: this.PATHS.base,
|
|
1789
1901
|
mode,
|
|
1790
1902
|
simStart,
|
|
1791
1903
|
simEnd,
|
|
1792
1904
|
index,
|
|
1793
|
-
totalSims
|
|
1905
|
+
totalSims,
|
|
1906
|
+
criteriaCounts: chunkCriteriaCounts[index]
|
|
1794
1907
|
});
|
|
1795
1908
|
})
|
|
1796
1909
|
);
|
|
1797
1910
|
}
|
|
1798
1911
|
async callWorker(opts) {
|
|
1799
|
-
const { mode, simEnd, simStart, basePath, index, totalSims } = opts;
|
|
1912
|
+
const { mode, simEnd, simStart, basePath, index, totalSims, criteriaCounts } = opts;
|
|
1800
1913
|
function logArrowProgress(current, total) {
|
|
1801
1914
|
const percentage = current / total * 100;
|
|
1802
1915
|
const progressBarLength = 50;
|
|
@@ -1807,6 +1920,11 @@ Simulating game mode: ${mode}`);
|
|
|
1807
1920
|
process.stdout.write("\n");
|
|
1808
1921
|
}
|
|
1809
1922
|
}
|
|
1923
|
+
const write = async (stream, chunk) => {
|
|
1924
|
+
if (!stream.write(chunk)) {
|
|
1925
|
+
await new Promise((resolve) => stream.once("drain", resolve));
|
|
1926
|
+
}
|
|
1927
|
+
};
|
|
1810
1928
|
return new Promise((resolve, reject) => {
|
|
1811
1929
|
const scriptPath = import_path.default.join(basePath, TEMP_FILENAME);
|
|
1812
1930
|
const worker = new import_worker_threads.Worker(scriptPath, {
|
|
@@ -1814,42 +1932,77 @@ Simulating game mode: ${mode}`);
|
|
|
1814
1932
|
mode,
|
|
1815
1933
|
simStart,
|
|
1816
1934
|
simEnd,
|
|
1817
|
-
index
|
|
1935
|
+
index,
|
|
1936
|
+
criteriaCounts
|
|
1818
1937
|
}
|
|
1819
1938
|
});
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
);
|
|
1825
|
-
const
|
|
1939
|
+
worker.postMessage({ type: "credit", amount: this.maxPendingSims });
|
|
1940
|
+
const tempBookPath = this.PATHS.tempBooks(mode, index);
|
|
1941
|
+
const bookStream = import_fs2.default.createWriteStream(tempBookPath, {
|
|
1942
|
+
highWaterMark: this.maxHighWaterMark
|
|
1943
|
+
});
|
|
1944
|
+
const tempLookupPath = this.PATHS.tempLookupTable(mode, index);
|
|
1945
|
+
const lookupStream = import_fs2.default.createWriteStream(tempLookupPath, {
|
|
1946
|
+
highWaterMark: this.maxHighWaterMark
|
|
1947
|
+
});
|
|
1948
|
+
const tempLookupSegPath = this.PATHS.tempLookupTableSegmented(mode, index);
|
|
1949
|
+
const lookupSegmentedStream = import_fs2.default.createWriteStream(tempLookupSegPath, {
|
|
1950
|
+
highWaterMark: this.maxHighWaterMark
|
|
1951
|
+
});
|
|
1952
|
+
let writeChain = Promise.resolve();
|
|
1826
1953
|
worker.on("message", (msg) => {
|
|
1827
1954
|
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;
|
|
1955
|
+
return;
|
|
1956
|
+
}
|
|
1957
|
+
if (msg.type === "complete") {
|
|
1958
|
+
writeChain = writeChain.then(async () => {
|
|
1959
|
+
completedSimulations++;
|
|
1960
|
+
if (completedSimulations % 250 === 0) {
|
|
1961
|
+
logArrowProgress(completedSimulations, totalSims);
|
|
1848
1962
|
}
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1963
|
+
const book = msg.book;
|
|
1964
|
+
const bookData = {
|
|
1965
|
+
id: book.id,
|
|
1966
|
+
payoutMultiplier: book.payout,
|
|
1967
|
+
events: book.events
|
|
1968
|
+
};
|
|
1969
|
+
const prefix = book.id === simStart ? "" : "\n";
|
|
1970
|
+
await write(bookStream, prefix + JSONL.stringify([bookData]));
|
|
1971
|
+
await write(lookupStream, `${book.id},1,${Math.round(book.payout)}
|
|
1972
|
+
`);
|
|
1973
|
+
await write(
|
|
1974
|
+
lookupSegmentedStream,
|
|
1975
|
+
`${book.id},${book.criteria},${book.basegameWins},${book.freespinsWins}
|
|
1976
|
+
`
|
|
1977
|
+
);
|
|
1978
|
+
if (this.recordsWriteStream) {
|
|
1979
|
+
for (const record of msg.records) {
|
|
1980
|
+
const recordPrefix = this.hasWrittenRecord ? "\n" : "";
|
|
1981
|
+
await write(
|
|
1982
|
+
this.recordsWriteStream,
|
|
1983
|
+
recordPrefix + JSONL.stringify([record])
|
|
1984
|
+
);
|
|
1985
|
+
this.hasWrittenRecord = true;
|
|
1986
|
+
}
|
|
1987
|
+
}
|
|
1988
|
+
this.wallet.mergeSerialized(msg.wallet);
|
|
1989
|
+
worker.postMessage({ type: "credit", amount: 1 });
|
|
1990
|
+
}).catch(reject);
|
|
1991
|
+
return;
|
|
1992
|
+
}
|
|
1993
|
+
if (msg.type === "done") {
|
|
1994
|
+
writeChain.then(async () => {
|
|
1995
|
+
bookStream.end();
|
|
1996
|
+
lookupStream.end();
|
|
1997
|
+
lookupSegmentedStream.end();
|
|
1998
|
+
await Promise.all([
|
|
1999
|
+
new Promise((r) => bookStream.on("finish", () => r())),
|
|
2000
|
+
new Promise((r) => lookupStream.on("finish", () => r())),
|
|
2001
|
+
new Promise((r) => lookupSegmentedStream.on("finish", () => r()))
|
|
2002
|
+
]);
|
|
2003
|
+
resolve(true);
|
|
2004
|
+
}).catch(reject);
|
|
2005
|
+
return;
|
|
1853
2006
|
}
|
|
1854
2007
|
});
|
|
1855
2008
|
worker.on("error", (error) => {
|
|
@@ -1910,6 +2063,31 @@ ${error.stack}
|
|
|
1910
2063
|
records: ctx.services.data._getRecords()
|
|
1911
2064
|
});
|
|
1912
2065
|
}
|
|
2066
|
+
initCreditListener() {
|
|
2067
|
+
if (this.creditListenerInit) return;
|
|
2068
|
+
this.creditListenerInit = true;
|
|
2069
|
+
import_worker_threads.parentPort?.on("message", (msg) => {
|
|
2070
|
+
if (msg?.type !== "credit") return;
|
|
2071
|
+
const amount = Number(msg?.amount ?? 0);
|
|
2072
|
+
if (!Number.isFinite(amount) || amount <= 0) return;
|
|
2073
|
+
this.credits += amount;
|
|
2074
|
+
while (this.credits > 0 && this.creditWaiters.length > 0) {
|
|
2075
|
+
this.credits -= 1;
|
|
2076
|
+
const resolve = this.creditWaiters.shift();
|
|
2077
|
+
resolve();
|
|
2078
|
+
}
|
|
2079
|
+
});
|
|
2080
|
+
}
|
|
2081
|
+
acquireCredit() {
|
|
2082
|
+
this.initCreditListener();
|
|
2083
|
+
if (this.credits > 0) {
|
|
2084
|
+
this.credits -= 1;
|
|
2085
|
+
return Promise.resolve();
|
|
2086
|
+
}
|
|
2087
|
+
return new Promise((resolve) => {
|
|
2088
|
+
this.creditWaiters.push(resolve);
|
|
2089
|
+
});
|
|
2090
|
+
}
|
|
1913
2091
|
/**
|
|
1914
2092
|
* If a simulation does not meet the required criteria, reset the state to run it again.
|
|
1915
2093
|
*
|
|
@@ -1937,7 +2115,7 @@ ${error.stack}
|
|
|
1937
2115
|
ctx.state.totalFreespinAmount = 0;
|
|
1938
2116
|
ctx.state.triggeredMaxWin = false;
|
|
1939
2117
|
ctx.state.triggeredFreespins = false;
|
|
1940
|
-
ctx.state.userData = ctx.config.userState
|
|
2118
|
+
ctx.state.userData = copy(ctx.config.userState);
|
|
1941
2119
|
}
|
|
1942
2120
|
/**
|
|
1943
2121
|
* Contains and executes the entire game logic:
|
|
@@ -1952,64 +2130,9 @@ ${error.stack}
|
|
|
1952
2130
|
handleGameFlow(ctx) {
|
|
1953
2131
|
this.gameConfig.hooks.onHandleGameFlow(ctx);
|
|
1954
2132
|
}
|
|
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
2133
|
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
|
-
);
|
|
2134
|
+
const tempRecordsPath = this.PATHS.tempRecords(mode);
|
|
2135
|
+
const forceRecordsPath = this.PATHS.forceRecords(mode);
|
|
2013
2136
|
const aggregatedRecords = /* @__PURE__ */ new Map();
|
|
2014
2137
|
if (import_fs2.default.existsSync(tempRecordsPath)) {
|
|
2015
2138
|
const fileStream = import_fs2.default.createReadStream(tempRecordsPath);
|
|
@@ -2055,15 +2178,10 @@ ${error.stack}
|
|
|
2055
2178
|
import_fs2.default.rmSync(tempRecordsPath, { force: true });
|
|
2056
2179
|
}
|
|
2057
2180
|
writeIndexJson() {
|
|
2058
|
-
const outputFilePath =
|
|
2059
|
-
this.gameConfig.rootDir,
|
|
2060
|
-
this.gameConfig.outputDir,
|
|
2061
|
-
"publish_files",
|
|
2062
|
-
"index.json"
|
|
2063
|
-
);
|
|
2181
|
+
const outputFilePath = this.PATHS.indexJson;
|
|
2064
2182
|
const modes = Object.keys(this.simRunsAmount).map((id) => {
|
|
2065
2183
|
const mode = this.gameConfig.gameModes[id];
|
|
2066
|
-
(0,
|
|
2184
|
+
(0, import_assert7.default)(mode, `Game mode "${id}" not found in game config.`);
|
|
2067
2185
|
return {
|
|
2068
2186
|
name: mode.name,
|
|
2069
2187
|
cost: mode.cost,
|
|
@@ -2074,17 +2192,8 @@ ${error.stack}
|
|
|
2074
2192
|
writeFile(outputFilePath, JSON.stringify({ modes }, null, 2));
|
|
2075
2193
|
}
|
|
2076
2194
|
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
|
-
);
|
|
2195
|
+
const outputFilePath = this.PATHS.books(gameMode);
|
|
2196
|
+
const compressedFilePath = this.PATHS.booksCompressed(gameMode);
|
|
2088
2197
|
import_fs2.default.rmSync(compressedFilePath, { force: true });
|
|
2089
2198
|
if (import_fs2.default.existsSync(outputFilePath)) {
|
|
2090
2199
|
await (0, import_promises.pipeline)(
|
|
@@ -2117,11 +2226,12 @@ ${error.stack}
|
|
|
2117
2226
|
});
|
|
2118
2227
|
}
|
|
2119
2228
|
getSimRangesForChunks(total, chunks) {
|
|
2120
|
-
const
|
|
2121
|
-
const
|
|
2229
|
+
const realChunks = Math.min(chunks, Math.max(total, 1));
|
|
2230
|
+
const base = Math.floor(total / realChunks);
|
|
2231
|
+
const remainder = total % realChunks;
|
|
2122
2232
|
const result = [];
|
|
2123
2233
|
let current = 1;
|
|
2124
|
-
for (let i = 0; i <
|
|
2234
|
+
for (let i = 0; i < realChunks; i++) {
|
|
2125
2235
|
const size = base + (i < remainder ? 1 : 0);
|
|
2126
2236
|
const start = current;
|
|
2127
2237
|
const end = current + size - 1;
|
|
@@ -2147,6 +2257,22 @@ ${error.stack}
|
|
|
2147
2257
|
}
|
|
2148
2258
|
}
|
|
2149
2259
|
}
|
|
2260
|
+
async mergeCsv(chunks, outPath, tempName) {
|
|
2261
|
+
import_fs2.default.rmSync(outPath, { force: true });
|
|
2262
|
+
const out = import_fs2.default.createWriteStream(outPath);
|
|
2263
|
+
let wroteAny = false;
|
|
2264
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
2265
|
+
const p = import_path.default.join(this.PATHS.base, TEMP_FOLDER, tempName(i));
|
|
2266
|
+
if (!import_fs2.default.existsSync(p)) continue;
|
|
2267
|
+
if (wroteAny) out.write("");
|
|
2268
|
+
const rs = import_fs2.default.createReadStream(p);
|
|
2269
|
+
for await (const buf of rs) out.write(buf);
|
|
2270
|
+
import_fs2.default.rmSync(p);
|
|
2271
|
+
wroteAny = true;
|
|
2272
|
+
}
|
|
2273
|
+
out.end();
|
|
2274
|
+
await new Promise((resolve) => out.on("finish", resolve));
|
|
2275
|
+
}
|
|
2150
2276
|
/**
|
|
2151
2277
|
* Confirms all pending records and adds them to the main records list.
|
|
2152
2278
|
*/
|
|
@@ -2182,7 +2308,7 @@ ${error.stack}
|
|
|
2182
2308
|
// src/analysis/index.ts
|
|
2183
2309
|
var import_fs3 = __toESM(require("fs"));
|
|
2184
2310
|
var import_path2 = __toESM(require("path"));
|
|
2185
|
-
var
|
|
2311
|
+
var import_assert8 = __toESM(require("assert"));
|
|
2186
2312
|
|
|
2187
2313
|
// src/analysis/utils.ts
|
|
2188
2314
|
function parseLookupTable(content) {
|
|
@@ -2359,7 +2485,7 @@ var Analysis = class {
|
|
|
2359
2485
|
booksJsonlCompressed
|
|
2360
2486
|
};
|
|
2361
2487
|
for (const p of Object.values(paths[modeStr])) {
|
|
2362
|
-
(0,
|
|
2488
|
+
(0, import_assert8.default)(
|
|
2363
2489
|
import_fs3.default.existsSync(p),
|
|
2364
2490
|
`File "${p}" does not exist. Run optimization to auto-create it.`
|
|
2365
2491
|
);
|
|
@@ -2481,14 +2607,14 @@ var Analysis = class {
|
|
|
2481
2607
|
}
|
|
2482
2608
|
getGameModeConfig(mode) {
|
|
2483
2609
|
const config = this.gameConfig.gameModes[mode];
|
|
2484
|
-
(0,
|
|
2610
|
+
(0, import_assert8.default)(config, `Game mode "${mode}" not found in game config`);
|
|
2485
2611
|
return config;
|
|
2486
2612
|
}
|
|
2487
2613
|
};
|
|
2488
2614
|
|
|
2489
2615
|
// src/optimizer/index.ts
|
|
2490
2616
|
var import_path5 = __toESM(require("path"));
|
|
2491
|
-
var
|
|
2617
|
+
var import_assert10 = __toESM(require("assert"));
|
|
2492
2618
|
var import_child_process = require("child_process");
|
|
2493
2619
|
var import_worker_threads3 = require("worker_threads");
|
|
2494
2620
|
|
|
@@ -2589,7 +2715,7 @@ function makeSetupFile(optimizer, gameMode) {
|
|
|
2589
2715
|
}
|
|
2590
2716
|
|
|
2591
2717
|
// src/optimizer/OptimizationConditions.ts
|
|
2592
|
-
var
|
|
2718
|
+
var import_assert9 = __toESM(require("assert"));
|
|
2593
2719
|
var OptimizationConditions = class {
|
|
2594
2720
|
rtp;
|
|
2595
2721
|
avgWin;
|
|
@@ -2600,14 +2726,14 @@ var OptimizationConditions = class {
|
|
|
2600
2726
|
constructor(opts) {
|
|
2601
2727
|
let { rtp, avgWin, hitRate, searchConditions, priority } = opts;
|
|
2602
2728
|
if (rtp == void 0 || rtp === "x") {
|
|
2603
|
-
(0,
|
|
2729
|
+
(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
2730
|
rtp = Math.round(avgWin / Number(hitRate) * 1e5) / 1e5;
|
|
2605
2731
|
}
|
|
2606
2732
|
let noneCount = 0;
|
|
2607
2733
|
for (const val of [rtp, avgWin, hitRate]) {
|
|
2608
2734
|
if (val === void 0) noneCount++;
|
|
2609
2735
|
}
|
|
2610
|
-
(0,
|
|
2736
|
+
(0, import_assert9.default)(noneCount <= 1, "Invalid combination of optimization conditions.");
|
|
2611
2737
|
this.searchRange = [-1, -1];
|
|
2612
2738
|
this.forceSearch = {};
|
|
2613
2739
|
if (typeof searchConditions === "number") {
|
|
@@ -2735,7 +2861,7 @@ var Optimizer = class {
|
|
|
2735
2861
|
}
|
|
2736
2862
|
gameModeRtp = Math.round(gameModeRtp * 1e3) / 1e3;
|
|
2737
2863
|
paramRtp = Math.round(paramRtp * 1e3) / 1e3;
|
|
2738
|
-
(0,
|
|
2864
|
+
(0, import_assert10.default)(
|
|
2739
2865
|
gameModeRtp === paramRtp,
|
|
2740
2866
|
`Sum of all RTP conditions (${paramRtp}) does not match the game mode RTP (${gameModeRtp}) in game mode "${k}".`
|
|
2741
2867
|
);
|
|
@@ -2874,7 +3000,7 @@ var defineSymbols = (symbols) => symbols;
|
|
|
2874
3000
|
var defineGameModes = (gameModes) => gameModes;
|
|
2875
3001
|
|
|
2876
3002
|
// src/game-mode/index.ts
|
|
2877
|
-
var
|
|
3003
|
+
var import_assert11 = __toESM(require("assert"));
|
|
2878
3004
|
var GameMode = class {
|
|
2879
3005
|
name;
|
|
2880
3006
|
_reelsAmount;
|
|
@@ -2897,12 +3023,12 @@ var GameMode = class {
|
|
|
2897
3023
|
this.reelSets = opts.reelSets;
|
|
2898
3024
|
this.resultSets = opts.resultSets;
|
|
2899
3025
|
this.isBonusBuy = opts.isBonusBuy;
|
|
2900
|
-
(0,
|
|
2901
|
-
(0,
|
|
3026
|
+
(0, import_assert11.default)(this.rtp >= 0.9 && this.rtp <= 0.99, "RTP must be between 0.9 and 0.99");
|
|
3027
|
+
(0, import_assert11.default)(
|
|
2902
3028
|
this.symbolsPerReel.length === this.reelsAmount,
|
|
2903
3029
|
"symbolsPerReel length must match reelsAmount."
|
|
2904
3030
|
);
|
|
2905
|
-
(0,
|
|
3031
|
+
(0, import_assert11.default)(this.reelSets.length > 0, "GameMode must have at least one ReelSet defined.");
|
|
2906
3032
|
}
|
|
2907
3033
|
/**
|
|
2908
3034
|
* Intended for internal use only.
|
|
@@ -2915,7 +3041,7 @@ var GameMode = class {
|
|
|
2915
3041
|
* Intended for internal use only.
|
|
2916
3042
|
*/
|
|
2917
3043
|
_setSymbolsPerReel(symbolsPerReel) {
|
|
2918
|
-
(0,
|
|
3044
|
+
(0, import_assert11.default)(
|
|
2919
3045
|
symbolsPerReel.length === this._reelsAmount,
|
|
2920
3046
|
"symbolsPerReel length must match reelsAmount."
|
|
2921
3047
|
);
|
|
@@ -2981,7 +3107,7 @@ var WinType = class {
|
|
|
2981
3107
|
};
|
|
2982
3108
|
|
|
2983
3109
|
// src/win-types/LinesWinType.ts
|
|
2984
|
-
var
|
|
3110
|
+
var import_assert12 = __toESM(require("assert"));
|
|
2985
3111
|
var LinesWinType = class extends WinType {
|
|
2986
3112
|
lines;
|
|
2987
3113
|
constructor(opts) {
|
|
@@ -3035,8 +3161,8 @@ var LinesWinType = class extends WinType {
|
|
|
3035
3161
|
if (!baseSymbol) {
|
|
3036
3162
|
baseSymbol = thisSymbol;
|
|
3037
3163
|
}
|
|
3038
|
-
(0,
|
|
3039
|
-
(0,
|
|
3164
|
+
(0, import_assert12.default)(baseSymbol, `No symbol found at line ${lineNum}, reel ${ridx}`);
|
|
3165
|
+
(0, import_assert12.default)(thisSymbol, `No symbol found at line ${lineNum}, reel ${ridx}`);
|
|
3040
3166
|
if (potentialWinLine.length == 0) {
|
|
3041
3167
|
if (this.isWild(thisSymbol)) {
|
|
3042
3168
|
potentialWildLine.push({ reel: ridx, row: sidx, symbol: thisSymbol });
|
|
@@ -3705,7 +3831,7 @@ var GeneratedReelSet = class extends ReelSet {
|
|
|
3705
3831
|
};
|
|
3706
3832
|
|
|
3707
3833
|
// src/reel-set/StaticReelSet.ts
|
|
3708
|
-
var
|
|
3834
|
+
var import_assert13 = __toESM(require("assert"));
|
|
3709
3835
|
var StaticReelSet = class extends ReelSet {
|
|
3710
3836
|
reels;
|
|
3711
3837
|
csvPath;
|
|
@@ -3715,7 +3841,7 @@ var StaticReelSet = class extends ReelSet {
|
|
|
3715
3841
|
this.reels = [];
|
|
3716
3842
|
this._strReels = opts.reels || [];
|
|
3717
3843
|
this.csvPath = opts.csvPath || "";
|
|
3718
|
-
(0,
|
|
3844
|
+
(0, import_assert13.default)(
|
|
3719
3845
|
opts.reels || opts.csvPath,
|
|
3720
3846
|
`Either 'reels' or 'csvPath' must be provided for StaticReelSet ${this.id}`
|
|
3721
3847
|
);
|