@energy8platform/platform-core 0.24.6 → 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.
Files changed (52) hide show
  1. package/dist/game-spec.cjs.js +209 -0
  2. package/dist/game-spec.cjs.js.map +1 -0
  3. package/dist/game-spec.d.ts +164 -0
  4. package/dist/game-spec.esm.js +198 -0
  5. package/dist/game-spec.esm.js.map +1 -0
  6. package/dist/index.cjs.js +57 -0
  7. package/dist/index.cjs.js.map +1 -1
  8. package/dist/index.d.ts +44 -1
  9. package/dist/index.esm.js +57 -0
  10. package/dist/index.esm.js.map +1 -1
  11. package/dist/lua.cjs.js +5 -2
  12. package/dist/lua.cjs.js.map +1 -1
  13. package/dist/lua.d.ts +9 -0
  14. package/dist/lua.esm.js +5 -2
  15. package/dist/lua.esm.js.map +1 -1
  16. package/dist/shell.cjs.js +35 -0
  17. package/dist/shell.cjs.js.map +1 -1
  18. package/dist/shell.d.ts +16 -1
  19. package/dist/shell.esm.js +35 -1
  20. package/dist/shell.esm.js.map +1 -1
  21. package/dist/simulation.cjs.js +40 -22
  22. package/dist/simulation.cjs.js.map +1 -1
  23. package/dist/simulation.d.ts +35 -2
  24. package/dist/simulation.esm.js +39 -23
  25. package/dist/simulation.esm.js.map +1 -1
  26. package/dist/slot-result.cjs.js +17 -0
  27. package/dist/slot-result.cjs.js.map +1 -0
  28. package/dist/slot-result.d.ts +26 -0
  29. package/dist/slot-result.esm.js +14 -0
  30. package/dist/slot-result.esm.js.map +1 -0
  31. package/package.json +12 -1
  32. package/scripts/gen-version.mjs +21 -0
  33. package/src/PlatformSession.ts +28 -0
  34. package/src/game-spec/defineGame.ts +16 -0
  35. package/src/game-spec/derive.ts +135 -0
  36. package/src/game-spec/export.ts +17 -0
  37. package/src/game-spec/index.ts +6 -0
  38. package/src/game-spec/types.ts +81 -0
  39. package/src/game-spec/validate.ts +49 -0
  40. package/src/lua/LuaEngine.ts +5 -2
  41. package/src/lua/types.ts +8 -0
  42. package/src/shell/GameShell.ts +19 -1
  43. package/src/shell/components/GameInfo.ts +13 -0
  44. package/src/shell/index.ts +1 -0
  45. package/src/shell/shell.css.ts +2 -0
  46. package/src/shell/types.ts +3 -0
  47. package/src/shell/version.ts +3 -0
  48. package/src/simulation/NativeSimulationRunner.ts +62 -26
  49. package/src/simulation/index.ts +3 -0
  50. package/src/slot-result/coerce.ts +11 -0
  51. package/src/slot-result/index.ts +2 -0
  52. package/src/slot-result/types.ts +19 -0
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { SessionData, GameConfigData, PlayParams, PlayResultData, BalanceData, CasinoGameSDK, InitData } from '@energy8platform/game-sdk';
1
+ import { SessionData, GameConfigData, PlayParams, PlayResultData, BalanceData, ConnectionStatePayload, CasinoGameSDK, InitData } from '@energy8platform/game-sdk';
2
2
  export { AnywhereWinData, BalanceData, GameConfigData, InitData, PaylineData, PlayParams, PlayResultData, SessionData, SymbolData, WinLineData } from '@energy8platform/game-sdk';
3
3
 
4
4
  interface GameDefinition {
@@ -76,6 +76,14 @@ interface LuaEngineConfig {
76
76
  logger?: (level: string, msg: string) => void;
77
77
  /** Skip marshalling data fields (matrix, wins, etc.) for faster simulation */
78
78
  simulationMode?: boolean;
79
+ /**
80
+ * Allow `requires_session` actions (e.g. `free_spin`) to run even with no active session.
81
+ * Default false (server-faithful). The dev harness sets this true: when a bonus is bought
82
+ * through the BOOKS path the LuaEngine never created a session, yet the scaffold then replays
83
+ * `free_spin` (which has no books) via the Lua fallback — without this it would throw
84
+ * "Action free_spin requires an active session".
85
+ */
86
+ allowSessionlessActions?: boolean;
79
87
  }
80
88
  interface LuaPlayResult {
81
89
  totalWin: number;
@@ -354,6 +362,9 @@ interface PlatformSessionEvents {
354
362
  balanceUpdate: BalanceData;
355
363
  /** SDK or transport error */
356
364
  error: Error;
365
+ /** Host link state changed (forwarded from the SDK): 'connecting' | 'lost' | 'restored'.
366
+ * The host renders a reconnect overlay on lost/connecting and dismisses it on restored. */
367
+ connectionStateChanged: ConnectionStatePayload;
357
368
  }
358
369
  /**
359
370
  * Lifecycle wrapper around CasinoGameSDK + (optional) DevBridge.
@@ -401,6 +412,20 @@ declare class PlatformSession extends EventEmitter<PlatformSessionEvents> {
401
412
  * Throws if the session was constructed with `sdk: false`.
402
413
  */
403
414
  play(params: PlayParams): Promise<PlayResultData>;
415
+ /**
416
+ * Acknowledge a finished PLAY_RESULT (call AFTER the game has animated it).
417
+ *
418
+ * The host uses this to know the client is ready for the next action and, on
419
+ * Stake, to settle the round (`/wallet/end-round`) only once the win
420
+ * animation has played. No-op when constructed with `sdk: false`.
421
+ */
422
+ playAck(result: PlayResultData): void;
423
+ /**
424
+ * Query the host for an in-flight round (e.g. after a page reload). Resolves with the last
425
+ * result snapshot when a round is still open, or `null`. Used to offer a "resume / finish"
426
+ * choice on boot. Resolves `null` when constructed with `sdk: false`.
427
+ */
428
+ getState(): Promise<PlayResultData | null>;
404
429
  /** Tear down the SDK, DevBridge, and clear listeners. */
405
430
  destroy(): void;
406
431
  }
@@ -712,6 +737,9 @@ interface ShellConfig {
712
737
  theme?: ThemeConfig;
713
738
  gameInfo: GameInfoContent;
714
739
  language: string;
740
+ /** Game version shown in the game-info footer (e.g. '1.2.0'). Defaults to '1.0.0'. The footer
741
+ * stamp is `${version}.${engineVersionWithoutDots}` — e.g. game 1.0.0 on engine 0.24.6 → '1.0.0.0246'. */
742
+ version?: string;
715
743
  /** When true, all built-in shell text is shown in the social-casino vocabulary (derived from
716
744
  * English via word-swap rules), regardless of `language`. Game-supplied content is untouched. */
717
745
  isSocial?: boolean;
@@ -803,6 +831,9 @@ declare class GameShell extends EventEmitter<ShellEvents> {
803
831
  * false, while a spin is running, while autoplay is active, outside base mode, when an
804
832
  * overlay/modal is open, or when an editable element is focused. `repeat` (held key) is
805
833
  * ignored so it can't spam. */
834
+ /** Pull window focus into the iframe on first pointer interaction so `document` keydown (the
835
+ * spacebar shortcut) fires. No-op / harmless when already focused or full-page. */
836
+ private pullFocus;
806
837
  private handleKeyDown;
807
838
  setLayout(layout: 'wide' | 'mobile'): void;
808
839
  /** Resolve a built-in shell string. English is the source; with `isSocial` it is run through
@@ -821,6 +852,9 @@ declare class GameShell extends EventEmitter<ShellEvents> {
821
852
  setBusy(busy: boolean): void;
822
853
  setAutoplay(a: AutoplayOptions): void;
823
854
  setTurbo(level: number): void;
855
+ /** Currency-aware money formatter for WIN amounts (variable decimals: 0.0041 stays 0.0041, not
856
+ * 0.00). The host hands this to a scene so games format money without knowing the currency. */
857
+ formatWin(value: number): string;
824
858
  setBuyBonusEnabled(enabled: boolean): void;
825
859
  setFreeSpins(fs: FreeSpinsState): void;
826
860
  private showModal;
@@ -845,6 +879,9 @@ declare class GameShell extends EventEmitter<ShellEvents> {
845
879
  /** Open a generic, externally-driven modal (title + body + optional action buttons).
846
880
  * Each action runs its `on` then closes; the ✕ shows when `availableClose` is true. */
847
881
  openModal(opts: ModalOptions): void;
882
+ /** Programmatically dismiss whatever modal/overlay is currently shown (e.g. auto-close the
883
+ * reconnect overlay once the link is restored). No-op when nothing is open. */
884
+ closeModal(): void;
848
885
  /** Open the non-dismissable replay summary modal (START REPLAY → onReplay → reopen). */
849
886
  openReplay(opts: ReplayModalOptions): void;
850
887
  /** Bet picker — list of available bets with an accent Confirm. */
@@ -897,6 +934,12 @@ interface NativeSimulationConfig {
897
934
  rng?: NativeRNGKind;
898
935
  /** Replay mode: requires `rng: 'provably-fair'` (or default). */
899
936
  replay?: NativeReplayParams;
937
+ /**
938
+ * Path to write per-round JSONL book dump. When set, the binary writes one
939
+ * JSON object per line (one per round) to this file, enabling post-run
940
+ * analysis of the full round log.
941
+ */
942
+ dump?: string;
900
943
  /** Progress callback */
901
944
  onProgress?: (completed: number, total: number) => void;
902
945
  }
package/dist/index.esm.js CHANGED
@@ -608,6 +608,26 @@ class PlatformSession extends EventEmitter {
608
608
  }
609
609
  return this.sdk.play(params);
610
610
  }
611
+ /**
612
+ * Acknowledge a finished PLAY_RESULT (call AFTER the game has animated it).
613
+ *
614
+ * The host uses this to know the client is ready for the next action and, on
615
+ * Stake, to settle the round (`/wallet/end-round`) only once the win
616
+ * animation has played. No-op when constructed with `sdk: false`.
617
+ */
618
+ playAck(result) {
619
+ this.sdk?.playAck(result);
620
+ }
621
+ /**
622
+ * Query the host for an in-flight round (e.g. after a page reload). Resolves with the last
623
+ * result snapshot when a round is still open, or `null`. Used to offer a "resume / finish"
624
+ * choice on boot. Resolves `null` when constructed with `sdk: false`.
625
+ */
626
+ async getState() {
627
+ if (!this.sdk)
628
+ return null;
629
+ return this.sdk.getState();
630
+ }
611
631
  /** Tear down the SDK, DevBridge, and clear listeners. */
612
632
  destroy() {
613
633
  this.sdk?.destroy();
@@ -647,6 +667,9 @@ async function createPlatformSession(config = {}) {
647
667
  sdk.on('balanceUpdate', (data) => {
648
668
  session.emit('balanceUpdate', data);
649
669
  });
670
+ sdk.on('connectionStateChanged', (state) => {
671
+ session.emit('connectionStateChanged', state);
672
+ });
650
673
  }
651
674
  return session;
652
675
  }
@@ -1216,6 +1239,8 @@ const SHELL_CSS = SHELL_FONT_CSS + `
1216
1239
  #${SHELL_ROOT_ID} .ge-gi-sec h3 { color:var(--shell-plaque-label); font-size:11px; letter-spacing:.14em;
1217
1240
  text-transform:uppercase; margin:0 0 12px; }
1218
1241
  #${SHELL_ROOT_ID} .ge-gi-sec p { color:rgba(255,255,255,.88); font-size:15px; line-height:1.6; margin:0; }
1242
+ #${SHELL_ROOT_ID} .ge-gi-version { text-align:center; color:var(--shell-muted); font-size:11px;
1243
+ letter-spacing:.08em; opacity:.7; margin:4px 0 2px; }
1219
1244
 
1220
1245
  /* controls — two blocks (gameplay / menu & info), icon/name/description per control */
1221
1246
  #${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); }
@@ -1925,6 +1950,10 @@ function openSettingsModal(shell) {
1925
1950
  return root;
1926
1951
  }
1927
1952
 
1953
+ // AUTO-GENERATED by scripts/gen-version.mjs — do not edit. Mirrors package.json "version".
1954
+ /** The @energy8platform/platform-core package version, stamped at build time. */
1955
+ const PACKAGE_VERSION = '0.25.0';
1956
+
1928
1957
  const SVG_NS = 'http://www.w3.org/2000/svg';
1929
1958
  function openGameInfoModal(shell) {
1930
1959
  const { root, body } = createOverlay({
@@ -1941,8 +1970,19 @@ function openGameInfoModal(shell) {
1941
1970
  .map((s, i) => ({ s, i, k: base(s, i) }))
1942
1971
  .sort((a, b) => a.k - b.k || a.i - b.i)
1943
1972
  .forEach(({ s }) => body.appendChild(renderSection(shell, s)));
1973
+ body.appendChild(versionFooter(shell));
1944
1974
  return root;
1945
1975
  }
1976
+ /** A muted version stamp pinned to the bottom of the game-info modal:
1977
+ * `${config.version ?? '1.0.0'}.${engine version without dots}` (e.g. '1.0.0.0246'). */
1978
+ function versionFooter(shell) {
1979
+ const gameVersion = shell.config.version ?? '1.0.0';
1980
+ const el = document.createElement('div');
1981
+ el.dataset.ge = 'info-version';
1982
+ el.className = 'ge-gi-version';
1983
+ el.textContent = `${gameVersion}.${PACKAGE_VERSION.replaceAll('.', '')}`;
1984
+ return el;
1985
+ }
1946
1986
  function renderSection(shell, s) {
1947
1987
  switch (s.type) {
1948
1988
  case 'modes': return sectionModes(shell, s.modes, sec('info-modes', s.title, shell.t('Modes')));
@@ -2736,6 +2776,10 @@ class GameShell extends EventEmitter {
2736
2776
  this.observeLayout();
2737
2777
  if (typeof document !== 'undefined') {
2738
2778
  document.addEventListener('keydown', this.handleKeyDown);
2779
+ // Stake serves the game in an iframe; on first paint focus is on the HOST page, so a `document`
2780
+ // keydown never fires and Space scrolls the parent. Pull window focus into the iframe on the
2781
+ // first pointer interaction so the spacebar shortcut works. Harmless on full-page Energy8.
2782
+ document.addEventListener('pointerdown', this.pullFocus, true);
2739
2783
  this.keysBound = true;
2740
2784
  }
2741
2785
  this.render();
@@ -2821,6 +2865,12 @@ class GameShell extends EventEmitter {
2821
2865
  * false, while a spin is running, while autoplay is active, outside base mode, when an
2822
2866
  * overlay/modal is open, or when an editable element is focused. `repeat` (held key) is
2823
2867
  * ignored so it can't spam. */
2868
+ /** Pull window focus into the iframe on first pointer interaction so `document` keydown (the
2869
+ * spacebar shortcut) fires. No-op / harmless when already focused or full-page. */
2870
+ pullFocus = () => { try {
2871
+ window.focus();
2872
+ }
2873
+ catch { /* cross-origin / non-browser */ } };
2824
2874
  handleKeyDown = (e) => {
2825
2875
  if (this.destroyed || e.code !== 'Space' || e.repeat)
2826
2876
  return;
@@ -2894,6 +2944,9 @@ class GameShell extends EventEmitter {
2894
2944
  setBusy(busy) { this.state.busy = busy; this.render(); }
2895
2945
  setAutoplay(a) { this.state.autoplay = a; this.render(); }
2896
2946
  setTurbo(level) { this.state.turbo = level; this.render(); }
2947
+ /** Currency-aware money formatter for WIN amounts (variable decimals: 0.0041 stays 0.0041, not
2948
+ * 0.00). The host hands this to a scene so games format money without knowing the currency. */
2949
+ formatWin(value) { return formatCurrency(value, this.config.currency, true); }
2897
2950
  setBuyBonusEnabled(enabled) { this.state.buyBonusEnabled = enabled; this.render(); }
2898
2951
  setFreeSpins(fs) { this.state.freeSpins = fs; this.render(); }
2899
2952
  showModal(el) {
@@ -2964,6 +3017,9 @@ class GameShell extends EventEmitter {
2964
3017
  /** Open a generic, externally-driven modal (title + body + optional action buttons).
2965
3018
  * Each action runs its `on` then closes; the ✕ shows when `availableClose` is true. */
2966
3019
  openModal(opts) { this.showModal(buildModal(opts)); }
3020
+ /** Programmatically dismiss whatever modal/overlay is currently shown (e.g. auto-close the
3021
+ * reconnect overlay once the link is restored). No-op when nothing is open. */
3022
+ closeModal() { this.modalHost.innerHTML = ''; }
2967
3023
  /** Open the non-dismissable replay summary modal (START REPLAY → onReplay → reopen). */
2968
3024
  openReplay(opts) {
2969
3025
  if (this.destroyed)
@@ -2982,6 +3038,7 @@ class GameShell extends EventEmitter {
2982
3038
  this.ro = null;
2983
3039
  if (this.keysBound) {
2984
3040
  document.removeEventListener('keydown', this.handleKeyDown);
3041
+ document.removeEventListener('pointerdown', this.pullFocus, true);
2985
3042
  this.keysBound = false;
2986
3043
  }
2987
3044
  this.cancelMoneyAnims();