@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.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 = [];
|
|
@@ -1324,17 +1316,6 @@ var Book = class _Book {
|
|
|
1324
1316
|
freespinsWins: this.freespinsWins
|
|
1325
1317
|
};
|
|
1326
1318
|
}
|
|
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
1319
|
};
|
|
1339
1320
|
|
|
1340
1321
|
// src/wallet/index.ts
|
|
@@ -1560,6 +1541,107 @@ var Wallet = class {
|
|
|
1560
1541
|
|
|
1561
1542
|
// src/simulation/index.ts
|
|
1562
1543
|
import { pipeline } from "stream/promises";
|
|
1544
|
+
|
|
1545
|
+
// src/simulation/utils.ts
|
|
1546
|
+
import assert6 from "assert";
|
|
1547
|
+
function hashStringToInt(input) {
|
|
1548
|
+
let h = 2166136261;
|
|
1549
|
+
for (let i = 0; i < input.length; i++) {
|
|
1550
|
+
h ^= input.charCodeAt(i);
|
|
1551
|
+
h = Math.imul(h, 16777619);
|
|
1552
|
+
}
|
|
1553
|
+
return h >>> 0;
|
|
1554
|
+
}
|
|
1555
|
+
function splitCountsAcrossChunks(totalCounts, chunkSizes) {
|
|
1556
|
+
const total = chunkSizes.reduce((a, b) => a + b, 0);
|
|
1557
|
+
const allCriteria = Object.keys(totalCounts);
|
|
1558
|
+
const totalCountsSum = allCriteria.reduce((s, c) => s + (totalCounts[c] ?? 0), 0);
|
|
1559
|
+
assert6(
|
|
1560
|
+
totalCountsSum === total,
|
|
1561
|
+
`Counts (${totalCountsSum}) must match chunk total (${total}).`
|
|
1562
|
+
);
|
|
1563
|
+
const perChunk = chunkSizes.map(() => ({}));
|
|
1564
|
+
for (const criteria of allCriteria) {
|
|
1565
|
+
const count = totalCounts[criteria] ?? 0;
|
|
1566
|
+
if (count <= 0) {
|
|
1567
|
+
for (let i = 0; i < chunkSizes.length; i++) perChunk[i][criteria] = 0;
|
|
1568
|
+
continue;
|
|
1569
|
+
}
|
|
1570
|
+
let chunks = chunkSizes.map((size) => count * size / total);
|
|
1571
|
+
chunks = chunks.map((x) => Math.floor(x));
|
|
1572
|
+
let assigned = chunks.reduce((a, b) => a + b, 0);
|
|
1573
|
+
let remaining = count - assigned;
|
|
1574
|
+
const remainders = chunks.map((x, i) => ({ i, r: x - Math.floor(x) })).sort((a, b) => b.r - a.r);
|
|
1575
|
+
for (let i = 0; i < chunkSizes.length; i++) {
|
|
1576
|
+
perChunk[i][criteria] = chunks[i];
|
|
1577
|
+
}
|
|
1578
|
+
let idx = 0;
|
|
1579
|
+
while (remaining > 0) {
|
|
1580
|
+
perChunk[remainders[idx].i][criteria] += 1;
|
|
1581
|
+
remaining--;
|
|
1582
|
+
idx = (idx + 1) % remainders.length;
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
const chunkTotals = () => perChunk.map((m) => Object.values(m).reduce((s, v) => s + v, 0));
|
|
1586
|
+
let totals = chunkTotals();
|
|
1587
|
+
const getDeficits = () => totals.map((t, i) => chunkSizes[i] - t);
|
|
1588
|
+
let deficits = getDeficits();
|
|
1589
|
+
for (let target = 0; target < chunkSizes.length; target++) {
|
|
1590
|
+
while (deficits[target] > 0) {
|
|
1591
|
+
const src = deficits.findIndex((d) => d < 0);
|
|
1592
|
+
assert6(src !== -1, "No surplus chunk found, but deficits remain.");
|
|
1593
|
+
const crit = allCriteria.find((c) => (perChunk[src][c] ?? 0) > 0);
|
|
1594
|
+
assert6(crit, `No movable criteria found from surplus chunk ${src}.`);
|
|
1595
|
+
perChunk[src][crit] -= 1;
|
|
1596
|
+
perChunk[target][crit] = (perChunk[target][crit] ?? 0) + 1;
|
|
1597
|
+
totals[src] -= 1;
|
|
1598
|
+
totals[target] += 1;
|
|
1599
|
+
deficits[src] += 1;
|
|
1600
|
+
deficits[target] -= 1;
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
totals = chunkTotals();
|
|
1604
|
+
for (let i = 0; i < chunkSizes.length; i++) {
|
|
1605
|
+
assert6(
|
|
1606
|
+
totals[i] === chunkSizes[i],
|
|
1607
|
+
`Chunk ${i} size mismatch. Expected ${chunkSizes[i]}, got ${totals[i]}`
|
|
1608
|
+
);
|
|
1609
|
+
}
|
|
1610
|
+
for (const c of allCriteria) {
|
|
1611
|
+
const sum = perChunk.reduce((s, m) => s + (m[c] ?? 0), 0);
|
|
1612
|
+
assert6(sum === (totalCounts[c] ?? 0), `Chunk split mismatch for criteria "${c}"`);
|
|
1613
|
+
}
|
|
1614
|
+
return perChunk;
|
|
1615
|
+
}
|
|
1616
|
+
function createCriteriaSampler(counts, seed) {
|
|
1617
|
+
const rng = new RandomNumberGenerator();
|
|
1618
|
+
rng.setSeed(seed);
|
|
1619
|
+
const keys = Object.keys(counts).filter((k) => (counts[k] ?? 0) > 0);
|
|
1620
|
+
const remaining = Object.fromEntries(keys.map((k) => [k, counts[k] ?? 0]));
|
|
1621
|
+
let remainingTotal = Object.values(remaining).reduce((a, b) => a + b, 0);
|
|
1622
|
+
return () => {
|
|
1623
|
+
if (remainingTotal <= 0) return "N/A";
|
|
1624
|
+
const roll = Math.min(
|
|
1625
|
+
remainingTotal - Number.EPSILON,
|
|
1626
|
+
rng.randomFloat(0, remainingTotal)
|
|
1627
|
+
);
|
|
1628
|
+
let acc = 0;
|
|
1629
|
+
for (const k of keys) {
|
|
1630
|
+
const w = remaining[k] ?? 0;
|
|
1631
|
+
if (w <= 0) continue;
|
|
1632
|
+
acc += w;
|
|
1633
|
+
if (roll < acc) {
|
|
1634
|
+
remaining[k] = w - 1;
|
|
1635
|
+
remainingTotal--;
|
|
1636
|
+
return k;
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
remainingTotal--;
|
|
1640
|
+
return keys.find((k) => (remaining[k] ?? 0) > 0) ?? "N/A";
|
|
1641
|
+
};
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
// src/simulation/index.ts
|
|
1563
1645
|
var completedSimulations = 0;
|
|
1564
1646
|
var TEMP_FILENAME = "__temp_compiled_src_IGNORE.js";
|
|
1565
1647
|
var TEMP_FOLDER = "temp_files";
|
|
@@ -1570,25 +1652,46 @@ var Simulation = class {
|
|
|
1570
1652
|
concurrency;
|
|
1571
1653
|
debug = false;
|
|
1572
1654
|
actualSims = 0;
|
|
1573
|
-
library;
|
|
1574
1655
|
wallet;
|
|
1575
1656
|
recordsWriteStream;
|
|
1576
1657
|
hasWrittenRecord = false;
|
|
1658
|
+
maxPendingSims;
|
|
1659
|
+
maxHighWaterMark;
|
|
1660
|
+
PATHS = {};
|
|
1661
|
+
// Worker related
|
|
1662
|
+
credits = 0;
|
|
1663
|
+
creditWaiters = [];
|
|
1664
|
+
creditListenerInit = false;
|
|
1577
1665
|
constructor(opts, gameConfigOpts) {
|
|
1578
1666
|
this.gameConfig = createGameConfig(gameConfigOpts);
|
|
1579
1667
|
this.gameConfigOpts = gameConfigOpts;
|
|
1580
1668
|
this.simRunsAmount = opts.simRunsAmount || {};
|
|
1581
1669
|
this.concurrency = (opts.concurrency || 6) >= 2 ? opts.concurrency || 6 : 2;
|
|
1582
|
-
this.library = /* @__PURE__ */ new Map();
|
|
1583
1670
|
this.wallet = new Wallet();
|
|
1671
|
+
this.maxPendingSims = Math.max(10, opts.maxPendingSims ?? 250);
|
|
1672
|
+
this.maxHighWaterMark = (opts.maxDiskBuffer ?? 50) * 1024 * 1024;
|
|
1584
1673
|
const gameModeKeys = Object.keys(this.gameConfig.gameModes);
|
|
1585
|
-
|
|
1674
|
+
assert7(
|
|
1586
1675
|
Object.values(this.gameConfig.gameModes).map((m) => gameModeKeys.includes(m.name)).every((v) => v === true),
|
|
1587
1676
|
"Game mode name must match its key in the gameModes object."
|
|
1588
1677
|
);
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1678
|
+
this.PATHS.base = path.join(this.gameConfig.rootDir, this.gameConfig.outputDir);
|
|
1679
|
+
this.PATHS = {
|
|
1680
|
+
...this.PATHS,
|
|
1681
|
+
books: (mode) => path.join(this.PATHS.base, `books_${mode}.jsonl`),
|
|
1682
|
+
booksCompressed: (mode) => path.join(this.PATHS.base, "publish_files", `books_${mode}.jsonl.zst`),
|
|
1683
|
+
tempBooks: (mode, i) => path.join(this.PATHS.base, TEMP_FOLDER, `temp_books_${mode}_${i}.jsonl`),
|
|
1684
|
+
lookupTable: (mode) => path.join(this.PATHS.base, `lookUpTable_${mode}.csv`),
|
|
1685
|
+
tempLookupTable: (mode, i) => path.join(this.PATHS.base, TEMP_FOLDER, `temp_lookup_${mode}_${i}.csv`),
|
|
1686
|
+
lookupTableSegmented: (mode) => path.join(this.PATHS.base, `lookUpTableSegmented_${mode}.csv`),
|
|
1687
|
+
tempLookupTableSegmented: (mode, i) => path.join(this.PATHS.base, TEMP_FOLDER, `temp_lookup_segmented_${mode}_${i}.csv`),
|
|
1688
|
+
lookupTablePublish: (mode) => path.join(this.PATHS.base, "publish_files", `lookUpTable_${mode}_0.csv`),
|
|
1689
|
+
tempRecords: (mode) => path.join(this.PATHS.base, TEMP_FOLDER, `temp_records_${mode}.jsonl`),
|
|
1690
|
+
forceRecords: (mode) => path.join(this.PATHS.base, `force_record_${mode}.json`),
|
|
1691
|
+
indexJson: path.join(this.PATHS.base, "publish_files", "index.json"),
|
|
1692
|
+
optimizationFiles: path.join(this.PATHS.base, "optimization_files"),
|
|
1693
|
+
publishFiles: path.join(this.PATHS.base, "publish_files")
|
|
1694
|
+
};
|
|
1592
1695
|
}
|
|
1593
1696
|
async runSimulation(opts) {
|
|
1594
1697
|
const debug = opts.debug || false;
|
|
@@ -1600,11 +1703,11 @@ var Simulation = class {
|
|
|
1600
1703
|
}
|
|
1601
1704
|
this.generateReelsetFiles();
|
|
1602
1705
|
if (isMainThread) {
|
|
1706
|
+
this.preprocessFiles();
|
|
1603
1707
|
const debugDetails = {};
|
|
1604
1708
|
for (const mode of gameModesToSimulate) {
|
|
1605
1709
|
completedSimulations = 0;
|
|
1606
1710
|
this.wallet = new Wallet();
|
|
1607
|
-
this.library = /* @__PURE__ */ new Map();
|
|
1608
1711
|
this.hasWrittenRecord = false;
|
|
1609
1712
|
debugDetails[mode] = {};
|
|
1610
1713
|
console.log(`
|
|
@@ -1617,37 +1720,37 @@ Simulating game mode: ${mode}`);
|
|
|
1617
1720
|
`Tried to simulate game mode "${mode}", but it's not configured in the game config.`
|
|
1618
1721
|
);
|
|
1619
1722
|
}
|
|
1620
|
-
const booksPath =
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
path.join(this.gameConfig.rootDir, this.gameConfig.outputDir)
|
|
1723
|
+
const booksPath = this.PATHS.books(mode);
|
|
1724
|
+
const tempRecordsPath = this.PATHS.tempRecords(mode);
|
|
1725
|
+
createDirIfNotExists(this.PATHS.base);
|
|
1726
|
+
createDirIfNotExists(path.join(this.PATHS.base, TEMP_FOLDER));
|
|
1727
|
+
this.recordsWriteStream = fs2.createWriteStream(tempRecordsPath, {
|
|
1728
|
+
highWaterMark: this.maxHighWaterMark
|
|
1729
|
+
});
|
|
1730
|
+
const criteriaCounts = ResultSet.getNumberOfSimsForCriteria(this, mode);
|
|
1731
|
+
const totalSims = Object.values(criteriaCounts).reduce((a, b) => a + b, 0);
|
|
1732
|
+
assert7(
|
|
1733
|
+
totalSims === runs,
|
|
1734
|
+
`Criteria mismatch for mode "${mode}". Expected ${runs}, got ${totalSims}`
|
|
1633
1735
|
);
|
|
1634
|
-
|
|
1635
|
-
|
|
1736
|
+
const chunks = this.getSimRangesForChunks(totalSims, this.concurrency);
|
|
1737
|
+
const chunkSizes = chunks.map(([s, e]) => Math.max(0, e - s + 1));
|
|
1738
|
+
const chunkCriteriaCounts = splitCountsAcrossChunks(criteriaCounts, chunkSizes);
|
|
1739
|
+
await this.spawnWorkersForGameMode({
|
|
1740
|
+
mode,
|
|
1741
|
+
chunks,
|
|
1742
|
+
chunkCriteriaCounts,
|
|
1743
|
+
totalSims
|
|
1744
|
+
});
|
|
1745
|
+
createDirIfNotExists(this.PATHS.optimizationFiles);
|
|
1746
|
+
createDirIfNotExists(this.PATHS.publishFiles);
|
|
1747
|
+
console.log(
|
|
1748
|
+
`Writing final files for game mode "${mode}". This may take a while...`
|
|
1636
1749
|
);
|
|
1637
|
-
this.recordsWriteStream = fs2.createWriteStream(tempRecordsPath);
|
|
1638
|
-
const simNumsToCriteria = ResultSet.assignCriteriaToSimulations(this, mode);
|
|
1639
|
-
await this.spawnWorkersForGameMode({ mode, simNumsToCriteria });
|
|
1640
1750
|
const finalBookStream = fs2.createWriteStream(booksPath);
|
|
1641
|
-
const numSims = Object.keys(simNumsToCriteria).length;
|
|
1642
|
-
const chunks = this.getSimRangesForChunks(numSims, this.concurrency);
|
|
1643
1751
|
let isFirstChunk = true;
|
|
1644
1752
|
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
|
-
);
|
|
1753
|
+
const tempBookPath = this.PATHS.tempBooks(mode, i);
|
|
1651
1754
|
if (fs2.existsSync(tempBookPath)) {
|
|
1652
1755
|
if (!isFirstChunk) {
|
|
1653
1756
|
finalBookStream.write("\n");
|
|
@@ -1662,6 +1765,16 @@ Simulating game mode: ${mode}`);
|
|
|
1662
1765
|
}
|
|
1663
1766
|
finalBookStream.end();
|
|
1664
1767
|
await new Promise((resolve) => finalBookStream.on("finish", resolve));
|
|
1768
|
+
const lutPath = this.PATHS.lookupTable(mode);
|
|
1769
|
+
const lutPathPublish = this.PATHS.lookupTablePublish(mode);
|
|
1770
|
+
const lutSegmentedPath = this.PATHS.lookupTableSegmented(mode);
|
|
1771
|
+
await this.mergeCsv(chunks, lutPath, (i) => `temp_lookup_${mode}_${i}.csv`);
|
|
1772
|
+
fs2.copyFileSync(lutPath, lutPathPublish);
|
|
1773
|
+
await this.mergeCsv(
|
|
1774
|
+
chunks,
|
|
1775
|
+
lutSegmentedPath,
|
|
1776
|
+
(i) => `temp_lookup_segmented_${mode}_${i}.csv`
|
|
1777
|
+
);
|
|
1665
1778
|
if (this.recordsWriteStream) {
|
|
1666
1779
|
await new Promise((resolve) => {
|
|
1667
1780
|
this.recordsWriteStream.end(() => {
|
|
@@ -1670,26 +1783,21 @@ Simulating game mode: ${mode}`);
|
|
|
1670
1783
|
});
|
|
1671
1784
|
this.recordsWriteStream = void 0;
|
|
1672
1785
|
}
|
|
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);
|
|
1786
|
+
await this.writeRecords(mode);
|
|
1687
1787
|
await this.writeBooksJson(mode);
|
|
1688
1788
|
this.writeIndexJson();
|
|
1689
1789
|
console.log(`Mode ${mode} done!`);
|
|
1690
|
-
debugDetails[mode].rtp =
|
|
1691
|
-
|
|
1692
|
-
|
|
1790
|
+
debugDetails[mode].rtp = round(
|
|
1791
|
+
this.wallet.getCumulativeWins() / (runs * this.gameConfig.gameModes[mode].cost),
|
|
1792
|
+
3
|
|
1793
|
+
);
|
|
1794
|
+
debugDetails[mode].wins = round(this.wallet.getCumulativeWins(), 3);
|
|
1795
|
+
debugDetails[mode].winsPerSpinType = Object.fromEntries(
|
|
1796
|
+
Object.entries(this.wallet.getCumulativeWinsPerSpinType()).map(([k, v]) => [
|
|
1797
|
+
k,
|
|
1798
|
+
round(v, 3)
|
|
1799
|
+
])
|
|
1800
|
+
);
|
|
1693
1801
|
console.timeEnd(mode);
|
|
1694
1802
|
}
|
|
1695
1803
|
console.log("\n=== SIMULATION SUMMARY ===");
|
|
@@ -1699,14 +1807,14 @@ Simulating game mode: ${mode}`);
|
|
|
1699
1807
|
let actualSims = 0;
|
|
1700
1808
|
const criteriaToRetries = {};
|
|
1701
1809
|
if (!isMainThread) {
|
|
1702
|
-
const { mode, simStart, simEnd, index } = workerData;
|
|
1703
|
-
const
|
|
1810
|
+
const { mode, simStart, simEnd, index, criteriaCounts } = workerData;
|
|
1811
|
+
const seed = hashStringToInt(mode) + index >>> 0;
|
|
1812
|
+
const nextCriteria = createCriteriaSampler(criteriaCounts, seed);
|
|
1704
1813
|
for (let simId = simStart; simId <= simEnd; simId++) {
|
|
1705
1814
|
if (this.debug) desiredSims++;
|
|
1706
|
-
const criteria =
|
|
1707
|
-
if (!criteriaToRetries[criteria])
|
|
1708
|
-
|
|
1709
|
-
}
|
|
1815
|
+
const criteria = nextCriteria();
|
|
1816
|
+
if (!criteriaToRetries[criteria]) criteriaToRetries[criteria] = 0;
|
|
1817
|
+
await this.acquireCredit();
|
|
1710
1818
|
this.runSingleSimulation({ simId, mode, criteria, index });
|
|
1711
1819
|
if (this.debug) {
|
|
1712
1820
|
criteriaToRetries[criteria] += this.actualSims - 1;
|
|
@@ -1721,30 +1829,30 @@ Simulating game mode: ${mode}`);
|
|
|
1721
1829
|
type: "done",
|
|
1722
1830
|
workerNum: index
|
|
1723
1831
|
});
|
|
1832
|
+
parentPort?.close();
|
|
1724
1833
|
}
|
|
1725
1834
|
}
|
|
1726
1835
|
/**
|
|
1727
1836
|
* Runs all simulations for a specific game mode.
|
|
1728
1837
|
*/
|
|
1729
1838
|
async spawnWorkersForGameMode(opts) {
|
|
1730
|
-
const { mode,
|
|
1731
|
-
const numSims = Object.keys(simNumsToCriteria).length;
|
|
1732
|
-
const simRangesPerChunk = this.getSimRangesForChunks(numSims, this.concurrency);
|
|
1839
|
+
const { mode, chunks, chunkCriteriaCounts, totalSims } = opts;
|
|
1733
1840
|
await Promise.all(
|
|
1734
|
-
|
|
1841
|
+
chunks.map(([simStart, simEnd], index) => {
|
|
1735
1842
|
return this.callWorker({
|
|
1736
|
-
basePath:
|
|
1843
|
+
basePath: this.PATHS.base,
|
|
1737
1844
|
mode,
|
|
1738
1845
|
simStart,
|
|
1739
1846
|
simEnd,
|
|
1740
1847
|
index,
|
|
1741
|
-
totalSims
|
|
1848
|
+
totalSims,
|
|
1849
|
+
criteriaCounts: chunkCriteriaCounts[index]
|
|
1742
1850
|
});
|
|
1743
1851
|
})
|
|
1744
1852
|
);
|
|
1745
1853
|
}
|
|
1746
1854
|
async callWorker(opts) {
|
|
1747
|
-
const { mode, simEnd, simStart, basePath, index, totalSims } = opts;
|
|
1855
|
+
const { mode, simEnd, simStart, basePath, index, totalSims, criteriaCounts } = opts;
|
|
1748
1856
|
function logArrowProgress(current, total) {
|
|
1749
1857
|
const percentage = current / total * 100;
|
|
1750
1858
|
const progressBarLength = 50;
|
|
@@ -1755,6 +1863,11 @@ Simulating game mode: ${mode}`);
|
|
|
1755
1863
|
process.stdout.write("\n");
|
|
1756
1864
|
}
|
|
1757
1865
|
}
|
|
1866
|
+
const write = async (stream, chunk) => {
|
|
1867
|
+
if (!stream.write(chunk)) {
|
|
1868
|
+
await new Promise((resolve) => stream.once("drain", resolve));
|
|
1869
|
+
}
|
|
1870
|
+
};
|
|
1758
1871
|
return new Promise((resolve, reject) => {
|
|
1759
1872
|
const scriptPath = path.join(basePath, TEMP_FILENAME);
|
|
1760
1873
|
const worker = new Worker(scriptPath, {
|
|
@@ -1762,42 +1875,77 @@ Simulating game mode: ${mode}`);
|
|
|
1762
1875
|
mode,
|
|
1763
1876
|
simStart,
|
|
1764
1877
|
simEnd,
|
|
1765
|
-
index
|
|
1878
|
+
index,
|
|
1879
|
+
criteriaCounts
|
|
1766
1880
|
}
|
|
1767
1881
|
});
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
);
|
|
1773
|
-
const
|
|
1882
|
+
worker.postMessage({ type: "credit", amount: this.maxPendingSims });
|
|
1883
|
+
const tempBookPath = this.PATHS.tempBooks(mode, index);
|
|
1884
|
+
const bookStream = fs2.createWriteStream(tempBookPath, {
|
|
1885
|
+
highWaterMark: this.maxHighWaterMark
|
|
1886
|
+
});
|
|
1887
|
+
const tempLookupPath = this.PATHS.tempLookupTable(mode, index);
|
|
1888
|
+
const lookupStream = fs2.createWriteStream(tempLookupPath, {
|
|
1889
|
+
highWaterMark: this.maxHighWaterMark
|
|
1890
|
+
});
|
|
1891
|
+
const tempLookupSegPath = this.PATHS.tempLookupTableSegmented(mode, index);
|
|
1892
|
+
const lookupSegmentedStream = fs2.createWriteStream(tempLookupSegPath, {
|
|
1893
|
+
highWaterMark: this.maxHighWaterMark
|
|
1894
|
+
});
|
|
1895
|
+
let writeChain = Promise.resolve();
|
|
1774
1896
|
worker.on("message", (msg) => {
|
|
1775
1897
|
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;
|
|
1898
|
+
return;
|
|
1899
|
+
}
|
|
1900
|
+
if (msg.type === "complete") {
|
|
1901
|
+
writeChain = writeChain.then(async () => {
|
|
1902
|
+
completedSimulations++;
|
|
1903
|
+
if (completedSimulations % 250 === 0) {
|
|
1904
|
+
logArrowProgress(completedSimulations, totalSims);
|
|
1796
1905
|
}
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1906
|
+
const book = msg.book;
|
|
1907
|
+
const bookData = {
|
|
1908
|
+
id: book.id,
|
|
1909
|
+
payoutMultiplier: book.payout,
|
|
1910
|
+
events: book.events
|
|
1911
|
+
};
|
|
1912
|
+
const prefix = book.id === simStart ? "" : "\n";
|
|
1913
|
+
await write(bookStream, prefix + JSONL.stringify([bookData]));
|
|
1914
|
+
await write(lookupStream, `${book.id},1,${Math.round(book.payout)}
|
|
1915
|
+
`);
|
|
1916
|
+
await write(
|
|
1917
|
+
lookupSegmentedStream,
|
|
1918
|
+
`${book.id},${book.criteria},${book.basegameWins},${book.freespinsWins}
|
|
1919
|
+
`
|
|
1920
|
+
);
|
|
1921
|
+
if (this.recordsWriteStream) {
|
|
1922
|
+
for (const record of msg.records) {
|
|
1923
|
+
const recordPrefix = this.hasWrittenRecord ? "\n" : "";
|
|
1924
|
+
await write(
|
|
1925
|
+
this.recordsWriteStream,
|
|
1926
|
+
recordPrefix + JSONL.stringify([record])
|
|
1927
|
+
);
|
|
1928
|
+
this.hasWrittenRecord = true;
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1931
|
+
this.wallet.mergeSerialized(msg.wallet);
|
|
1932
|
+
worker.postMessage({ type: "credit", amount: 1 });
|
|
1933
|
+
}).catch(reject);
|
|
1934
|
+
return;
|
|
1935
|
+
}
|
|
1936
|
+
if (msg.type === "done") {
|
|
1937
|
+
writeChain.then(async () => {
|
|
1938
|
+
bookStream.end();
|
|
1939
|
+
lookupStream.end();
|
|
1940
|
+
lookupSegmentedStream.end();
|
|
1941
|
+
await Promise.all([
|
|
1942
|
+
new Promise((r) => bookStream.on("finish", () => r())),
|
|
1943
|
+
new Promise((r) => lookupStream.on("finish", () => r())),
|
|
1944
|
+
new Promise((r) => lookupSegmentedStream.on("finish", () => r()))
|
|
1945
|
+
]);
|
|
1946
|
+
resolve(true);
|
|
1947
|
+
}).catch(reject);
|
|
1948
|
+
return;
|
|
1801
1949
|
}
|
|
1802
1950
|
});
|
|
1803
1951
|
worker.on("error", (error) => {
|
|
@@ -1858,6 +2006,31 @@ ${error.stack}
|
|
|
1858
2006
|
records: ctx.services.data._getRecords()
|
|
1859
2007
|
});
|
|
1860
2008
|
}
|
|
2009
|
+
initCreditListener() {
|
|
2010
|
+
if (this.creditListenerInit) return;
|
|
2011
|
+
this.creditListenerInit = true;
|
|
2012
|
+
parentPort?.on("message", (msg) => {
|
|
2013
|
+
if (msg?.type !== "credit") return;
|
|
2014
|
+
const amount = Number(msg?.amount ?? 0);
|
|
2015
|
+
if (!Number.isFinite(amount) || amount <= 0) return;
|
|
2016
|
+
this.credits += amount;
|
|
2017
|
+
while (this.credits > 0 && this.creditWaiters.length > 0) {
|
|
2018
|
+
this.credits -= 1;
|
|
2019
|
+
const resolve = this.creditWaiters.shift();
|
|
2020
|
+
resolve();
|
|
2021
|
+
}
|
|
2022
|
+
});
|
|
2023
|
+
}
|
|
2024
|
+
acquireCredit() {
|
|
2025
|
+
this.initCreditListener();
|
|
2026
|
+
if (this.credits > 0) {
|
|
2027
|
+
this.credits -= 1;
|
|
2028
|
+
return Promise.resolve();
|
|
2029
|
+
}
|
|
2030
|
+
return new Promise((resolve) => {
|
|
2031
|
+
this.creditWaiters.push(resolve);
|
|
2032
|
+
});
|
|
2033
|
+
}
|
|
1861
2034
|
/**
|
|
1862
2035
|
* If a simulation does not meet the required criteria, reset the state to run it again.
|
|
1863
2036
|
*
|
|
@@ -1900,64 +2073,9 @@ ${error.stack}
|
|
|
1900
2073
|
handleGameFlow(ctx) {
|
|
1901
2074
|
this.gameConfig.hooks.onHandleGameFlow(ctx);
|
|
1902
2075
|
}
|
|
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
2076
|
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
|
-
);
|
|
2077
|
+
const tempRecordsPath = this.PATHS.tempRecords(mode);
|
|
2078
|
+
const forceRecordsPath = this.PATHS.forceRecords(mode);
|
|
1961
2079
|
const aggregatedRecords = /* @__PURE__ */ new Map();
|
|
1962
2080
|
if (fs2.existsSync(tempRecordsPath)) {
|
|
1963
2081
|
const fileStream = fs2.createReadStream(tempRecordsPath);
|
|
@@ -2003,15 +2121,10 @@ ${error.stack}
|
|
|
2003
2121
|
fs2.rmSync(tempRecordsPath, { force: true });
|
|
2004
2122
|
}
|
|
2005
2123
|
writeIndexJson() {
|
|
2006
|
-
const outputFilePath =
|
|
2007
|
-
this.gameConfig.rootDir,
|
|
2008
|
-
this.gameConfig.outputDir,
|
|
2009
|
-
"publish_files",
|
|
2010
|
-
"index.json"
|
|
2011
|
-
);
|
|
2124
|
+
const outputFilePath = this.PATHS.indexJson;
|
|
2012
2125
|
const modes = Object.keys(this.simRunsAmount).map((id) => {
|
|
2013
2126
|
const mode = this.gameConfig.gameModes[id];
|
|
2014
|
-
|
|
2127
|
+
assert7(mode, `Game mode "${id}" not found in game config.`);
|
|
2015
2128
|
return {
|
|
2016
2129
|
name: mode.name,
|
|
2017
2130
|
cost: mode.cost,
|
|
@@ -2022,17 +2135,8 @@ ${error.stack}
|
|
|
2022
2135
|
writeFile(outputFilePath, JSON.stringify({ modes }, null, 2));
|
|
2023
2136
|
}
|
|
2024
2137
|
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
|
-
);
|
|
2138
|
+
const outputFilePath = this.PATHS.books(gameMode);
|
|
2139
|
+
const compressedFilePath = this.PATHS.booksCompressed(gameMode);
|
|
2036
2140
|
fs2.rmSync(compressedFilePath, { force: true });
|
|
2037
2141
|
if (fs2.existsSync(outputFilePath)) {
|
|
2038
2142
|
await pipeline(
|
|
@@ -2065,11 +2169,12 @@ ${error.stack}
|
|
|
2065
2169
|
});
|
|
2066
2170
|
}
|
|
2067
2171
|
getSimRangesForChunks(total, chunks) {
|
|
2068
|
-
const
|
|
2069
|
-
const
|
|
2172
|
+
const realChunks = Math.min(chunks, Math.max(total, 1));
|
|
2173
|
+
const base = Math.floor(total / realChunks);
|
|
2174
|
+
const remainder = total % realChunks;
|
|
2070
2175
|
const result = [];
|
|
2071
2176
|
let current = 1;
|
|
2072
|
-
for (let i = 0; i <
|
|
2177
|
+
for (let i = 0; i < realChunks; i++) {
|
|
2073
2178
|
const size = base + (i < remainder ? 1 : 0);
|
|
2074
2179
|
const start = current;
|
|
2075
2180
|
const end = current + size - 1;
|
|
@@ -2095,6 +2200,22 @@ ${error.stack}
|
|
|
2095
2200
|
}
|
|
2096
2201
|
}
|
|
2097
2202
|
}
|
|
2203
|
+
async mergeCsv(chunks, outPath, tempName) {
|
|
2204
|
+
fs2.rmSync(outPath, { force: true });
|
|
2205
|
+
const out = fs2.createWriteStream(outPath);
|
|
2206
|
+
let wroteAny = false;
|
|
2207
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
2208
|
+
const p = path.join(this.PATHS.base, TEMP_FOLDER, tempName(i));
|
|
2209
|
+
if (!fs2.existsSync(p)) continue;
|
|
2210
|
+
if (wroteAny) out.write("");
|
|
2211
|
+
const rs = fs2.createReadStream(p);
|
|
2212
|
+
for await (const buf of rs) out.write(buf);
|
|
2213
|
+
fs2.rmSync(p);
|
|
2214
|
+
wroteAny = true;
|
|
2215
|
+
}
|
|
2216
|
+
out.end();
|
|
2217
|
+
await new Promise((resolve) => out.on("finish", resolve));
|
|
2218
|
+
}
|
|
2098
2219
|
/**
|
|
2099
2220
|
* Confirms all pending records and adds them to the main records list.
|
|
2100
2221
|
*/
|
|
@@ -2130,7 +2251,7 @@ ${error.stack}
|
|
|
2130
2251
|
// src/analysis/index.ts
|
|
2131
2252
|
import fs3 from "fs";
|
|
2132
2253
|
import path2 from "path";
|
|
2133
|
-
import
|
|
2254
|
+
import assert8 from "assert";
|
|
2134
2255
|
|
|
2135
2256
|
// src/analysis/utils.ts
|
|
2136
2257
|
function parseLookupTable(content) {
|
|
@@ -2307,7 +2428,7 @@ var Analysis = class {
|
|
|
2307
2428
|
booksJsonlCompressed
|
|
2308
2429
|
};
|
|
2309
2430
|
for (const p of Object.values(paths[modeStr])) {
|
|
2310
|
-
|
|
2431
|
+
assert8(
|
|
2311
2432
|
fs3.existsSync(p),
|
|
2312
2433
|
`File "${p}" does not exist. Run optimization to auto-create it.`
|
|
2313
2434
|
);
|
|
@@ -2429,14 +2550,14 @@ var Analysis = class {
|
|
|
2429
2550
|
}
|
|
2430
2551
|
getGameModeConfig(mode) {
|
|
2431
2552
|
const config = this.gameConfig.gameModes[mode];
|
|
2432
|
-
|
|
2553
|
+
assert8(config, `Game mode "${mode}" not found in game config`);
|
|
2433
2554
|
return config;
|
|
2434
2555
|
}
|
|
2435
2556
|
};
|
|
2436
2557
|
|
|
2437
2558
|
// src/optimizer/index.ts
|
|
2438
2559
|
import path5 from "path";
|
|
2439
|
-
import
|
|
2560
|
+
import assert10 from "assert";
|
|
2440
2561
|
import { spawn } from "child_process";
|
|
2441
2562
|
import { isMainThread as isMainThread3 } from "worker_threads";
|
|
2442
2563
|
|
|
@@ -2537,7 +2658,7 @@ function makeSetupFile(optimizer, gameMode) {
|
|
|
2537
2658
|
}
|
|
2538
2659
|
|
|
2539
2660
|
// src/optimizer/OptimizationConditions.ts
|
|
2540
|
-
import
|
|
2661
|
+
import assert9 from "assert";
|
|
2541
2662
|
var OptimizationConditions = class {
|
|
2542
2663
|
rtp;
|
|
2543
2664
|
avgWin;
|
|
@@ -2548,14 +2669,14 @@ var OptimizationConditions = class {
|
|
|
2548
2669
|
constructor(opts) {
|
|
2549
2670
|
let { rtp, avgWin, hitRate, searchConditions, priority } = opts;
|
|
2550
2671
|
if (rtp == void 0 || rtp === "x") {
|
|
2551
|
-
|
|
2672
|
+
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
2673
|
rtp = Math.round(avgWin / Number(hitRate) * 1e5) / 1e5;
|
|
2553
2674
|
}
|
|
2554
2675
|
let noneCount = 0;
|
|
2555
2676
|
for (const val of [rtp, avgWin, hitRate]) {
|
|
2556
2677
|
if (val === void 0) noneCount++;
|
|
2557
2678
|
}
|
|
2558
|
-
|
|
2679
|
+
assert9(noneCount <= 1, "Invalid combination of optimization conditions.");
|
|
2559
2680
|
this.searchRange = [-1, -1];
|
|
2560
2681
|
this.forceSearch = {};
|
|
2561
2682
|
if (typeof searchConditions === "number") {
|
|
@@ -2683,7 +2804,7 @@ var Optimizer = class {
|
|
|
2683
2804
|
}
|
|
2684
2805
|
gameModeRtp = Math.round(gameModeRtp * 1e3) / 1e3;
|
|
2685
2806
|
paramRtp = Math.round(paramRtp * 1e3) / 1e3;
|
|
2686
|
-
|
|
2807
|
+
assert10(
|
|
2687
2808
|
gameModeRtp === paramRtp,
|
|
2688
2809
|
`Sum of all RTP conditions (${paramRtp}) does not match the game mode RTP (${gameModeRtp}) in game mode "${k}".`
|
|
2689
2810
|
);
|
|
@@ -2822,7 +2943,7 @@ var defineSymbols = (symbols) => symbols;
|
|
|
2822
2943
|
var defineGameModes = (gameModes) => gameModes;
|
|
2823
2944
|
|
|
2824
2945
|
// src/game-mode/index.ts
|
|
2825
|
-
import
|
|
2946
|
+
import assert11 from "assert";
|
|
2826
2947
|
var GameMode = class {
|
|
2827
2948
|
name;
|
|
2828
2949
|
_reelsAmount;
|
|
@@ -2845,12 +2966,12 @@ var GameMode = class {
|
|
|
2845
2966
|
this.reelSets = opts.reelSets;
|
|
2846
2967
|
this.resultSets = opts.resultSets;
|
|
2847
2968
|
this.isBonusBuy = opts.isBonusBuy;
|
|
2848
|
-
|
|
2849
|
-
|
|
2969
|
+
assert11(this.rtp >= 0.9 && this.rtp <= 0.99, "RTP must be between 0.9 and 0.99");
|
|
2970
|
+
assert11(
|
|
2850
2971
|
this.symbolsPerReel.length === this.reelsAmount,
|
|
2851
2972
|
"symbolsPerReel length must match reelsAmount."
|
|
2852
2973
|
);
|
|
2853
|
-
|
|
2974
|
+
assert11(this.reelSets.length > 0, "GameMode must have at least one ReelSet defined.");
|
|
2854
2975
|
}
|
|
2855
2976
|
/**
|
|
2856
2977
|
* Intended for internal use only.
|
|
@@ -2863,7 +2984,7 @@ var GameMode = class {
|
|
|
2863
2984
|
* Intended for internal use only.
|
|
2864
2985
|
*/
|
|
2865
2986
|
_setSymbolsPerReel(symbolsPerReel) {
|
|
2866
|
-
|
|
2987
|
+
assert11(
|
|
2867
2988
|
symbolsPerReel.length === this._reelsAmount,
|
|
2868
2989
|
"symbolsPerReel length must match reelsAmount."
|
|
2869
2990
|
);
|
|
@@ -2929,7 +3050,7 @@ var WinType = class {
|
|
|
2929
3050
|
};
|
|
2930
3051
|
|
|
2931
3052
|
// src/win-types/LinesWinType.ts
|
|
2932
|
-
import
|
|
3053
|
+
import assert12 from "assert";
|
|
2933
3054
|
var LinesWinType = class extends WinType {
|
|
2934
3055
|
lines;
|
|
2935
3056
|
constructor(opts) {
|
|
@@ -2983,8 +3104,8 @@ var LinesWinType = class extends WinType {
|
|
|
2983
3104
|
if (!baseSymbol) {
|
|
2984
3105
|
baseSymbol = thisSymbol;
|
|
2985
3106
|
}
|
|
2986
|
-
|
|
2987
|
-
|
|
3107
|
+
assert12(baseSymbol, `No symbol found at line ${lineNum}, reel ${ridx}`);
|
|
3108
|
+
assert12(thisSymbol, `No symbol found at line ${lineNum}, reel ${ridx}`);
|
|
2988
3109
|
if (potentialWinLine.length == 0) {
|
|
2989
3110
|
if (this.isWild(thisSymbol)) {
|
|
2990
3111
|
potentialWildLine.push({ reel: ridx, row: sidx, symbol: thisSymbol });
|
|
@@ -3653,7 +3774,7 @@ var GeneratedReelSet = class extends ReelSet {
|
|
|
3653
3774
|
};
|
|
3654
3775
|
|
|
3655
3776
|
// src/reel-set/StaticReelSet.ts
|
|
3656
|
-
import
|
|
3777
|
+
import assert13 from "assert";
|
|
3657
3778
|
var StaticReelSet = class extends ReelSet {
|
|
3658
3779
|
reels;
|
|
3659
3780
|
csvPath;
|
|
@@ -3663,7 +3784,7 @@ var StaticReelSet = class extends ReelSet {
|
|
|
3663
3784
|
this.reels = [];
|
|
3664
3785
|
this._strReels = opts.reels || [];
|
|
3665
3786
|
this.csvPath = opts.csvPath || "";
|
|
3666
|
-
|
|
3787
|
+
assert13(
|
|
3667
3788
|
opts.reels || opts.csvPath,
|
|
3668
3789
|
`Either 'reels' or 'csvPath' must be provided for StaticReelSet ${this.id}`
|
|
3669
3790
|
);
|