@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.
Files changed (57) 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 +9 -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 +70 -27
  9. package/dist/game-spec.cjs.js.map +1 -1
  10. package/dist/game-spec.d.ts +47 -11
  11. package/dist/game-spec.esm.js +68 -25
  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 +28 -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 +8 -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 +33 -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 +46 -17
  38. package/src/game-spec/export.ts +28 -8
  39. package/src/game-spec/index.ts +3 -2
  40. package/src/game-spec/types.ts +11 -1
  41. package/src/index.ts +6 -12
  42. package/src/lua/index.ts +4 -11
  43. package/src/lua/types.ts +7 -0
  44. package/src/simulation/NativeSimulationRunner.ts +71 -45
  45. package/src/simulation/index.ts +2 -4
  46. package/src/vite/index.ts +4 -121
  47. package/src/vite/spinPlugin.ts +338 -0
  48. package/scripts/install-simulate.mjs +0 -101
  49. package/src/lua/ActionRouter.ts +0 -132
  50. package/src/lua/LuaEngine.ts +0 -520
  51. package/src/lua/LuaEngineAPI.ts +0 -314
  52. package/src/lua/PersistentState.ts +0 -80
  53. package/src/lua/SessionManager.ts +0 -249
  54. package/src/lua/SimulationRunner.ts +0 -190
  55. package/src/lua/fengari.d.ts +0 -10
  56. package/src/simulation/ParallelSimulationRunner.ts +0 -156
  57. 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 fengari in the browser.
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.executeLuaOnServer({ action, bet, roundId: serverRoundId, params })
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 executeLuaOnServer(params: PlayParams): Promise<PlayResultData> {
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, toLuaPrelude, toModeMap, toMathModes, toPaytableView } from './derive';
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
- luaPrelude: toLuaPrelude(spec),
10
+ spinPrelude: toSpinPrelude(spec),
11
11
  modeMap: toModeMap(spec),
12
12
  mathModes: toMathModes(spec),
13
13
  paytable: toPaytableView(spec),
@@ -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
- return {
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
- function luaTable(record: Record<number, number>): string {
74
- const parts = Object.entries(record).map(([k, v]) => `[${k}]=${v}`);
75
- return `{${parts.join(', ')}}`;
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
- export function toLuaPrelude(spec: GameSpec): string {
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(`SPEC = { cols = ${spec.grid.cols}, rows = ${spec.grid.rows}, max_win = ${spec.maxWin} }`);
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 = { ${symNames} }`);
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}=${i + 1}`).join(', ');
86
- lines.push(`SYM = { ${symIndex} }`);
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 payEntries = spec.symbols
89
- .filter((s) => s.pay)
90
- .map((s) => ` ${s.id} = ${luaTable(s.pay as Record<number, number>)}`);
91
- lines.push(`PAYTABLE = {\n${payEntries.join(',\n')}\n}`);
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 valEntries = spec.symbols
94
- .filter((s) => s.value !== undefined)
95
- .map((s) => ` ${s.id} = ${Array.isArray(s.value) ? `{${s.value.join(', ')}}` : s.value}`);
96
- if (valEntries.length) lines.push(`VALUES = {\n${valEntries.join(',\n')}\n}`);
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
  }
@@ -1,17 +1,37 @@
1
1
  import type { GameSpec, GameModel } from './types';
2
2
  import { defineGame } from './defineGame';
3
3
 
4
- export function buildLuaScript(model: GameModel, logicLua: string): string {
5
- return model.luaPrelude + '\n' + logicLua;
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
- export function exportGame(
9
- spec: GameSpec,
10
- opts: { logicLua: string },
11
- ): { 'gameDefinition.json': string; 'script.lua': string } {
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
- 'gameDefinition.json': JSON.stringify(model.gameDefinition, null, 2),
15
- 'script.lua': buildLuaScript(model, opts.logicLua),
34
+ 'config.json': JSON.stringify(config, null, 2),
35
+ 'script.spin': script,
16
36
  };
17
37
  }
@@ -1,6 +1,7 @@
1
1
  export * from './types';
2
2
  export { validateSpec, GameSpecError } from './validate';
3
- export { toGameDefinition, toLuaPrelude, toModeMap, toMathModes, toPaytableView } from './derive';
3
+ export { toGameDefinition, toSpinPrelude, toModeMap, toMathModes, toPaytableView } from './derive';
4
4
  export { defineGame } from './defineGame';
5
- export { buildLuaScript, exportGame } from './export';
5
+ export { buildSpinScript, exportGameSpin } from './export';
6
+ export type { E8SpinBundle } from './export';
6
7
  export type { GameDefinition, ActionDefinition, TransitionRule, MaxWinConfig } from '../lua/types';
@@ -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
- luaPrelude: string;
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` — Lua engine + simulation
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
- // ─── Lua ────────────────────────────────────────────────
25
- // LuaEngine and friends are available only via the /lua sub-path. We
26
- // don't re-export them as runtime values from the main entry because
27
- // `fengari` (the underlying Lua VM) is a CommonJS module — pulling it
28
- // in unconditionally breaks Vite dev-mode ESM resolution for any
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
- // 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';
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 luaPath = join(tmpDir, `sim-${id}.lua`);
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(luaPath, script, 'utf-8'),
208
- writeFile(configPath, JSON.stringify({ ...gameDefinition, script_path: luaPath }), 'utf-8'),
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(luaPath), unlink(configPath)]);
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 a native simulation binary in standard locations.
322
- * Returns the absolute path if found, null otherwise.
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 findNativeBinary(baseDir?: string): string | null {
325
- // 1. Explicit env var
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; // darwin, linux, win32
332
- const nodeArch = process.arch; // arm64, x64
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
- // This package's root (where postinstall downloads the binary)
368
+ // Раскладки различаются: dist/simulation.esm.js → ../bin,
369
+ // src/simulation/*.ts (vitest/tsx) ../../bin. Пробуем оба уровня.
349
370
  try {
350
- const pkgRoot = join(dirname(fileURLToPath(import.meta.url)), '..');
351
- if (!searchDirs.includes(pkgRoot)) searchDirs.push(pkgRoot);
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 pkgRoot = join(__dirname, '..');
356
- if (!searchDirs.includes(pkgRoot)) searchDirs.push(pkgRoot);
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
- // Check $PATH
368
- for (const bin of ['simulate', 'simulation']) {
369
- try {
370
- const cmd = platform === 'win32' ? `where ${bin}` : `which ${bin}`;
371
- const result = execSync(cmd, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'] }).trim();
372
- if (result) return result.split('\n')[0];
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
- * Return the native binary path or throw a clear, install-guiding error.
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
- 'native simulation binary not found — run `npm install` (platform-core fetches it via install-simulate). ' +
392
- 'The math pipeline is go-native only (no JS fallback).',
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(
@@ -8,10 +8,10 @@
8
8
 
9
9
  export {
10
10
  NativeSimulationRunner,
11
- findNativeBinary,
12
11
  formatNativeResult,
13
12
  buildNativeArgs,
14
- requireNativeBinary,
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 `luaPlugin` to also enable `.lua` raw imports and serve a
11
- * Lua execution endpoint at POST /__lua-play.
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
- // ─── Lua Plugin ─────────────────────────────────────────
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';