@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
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @energy8platform/platform-core
2
2
 
3
- Renderer-agnostic core for games on the Energy8 casino platform. Pair it with PixiJS, Phaser, Three.js, DOM, or your own engine — `platform-core` ships everything that is platform-specific (Energy8 SDK lifecycle, Lua game scripts, RTP simulation, mock host bridge for local dev, branded loading frame, Vite plugins) without dragging in a renderer.
3
+ Renderer-agnostic core for games on the Energy8 casino platform. Pair it with PixiJS, Phaser, Three.js, DOM, or your own engine — `platform-core` ships everything that is platform-specific (Energy8 SDK lifecycle, the SpinML math runtime, RTP simulation, mock host bridge for local dev, branded loading frame, Vite plugins) without dragging in a renderer.
4
4
 
5
5
  If you want the full PixiJS engine on top of this, install [`@energy8platform/game-engine`](../game-engine/README.md) instead — it depends on `platform-core` and adds scenes, UI, animation, viewport, and React integration.
6
6
 
@@ -13,8 +13,8 @@ If you want the full PixiJS engine on top of this, install [`@energy8platform/ga
13
13
  - [Quick Start](#quick-start)
14
14
  - [Public API](#public-api)
15
15
  - [PlatformSession](#platformsession)
16
- - [Writing your game (config + Lua)](#writing-your-game-config--lua)
17
- - [Lua Engine](#lua-engine)
16
+ - [Writing your game (spec + SpinML)](#writing-your-game-spec--spinml)
17
+ - [SpinML Runtime (e8)](#spinml-runtime-e8)
18
18
  - [DevBridge (mock casino host)](#devbridge-mock-casino-host)
19
19
  - [RTP Simulation CLI](#rtp-simulation-cli)
20
20
  - [Branded Loading Screen](#branded-loading-screen)
@@ -29,7 +29,7 @@ If you want the full PixiJS engine on top of this, install [`@energy8platform/ga
29
29
 
30
30
  ## Why this package exists
31
31
 
32
- The Energy8 casino platform has a contract every game must speak: an SDK handshake, a play-action lifecycle, a Lua execution model used both server-side and locally for development and RTP verification, and a host-side branded loading frame.
32
+ The Energy8 casino platform has a contract every game must speak: an SDK handshake, a play-action lifecycle, a SpinML execution model used both server-side and locally for development and RTP verification, and a host-side branded loading frame.
33
33
 
34
34
  That contract is identical regardless of how you render. So it lives here, with **zero rendering or DOM-coupled code** in the bundle (the only DOM API used is `window` in the dev-mode `MemoryChannel` and `document` in the CSS preloader — neither touches a canvas/WebGL).
35
35
 
@@ -40,15 +40,20 @@ You bring the renderer; `platform-core` brings the platform.
40
40
  ## Installation
41
41
 
42
42
  ```bash
43
- npm install @energy8platform/platform-core @energy8platform/game-sdk fengari
43
+ npm install @energy8platform/platform-core @energy8platform/game-sdk
44
44
  ```
45
45
 
46
+ Postinstall downloads the `e8` / `e8-server` engine binaries for your
47
+ platform from the game-engine repo's GitHub Releases (tag `e8-v<version>`)
48
+ into `platform-core/bin`. Overrides: `E8_BINARY` / `E8_SERVER_BINARY` (local
49
+ build), `E8_RELEASE_REPO` / `E8_DOWNLOAD_BASE` (mirror). The download is
50
+ non-fatal — without it, spin games need one of the overrides.
51
+
46
52
  ### Peer dependencies
47
53
 
48
54
  | Package | Version | Required |
49
55
  | --- | --- | --- |
50
56
  | `@energy8platform/game-sdk` | `^2.7.0` | Yes |
51
- | `fengari` | `^0.1.4` | Yes — Lua engine runtime |
52
57
  | `vite` | `^5.0.0 \|\| ^6.0.0` | Optional — only if you import `/vite` |
53
58
 
54
59
  No `pixi.js`, no `react`, no `phaser`, no DOM rendering library is required.
@@ -59,7 +64,10 @@ No `pixi.js`, no `react`, no `phaser`, no DOM rendering library is required.
59
64
 
60
65
  ```typescript
61
66
  import { createPlatformSession, createCSSPreloader, removeCSSPreloader } from '@energy8platform/platform-core';
62
- import luaScript from './game.lua?raw';
67
+ // The .spin math source. The DevBridge field is still called `luaScript` —
68
+ // for the client it is only the "play via POST /__lua-play" marker; the
69
+ // route is served by e8-server through the `spinPlugin` vite plugin.
70
+ import mathScript from './game/script.spin?raw';
63
71
  import { gameDefinition } from './gameDefinition';
64
72
 
65
73
  const container = document.getElementById('app')!;
@@ -70,7 +78,7 @@ createCSSPreloader(container);
70
78
  // 2. Boot the platform session — DevBridge in dev, real SDK in prod.
71
79
  const session = await createPlatformSession({
72
80
  dev: {
73
- luaScript,
81
+ luaScript: mathScript,
74
82
  gameDefinition,
75
83
  balance: 10000,
76
84
  currency: 'EUR',
@@ -100,13 +108,10 @@ import {
100
108
  createPlatformSession, PlatformSession,
101
109
  type PlatformSessionConfig, type PlatformSessionEvents, type SDKOptions,
102
110
 
103
- // Lua engine + simulation
104
- LuaEngine, LuaEngineAPI, createSeededRng,
105
- ActionRouter, evaluateCondition,
106
- SessionManager, PersistentState,
107
- SimulationRunner, formatSimulationResult,
108
- ParallelSimulationRunner,
109
- NativeSimulationRunner, findNativeBinary, formatNativeResult,
111
+ // Simulation types (runtime classes live in the Node-only /simulation
112
+ // sub-path: NativeSimulationRunner, findE8Binary, formatNativeResult)
113
+ type NativeSimulationConfig, type NativeSimulationResult,
114
+ type StageStats, type DistributionBucket,
110
115
 
111
116
  // DevBridge mock host
112
117
  DevBridge, type DevBridgeConfig,
@@ -119,16 +124,21 @@ import {
119
124
  // Internal utility
120
125
  EventEmitter,
121
126
 
122
- // Platform types (re-exported from @energy8platform/game-sdk + Lua module)
127
+ // Platform types (re-exported from @energy8platform/game-sdk + legacy /lua types)
123
128
  type InitData, type GameConfigData, type SessionData,
124
129
  type PlayParams, type PlayResultData, type BalanceData,
125
130
  type GameDefinition, type ActionDefinition, type TransitionRule,
126
- type LuaEngineConfig, type LuaPlayResult, type SessionConfig,
131
+ type SessionConfig,
127
132
  type BuyBonusConfig, type AnteBetConfig, type MaxWinConfig,
128
133
  type AssetManifest, type AssetBundle, type AssetEntry,
129
134
  type LoadingScreenConfig,
130
135
  // …more — see src/types.ts
131
136
  } from '@energy8platform/platform-core';
137
+
138
+ // Game-spec derivation (spec → SpinML prelude → platform bundle):
139
+ import {
140
+ defineGame, buildSpinScript, exportGameSpin,
141
+ } from '@energy8platform/platform-core/game-spec';
132
142
  ```
133
143
 
134
144
  ---
@@ -186,122 +196,100 @@ In dev, set up the recorded rounds via [`DevBridge` replay mode](#replay-mode-hi
186
196
  const fs = await session.play({ action: 'free_spin', bet: triggeringBet, roundId: result.roundId });
187
197
  ```
188
198
 
189
- The platform validates `bet` against `bet_levels` and rejects `bet: 0`. No double debit happens — the action's `debit: 'none'` keeps the wallet still, and LuaEngine reads the actual session bet from server-side session state regardless of what the client sends. See [Game Development Guide §13.16](https://github.com/energy8platform/game-engine/blob/main/game_development_guide.md#13-conventions-and-best-practices) for the full conventions list.
199
+ The platform validates `bet` against `bet_levels` and rejects `bet: 0`. No double debit happens — session actions don't debit, and the engine reads the actual session bet from server-side session state regardless of what the client sends. See [Game Development Guide §13.16](https://github.com/energy8platform/game-engine/blob/main/game_development_guide.md#13-conventions-and-best-practices) for the full conventions list.
190
200
 
191
201
  ---
192
202
 
193
- ## Writing your game (config + Lua)
203
+ ## Writing your game (spec + SpinML)
194
204
 
195
- Each game on the Energy8 platform consists of two artefacts:
205
+ Each game on the Energy8 platform consists of two sources:
196
206
 
197
- 1. A **`GameDefinition`** (JSON-shaped) — platform metadata: id, type, bet levels, max-win cap, action map with stage transitions, optional buy-bonus / ante-bet config. **No game math here.**
198
- 2. A **Lua script** — exports a single `execute(state)` function that owns *all* game math (reels, paylines, payouts, cascades, free spins, multipliers).
207
+ 1. A **game spec** (`src/game.spec.ts`, for scaffolded games) — symbols, paytable, bet levels, max win, modes, RTP targets. One source of truth; `GameDefinition`, the SpinML prelude, and the math-pipeline mode map are all derived from it via `/game-spec`.
208
+ 2. A **SpinML script** (`.spin`) — a statically-typed, Lua-flavored DSL that owns *all* game math **and** declares its actions, costs, and session transitions. JIT-compiled to native code by the `e8` engine.
199
209
 
200
- The same pair runs server-side in production and locally in dev / RTP simulations.
210
+ The same `.spin` runs in dev (e8-server behind the Vite plugin), in RTP simulation (`e8 simulate`), and in production (the platform's `engine_mode: "spin"`).
201
211
 
202
- ### Minimal slot — `dev.config.ts`
212
+ ### Minimal slot — `script.spin`
203
213
 
204
- ```typescript
205
- import luaScript from './script.lua?raw';
206
- import type { GameDefinition } from '@energy8platform/platform-core';
207
-
208
- const gameDefinition: GameDefinition = {
209
- id: 'my-slot',
210
- type: 'SLOT',
211
- script_path: 'games/my-slot/script.lua', // S3 key in production
212
- bet_levels: [0.20, 0.50, 1.00, 2.00, 5.00],
213
- max_win: { multiplier: 10000 }, // cap = bet × 10000
214
-
215
- actions: {
216
- spin: {
217
- stage: 'base_game',
218
- debit: 'bet', // deducts the bet
219
- credit: 'win', // credits total_win
220
- transitions: [
221
- // Could branch into a free-spins session here. See full guide.
222
- { condition: 'always', next_actions: ['spin'] },
223
- ],
224
- },
225
- },
226
- };
214
+ ```spin
215
+ record Vars { free_spins_awarded: int }
227
216
 
228
- export default {
229
- balance: 10_000,
230
- currency: 'EUR',
231
- networkDelay: 200,
232
- luaScript,
233
- gameDefinition,
234
- };
235
- ```
217
+ enum SpinData tag stage {
218
+ base_game { matrix: [[int]] win_line: int }
219
+ }
236
220
 
237
- ### Minimal slot — `script.lua`
221
+ game "my-slot" {
222
+ bet_levels = [0.20, 0.50, 1.00, 2.00, 5.00]
223
+ max_win = 10000.0
224
+ vars = Vars
225
+ data = SpinData
226
+ }
227
+
228
+ action spin { stage = base_game cost = 1.0 }
238
229
 
239
- ```lua
240
- local SYMBOLS = { 'A', 'K', 'Q', 'J', '10', '9' }
241
230
  -- Payouts are *bet multipliers*. The platform scales by the player's
242
231
  -- actual bet on the way out — never multiply by bet inside the script.
243
- local PAYOUT = { A = 50, K = 30, Q = 20, J = 10, ['10'] = 5, ['9'] = 2 }
244
-
245
- function execute(state)
246
- -- 3 columns × 3 rows of random symbols
247
- local matrix = {}
248
- for col = 1, 3 do
249
- matrix[col] = {}
250
- for row = 1, 3 do
251
- matrix[col][row] = SYMBOLS[engine.random(1, #SYMBOLS)]
252
- end
253
- end
254
-
255
- -- Pay out if all 3 symbols on the middle row match
256
- local center = { matrix[1][2], matrix[2][2], matrix[3][2] }
257
- local total_win = 0
258
- if center[1] == center[2] and center[2] == center[3] then
259
- total_win = PAYOUT[center[1]]
260
- end
261
-
262
- return {
263
- total_win = total_win,
264
- data = { matrix = matrix, win_lines = total_win > 0 and { 2 } or {} },
265
- }
266
- end
232
+ const PAYS: [float; 6] = [50.0, 30.0, 20.0, 10.0, 5.0, 2.0]
233
+
234
+ fn execute(c: ctx, v: Vars) -> outcome {
235
+ let matrix: [[int]] = list()
236
+ for col in 0..3 {
237
+ let rows: [int] = list()
238
+ for row in 0..3 { push(rows, rng(c, 0, 5)) }
239
+ push(matrix, rows)
240
+ }
241
+
242
+ -- pay if all 3 middle-row symbols match
243
+ let a = matrix[0][1]
244
+ let win = 0.0
245
+ let line = 0
246
+ if a == matrix[1][1] && a == matrix[2][1] {
247
+ win = PAYS[a]
248
+ line = 1
249
+ }
250
+
251
+ return outcome {
252
+ win: win,
253
+ vars: Vars { free_spins_awarded: 0 },
254
+ data: SpinData.base_game { matrix: matrix, win_line: line },
255
+ }
256
+ }
267
257
  ```
268
258
 
269
- That's the entire contract: a stage to dispatch on (here just `base_game`) plus a `total_win` (a **bet multiplier**, not absolute currency) and an arbitrary `data` payload. The platform handles the rest debit/credit (`real_win = bet × total_win`), balance updates, session lifecycle, cap enforcement. See [Game Development Guide §13.2](https://github.com/energy8platform/game-engine/blob/main/game_development_guide.md#13-conventions-and-best-practices) for the full convention.
259
+ That's the entire contract: `outcome { win, vars, data }` — a bet-multiplier
260
+ win, the typed persisted state, and a stage-tagged payload. The platform
261
+ handles the rest — debit/credit (`real_win = bet × win`), balance updates,
262
+ session lifecycle (from the `opens`/`extends`/`ends when` action
263
+ declarations), cap enforcement.
270
264
 
271
- ### Full reference
272
-
273
- The mini-example above covers a base-game spin only. For everything else — free spins via `creates_session` + `next_actions`, retrigger logic, persistent meters across spins (`_persist_*`), buy-bonus and ante-bet configuration, table-game session models, the full `engine.*` Lua API, JSON-Schema input/output validation, deployment and S3 layout — see the comprehensive guide:
265
+ In dev, `dev.config.ts` carries the script source and the (spec-derived)
266
+ `GameDefinition` — see [Quick Start](#quick-start).
274
267
 
275
- - **[Game Development Guide](https://github.com/energy8platform/game-engine/blob/main/game_development_guide.md)** (1100+ lines)
268
+ ### Full reference
276
269
 
277
- Key sections to start with: §2 (`GameDefinition` shape), §7 (Lua script), §8 (`engine.*` API), §15 (table games), §16 (persistent state).
270
+ - **[SpinML language guide](../../docs/spinml.md)** declarations, types, const groups, builtins, limits, simulation dialect.
271
+ - **[Lua → SpinML migration](../../docs/lua-to-spin-migration.md)** — porting an existing game.
272
+ - **[Game Development Guide](../../game_development_guide.md)** — the platform contract: `GameDefinition` shape, actions, deployment, S3 layout, table games.
278
273
 
279
274
  ---
280
275
 
281
- ## Lua Engine
276
+ ## SpinML Runtime (e8)
282
277
 
283
- Run platform Lua scripts locally in Node or the browser via `fengari` (Lua 5.3, pure JS). This replicates server-side execution byte-for-byte, so the same script you ship to production also drives local development and RTP simulations.
278
+ The math runtime is the Rust `e8` engine (SpinML Cranelift JIT native
279
+ code), delivered as per-platform binaries by postinstall:
284
280
 
285
- ```typescript
286
- import { LuaEngine } from '@energy8platform/platform-core';
281
+ - **`e8`** — CLI: `check` (compile + type-check a `.spin`) and `simulate`
282
+ (Go-CLI-compatible dialect same flags, JSON shape, and statistics as the
283
+ old simulate binary; ~4M rounds/sec per core).
284
+ - **`e8-server`** — the round server. In dev the Vite `spinPlugin` spawns it
285
+ (`--sessions memory --watch`: hot-reloads the `.spin` on save, open rounds
286
+ finish on the old version). In production the platform talks to the same
287
+ domain API over gRPC — dev exercises the exact prod contract.
287
288
 
288
- const engine = new LuaEngine({
289
- script: '<your lua source>',
290
- gameDefinition: { /* */ },
291
- seed: 42, // optional deterministic RNG
292
- });
293
-
294
- const result = engine.execute({
295
- variables: { bet: 1, balance: 5000 },
296
- stage: 'base_game',
297
- });
298
- // → { total_win, data, next_actions, session, persistent_state }
299
- ```
300
-
301
- Companion classes:
302
- - `ActionRouter` — dispatch a play request to the matching action and evaluate transition conditions (`&&`, `||`, comparisons, `"always"`).
303
- - `SessionManager` — track session lifecycle: creation, spin counting, retrigger, `_persist_` data roundtrip, completion. Supports both fixed-spin slot sessions and unlimited table sessions.
304
- - `PersistentState` — cross-spin persistent vars (`persistent_state.vars` and `_persist_game_*` convention).
289
+ The old in-process classes (`LuaEngine`, `ActionRouter`, `SessionManager`,
290
+ `SimulationRunner`) were removed with the fengari runtime — the engine now
291
+ owns rounds, sessions, idempotency, and RNG. Legacy Lua games stay on
292
+ platform-core 0.28.x; see the migration guide to move.
305
293
 
306
294
  ---
307
295
 
@@ -324,7 +312,8 @@ const bridge = new DevBridge({
324
312
  totalWin: Math.random() < 0.4 ? bet * 5 : 0,
325
313
  }),
326
314
 
327
- // Or: hand it your Lua game logic (preferred — same code as prod)
315
+ // Or: hand it your .spin game math (preferred — same code as prod;
316
+ // served by e8-server via spinPlugin, the field name is legacy)
328
317
  // luaScript, gameDefinition, luaSeed,
329
318
  });
330
319
 
@@ -336,9 +325,9 @@ bridge.destroy();
336
325
 
337
326
  Most of the time you don't construct DevBridge yourself — `createPlatformSession({ dev: { … } })` does it for you.
338
327
 
339
- ### Platform-parity behavior (Lua mode)
328
+ ### Platform-parity behavior (server mode)
340
329
 
341
- In Lua mode (`luaScript` + `gameDefinition`), DevBridge mirrors the server's `PlayRound` contract so error-handling code written against dev runs unchanged in prod. Invalid requests come back as `PLAY_ERROR` and the SDK's `play()` rejects with `SDKError(code, message)`:
330
+ With `luaScript` + `gameDefinition` set (the field name is legacy — it carries the `.spin` source and marks "play via `POST /__lua-play`", served by e8-server through `spinPlugin`), DevBridge mirrors the server's `PlayRound` contract so error-handling code written against dev runs unchanged in prod. Invalid requests come back as `PLAY_ERROR` and the SDK's `play()` rejects with `SDKError(code, message)`:
342
331
 
343
332
  | code | when |
344
333
  |-------------------------|----------------------------------------------------------|
@@ -348,7 +337,7 @@ In Lua mode (`luaScript` + `gameDefinition`), DevBridge mirrors the server's `Pl
348
337
  | `ACTIVE_SESSION_EXISTS` | non-session action while a session is in progress |
349
338
  | `NO_ACTIVE_SESSION` | session-required action without an active session |
350
339
  | `SESSION_EXPIRED` | session past `gameDefinition.session_ttl` (default 24h) |
351
- | `ENGINE_ERROR` | Lua execution failed (debit is rolled back) |
340
+ | `ENGINE_ERROR` | script execution failed (debit is rolled back) |
352
341
 
353
342
  Other contract details DevBridge enforces:
354
343
 
@@ -356,7 +345,7 @@ Other contract details DevBridge enforces:
356
345
  - **`STATE_RESPONSE`** returns the last `PlayResultData` (with `session.history` populated) while a session is active and not yet completed, mirroring `GET /api/games/{id}/session`.
357
346
  - **`creditPending`** is `false` in the normal path. The wire flag means "wallet credit failed, queued for retry" — never "credit deferred until session completes".
358
347
  - **`session.history`** is appended on every session round (`{spinIndex, win, data}`), so the client can rebuild the screen after reload.
359
- - **`MapState` parity** — `multiplier`, `global_multiplier`, `free_spins_total`, `max_win_reached` are auto-injected into `result.data` from engine variables when the Lua script doesn't set them explicitly.
348
+ - **`MapState` parity** — `multiplier`, `global_multiplier`, `free_spins_total`, `max_win_reached` are auto-injected into `result.data` from engine variables when the script doesn't set them explicitly.
360
349
 
361
350
  ### Replay mode (historical rounds)
362
351
 
@@ -391,28 +380,25 @@ The game reacts via a single flag — see [`session.isReplay`](#platformsession)
391
380
 
392
381
  ## RTP Simulation CLI
393
382
 
394
- `platform-core` ships a binary that runs your Lua script through millions of iterations to verify math and stage distributions. It picks up `luaScript` and `gameDefinition` from your `dev.config.ts` automatically.
383
+ `platform-core` ships a dev CLI that runs your `.spin` through millions of iterations via `e8 simulate` to verify math and stage distributions. It picks up the script and `gameDefinition` from your `dev.config.ts` automatically. (For the full book-bundle pipeline — pool/curate/publish — use `e8-math` from `@energy8platform/stake-math-tools`.)
395
384
 
396
385
  ```bash
397
386
  # 1M spins (default)
398
387
  npx platform-core-simulate
399
388
 
400
- # Buy-bonus action (v5: just simulate the action by name)
389
+ # Buy-bonus action (just simulate the action by name)
401
390
  npx platform-core-simulate --action buy_bonus
402
391
 
403
- # Ante bet — also a regular action in v5
392
+ # Ante bet — also a regular action
404
393
  npx platform-core-simulate --action ante_spin
405
394
 
406
395
  # Custom: 5M iterations, custom config path
407
396
  npx platform-core-simulate --iterations 5000000 --bet 1 --config ./dev.config.ts
408
-
409
- # Force the JS runner (skip native binary)
410
- npx platform-core-simulate --js
411
397
  ```
412
398
 
413
399
  ### Reproducibility: seeds, RNG backend, and replay
414
400
 
415
- The native binary supports the same provably-fair seeding contract as the casino platform's `cmd/simulation` tool. Pass `--seed=<hex>` to reproduce a previous run bit-for-bit; if you omit it, the binary generates one and reports it in the output (`Master seed: …`) so you can rerun the exact distribution later.
401
+ The engine keeps the provably-fair seeding contract of the old Go simulate tool. Pass `--seed=<hex>` to reproduce a previous run bit-for-bit (results are also **core-count independent** — the master seed derives 64 RNG lanes, `round % 64`); if you omit it, a seed is generated and reported (`Master seed: …`) so you can rerun the exact distribution later.
416
402
 
417
403
  ```bash
418
404
  # Reproducible run — supply the master seed yourself
@@ -420,12 +406,12 @@ npx platform-core-simulate \
420
406
  --iterations 1000000 \
421
407
  --seed 00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff
422
408
 
423
- # Fast PCG RNG — ~50× faster but diverges from production. Local iteration only;
409
+ # Fast PCG RNG — faster but diverges from production. Local iteration only;
424
410
  # do NOT publish RTP numbers from --rng=fast.
425
411
  npx platform-core-simulate --rng fast
426
412
 
427
- # Replay a single round captured in `provably_fair_rounds`. Forces single-worker
428
- # deterministic execution. All three flags are required and require provably-fair RNG.
413
+ # Replay a single round captured in `provably_fair_rounds`. All three flags
414
+ # are required and require provably-fair RNG.
429
415
  npx platform-core-simulate \
430
416
  --iterations 1 \
431
417
  --replay-server-seed <hex> \
@@ -433,32 +419,24 @@ npx platform-core-simulate \
433
419
  --replay-nonce-start 42
434
420
  ```
435
421
 
436
- The result echoes `masterSeed`, `rngKind`, `workerSeeds[]` (per-worker server_seed sequence), and `replay` when in replay mode — all also surface on `NativeSimulationResult` for programmatic use. Hex seeds only apply to the native binary; the JS fallback uses an integer RNG seed (decimal `--seed=42`) and ignores hex strings with a warning.
422
+ The result echoes `masterSeed`, `rngKind`, `workerSeeds[]`, and `replay` when in replay mode — all also surface on `NativeSimulationResult` for programmatic use. Output matches the old server-side simulation format field-for-field, plus stddev/CV, the 0–10 volatility score, and the win-distribution buckets.
437
423
 
438
- Output matches the platform's server-side simulation format. A native Go binary is downloaded for your OS via postinstall (`packages/platform-core/bin/simulate-*`) for high-throughput runs; if it isn't available, the JS / worker-thread runner is used as a fallback.
439
-
440
- Programmatic use:
424
+ Programmatic use (Node-only sub-path):
441
425
 
442
426
  ```typescript
443
- import { ParallelSimulationRunner, NativeSimulationRunner, formatSimulationResult } from '@energy8platform/platform-core';
444
-
445
- const runner = new ParallelSimulationRunner({
446
- script, gameDefinition,
447
- iterations: 1_000_000,
448
- workers: 8,
449
- });
450
- const result = await runner.run();
451
- console.log(formatSimulationResult(result));
427
+ import { NativeSimulationRunner, findE8Binary, formatNativeResult } from '@energy8platform/platform-core/simulation';
452
428
 
453
- // Native runner with the full provably-fair contract:
454
429
  const native = new NativeSimulationRunner({
455
- binaryPath, script, gameDefinition,
430
+ binaryPath: findE8Binary() ?? process.env.E8_BINARY!,
431
+ argsPrefix: ['simulate'],
432
+ script, scriptExt: 'spin', gameDefinition,
456
433
  iterations: 1_000_000, bet: 1,
457
434
  rng: 'provably-fair', // default; use 'fast' for local iteration only
458
435
  seed: '00112233...eeff', // hex master seed; omit to auto-generate
459
436
  // replay: { serverSeed, clientSeed, nonceStart }, // single-round reproduction
460
437
  });
461
438
  const r = await native.run();
439
+ console.log(formatNativeResult(r));
462
440
  console.log(`Reproduce with seed=${r.masterSeed}, RTP=${r.totalRtp.toFixed(4)}%`);
463
441
  ```
464
442
 
@@ -518,22 +496,19 @@ The animated shimmer inside the SVG is pure CSS keyframes, so it appears in offl
518
496
  ```typescript
519
497
  // vite.config.ts (Phaser/Three/custom — full control over your config)
520
498
  import { defineConfig } from 'vite';
521
- import { devBridgePlugin, luaPlugin } from '@energy8platform/platform-core/vite';
499
+ import { devBridgePlugin, spinPlugin } from '@energy8platform/platform-core/vite';
522
500
 
523
501
  export default defineConfig({
524
502
  plugins: [
525
503
  devBridgePlugin('./dev.config'),
526
- luaPlugin('./dev.config'),
504
+ spinPlugin({ spinPath: './src/game/script.spin', gameId: 'my-slot' }),
527
505
  ],
528
506
  });
529
507
  ```
530
508
 
531
509
  What they do:
532
510
  - **`devBridgePlugin`** injects a virtual entry that boots `DevBridge` from your `./dev.config` *before* your real entry imports. Dev-only.
533
- - **`luaPlugin`**:
534
- 1. Lets you `import luaScript from './game.lua?raw'` — Vite returns the file contents.
535
- 2. Spins up a server-side `LuaEngine` and exposes `POST /__lua-play`. `DevBridge` calls this endpoint, so `fengari` only ever runs in Node and never ships to the browser bundle.
536
- 3. HMR-reloads the Lua engine when `*.lua` or `dev.config*` changes.
511
+ - **`spinPlugin`** spawns `e8-server` (`--sessions memory --watch`) and exposes `POST /__lua-play` (the route name is the frozen DevBridge contract). The server owns rounds/sessions/idempotency — dev exercises the exact production domain API. Saving the `.spin` hot-reloads the math; open rounds finish on the old version. Binary resolution: `binPath` option → `E8_SERVER_BINARY` → `platform-core/bin` (postinstall) → `PATH`.
537
512
 
538
513
  If you're building a Pixi game, prefer `defineGameConfig` from `@energy8platform/game-engine/vite` — it wires both plugins for you and adds Pixi-flavored Vite defaults (chunk splitting, dedupe, etc.).
539
514
 
@@ -837,14 +812,16 @@ section, all three bar modes, theme/social toggles, viewport presets, and event
837
812
  | Path | What's there |
838
813
  | --- | --- |
839
814
  | `@energy8platform/platform-core` | Everything — re-exports from all sub-paths |
840
- | `@energy8platform/platform-core/lua` | Browser-safe Lua engine surface: LuaEngine, ActionRouter, SessionManager, PersistentState, JS `SimulationRunner`, types |
841
- | `@energy8platform/platform-core/simulation` | **Node-only.** `NativeSimulationRunner` (Go binary) and `ParallelSimulationRunner` (worker_threads). Don't import from a browser bundlethe main entry and `/lua` deliberately exclude these so they can't be tree-shake-leaked. |
815
+ | `@energy8platform/platform-core/game-spec` | `defineGame`, `validateSpec`, `toGameDefinition`, `toSpinPrelude`, `buildSpinScript`, `exportGameSpin` the one-source-of-truth spec layer |
816
+ | `@energy8platform/platform-core/lua` | **Types only** (GameDefinition, ActionDefinition, ). The fengari runtime was removed in 0.29 legacy Lua games stay on 0.28.x |
817
+ | `@energy8platform/platform-core/simulation` | **Node-only.** `NativeSimulationRunner` / `findE8Binary` / `formatNativeResult` (wraps `e8 simulate`). Don't import from a browser bundle |
842
818
  | `@energy8platform/platform-core/dev-bridge` | `DevBridge`, `DevBridgeConfig`, `ReplayConfig`, `ReplayLaunch` |
843
- | `@energy8platform/platform-core/vite` | `devBridgePlugin`, `luaPlugin` |
819
+ | `@energy8platform/platform-core/vite` | `devBridgePlugin`, `spinPlugin` |
844
820
  | `@energy8platform/platform-core/loading` | `createCSSPreloader`, `setCSSPreloaderProgress`, `waitCSSPreloaderTap`, `removeCSSPreloader`, `buildLogoSVG`, `LOADER_BAR_MAX_WIDTH` |
845
- | `@energy8platform/platform-core/shell` | `createGameShell`, `removeGameShell` branded renderer-agnostic DOM game shell (control bar, menu, settings, game info, buy bonus) |
821
+ | `@energy8platform/platform-core/slot-result` | Slot result normalization helpers shared by scaffolded games |
822
+ | `@energy8platform/platform-core/shell` | Moved to `@energy8platform/shell` (subpaths `/html`, `/pixi`) |
846
823
 
847
- The sub-paths exist for tree-shaking — pulling only `/lua` doesn't drag in DevBridge or vite types. The main entry is convenient for app-level code where size hardly matters.
824
+ The sub-paths exist for tree-shaking — pulling only `/game-spec` doesn't drag in DevBridge or vite types. The main entry is convenient for app-level code where size hardly matters.
848
825
 
849
826
  ---
850
827