@energy8platform/game-engine 0.10.8 → 0.10.10
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/bin/simulate.ts +38 -15
- package/dist/lua.cjs.js +204 -36
- package/dist/lua.cjs.js.map +1 -1
- package/dist/lua.d.ts +45 -2
- package/dist/lua.esm.js +203 -37
- package/dist/lua.esm.js.map +1 -1
- package/package.json +2 -1
- package/src/lua/LuaEngine.ts +56 -35
- package/src/lua/LuaEngineAPI.ts +14 -2
- package/src/lua/ParallelSimulationRunner.ts +156 -0
- package/src/lua/SimulationRunner.ts +1 -0
- package/src/lua/SimulationWorker.ts +44 -0
- package/src/lua/index.ts +2 -0
- package/src/lua/types.ts +12 -0
package/src/lua/LuaEngine.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { PlayParams, SessionData } from '@energy8platform/game-sdk';
|
|
2
2
|
import type { LuaEngineConfig, LuaPlayResult, GameDefinition } from './types';
|
|
3
|
-
import { LuaEngineAPI, createSeededRng, luaToJS, pushJSValue } from './LuaEngineAPI';
|
|
3
|
+
import { LuaEngineAPI, createSeededRng, luaToJS, pushJSValue, cachedToLuastring } from './LuaEngineAPI';
|
|
4
4
|
import { ActionRouter } from './ActionRouter';
|
|
5
5
|
import { SessionManager } from './SessionManager';
|
|
6
6
|
import { PersistentState } from './PersistentState';
|
|
@@ -36,6 +36,11 @@ export class LuaEngine {
|
|
|
36
36
|
private gameDefinition: GameDefinition;
|
|
37
37
|
private variables: Record<string, number> = {};
|
|
38
38
|
private simulationMode: boolean;
|
|
39
|
+
/** Reusable state objects to avoid per-iteration allocation */
|
|
40
|
+
private _stateVars: Record<string, number> = {};
|
|
41
|
+
private _stateParams: Record<string, unknown> = {};
|
|
42
|
+
/** Whether the game uses _persist_ keys (detected on first run, cached) */
|
|
43
|
+
private _hasPersistKeys: boolean | undefined;
|
|
39
44
|
|
|
40
45
|
constructor(config: LuaEngineConfig) {
|
|
41
46
|
this.gameDefinition = config.gameDefinition;
|
|
@@ -103,11 +108,13 @@ export class LuaEngine {
|
|
|
103
108
|
}
|
|
104
109
|
|
|
105
110
|
// 3. Build state.variables (matching server's NewGameState + restore)
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
+
// Reuse pooled object to avoid per-iteration allocation
|
|
112
|
+
const stateVars = this._stateVars;
|
|
113
|
+
// Clear previous keys
|
|
114
|
+
for (const key in stateVars) delete stateVars[key];
|
|
115
|
+
// Apply defaults, then engine vars, then bet
|
|
116
|
+
Object.assign(stateVars, DEFAULT_VARIABLES, this.variables);
|
|
117
|
+
stateVars.bet = bet;
|
|
111
118
|
|
|
112
119
|
// Load cross-spin persistent state
|
|
113
120
|
this.persistentState.loadIntoVariables(stateVars);
|
|
@@ -128,8 +135,10 @@ export class LuaEngine {
|
|
|
128
135
|
stateVars.free_spins_remaining = this.sessionManager.spinsRemaining;
|
|
129
136
|
}
|
|
130
137
|
|
|
131
|
-
// 4. Build state.params
|
|
132
|
-
const stateParams
|
|
138
|
+
// 4. Build state.params (reuse pooled object)
|
|
139
|
+
const stateParams = this._stateParams;
|
|
140
|
+
for (const key in stateParams) delete stateParams[key];
|
|
141
|
+
if (clientParams) Object.assign(stateParams, clientParams);
|
|
133
142
|
stateParams._action = actionName;
|
|
134
143
|
|
|
135
144
|
// Inject session _ps_* persistent data
|
|
@@ -169,10 +178,12 @@ export class LuaEngine {
|
|
|
169
178
|
const resultVariables = (luaResult.variables ?? {}) as Record<string, number>;
|
|
170
179
|
const spinWin = Math.round(totalWinMultiplier * bet * 100) / 100;
|
|
171
180
|
|
|
172
|
-
// Merge
|
|
181
|
+
// Merge ONLY Lua return variables into engine state (not the whole stateVars).
|
|
182
|
+
// On the server, state.Variables is a temporary object rebuilt each call.
|
|
183
|
+
// Only the Lua result's `variables` table persists between calls.
|
|
184
|
+
Object.assign(this.variables, resultVariables);
|
|
185
|
+
// Also update stateVars for transition evaluation below
|
|
173
186
|
Object.assign(stateVars, resultVariables);
|
|
174
|
-
this.variables = { ...stateVars };
|
|
175
|
-
delete this.variables.bet;
|
|
176
187
|
|
|
177
188
|
// Build client data (everything except special keys)
|
|
178
189
|
const data: Record<string, unknown> = {};
|
|
@@ -186,8 +197,8 @@ export class LuaEngine {
|
|
|
186
197
|
this.sessionManager.storePersistData(data);
|
|
187
198
|
this.persistentState.storeGameData(data);
|
|
188
199
|
|
|
189
|
-
// Save cross-spin persistent state
|
|
190
|
-
this.persistentState.saveFromVariables(
|
|
200
|
+
// Save cross-spin persistent state (from stateVars which has Lua result merged)
|
|
201
|
+
this.persistentState.saveFromVariables(stateVars);
|
|
191
202
|
|
|
192
203
|
// Add exposed persistent vars to client data
|
|
193
204
|
const exposedVars = this.persistentState.getExposedVars();
|
|
@@ -202,8 +213,8 @@ export class LuaEngine {
|
|
|
202
213
|
}
|
|
203
214
|
}
|
|
204
215
|
|
|
205
|
-
// 8. Evaluate transitions (server
|
|
206
|
-
const { rule, nextActions } = this.actionRouter.evaluateTransitions(action,
|
|
216
|
+
// 8. Evaluate transitions (server uses state.Variables which is stateVars)
|
|
217
|
+
const { rule, nextActions } = this.actionRouter.evaluateTransitions(action, stateVars);
|
|
207
218
|
|
|
208
219
|
// 9. Determine credit behavior (server: creditNow logic)
|
|
209
220
|
let creditDeferred = action.credit === 'defer' || rule.credit_override === 'defer';
|
|
@@ -218,7 +229,7 @@ export class LuaEngine {
|
|
|
218
229
|
|
|
219
230
|
if (rule.creates_session && !this.sessionManager.isActive) {
|
|
220
231
|
// CREATE SESSION — initial spin counted (server: createSession includes spinWin)
|
|
221
|
-
session = this.sessionManager.createSession(rule,
|
|
232
|
+
session = this.sessionManager.createSession(rule, stateVars, bet, spinWin, maxWinCap);
|
|
222
233
|
creditDeferred = true;
|
|
223
234
|
resultTotalWin = spinWin;
|
|
224
235
|
|
|
@@ -228,7 +239,7 @@ export class LuaEngine {
|
|
|
228
239
|
}
|
|
229
240
|
} else if (this.sessionManager.isActive) {
|
|
230
241
|
// UPDATE SESSION — accumulate win, check completion
|
|
231
|
-
session = this.sessionManager.updateSession(rule,
|
|
242
|
+
session = this.sessionManager.updateSession(rule, stateVars, spinWin);
|
|
232
243
|
|
|
233
244
|
if (session?.completed) {
|
|
234
245
|
// SESSION COMPLETED — server returns session.TotalWin as result.TotalWin
|
|
@@ -262,7 +273,8 @@ export class LuaEngine {
|
|
|
262
273
|
data,
|
|
263
274
|
nextActions,
|
|
264
275
|
session,
|
|
265
|
-
|
|
276
|
+
// In simulation mode, return reference directly (caller only reads, never mutates)
|
|
277
|
+
variables: this.simulationMode ? this.variables : { ...this.variables },
|
|
266
278
|
creditDeferred,
|
|
267
279
|
};
|
|
268
280
|
}
|
|
@@ -290,7 +302,7 @@ export class LuaEngine {
|
|
|
290
302
|
throw new Error(`Failed to load Lua script: ${err}`);
|
|
291
303
|
}
|
|
292
304
|
|
|
293
|
-
lua.lua_getglobal(this.L,
|
|
305
|
+
lua.lua_getglobal(this.L, cachedToLuastring('execute'));
|
|
294
306
|
if (lua.lua_type(this.L, -1) !== lua.LUA_TFUNCTION) {
|
|
295
307
|
lua.lua_pop(this.L, 1);
|
|
296
308
|
throw new Error('Lua script must define a global `execute(state)` function');
|
|
@@ -304,26 +316,26 @@ export class LuaEngine {
|
|
|
304
316
|
params: Record<string, unknown>,
|
|
305
317
|
variables: Record<string, number>,
|
|
306
318
|
): Record<string, unknown> {
|
|
307
|
-
lua.lua_getglobal(this.L,
|
|
319
|
+
lua.lua_getglobal(this.L, cachedToLuastring('execute'));
|
|
308
320
|
|
|
309
321
|
// Build state table: {stage, action, params, variables}
|
|
310
322
|
lua.lua_createtable(this.L, 0, 4);
|
|
311
323
|
|
|
312
324
|
// state.stage
|
|
313
|
-
lua.lua_pushstring(this.L,
|
|
314
|
-
lua.lua_setfield(this.L, -2,
|
|
325
|
+
lua.lua_pushstring(this.L, cachedToLuastring(stage));
|
|
326
|
+
lua.lua_setfield(this.L, -2, cachedToLuastring('stage'));
|
|
315
327
|
|
|
316
328
|
// state.action (server sets this at top level)
|
|
317
|
-
lua.lua_pushstring(this.L,
|
|
318
|
-
lua.lua_setfield(this.L, -2,
|
|
329
|
+
lua.lua_pushstring(this.L, cachedToLuastring(action));
|
|
330
|
+
lua.lua_setfield(this.L, -2, cachedToLuastring('action'));
|
|
319
331
|
|
|
320
332
|
// state.params
|
|
321
333
|
pushJSValue(this.L, params);
|
|
322
|
-
lua.lua_setfield(this.L, -2,
|
|
334
|
+
lua.lua_setfield(this.L, -2, cachedToLuastring('params'));
|
|
323
335
|
|
|
324
336
|
// state.variables
|
|
325
337
|
pushJSValue(this.L, variables);
|
|
326
|
-
lua.lua_setfield(this.L, -2,
|
|
338
|
+
lua.lua_setfield(this.L, -2, cachedToLuastring('variables'));
|
|
327
339
|
|
|
328
340
|
const status = lua.lua_pcall(this.L, 1, 1, 0);
|
|
329
341
|
if (status !== lua.LUA_OK) {
|
|
@@ -336,26 +348,35 @@ export class LuaEngine {
|
|
|
336
348
|
// Fast path: extract only total_win, variables, _persist_* keys
|
|
337
349
|
const result: Record<string, unknown> = {};
|
|
338
350
|
|
|
339
|
-
lua.lua_getfield(this.L, -1,
|
|
351
|
+
lua.lua_getfield(this.L, -1, cachedToLuastring('total_win'));
|
|
340
352
|
result.total_win = lua.lua_type(this.L, -1) === lua.LUA_TNUMBER
|
|
341
353
|
? lua.lua_tonumber(this.L, -1) : 0;
|
|
342
354
|
lua.lua_pop(this.L, 1);
|
|
343
355
|
|
|
344
|
-
lua.lua_getfield(this.L, -1,
|
|
356
|
+
lua.lua_getfield(this.L, -1, cachedToLuastring('variables'));
|
|
345
357
|
if (lua.lua_type(this.L, -1) === lua.LUA_TTABLE) {
|
|
346
358
|
result.variables = luaToJS(this.L, -1);
|
|
347
359
|
}
|
|
348
360
|
lua.lua_pop(this.L, 1);
|
|
349
361
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
362
|
+
// Scan for _persist_* keys only if the game might use them
|
|
363
|
+
if (this._hasPersistKeys !== false) {
|
|
364
|
+
let foundPersist = false;
|
|
365
|
+
lua.lua_pushnil(this.L);
|
|
366
|
+
while (lua.lua_next(this.L, -2) !== 0) {
|
|
367
|
+
if (lua.lua_type(this.L, -2) === lua.LUA_TSTRING) {
|
|
368
|
+
const key = to_jsstring(lua.lua_tostring(this.L, -2));
|
|
369
|
+
if (key.startsWith('_persist_')) {
|
|
370
|
+
result[key] = luaToJS(this.L, -1);
|
|
371
|
+
foundPersist = true;
|
|
372
|
+
}
|
|
356
373
|
}
|
|
374
|
+
lua.lua_pop(this.L, 1);
|
|
375
|
+
}
|
|
376
|
+
// After first iteration, cache whether persist keys exist
|
|
377
|
+
if (this._hasPersistKeys === undefined) {
|
|
378
|
+
this._hasPersistKeys = foundPersist;
|
|
357
379
|
}
|
|
358
|
-
lua.lua_pop(this.L, 1);
|
|
359
380
|
}
|
|
360
381
|
|
|
361
382
|
lua.lua_pop(this.L, 1);
|
package/src/lua/LuaEngineAPI.ts
CHANGED
|
@@ -6,6 +6,18 @@ const { to_luastring, to_jsstring } = fengari;
|
|
|
6
6
|
|
|
7
7
|
export type RngFunction = () => number;
|
|
8
8
|
|
|
9
|
+
/** Cache for to_luastring() results — avoids re-encoding the same keys every iteration */
|
|
10
|
+
const luaStringCache = new Map<string, Uint8Array>();
|
|
11
|
+
|
|
12
|
+
export function cachedToLuastring(s: string): Uint8Array {
|
|
13
|
+
let cached = luaStringCache.get(s);
|
|
14
|
+
if (!cached) {
|
|
15
|
+
cached = to_luastring(s);
|
|
16
|
+
luaStringCache.set(s, cached);
|
|
17
|
+
}
|
|
18
|
+
return cached;
|
|
19
|
+
}
|
|
20
|
+
|
|
9
21
|
/**
|
|
10
22
|
* Seeded xoshiro128** PRNG for deterministic simulation/replay.
|
|
11
23
|
* Period: 2^128 - 1
|
|
@@ -272,7 +284,7 @@ export function pushJSValue(L: any, value: unknown): void {
|
|
|
272
284
|
lua.lua_pushnumber(L, value);
|
|
273
285
|
}
|
|
274
286
|
} else if (typeof value === 'string') {
|
|
275
|
-
lua.lua_pushstring(L,
|
|
287
|
+
lua.lua_pushstring(L, cachedToLuastring(value));
|
|
276
288
|
} else if (Array.isArray(value)) {
|
|
277
289
|
pushJSArray(L, value);
|
|
278
290
|
} else if (typeof value === 'object') {
|
|
@@ -297,6 +309,6 @@ function pushJSObject(L: any, obj: Record<string, unknown>): void {
|
|
|
297
309
|
lua.lua_createtable(L, 0, keys.length);
|
|
298
310
|
for (const key of keys) {
|
|
299
311
|
pushJSValue(L, obj[key]);
|
|
300
|
-
lua.lua_setfield(L, -2,
|
|
312
|
+
lua.lua_setfield(L, -2, cachedToLuastring(key));
|
|
301
313
|
}
|
|
302
314
|
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
import { Worker } from 'worker_threads';
|
|
3
|
+
import { cpus } from 'os';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
import { dirname, join } from 'path';
|
|
6
|
+
import type { SimulationConfig, SimulationResult, SimulationRawAccumulators } from './types';
|
|
7
|
+
import type { WorkerMessage, WorkerConfig } from './SimulationWorker';
|
|
8
|
+
|
|
9
|
+
const SEED_STRIDE = 1 << 20; // 2^20 — gap between worker seeds to avoid overlap
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Runs simulation across multiple worker threads for parallel speedup.
|
|
13
|
+
* Each worker gets an independent LuaEngine with a partitioned seed range.
|
|
14
|
+
*
|
|
15
|
+
* Results are statistically equivalent to single-threaded mode but not
|
|
16
|
+
* bit-identical (different RNG sequence ordering).
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```ts
|
|
20
|
+
* const runner = new ParallelSimulationRunner({
|
|
21
|
+
* script: luaSource,
|
|
22
|
+
* gameDefinition,
|
|
23
|
+
* iterations: 1_000_000,
|
|
24
|
+
* bet: 1.0,
|
|
25
|
+
* workerCount: 8,
|
|
26
|
+
* onProgress: (done, total) => console.log(`${done}/${total}`),
|
|
27
|
+
* });
|
|
28
|
+
* const result = await runner.run();
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export class ParallelSimulationRunner {
|
|
32
|
+
private config: SimulationConfig;
|
|
33
|
+
private workerCount: number;
|
|
34
|
+
|
|
35
|
+
constructor(config: SimulationConfig) {
|
|
36
|
+
this.config = config;
|
|
37
|
+
const maxWorkers = cpus().length;
|
|
38
|
+
this.workerCount = Math.max(1, Math.min(
|
|
39
|
+
config.workerCount ?? maxWorkers,
|
|
40
|
+
maxWorkers,
|
|
41
|
+
config.iterations, // no point having more workers than iterations
|
|
42
|
+
));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async run(): Promise<SimulationResult> {
|
|
46
|
+
const {
|
|
47
|
+
iterations,
|
|
48
|
+
seed,
|
|
49
|
+
onProgress,
|
|
50
|
+
workerCount: _,
|
|
51
|
+
...restConfig
|
|
52
|
+
} = this.config;
|
|
53
|
+
|
|
54
|
+
const workerCount = this.workerCount;
|
|
55
|
+
|
|
56
|
+
// Split iterations evenly, remainder goes to last worker
|
|
57
|
+
const baseChunk = Math.floor(iterations / workerCount);
|
|
58
|
+
const remainder = iterations - baseChunk * workerCount;
|
|
59
|
+
|
|
60
|
+
const workerPath = join(dirname(fileURLToPath(import.meta.url)), 'SimulationWorker.ts');
|
|
61
|
+
|
|
62
|
+
const progressPerWorker = new Array<number>(workerCount).fill(0);
|
|
63
|
+
const totalIterations = iterations;
|
|
64
|
+
|
|
65
|
+
const promises = Array.from({ length: workerCount }, (_, i) => {
|
|
66
|
+
const workerIterations = baseChunk + (i < remainder ? 1 : 0);
|
|
67
|
+
const workerSeed = seed !== undefined ? seed + i * SEED_STRIDE : undefined;
|
|
68
|
+
|
|
69
|
+
const workerConfig: WorkerConfig = {
|
|
70
|
+
config: {
|
|
71
|
+
...restConfig,
|
|
72
|
+
iterations: workerIterations,
|
|
73
|
+
seed: workerSeed,
|
|
74
|
+
progressInterval: this.config.progressInterval,
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
return new Promise<SimulationResult>((resolve, reject) => {
|
|
79
|
+
const worker = new Worker(workerPath, {
|
|
80
|
+
workerData: workerConfig,
|
|
81
|
+
// tsx registers itself via --require/--import; pass through to workers
|
|
82
|
+
execArgv: process.execArgv,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
worker.on('message', (msg: WorkerMessage) => {
|
|
86
|
+
if (msg.type === 'progress' && onProgress) {
|
|
87
|
+
progressPerWorker[i] = msg.progress!.completed;
|
|
88
|
+
const totalCompleted = progressPerWorker.reduce((a, b) => a + b, 0);
|
|
89
|
+
onProgress(totalCompleted, totalIterations);
|
|
90
|
+
} else if (msg.type === 'result') {
|
|
91
|
+
resolve(msg.result!);
|
|
92
|
+
} else if (msg.type === 'error') {
|
|
93
|
+
reject(new Error(`Worker ${i} failed: ${msg.error}`));
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
worker.on('error', (err: Error) => reject(new Error(`Worker ${i} error: ${err.message}`)));
|
|
98
|
+
worker.on('exit', (code) => {
|
|
99
|
+
if (code !== 0) reject(new Error(`Worker ${i} exited with code ${code}`));
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const results = await Promise.all(promises);
|
|
105
|
+
return aggregateResults(results);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function aggregateResults(results: SimulationResult[]): SimulationResult {
|
|
110
|
+
const raw: SimulationRawAccumulators = {
|
|
111
|
+
totalWagered: 0,
|
|
112
|
+
totalWon: 0,
|
|
113
|
+
baseGameWin: 0,
|
|
114
|
+
bonusWin: 0,
|
|
115
|
+
hits: 0,
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
let iterations = 0;
|
|
119
|
+
let maxWin = 0;
|
|
120
|
+
let maxWinHits = 0;
|
|
121
|
+
let bonusTriggered = 0;
|
|
122
|
+
let bonusSpinsPlayed = 0;
|
|
123
|
+
let maxDurationMs = 0;
|
|
124
|
+
|
|
125
|
+
for (const r of results) {
|
|
126
|
+
const rr = r._raw!;
|
|
127
|
+
raw.totalWagered += rr.totalWagered;
|
|
128
|
+
raw.totalWon += rr.totalWon;
|
|
129
|
+
raw.baseGameWin += rr.baseGameWin;
|
|
130
|
+
raw.bonusWin += rr.bonusWin;
|
|
131
|
+
raw.hits += rr.hits;
|
|
132
|
+
|
|
133
|
+
iterations += r.iterations;
|
|
134
|
+
if (r.maxWin > maxWin) maxWin = r.maxWin;
|
|
135
|
+
maxWinHits += r.maxWinHits;
|
|
136
|
+
bonusTriggered += r.bonusTriggered;
|
|
137
|
+
bonusSpinsPlayed += r.bonusSpinsPlayed;
|
|
138
|
+
if (r.durationMs > maxDurationMs) maxDurationMs = r.durationMs;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
gameId: results[0].gameId,
|
|
143
|
+
action: results[0].action,
|
|
144
|
+
iterations,
|
|
145
|
+
durationMs: maxDurationMs,
|
|
146
|
+
totalRtp: raw.totalWagered > 0 ? (raw.totalWon / raw.totalWagered) * 100 : 0,
|
|
147
|
+
baseGameRtp: raw.totalWagered > 0 ? (raw.baseGameWin / raw.totalWagered) * 100 : 0,
|
|
148
|
+
bonusRtp: raw.totalWagered > 0 ? (raw.bonusWin / raw.totalWagered) * 100 : 0,
|
|
149
|
+
hitFrequency: iterations > 0 ? (raw.hits / iterations) * 100 : 0,
|
|
150
|
+
maxWin,
|
|
151
|
+
maxWinHits,
|
|
152
|
+
bonusTriggered,
|
|
153
|
+
bonusSpinsPlayed,
|
|
154
|
+
_raw: raw,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
import { parentPort, workerData } from 'worker_threads';
|
|
3
|
+
import { SimulationRunner } from './SimulationRunner';
|
|
4
|
+
import type { SimulationConfig, SimulationResult } from './types';
|
|
5
|
+
|
|
6
|
+
export interface WorkerMessage {
|
|
7
|
+
type: 'progress' | 'result' | 'error';
|
|
8
|
+
progress?: { completed: number; total: number };
|
|
9
|
+
result?: SimulationResult;
|
|
10
|
+
error?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface WorkerConfig {
|
|
14
|
+
config: Omit<SimulationConfig, 'onProgress' | 'workerCount'>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function run() {
|
|
18
|
+
const { config } = workerData as WorkerConfig;
|
|
19
|
+
|
|
20
|
+
const runner = new SimulationRunner({
|
|
21
|
+
...config,
|
|
22
|
+
onProgress: (completed, total) => {
|
|
23
|
+
parentPort!.postMessage({
|
|
24
|
+
type: 'progress',
|
|
25
|
+
progress: { completed, total },
|
|
26
|
+
} satisfies WorkerMessage);
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
const result = runner.run();
|
|
32
|
+
parentPort!.postMessage({
|
|
33
|
+
type: 'result',
|
|
34
|
+
result,
|
|
35
|
+
} satisfies WorkerMessage);
|
|
36
|
+
} catch (e: any) {
|
|
37
|
+
parentPort!.postMessage({
|
|
38
|
+
type: 'error',
|
|
39
|
+
error: e.message ?? String(e),
|
|
40
|
+
} satisfies WorkerMessage);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
run();
|
package/src/lua/index.ts
CHANGED
|
@@ -4,6 +4,7 @@ export { ActionRouter, evaluateCondition } from './ActionRouter';
|
|
|
4
4
|
export { SessionManager } from './SessionManager';
|
|
5
5
|
export { PersistentState } from './PersistentState';
|
|
6
6
|
export { SimulationRunner, formatSimulationResult } from './SimulationRunner';
|
|
7
|
+
export { ParallelSimulationRunner } from './ParallelSimulationRunner';
|
|
7
8
|
export type {
|
|
8
9
|
GameDefinition,
|
|
9
10
|
ActionDefinition,
|
|
@@ -19,4 +20,5 @@ export type {
|
|
|
19
20
|
BetLevelsConfig,
|
|
20
21
|
SimulationConfig,
|
|
21
22
|
SimulationResult,
|
|
23
|
+
SimulationRawAccumulators,
|
|
22
24
|
} from './types';
|
package/src/lua/types.ts
CHANGED
|
@@ -114,6 +114,8 @@ export interface SimulationConfig {
|
|
|
114
114
|
progressInterval?: number;
|
|
115
115
|
/** Progress callback */
|
|
116
116
|
onProgress?: (completed: number, total: number) => void;
|
|
117
|
+
/** Number of worker threads for parallel simulation (default: os.cpus().length) */
|
|
118
|
+
workerCount?: number;
|
|
117
119
|
}
|
|
118
120
|
|
|
119
121
|
export interface SimulationResult {
|
|
@@ -129,6 +131,16 @@ export interface SimulationResult {
|
|
|
129
131
|
maxWinHits: number;
|
|
130
132
|
bonusTriggered: number;
|
|
131
133
|
bonusSpinsPlayed: number;
|
|
134
|
+
/** Raw accumulators for aggregation across workers */
|
|
135
|
+
_raw?: SimulationRawAccumulators;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export interface SimulationRawAccumulators {
|
|
139
|
+
totalWagered: number;
|
|
140
|
+
totalWon: number;
|
|
141
|
+
baseGameWin: number;
|
|
142
|
+
bonusWin: number;
|
|
143
|
+
hits: number;
|
|
132
144
|
}
|
|
133
145
|
|
|
134
146
|
export type { SessionData, PlayParams };
|