@energy8platform/platform-core 0.24.5 → 0.25.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/dist/game-spec.cjs.js +209 -0
- package/dist/game-spec.cjs.js.map +1 -0
- package/dist/game-spec.d.ts +164 -0
- package/dist/game-spec.esm.js +198 -0
- package/dist/game-spec.esm.js.map +1 -0
- package/dist/index.cjs.js +67 -3
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +52 -1
- package/dist/index.esm.js +67 -3
- package/dist/index.esm.js.map +1 -1
- package/dist/lua.cjs.js +5 -2
- package/dist/lua.cjs.js.map +1 -1
- package/dist/lua.d.ts +9 -0
- package/dist/lua.esm.js +5 -2
- package/dist/lua.esm.js.map +1 -1
- package/dist/shell.cjs.js +45 -3
- package/dist/shell.cjs.js.map +1 -1
- package/dist/shell.d.ts +24 -1
- package/dist/shell.esm.js +45 -4
- package/dist/shell.esm.js.map +1 -1
- package/dist/simulation.cjs.js +40 -22
- package/dist/simulation.cjs.js.map +1 -1
- package/dist/simulation.d.ts +35 -2
- package/dist/simulation.esm.js +39 -23
- package/dist/simulation.esm.js.map +1 -1
- package/dist/slot-result.cjs.js +17 -0
- package/dist/slot-result.cjs.js.map +1 -0
- package/dist/slot-result.d.ts +26 -0
- package/dist/slot-result.esm.js +14 -0
- package/dist/slot-result.esm.js.map +1 -0
- package/package.json +12 -1
- package/scripts/gen-version.mjs +21 -0
- package/src/PlatformSession.ts +28 -0
- package/src/game-spec/defineGame.ts +16 -0
- package/src/game-spec/derive.ts +135 -0
- package/src/game-spec/export.ts +17 -0
- package/src/game-spec/index.ts +6 -0
- package/src/game-spec/types.ts +81 -0
- package/src/game-spec/validate.ts +49 -0
- package/src/lua/LuaEngine.ts +5 -2
- package/src/lua/types.ts +8 -0
- package/src/shell/GameShell.ts +24 -2
- package/src/shell/components/BottomBar.ts +3 -2
- package/src/shell/components/GameInfo.ts +13 -0
- package/src/shell/index.ts +1 -0
- package/src/shell/shell.css.ts +2 -0
- package/src/shell/state.ts +1 -0
- package/src/shell/types.ts +11 -0
- package/src/shell/version.ts +3 -0
- package/src/simulation/NativeSimulationRunner.ts +62 -26
- package/src/simulation/index.ts +3 -0
- package/src/slot-result/coerce.ts +11 -0
- package/src/slot-result/index.ts +2 -0
- package/src/slot-result/types.ts +19 -0
package/src/lua/LuaEngine.ts
CHANGED
|
@@ -36,6 +36,7 @@ export class LuaEngine {
|
|
|
36
36
|
private gameDefinition: GameDefinition;
|
|
37
37
|
private variables: Record<string, number> = {};
|
|
38
38
|
private simulationMode: boolean;
|
|
39
|
+
private allowSessionlessActions: boolean;
|
|
39
40
|
/** Reusable state objects to avoid per-iteration allocation */
|
|
40
41
|
private _stateVars: Record<string, number> = {};
|
|
41
42
|
private _stateParams: Record<string, unknown> = {};
|
|
@@ -43,6 +44,7 @@ export class LuaEngine {
|
|
|
43
44
|
constructor(config: LuaEngineConfig) {
|
|
44
45
|
this.gameDefinition = config.gameDefinition;
|
|
45
46
|
this.simulationMode = config.simulationMode ?? false;
|
|
47
|
+
this.allowSessionlessActions = config.allowSessionlessActions ?? false;
|
|
46
48
|
|
|
47
49
|
const rng = config.seed !== undefined
|
|
48
50
|
? createSeededRng(config.seed)
|
|
@@ -93,10 +95,11 @@ export class LuaEngine {
|
|
|
93
95
|
execute(params: PlayParams): LuaPlayResult {
|
|
94
96
|
const { action: actionName, params: clientParams } = params;
|
|
95
97
|
|
|
96
|
-
// 1. Resolve action
|
|
98
|
+
// 1. Resolve action. In the harness, `allowSessionlessActions` lets a standalone free_spin
|
|
99
|
+
// (replayed after a books-path bonus buy that never created an engine session) run anyway.
|
|
97
100
|
const action = this.actionRouter.resolveAction(
|
|
98
101
|
actionName,
|
|
99
|
-
this.sessionManager.isActive,
|
|
102
|
+
this.sessionManager.isActive || this.allowSessionlessActions,
|
|
100
103
|
);
|
|
101
104
|
|
|
102
105
|
// 2. Determine bet — server uses session bet for session actions
|
package/src/lua/types.ts
CHANGED
|
@@ -88,6 +88,14 @@ export interface LuaEngineConfig {
|
|
|
88
88
|
logger?: (level: string, msg: string) => void;
|
|
89
89
|
/** Skip marshalling data fields (matrix, wins, etc.) for faster simulation */
|
|
90
90
|
simulationMode?: boolean;
|
|
91
|
+
/**
|
|
92
|
+
* Allow `requires_session` actions (e.g. `free_spin`) to run even with no active session.
|
|
93
|
+
* Default false (server-faithful). The dev harness sets this true: when a bonus is bought
|
|
94
|
+
* through the BOOKS path the LuaEngine never created a session, yet the scaffold then replays
|
|
95
|
+
* `free_spin` (which has no books) via the Lua fallback — without this it would throw
|
|
96
|
+
* "Action free_spin requires an active session".
|
|
97
|
+
*/
|
|
98
|
+
allowSessionlessActions?: boolean;
|
|
91
99
|
}
|
|
92
100
|
|
|
93
101
|
export interface LuaPlayResult {
|
package/src/shell/GameShell.ts
CHANGED
|
@@ -64,6 +64,10 @@ export class GameShell extends EventEmitter<ShellEvents> {
|
|
|
64
64
|
this.observeLayout();
|
|
65
65
|
if (typeof document !== 'undefined') {
|
|
66
66
|
document.addEventListener('keydown', this.handleKeyDown);
|
|
67
|
+
// Stake serves the game in an iframe; on first paint focus is on the HOST page, so a `document`
|
|
68
|
+
// keydown never fires and Space scrolls the parent. Pull window focus into the iframe on the
|
|
69
|
+
// first pointer interaction so the spacebar shortcut works. Harmless on full-page Energy8.
|
|
70
|
+
document.addEventListener('pointerdown', this.pullFocus, true);
|
|
67
71
|
this.keysBound = true;
|
|
68
72
|
}
|
|
69
73
|
this.render();
|
|
@@ -139,6 +143,10 @@ export class GameShell extends EventEmitter<ShellEvents> {
|
|
|
139
143
|
* false, while a spin is running, while autoplay is active, outside base mode, when an
|
|
140
144
|
* overlay/modal is open, or when an editable element is focused. `repeat` (held key) is
|
|
141
145
|
* ignored so it can't spam. */
|
|
146
|
+
/** Pull window focus into the iframe on first pointer interaction so `document` keydown (the
|
|
147
|
+
* spacebar shortcut) fires. No-op / harmless when already focused or full-page. */
|
|
148
|
+
private pullFocus = (): void => { try { window.focus(); } catch { /* cross-origin / non-browser */ } };
|
|
149
|
+
|
|
142
150
|
private handleKeyDown = (e: KeyboardEvent): void => {
|
|
143
151
|
if (this.destroyed || e.code !== 'Space' || e.repeat) return;
|
|
144
152
|
if (this.config.features.spacebar === false) return; // shortcut disabled (e.g. jurisdiction)
|
|
@@ -201,10 +209,17 @@ export class GameShell extends EventEmitter<ShellEvents> {
|
|
|
201
209
|
setBalance(n: number): void { this.state.balance = n; this.render(); }
|
|
202
210
|
setWin(n: number): void { this.state.win = n; this.render(); }
|
|
203
211
|
setBet(n: number): void { this.state.bet = n; this.render(); }
|
|
204
|
-
setMode(mode: ShellMode): void {
|
|
212
|
+
setMode(mode: ShellMode): void {
|
|
213
|
+
if (mode === 'replay') this.state.replay = true; // sticky: a replay stays a replay across modes
|
|
214
|
+
this.state.mode = mode;
|
|
215
|
+
this.render();
|
|
216
|
+
}
|
|
205
217
|
setBusy(busy: boolean): void { this.state.busy = busy; this.render(); }
|
|
206
218
|
setAutoplay(a: AutoplayOptions): void { this.state.autoplay = a; this.render(); }
|
|
207
219
|
setTurbo(level: number): void { this.state.turbo = level; this.render(); }
|
|
220
|
+
/** Currency-aware money formatter for WIN amounts (variable decimals: 0.0041 stays 0.0041, not
|
|
221
|
+
* 0.00). The host hands this to a scene so games format money without knowing the currency. */
|
|
222
|
+
formatWin(value: number): string { return formatCurrency(value, this.config.currency, true); }
|
|
208
223
|
setBuyBonusEnabled(enabled: boolean): void { this.state.buyBonusEnabled = enabled; this.render(); }
|
|
209
224
|
setFreeSpins(fs: FreeSpinsState): void { this.state.freeSpins = fs; this.render(); }
|
|
210
225
|
|
|
@@ -272,6 +287,9 @@ export class GameShell extends EventEmitter<ShellEvents> {
|
|
|
272
287
|
/** Open a generic, externally-driven modal (title + body + optional action buttons).
|
|
273
288
|
* Each action runs its `on` then closes; the ✕ shows when `availableClose` is true. */
|
|
274
289
|
openModal(opts: ModalOptions): void { this.showModal(buildModal(opts)); }
|
|
290
|
+
/** Programmatically dismiss whatever modal/overlay is currently shown (e.g. auto-close the
|
|
291
|
+
* reconnect overlay once the link is restored). No-op when nothing is open. */
|
|
292
|
+
closeModal(): void { this.modalHost.innerHTML = ''; }
|
|
275
293
|
/** Open the non-dismissable replay summary modal (START REPLAY → onReplay → reopen). */
|
|
276
294
|
openReplay(opts: ReplayModalOptions): void {
|
|
277
295
|
if (this.destroyed) return;
|
|
@@ -288,7 +306,11 @@ export class GameShell extends EventEmitter<ShellEvents> {
|
|
|
288
306
|
this.destroyed = true;
|
|
289
307
|
this.ro?.disconnect();
|
|
290
308
|
this.ro = null;
|
|
291
|
-
if (this.keysBound) {
|
|
309
|
+
if (this.keysBound) {
|
|
310
|
+
document.removeEventListener('keydown', this.handleKeyDown);
|
|
311
|
+
document.removeEventListener('pointerdown', this.pullFocus, true);
|
|
312
|
+
this.keysBound = false;
|
|
313
|
+
}
|
|
292
314
|
this.cancelMoneyAnims();
|
|
293
315
|
this.removeAllListeners();
|
|
294
316
|
this.root.classList.add('ge-shell-hidden');
|
|
@@ -52,8 +52,9 @@ export function renderBottomBar(shell: GameShell): HTMLElement {
|
|
|
52
52
|
// only when it's a free-spins replay (freeSpins.total > 0).
|
|
53
53
|
const showFsBlocks = isFS || (state.mode === 'replay' && state.freeSpins.total > 0);
|
|
54
54
|
|
|
55
|
-
// Replay is a read-only historical round — there's no real balance to show, so hide it.
|
|
56
|
-
|
|
55
|
+
// Replay is a read-only historical round — there's no real balance to show, so hide it. Keyed on
|
|
56
|
+
// the sticky `replay` flag (not `mode`) so it stays hidden through a replay's free-spins phase.
|
|
57
|
+
const balance = state.replay
|
|
57
58
|
? null
|
|
58
59
|
: readout('balance', shell.t('Balance'), fmt(state.balance));
|
|
59
60
|
// With a feature active (e.g. Ante) the BET readout shows the effective stake, tinted with
|
|
@@ -2,6 +2,7 @@ import type { GameShell } from '../GameShell';
|
|
|
2
2
|
import type { CellRef, GameInfoSection, GameMode, PaytableRow, PaylineDef, ShapeDef, WinSection } from '../types';
|
|
3
3
|
import { createOverlay, twoLine } from './primitives';
|
|
4
4
|
import { icon } from './icons';
|
|
5
|
+
import { PACKAGE_VERSION } from '../version';
|
|
5
6
|
|
|
6
7
|
const SVG_NS = 'http://www.w3.org/2000/svg';
|
|
7
8
|
|
|
@@ -23,9 +24,21 @@ export function openGameInfoModal(shell: GameShell): HTMLElement {
|
|
|
23
24
|
.sort((a, b) => a.k - b.k || a.i - b.i)
|
|
24
25
|
.forEach(({ s }) => body.appendChild(renderSection(shell, s)));
|
|
25
26
|
|
|
27
|
+
body.appendChild(versionFooter(shell));
|
|
26
28
|
return root;
|
|
27
29
|
}
|
|
28
30
|
|
|
31
|
+
/** A muted version stamp pinned to the bottom of the game-info modal:
|
|
32
|
+
* `${config.version ?? '1.0.0'}.${engine version without dots}` (e.g. '1.0.0.0246'). */
|
|
33
|
+
function versionFooter(shell: GameShell): HTMLElement {
|
|
34
|
+
const gameVersion = shell.config.version ?? '1.0.0';
|
|
35
|
+
const el = document.createElement('div');
|
|
36
|
+
el.dataset.ge = 'info-version';
|
|
37
|
+
el.className = 'ge-gi-version';
|
|
38
|
+
el.textContent = `${gameVersion}.${PACKAGE_VERSION.replaceAll('.', '')}`;
|
|
39
|
+
return el;
|
|
40
|
+
}
|
|
41
|
+
|
|
29
42
|
function renderSection(shell: GameShell, s: GameInfoSection): HTMLElement {
|
|
30
43
|
switch (s.type) {
|
|
31
44
|
case 'modes': return sectionModes(shell, s.modes, sec('info-modes', s.title, shell.t('Modes')));
|
package/src/shell/index.ts
CHANGED
package/src/shell/shell.css.ts
CHANGED
|
@@ -148,6 +148,8 @@ export const SHELL_CSS = SHELL_FONT_CSS + `
|
|
|
148
148
|
#${SHELL_ROOT_ID} .ge-gi-sec h3 { color:var(--shell-plaque-label); font-size:11px; letter-spacing:.14em;
|
|
149
149
|
text-transform:uppercase; margin:0 0 12px; }
|
|
150
150
|
#${SHELL_ROOT_ID} .ge-gi-sec p { color:rgba(255,255,255,.88); font-size:15px; line-height:1.6; margin:0; }
|
|
151
|
+
#${SHELL_ROOT_ID} .ge-gi-version { text-align:center; color:var(--shell-muted); font-size:11px;
|
|
152
|
+
letter-spacing:.08em; opacity:.7; margin:4px 0 2px; }
|
|
151
153
|
|
|
152
154
|
/* controls — two blocks (gameplay / menu & info), icon/name/description per control */
|
|
153
155
|
#${SHELL_ROOT_ID} .ge-gi-ctl-block + .ge-gi-ctl-block { margin-top:16px; padding-top:4px; border-top:1px solid var(--shell-plaque-line); }
|
package/src/shell/state.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { ShellConfig, ShellState } from './types';
|
|
|
3
3
|
export function createInitialState(config: ShellConfig): ShellState {
|
|
4
4
|
return {
|
|
5
5
|
mode: config.mode,
|
|
6
|
+
replay: config.replay ?? config.mode === 'replay',
|
|
6
7
|
balance: config.balance,
|
|
7
8
|
win: config.win,
|
|
8
9
|
bet: config.currentBet ?? config.defaultBet,
|
package/src/shell/types.ts
CHANGED
|
@@ -194,6 +194,9 @@ export interface ShellConfig {
|
|
|
194
194
|
theme?: ThemeConfig;
|
|
195
195
|
gameInfo: GameInfoContent;
|
|
196
196
|
language: string;
|
|
197
|
+
/** Game version shown in the game-info footer (e.g. '1.2.0'). Defaults to '1.0.0'. The footer
|
|
198
|
+
* stamp is `${version}.${engineVersionWithoutDots}` — e.g. game 1.0.0 on engine 0.24.6 → '1.0.0.0246'. */
|
|
199
|
+
version?: string;
|
|
197
200
|
/** When true, all built-in shell text is shown in the social-casino vocabulary (derived from
|
|
198
201
|
* English via word-swap rules), regardless of `language`. Game-supplied content is untouched. */
|
|
199
202
|
isSocial?: boolean;
|
|
@@ -204,6 +207,10 @@ export interface ShellConfig {
|
|
|
204
207
|
balance: number;
|
|
205
208
|
win: number;
|
|
206
209
|
mode: ShellMode;
|
|
210
|
+
/** Mark this shell as a read-only historical-round replay. A replay never shows the player's
|
|
211
|
+
* balance (there's no live wallet), even while its free-spins phase runs in `freeSpins` mode.
|
|
212
|
+
* Defaults to `mode === 'replay'`; set explicitly when a replay starts in another mode. */
|
|
213
|
+
replay?: boolean;
|
|
207
214
|
features: ShellFeatures;
|
|
208
215
|
/** Override the BUY BONUS bar button's action: when set, tapping it calls this instead of
|
|
209
216
|
* opening the built-in buy-bonus overlay (e.g. the game shows its own bonus UI). The button
|
|
@@ -213,6 +220,10 @@ export interface ShellConfig {
|
|
|
213
220
|
|
|
214
221
|
export interface ShellState {
|
|
215
222
|
mode: ShellMode;
|
|
223
|
+
/** Sticky replay marker — true for a historical-round replay, regardless of the current
|
|
224
|
+
* `mode`. Set once (from config or when `mode` becomes 'replay') and never cleared, since a
|
|
225
|
+
* shell instance is either a live game or a replay viewer for its whole lifetime. */
|
|
226
|
+
replay: boolean;
|
|
216
227
|
balance: number;
|
|
217
228
|
win: number;
|
|
218
229
|
bet: number;
|
|
@@ -52,6 +52,12 @@ export interface NativeSimulationConfig {
|
|
|
52
52
|
rng?: NativeRNGKind;
|
|
53
53
|
/** Replay mode: requires `rng: 'provably-fair'` (or default). */
|
|
54
54
|
replay?: NativeReplayParams;
|
|
55
|
+
/**
|
|
56
|
+
* Path to write per-round JSONL book dump. When set, the binary writes one
|
|
57
|
+
* JSON object per line (one per round) to this file, enabling post-run
|
|
58
|
+
* analysis of the full round log.
|
|
59
|
+
*/
|
|
60
|
+
dump?: string;
|
|
55
61
|
/** Progress callback */
|
|
56
62
|
onProgress?: (completed: number, total: number) => void;
|
|
57
63
|
}
|
|
@@ -137,6 +143,43 @@ interface GoSimulationOutput {
|
|
|
137
143
|
};
|
|
138
144
|
}
|
|
139
145
|
|
|
146
|
+
// ─── Pure arg builder ────────────────────────────────────
|
|
147
|
+
|
|
148
|
+
export interface NativeArgsInput {
|
|
149
|
+
configPath: string;
|
|
150
|
+
iterations: number;
|
|
151
|
+
bet: number;
|
|
152
|
+
action?: string;
|
|
153
|
+
params?: unknown;
|
|
154
|
+
rng?: string;
|
|
155
|
+
seed?: string;
|
|
156
|
+
dump?: string;
|
|
157
|
+
replay?: { serverSeed: string; clientSeed: string; nonceStart: number };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Build the CLI args array for the native simulation binary.
|
|
162
|
+
* Pure function — no I/O, easily testable.
|
|
163
|
+
*/
|
|
164
|
+
export function buildNativeArgs(a: NativeArgsInput): string[] {
|
|
165
|
+
const args = ['-config', a.configPath, '-iterations', String(a.iterations), '-bet', String(a.bet), '-format', 'json'];
|
|
166
|
+
if (a.action) args.push('-action', a.action);
|
|
167
|
+
if (a.params !== undefined && a.params !== null && (typeof a.params !== 'object' || Object.keys(a.params as object).length > 0)) {
|
|
168
|
+
args.push('-params', JSON.stringify(a.params));
|
|
169
|
+
}
|
|
170
|
+
if (a.rng) args.push('-rng', a.rng);
|
|
171
|
+
if (a.seed) args.push('-seed', a.seed);
|
|
172
|
+
if (a.dump) args.push('-dump', a.dump);
|
|
173
|
+
if (a.replay) {
|
|
174
|
+
args.push(
|
|
175
|
+
'-replay-server-seed', a.replay.serverSeed,
|
|
176
|
+
'-replay-client-seed', a.replay.clientSeed,
|
|
177
|
+
'-replay-nonce-start', String(a.replay.nonceStart),
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
return args;
|
|
181
|
+
}
|
|
182
|
+
|
|
140
183
|
// ─── Runner ─────────────────────────────────────────────
|
|
141
184
|
|
|
142
185
|
export class NativeSimulationRunner {
|
|
@@ -147,7 +190,7 @@ export class NativeSimulationRunner {
|
|
|
147
190
|
}
|
|
148
191
|
|
|
149
192
|
async run(): Promise<NativeSimulationResult> {
|
|
150
|
-
const { binaryPath, script, gameDefinition, iterations, bet, action, params, seed, rng, replay } = this.config;
|
|
193
|
+
const { binaryPath, script, gameDefinition, iterations, bet, action, params, seed, rng, replay, dump } = this.config;
|
|
151
194
|
|
|
152
195
|
if (replay && rng && rng !== 'provably-fair') {
|
|
153
196
|
throw new Error(`Replay mode requires rng="provably-fair" (got rng="${rng}")`);
|
|
@@ -166,31 +209,7 @@ export class NativeSimulationRunner {
|
|
|
166
209
|
]);
|
|
167
210
|
|
|
168
211
|
// Build CLI args
|
|
169
|
-
const args =
|
|
170
|
-
'-config', configPath,
|
|
171
|
-
'-iterations', String(iterations),
|
|
172
|
-
'-bet', String(bet),
|
|
173
|
-
'-format', 'json',
|
|
174
|
-
];
|
|
175
|
-
if (action) {
|
|
176
|
-
args.push('-action', action);
|
|
177
|
-
}
|
|
178
|
-
if (params && Object.keys(params).length > 0) {
|
|
179
|
-
args.push('-params', JSON.stringify(params));
|
|
180
|
-
}
|
|
181
|
-
if (rng) {
|
|
182
|
-
args.push('-rng', rng);
|
|
183
|
-
}
|
|
184
|
-
if (seed) {
|
|
185
|
-
args.push('-seed', seed);
|
|
186
|
-
}
|
|
187
|
-
if (replay) {
|
|
188
|
-
args.push(
|
|
189
|
-
'-replay-server-seed', replay.serverSeed,
|
|
190
|
-
'-replay-client-seed', replay.clientSeed,
|
|
191
|
-
'-replay-nonce-start', String(replay.nonceStart),
|
|
192
|
-
);
|
|
193
|
-
}
|
|
212
|
+
const args = buildNativeArgs({ configPath, iterations, bet, action, params, rng, seed, dump, replay });
|
|
194
213
|
|
|
195
214
|
// Execute binary
|
|
196
215
|
const output = await this.exec(binaryPath, args);
|
|
@@ -359,6 +378,23 @@ export function findNativeBinary(baseDir?: string): string | null {
|
|
|
359
378
|
return null;
|
|
360
379
|
}
|
|
361
380
|
|
|
381
|
+
/**
|
|
382
|
+
* Return the native binary path or throw a clear, install-guiding error.
|
|
383
|
+
* The math pipeline is go-native only — NO JS fallback.
|
|
384
|
+
*
|
|
385
|
+
* @param finder - injectable finder for testability; defaults to findNativeBinary.
|
|
386
|
+
*/
|
|
387
|
+
export function requireNativeBinary(finder: () => string | null = findNativeBinary): string {
|
|
388
|
+
const bin = finder();
|
|
389
|
+
if (!bin) {
|
|
390
|
+
throw new Error(
|
|
391
|
+
'native simulation binary not found — run `npm install` (platform-core fetches it via install-simulate). ' +
|
|
392
|
+
'The math pipeline is go-native only (no JS fallback).',
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
return bin;
|
|
396
|
+
}
|
|
397
|
+
|
|
362
398
|
function isExecutable(path: string): boolean {
|
|
363
399
|
try {
|
|
364
400
|
accessSync(path, fsConstants.X_OK);
|
package/src/simulation/index.ts
CHANGED
|
@@ -10,6 +10,8 @@ export {
|
|
|
10
10
|
NativeSimulationRunner,
|
|
11
11
|
findNativeBinary,
|
|
12
12
|
formatNativeResult,
|
|
13
|
+
buildNativeArgs,
|
|
14
|
+
requireNativeBinary,
|
|
13
15
|
} from './NativeSimulationRunner';
|
|
14
16
|
export type {
|
|
15
17
|
NativeSimulationConfig,
|
|
@@ -18,6 +20,7 @@ export type {
|
|
|
18
20
|
NativeReplayParams,
|
|
19
21
|
StageStats,
|
|
20
22
|
DistributionBucket,
|
|
23
|
+
NativeArgsInput,
|
|
21
24
|
} from './NativeSimulationRunner';
|
|
22
25
|
|
|
23
26
|
export { ParallelSimulationRunner } from './ParallelSimulationRunner';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Lua empty tables decode as {} — turn a possibly-{} value into a real array. */
|
|
2
|
+
export function asArray<T = unknown>(value: unknown): T[] {
|
|
3
|
+
return Array.isArray(value) ? (value as T[]) : [];
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
/** Apply asArray() to the named fields of a record (Lua {} → []). Optional helper for a game's normalizer. */
|
|
7
|
+
export function coerceLuaArrays<T extends Record<string, unknown>>(obj: T, fields: string[]): T {
|
|
8
|
+
const out: Record<string, unknown> = { ...obj };
|
|
9
|
+
for (const f of fields) out[f] = asArray(out[f]);
|
|
10
|
+
return out as T;
|
|
11
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** Minimal, renderer-agnostic base every normalized slot result satisfies. */
|
|
2
|
+
export interface SlotSpinResultBase {
|
|
3
|
+
/** Currency win amount for this play (PlatformSession.play() already applied the bet). */
|
|
4
|
+
totalWin: number;
|
|
5
|
+
freeSpins?: { awarded?: number; total?: number; remaining?: number };
|
|
6
|
+
// ── Round-continuation metadata (Stake segment-drain) ──────────────────
|
|
7
|
+
// A round may be split into several SEGMENTS (e.g. a base trigger + every free spin of one
|
|
8
|
+
// bonus). The host fills these from the raw play result so a scene can drain the remaining
|
|
9
|
+
// segments by replaying the SAME round; the game's normalizer never sets them.
|
|
10
|
+
/** Round id to pass back to `play(action, bet, roundId)` for the next segment of THIS round. */
|
|
11
|
+
roundId?: string;
|
|
12
|
+
/** Actions valid for the next segment; `nextActions[0]` is what to play to advance the round. */
|
|
13
|
+
nextActions?: string[];
|
|
14
|
+
/** True once the round has no further segments to drain (single spin, or final bonus spin). */
|
|
15
|
+
complete?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** A game declares one of these; the host invokes it on every play. Generic over the game's result type. */
|
|
19
|
+
export type SlotResultNormalizer<T extends SlotSpinResultBase> = (raw: unknown) => T;
|