@energy8platform/platform-core 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/README.md +482 -0
  2. package/bin/simulate.ts +139 -0
  3. package/dist/dev-bridge.cjs.js +237 -0
  4. package/dist/dev-bridge.cjs.js.map +1 -0
  5. package/dist/dev-bridge.d.ts +141 -0
  6. package/dist/dev-bridge.esm.js +235 -0
  7. package/dist/dev-bridge.esm.js.map +1 -0
  8. package/dist/index.cjs.js +569 -0
  9. package/dist/index.cjs.js.map +1 -0
  10. package/dist/index.d.ts +439 -0
  11. package/dist/index.esm.js +560 -0
  12. package/dist/index.esm.js.map +1 -0
  13. package/dist/loading.cjs.js +190 -0
  14. package/dist/loading.cjs.js.map +1 -0
  15. package/dist/loading.d.ts +86 -0
  16. package/dist/loading.esm.js +185 -0
  17. package/dist/loading.esm.js.map +1 -0
  18. package/dist/lua.cjs.js +1129 -0
  19. package/dist/lua.cjs.js.map +1 -0
  20. package/dist/lua.d.ts +319 -0
  21. package/dist/lua.esm.js +1119 -0
  22. package/dist/lua.esm.js.map +1 -0
  23. package/dist/simulation.cjs.js +374 -0
  24. package/dist/simulation.cjs.js.map +1 -0
  25. package/dist/simulation.d.ts +190 -0
  26. package/dist/simulation.esm.js +368 -0
  27. package/dist/simulation.esm.js.map +1 -0
  28. package/dist/vite.cjs.js +179 -0
  29. package/dist/vite.cjs.js.map +1 -0
  30. package/dist/vite.d.ts +13 -0
  31. package/dist/vite.esm.js +176 -0
  32. package/dist/vite.esm.js.map +1 -0
  33. package/package.json +100 -0
  34. package/scripts/install-simulate.mjs +101 -0
  35. package/src/EventEmitter.ts +55 -0
  36. package/src/PlatformSession.ts +156 -0
  37. package/src/dev-bridge/DevBridge.ts +305 -0
  38. package/src/dev-bridge/index.ts +2 -0
  39. package/src/index.ts +98 -0
  40. package/src/loading/CSSPreloader.ts +129 -0
  41. package/src/loading/index.ts +3 -0
  42. package/src/loading/logo.ts +95 -0
  43. package/src/lua/ActionRouter.ts +132 -0
  44. package/src/lua/LuaEngine.ts +412 -0
  45. package/src/lua/LuaEngineAPI.ts +314 -0
  46. package/src/lua/PersistentState.ts +80 -0
  47. package/src/lua/SessionManager.ts +227 -0
  48. package/src/lua/SimulationRunner.ts +192 -0
  49. package/src/lua/fengari.d.ts +10 -0
  50. package/src/lua/index.ts +28 -0
  51. package/src/lua/types.ts +149 -0
  52. package/src/simulation/NativeSimulationRunner.ts +367 -0
  53. package/src/simulation/ParallelSimulationRunner.ts +156 -0
  54. package/src/simulation/SimulationWorker.ts +44 -0
  55. package/src/simulation/index.ts +21 -0
  56. package/src/types.ts +85 -0
  57. package/src/vite/index.ts +196 -0
package/README.md ADDED
@@ -0,0 +1,482 @@
1
+ # @energy8platform/platform-core
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.
4
+
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
+
7
+ ---
8
+
9
+ ## Table of Contents
10
+
11
+ - [Why this package exists](#why-this-package-exists)
12
+ - [Installation](#installation)
13
+ - [Quick Start](#quick-start)
14
+ - [Public API](#public-api)
15
+ - [PlatformSession](#platformsession)
16
+ - [Writing your game (config + Lua)](#writing-your-game-config--lua)
17
+ - [Lua Engine](#lua-engine)
18
+ - [DevBridge (mock casino host)](#devbridge-mock-casino-host)
19
+ - [RTP Simulation CLI](#rtp-simulation-cli)
20
+ - [Branded Loading Screen](#branded-loading-screen)
21
+ - [Vite Plugins](#vite-plugins)
22
+ - [Asset Manifest type](#asset-manifest-type)
23
+ - [Pairing with another renderer](#pairing-with-another-renderer)
24
+ - [Sub-path exports](#sub-path-exports)
25
+ - [License](#license)
26
+
27
+ ---
28
+
29
+ ## Why this package exists
30
+
31
+ 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
+
33
+ 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).
34
+
35
+ You bring the renderer; `platform-core` brings the platform.
36
+
37
+ ---
38
+
39
+ ## Installation
40
+
41
+ ```bash
42
+ npm install @energy8platform/platform-core @energy8platform/game-sdk fengari
43
+ ```
44
+
45
+ ### Peer dependencies
46
+
47
+ | Package | Version | Required |
48
+ | --- | --- | --- |
49
+ | `@energy8platform/game-sdk` | `^2.7.0` | Yes |
50
+ | `fengari` | `^0.1.4` | Yes — Lua engine runtime |
51
+ | `vite` | `^5.0.0 \|\| ^6.0.0` | Optional — only if you import `/vite` |
52
+
53
+ No `pixi.js`, no `react`, no `phaser`, no DOM rendering library is required.
54
+
55
+ ---
56
+
57
+ ## Quick Start
58
+
59
+ ```typescript
60
+ import { createPlatformSession, createCSSPreloader, removeCSSPreloader } from '@energy8platform/platform-core';
61
+ import luaScript from './game.lua?raw';
62
+ import { gameDefinition } from './gameDefinition';
63
+
64
+ const container = document.getElementById('app')!;
65
+
66
+ // 1. Show the Energy8 brand frame immediately.
67
+ createCSSPreloader(container);
68
+
69
+ // 2. Boot the platform session — DevBridge in dev, real SDK in prod.
70
+ const session = await createPlatformSession({
71
+ dev: {
72
+ luaScript,
73
+ gameDefinition,
74
+ balance: 10000,
75
+ currency: 'EUR',
76
+ networkDelay: 200,
77
+ },
78
+ sdk: { devMode: true },
79
+ });
80
+
81
+ session.on('balanceUpdate', ({ balance }) => updateHud(balance));
82
+
83
+ // 3. Initialize *your* renderer (Phaser, Three, custom). When ready,
84
+ // pull session.initData.assetsUrl, load your assets, then…
85
+ removeCSSPreloader(container);
86
+
87
+ // 4. Drive plays through the SDK.
88
+ const result = await session.play({ action: 'spin', bet: 1 });
89
+ renderResult(result);
90
+ ```
91
+
92
+ ---
93
+
94
+ ## Public API
95
+
96
+ ```typescript
97
+ import {
98
+ // Session lifecycle
99
+ createPlatformSession, PlatformSession,
100
+ type PlatformSessionConfig, type PlatformSessionEvents, type SDKOptions,
101
+
102
+ // Lua engine + simulation
103
+ LuaEngine, LuaEngineAPI, createSeededRng,
104
+ ActionRouter, evaluateCondition,
105
+ SessionManager, PersistentState,
106
+ SimulationRunner, formatSimulationResult,
107
+ ParallelSimulationRunner,
108
+ NativeSimulationRunner, findNativeBinary, formatNativeResult,
109
+
110
+ // DevBridge mock host
111
+ DevBridge, type DevBridgeConfig,
112
+
113
+ // Branded loading frame
114
+ createCSSPreloader, removeCSSPreloader, buildLogoSVG, LOADER_BAR_MAX_WIDTH,
115
+
116
+ // Internal utility
117
+ EventEmitter,
118
+
119
+ // Platform types (re-exported from @energy8platform/game-sdk + Lua module)
120
+ type InitData, type GameConfigData, type SessionData,
121
+ type PlayParams, type PlayResultData, type BalanceData,
122
+ type GameDefinition, type ActionDefinition, type TransitionRule,
123
+ type LuaEngineConfig, type LuaPlayResult, type SessionConfig,
124
+ type BuyBonusConfig, type AnteBetConfig, type MaxWinConfig,
125
+ type AssetManifest, type AssetBundle, type AssetEntry,
126
+ type LoadingScreenConfig,
127
+ // …more — see src/types.ts
128
+ } from '@energy8platform/platform-core';
129
+ ```
130
+
131
+ ---
132
+
133
+ ## PlatformSession
134
+
135
+ `createPlatformSession(config)` is the entry point. It performs the SDK handshake (and optionally starts a local DevBridge mock host) and returns a typed event source.
136
+
137
+ ```typescript
138
+ const session = await createPlatformSession({
139
+ // Optional. When present, an in-process DevBridge is started so the
140
+ // SDK connects to a local mock host without any real backend.
141
+ dev: {
142
+ balance: 10000,
143
+ currency: 'EUR',
144
+ luaScript: '<your lua source>', // optional, runs locally via fengari
145
+ gameDefinition: { /* … */ },
146
+ networkDelay: 200,
147
+ },
148
+
149
+ // Optional. Pass `false` for offline / head-less use (no SDK at all).
150
+ sdk: { devMode: true },
151
+ });
152
+
153
+ session.sdk; // CasinoGameSDK | null
154
+ session.initData; // InitData | null — first handshake response
155
+ session.devBridge; // DevBridge | null
156
+ session.balance; // number — proxied to SDK
157
+ session.currency; // string
158
+ session.on('balanceUpdate', ({ balance }) => { /* … */ });
159
+ session.on('error', (err) => { /* … */ });
160
+
161
+ const result = await session.play({ action: 'spin', bet: 1 });
162
+ session.destroy();
163
+ ```
164
+
165
+ Inside `game-engine`, `GameApplication` wraps this. For non-pixi consumers, this is the layer you talk to directly.
166
+
167
+ ---
168
+
169
+ ## Writing your game (config + Lua)
170
+
171
+ Each game on the Energy8 platform consists of two artefacts:
172
+
173
+ 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.**
174
+ 2. A **Lua script** — exports a single `execute(state)` function that owns *all* game math (reels, paylines, payouts, cascades, free spins, multipliers).
175
+
176
+ The same pair runs server-side in production and locally in dev / RTP simulations.
177
+
178
+ ### Minimal slot — `dev.config.ts`
179
+
180
+ ```typescript
181
+ import luaScript from './script.lua?raw';
182
+ import type { GameDefinition } from '@energy8platform/platform-core';
183
+
184
+ const gameDefinition: GameDefinition = {
185
+ id: 'my-slot',
186
+ type: 'SLOT',
187
+ script_path: 'games/my-slot/script.lua', // S3 key in production
188
+ bet_levels: [0.20, 0.50, 1.00, 2.00, 5.00],
189
+ max_win: { multiplier: 10000 }, // cap = bet × 10000
190
+
191
+ actions: {
192
+ spin: {
193
+ stage: 'base_game',
194
+ debit: 'bet', // deducts the bet
195
+ credit: 'win', // credits total_win
196
+ transitions: [
197
+ // Could branch into a free-spins session here. See full guide.
198
+ { condition: 'always', next_actions: ['spin'] },
199
+ ],
200
+ },
201
+ },
202
+ };
203
+
204
+ export default {
205
+ balance: 10_000,
206
+ currency: 'EUR',
207
+ networkDelay: 200,
208
+ luaScript,
209
+ gameDefinition,
210
+ };
211
+ ```
212
+
213
+ ### Minimal slot — `script.lua`
214
+
215
+ ```lua
216
+ local SYMBOLS = { 'A', 'K', 'Q', 'J', '10', '9' }
217
+ local PAYOUT = { A = 50, K = 30, Q = 20, J = 10, ['10'] = 5, ['9'] = 2 } -- × bet
218
+
219
+ function execute(state)
220
+ local bet = state.variables.bet
221
+
222
+ -- 3 columns × 3 rows of random symbols
223
+ local matrix = {}
224
+ for col = 1, 3 do
225
+ matrix[col] = {}
226
+ for row = 1, 3 do
227
+ matrix[col][row] = SYMBOLS[engine.random(1, #SYMBOLS)]
228
+ end
229
+ end
230
+
231
+ -- Pay out if all 3 symbols on the middle row match
232
+ local center = { matrix[1][2], matrix[2][2], matrix[3][2] }
233
+ local total_win = 0
234
+ if center[1] == center[2] and center[2] == center[3] then
235
+ total_win = bet * PAYOUT[center[1]]
236
+ end
237
+
238
+ return {
239
+ total_win = total_win,
240
+ data = { matrix = matrix, win_lines = total_win > 0 and { 2 } or {} },
241
+ }
242
+ end
243
+ ```
244
+
245
+ That's the entire contract: `state.variables.bet` in, a `total_win` and arbitrary `data` payload out. The platform handles the rest (debit/credit, balance updates, session lifecycle, cap enforcement).
246
+
247
+ ### Full reference
248
+
249
+ 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:
250
+
251
+ - **[Game Development Guide](https://github.com/energy8platform/game-engine/blob/main/game_development_guide.md)** (1100+ lines)
252
+
253
+ Key sections to start with: §2 (`GameDefinition` shape), §7 (Lua script), §8 (`engine.*` API), §15 (table games), §16 (persistent state).
254
+
255
+ ---
256
+
257
+ ## Lua Engine
258
+
259
+ 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.
260
+
261
+ ```typescript
262
+ import { LuaEngine } from '@energy8platform/platform-core';
263
+
264
+ const engine = new LuaEngine({
265
+ script: '<your lua source>',
266
+ gameDefinition: { /* … */ },
267
+ seed: 42, // optional — deterministic RNG
268
+ });
269
+
270
+ const result = engine.execute({
271
+ variables: { bet: 1, balance: 5000 },
272
+ stage: 'base_game',
273
+ });
274
+ // → { total_win, data, next_actions, session, persistent_state }
275
+ ```
276
+
277
+ Companion classes:
278
+ - `ActionRouter` — dispatch a play request to the matching action and evaluate transition conditions (`&&`, `||`, comparisons, `"always"`).
279
+ - `SessionManager` — track session lifecycle: creation, spin counting, retrigger, `_persist_` data roundtrip, completion. Supports both fixed-spin slot sessions and unlimited table sessions.
280
+ - `PersistentState` — cross-spin persistent vars (`persistent_state.vars` and `_persist_game_*` convention).
281
+
282
+ ---
283
+
284
+ ## DevBridge (mock casino host)
285
+
286
+ Mock the casino host for offline development. Uses the SDK's `Bridge` in `devMode` with an in-memory `MemoryChannel`, so there is no postMessage or iframe involved.
287
+
288
+ ```typescript
289
+ import { DevBridge } from '@energy8platform/platform-core/dev-bridge';
290
+
291
+ const bridge = new DevBridge({
292
+ balance: 10000,
293
+ currency: 'USD',
294
+ networkDelay: 200,
295
+ debug: true,
296
+ gameConfig: { id: 'my-slot', type: 'slot', betLevels: [0.1, 0.5, 1, 5, 10] },
297
+
298
+ // Either: implement onPlay yourself
299
+ onPlay: ({ action, bet }) => ({
300
+ totalWin: Math.random() < 0.4 ? bet * 5 : 0,
301
+ }),
302
+
303
+ // Or: hand it your Lua game logic (preferred — same code as prod)
304
+ // luaScript, gameDefinition, luaSeed,
305
+ });
306
+
307
+ bridge.start();
308
+ // later:
309
+ bridge.setBalance(5000);
310
+ bridge.destroy();
311
+ ```
312
+
313
+ Most of the time you don't construct DevBridge yourself — `createPlatformSession({ dev: { … } })` does it for you.
314
+
315
+ ---
316
+
317
+ ## RTP Simulation CLI
318
+
319
+ `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.
320
+
321
+ ```bash
322
+ # 1M spins (default)
323
+ npx platform-core-simulate
324
+
325
+ # Buy-bonus stage
326
+ npx platform-core-simulate --action buy_bonus
327
+
328
+ # Ante bet
329
+ npx platform-core-simulate --params '{"ante_bet":true}'
330
+
331
+ # Custom: 5M iterations, fixed seed, custom config path
332
+ npx platform-core-simulate --iterations 5000000 --bet 1 --seed 42 --config ./dev.config.ts
333
+
334
+ # Force the JS runner (skip native binary)
335
+ npx platform-core-simulate --js
336
+ ```
337
+
338
+ 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.
339
+
340
+ Programmatic use:
341
+
342
+ ```typescript
343
+ import { ParallelSimulationRunner, NativeSimulationRunner, formatSimulationResult } from '@energy8platform/platform-core';
344
+
345
+ const runner = new ParallelSimulationRunner({
346
+ script, gameDefinition,
347
+ iterations: 1_000_000,
348
+ workers: 8,
349
+ });
350
+ const result = await runner.run();
351
+ console.log(formatSimulationResult(result));
352
+ ```
353
+
354
+ ---
355
+
356
+ ## Branded Loading Screen
357
+
358
+ Every Energy8 game shows the same brand frame while it boots. The CSS-only preloader lives here so any renderer hosts the same frame without needing to render anything itself.
359
+
360
+ ```typescript
361
+ import { createCSSPreloader, removeCSSPreloader } from '@energy8platform/platform-core/loading';
362
+
363
+ createCSSPreloader(document.getElementById('app')!, {
364
+ backgroundColor: 0x0a0a1a,
365
+ backgroundGradient: 'linear-gradient(135deg, #0a0a1a 0%, #1a1a3e 100%)',
366
+ cssPreloaderHTML: '<custom HTML to override the default frame>',
367
+ });
368
+
369
+ // later, when your renderer has mounted and assets are loaded:
370
+ removeCSSPreloader(container);
371
+ ```
372
+
373
+ The animated loader bar inside the SVG is purely CSS keyframes, so it works in offline / first-paint conditions before any JS module finishes parsing.
374
+
375
+ ---
376
+
377
+ ## Vite Plugins
378
+
379
+ ```typescript
380
+ // vite.config.ts (Phaser/Three/custom — full control over your config)
381
+ import { defineConfig } from 'vite';
382
+ import { devBridgePlugin, luaPlugin } from '@energy8platform/platform-core/vite';
383
+
384
+ export default defineConfig({
385
+ plugins: [
386
+ devBridgePlugin('./dev.config'),
387
+ luaPlugin('./dev.config'),
388
+ ],
389
+ });
390
+ ```
391
+
392
+ What they do:
393
+ - **`devBridgePlugin`** injects a virtual entry that boots `DevBridge` from your `./dev.config` *before* your real entry imports. Dev-only.
394
+ - **`luaPlugin`**:
395
+ 1. Lets you `import luaScript from './game.lua?raw'` — Vite returns the file contents.
396
+ 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.
397
+ 3. HMR-reloads the Lua engine when `*.lua` or `dev.config*` changes.
398
+
399
+ 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.).
400
+
401
+ ---
402
+
403
+ ## Asset Manifest type
404
+
405
+ `AssetManifest` describes "what to load and in which bundles", in a format both Pixi's `Assets`, `Phaser.Loader`, and your own loader can consume.
406
+
407
+ ```typescript
408
+ import type { AssetManifest } from '@energy8platform/platform-core';
409
+
410
+ const manifest: AssetManifest = {
411
+ bundles: [
412
+ { name: 'preload', assets: [{ alias: 'logo', src: 'logo.png' }] },
413
+ { name: 'game', assets: [
414
+ { alias: 'background', src: 'background.png' },
415
+ { alias: 'symbols', src: 'symbols.json' },
416
+ ]},
417
+ ],
418
+ };
419
+ ```
420
+
421
+ `platform-core` does **not** load the assets itself — actual loading is renderer-specific. Pixi-side, `game-engine`'s `AssetManager` wraps `pixi.Assets` and consumes this format directly.
422
+
423
+ ---
424
+
425
+ ## Pairing with another renderer
426
+
427
+ A typical Phaser / Three / custom-engine bootstrap looks like:
428
+
429
+ ```typescript
430
+ import {
431
+ createPlatformSession,
432
+ createCSSPreloader,
433
+ removeCSSPreloader,
434
+ type AssetManifest,
435
+ } from '@energy8platform/platform-core';
436
+
437
+ const container = document.getElementById('app')!;
438
+ createCSSPreloader(container);
439
+
440
+ const session = await createPlatformSession({
441
+ dev: { luaScript, gameDefinition, balance: 10000, currency: 'EUR' },
442
+ sdk: { devMode: true },
443
+ });
444
+
445
+ // 1. Read SDK init data for assetsUrl and config dimensions
446
+ const { assetsUrl } = session.initData ?? { assetsUrl: '/assets/' };
447
+
448
+ // 2. Boot YOUR renderer however it likes:
449
+ const game = new Phaser.Game({ /* … */ });
450
+ // 3. Load assets through Phaser's loader, treating `manifest` as
451
+ // the source of truth for what's needed.
452
+ await loadBundles(game.loader, manifest, assetsUrl);
453
+
454
+ removeCSSPreloader(container);
455
+
456
+ // 4. Wire SDK events / play requests
457
+ session.on('balanceUpdate', ({ balance }) => game.events.emit('balance', balance));
458
+ const result = await session.play({ action: 'spin', bet: 1 });
459
+ ```
460
+
461
+ Nothing in this code is Pixi-specific. The same pattern fits Three.js, Babylon, custom WebGL, or even a DOM-only game.
462
+
463
+ ---
464
+
465
+ ## Sub-path exports
466
+
467
+ | Path | What's there |
468
+ | --- | --- |
469
+ | `@energy8platform/platform-core` | Everything — re-exports from all sub-paths |
470
+ | `@energy8platform/platform-core/lua` | Browser-safe Lua engine surface: LuaEngine, ActionRouter, SessionManager, PersistentState, JS `SimulationRunner`, types |
471
+ | `@energy8platform/platform-core/simulation` | **Node-only.** `NativeSimulationRunner` (Go binary) and `ParallelSimulationRunner` (worker_threads). Don't import from a browser bundle — the main entry and `/lua` deliberately exclude these so they can't be tree-shake-leaked. |
472
+ | `@energy8platform/platform-core/dev-bridge` | `DevBridge`, `DevBridgeConfig` |
473
+ | `@energy8platform/platform-core/vite` | `devBridgePlugin`, `luaPlugin` |
474
+ | `@energy8platform/platform-core/loading` | `createCSSPreloader`, `removeCSSPreloader`, `buildLogoSVG`, `LOADER_BAR_MAX_WIDTH` |
475
+
476
+ 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.
477
+
478
+ ---
479
+
480
+ ## License
481
+
482
+ MIT
@@ -0,0 +1,139 @@
1
+ #!/usr/bin/env npx tsx
2
+ import { SimulationRunner, formatSimulationResult } from '../src/lua/SimulationRunner';
3
+ import { ParallelSimulationRunner } from '../src/simulation/ParallelSimulationRunner';
4
+ import { NativeSimulationRunner, findNativeBinary, formatNativeResult } from '../src/simulation/NativeSimulationRunner';
5
+ import { cpus } from 'os';
6
+ import { resolve, dirname } from 'path';
7
+ import { fileURLToPath } from 'url';
8
+
9
+ // ─── Argument Parsing ───────────────────────────────────
10
+
11
+ function parseArgs(argv: string[]): Record<string, string> {
12
+ const args: Record<string, string> = {};
13
+ for (let i = 2; i < argv.length; i++) {
14
+ const arg = argv[i];
15
+ if (arg.startsWith('--')) {
16
+ const key = arg.slice(2);
17
+ // Boolean flags (no value)
18
+ if (key === 'native' || key === 'js') {
19
+ args[key] = 'true';
20
+ } else if (i + 1 < argv.length) {
21
+ args[key] = argv[++i];
22
+ }
23
+ }
24
+ }
25
+ return args;
26
+ }
27
+
28
+ async function main() {
29
+ const args = parseArgs(process.argv);
30
+
31
+ const configPath = resolve(process.cwd(), args.config ?? './dev.config.ts');
32
+ const iterations = parseInt(args.iterations ?? '1000000', 10);
33
+ const bet = parseFloat(args.bet ?? '1');
34
+ const seed = args.seed ? parseInt(args.seed, 10) : undefined;
35
+ const action = args.action ?? 'spin';
36
+ const params = args.params ? JSON.parse(args.params) : undefined;
37
+ const workers = args.workers ? parseInt(args.workers, 10) : cpus().length;
38
+ const useNative = args.native === 'true';
39
+ const useJs = args.js === 'true';
40
+
41
+ // Load dev config
42
+ let config: any;
43
+ try {
44
+ const mod = await import(configPath);
45
+ config = mod.default ?? mod.config ?? mod;
46
+ } catch (e: any) {
47
+ console.error(`Failed to load config from ${configPath}:`);
48
+ console.error(e.message);
49
+ process.exit(1);
50
+ }
51
+
52
+ if (!config.luaScript) {
53
+ console.error('Config must contain `luaScript` (Lua source code string).');
54
+ console.error('Make sure your dev.config.ts exports luaScript and gameDefinition.');
55
+ process.exit(1);
56
+ }
57
+
58
+ if (!config.gameDefinition) {
59
+ console.error('Config must contain `gameDefinition` (GameDefinition object).');
60
+ process.exit(1);
61
+ }
62
+
63
+ const gameId = config.gameDefinition.id ?? 'unknown';
64
+
65
+ // ─── Native binary detection ────────────────────────────
66
+ // Search in config dir first, then in the game-engine package root
67
+ const engineRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
68
+ const binaryPath = args.binary ?? (useJs ? null : (findNativeBinary(dirname(configPath)) ?? findNativeBinary(engineRoot)));
69
+
70
+ if (useNative && !binaryPath) {
71
+ console.error('Native simulation binary not found.');
72
+ console.error('Use --binary <path> or set SIMULATE_BINARY environment variable.');
73
+ process.exit(1);
74
+ }
75
+
76
+ const onProgress = (completed: number, total: number) => {
77
+ const pct = Math.round((completed / total) * 100);
78
+ console.log(`Progress: ${completed.toLocaleString()}/${total.toLocaleString()} (${pct}%)`);
79
+ };
80
+
81
+ // ─── Native binary path ─────────────────────────────────
82
+ if (binaryPath) {
83
+ console.log(`Using native binary: ${binaryPath}`);
84
+ console.log(`Starting simulation for ${gameId} (${iterations.toLocaleString()} iterations, action: ${action})...`);
85
+
86
+ const runner = new NativeSimulationRunner({
87
+ binaryPath,
88
+ script: config.luaScript,
89
+ gameDefinition: config.gameDefinition,
90
+ iterations,
91
+ bet,
92
+ action,
93
+ params,
94
+ });
95
+ const result = await runner.run();
96
+ console.log(formatNativeResult(result));
97
+ return;
98
+ }
99
+
100
+ // ─── JS simulation path ─────────────────────────────────
101
+ const useParallel = workers > 1;
102
+ console.log(`Starting simulation for ${gameId} (${iterations.toLocaleString()} iterations, action: ${action}, workers: ${useParallel ? workers : 1})...`);
103
+
104
+ let result;
105
+
106
+ if (useParallel) {
107
+ const runner = new ParallelSimulationRunner({
108
+ script: config.luaScript,
109
+ gameDefinition: config.gameDefinition,
110
+ iterations,
111
+ bet,
112
+ seed,
113
+ action,
114
+ params,
115
+ workerCount: workers,
116
+ onProgress,
117
+ });
118
+ result = await runner.run();
119
+ } else {
120
+ const runner = new SimulationRunner({
121
+ script: config.luaScript,
122
+ gameDefinition: config.gameDefinition,
123
+ iterations,
124
+ bet,
125
+ seed,
126
+ action,
127
+ params,
128
+ onProgress,
129
+ });
130
+ result = runner.run();
131
+ }
132
+
133
+ console.log(formatSimulationResult(result));
134
+ }
135
+
136
+ main().catch((err) => {
137
+ console.error(err);
138
+ process.exit(1);
139
+ });