@energy8platform/platform-core 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +482 -0
- package/bin/simulate.ts +139 -0
- package/dist/dev-bridge.cjs.js +237 -0
- package/dist/dev-bridge.cjs.js.map +1 -0
- package/dist/dev-bridge.d.ts +141 -0
- package/dist/dev-bridge.esm.js +235 -0
- package/dist/dev-bridge.esm.js.map +1 -0
- package/dist/index.cjs.js +569 -0
- package/dist/index.cjs.js.map +1 -0
- package/dist/index.d.ts +439 -0
- package/dist/index.esm.js +560 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/loading.cjs.js +190 -0
- package/dist/loading.cjs.js.map +1 -0
- package/dist/loading.d.ts +86 -0
- package/dist/loading.esm.js +185 -0
- package/dist/loading.esm.js.map +1 -0
- package/dist/lua.cjs.js +1129 -0
- package/dist/lua.cjs.js.map +1 -0
- package/dist/lua.d.ts +319 -0
- package/dist/lua.esm.js +1119 -0
- package/dist/lua.esm.js.map +1 -0
- package/dist/simulation.cjs.js +374 -0
- package/dist/simulation.cjs.js.map +1 -0
- package/dist/simulation.d.ts +190 -0
- package/dist/simulation.esm.js +368 -0
- package/dist/simulation.esm.js.map +1 -0
- package/dist/vite.cjs.js +179 -0
- package/dist/vite.cjs.js.map +1 -0
- package/dist/vite.d.ts +13 -0
- package/dist/vite.esm.js +176 -0
- package/dist/vite.esm.js.map +1 -0
- package/package.json +100 -0
- package/scripts/install-simulate.mjs +101 -0
- package/src/EventEmitter.ts +55 -0
- package/src/PlatformSession.ts +156 -0
- package/src/dev-bridge/DevBridge.ts +305 -0
- package/src/dev-bridge/index.ts +2 -0
- package/src/index.ts +98 -0
- package/src/loading/CSSPreloader.ts +129 -0
- package/src/loading/index.ts +3 -0
- package/src/loading/logo.ts +95 -0
- package/src/lua/ActionRouter.ts +132 -0
- package/src/lua/LuaEngine.ts +412 -0
- package/src/lua/LuaEngineAPI.ts +314 -0
- package/src/lua/PersistentState.ts +80 -0
- package/src/lua/SessionManager.ts +227 -0
- package/src/lua/SimulationRunner.ts +192 -0
- package/src/lua/fengari.d.ts +10 -0
- package/src/lua/index.ts +28 -0
- package/src/lua/types.ts +149 -0
- package/src/simulation/NativeSimulationRunner.ts +367 -0
- package/src/simulation/ParallelSimulationRunner.ts +156 -0
- package/src/simulation/SimulationWorker.ts +44 -0
- package/src/simulation/index.ts +21 -0
- package/src/types.ts +85 -0
- package/src/vite/index.ts +196 -0
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
|
package/bin/simulate.ts
ADDED
|
@@ -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
|
+
});
|