@energy8platform/platform-core 0.24.6 → 0.25.1
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 -1
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +44 -1
- package/dist/index.esm.js +67 -1
- package/dist/index.esm.js.map +1 -1
- package/dist/loading.cjs.js +10 -1
- package/dist/loading.cjs.js.map +1 -1
- package/dist/loading.esm.js +10 -1
- package/dist/loading.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 +35 -0
- package/dist/shell.cjs.js.map +1 -1
- package/dist/shell.d.ts +16 -1
- package/dist/shell.esm.js +35 -1
- 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/loading/CSSPreloader.ts +14 -1
- package/src/lua/LuaEngine.ts +5 -2
- package/src/lua/types.ts +8 -0
- package/src/shell/GameShell.ts +19 -1
- 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/types.ts +3 -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
|
@@ -17,6 +17,10 @@ const LOGO_SVG = buildLogoSVG({
|
|
|
17
17
|
|
|
18
18
|
interface PreloaderState {
|
|
19
19
|
container: HTMLElement;
|
|
20
|
+
/** The container's inline `position` before we overrode it — restored on removal so we don't
|
|
21
|
+
* permanently clobber the host page's layout (e.g. a `#game { position: fixed; inset: 0 }`
|
|
22
|
+
* stylesheet rule, which an inline `relative` would otherwise defeat, collapsing its height). */
|
|
23
|
+
prevPosition: string;
|
|
20
24
|
overlay: HTMLDivElement;
|
|
21
25
|
styleEl: HTMLStyleElement;
|
|
22
26
|
rectEl: SVGRectElement;
|
|
@@ -137,6 +141,10 @@ export function createCSSPreloader(
|
|
|
137
141
|
}
|
|
138
142
|
`;
|
|
139
143
|
|
|
144
|
+
// The absolute overlay needs a positioned ancestor. Only override a STATIC container, and
|
|
145
|
+
// remember the prior inline value so removeCSSPreloader can restore it (an inline `relative`
|
|
146
|
+
// left behind would beat the game's `#game { position: fixed; inset: 0 }` and collapse it).
|
|
147
|
+
const prevPosition = container.style.position;
|
|
140
148
|
container.style.position = container.style.position || 'relative';
|
|
141
149
|
container.appendChild(styleEl);
|
|
142
150
|
container.appendChild(overlay);
|
|
@@ -148,6 +156,7 @@ export function createCSSPreloader(
|
|
|
148
156
|
// We still record state so removeCSSPreloader works.
|
|
149
157
|
state = {
|
|
150
158
|
container,
|
|
159
|
+
prevPosition,
|
|
151
160
|
overlay,
|
|
152
161
|
styleEl,
|
|
153
162
|
rectEl: null as unknown as SVGRectElement,
|
|
@@ -167,6 +176,7 @@ export function createCSSPreloader(
|
|
|
167
176
|
|
|
168
177
|
state = {
|
|
169
178
|
container,
|
|
179
|
+
prevPosition,
|
|
170
180
|
overlay,
|
|
171
181
|
styleEl,
|
|
172
182
|
rectEl,
|
|
@@ -251,7 +261,7 @@ export function removeCSSPreloader(_container: HTMLElement): Promise<void> {
|
|
|
251
261
|
}
|
|
252
262
|
|
|
253
263
|
state.removed = true;
|
|
254
|
-
const { overlay, styleEl } = state;
|
|
264
|
+
const { overlay, styleEl, container, prevPosition } = state;
|
|
255
265
|
overlay.classList.add('ge-preloader-hidden');
|
|
256
266
|
|
|
257
267
|
return new Promise<void>((resolve) => {
|
|
@@ -261,6 +271,9 @@ export function removeCSSPreloader(_container: HTMLElement): Promise<void> {
|
|
|
261
271
|
settled = true;
|
|
262
272
|
overlay.remove();
|
|
263
273
|
styleEl.remove();
|
|
274
|
+
// Restore the container's original inline position so the game's own layout
|
|
275
|
+
// (e.g. `#game { position: fixed; inset: 0 }`) is no longer defeated by our inline override.
|
|
276
|
+
container.style.position = prevPosition;
|
|
264
277
|
state = null;
|
|
265
278
|
resolve();
|
|
266
279
|
};
|
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)
|
|
@@ -209,6 +217,9 @@ export class GameShell extends EventEmitter<ShellEvents> {
|
|
|
209
217
|
setBusy(busy: boolean): void { this.state.busy = busy; this.render(); }
|
|
210
218
|
setAutoplay(a: AutoplayOptions): void { this.state.autoplay = a; this.render(); }
|
|
211
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); }
|
|
212
223
|
setBuyBonusEnabled(enabled: boolean): void { this.state.buyBonusEnabled = enabled; this.render(); }
|
|
213
224
|
setFreeSpins(fs: FreeSpinsState): void { this.state.freeSpins = fs; this.render(); }
|
|
214
225
|
|
|
@@ -276,6 +287,9 @@ export class GameShell extends EventEmitter<ShellEvents> {
|
|
|
276
287
|
/** Open a generic, externally-driven modal (title + body + optional action buttons).
|
|
277
288
|
* Each action runs its `on` then closes; the ✕ shows when `availableClose` is true. */
|
|
278
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 = ''; }
|
|
279
293
|
/** Open the non-dismissable replay summary modal (START REPLAY → onReplay → reopen). */
|
|
280
294
|
openReplay(opts: ReplayModalOptions): void {
|
|
281
295
|
if (this.destroyed) return;
|
|
@@ -292,7 +306,11 @@ export class GameShell extends EventEmitter<ShellEvents> {
|
|
|
292
306
|
this.destroyed = true;
|
|
293
307
|
this.ro?.disconnect();
|
|
294
308
|
this.ro = null;
|
|
295
|
-
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
|
+
}
|
|
296
314
|
this.cancelMoneyAnims();
|
|
297
315
|
this.removeAllListeners();
|
|
298
316
|
this.root.classList.add('ge-shell-hidden');
|
|
@@ -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/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;
|
|
@@ -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;
|