@energy8platform/platform-core 0.28.3 → 0.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +127 -150
- package/bin/simulate.ts +35 -98
- package/dist/dev-bridge.cjs.js +15 -12
- package/dist/dev-bridge.cjs.js.map +1 -1
- package/dist/dev-bridge.d.ts +4 -3
- package/dist/dev-bridge.esm.js +15 -12
- package/dist/dev-bridge.esm.js.map +1 -1
- package/dist/game-spec.cjs.js +59 -59
- package/dist/game-spec.cjs.js.map +1 -1
- package/dist/game-spec.d.ts +24 -23
- package/dist/game-spec.esm.js +57 -56
- package/dist/game-spec.esm.js.map +1 -1
- package/dist/index.cjs.js +15 -12
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +23 -3
- package/dist/index.esm.js +15 -12
- package/dist/index.esm.js.map +1 -1
- package/dist/lua.cjs.js +0 -1234
- package/dist/lua.cjs.js.map +1 -1
- package/dist/lua.d.ts +1 -206
- package/dist/lua.esm.js +0 -1225
- package/dist/lua.esm.js.map +1 -1
- package/dist/simulation.cjs.js +48 -179
- package/dist/simulation.cjs.js.map +1 -1
- package/dist/simulation.d.ts +26 -60
- package/dist/simulation.esm.js +48 -178
- package/dist/simulation.esm.js.map +1 -1
- package/dist/vite.cjs.js +329 -109
- package/dist/vite.cjs.js.map +1 -1
- package/dist/vite.d.ts +19 -9
- package/dist/vite.esm.js +328 -109
- package/dist/vite.esm.js.map +1 -1
- package/package.json +6 -5
- package/scripts/install-e8.mjs +113 -0
- package/src/dev-bridge/DevBridge.ts +39 -23
- package/src/game-spec/defineGame.ts +2 -2
- package/src/game-spec/derive.ts +39 -16
- package/src/game-spec/export.ts +23 -39
- package/src/game-spec/index.ts +3 -3
- package/src/game-spec/types.ts +2 -1
- package/src/index.ts +6 -12
- package/src/lua/index.ts +4 -11
- package/src/simulation/NativeSimulationRunner.ts +71 -45
- package/src/simulation/index.ts +2 -4
- package/src/vite/index.ts +4 -121
- package/src/vite/spinPlugin.ts +344 -0
- package/scripts/install-simulate.mjs +0 -101
- package/src/lua/ActionRouter.ts +0 -132
- package/src/lua/LuaEngine.ts +0 -520
- package/src/lua/LuaEngineAPI.ts +0 -314
- package/src/lua/PersistentState.ts +0 -80
- package/src/lua/SessionManager.ts +0 -249
- package/src/lua/SimulationRunner.ts +0 -190
- package/src/lua/fengari.d.ts +0 -10
- package/src/simulation/ParallelSimulationRunner.ts +0 -156
- package/src/simulation/SimulationWorker.ts +0 -44
|
@@ -81,11 +81,16 @@ function parseSessionTtl(ttl: string | undefined): number {
|
|
|
81
81
|
if (!m) return DEFAULT_SESSION_TTL_MS;
|
|
82
82
|
const n = parseFloat(m[1]);
|
|
83
83
|
switch (m[2]) {
|
|
84
|
-
case 'ms':
|
|
85
|
-
|
|
86
|
-
case '
|
|
87
|
-
|
|
88
|
-
|
|
84
|
+
case 'ms':
|
|
85
|
+
return n;
|
|
86
|
+
case 's':
|
|
87
|
+
return n * 1000;
|
|
88
|
+
case 'm':
|
|
89
|
+
return n * 60 * 1000;
|
|
90
|
+
case 'h':
|
|
91
|
+
return n * 60 * 60 * 1000;
|
|
92
|
+
default:
|
|
93
|
+
return DEFAULT_SESSION_TTL_MS;
|
|
89
94
|
}
|
|
90
95
|
}
|
|
91
96
|
|
|
@@ -124,7 +129,8 @@ export interface DevBridgeConfig {
|
|
|
124
129
|
currency?: string;
|
|
125
130
|
/** Game config */
|
|
126
131
|
gameConfig?: Partial<GameConfigData>;
|
|
127
|
-
/** Base URL for assets (default: '/
|
|
132
|
+
/** Base URL for assets (default: '/' — the site root; the folder lives in the asset paths, not
|
|
133
|
+
* the base, so a game isn't forced to name its folder `assets`). */
|
|
128
134
|
assetsUrl?: string;
|
|
129
135
|
/** Active session to resume (null = no active session) */
|
|
130
136
|
session?: SessionData | null;
|
|
@@ -148,7 +154,10 @@ export interface DevBridgeConfig {
|
|
|
148
154
|
replay?: ReplayConfig;
|
|
149
155
|
}
|
|
150
156
|
|
|
151
|
-
const DEFAULT_CONFIG: Omit<
|
|
157
|
+
const DEFAULT_CONFIG: Omit<
|
|
158
|
+
Required<DevBridgeConfig>,
|
|
159
|
+
'luaScript' | 'gameDefinition' | 'luaSeed' | 'replay'
|
|
160
|
+
> = {
|
|
152
161
|
balance: 10000,
|
|
153
162
|
currency: 'USD',
|
|
154
163
|
gameConfig: {
|
|
@@ -158,7 +167,7 @@ const DEFAULT_CONFIG: Omit<Required<DevBridgeConfig>, 'luaScript' | 'gameDefinit
|
|
|
158
167
|
viewport: { width: 1920, height: 1080 },
|
|
159
168
|
betLevels: [0.1, 0.2, 0.5, 1, 2, 5, 10, 20, 50],
|
|
160
169
|
},
|
|
161
|
-
assetsUrl: '/
|
|
170
|
+
assetsUrl: '/',
|
|
162
171
|
session: null,
|
|
163
172
|
onPlay: () => ({}),
|
|
164
173
|
networkDelay: 200,
|
|
@@ -173,7 +182,7 @@ const DEFAULT_CONFIG: Omit<Required<DevBridgeConfig>, 'luaScript' | 'gameDefinit
|
|
|
173
182
|
* the need for postMessage and iframes.
|
|
174
183
|
*
|
|
175
184
|
* When `luaScript` is set, play requests are sent to the Vite dev server
|
|
176
|
-
* which runs LuaEngine in Node.js — no
|
|
185
|
+
* which runs LuaEngine in Node.js — no math in the browser.
|
|
177
186
|
*
|
|
178
187
|
* @example
|
|
179
188
|
* ```ts
|
|
@@ -192,7 +201,20 @@ const DEFAULT_CONFIG: Omit<Required<DevBridgeConfig>, 'luaScript' | 'gameDefinit
|
|
|
192
201
|
* ```
|
|
193
202
|
*/
|
|
194
203
|
export class DevBridge {
|
|
195
|
-
private _config: Required<
|
|
204
|
+
private _config: Required<
|
|
205
|
+
Pick<
|
|
206
|
+
DevBridgeConfig,
|
|
207
|
+
| 'balance'
|
|
208
|
+
| 'currency'
|
|
209
|
+
| 'gameConfig'
|
|
210
|
+
| 'assetsUrl'
|
|
211
|
+
| 'session'
|
|
212
|
+
| 'onPlay'
|
|
213
|
+
| 'networkDelay'
|
|
214
|
+
| 'debug'
|
|
215
|
+
>
|
|
216
|
+
> &
|
|
217
|
+
Pick<DevBridgeConfig, 'luaScript' | 'gameDefinition' | 'luaSeed' | 'replay'>;
|
|
196
218
|
private _balance: number;
|
|
197
219
|
private _roundCounter = 0;
|
|
198
220
|
private _bridge: Bridge | null = null;
|
|
@@ -346,9 +368,7 @@ export class DevBridge {
|
|
|
346
368
|
private resolveReplayResults(): Promise<PlayResultData[]> {
|
|
347
369
|
if (!this._replayResults) {
|
|
348
370
|
const { mode, roundId } = this._replayLaunch ?? {};
|
|
349
|
-
this._replayResults = Promise.resolve(
|
|
350
|
-
this._config.replay!.resolve(mode, roundId),
|
|
351
|
-
);
|
|
371
|
+
this._replayResults = Promise.resolve(this._config.replay!.resolve(mode, roundId));
|
|
352
372
|
}
|
|
353
373
|
return this._replayResults;
|
|
354
374
|
}
|
|
@@ -382,10 +402,7 @@ export class DevBridge {
|
|
|
382
402
|
});
|
|
383
403
|
}
|
|
384
404
|
|
|
385
|
-
private handlePlayRequest(
|
|
386
|
-
payload: PlayParams,
|
|
387
|
-
id?: string,
|
|
388
|
-
): void {
|
|
405
|
+
private handlePlayRequest(payload: PlayParams, id?: string): void {
|
|
389
406
|
if (this._replayLaunch) {
|
|
390
407
|
this.handleReplayPlay(id);
|
|
391
408
|
return;
|
|
@@ -462,11 +479,9 @@ export class DevBridge {
|
|
|
462
479
|
// Round id rules mirror server's playRound:
|
|
463
480
|
// non-session → fresh UUID, client-supplied id is ignored
|
|
464
481
|
// session-based → reuse the active session's round id
|
|
465
|
-
const serverRoundId = actionDef.requires_session
|
|
466
|
-
? this._activeRoundId!
|
|
467
|
-
: generateRoundId();
|
|
482
|
+
const serverRoundId = actionDef.requires_session ? this._activeRoundId! : generateRoundId();
|
|
468
483
|
|
|
469
|
-
this.
|
|
484
|
+
this.executeOnServer({ action, bet, roundId: serverRoundId, params })
|
|
470
485
|
.then((result) => {
|
|
471
486
|
this._lastPlayResult = result;
|
|
472
487
|
this.updateSessionState(result);
|
|
@@ -481,7 +496,8 @@ export class DevBridge {
|
|
|
481
496
|
// Fallback to onPlay callback
|
|
482
497
|
const { roundId } = payload;
|
|
483
498
|
const customResult = this._config.onPlay({ action, bet, roundId, params });
|
|
484
|
-
const totalWin =
|
|
499
|
+
const totalWin =
|
|
500
|
+
customResult.totalWin ?? (Math.random() > 0.6 ? bet * (1 + Math.random() * 10) : 0);
|
|
485
501
|
|
|
486
502
|
this._balance += totalWin;
|
|
487
503
|
|
|
@@ -561,7 +577,7 @@ export class DevBridge {
|
|
|
561
577
|
return bet;
|
|
562
578
|
}
|
|
563
579
|
|
|
564
|
-
private async
|
|
580
|
+
private async executeOnServer(params: PlayParams): Promise<PlayResultData> {
|
|
565
581
|
const response = await fetch('/__lua-play', {
|
|
566
582
|
method: 'POST',
|
|
567
583
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import type { GameSpec, GameModel } from './types';
|
|
2
2
|
import { validateSpec } from './validate';
|
|
3
|
-
import { toGameDefinition,
|
|
3
|
+
import { toGameDefinition, toSpinPrelude, toModeMap, toMathModes, toPaytableView } from './derive';
|
|
4
4
|
|
|
5
5
|
export function defineGame(spec: GameSpec): GameModel {
|
|
6
6
|
validateSpec(spec);
|
|
7
7
|
return {
|
|
8
8
|
spec,
|
|
9
9
|
gameDefinition: toGameDefinition(spec),
|
|
10
|
-
|
|
10
|
+
spinPrelude: toSpinPrelude(spec),
|
|
11
11
|
modeMap: toModeMap(spec),
|
|
12
12
|
mathModes: toMathModes(spec),
|
|
13
13
|
paytable: toPaytableView(spec),
|
package/src/game-spec/derive.ts
CHANGED
|
@@ -76,30 +76,53 @@ export function toGameDefinition(spec: GameSpec): GameDefinition {
|
|
|
76
76
|
return def;
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
return
|
|
79
|
+
/** Число с гарантированной float-формой для SpinML (10 → "10.0"). */
|
|
80
|
+
function spinF(n: number): string {
|
|
81
|
+
return Number.isInteger(n) ? `${n}.0` : String(n);
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
|
|
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 {
|
|
85
91
|
const lines: string[] = ['-- AUTO-GENERATED from game.spec.ts — do not edit'];
|
|
86
|
-
lines.push(
|
|
92
|
+
lines.push(
|
|
93
|
+
`const SPEC = { cols: ${spec.grid.cols}, rows: ${spec.grid.rows}, cells: ${spec.grid.cols * spec.grid.rows}, max_win: ${spinF(spec.maxWin)} }`,
|
|
94
|
+
);
|
|
87
95
|
|
|
88
96
|
const symNames = spec.symbols.map((s) => `"${s.id}"`).join(', ');
|
|
89
|
-
lines.push(`SYMBOLS
|
|
97
|
+
lines.push(`const SYMBOLS: [str; ${spec.symbols.length}] = [${symNames}]`);
|
|
98
|
+
lines.push(`const N_SYMBOLS: int = ${spec.symbols.length}`);
|
|
90
99
|
|
|
91
|
-
const symIndex = spec.symbols.map((s, i) => `${s.id}
|
|
92
|
-
lines.push(
|
|
100
|
+
const symIndex = spec.symbols.map((s, i) => `${s.id}: ${i + 1}`).join(', ');
|
|
101
|
+
lines.push(`-- 1-based индексы символов (порядок SYMBOLS)`);
|
|
102
|
+
lines.push(`const SYM = { ${symIndex} }`);
|
|
93
103
|
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
+
}
|
|
98
117
|
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
+
}
|
|
103
126
|
|
|
104
127
|
return lines.join('\n') + '\n';
|
|
105
128
|
}
|
package/src/game-spec/export.ts
CHANGED
|
@@ -1,53 +1,37 @@
|
|
|
1
1
|
import type { GameSpec, GameModel } from './types';
|
|
2
2
|
import { defineGame } from './defineGame';
|
|
3
3
|
|
|
4
|
-
/** Compose the self-contained
|
|
5
|
-
export function
|
|
6
|
-
return model.
|
|
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;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
/** The
|
|
10
|
-
export interface
|
|
11
|
-
/** GameDefinition JSON —
|
|
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
12
|
'config.json': string;
|
|
13
|
-
/** Self-contained
|
|
14
|
-
'script.
|
|
13
|
+
/** Self-contained SpinML (prelude ⧺ math) — S3: games/{id}/script.spin. */
|
|
14
|
+
'script.spin': string;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
|
-
* Produce the
|
|
19
|
-
*
|
|
20
|
-
*
|
|
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
21
|
*/
|
|
22
|
-
export function
|
|
22
|
+
export function exportGameSpin(spec: GameSpec, opts: { logicSpin: string }): E8SpinBundle {
|
|
23
23
|
const model = defineGame(spec);
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
'
|
|
24
|
+
const config = {
|
|
25
|
+
...(model.gameDefinition as unknown as Record<string, unknown>),
|
|
26
|
+
engine_mode: 'spin',
|
|
27
|
+
script_path: 'script.spin',
|
|
27
28
|
};
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Structural (fengari-free) checks that catch the obvious ways an export is unusable before it
|
|
34
|
-
* reaches the platform: malformed config, missing `script_path`/`actions`, or a script with no
|
|
35
|
-
* `execute` entry point. A full boot check (running the script in a LuaEngine) is the caller's job.
|
|
36
|
-
*/
|
|
37
|
-
export function validateE8Bundle(bundle: E8Bundle): void {
|
|
38
|
-
let config: Record<string, unknown>;
|
|
39
|
-
try {
|
|
40
|
-
config = JSON.parse(bundle['config.json']);
|
|
41
|
-
} catch (e) {
|
|
42
|
-
throw new Error(`E8 export: config.json is not valid JSON — ${(e as Error).message}`);
|
|
43
|
-
}
|
|
44
|
-
if (!config.id || typeof config.id !== 'string') throw new Error('E8 export: config.json missing "id"');
|
|
45
|
-
if (config.type !== 'SLOT' && config.type !== 'TABLE') throw new Error('E8 export: config.json "type" must be SLOT or TABLE');
|
|
46
|
-
if (!config.script_path) throw new Error('E8 export: config.json missing "script_path"');
|
|
47
|
-
if (!config.actions || typeof config.actions !== 'object' || Object.keys(config.actions as object).length === 0) {
|
|
48
|
-
throw new Error('E8 export: config.json has no actions');
|
|
49
|
-
}
|
|
50
|
-
if (!/function\s+execute\s*\(/.test(bundle['script.lua'])) {
|
|
51
|
-
throw new Error('E8 export: script.lua does not define a global `execute(state)` function');
|
|
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(...)`');
|
|
52
32
|
}
|
|
33
|
+
return {
|
|
34
|
+
'config.json': JSON.stringify(config, null, 2),
|
|
35
|
+
'script.spin': script,
|
|
36
|
+
};
|
|
53
37
|
}
|
package/src/game-spec/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export * from './types';
|
|
2
2
|
export { validateSpec, GameSpecError } from './validate';
|
|
3
|
-
export { toGameDefinition,
|
|
3
|
+
export { toGameDefinition, toSpinPrelude, toModeMap, toMathModes, toPaytableView } from './derive';
|
|
4
4
|
export { defineGame } from './defineGame';
|
|
5
|
-
export {
|
|
6
|
-
export type {
|
|
5
|
+
export { buildSpinScript, exportGameSpin } from './export';
|
|
6
|
+
export type { E8SpinBundle } from './export';
|
|
7
7
|
export type { GameDefinition, ActionDefinition, TransitionRule, MaxWinConfig } from '../lua/types';
|
package/src/game-spec/types.ts
CHANGED
|
@@ -85,7 +85,8 @@ export interface PaytableView {
|
|
|
85
85
|
export interface GameModel {
|
|
86
86
|
spec: GameSpec;
|
|
87
87
|
gameDefinition: GameDefinition;
|
|
88
|
-
|
|
88
|
+
/** Generated .spin prelude (SPEC/SYM/PAYTABLE consts) for the spin runtime. */
|
|
89
|
+
spinPrelude: string;
|
|
89
90
|
modeMap: Record<string, string>;
|
|
90
91
|
mathModes: MathModeSpec[];
|
|
91
92
|
paytable: PaytableView;
|
package/src/index.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* engine.
|
|
6
6
|
*
|
|
7
7
|
* Sub-paths for fine-grained imports:
|
|
8
|
-
* - `@energy8platform/platform-core/lua` —
|
|
8
|
+
* - `@energy8platform/platform-core/lua` — shared game-definition types
|
|
9
9
|
* - `@energy8platform/platform-core/dev-bridge` — DevBridge mock host
|
|
10
10
|
* - `@energy8platform/platform-core/vite` — Vite plugins
|
|
11
11
|
*/
|
|
@@ -21,17 +21,11 @@ export type {
|
|
|
21
21
|
SDKOptions,
|
|
22
22
|
} from './PlatformSession';
|
|
23
23
|
|
|
24
|
-
// ───
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
// consumer that doesn't actually use Lua in the browser. Use:
|
|
30
|
-
//
|
|
31
|
-
// import { LuaEngine } from '@energy8platform/platform-core/lua';
|
|
32
|
-
//
|
|
33
|
-
// For Node-only RTP simulation (Go binary, worker_threads), import from
|
|
34
|
-
// '@energy8platform/platform-core/simulation' instead.
|
|
24
|
+
// ─── Math runtime ───────────────────────────────────────
|
|
25
|
+
// The math runtime is SpinML (the Rust e8 engine): dev rounds via the
|
|
26
|
+
// `spinPlugin` vite plugin + e8-server, simulation via `e8 simulate`
|
|
27
|
+
// (see '@energy8platform/platform-core/simulation'). The fengari Lua
|
|
28
|
+
// engine was removed — legacy Lua games stay on <= 0.28.x.
|
|
35
29
|
|
|
36
30
|
// ─── DevBridge ──────────────────────────────────────────
|
|
37
31
|
export { DevBridge } from './dev-bridge';
|
package/src/lua/index.ts
CHANGED
|
@@ -1,14 +1,7 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// `@energy8platform/platform-core/simulation` instead.
|
|
6
|
-
export { LuaEngine } from './LuaEngine';
|
|
7
|
-
export { LuaEngineAPI, createSeededRng } from './LuaEngineAPI';
|
|
8
|
-
export { ActionRouter, evaluateCondition } from './ActionRouter';
|
|
9
|
-
export { SessionManager } from './SessionManager';
|
|
10
|
-
export { PersistentState } from './PersistentState';
|
|
11
|
-
export { SimulationRunner, formatSimulationResult } from './SimulationRunner';
|
|
1
|
+
// Shared game-definition types. The fengari Lua engine that used to live
|
|
2
|
+
// here is GONE — the math runtime is SpinML (e8): dev rounds via spinPlugin
|
|
3
|
+
// + e8-server, simulation via `e8 simulate` (stake-math-tools). Legacy Lua
|
|
4
|
+
// games stay on platform-core <= 0.28.x.
|
|
12
5
|
export type {
|
|
13
6
|
GameDefinition,
|
|
14
7
|
ActionDefinition,
|
|
@@ -28,6 +28,17 @@ export interface NativeSimulationConfig {
|
|
|
28
28
|
binaryPath: string;
|
|
29
29
|
/** Lua script source code */
|
|
30
30
|
script: string;
|
|
31
|
+
/**
|
|
32
|
+
* Extension of the temp script file (default 'lua'). The e8 SpinML engine
|
|
33
|
+
* takes 'spin' — the runner is otherwise runtime-agnostic: it writes the
|
|
34
|
+
* script, points config.script_path at it and spawns the binary.
|
|
35
|
+
*/
|
|
36
|
+
scriptExt?: 'lua' | 'spin';
|
|
37
|
+
/**
|
|
38
|
+
* Args prepended before the flag args (default none). The e8 binary is a
|
|
39
|
+
* multi-command CLI — its Go-compatible mode is the `simulate` subcommand.
|
|
40
|
+
*/
|
|
41
|
+
argsPrefix?: string[];
|
|
31
42
|
/** Platform game definition */
|
|
32
43
|
gameDefinition: GameDefinition;
|
|
33
44
|
/** Number of iterations */
|
|
@@ -100,6 +111,14 @@ export interface NativeSimulationResult extends SimulationResult {
|
|
|
100
111
|
workerSeeds?: string[];
|
|
101
112
|
/** Echo of replay params when the run was in replay mode. */
|
|
102
113
|
replay?: NativeReplayParams;
|
|
114
|
+
/** Round-win standard deviation (currency units). */
|
|
115
|
+
stddev?: number;
|
|
116
|
+
/** Coefficient of variation (stddev / mean round win). */
|
|
117
|
+
cv?: number;
|
|
118
|
+
/** Volatility score 0..10 (casino_platform classifyVolatility buckets). */
|
|
119
|
+
volatility?: number;
|
|
120
|
+
/** Volatility tier label (Low … Extreme). */
|
|
121
|
+
volatilityLabel?: string;
|
|
103
122
|
}
|
|
104
123
|
|
|
105
124
|
// ─── Go JSON output shape (snake_case) ──────────────────
|
|
@@ -136,6 +155,10 @@ interface GoSimulationOutput {
|
|
|
136
155
|
rng_kind?: NativeRNGKind;
|
|
137
156
|
master_seed?: string;
|
|
138
157
|
worker_seeds?: string[];
|
|
158
|
+
stddev?: number;
|
|
159
|
+
cv?: number;
|
|
160
|
+
volatility?: number;
|
|
161
|
+
volatility_label?: string;
|
|
139
162
|
replay?: {
|
|
140
163
|
server_seed: string;
|
|
141
164
|
client_seed: string;
|
|
@@ -190,7 +213,7 @@ export class NativeSimulationRunner {
|
|
|
190
213
|
}
|
|
191
214
|
|
|
192
215
|
async run(): Promise<NativeSimulationResult> {
|
|
193
|
-
const { binaryPath, script, gameDefinition, iterations, bet, action, params, seed, rng, replay, dump } = this.config;
|
|
216
|
+
const { binaryPath, script, scriptExt, argsPrefix, gameDefinition, iterations, bet, action, params, seed, rng, replay, dump } = this.config;
|
|
194
217
|
|
|
195
218
|
if (replay && rng && rng !== 'provably-fair') {
|
|
196
219
|
throw new Error(`Replay mode requires rng="provably-fair" (got rng="${rng}")`);
|
|
@@ -198,28 +221,28 @@ export class NativeSimulationRunner {
|
|
|
198
221
|
|
|
199
222
|
const id = randomBytes(8).toString('hex');
|
|
200
223
|
const tmpDir = tmpdir();
|
|
201
|
-
const
|
|
224
|
+
const scriptPath = join(tmpDir, `sim-${id}.${scriptExt ?? 'lua'}`);
|
|
202
225
|
const configPath = join(tmpDir, `sim-${id}.json`);
|
|
203
226
|
|
|
204
227
|
try {
|
|
205
228
|
// Write temp files
|
|
206
229
|
await Promise.all([
|
|
207
|
-
writeFile(
|
|
208
|
-
writeFile(configPath, JSON.stringify({ ...gameDefinition, script_path:
|
|
230
|
+
writeFile(scriptPath, script, 'utf-8'),
|
|
231
|
+
writeFile(configPath, JSON.stringify({ ...gameDefinition, script_path: scriptPath }), 'utf-8'),
|
|
209
232
|
]);
|
|
210
233
|
|
|
211
234
|
// Build CLI args
|
|
212
235
|
const args = buildNativeArgs({ configPath, iterations, bet, action, params, rng, seed, dump, replay });
|
|
213
236
|
|
|
214
237
|
// Execute binary
|
|
215
|
-
const output = await this.exec(binaryPath, args);
|
|
238
|
+
const output = await this.exec(binaryPath, [...(argsPrefix ?? []), ...args]);
|
|
216
239
|
|
|
217
240
|
// Parse JSON output
|
|
218
241
|
const json: GoSimulationOutput = JSON.parse(output);
|
|
219
242
|
return mapGoResult(json);
|
|
220
243
|
} finally {
|
|
221
244
|
// Cleanup temp files
|
|
222
|
-
await Promise.allSettled([unlink(
|
|
245
|
+
await Promise.allSettled([unlink(scriptPath), unlink(configPath)]);
|
|
223
246
|
}
|
|
224
247
|
}
|
|
225
248
|
|
|
@@ -298,6 +321,10 @@ function mapGoResult(json: GoSimulationOutput): NativeSimulationResult {
|
|
|
298
321
|
rngKind: json.rng_kind,
|
|
299
322
|
masterSeed: json.master_seed,
|
|
300
323
|
workerSeeds: json.worker_seeds,
|
|
324
|
+
stddev: json.stddev,
|
|
325
|
+
cv: json.cv,
|
|
326
|
+
volatility: json.volatility,
|
|
327
|
+
volatilityLabel: json.volatility_label,
|
|
301
328
|
replay: json.replay
|
|
302
329
|
? {
|
|
303
330
|
serverSeed: json.replay.server_seed,
|
|
@@ -318,42 +345,40 @@ function mapGoResult(json: GoSimulationOutput): NativeSimulationResult {
|
|
|
318
345
|
// ─── Binary discovery ───────────────────────────────────
|
|
319
346
|
|
|
320
347
|
/**
|
|
321
|
-
* Search for
|
|
322
|
-
*
|
|
348
|
+
* Search for the e8 SpinML engine binary (Rust) — same lookup discipline as
|
|
349
|
+
* findNativeBinary: env override → <pkg>/bin/e8-<platform>-<arch> → $PATH.
|
|
350
|
+
* Fetched by scripts/install-e8.mjs on postinstall.
|
|
323
351
|
*/
|
|
324
|
-
export function
|
|
325
|
-
|
|
326
|
-
const envPath = process.env.SIMULATE_BINARY;
|
|
352
|
+
export function findE8Binary(baseDir?: string): string | null {
|
|
353
|
+
const envPath = process.env.E8_BINARY;
|
|
327
354
|
if (envPath && isExecutable(envPath)) {
|
|
328
355
|
return envPath;
|
|
329
356
|
}
|
|
330
357
|
|
|
331
|
-
const platform = process.platform;
|
|
332
|
-
const nodeArch = process.arch;
|
|
358
|
+
const platform = process.platform;
|
|
359
|
+
const nodeArch = process.arch;
|
|
333
360
|
const goArch = nodeArch === 'x64' ? 'amd64' : nodeArch;
|
|
334
361
|
const goPlatform = platform === 'win32' ? 'windows' : platform;
|
|
335
362
|
const ext = platform === 'win32' ? '.exe' : '';
|
|
336
363
|
|
|
337
|
-
const names = [
|
|
338
|
-
`simulate-${goPlatform}-${goArch}${ext}`,
|
|
339
|
-
`simulation-${goPlatform}-${goArch}${ext}`,
|
|
340
|
-
`simulate${ext}`,
|
|
341
|
-
`simulation${ext}`,
|
|
342
|
-
];
|
|
364
|
+
const names = [`e8-${goPlatform}-${goArch}${ext}`, `e8${ext}`];
|
|
343
365
|
|
|
344
|
-
// Search directories: user's project first, then this package's bin/
|
|
345
366
|
const searchDirs: string[] = [];
|
|
346
367
|
if (baseDir) searchDirs.push(baseDir);
|
|
347
|
-
|
|
348
|
-
//
|
|
368
|
+
// Раскладки различаются: dist/simulation.esm.js → ../bin,
|
|
369
|
+
// src/simulation/*.ts (vitest/tsx) → ../../bin. Пробуем оба уровня.
|
|
349
370
|
try {
|
|
350
|
-
const
|
|
351
|
-
|
|
371
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
372
|
+
for (const up of ['..', '../..']) {
|
|
373
|
+
const root = join(here, up);
|
|
374
|
+
if (!searchDirs.includes(root)) searchDirs.push(root);
|
|
375
|
+
}
|
|
352
376
|
} catch {
|
|
353
|
-
// fallback for CJS
|
|
354
377
|
if (typeof __dirname !== 'undefined') {
|
|
355
|
-
const
|
|
356
|
-
|
|
378
|
+
for (const up of ['..', '../..']) {
|
|
379
|
+
const root = join(__dirname, up);
|
|
380
|
+
if (!searchDirs.includes(root)) searchDirs.push(root);
|
|
381
|
+
}
|
|
357
382
|
}
|
|
358
383
|
}
|
|
359
384
|
|
|
@@ -364,32 +389,24 @@ export function findNativeBinary(baseDir?: string): string | null {
|
|
|
364
389
|
}
|
|
365
390
|
}
|
|
366
391
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
} catch {
|
|
374
|
-
// not found
|
|
375
|
-
}
|
|
392
|
+
try {
|
|
393
|
+
const cmd = platform === 'win32' ? 'where e8' : 'which e8';
|
|
394
|
+
const result = execSync(cmd, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'] }).trim();
|
|
395
|
+
if (result) return result.split('\n')[0];
|
|
396
|
+
} catch {
|
|
397
|
+
// not found
|
|
376
398
|
}
|
|
377
399
|
|
|
378
400
|
return null;
|
|
379
401
|
}
|
|
380
402
|
|
|
381
|
-
/**
|
|
382
|
-
|
|
383
|
-
* The math pipeline is go-native only — NO JS fallback.
|
|
384
|
-
*
|
|
385
|
-
* @param finder - injectable finder for testability; defaults to findNativeBinary.
|
|
386
|
-
*/
|
|
387
|
-
export function requireNativeBinary(finder: () => string | null = findNativeBinary): string {
|
|
403
|
+
/** Return the e8 binary path or throw an install-guiding error. */
|
|
404
|
+
export function requireE8Binary(finder: () => string | null = findE8Binary): string {
|
|
388
405
|
const bin = finder();
|
|
389
406
|
if (!bin) {
|
|
390
407
|
throw new Error(
|
|
391
|
-
'
|
|
392
|
-
'
|
|
408
|
+
'e8 engine binary not found — run `npm install` (platform-core fetches it via install-e8), ' +
|
|
409
|
+
'or point E8_BINARY at a local build (casino-platform/e8/target/release/e8).',
|
|
393
410
|
);
|
|
394
411
|
}
|
|
395
412
|
return bin;
|
|
@@ -445,6 +462,15 @@ export function formatNativeResult(result: NativeSimulationResult): string {
|
|
|
445
462
|
`Max Win Cap Hits: ${result.maxWinHits}`,
|
|
446
463
|
);
|
|
447
464
|
|
|
465
|
+
if (result.volatilityLabel !== undefined && result.cv !== undefined) {
|
|
466
|
+
lines.push(
|
|
467
|
+
'',
|
|
468
|
+
'--- Volatility ---',
|
|
469
|
+
`Volatility: ${result.volatility}/10 (${result.volatilityLabel})`,
|
|
470
|
+
`StdDev: ${(result.stddev ?? 0).toFixed(2)} CV: ${result.cv.toFixed(2)}`,
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
|
|
448
474
|
if (result.bonusTriggered > 0) {
|
|
449
475
|
const frequency = Math.round(result.iterations / result.bonusTriggered);
|
|
450
476
|
lines.push(
|
package/src/simulation/index.ts
CHANGED
|
@@ -8,10 +8,10 @@
|
|
|
8
8
|
|
|
9
9
|
export {
|
|
10
10
|
NativeSimulationRunner,
|
|
11
|
-
findNativeBinary,
|
|
12
11
|
formatNativeResult,
|
|
13
12
|
buildNativeArgs,
|
|
14
|
-
|
|
13
|
+
findE8Binary,
|
|
14
|
+
requireE8Binary,
|
|
15
15
|
} from './NativeSimulationRunner';
|
|
16
16
|
export type {
|
|
17
17
|
NativeSimulationConfig,
|
|
@@ -22,5 +22,3 @@ export type {
|
|
|
22
22
|
DistributionBucket,
|
|
23
23
|
NativeArgsInput,
|
|
24
24
|
} from './NativeSimulationRunner';
|
|
25
|
-
|
|
26
|
-
export { ParallelSimulationRunner } from './ParallelSimulationRunner';
|