@energy8platform/platform-core 0.16.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.
- package/README.md +482 -0
- package/bin/simulate.ts +139 -0
- package/dist/dev-bridge.cjs.js +237 -0
- package/dist/dev-bridge.cjs.js.map +1 -0
- package/dist/dev-bridge.d.ts +141 -0
- package/dist/dev-bridge.esm.js +235 -0
- package/dist/dev-bridge.esm.js.map +1 -0
- package/dist/index.cjs.js +569 -0
- package/dist/index.cjs.js.map +1 -0
- package/dist/index.d.ts +439 -0
- package/dist/index.esm.js +560 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/loading.cjs.js +190 -0
- package/dist/loading.cjs.js.map +1 -0
- package/dist/loading.d.ts +86 -0
- package/dist/loading.esm.js +185 -0
- package/dist/loading.esm.js.map +1 -0
- package/dist/lua.cjs.js +1129 -0
- package/dist/lua.cjs.js.map +1 -0
- package/dist/lua.d.ts +319 -0
- package/dist/lua.esm.js +1119 -0
- package/dist/lua.esm.js.map +1 -0
- package/dist/simulation.cjs.js +374 -0
- package/dist/simulation.cjs.js.map +1 -0
- package/dist/simulation.d.ts +190 -0
- package/dist/simulation.esm.js +368 -0
- package/dist/simulation.esm.js.map +1 -0
- package/dist/vite.cjs.js +179 -0
- package/dist/vite.cjs.js.map +1 -0
- package/dist/vite.d.ts +13 -0
- package/dist/vite.esm.js +176 -0
- package/dist/vite.esm.js.map +1 -0
- package/package.json +100 -0
- package/scripts/install-simulate.mjs +101 -0
- package/src/EventEmitter.ts +55 -0
- package/src/PlatformSession.ts +156 -0
- package/src/dev-bridge/DevBridge.ts +305 -0
- package/src/dev-bridge/index.ts +2 -0
- package/src/index.ts +98 -0
- package/src/loading/CSSPreloader.ts +129 -0
- package/src/loading/index.ts +3 -0
- package/src/loading/logo.ts +95 -0
- package/src/lua/ActionRouter.ts +132 -0
- package/src/lua/LuaEngine.ts +412 -0
- package/src/lua/LuaEngineAPI.ts +314 -0
- package/src/lua/PersistentState.ts +80 -0
- package/src/lua/SessionManager.ts +227 -0
- package/src/lua/SimulationRunner.ts +192 -0
- package/src/lua/fengari.d.ts +10 -0
- package/src/lua/index.ts +28 -0
- package/src/lua/types.ts +149 -0
- package/src/simulation/NativeSimulationRunner.ts +367 -0
- package/src/simulation/ParallelSimulationRunner.ts +156 -0
- package/src/simulation/SimulationWorker.ts +44 -0
- package/src/simulation/index.ts +21 -0
- package/src/types.ts +85 -0
- package/src/vite/index.ts +196 -0
|
@@ -0,0 +1,192 @@
|
|
|
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
|
+
/** Calculate the real cost of one spin (accounting for buy bonus / ante bet) */
|
|
144
|
+
private calculateSpinCost(
|
|
145
|
+
action: string,
|
|
146
|
+
bet: number,
|
|
147
|
+
gameDefinition: GameDefinition,
|
|
148
|
+
params?: Record<string, unknown>,
|
|
149
|
+
): number {
|
|
150
|
+
// Check if this is a buy bonus action
|
|
151
|
+
const actionDef = gameDefinition.actions[action];
|
|
152
|
+
if (actionDef?.buy_bonus_mode && gameDefinition.buy_bonus) {
|
|
153
|
+
const mode = gameDefinition.buy_bonus.modes[actionDef.buy_bonus_mode];
|
|
154
|
+
if (mode) {
|
|
155
|
+
return bet * mode.cost_multiplier;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Check ante bet
|
|
160
|
+
if (params?.ante_bet && gameDefinition.ante_bet) {
|
|
161
|
+
return bet * gameDefinition.ante_bet.cost_multiplier;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return bet;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Format a SimulationResult for console output */
|
|
169
|
+
export function formatSimulationResult(result: SimulationResult): string {
|
|
170
|
+
const lines: string[] = [
|
|
171
|
+
'',
|
|
172
|
+
'--- Simulation Results ---',
|
|
173
|
+
`Game: ${result.gameId}`,
|
|
174
|
+
`Action: ${result.action}`,
|
|
175
|
+
`Iterations: ${result.iterations.toLocaleString()}`,
|
|
176
|
+
`Duration: ${(result.durationMs / 1000).toFixed(1)}s`,
|
|
177
|
+
`Total RTP: ${result.totalRtp.toFixed(2)}%`,
|
|
178
|
+
`Base Game RTP: ${result.baseGameRtp.toFixed(2)}%`,
|
|
179
|
+
`Bonus RTP: ${result.bonusRtp.toFixed(2)}%`,
|
|
180
|
+
`Hit Frequency: ${result.hitFrequency.toFixed(2)}%`,
|
|
181
|
+
`Max Win: ${result.maxWin.toFixed(2)}x`,
|
|
182
|
+
`Max Win Hits: ${result.maxWinHits} (rounds capped by max_win)`,
|
|
183
|
+
];
|
|
184
|
+
|
|
185
|
+
if (result.bonusTriggered > 0) {
|
|
186
|
+
const frequency = Math.round(result.iterations / result.bonusTriggered);
|
|
187
|
+
lines.push(`Bonus Triggered: ${result.bonusTriggered.toLocaleString()} (1 in ${frequency} spins)`);
|
|
188
|
+
lines.push(`Bonus Spins Played: ${result.bonusSpinsPlayed.toLocaleString()}`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return lines.join('\n');
|
|
192
|
+
}
|
package/src/lua/index.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Browser-safe Lua engine surface. Only depends on `fengari`, no Node built-ins.
|
|
2
|
+
//
|
|
3
|
+
// For the Node-only runners (NativeSimulationRunner backed by a Go binary,
|
|
4
|
+
// ParallelSimulationRunner backed by worker_threads), import from
|
|
5
|
+
// `@energy8platform/platform-core/simulation` instead.
|
|
6
|
+
export { LuaEngine } from './LuaEngine';
|
|
7
|
+
export { LuaEngineAPI, createSeededRng } from './LuaEngineAPI';
|
|
8
|
+
export { ActionRouter, evaluateCondition } from './ActionRouter';
|
|
9
|
+
export { SessionManager } from './SessionManager';
|
|
10
|
+
export { PersistentState } from './PersistentState';
|
|
11
|
+
export { SimulationRunner, formatSimulationResult } from './SimulationRunner';
|
|
12
|
+
export type {
|
|
13
|
+
GameDefinition,
|
|
14
|
+
ActionDefinition,
|
|
15
|
+
TransitionRule,
|
|
16
|
+
SessionConfig,
|
|
17
|
+
LuaEngineConfig,
|
|
18
|
+
LuaPlayResult,
|
|
19
|
+
MaxWinConfig,
|
|
20
|
+
BuyBonusConfig,
|
|
21
|
+
BuyBonusMode,
|
|
22
|
+
AnteBetConfig,
|
|
23
|
+
PersistentStateConfig,
|
|
24
|
+
BetLevelsConfig,
|
|
25
|
+
SimulationConfig,
|
|
26
|
+
SimulationResult,
|
|
27
|
+
SimulationRawAccumulators,
|
|
28
|
+
} from './types';
|
package/src/lua/types.ts
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import type { SessionData, PlayParams } from '@energy8platform/game-sdk';
|
|
2
|
+
|
|
3
|
+
// ─── Game Definition (Platform JSON Config) ─────────────
|
|
4
|
+
|
|
5
|
+
export interface GameDefinition {
|
|
6
|
+
id: string;
|
|
7
|
+
type: 'SLOT' | 'TABLE';
|
|
8
|
+
actions: Record<string, ActionDefinition>;
|
|
9
|
+
bet_levels?: number[] | BetLevelsConfig;
|
|
10
|
+
max_win?: MaxWinConfig;
|
|
11
|
+
buy_bonus?: BuyBonusConfig;
|
|
12
|
+
ante_bet?: AnteBetConfig;
|
|
13
|
+
persistent_state?: PersistentStateConfig;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface BetLevelsConfig {
|
|
17
|
+
levels?: number[];
|
|
18
|
+
min?: number;
|
|
19
|
+
max?: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface MaxWinConfig {
|
|
23
|
+
multiplier?: number;
|
|
24
|
+
fixed?: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface BuyBonusConfig {
|
|
28
|
+
modes: Record<string, BuyBonusMode>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface BuyBonusMode {
|
|
32
|
+
cost_multiplier: number;
|
|
33
|
+
/** Distribution of forced scatter counts. Optional — if omitted, Lua script handles bonus setup itself. */
|
|
34
|
+
scatter_distribution?: Record<string, number>;
|
|
35
|
+
/** Optional description */
|
|
36
|
+
description?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface AnteBetConfig {
|
|
40
|
+
cost_multiplier: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface PersistentStateConfig {
|
|
44
|
+
vars: string[];
|
|
45
|
+
exposed_vars: string[];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ─── Actions & Transitions ──────────────────────────────
|
|
49
|
+
|
|
50
|
+
export interface ActionDefinition {
|
|
51
|
+
stage: string;
|
|
52
|
+
debit: 'bet' | 'buy_bonus_cost' | 'ante_bet_cost' | 'none';
|
|
53
|
+
credit?: 'win' | 'none' | 'defer';
|
|
54
|
+
requires_session?: boolean;
|
|
55
|
+
buy_bonus_mode?: string;
|
|
56
|
+
transitions: TransitionRule[];
|
|
57
|
+
input_schema?: Record<string, unknown>;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface TransitionRule {
|
|
61
|
+
condition: string;
|
|
62
|
+
creates_session?: boolean;
|
|
63
|
+
complete_session?: boolean;
|
|
64
|
+
credit_override?: 'defer';
|
|
65
|
+
next_actions: string[];
|
|
66
|
+
session_config?: SessionConfig;
|
|
67
|
+
add_spins_var?: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface SessionConfig {
|
|
71
|
+
total_spins_var: string;
|
|
72
|
+
persistent_vars?: string[];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ─── Lua Engine Config & Results ────────────────────────
|
|
76
|
+
|
|
77
|
+
export interface LuaEngineConfig {
|
|
78
|
+
/** Lua script source code */
|
|
79
|
+
script: string;
|
|
80
|
+
/** Platform game definition (actions, transitions, bet levels, etc.) */
|
|
81
|
+
gameDefinition: GameDefinition;
|
|
82
|
+
/** Seed for deterministic RNG (for simulation/replay) */
|
|
83
|
+
seed?: number;
|
|
84
|
+
/** Custom logger function */
|
|
85
|
+
logger?: (level: string, msg: string) => void;
|
|
86
|
+
/** Skip marshalling data fields (matrix, wins, etc.) for faster simulation */
|
|
87
|
+
simulationMode?: boolean;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface LuaPlayResult {
|
|
91
|
+
totalWin: number;
|
|
92
|
+
data: Record<string, unknown>;
|
|
93
|
+
nextActions: string[];
|
|
94
|
+
session: SessionData | null;
|
|
95
|
+
variables: Record<string, number>;
|
|
96
|
+
creditDeferred: boolean;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ─── Simulation ─────────────────────────────────────────
|
|
100
|
+
|
|
101
|
+
export interface SimulationConfig {
|
|
102
|
+
/** Lua script source code */
|
|
103
|
+
script: string;
|
|
104
|
+
/** Platform game definition */
|
|
105
|
+
gameDefinition: GameDefinition;
|
|
106
|
+
/** Number of iterations to run */
|
|
107
|
+
iterations: number;
|
|
108
|
+
/** Bet amount per spin */
|
|
109
|
+
bet: number;
|
|
110
|
+
/** RNG seed for deterministic results */
|
|
111
|
+
seed?: number;
|
|
112
|
+
/** Which action to simulate (default: 'spin') */
|
|
113
|
+
action?: string;
|
|
114
|
+
/** Params for the action (buy_bonus mode, ante_bet, etc.) */
|
|
115
|
+
params?: Record<string, unknown>;
|
|
116
|
+
/** Report progress every N iterations (default: 100_000) */
|
|
117
|
+
progressInterval?: number;
|
|
118
|
+
/** Progress callback */
|
|
119
|
+
onProgress?: (completed: number, total: number) => void;
|
|
120
|
+
/** Number of worker threads for parallel simulation (default: os.cpus().length) */
|
|
121
|
+
workerCount?: number;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export interface SimulationResult {
|
|
125
|
+
gameId: string;
|
|
126
|
+
action: string;
|
|
127
|
+
iterations: number;
|
|
128
|
+
durationMs: number;
|
|
129
|
+
totalRtp: number;
|
|
130
|
+
baseGameRtp: number;
|
|
131
|
+
bonusRtp: number;
|
|
132
|
+
hitFrequency: number;
|
|
133
|
+
maxWin: number;
|
|
134
|
+
maxWinHits: number;
|
|
135
|
+
bonusTriggered: number;
|
|
136
|
+
bonusSpinsPlayed: number;
|
|
137
|
+
/** Raw accumulators for aggregation across workers */
|
|
138
|
+
_raw?: SimulationRawAccumulators;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export interface SimulationRawAccumulators {
|
|
142
|
+
totalWagered: number;
|
|
143
|
+
totalWon: number;
|
|
144
|
+
baseGameWin: number;
|
|
145
|
+
bonusWin: number;
|
|
146
|
+
hits: number;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export type { SessionData, PlayParams };
|