@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.
Files changed (46) hide show
  1. package/.turbo/turbo-build.log +33 -0
  2. package/.turbo/turbo-typecheck.log +4 -0
  3. package/CHANGELOG.md +7 -0
  4. package/README.md +8 -0
  5. package/dist/index.d.mts +1306 -0
  6. package/dist/index.d.ts +1306 -0
  7. package/dist/index.js +2929 -0
  8. package/dist/index.js.map +1 -0
  9. package/dist/index.mjs +2874 -0
  10. package/dist/index.mjs.map +1 -0
  11. package/dist/lib/zstd.exe +0 -0
  12. package/dist/optimizer-rust/Cargo.toml +19 -0
  13. package/dist/optimizer-rust/src/exes.rs +154 -0
  14. package/dist/optimizer-rust/src/main.rs +1659 -0
  15. package/index.ts +205 -0
  16. package/lib/zstd.exe +0 -0
  17. package/optimizer-rust/Cargo.toml +19 -0
  18. package/optimizer-rust/src/exes.rs +154 -0
  19. package/optimizer-rust/src/main.rs +1659 -0
  20. package/package.json +33 -0
  21. package/src/Board.ts +527 -0
  22. package/src/Book.ts +83 -0
  23. package/src/GameConfig.ts +148 -0
  24. package/src/GameMode.ts +86 -0
  25. package/src/GameState.ts +272 -0
  26. package/src/GameSymbol.ts +61 -0
  27. package/src/ReelGenerator.ts +589 -0
  28. package/src/ResultSet.ts +207 -0
  29. package/src/Simulation.ts +625 -0
  30. package/src/SlotGame.ts +117 -0
  31. package/src/Wallet.ts +203 -0
  32. package/src/WinType.ts +102 -0
  33. package/src/analysis/index.ts +198 -0
  34. package/src/analysis/utils.ts +128 -0
  35. package/src/optimizer/OptimizationConditions.ts +99 -0
  36. package/src/optimizer/OptimizationParameters.ts +46 -0
  37. package/src/optimizer/OptimizationScaling.ts +18 -0
  38. package/src/optimizer/index.ts +142 -0
  39. package/src/utils/math-config.ts +109 -0
  40. package/src/utils/setup-file.ts +36 -0
  41. package/src/utils/zstd.ts +28 -0
  42. package/src/winTypes/ClusterWinType.ts +3 -0
  43. package/src/winTypes/LinesWinType.ts +208 -0
  44. package/src/winTypes/ManywaysWinType.ts +3 -0
  45. package/tsconfig.json +19 -0
  46. 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
+ }
@@ -0,0 +1,3 @@
1
+ import { WinType } from "../WinType"
2
+
3
+ export class ClusterWinType extends WinType {}