@slot-engine/core 0.0.1 → 0.0.3

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.
Files changed (43) hide show
  1. package/README.md +1 -1
  2. package/dist/index.d.mts +7 -14
  3. package/dist/index.d.ts +7 -14
  4. package/dist/index.js +68 -90
  5. package/dist/index.js.map +1 -1
  6. package/dist/index.mjs +68 -90
  7. package/dist/index.mjs.map +1 -1
  8. package/package.json +5 -1
  9. package/.turbo/turbo-build.log +0 -33
  10. package/.turbo/turbo-typecheck.log +0 -4
  11. package/CHANGELOG.md +0 -7
  12. package/dist/lib/zstd.exe +0 -0
  13. package/index.ts +0 -205
  14. package/lib/zstd.exe +0 -0
  15. package/optimizer-rust/Cargo.toml +0 -19
  16. package/optimizer-rust/src/exes.rs +0 -154
  17. package/optimizer-rust/src/main.rs +0 -1659
  18. package/src/Board.ts +0 -527
  19. package/src/Book.ts +0 -83
  20. package/src/GameConfig.ts +0 -148
  21. package/src/GameMode.ts +0 -86
  22. package/src/GameState.ts +0 -272
  23. package/src/GameSymbol.ts +0 -61
  24. package/src/ReelGenerator.ts +0 -589
  25. package/src/ResultSet.ts +0 -207
  26. package/src/Simulation.ts +0 -625
  27. package/src/SlotGame.ts +0 -117
  28. package/src/Wallet.ts +0 -203
  29. package/src/WinType.ts +0 -102
  30. package/src/analysis/index.ts +0 -198
  31. package/src/analysis/utils.ts +0 -128
  32. package/src/optimizer/OptimizationConditions.ts +0 -99
  33. package/src/optimizer/OptimizationParameters.ts +0 -46
  34. package/src/optimizer/OptimizationScaling.ts +0 -18
  35. package/src/optimizer/index.ts +0 -142
  36. package/src/utils/math-config.ts +0 -109
  37. package/src/utils/setup-file.ts +0 -36
  38. package/src/utils/zstd.ts +0 -28
  39. package/src/winTypes/ClusterWinType.ts +0 -3
  40. package/src/winTypes/LinesWinType.ts +0 -208
  41. package/src/winTypes/ManywaysWinType.ts +0 -3
  42. package/tsconfig.json +0 -19
  43. package/utils.ts +0 -270
package/src/Simulation.ts DELETED
@@ -1,625 +0,0 @@
1
- import fs from "fs"
2
- import path from "path"
3
- import assert from "assert"
4
- import { buildSync } from "esbuild"
5
- import { createDirIfNotExists, JSONL, printBoard, writeFile } from "../utils"
6
- import { Board } from "./Board"
7
- import { GameConfig } from "./GameConfig"
8
- import { GameModeName } from "./GameMode"
9
- import { ResultSet } from "./ResultSet"
10
- import { Wallet } from "./Wallet"
11
- import { Book } from "./Book"
12
- import { Worker, isMainThread, parentPort, workerData } from "worker_threads"
13
- import { RecordItem } from "./GameState"
14
- import { zstd } from "./utils/zstd"
15
- import { AnyGameModes, AnySymbols, AnyUserData, CommonGameOptions } from "../index"
16
-
17
- let completedSimulations = 0
18
- const TEMP_FILENAME = "__temp_compiled_src_IGNORE.js"
19
-
20
- export class Simulation {
21
- protected readonly gameConfigOpts: CommonGameOptions
22
- readonly gameConfig: GameConfig
23
- readonly simRunsAmount: Partial<Record<GameModeName, number>>
24
- private readonly concurrency: number
25
- private wallet: Wallet
26
- private library: Map<string, Book>
27
- readonly records: RecordItem[]
28
- protected debug = false
29
-
30
- constructor(opts: SimulationConfigOpts, gameConfigOpts: CommonGameOptions) {
31
- this.gameConfig = new GameConfig(gameConfigOpts)
32
- this.gameConfigOpts = gameConfigOpts
33
- this.simRunsAmount = opts.simRunsAmount || {}
34
- this.concurrency = (opts.concurrency || 6) >= 2 ? opts.concurrency || 6 : 2
35
- this.wallet = new Wallet()
36
- this.library = new Map()
37
- this.records = []
38
-
39
- const gameModeKeys = Object.keys(this.gameConfig.config.gameModes)
40
- assert(
41
- Object.values(this.gameConfig.config.gameModes)
42
- .map((m) => gameModeKeys.includes(m.name))
43
- .every((v) => v === true),
44
- "Game mode name must match its key in the gameModes object.",
45
- )
46
-
47
- if (isMainThread) {
48
- this.preprocessFiles()
49
- }
50
- }
51
-
52
- async runSimulation(opts: SimulationOpts) {
53
- const debug = opts.debug || false
54
- this.debug = debug
55
-
56
- const gameModesToSimulate = Object.keys(this.simRunsAmount)
57
- const configuredGameModes = Object.keys(this.gameConfig.config.gameModes)
58
-
59
- if (gameModesToSimulate.length === 0) {
60
- throw new Error("No game modes configured for simulation.")
61
- }
62
-
63
- this.gameConfig.generateReelsetFiles()
64
-
65
- // Code that runs when the user executes the simulations.
66
- // This spawns individual processes and merges the results afterwards.
67
- if (isMainThread) {
68
- const debugDetails: Record<string, Record<string, any>> = {}
69
-
70
- for (const mode of gameModesToSimulate) {
71
- completedSimulations = 0
72
- this.wallet = new Wallet()
73
- this.library = new Map()
74
-
75
- debugDetails[mode] = {}
76
-
77
- console.log(`\nSimulating game mode: ${mode}`)
78
- console.time(mode)
79
-
80
- const runs = this.simRunsAmount[mode] || 0
81
-
82
- if (runs <= 0) continue
83
-
84
- if (!configuredGameModes.includes(mode)) {
85
- throw new Error(
86
- `Tried to simulate game mode "${mode}", but it's not configured in the game config.`,
87
- )
88
- }
89
-
90
- const simNumsToCriteria = ResultSet.assignCriteriaToSimulations(this, mode)
91
-
92
- await this.spawnWorkersForGameMode({ mode, simNumsToCriteria })
93
-
94
- createDirIfNotExists(
95
- path.join(
96
- process.cwd(),
97
- this.gameConfig.config.outputDir,
98
- "optimization_files",
99
- ),
100
- )
101
- createDirIfNotExists(
102
- path.join(process.cwd(), this.gameConfig.config.outputDir, "publish_files"),
103
- )
104
-
105
- Simulation.writeLookupTableCSV({
106
- gameMode: mode,
107
- library: this.library,
108
- gameConfig: this.gameConfig.config,
109
- })
110
- Simulation.writeLookupTableSegmentedCSV({
111
- gameMode: mode,
112
- library: this.library,
113
- gameConfig: this.gameConfig.config,
114
- })
115
- Simulation.writeRecords({
116
- gameMode: mode,
117
- records: this.records,
118
- gameConfig: this.gameConfig.config,
119
- debug: this.debug,
120
- })
121
- await Simulation.writeBooksJson({
122
- gameMode: mode,
123
- library: this.library,
124
- gameConfig: this.gameConfig.config,
125
- })
126
- Simulation.writeIndexJson({
127
- gameConfig: this.gameConfig.config,
128
- })
129
-
130
- debugDetails[mode].rtp =
131
- this.wallet.getCumulativeWins() /
132
- (runs * this.gameConfig.config.gameModes[mode]!.cost)
133
-
134
- debugDetails[mode].wins = this.wallet.getCumulativeWins()
135
- debugDetails[mode].winsPerSpinType = this.wallet.getCumulativeWinsPerSpinType()
136
-
137
- console.timeEnd(mode)
138
- }
139
-
140
- console.log("\n=== SIMULATION SUMMARY ===")
141
- console.table(debugDetails)
142
- }
143
-
144
- let desiredSims = 0
145
- let actualSims = 0
146
- const criteriaToRetries: Record<string, number> = {}
147
-
148
- // Code that runs for individual processes
149
- if (!isMainThread) {
150
- const { mode, simStart, simEnd, index } = workerData
151
-
152
- const simNumsToCriteria = ResultSet.assignCriteriaToSimulations(this, mode)
153
-
154
- // Run each simulation until the criteria is met.
155
- for (let simId = simStart; simId <= simEnd; simId++) {
156
- if (this.debug) desiredSims++
157
-
158
- const criteria = simNumsToCriteria[simId] || "N/A"
159
- const ctx = new SimulationContext(this.gameConfigOpts)
160
-
161
- if (!criteriaToRetries[criteria]) {
162
- criteriaToRetries[criteria] = 0
163
- }
164
-
165
- ctx.runSingleSimulation({ simId, mode, criteria, index })
166
-
167
- if (this.debug) {
168
- criteriaToRetries[criteria] += ctx.actualSims - 1
169
- actualSims += ctx.actualSims
170
- }
171
- }
172
-
173
- if (this.debug) {
174
- console.log(`Desired ${desiredSims}, Actual ${actualSims}`)
175
- console.log(`Retries per criteria:`, criteriaToRetries)
176
- }
177
-
178
- parentPort?.postMessage({
179
- type: "done",
180
- workerNum: index,
181
- })
182
- }
183
- }
184
-
185
- /**
186
- * Runs all simulations for a specific game mode.
187
- */
188
- async spawnWorkersForGameMode(opts: {
189
- mode: string
190
- simNumsToCriteria: Record<number, string>
191
- }) {
192
- const { mode, simNumsToCriteria } = opts
193
-
194
- const numSims = Object.keys(simNumsToCriteria).length
195
- const simRangesPerChunk = this.getSimRangesForChunks(numSims, this.concurrency!)
196
-
197
- await Promise.all(
198
- simRangesPerChunk.map(([simStart, simEnd], index) => {
199
- return this.callWorker({
200
- basePath: this.gameConfig.config.outputDir,
201
- mode,
202
- simStart,
203
- simEnd,
204
- index,
205
- totalSims: numSims,
206
- })
207
- }),
208
- )
209
- }
210
-
211
- async callWorker(opts: {
212
- basePath: string
213
- mode: string
214
- simStart: number
215
- simEnd: number
216
- index: number
217
- totalSims: number
218
- }) {
219
- const { mode, simEnd, simStart, basePath, index, totalSims } = opts
220
-
221
- function logArrowProgress(current: number, total: number) {
222
- const percentage = (current / total) * 100
223
- const progressBarLength = 50
224
- const filledLength = Math.round((progressBarLength * current) / total)
225
- const bar = "█".repeat(filledLength) + "-".repeat(progressBarLength - filledLength)
226
- process.stdout.write(`\r[${bar}] ${percentage.toFixed(2)}% (${current}/${total})`)
227
- if (current === total) {
228
- process.stdout.write("\n")
229
- }
230
- }
231
-
232
- return new Promise((resolve, reject) => {
233
- const scriptPath = path.join(process.cwd(), basePath, TEMP_FILENAME)
234
-
235
- const worker = new Worker(scriptPath, {
236
- workerData: {
237
- mode,
238
- simStart,
239
- simEnd,
240
- index,
241
- },
242
- })
243
-
244
- worker.on("message", (msg) => {
245
- if (msg.type === "log") {
246
- //console.log(`[Worker ${msg.workerNum}] ${msg.message}`)
247
- } else if (msg.type === "complete") {
248
- completedSimulations++
249
-
250
- if (completedSimulations % 250 === 0) {
251
- logArrowProgress(completedSimulations, totalSims)
252
- }
253
-
254
- // Write data to global library
255
- const book = Book.fromSerialized(msg.book)
256
- this.library.set(book.id.toString(), book)
257
- this.wallet.mergeSerialized(msg.wallet)
258
- this.mergeRecords(msg.records)
259
- } else if (msg.type === "done") {
260
- resolve(true)
261
- }
262
- })
263
-
264
- worker.on("error", (error) => {
265
- console.error("Error:", error)
266
- reject(error)
267
- })
268
-
269
- worker.on("exit", (code) => {
270
- if (code !== 0) {
271
- reject(new Error(`Worker stopped with exit code ${code}`))
272
- }
273
- })
274
- })
275
- }
276
-
277
- /**
278
- * Creates a CSV file in the format "simulationId,weight,payout".
279
- *
280
- * `weight` defaults to 1.
281
- */
282
- private static writeLookupTableCSV(opts: {
283
- gameMode: string
284
- library: Map<string, Book>
285
- gameConfig: GameConfig["config"]
286
- fileNameWithoutExtension?: string
287
- }) {
288
- const { gameMode, fileNameWithoutExtension, library, gameConfig } = opts
289
-
290
- const rows: string[] = []
291
-
292
- for (const [bookId, book] of library.entries()) {
293
- rows.push(`${book.id},1,${Math.round(book.getPayout())}`)
294
- }
295
-
296
- rows.sort((a, b) => Number(a.split(",")[0]) - Number(b.split(",")[0]))
297
-
298
- const outputFileName = fileNameWithoutExtension
299
- ? `${fileNameWithoutExtension}.csv`
300
- : `lookUpTable_${gameMode}.csv`
301
-
302
- const outputFilePath = path.join(gameConfig.outputDir, outputFileName)
303
-
304
- writeFile(outputFilePath, rows.join("\n"))
305
-
306
- return outputFilePath
307
- }
308
-
309
- /**
310
- * Creates a CSV file in the format "simulationId,criteria,payoutBase,payoutFreespins".
311
- */
312
- private static writeLookupTableSegmentedCSV(opts: {
313
- gameMode: string
314
- library: Map<string, Book>
315
- gameConfig: GameConfig["config"]
316
- fileNameWithoutExtension?: string
317
- }) {
318
- const { gameMode, fileNameWithoutExtension, library, gameConfig } = opts
319
-
320
- const rows: string[] = []
321
-
322
- for (const [bookId, book] of library.entries()) {
323
- rows.push(
324
- `${book.id},${book.criteria},${book.getBasegameWins()},${book.getFreespinsWins()}`,
325
- )
326
- }
327
-
328
- rows.sort((a, b) => Number(a.split(",")[0]) - Number(b.split(",")[0]))
329
-
330
- const outputFileName = fileNameWithoutExtension
331
- ? `${fileNameWithoutExtension}.csv`
332
- : `lookUpTableSegmented_${gameMode}.csv`
333
-
334
- const outputFilePath = path.join(gameConfig.outputDir, outputFileName)
335
- writeFile(outputFilePath, rows.join("\n"))
336
-
337
- return outputFilePath
338
- }
339
-
340
- private static writeRecords(opts: {
341
- gameMode: string
342
- records: RecordItem[]
343
- gameConfig: GameConfig["config"]
344
- fileNameWithoutExtension?: string
345
- debug?: boolean
346
- }) {
347
- const { gameMode, fileNameWithoutExtension, records, gameConfig, debug } = opts
348
-
349
- const outputFileName = fileNameWithoutExtension
350
- ? `${fileNameWithoutExtension}.json`
351
- : `force_record_${gameMode}.json`
352
-
353
- const outputFilePath = path.join(gameConfig.outputDir, outputFileName)
354
- writeFile(outputFilePath, JSON.stringify(records, null, 2))
355
-
356
- if (debug) Simulation.logSymbolOccurrences(records)
357
-
358
- return outputFilePath
359
- }
360
-
361
- private static writeIndexJson(opts: { gameConfig: GameConfig["config"] }) {
362
- const { gameConfig } = opts
363
-
364
- const outputFilePath = path.join(
365
- process.cwd(),
366
- gameConfig.outputDir,
367
- "publish_files",
368
- "index.json",
369
- )
370
-
371
- const modes = Object.entries(gameConfig.gameModes).map(([mode, modeConfig]) => ({
372
- name: mode,
373
- cost: modeConfig.cost,
374
- events: `books_${mode}.jsonl.zst`,
375
- weights: `lookUpTable_${mode}_0.csv`,
376
- }))
377
-
378
- writeFile(outputFilePath, JSON.stringify({ modes }, null, 2))
379
- }
380
-
381
- private static async writeBooksJson(opts: {
382
- gameMode: string
383
- library: Map<string, Book>
384
- gameConfig: GameConfig["config"]
385
- fileNameWithoutExtension?: string
386
- }) {
387
- const { gameMode, fileNameWithoutExtension, library, gameConfig } = opts
388
-
389
- const outputFileName = fileNameWithoutExtension
390
- ? `${fileNameWithoutExtension}.jsonl`
391
- : `books_${gameMode}.jsonl`
392
-
393
- const outputFilePath = path.join(gameConfig.outputDir, outputFileName)
394
- const books = Array.from(library.values())
395
- .map((b) => b.serialize())
396
- .map((b) => ({
397
- id: b.id,
398
- payoutMultiplier: b.payout,
399
- events: b.events,
400
- }))
401
- .sort((a, b) => a.id - b.id)
402
-
403
- const contents = JSONL.stringify(books)
404
-
405
- writeFile(outputFilePath, contents)
406
-
407
- const compressedFileName = fileNameWithoutExtension
408
- ? `${fileNameWithoutExtension}.jsonl.zst`
409
- : `books_${gameMode}.jsonl.zst`
410
-
411
- const compressedFilePath = path.join(
412
- process.cwd(),
413
- gameConfig.outputDir,
414
- "publish_files",
415
- compressedFileName,
416
- )
417
-
418
- fs.rmSync(compressedFilePath, { force: true })
419
-
420
- await zstd("-f", outputFilePath, "-o", compressedFilePath)
421
- }
422
-
423
- private static logSymbolOccurrences(records: RecordItem[]) {
424
- const validRecords = records.filter(
425
- (r) =>
426
- r.search.some((s) => s.name === "symbolId") &&
427
- r.search.some((s) => s.name === "kind"),
428
- )
429
-
430
- const structuredRecords = validRecords
431
- .map((r) => {
432
- const symbolEntry = r.search.find((s) => s.name === "symbolId")
433
- const kindEntry = r.search.find((s) => s.name === "kind")
434
- const spinTypeEntry = r.search.find((s) => s.name === "spinType")
435
- return {
436
- symbol: symbolEntry ? symbolEntry.value : "unknown",
437
- kind: kindEntry ? kindEntry.value : "unknown",
438
- spinType: spinTypeEntry ? spinTypeEntry.value : "unknown",
439
- timesTriggered: r.timesTriggered,
440
- }
441
- })
442
- .sort((a, b) => {
443
- if (a.symbol < b.symbol) return -1
444
- if (a.symbol > b.symbol) return 1
445
- if (a.kind < b.kind) return -1
446
- if (a.kind > b.kind) return 1
447
- if (a.spinType < b.spinType) return -1
448
- if (a.spinType > b.spinType) return 1
449
- return 0
450
- })
451
-
452
- console.table(structuredRecords)
453
- }
454
-
455
- /**
456
- * Compiles user configured game to JS for use in different Node processes
457
- */
458
- private preprocessFiles() {
459
- const builtFilePath = path.join(this.gameConfig.config.outputDir, TEMP_FILENAME)
460
- fs.rmSync(builtFilePath, { force: true })
461
- buildSync({
462
- entryPoints: [process.cwd()],
463
- bundle: true,
464
- platform: "node",
465
- outfile: path.join(this.gameConfig.config.outputDir, TEMP_FILENAME),
466
- external: ["esbuild", "@mongodb-js/zstd"],
467
- })
468
- }
469
-
470
- private getSimRangesForChunks(total: number, chunks: number): [number, number][] {
471
- const base = Math.floor(total / chunks)
472
- const remainder = total % chunks
473
- const result: [number, number][] = []
474
-
475
- let current = 1
476
-
477
- for (let i = 0; i < chunks; i++) {
478
- const size = base + (i < remainder ? 1 : 0)
479
- const start = current
480
- const end = current + size - 1
481
- result.push([start, end])
482
- current = end + 1
483
- }
484
-
485
- return result
486
- }
487
-
488
- private mergeRecords(otherRecords: RecordItem[]) {
489
- for (const otherRecord of otherRecords) {
490
- let record = this.records.find((r) => {
491
- if (r.search.length !== otherRecord.search.length) return false
492
- for (let i = 0; i < r.search.length; i++) {
493
- if (r.search[i]!.name !== otherRecord.search[i]!.name) return false
494
- if (r.search[i]!.value !== otherRecord.search[i]!.value) return false
495
- }
496
- return true
497
- })
498
- if (!record) {
499
- record = {
500
- search: otherRecord.search,
501
- timesTriggered: 0,
502
- bookIds: [],
503
- }
504
- this.records.push(record)
505
- }
506
- record.timesTriggered += otherRecord.timesTriggered
507
- for (const bookId of otherRecord.bookIds) {
508
- if (!record.bookIds.includes(bookId)) {
509
- record.bookIds.push(bookId)
510
- }
511
- }
512
- }
513
- }
514
- }
515
-
516
- export type SimulationConfigOpts = {
517
- /**
518
- * Object containing the game modes and their respective simulation runs amount.
519
- */
520
- simRunsAmount: Partial<Record<GameModeName, number>>
521
- /**
522
- * Number of concurrent processes to use for simulations.
523
- *
524
- * Default: 6
525
- */
526
- concurrency?: number
527
- }
528
-
529
- /**
530
- * @internal
531
- */
532
- export type AnySimulationContext<
533
- TGameModes extends AnyGameModes = AnyGameModes,
534
- TSymbols extends AnySymbols = AnySymbols,
535
- TUserState extends AnyUserData = AnyUserData,
536
- > = SimulationContext<TGameModes, TSymbols, TUserState>
537
-
538
- /**
539
- * @internal
540
- */
541
- export class SimulationContext<
542
- TGameModes extends AnyGameModes,
543
- TSymbols extends AnySymbols,
544
- TUserState extends AnyUserData,
545
- > extends Board<TGameModes, TSymbols, TUserState> {
546
- constructor(opts: CommonGameOptions<any, any, TUserState>) {
547
- super(opts)
548
- }
549
-
550
- actualSims = 0
551
-
552
- /**
553
- * Will run a single simulation until the specified criteria is met.
554
- */
555
- runSingleSimulation(opts: {
556
- simId: number
557
- mode: string
558
- criteria: string
559
- index: number
560
- }) {
561
- const { simId, mode, criteria, index } = opts
562
-
563
- this.state.currentGameMode = mode
564
- this.state.currentSimulationId = simId
565
- this.state.isCriteriaMet = false
566
-
567
- while (!this.state.isCriteriaMet) {
568
- this.actualSims++
569
- this.resetSimulation()
570
-
571
- const resultSet = this.getGameModeCriteria(this.state.currentGameMode, criteria)
572
- this.state.currentResultSet = resultSet
573
- this.state.book.criteria = resultSet.criteria
574
-
575
- this.handleGameFlow()
576
-
577
- if (resultSet.meetsCriteria(this)) {
578
- this.state.isCriteriaMet = true
579
- this.config.hooks.onSimulationAccepted?.(this)
580
- this.record({
581
- criteria: resultSet.criteria,
582
- })
583
- }
584
- }
585
-
586
- this.wallet.confirmWins(this)
587
- this.confirmRecords()
588
-
589
- parentPort?.postMessage({
590
- type: "complete",
591
- simId,
592
- book: this.state.book.serialize(),
593
- wallet: this.wallet.serialize(),
594
- records: this.getRecords(),
595
- })
596
- }
597
-
598
- /**
599
- * If a simulation does not meet the required criteria, reset the state to run it again.
600
- *
601
- * This also runs once before each simulation to ensure a clean state.
602
- */
603
- protected resetSimulation() {
604
- this.resetState()
605
- this.resetBoard()
606
- }
607
-
608
- /**
609
- * Contains and executes the entire game logic:
610
- * - Drawing the board
611
- * - Evaluating wins
612
- * - Updating wallet
613
- * - Handling free spins
614
- * - Recording events
615
- *
616
- * You can customize the game flow by implementing the `onHandleGameFlow` hook in the game configuration.
617
- */
618
- protected handleGameFlow() {
619
- this.config.hooks.onHandleGameFlow(this)
620
- }
621
- }
622
-
623
- export interface SimulationOpts {
624
- debug?: boolean
625
- }