@energy8platform/platform-core 0.28.3 → 0.29.0

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 (56) hide show
  1. package/README.md +127 -150
  2. package/bin/simulate.ts +35 -98
  3. package/dist/dev-bridge.cjs.js +3 -3
  4. package/dist/dev-bridge.cjs.js.map +1 -1
  5. package/dist/dev-bridge.d.ts +2 -2
  6. package/dist/dev-bridge.esm.js +3 -3
  7. package/dist/dev-bridge.esm.js.map +1 -1
  8. package/dist/game-spec.cjs.js +59 -59
  9. package/dist/game-spec.cjs.js.map +1 -1
  10. package/dist/game-spec.d.ts +24 -23
  11. package/dist/game-spec.esm.js +57 -56
  12. package/dist/game-spec.esm.js.map +1 -1
  13. package/dist/index.cjs.js +3 -3
  14. package/dist/index.cjs.js.map +1 -1
  15. package/dist/index.d.ts +21 -2
  16. package/dist/index.esm.js +3 -3
  17. package/dist/index.esm.js.map +1 -1
  18. package/dist/lua.cjs.js +0 -1234
  19. package/dist/lua.cjs.js.map +1 -1
  20. package/dist/lua.d.ts +1 -206
  21. package/dist/lua.esm.js +0 -1225
  22. package/dist/lua.esm.js.map +1 -1
  23. package/dist/simulation.cjs.js +48 -179
  24. package/dist/simulation.cjs.js.map +1 -1
  25. package/dist/simulation.d.ts +26 -60
  26. package/dist/simulation.esm.js +48 -178
  27. package/dist/simulation.esm.js.map +1 -1
  28. package/dist/vite.cjs.js +323 -109
  29. package/dist/vite.cjs.js.map +1 -1
  30. package/dist/vite.d.ts +19 -9
  31. package/dist/vite.esm.js +322 -109
  32. package/dist/vite.esm.js.map +1 -1
  33. package/package.json +6 -5
  34. package/scripts/install-e8.mjs +113 -0
  35. package/src/dev-bridge/DevBridge.ts +3 -3
  36. package/src/game-spec/defineGame.ts +2 -2
  37. package/src/game-spec/derive.ts +39 -16
  38. package/src/game-spec/export.ts +23 -39
  39. package/src/game-spec/index.ts +3 -3
  40. package/src/game-spec/types.ts +2 -1
  41. package/src/index.ts +6 -12
  42. package/src/lua/index.ts +4 -11
  43. package/src/simulation/NativeSimulationRunner.ts +71 -45
  44. package/src/simulation/index.ts +2 -4
  45. package/src/vite/index.ts +4 -121
  46. package/src/vite/spinPlugin.ts +338 -0
  47. package/scripts/install-simulate.mjs +0 -101
  48. package/src/lua/ActionRouter.ts +0 -132
  49. package/src/lua/LuaEngine.ts +0 -520
  50. package/src/lua/LuaEngineAPI.ts +0 -314
  51. package/src/lua/PersistentState.ts +0 -80
  52. package/src/lua/SessionManager.ts +0 -249
  53. package/src/lua/SimulationRunner.ts +0 -190
  54. package/src/lua/fengari.d.ts +0 -10
  55. package/src/simulation/ParallelSimulationRunner.ts +0 -156
  56. package/src/simulation/SimulationWorker.ts +0 -44
@@ -1,190 +0,0 @@
1
- import { LuaEngine } from './LuaEngine';
2
- import type { SimulationConfig, SimulationResult, GameDefinition } from './types';
3
-
4
- /**
5
- * Runs N iterations of a Lua game script and collects RTP statistics.
6
- * Supports regular spins, buy bonus, and ante bet simulation.
7
- *
8
- * @example
9
- * ```ts
10
- * const runner = new SimulationRunner({
11
- * script: luaSource,
12
- * gameDefinition,
13
- * iterations: 1_000_000,
14
- * bet: 1.0,
15
- * seed: 42,
16
- * onProgress: (done, total) => console.log(`${done}/${total}`),
17
- * });
18
- *
19
- * const result = runner.run();
20
- * console.log(`RTP: ${result.totalRtp.toFixed(2)}%`);
21
- * ```
22
- */
23
- export class SimulationRunner {
24
- private config: SimulationConfig;
25
-
26
- constructor(config: SimulationConfig) {
27
- this.config = config;
28
- }
29
-
30
- run(): SimulationResult {
31
- const {
32
- script,
33
- gameDefinition,
34
- iterations,
35
- bet,
36
- seed,
37
- action: startAction = 'spin',
38
- params,
39
- progressInterval = 100_000,
40
- onProgress,
41
- } = this.config;
42
-
43
- const engine = new LuaEngine({
44
- script,
45
- gameDefinition,
46
- seed,
47
- logger: () => {},
48
- simulationMode: true,
49
- });
50
-
51
- const spinCost = this.calculateSpinCost(startAction, bet, gameDefinition, params);
52
-
53
- let totalWagered = 0;
54
- let totalWon = 0;
55
- let baseGameWin = 0;
56
- let bonusWin = 0;
57
- let hits = 0;
58
- let maxWinMultiplier = 0;
59
- let maxWinHits = 0;
60
- let bonusTriggered = 0;
61
- let bonusSpinsPlayed = 0;
62
-
63
- const startTime = Date.now();
64
-
65
- try {
66
- for (let i = 0; i < iterations; i++) {
67
- totalWagered += spinCost;
68
- let roundWin = 0;
69
- let roundBonusWin = 0;
70
-
71
- // Execute the starting action
72
- let result = engine.execute({
73
- action: startAction,
74
- bet,
75
- params,
76
- });
77
-
78
- const baseWin = result.totalWin;
79
-
80
- // If a session was created, play through it using nextActions from the engine
81
- if (result.session && !result.session.completed) {
82
- bonusTriggered++;
83
-
84
- let safetyLimit = 10_000;
85
- while (result.session && !result.session.completed && safetyLimit-- > 0) {
86
- const nextAction = result.nextActions[0];
87
- result = engine.execute({ action: nextAction, bet });
88
- bonusSpinsPlayed++;
89
- }
90
-
91
- // Session completion returns cumulative totalWin (includes trigger spin).
92
- // Use it as the full round win — don't add baseWin separately.
93
- roundWin = result.totalWin;
94
- roundBonusWin = roundWin - baseWin;
95
- } else {
96
- // No session — just base game win
97
- roundWin = baseWin;
98
- }
99
-
100
- baseGameWin += baseWin;
101
- bonusWin += roundBonusWin;
102
- totalWon += roundWin;
103
-
104
- if (roundWin > 0) hits++;
105
-
106
- const roundMultiplier = roundWin / bet;
107
- if (roundMultiplier > maxWinMultiplier) {
108
- maxWinMultiplier = roundMultiplier;
109
- }
110
-
111
- if (result.variables?.max_win_reached === 1) {
112
- maxWinHits++;
113
- }
114
-
115
- // Progress reporting
116
- if (onProgress && (i + 1) % progressInterval === 0) {
117
- onProgress(i + 1, iterations);
118
- }
119
- }
120
- } finally {
121
- engine.destroy();
122
- }
123
-
124
- const durationMs = Date.now() - startTime;
125
-
126
- return {
127
- gameId: gameDefinition.id,
128
- action: startAction,
129
- iterations,
130
- durationMs,
131
- totalRtp: totalWagered > 0 ? (totalWon / totalWagered) * 100 : 0,
132
- baseGameRtp: totalWagered > 0 ? (baseGameWin / totalWagered) * 100 : 0,
133
- bonusRtp: totalWagered > 0 ? (bonusWin / totalWagered) * 100 : 0,
134
- hitFrequency: iterations > 0 ? (hits / iterations) * 100 : 0,
135
- maxWin: Math.round(maxWinMultiplier * 100) / 100,
136
- maxWinHits,
137
- bonusTriggered,
138
- bonusSpinsPlayed,
139
- _raw: { totalWagered, totalWon, baseGameWin, bonusWin, hits },
140
- };
141
- }
142
-
143
- /**
144
- * Calculate the real cost of one spin — mirrors v5
145
- * ActionDefinition.DebitAmount: bet × (cost_multiplier || 1) when
146
- * debit==='bet', otherwise 0. Returns `bet` as a fallback for unknown
147
- * actions so RTP math still progresses (we count those as wagered).
148
- */
149
- private calculateSpinCost(
150
- action: string,
151
- bet: number,
152
- gameDefinition: GameDefinition,
153
- _params?: Record<string, unknown>,
154
- ): number {
155
- const actionDef = gameDefinition.actions[action];
156
- if (!actionDef) return bet;
157
- if (actionDef.debit !== 'bet') return 0;
158
- const mult = actionDef.cost_multiplier;
159
- if (typeof mult === 'number' && mult > 0 && mult !== 1) {
160
- return bet * mult;
161
- }
162
- return bet;
163
- }
164
- }
165
-
166
- /** Format a SimulationResult for console output */
167
- export function formatSimulationResult(result: SimulationResult): string {
168
- const lines: string[] = [
169
- '',
170
- '--- Simulation Results ---',
171
- `Game: ${result.gameId}`,
172
- `Action: ${result.action}`,
173
- `Iterations: ${result.iterations.toLocaleString()}`,
174
- `Duration: ${(result.durationMs / 1000).toFixed(1)}s`,
175
- `Total RTP: ${result.totalRtp.toFixed(2)}%`,
176
- `Base Game RTP: ${result.baseGameRtp.toFixed(2)}%`,
177
- `Bonus RTP: ${result.bonusRtp.toFixed(2)}%`,
178
- `Hit Frequency: ${result.hitFrequency.toFixed(2)}%`,
179
- `Max Win: ${result.maxWin.toFixed(2)}x`,
180
- `Max Win Hits: ${result.maxWinHits} (rounds capped by max_win)`,
181
- ];
182
-
183
- if (result.bonusTriggered > 0) {
184
- const frequency = Math.round(result.iterations / result.bonusTriggered);
185
- lines.push(`Bonus Triggered: ${result.bonusTriggered.toLocaleString()} (1 in ${frequency} spins)`);
186
- lines.push(`Bonus Spins Played: ${result.bonusSpinsPlayed.toLocaleString()}`);
187
- }
188
-
189
- return lines.join('\n');
190
- }
@@ -1,10 +0,0 @@
1
- declare module 'fengari' {
2
- const fengari: {
3
- lua: any;
4
- lauxlib: any;
5
- lualib: any;
6
- to_luastring: (str: string) => Uint8Array;
7
- to_jsstring: (str: Uint8Array) => string;
8
- };
9
- export default fengari;
10
- }
@@ -1,156 +0,0 @@
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 '../lua/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
- }
@@ -1,44 +0,0 @@
1
- /// <reference types="node" />
2
- import { parentPort, workerData } from 'worker_threads';
3
- import { SimulationRunner } from '../lua/SimulationRunner';
4
- import type { SimulationConfig, SimulationResult } from '../lua/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();