@energy8platform/platform-core 0.28.2 → 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.
- package/README.md +127 -150
- package/bin/simulate.ts +35 -98
- package/dist/dev-bridge.cjs.js +3 -3
- package/dist/dev-bridge.cjs.js.map +1 -1
- package/dist/dev-bridge.d.ts +9 -2
- package/dist/dev-bridge.esm.js +3 -3
- package/dist/dev-bridge.esm.js.map +1 -1
- package/dist/game-spec.cjs.js +70 -27
- package/dist/game-spec.cjs.js.map +1 -1
- package/dist/game-spec.d.ts +47 -11
- package/dist/game-spec.esm.js +68 -25
- package/dist/game-spec.esm.js.map +1 -1
- package/dist/index.cjs.js +3 -3
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +28 -2
- package/dist/index.esm.js +3 -3
- package/dist/index.esm.js.map +1 -1
- package/dist/lua.cjs.js +0 -1234
- package/dist/lua.cjs.js.map +1 -1
- package/dist/lua.d.ts +8 -206
- package/dist/lua.esm.js +0 -1225
- package/dist/lua.esm.js.map +1 -1
- package/dist/simulation.cjs.js +48 -179
- package/dist/simulation.cjs.js.map +1 -1
- package/dist/simulation.d.ts +33 -60
- package/dist/simulation.esm.js +48 -178
- package/dist/simulation.esm.js.map +1 -1
- package/dist/vite.cjs.js +323 -109
- package/dist/vite.cjs.js.map +1 -1
- package/dist/vite.d.ts +19 -9
- package/dist/vite.esm.js +322 -109
- package/dist/vite.esm.js.map +1 -1
- package/package.json +6 -5
- package/scripts/install-e8.mjs +113 -0
- package/src/dev-bridge/DevBridge.ts +3 -3
- package/src/game-spec/defineGame.ts +2 -2
- package/src/game-spec/derive.ts +46 -17
- package/src/game-spec/export.ts +28 -8
- package/src/game-spec/index.ts +3 -2
- package/src/game-spec/types.ts +11 -1
- package/src/index.ts +6 -12
- package/src/lua/index.ts +4 -11
- package/src/lua/types.ts +7 -0
- package/src/simulation/NativeSimulationRunner.ts +71 -45
- package/src/simulation/index.ts +2 -4
- package/src/vite/index.ts +4 -121
- package/src/vite/spinPlugin.ts +338 -0
- package/scripts/install-simulate.mjs +0 -101
- package/src/lua/ActionRouter.ts +0 -132
- package/src/lua/LuaEngine.ts +0 -520
- package/src/lua/LuaEngineAPI.ts +0 -314
- package/src/lua/PersistentState.ts +0 -80
- package/src/lua/SessionManager.ts +0 -249
- package/src/lua/SimulationRunner.ts +0 -190
- package/src/lua/fengari.d.ts +0 -10
- package/src/simulation/ParallelSimulationRunner.ts +0 -156
- package/src/simulation/SimulationWorker.ts +0 -44
|
@@ -173,7 +173,7 @@ const DEFAULT_CONFIG: Omit<Required<DevBridgeConfig>, 'luaScript' | 'gameDefinit
|
|
|
173
173
|
* the need for postMessage and iframes.
|
|
174
174
|
*
|
|
175
175
|
* When `luaScript` is set, play requests are sent to the Vite dev server
|
|
176
|
-
* which runs LuaEngine in Node.js — no
|
|
176
|
+
* which runs LuaEngine in Node.js — no math in the browser.
|
|
177
177
|
*
|
|
178
178
|
* @example
|
|
179
179
|
* ```ts
|
|
@@ -466,7 +466,7 @@ export class DevBridge {
|
|
|
466
466
|
? this._activeRoundId!
|
|
467
467
|
: generateRoundId();
|
|
468
468
|
|
|
469
|
-
this.
|
|
469
|
+
this.executeOnServer({ action, bet, roundId: serverRoundId, params })
|
|
470
470
|
.then((result) => {
|
|
471
471
|
this._lastPlayResult = result;
|
|
472
472
|
this.updateSessionState(result);
|
|
@@ -561,7 +561,7 @@ export class DevBridge {
|
|
|
561
561
|
return bet;
|
|
562
562
|
}
|
|
563
563
|
|
|
564
|
-
private async
|
|
564
|
+
private async executeOnServer(params: PlayParams): Promise<PlayResultData> {
|
|
565
565
|
const response = await fetch('/__lua-play', {
|
|
566
566
|
method: 'POST',
|
|
567
567
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import type { GameSpec, GameModel } from './types';
|
|
2
2
|
import { validateSpec } from './validate';
|
|
3
|
-
import { toGameDefinition,
|
|
3
|
+
import { toGameDefinition, toSpinPrelude, toModeMap, toMathModes, toPaytableView } from './derive';
|
|
4
4
|
|
|
5
5
|
export function defineGame(spec: GameSpec): GameModel {
|
|
6
6
|
validateSpec(spec);
|
|
7
7
|
return {
|
|
8
8
|
spec,
|
|
9
9
|
gameDefinition: toGameDefinition(spec),
|
|
10
|
-
|
|
10
|
+
spinPrelude: toSpinPrelude(spec),
|
|
11
11
|
modeMap: toModeMap(spec),
|
|
12
12
|
mathModes: toMathModes(spec),
|
|
13
13
|
paytable: toPaytableView(spec),
|
package/src/game-spec/derive.ts
CHANGED
|
@@ -61,39 +61,68 @@ export function toGameDefinition(spec: GameSpec): GameDefinition {
|
|
|
61
61
|
for (const [key, action] of Object.entries(spec.actions)) {
|
|
62
62
|
actions[key] = toActionDefinition(key, action, freeKey);
|
|
63
63
|
}
|
|
64
|
-
|
|
64
|
+
const def: GameDefinition = {
|
|
65
65
|
id: spec.id,
|
|
66
66
|
type: 'SLOT',
|
|
67
|
+
// Bare filename — the platform resolves it to games/{id}/script.lua. Required so the exported
|
|
68
|
+
// config.json points at the uploaded script.
|
|
69
|
+
script_path: spec.scriptPath ?? 'script.lua',
|
|
67
70
|
actions,
|
|
68
71
|
bet_levels: [...spec.betLevels],
|
|
69
72
|
max_win: { multiplier: spec.maxWin },
|
|
70
73
|
};
|
|
74
|
+
if (spec.sessionTtl) def.session_ttl = spec.sessionTtl;
|
|
75
|
+
if (spec.persistentState) def.persistent_state = { ...spec.persistentState };
|
|
76
|
+
return def;
|
|
71
77
|
}
|
|
72
78
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
return
|
|
79
|
+
/** Число с гарантированной float-формой для SpinML (10 → "10.0"). */
|
|
80
|
+
function spinF(n: number): string {
|
|
81
|
+
return Number.isInteger(n) ? `${n}.0` : String(n);
|
|
76
82
|
}
|
|
77
83
|
|
|
78
|
-
|
|
84
|
+
/**
|
|
85
|
+
* The generated .spin prelude: SPEC/SYM as const groups,
|
|
86
|
+
* SYMBOLS as a str array, the paytable as indexable threshold/payout arrays
|
|
87
|
+
* (PAY_COUNTS + PAY_<ID>, 0.0 = no pay at that threshold), VALUES as
|
|
88
|
+
* VAL_<ID>. Prepended to the author's script.spin by buildSpinScript.
|
|
89
|
+
*/
|
|
90
|
+
export function toSpinPrelude(spec: GameSpec): string {
|
|
79
91
|
const lines: string[] = ['-- AUTO-GENERATED from game.spec.ts — do not edit'];
|
|
80
|
-
lines.push(
|
|
92
|
+
lines.push(
|
|
93
|
+
`const SPEC = { cols: ${spec.grid.cols}, rows: ${spec.grid.rows}, cells: ${spec.grid.cols * spec.grid.rows}, max_win: ${spinF(spec.maxWin)} }`,
|
|
94
|
+
);
|
|
81
95
|
|
|
82
96
|
const symNames = spec.symbols.map((s) => `"${s.id}"`).join(', ');
|
|
83
|
-
lines.push(`SYMBOLS
|
|
97
|
+
lines.push(`const SYMBOLS: [str; ${spec.symbols.length}] = [${symNames}]`);
|
|
98
|
+
lines.push(`const N_SYMBOLS: int = ${spec.symbols.length}`);
|
|
84
99
|
|
|
85
|
-
const symIndex = spec.symbols.map((s, i) => `${s.id}
|
|
86
|
-
lines.push(
|
|
100
|
+
const symIndex = spec.symbols.map((s, i) => `${s.id}: ${i + 1}`).join(', ');
|
|
101
|
+
lines.push(`-- 1-based индексы символов (порядок SYMBOLS)`);
|
|
102
|
+
lines.push(`const SYM = { ${symIndex} }`);
|
|
87
103
|
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
104
|
+
const paying = spec.symbols.filter((s) => s.pay);
|
|
105
|
+
if (paying.length) {
|
|
106
|
+
const counts = Array.from(
|
|
107
|
+
new Set(paying.flatMap((s) => Object.keys(s.pay as Record<number, number>).map(Number))),
|
|
108
|
+
).sort((a, b) => a - b);
|
|
109
|
+
lines.push(`-- пейтейбл: пороги count + выплата на порог (0.0 = нет выплаты)`);
|
|
110
|
+
lines.push(`const PAY_COUNTS: [int; ${counts.length}] = [${counts.join(', ')}]`);
|
|
111
|
+
for (const s of paying) {
|
|
112
|
+
const pay = s.pay as Record<number, number>;
|
|
113
|
+
const row = counts.map((c) => spinF(pay[c] ?? 0));
|
|
114
|
+
lines.push(`const PAY_${s.id}: [float; ${counts.length}] = [${row.join(', ')}]`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
92
117
|
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
118
|
+
for (const s of spec.symbols) {
|
|
119
|
+
if (s.value === undefined) continue;
|
|
120
|
+
if (Array.isArray(s.value)) {
|
|
121
|
+
lines.push(`const VAL_${s.id}: [int; ${s.value.length}] = [${s.value.join(', ')}]`);
|
|
122
|
+
} else {
|
|
123
|
+
lines.push(`const VAL_${s.id}: int = ${s.value}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
97
126
|
|
|
98
127
|
return lines.join('\n') + '\n';
|
|
99
128
|
}
|
package/src/game-spec/export.ts
CHANGED
|
@@ -1,17 +1,37 @@
|
|
|
1
1
|
import type { GameSpec, GameModel } from './types';
|
|
2
2
|
import { defineGame } from './defineGame';
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
/** Compose the self-contained .spin: generated const prelude ⧺ author math. */
|
|
5
|
+
export function buildSpinScript(model: GameModel, logicSpin: string): string {
|
|
6
|
+
return model.spinPrelude + '\n' + logicSpin;
|
|
6
7
|
}
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
/** The spin-runtime platform deliverables, keyed by on-disk filenames. */
|
|
10
|
+
export interface E8SpinBundle {
|
|
11
|
+
/** GameDefinition JSON (engine_mode=spin) — S3: games/{id}/config.json. */
|
|
12
|
+
'config.json': string;
|
|
13
|
+
/** Self-contained SpinML (prelude ⧺ math) — S3: games/{id}/script.spin. */
|
|
14
|
+
'script.spin': string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Produce the spin-runtime deliverables from one spec + the author's
|
|
19
|
+
* `script.spin`. The platform routes engine_mode="spin" games to the e8
|
|
20
|
+
* engine; script_path points at the uploaded .spin.
|
|
21
|
+
*/
|
|
22
|
+
export function exportGameSpin(spec: GameSpec, opts: { logicSpin: string }): E8SpinBundle {
|
|
12
23
|
const model = defineGame(spec);
|
|
24
|
+
const config = {
|
|
25
|
+
...(model.gameDefinition as unknown as Record<string, unknown>),
|
|
26
|
+
engine_mode: 'spin',
|
|
27
|
+
script_path: 'script.spin',
|
|
28
|
+
};
|
|
29
|
+
const script = buildSpinScript(model, opts.logicSpin);
|
|
30
|
+
if (!/fn\s+execute\s*\(/.test(script)) {
|
|
31
|
+
throw new Error('E8 spin export: script.spin does not define `fn execute(...)`');
|
|
32
|
+
}
|
|
13
33
|
return {
|
|
14
|
-
'
|
|
15
|
-
'script.
|
|
34
|
+
'config.json': JSON.stringify(config, null, 2),
|
|
35
|
+
'script.spin': script,
|
|
16
36
|
};
|
|
17
37
|
}
|
package/src/game-spec/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export * from './types';
|
|
2
2
|
export { validateSpec, GameSpecError } from './validate';
|
|
3
|
-
export { toGameDefinition,
|
|
3
|
+
export { toGameDefinition, toSpinPrelude, toModeMap, toMathModes, toPaytableView } from './derive';
|
|
4
4
|
export { defineGame } from './defineGame';
|
|
5
|
-
export {
|
|
5
|
+
export { buildSpinScript, exportGameSpin } from './export';
|
|
6
|
+
export type { E8SpinBundle } from './export';
|
|
6
7
|
export type { GameDefinition, ActionDefinition, TransitionRule, MaxWinConfig } from '../lua/types';
|
package/src/game-spec/types.ts
CHANGED
|
@@ -48,6 +48,15 @@ export interface GameSpec {
|
|
|
48
48
|
actions: Record<string, ActionSpec>;
|
|
49
49
|
/** Open hint for codegen/UI: 'cascade' | 'cluster' | 'ways' | 'lines' | … */
|
|
50
50
|
mechanic?: string;
|
|
51
|
+
/**
|
|
52
|
+
* Lua script key for the exported E8 `config.json`. Default `"script.lua"`, which the platform
|
|
53
|
+
* resolves to `games/{id}/script.lua`. Override only when the script lives at a custom S3 key.
|
|
54
|
+
*/
|
|
55
|
+
scriptPath?: string;
|
|
56
|
+
/** Session expiry for the exported config (Go-style duration, e.g. "24h"). Omitted when unset. */
|
|
57
|
+
sessionTtl?: string;
|
|
58
|
+
/** Cross-spin persistent state (charge meters etc.) surfaced into the exported config. */
|
|
59
|
+
persistentState?: { vars: string[]; exposed_vars: string[] };
|
|
51
60
|
/** Game-level escape hatch. */
|
|
52
61
|
meta?: Record<string, unknown>;
|
|
53
62
|
}
|
|
@@ -76,7 +85,8 @@ export interface PaytableView {
|
|
|
76
85
|
export interface GameModel {
|
|
77
86
|
spec: GameSpec;
|
|
78
87
|
gameDefinition: GameDefinition;
|
|
79
|
-
|
|
88
|
+
/** Generated .spin prelude (SPEC/SYM/PAYTABLE consts) for the spin runtime. */
|
|
89
|
+
spinPrelude: string;
|
|
80
90
|
modeMap: Record<string, string>;
|
|
81
91
|
mathModes: MathModeSpec[];
|
|
82
92
|
paytable: PaytableView;
|
package/src/index.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* engine.
|
|
6
6
|
*
|
|
7
7
|
* Sub-paths for fine-grained imports:
|
|
8
|
-
* - `@energy8platform/platform-core/lua` —
|
|
8
|
+
* - `@energy8platform/platform-core/lua` — shared game-definition types
|
|
9
9
|
* - `@energy8platform/platform-core/dev-bridge` — DevBridge mock host
|
|
10
10
|
* - `@energy8platform/platform-core/vite` — Vite plugins
|
|
11
11
|
*/
|
|
@@ -21,17 +21,11 @@ export type {
|
|
|
21
21
|
SDKOptions,
|
|
22
22
|
} from './PlatformSession';
|
|
23
23
|
|
|
24
|
-
// ───
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
// consumer that doesn't actually use Lua in the browser. Use:
|
|
30
|
-
//
|
|
31
|
-
// import { LuaEngine } from '@energy8platform/platform-core/lua';
|
|
32
|
-
//
|
|
33
|
-
// For Node-only RTP simulation (Go binary, worker_threads), import from
|
|
34
|
-
// '@energy8platform/platform-core/simulation' instead.
|
|
24
|
+
// ─── Math runtime ───────────────────────────────────────
|
|
25
|
+
// The math runtime is SpinML (the Rust e8 engine): dev rounds via the
|
|
26
|
+
// `spinPlugin` vite plugin + e8-server, simulation via `e8 simulate`
|
|
27
|
+
// (see '@energy8platform/platform-core/simulation'). The fengari Lua
|
|
28
|
+
// engine was removed — legacy Lua games stay on <= 0.28.x.
|
|
35
29
|
|
|
36
30
|
// ─── DevBridge ──────────────────────────────────────────
|
|
37
31
|
export { DevBridge } from './dev-bridge';
|
package/src/lua/index.ts
CHANGED
|
@@ -1,14 +1,7 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
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';
|
|
1
|
+
// Shared game-definition types. The fengari Lua engine that used to live
|
|
2
|
+
// here is GONE — the math runtime is SpinML (e8): dev rounds via spinPlugin
|
|
3
|
+
// + e8-server, simulation via `e8 simulate` (stake-math-tools). Legacy Lua
|
|
4
|
+
// games stay on platform-core <= 0.28.x.
|
|
12
5
|
export type {
|
|
13
6
|
GameDefinition,
|
|
14
7
|
ActionDefinition,
|
package/src/lua/types.ts
CHANGED
|
@@ -5,6 +5,13 @@ import type { SessionData, PlayParams } from '@energy8platform/game-sdk';
|
|
|
5
5
|
export interface GameDefinition {
|
|
6
6
|
id: string;
|
|
7
7
|
type: 'SLOT' | 'TABLE';
|
|
8
|
+
/**
|
|
9
|
+
* Lua script S3 object key or bare filename (e.g. "script.lua" — the platform resolves a bare
|
|
10
|
+
* name to `games/{id}/script.lua`). Mirrors the server's `GameDefinition.ScriptPath` (omitempty).
|
|
11
|
+
* The engine itself loads the script from `LuaEngineConfig.script`, so this is only carried for
|
|
12
|
+
* the exported platform `config.json`.
|
|
13
|
+
*/
|
|
14
|
+
script_path?: string;
|
|
8
15
|
actions: Record<string, ActionDefinition>;
|
|
9
16
|
bet_levels?: number[] | BetLevelsConfig;
|
|
10
17
|
max_win?: MaxWinConfig;
|
|
@@ -28,6 +28,17 @@ export interface NativeSimulationConfig {
|
|
|
28
28
|
binaryPath: string;
|
|
29
29
|
/** Lua script source code */
|
|
30
30
|
script: string;
|
|
31
|
+
/**
|
|
32
|
+
* Extension of the temp script file (default 'lua'). The e8 SpinML engine
|
|
33
|
+
* takes 'spin' — the runner is otherwise runtime-agnostic: it writes the
|
|
34
|
+
* script, points config.script_path at it and spawns the binary.
|
|
35
|
+
*/
|
|
36
|
+
scriptExt?: 'lua' | 'spin';
|
|
37
|
+
/**
|
|
38
|
+
* Args prepended before the flag args (default none). The e8 binary is a
|
|
39
|
+
* multi-command CLI — its Go-compatible mode is the `simulate` subcommand.
|
|
40
|
+
*/
|
|
41
|
+
argsPrefix?: string[];
|
|
31
42
|
/** Platform game definition */
|
|
32
43
|
gameDefinition: GameDefinition;
|
|
33
44
|
/** Number of iterations */
|
|
@@ -100,6 +111,14 @@ export interface NativeSimulationResult extends SimulationResult {
|
|
|
100
111
|
workerSeeds?: string[];
|
|
101
112
|
/** Echo of replay params when the run was in replay mode. */
|
|
102
113
|
replay?: NativeReplayParams;
|
|
114
|
+
/** Round-win standard deviation (currency units). */
|
|
115
|
+
stddev?: number;
|
|
116
|
+
/** Coefficient of variation (stddev / mean round win). */
|
|
117
|
+
cv?: number;
|
|
118
|
+
/** Volatility score 0..10 (casino_platform classifyVolatility buckets). */
|
|
119
|
+
volatility?: number;
|
|
120
|
+
/** Volatility tier label (Low … Extreme). */
|
|
121
|
+
volatilityLabel?: string;
|
|
103
122
|
}
|
|
104
123
|
|
|
105
124
|
// ─── Go JSON output shape (snake_case) ──────────────────
|
|
@@ -136,6 +155,10 @@ interface GoSimulationOutput {
|
|
|
136
155
|
rng_kind?: NativeRNGKind;
|
|
137
156
|
master_seed?: string;
|
|
138
157
|
worker_seeds?: string[];
|
|
158
|
+
stddev?: number;
|
|
159
|
+
cv?: number;
|
|
160
|
+
volatility?: number;
|
|
161
|
+
volatility_label?: string;
|
|
139
162
|
replay?: {
|
|
140
163
|
server_seed: string;
|
|
141
164
|
client_seed: string;
|
|
@@ -190,7 +213,7 @@ export class NativeSimulationRunner {
|
|
|
190
213
|
}
|
|
191
214
|
|
|
192
215
|
async run(): Promise<NativeSimulationResult> {
|
|
193
|
-
const { binaryPath, script, gameDefinition, iterations, bet, action, params, seed, rng, replay, dump } = this.config;
|
|
216
|
+
const { binaryPath, script, scriptExt, argsPrefix, gameDefinition, iterations, bet, action, params, seed, rng, replay, dump } = this.config;
|
|
194
217
|
|
|
195
218
|
if (replay && rng && rng !== 'provably-fair') {
|
|
196
219
|
throw new Error(`Replay mode requires rng="provably-fair" (got rng="${rng}")`);
|
|
@@ -198,28 +221,28 @@ export class NativeSimulationRunner {
|
|
|
198
221
|
|
|
199
222
|
const id = randomBytes(8).toString('hex');
|
|
200
223
|
const tmpDir = tmpdir();
|
|
201
|
-
const
|
|
224
|
+
const scriptPath = join(tmpDir, `sim-${id}.${scriptExt ?? 'lua'}`);
|
|
202
225
|
const configPath = join(tmpDir, `sim-${id}.json`);
|
|
203
226
|
|
|
204
227
|
try {
|
|
205
228
|
// Write temp files
|
|
206
229
|
await Promise.all([
|
|
207
|
-
writeFile(
|
|
208
|
-
writeFile(configPath, JSON.stringify({ ...gameDefinition, script_path:
|
|
230
|
+
writeFile(scriptPath, script, 'utf-8'),
|
|
231
|
+
writeFile(configPath, JSON.stringify({ ...gameDefinition, script_path: scriptPath }), 'utf-8'),
|
|
209
232
|
]);
|
|
210
233
|
|
|
211
234
|
// Build CLI args
|
|
212
235
|
const args = buildNativeArgs({ configPath, iterations, bet, action, params, rng, seed, dump, replay });
|
|
213
236
|
|
|
214
237
|
// Execute binary
|
|
215
|
-
const output = await this.exec(binaryPath, args);
|
|
238
|
+
const output = await this.exec(binaryPath, [...(argsPrefix ?? []), ...args]);
|
|
216
239
|
|
|
217
240
|
// Parse JSON output
|
|
218
241
|
const json: GoSimulationOutput = JSON.parse(output);
|
|
219
242
|
return mapGoResult(json);
|
|
220
243
|
} finally {
|
|
221
244
|
// Cleanup temp files
|
|
222
|
-
await Promise.allSettled([unlink(
|
|
245
|
+
await Promise.allSettled([unlink(scriptPath), unlink(configPath)]);
|
|
223
246
|
}
|
|
224
247
|
}
|
|
225
248
|
|
|
@@ -298,6 +321,10 @@ function mapGoResult(json: GoSimulationOutput): NativeSimulationResult {
|
|
|
298
321
|
rngKind: json.rng_kind,
|
|
299
322
|
masterSeed: json.master_seed,
|
|
300
323
|
workerSeeds: json.worker_seeds,
|
|
324
|
+
stddev: json.stddev,
|
|
325
|
+
cv: json.cv,
|
|
326
|
+
volatility: json.volatility,
|
|
327
|
+
volatilityLabel: json.volatility_label,
|
|
301
328
|
replay: json.replay
|
|
302
329
|
? {
|
|
303
330
|
serverSeed: json.replay.server_seed,
|
|
@@ -318,42 +345,40 @@ function mapGoResult(json: GoSimulationOutput): NativeSimulationResult {
|
|
|
318
345
|
// ─── Binary discovery ───────────────────────────────────
|
|
319
346
|
|
|
320
347
|
/**
|
|
321
|
-
* Search for
|
|
322
|
-
*
|
|
348
|
+
* Search for the e8 SpinML engine binary (Rust) — same lookup discipline as
|
|
349
|
+
* findNativeBinary: env override → <pkg>/bin/e8-<platform>-<arch> → $PATH.
|
|
350
|
+
* Fetched by scripts/install-e8.mjs on postinstall.
|
|
323
351
|
*/
|
|
324
|
-
export function
|
|
325
|
-
|
|
326
|
-
const envPath = process.env.SIMULATE_BINARY;
|
|
352
|
+
export function findE8Binary(baseDir?: string): string | null {
|
|
353
|
+
const envPath = process.env.E8_BINARY;
|
|
327
354
|
if (envPath && isExecutable(envPath)) {
|
|
328
355
|
return envPath;
|
|
329
356
|
}
|
|
330
357
|
|
|
331
|
-
const platform = process.platform;
|
|
332
|
-
const nodeArch = process.arch;
|
|
358
|
+
const platform = process.platform;
|
|
359
|
+
const nodeArch = process.arch;
|
|
333
360
|
const goArch = nodeArch === 'x64' ? 'amd64' : nodeArch;
|
|
334
361
|
const goPlatform = platform === 'win32' ? 'windows' : platform;
|
|
335
362
|
const ext = platform === 'win32' ? '.exe' : '';
|
|
336
363
|
|
|
337
|
-
const names = [
|
|
338
|
-
`simulate-${goPlatform}-${goArch}${ext}`,
|
|
339
|
-
`simulation-${goPlatform}-${goArch}${ext}`,
|
|
340
|
-
`simulate${ext}`,
|
|
341
|
-
`simulation${ext}`,
|
|
342
|
-
];
|
|
364
|
+
const names = [`e8-${goPlatform}-${goArch}${ext}`, `e8${ext}`];
|
|
343
365
|
|
|
344
|
-
// Search directories: user's project first, then this package's bin/
|
|
345
366
|
const searchDirs: string[] = [];
|
|
346
367
|
if (baseDir) searchDirs.push(baseDir);
|
|
347
|
-
|
|
348
|
-
//
|
|
368
|
+
// Раскладки различаются: dist/simulation.esm.js → ../bin,
|
|
369
|
+
// src/simulation/*.ts (vitest/tsx) → ../../bin. Пробуем оба уровня.
|
|
349
370
|
try {
|
|
350
|
-
const
|
|
351
|
-
|
|
371
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
372
|
+
for (const up of ['..', '../..']) {
|
|
373
|
+
const root = join(here, up);
|
|
374
|
+
if (!searchDirs.includes(root)) searchDirs.push(root);
|
|
375
|
+
}
|
|
352
376
|
} catch {
|
|
353
|
-
// fallback for CJS
|
|
354
377
|
if (typeof __dirname !== 'undefined') {
|
|
355
|
-
const
|
|
356
|
-
|
|
378
|
+
for (const up of ['..', '../..']) {
|
|
379
|
+
const root = join(__dirname, up);
|
|
380
|
+
if (!searchDirs.includes(root)) searchDirs.push(root);
|
|
381
|
+
}
|
|
357
382
|
}
|
|
358
383
|
}
|
|
359
384
|
|
|
@@ -364,32 +389,24 @@ export function findNativeBinary(baseDir?: string): string | null {
|
|
|
364
389
|
}
|
|
365
390
|
}
|
|
366
391
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
} catch {
|
|
374
|
-
// not found
|
|
375
|
-
}
|
|
392
|
+
try {
|
|
393
|
+
const cmd = platform === 'win32' ? 'where e8' : 'which e8';
|
|
394
|
+
const result = execSync(cmd, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'] }).trim();
|
|
395
|
+
if (result) return result.split('\n')[0];
|
|
396
|
+
} catch {
|
|
397
|
+
// not found
|
|
376
398
|
}
|
|
377
399
|
|
|
378
400
|
return null;
|
|
379
401
|
}
|
|
380
402
|
|
|
381
|
-
/**
|
|
382
|
-
|
|
383
|
-
* The math pipeline is go-native only — NO JS fallback.
|
|
384
|
-
*
|
|
385
|
-
* @param finder - injectable finder for testability; defaults to findNativeBinary.
|
|
386
|
-
*/
|
|
387
|
-
export function requireNativeBinary(finder: () => string | null = findNativeBinary): string {
|
|
403
|
+
/** Return the e8 binary path or throw an install-guiding error. */
|
|
404
|
+
export function requireE8Binary(finder: () => string | null = findE8Binary): string {
|
|
388
405
|
const bin = finder();
|
|
389
406
|
if (!bin) {
|
|
390
407
|
throw new Error(
|
|
391
|
-
'
|
|
392
|
-
'
|
|
408
|
+
'e8 engine binary not found — run `npm install` (platform-core fetches it via install-e8), ' +
|
|
409
|
+
'or point E8_BINARY at a local build (casino-platform/e8/target/release/e8).',
|
|
393
410
|
);
|
|
394
411
|
}
|
|
395
412
|
return bin;
|
|
@@ -445,6 +462,15 @@ export function formatNativeResult(result: NativeSimulationResult): string {
|
|
|
445
462
|
`Max Win Cap Hits: ${result.maxWinHits}`,
|
|
446
463
|
);
|
|
447
464
|
|
|
465
|
+
if (result.volatilityLabel !== undefined && result.cv !== undefined) {
|
|
466
|
+
lines.push(
|
|
467
|
+
'',
|
|
468
|
+
'--- Volatility ---',
|
|
469
|
+
`Volatility: ${result.volatility}/10 (${result.volatilityLabel})`,
|
|
470
|
+
`StdDev: ${(result.stddev ?? 0).toFixed(2)} CV: ${result.cv.toFixed(2)}`,
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
|
|
448
474
|
if (result.bonusTriggered > 0) {
|
|
449
475
|
const frequency = Math.round(result.iterations / result.bonusTriggered);
|
|
450
476
|
lines.push(
|
package/src/simulation/index.ts
CHANGED
|
@@ -8,10 +8,10 @@
|
|
|
8
8
|
|
|
9
9
|
export {
|
|
10
10
|
NativeSimulationRunner,
|
|
11
|
-
findNativeBinary,
|
|
12
11
|
formatNativeResult,
|
|
13
12
|
buildNativeArgs,
|
|
14
|
-
|
|
13
|
+
findE8Binary,
|
|
14
|
+
requireE8Binary,
|
|
15
15
|
} from './NativeSimulationRunner';
|
|
16
16
|
export type {
|
|
17
17
|
NativeSimulationConfig,
|
|
@@ -22,5 +22,3 @@ export type {
|
|
|
22
22
|
DistributionBucket,
|
|
23
23
|
NativeArgsInput,
|
|
24
24
|
} from './NativeSimulationRunner';
|
|
25
|
-
|
|
26
|
-
export { ParallelSimulationRunner } from './ParallelSimulationRunner';
|
package/src/vite/index.ts
CHANGED
|
@@ -7,8 +7,8 @@ import type { Plugin } from 'vite';
|
|
|
7
7
|
* into the HTML during development, so the game can communicate with
|
|
8
8
|
* a mock casino host without manual setup.
|
|
9
9
|
*
|
|
10
|
-
* Pair with `
|
|
11
|
-
*
|
|
10
|
+
* Pair with `spinPlugin` to serve the math endpoint at POST /__lua-play
|
|
11
|
+
* (the route name is the frozen frontend contract; the engine is e8).
|
|
12
12
|
*/
|
|
13
13
|
const VIRTUAL_ID = '/@dev-bridge-entry.js';
|
|
14
14
|
|
|
@@ -75,122 +75,5 @@ await import('${entrySrc}');
|
|
|
75
75
|
};
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Vite plugin that:
|
|
82
|
-
* 1. Enables importing `.lua` files as raw strings with HMR
|
|
83
|
-
* 2. Runs a LuaEngine on the Vite dev server (Node.js) via POST /__lua-play
|
|
84
|
-
*
|
|
85
|
-
* fengari runs server-side only — no browser shims needed.
|
|
86
|
-
*/
|
|
87
|
-
export function luaPlugin(configPath: string): Plugin {
|
|
88
|
-
let luaEngine: any = null;
|
|
89
|
-
let viteServer: any = null;
|
|
90
|
-
|
|
91
|
-
async function initEngine() {
|
|
92
|
-
if (!viteServer) return;
|
|
93
|
-
|
|
94
|
-
try {
|
|
95
|
-
// Invalidate cached modules so HMR picks up changes
|
|
96
|
-
const root = viteServer.config.root;
|
|
97
|
-
const fullConfigPath = configPath.startsWith('.')
|
|
98
|
-
? root + '/' + configPath.replace(/^\.\//, '')
|
|
99
|
-
: configPath;
|
|
100
|
-
|
|
101
|
-
// Invalidate the config module and its dependencies
|
|
102
|
-
const configMod = viteServer.moduleGraph.getModuleById(fullConfigPath);
|
|
103
|
-
if (configMod) viteServer.moduleGraph.invalidateModule(configMod);
|
|
104
|
-
|
|
105
|
-
// ssrLoadModule handles TS transpilation and resolves all imports
|
|
106
|
-
const mod = await viteServer.ssrLoadModule(fullConfigPath);
|
|
107
|
-
const config = mod.default ?? mod.config ?? mod;
|
|
108
|
-
|
|
109
|
-
if (!config.luaScript || !config.gameDefinition) {
|
|
110
|
-
console.log('[LuaPlugin] No luaScript/gameDefinition in config — Lua server disabled');
|
|
111
|
-
luaEngine = null;
|
|
112
|
-
return;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// Load LuaEngine via SSR (fengari runs natively in Node.js)
|
|
116
|
-
const luaMod = await viteServer.ssrLoadModule('@energy8platform/platform-core/lua');
|
|
117
|
-
const { LuaEngine } = luaMod;
|
|
118
|
-
|
|
119
|
-
if (luaEngine) luaEngine.destroy();
|
|
120
|
-
luaEngine = new LuaEngine({
|
|
121
|
-
script: config.luaScript,
|
|
122
|
-
gameDefinition: config.gameDefinition,
|
|
123
|
-
seed: config.luaSeed,
|
|
124
|
-
});
|
|
125
|
-
console.log('[LuaPlugin] LuaEngine initialized (server-side)');
|
|
126
|
-
} catch (e: any) {
|
|
127
|
-
console.warn('[LuaPlugin] Failed to initialize LuaEngine:', e.message);
|
|
128
|
-
luaEngine = null;
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
return {
|
|
133
|
-
name: 'platform-core:lua',
|
|
134
|
-
apply: 'serve',
|
|
135
|
-
|
|
136
|
-
async configureServer(server) {
|
|
137
|
-
viteServer = server;
|
|
138
|
-
await initEngine();
|
|
139
|
-
|
|
140
|
-
// POST /__lua-play — execute Lua on the server
|
|
141
|
-
server.middlewares.use('/__lua-play', (req: any, res: any) => {
|
|
142
|
-
if (req.method !== 'POST') {
|
|
143
|
-
res.statusCode = 405;
|
|
144
|
-
res.end('Method Not Allowed');
|
|
145
|
-
return;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
let body = '';
|
|
149
|
-
req.on('data', (chunk: string) => { body += chunk; });
|
|
150
|
-
req.on('end', () => {
|
|
151
|
-
try {
|
|
152
|
-
if (!luaEngine) {
|
|
153
|
-
res.statusCode = 503;
|
|
154
|
-
res.setHeader('Content-Type', 'application/json');
|
|
155
|
-
res.end(JSON.stringify({ error: 'LuaEngine not initialized' }));
|
|
156
|
-
return;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
const params = JSON.parse(body);
|
|
160
|
-
const result = luaEngine.execute(params);
|
|
161
|
-
|
|
162
|
-
res.statusCode = 200;
|
|
163
|
-
res.setHeader('Content-Type', 'application/json');
|
|
164
|
-
res.end(JSON.stringify(result));
|
|
165
|
-
} catch (e: any) {
|
|
166
|
-
res.statusCode = 500;
|
|
167
|
-
res.setHeader('Content-Type', 'application/json');
|
|
168
|
-
res.end(JSON.stringify({ error: e.message }));
|
|
169
|
-
}
|
|
170
|
-
});
|
|
171
|
-
});
|
|
172
|
-
},
|
|
173
|
-
|
|
174
|
-
transform(code: string, id: string) {
|
|
175
|
-
if (id.endsWith('.lua')) {
|
|
176
|
-
return {
|
|
177
|
-
code: `export default ${JSON.stringify(code)};`,
|
|
178
|
-
map: null,
|
|
179
|
-
};
|
|
180
|
-
}
|
|
181
|
-
},
|
|
182
|
-
|
|
183
|
-
async handleHotUpdate({ file, server }: { file: string; server: any }) {
|
|
184
|
-
if (file.endsWith('.lua') || file.includes('dev.config')) {
|
|
185
|
-
console.log('[LuaPlugin] Reloading LuaEngine...');
|
|
186
|
-
|
|
187
|
-
// Invalidate all SSR modules so ssrLoadModule picks up fresh code
|
|
188
|
-
server.moduleGraph.invalidateAll();
|
|
189
|
-
|
|
190
|
-
await initEngine();
|
|
191
|
-
server.ws.send({ type: 'full-reload' });
|
|
192
|
-
return [];
|
|
193
|
-
}
|
|
194
|
-
},
|
|
195
|
-
};
|
|
196
|
-
}
|
|
78
|
+
export { spinPlugin } from './spinPlugin';
|
|
79
|
+
export type { SpinPluginOptions } from './spinPlugin';
|