@slot-engine/core 0.0.1
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/.turbo/turbo-build.log +33 -0
- package/.turbo/turbo-typecheck.log +4 -0
- package/CHANGELOG.md +7 -0
- package/README.md +8 -0
- package/dist/index.d.mts +1306 -0
- package/dist/index.d.ts +1306 -0
- package/dist/index.js +2929 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +2874 -0
- package/dist/index.mjs.map +1 -0
- package/dist/lib/zstd.exe +0 -0
- package/dist/optimizer-rust/Cargo.toml +19 -0
- package/dist/optimizer-rust/src/exes.rs +154 -0
- package/dist/optimizer-rust/src/main.rs +1659 -0
- package/index.ts +205 -0
- package/lib/zstd.exe +0 -0
- package/optimizer-rust/Cargo.toml +19 -0
- package/optimizer-rust/src/exes.rs +154 -0
- package/optimizer-rust/src/main.rs +1659 -0
- package/package.json +33 -0
- package/src/Board.ts +527 -0
- package/src/Book.ts +83 -0
- package/src/GameConfig.ts +148 -0
- package/src/GameMode.ts +86 -0
- package/src/GameState.ts +272 -0
- package/src/GameSymbol.ts +61 -0
- package/src/ReelGenerator.ts +589 -0
- package/src/ResultSet.ts +207 -0
- package/src/Simulation.ts +625 -0
- package/src/SlotGame.ts +117 -0
- package/src/Wallet.ts +203 -0
- package/src/WinType.ts +102 -0
- package/src/analysis/index.ts +198 -0
- package/src/analysis/utils.ts +128 -0
- package/src/optimizer/OptimizationConditions.ts +99 -0
- package/src/optimizer/OptimizationParameters.ts +46 -0
- package/src/optimizer/OptimizationScaling.ts +18 -0
- package/src/optimizer/index.ts +142 -0
- package/src/utils/math-config.ts +109 -0
- package/src/utils/setup-file.ts +36 -0
- package/src/utils/zstd.ts +28 -0
- package/src/winTypes/ClusterWinType.ts +3 -0
- package/src/winTypes/LinesWinType.ts +208 -0
- package/src/winTypes/ManywaysWinType.ts +3 -0
- package/tsconfig.json +19 -0
- package/utils.ts +270 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
export function parseLookupTable(content: string) {
|
|
2
|
+
const lines = content.trim().split("\n")
|
|
3
|
+
const lut: LookupTable = []
|
|
4
|
+
for (const line of lines) {
|
|
5
|
+
const [indexStr, weightStr, payoutStr] = line.split(",")
|
|
6
|
+
const index = parseInt(indexStr!.trim())
|
|
7
|
+
const weight = parseInt(weightStr!.trim())
|
|
8
|
+
const payout = parseFloat(payoutStr!.trim())
|
|
9
|
+
lut.push([index, weight, payout])
|
|
10
|
+
}
|
|
11
|
+
return lut
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type LookupTable = [number, number, number][]
|
|
15
|
+
|
|
16
|
+
export function getTotalLutWeight(lut: LookupTable) {
|
|
17
|
+
return lut.reduce((sum, [, weight]) => sum + weight, 0)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function getTotalWeight(payoutWeights: PayoutWeights) {
|
|
21
|
+
return Object.values(payoutWeights).reduce((sum, w) => sum + w, 0)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
type PayoutWeights = Record<number, number>
|
|
25
|
+
|
|
26
|
+
export function getPayoutWeights(
|
|
27
|
+
lut: LookupTable,
|
|
28
|
+
opts: { normalize?: boolean } = {},
|
|
29
|
+
): PayoutWeights {
|
|
30
|
+
const { normalize = true } = opts
|
|
31
|
+
const totalWeight = getTotalLutWeight(lut)
|
|
32
|
+
|
|
33
|
+
let payoutWeights: Record<number, number> = {}
|
|
34
|
+
|
|
35
|
+
for (const [, weight, p] of lut) {
|
|
36
|
+
const payout = p / 100
|
|
37
|
+
if (payoutWeights[payout] === undefined) {
|
|
38
|
+
payoutWeights[payout] = 0
|
|
39
|
+
}
|
|
40
|
+
payoutWeights[payout] += weight
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Sort by payout value
|
|
44
|
+
payoutWeights = Object.fromEntries(
|
|
45
|
+
Object.entries(payoutWeights).sort(([a], [b]) => parseFloat(a) - parseFloat(b)),
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
if (normalize) {
|
|
49
|
+
for (const payout in payoutWeights) {
|
|
50
|
+
payoutWeights[payout]! /= totalWeight
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return payoutWeights
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function getNonZeroHitrate(payoutWeights: PayoutWeights) {
|
|
58
|
+
const totalWeight = getTotalWeight(payoutWeights)
|
|
59
|
+
const nonZeroWeight = totalWeight - (payoutWeights[0] ?? 0) * totalWeight
|
|
60
|
+
return nonZeroWeight / totalWeight
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function getNullHitrate(payoutWeights: PayoutWeights) {
|
|
64
|
+
return payoutWeights[0] ?? 0
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function getMaxwinHitrate(payoutWeights: PayoutWeights) {
|
|
68
|
+
const totalWeight = getTotalWeight(payoutWeights)
|
|
69
|
+
const maxWin = Math.max(...Object.keys(payoutWeights).map(Number))
|
|
70
|
+
const hitRate = (payoutWeights[maxWin] || 0) / totalWeight
|
|
71
|
+
return 1 / hitRate
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function getUniquePayouts(payoutWeights: PayoutWeights) {
|
|
75
|
+
return Object.keys(payoutWeights).length
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function getMinWin(payoutWeights: PayoutWeights) {
|
|
79
|
+
const payouts = Object.keys(payoutWeights).map(Number)
|
|
80
|
+
return Math.min(...payouts)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function getMaxWin(payoutWeights: PayoutWeights) {
|
|
84
|
+
const payouts = Object.keys(payoutWeights).map(Number)
|
|
85
|
+
return Math.max(...payouts)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function getAvgWin(payoutWeights: PayoutWeights) {
|
|
89
|
+
let avgWin = 0
|
|
90
|
+
for (const [payoutStr, weight] of Object.entries(payoutWeights)) {
|
|
91
|
+
const payout = parseFloat(payoutStr)
|
|
92
|
+
avgWin += payout * weight
|
|
93
|
+
}
|
|
94
|
+
return avgWin
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function getRtp(payoutWeights: PayoutWeights, cost: number) {
|
|
98
|
+
const avgWin = getAvgWin(payoutWeights)
|
|
99
|
+
return avgWin / cost
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function getStandardDeviation(payoutWeights: PayoutWeights) {
|
|
103
|
+
const variance = getVariance(payoutWeights)
|
|
104
|
+
return Math.sqrt(variance)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function getVariance(payoutWeights: PayoutWeights) {
|
|
108
|
+
const totalWeight = getTotalWeight(payoutWeights)
|
|
109
|
+
const avgWin = getAvgWin(payoutWeights)
|
|
110
|
+
let variance = 0
|
|
111
|
+
for (const [payoutStr, weight] of Object.entries(payoutWeights)) {
|
|
112
|
+
const payout = parseFloat(payoutStr)
|
|
113
|
+
variance += Math.pow(payout - avgWin, 2) * (weight / totalWeight)
|
|
114
|
+
}
|
|
115
|
+
return variance
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function getLessBetHitrate(payoutWeights: PayoutWeights, cost: number) {
|
|
119
|
+
let lessBetWeight = 0
|
|
120
|
+
const totalWeight = getTotalWeight(payoutWeights)
|
|
121
|
+
for (const [payoutStr, weight] of Object.entries(payoutWeights)) {
|
|
122
|
+
const payout = parseFloat(payoutStr)
|
|
123
|
+
if (payout < cost) {
|
|
124
|
+
lessBetWeight += weight
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return lessBetWeight / totalWeight
|
|
128
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import assert from "assert"
|
|
2
|
+
|
|
3
|
+
export class OptimizationConditions {
|
|
4
|
+
protected rtp?: number | "x"
|
|
5
|
+
protected avgWin?: number
|
|
6
|
+
protected hitRate?: number | "x"
|
|
7
|
+
protected searchRange: number[]
|
|
8
|
+
protected forceSearch: Record<string, string>
|
|
9
|
+
priority: number
|
|
10
|
+
|
|
11
|
+
constructor(opts: OptimizationConditionsOpts) {
|
|
12
|
+
let { rtp, avgWin, hitRate, searchConditions, priority } = opts
|
|
13
|
+
|
|
14
|
+
if (rtp == undefined || rtp === "x") {
|
|
15
|
+
assert(avgWin !== undefined && hitRate !== undefined, "If RTP is not specified, hit-rate (hr) and average win amount (av_win) must be given.")
|
|
16
|
+
rtp = Math.round((avgWin! / Number(hitRate)) * 100000) / 100000
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let noneCount = 0
|
|
20
|
+
for (const val of [rtp, avgWin, hitRate]) {
|
|
21
|
+
if (val === undefined) noneCount++
|
|
22
|
+
}
|
|
23
|
+
assert(noneCount <= 1, "Invalid combination of optimization conditions.")
|
|
24
|
+
|
|
25
|
+
this.searchRange = [-1, -1]
|
|
26
|
+
this.forceSearch = {}
|
|
27
|
+
|
|
28
|
+
if (typeof searchConditions === "number") {
|
|
29
|
+
this.searchRange = [searchConditions, searchConditions]
|
|
30
|
+
}
|
|
31
|
+
if (Array.isArray(searchConditions)) {
|
|
32
|
+
if (searchConditions[0] > searchConditions[1] || searchConditions.length !== 2) {
|
|
33
|
+
throw new Error("Invalid searchConditions range.")
|
|
34
|
+
}
|
|
35
|
+
this.searchRange = searchConditions
|
|
36
|
+
}
|
|
37
|
+
if (typeof searchConditions === "object" && !Array.isArray(searchConditions)) {
|
|
38
|
+
this.searchRange = [-1, -1]
|
|
39
|
+
this.forceSearch = searchConditions
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
this.rtp = rtp
|
|
43
|
+
this.avgWin = avgWin
|
|
44
|
+
this.hitRate = hitRate
|
|
45
|
+
this.priority = priority
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
getRtp() {
|
|
49
|
+
return this.rtp
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
getAvgWin() {
|
|
53
|
+
return this.avgWin
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
getHitRate() {
|
|
57
|
+
return this.hitRate
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
getSearchRange() {
|
|
61
|
+
return this.searchRange
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
getForceSearch() {
|
|
65
|
+
return this.forceSearch
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
interface OptimizationConditionsOpts {
|
|
70
|
+
/**
|
|
71
|
+
* The desired RTP (0-1)
|
|
72
|
+
*/
|
|
73
|
+
rtp?: number | "x"
|
|
74
|
+
/**
|
|
75
|
+
* The desired average win (per spin).
|
|
76
|
+
*/
|
|
77
|
+
avgWin?: number
|
|
78
|
+
/**
|
|
79
|
+
* The desired hit rate (e.g. `200` to hit 1 in 200 spins).
|
|
80
|
+
*/
|
|
81
|
+
hitRate?: number | "x"
|
|
82
|
+
/**
|
|
83
|
+
* A way of filtering results by
|
|
84
|
+
*
|
|
85
|
+
* - A number (payout multiplier), e.g. `5000`
|
|
86
|
+
* - Force record value, e.g. `{ "symbolId": "scatter" }`
|
|
87
|
+
* - A range of numbers, e.g. `[0, 100]` (payout multiplier range)
|
|
88
|
+
*/
|
|
89
|
+
searchConditions?: number | Record<string, string> | [number, number]
|
|
90
|
+
/**
|
|
91
|
+
* **Priority matters!**\
|
|
92
|
+
* Higher priority conditions will be evaluated first.\
|
|
93
|
+
* After a book matching this condition is found, the book will be removed from the pool\
|
|
94
|
+
* and can't be used to satisfy other conditions with lower priority.
|
|
95
|
+
*
|
|
96
|
+
* TODO add better explanation
|
|
97
|
+
*/
|
|
98
|
+
priority: number
|
|
99
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export class OptimizationParameters {
|
|
2
|
+
protected parameters: OptimizationParametersOpts
|
|
3
|
+
|
|
4
|
+
constructor(opts?: OptimizationParametersOpts) {
|
|
5
|
+
this.parameters = {
|
|
6
|
+
...OptimizationParameters.DEFAULT_PARAMETERS,
|
|
7
|
+
...opts,
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
static DEFAULT_PARAMETERS: OptimizationParametersOpts = {
|
|
12
|
+
numShowPigs: 5000,
|
|
13
|
+
numPigsPerFence: 10000,
|
|
14
|
+
threadsFenceConstruction: 16,
|
|
15
|
+
threadsShowConstruction: 16,
|
|
16
|
+
testSpins: [50, 100, 200],
|
|
17
|
+
testSpinsWeights: [0.3, 0.4, 0.3],
|
|
18
|
+
simulationTrials: 5000,
|
|
19
|
+
graphIndexes: [],
|
|
20
|
+
run1000Batch: false,
|
|
21
|
+
minMeanToMedian: 4,
|
|
22
|
+
maxMeanToMedian: 8,
|
|
23
|
+
pmbRtp: 1.0,
|
|
24
|
+
scoreType: "rtp",
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
getParameters() {
|
|
28
|
+
return this.parameters
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface OptimizationParametersOpts {
|
|
33
|
+
readonly numShowPigs: number
|
|
34
|
+
readonly numPigsPerFence: number
|
|
35
|
+
readonly threadsFenceConstruction: number
|
|
36
|
+
readonly threadsShowConstruction: number
|
|
37
|
+
readonly testSpins: number[]
|
|
38
|
+
readonly testSpinsWeights: number[]
|
|
39
|
+
readonly simulationTrials: number
|
|
40
|
+
readonly graphIndexes: number[]
|
|
41
|
+
readonly run1000Batch: false
|
|
42
|
+
readonly minMeanToMedian: number
|
|
43
|
+
readonly maxMeanToMedian: number
|
|
44
|
+
readonly pmbRtp: number
|
|
45
|
+
readonly scoreType: "rtp"
|
|
46
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export class OptimizationScaling {
|
|
2
|
+
protected config: OptimizationScalingOpts
|
|
3
|
+
|
|
4
|
+
constructor(opts: OptimizationScalingOpts) {
|
|
5
|
+
this.config = opts
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
getConfig() {
|
|
9
|
+
return this.config
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
type OptimizationScalingOpts = Array<{
|
|
14
|
+
criteria: string
|
|
15
|
+
scaleFactor: number
|
|
16
|
+
winRange: [number, number]
|
|
17
|
+
probability: number
|
|
18
|
+
}>
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { GameConfig, OptimizationConditions, OptimizationScaling } from "../../index"
|
|
2
|
+
import { GameModeName } from "../GameMode"
|
|
3
|
+
import { OptimizationParameters } from "./OptimizationParameters"
|
|
4
|
+
import { makeMathConfig } from "../utils/math-config"
|
|
5
|
+
import { makeSetupFile } from "../utils/setup-file"
|
|
6
|
+
import { spawn } from "child_process"
|
|
7
|
+
import path from "path"
|
|
8
|
+
import { Analysis } from "../analysis"
|
|
9
|
+
import assert from "assert"
|
|
10
|
+
import { isMainThread } from "worker_threads"
|
|
11
|
+
import { SlotGame } from "../SlotGame"
|
|
12
|
+
|
|
13
|
+
export class Optimizer {
|
|
14
|
+
protected readonly gameConfig: GameConfig["config"]
|
|
15
|
+
protected readonly gameModes: OptimzierGameModeConfig
|
|
16
|
+
|
|
17
|
+
constructor(opts: OptimizerOpts) {
|
|
18
|
+
this.gameConfig = opts.game.getConfig().config
|
|
19
|
+
this.gameModes = opts.gameModes
|
|
20
|
+
|
|
21
|
+
this.verifyConfig()
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Runs the optimization process, and runs analysis after.
|
|
26
|
+
*/
|
|
27
|
+
async runOptimization({ gameModes }: OptimizationOpts) {
|
|
28
|
+
if (!isMainThread) return // IMPORTANT: Prevent workers from kicking off (multiple) optimizations
|
|
29
|
+
|
|
30
|
+
const mathConfig = makeMathConfig(this, { writeToFile: true })
|
|
31
|
+
|
|
32
|
+
for (const mode of gameModes) {
|
|
33
|
+
const setupFile = makeSetupFile(this, mode)
|
|
34
|
+
await this.runSingleOptimization()
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
private async runSingleOptimization() {
|
|
39
|
+
return await rustProgram()
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
private verifyConfig() {
|
|
43
|
+
for (const [k, mode] of Object.entries(this.gameModes)) {
|
|
44
|
+
const configMode = this.gameConfig.gameModes[k]
|
|
45
|
+
|
|
46
|
+
if (!configMode) {
|
|
47
|
+
throw new Error(
|
|
48
|
+
`Game mode "${mode}" defined in optimizer config does not exist in the game config.`,
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const conditions = Object.keys(mode.conditions)
|
|
53
|
+
const scalings = Object.keys(mode.scaling)
|
|
54
|
+
const parameters = Object.keys(mode.parameters)
|
|
55
|
+
|
|
56
|
+
for (const condition of conditions) {
|
|
57
|
+
if (!configMode.resultSets.find((r) => r.criteria === condition)) {
|
|
58
|
+
throw new Error(
|
|
59
|
+
`Condition "${condition}" defined in optimizer config for game mode "${k}" does not exist as criteria in any ResultSet of the same game mode.`,
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const criteria = configMode.resultSets.map((r) => r.criteria)
|
|
65
|
+
assert(
|
|
66
|
+
conditions.every((c) => criteria.includes(c)),
|
|
67
|
+
`Not all ResultSet criteria in game mode "${k}" are defined as optimization conditions.`,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
let gameModeRtp = configMode.rtp
|
|
71
|
+
let paramRtp = 0
|
|
72
|
+
for (const cond of conditions) {
|
|
73
|
+
const paramConfig = mode.conditions[cond]!
|
|
74
|
+
paramRtp += Number(paramConfig.getRtp())
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
gameModeRtp = Math.round(gameModeRtp * 1000) / 1000
|
|
78
|
+
paramRtp = Math.round(paramRtp * 1000) / 1000
|
|
79
|
+
|
|
80
|
+
assert(
|
|
81
|
+
gameModeRtp === paramRtp,
|
|
82
|
+
`Sum of all RTP conditions (${paramRtp}) does not match the game mode RTP (${gameModeRtp}) in game mode "${k}".`,
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
getGameConfig() {
|
|
88
|
+
return this.gameConfig
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
getOptimizerGameModes() {
|
|
92
|
+
return this.gameModes
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function rustProgram(...args: string[]) {
|
|
97
|
+
return new Promise((resolve, reject) => {
|
|
98
|
+
const task = spawn("cargo", ["run", "--release", ...args], {
|
|
99
|
+
shell: true,
|
|
100
|
+
cwd: path.join(__dirname, "./optimizer-rust"),
|
|
101
|
+
stdio: "pipe",
|
|
102
|
+
})
|
|
103
|
+
task.on("error", (error) => {
|
|
104
|
+
console.error("Error:", error)
|
|
105
|
+
reject(error)
|
|
106
|
+
})
|
|
107
|
+
task.on("exit", () => {
|
|
108
|
+
resolve(true)
|
|
109
|
+
})
|
|
110
|
+
task.on("close", () => {
|
|
111
|
+
resolve(true)
|
|
112
|
+
})
|
|
113
|
+
task.stdout.on("data", (data) => {
|
|
114
|
+
console.log(data.toString())
|
|
115
|
+
})
|
|
116
|
+
task.stderr.on("data", (data) => {
|
|
117
|
+
console.log(data.toString())
|
|
118
|
+
})
|
|
119
|
+
task.stdout.on("error", (data) => {
|
|
120
|
+
console.log(data.toString())
|
|
121
|
+
reject(data.toString())
|
|
122
|
+
})
|
|
123
|
+
})
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface OptimizationOpts {
|
|
127
|
+
gameModes: string[]
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export interface OptimizerOpts {
|
|
131
|
+
game: SlotGame<any, any, any>
|
|
132
|
+
gameModes: OptimzierGameModeConfig
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export type OptimzierGameModeConfig = Record<
|
|
136
|
+
GameModeName,
|
|
137
|
+
{
|
|
138
|
+
conditions: Record<string, OptimizationConditions>
|
|
139
|
+
scaling: OptimizationScaling
|
|
140
|
+
parameters: OptimizationParameters
|
|
141
|
+
}
|
|
142
|
+
>
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import path from "path"
|
|
2
|
+
import { type Optimizer } from "../optimizer"
|
|
3
|
+
import { writeJsonFile } from "../../utils"
|
|
4
|
+
|
|
5
|
+
export function makeMathConfig(
|
|
6
|
+
optimizer: Optimizer,
|
|
7
|
+
opts: { writeToFile?: boolean } = {},
|
|
8
|
+
) {
|
|
9
|
+
const game = optimizer.getGameConfig()
|
|
10
|
+
const gameModesCfg = optimizer.getOptimizerGameModes()
|
|
11
|
+
const { writeToFile } = opts
|
|
12
|
+
|
|
13
|
+
const isDefined = <T>(v: T | undefined): v is T => v !== undefined
|
|
14
|
+
|
|
15
|
+
const config: MathConfig = {
|
|
16
|
+
game_id: game.id,
|
|
17
|
+
bet_modes: Object.entries(game.gameModes).map(([key, mode]) => ({
|
|
18
|
+
bet_mode: mode.name,
|
|
19
|
+
cost: mode.cost,
|
|
20
|
+
rtp: mode.rtp,
|
|
21
|
+
max_win: game.maxWinX,
|
|
22
|
+
})),
|
|
23
|
+
fences: Object.entries(gameModesCfg).map(([gameModeName, modeCfg]) => ({
|
|
24
|
+
bet_mode: gameModeName,
|
|
25
|
+
fences: Object.entries(modeCfg.conditions)
|
|
26
|
+
.map(([fenceName, fence]) => ({
|
|
27
|
+
name: fenceName,
|
|
28
|
+
avg_win: isDefined(fence.getAvgWin())
|
|
29
|
+
? fence.getAvgWin()!.toString()
|
|
30
|
+
: undefined,
|
|
31
|
+
hr: isDefined(fence.getHitRate()) ? fence.getHitRate()!.toString() : undefined,
|
|
32
|
+
rtp: isDefined(fence.getRtp()) ? fence.getRtp()!.toString() : undefined,
|
|
33
|
+
identity_condition: {
|
|
34
|
+
search: Object.entries(fence.getForceSearch()).map(([k, v]) => ({
|
|
35
|
+
name: k,
|
|
36
|
+
value: v,
|
|
37
|
+
})),
|
|
38
|
+
win_range_start: fence.getSearchRange()[0]!,
|
|
39
|
+
win_range_end: fence.getSearchRange()[1]!,
|
|
40
|
+
opposite: false,
|
|
41
|
+
},
|
|
42
|
+
priority: fence.priority,
|
|
43
|
+
}))
|
|
44
|
+
.sort((a, b) => b.priority - a.priority),
|
|
45
|
+
})),
|
|
46
|
+
dresses: Object.entries(gameModesCfg).flatMap(([gameModeName, modeCfg]) => ({
|
|
47
|
+
bet_mode: gameModeName,
|
|
48
|
+
dresses: modeCfg.scaling.getConfig().map((s) => ({
|
|
49
|
+
fence: s.criteria,
|
|
50
|
+
scale_factor: s.scaleFactor.toString(),
|
|
51
|
+
identity_condition_win_range: s.winRange,
|
|
52
|
+
prob: s.probability,
|
|
53
|
+
})),
|
|
54
|
+
})),
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (writeToFile) {
|
|
58
|
+
const outPath = path.join(process.cwd(), game.outputDir, "math_config.json")
|
|
59
|
+
writeJsonFile(outPath, config)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return config
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export type MathConfig = {
|
|
66
|
+
game_id: string
|
|
67
|
+
bet_modes: Array<{
|
|
68
|
+
bet_mode: string
|
|
69
|
+
cost: number
|
|
70
|
+
rtp: number
|
|
71
|
+
max_win: number
|
|
72
|
+
}>
|
|
73
|
+
fences: Array<{
|
|
74
|
+
bet_mode: string
|
|
75
|
+
fences: Array<Fence>
|
|
76
|
+
}>
|
|
77
|
+
dresses: Array<{
|
|
78
|
+
bet_mode: string
|
|
79
|
+
dresses: Dress[]
|
|
80
|
+
}>
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
interface Search {
|
|
84
|
+
name: string
|
|
85
|
+
value: string
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
interface IdentityCondition {
|
|
89
|
+
search: Search[]
|
|
90
|
+
opposite: boolean
|
|
91
|
+
win_range_start: number
|
|
92
|
+
win_range_end: number
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
interface Fence {
|
|
96
|
+
name: string
|
|
97
|
+
avg_win?: string
|
|
98
|
+
rtp?: string
|
|
99
|
+
hr?: string
|
|
100
|
+
identity_condition: IdentityCondition
|
|
101
|
+
priority: number
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
interface Dress {
|
|
105
|
+
fence: string
|
|
106
|
+
scale_factor: string
|
|
107
|
+
identity_condition_win_range: [number, number]
|
|
108
|
+
prob: number
|
|
109
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import path from "path"
|
|
2
|
+
import { writeFile } from "../../utils"
|
|
3
|
+
import { Optimizer } from "../optimizer"
|
|
4
|
+
|
|
5
|
+
export function makeSetupFile(optimizer: Optimizer, gameMode: string) {
|
|
6
|
+
const gameConfig = optimizer.getGameConfig()
|
|
7
|
+
const optimizerGameModes = optimizer.getOptimizerGameModes()
|
|
8
|
+
const modeConfig = optimizerGameModes[gameMode]
|
|
9
|
+
|
|
10
|
+
if (!modeConfig) {
|
|
11
|
+
throw new Error(`Game mode "${gameMode}" not found in optimizer configuration.`)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const params = modeConfig.parameters.getParameters()
|
|
15
|
+
|
|
16
|
+
let content = ""
|
|
17
|
+
content += `game_name;${gameConfig.id}\n`
|
|
18
|
+
content += `bet_type;${gameMode}\n`
|
|
19
|
+
content += `num_show_pigs;${params.numShowPigs}\n`
|
|
20
|
+
content += `num_pigs_per_fence;${params.numPigsPerFence}\n`
|
|
21
|
+
content += `threads_for_fence_construction;${params.threadsFenceConstruction}\n`
|
|
22
|
+
content += `threads_for_show_construction;${params.threadsShowConstruction}\n`
|
|
23
|
+
content += `score_type;${params.scoreType}\n`
|
|
24
|
+
content += `test_spins;${JSON.stringify(params.testSpins)}\n`
|
|
25
|
+
content += `test_spins_weights;${JSON.stringify(params.testSpinsWeights)}\n`
|
|
26
|
+
content += `simulation_trials;${params.simulationTrials}\n`
|
|
27
|
+
content += `graph_indexes;0\n`
|
|
28
|
+
content += `run_1000_batch;False\n`
|
|
29
|
+
content += `simulation_trials;${params.simulationTrials}\n`
|
|
30
|
+
content += `user_game_build_path;${path.join(process.cwd(), gameConfig.outputDir)}\n`
|
|
31
|
+
content += `pmb_rtp;${params.pmbRtp}\n`
|
|
32
|
+
|
|
33
|
+
const outPath = path.join(__dirname, "./optimizer-rust/src", "setup.txt")
|
|
34
|
+
|
|
35
|
+
writeFile(outPath, content)
|
|
36
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import path from "path"
|
|
2
|
+
import { spawn } from "child_process"
|
|
3
|
+
|
|
4
|
+
export async function zstd(...args: string[]) {
|
|
5
|
+
return new Promise((resolve, reject) => {
|
|
6
|
+
const task = spawn(path.join(__dirname, "./lib/zstd.exe"), args)
|
|
7
|
+
task.on("error", (error) => {
|
|
8
|
+
console.error("Error:", error)
|
|
9
|
+
reject(error)
|
|
10
|
+
})
|
|
11
|
+
task.on("exit", () => {
|
|
12
|
+
resolve(true)
|
|
13
|
+
})
|
|
14
|
+
task.on("close", () => {
|
|
15
|
+
resolve(true)
|
|
16
|
+
})
|
|
17
|
+
task.stdout.on("data", (data) => {
|
|
18
|
+
console.log(data.toString())
|
|
19
|
+
})
|
|
20
|
+
task.stderr.on("data", (data) => {
|
|
21
|
+
console.log(data.toString())
|
|
22
|
+
})
|
|
23
|
+
task.stdout.on("error", (data) => {
|
|
24
|
+
console.log(data.toString())
|
|
25
|
+
reject(data.toString())
|
|
26
|
+
})
|
|
27
|
+
})
|
|
28
|
+
}
|