@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.
Files changed (54) 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 +67 -3
  7. package/dist/index.cjs.js.map +1 -1
  8. package/dist/index.d.ts +52 -1
  9. package/dist/index.esm.js +67 -3
  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 +45 -3
  17. package/dist/shell.cjs.js.map +1 -1
  18. package/dist/shell.d.ts +24 -1
  19. package/dist/shell.esm.js +45 -4
  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 +24 -2
  43. package/src/shell/components/BottomBar.ts +3 -2
  44. package/src/shell/components/GameInfo.ts +13 -0
  45. package/src/shell/index.ts +1 -0
  46. package/src/shell/shell.css.ts +2 -0
  47. package/src/shell/state.ts +1 -0
  48. package/src/shell/types.ts +11 -0
  49. package/src/shell/version.ts +3 -0
  50. package/src/simulation/NativeSimulationRunner.ts +62 -26
  51. package/src/simulation/index.ts +3 -0
  52. package/src/slot-result/coerce.ts +11 -0
  53. package/src/slot-result/index.ts +2 -0
  54. 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;
@@ -722,6 +750,10 @@ interface ShellConfig {
722
750
  balance: number;
723
751
  win: number;
724
752
  mode: ShellMode;
753
+ /** Mark this shell as a read-only historical-round replay. A replay never shows the player's
754
+ * balance (there's no live wallet), even while its free-spins phase runs in `freeSpins` mode.
755
+ * Defaults to `mode === 'replay'`; set explicitly when a replay starts in another mode. */
756
+ replay?: boolean;
725
757
  features: ShellFeatures;
726
758
  /** Override the BUY BONUS bar button's action: when set, tapping it calls this instead of
727
759
  * opening the built-in buy-bonus overlay (e.g. the game shows its own bonus UI). The button
@@ -730,6 +762,10 @@ interface ShellConfig {
730
762
  }
731
763
  interface ShellState {
732
764
  mode: ShellMode;
765
+ /** Sticky replay marker — true for a historical-round replay, regardless of the current
766
+ * `mode`. Set once (from config or when `mode` becomes 'replay') and never cleared, since a
767
+ * shell instance is either a live game or a replay viewer for its whole lifetime. */
768
+ replay: boolean;
733
769
  balance: number;
734
770
  win: number;
735
771
  bet: number;
@@ -795,6 +831,9 @@ declare class GameShell extends EventEmitter<ShellEvents> {
795
831
  * false, while a spin is running, while autoplay is active, outside base mode, when an
796
832
  * overlay/modal is open, or when an editable element is focused. `repeat` (held key) is
797
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;
798
837
  private handleKeyDown;
799
838
  setLayout(layout: 'wide' | 'mobile'): void;
800
839
  /** Resolve a built-in shell string. English is the source; with `isSocial` it is run through
@@ -813,6 +852,9 @@ declare class GameShell extends EventEmitter<ShellEvents> {
813
852
  setBusy(busy: boolean): void;
814
853
  setAutoplay(a: AutoplayOptions): void;
815
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;
816
858
  setBuyBonusEnabled(enabled: boolean): void;
817
859
  setFreeSpins(fs: FreeSpinsState): void;
818
860
  private showModal;
@@ -837,6 +879,9 @@ declare class GameShell extends EventEmitter<ShellEvents> {
837
879
  /** Open a generic, externally-driven modal (title + body + optional action buttons).
838
880
  * Each action runs its `on` then closes; the ✕ shows when `availableClose` is true. */
839
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;
840
885
  /** Open the non-dismissable replay summary modal (START REPLAY → onReplay → reopen). */
841
886
  openReplay(opts: ReplayModalOptions): void;
842
887
  /** Bet picker — list of available bets with an accent Confirm. */
@@ -889,6 +934,12 @@ interface NativeSimulationConfig {
889
934
  rng?: NativeRNGKind;
890
935
  /** Replay mode: requires `rng: 'provably-fair'` (or default). */
891
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;
892
943
  /** Progress callback */
893
944
  onProgress?: (completed: number, total: number) => void;
894
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
  }
@@ -953,6 +976,7 @@ function removeCSSPreloader(_container) {
953
976
  function createInitialState(config) {
954
977
  return {
955
978
  mode: config.mode,
979
+ replay: config.replay ?? config.mode === 'replay',
956
980
  balance: config.balance,
957
981
  win: config.win,
958
982
  bet: config.currentBet ?? config.defaultBet,
@@ -1215,6 +1239,8 @@ const SHELL_CSS = SHELL_FONT_CSS + `
1215
1239
  #${SHELL_ROOT_ID} .ge-gi-sec h3 { color:var(--shell-plaque-label); font-size:11px; letter-spacing:.14em;
1216
1240
  text-transform:uppercase; margin:0 0 12px; }
1217
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; }
1218
1244
 
1219
1245
  /* controls — two blocks (gameplay / menu & info), icon/name/description per control */
1220
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); }
@@ -1652,8 +1678,9 @@ function renderBottomBar(shell) {
1652
1678
  // FS always shows the spins counter + accumulated Total Win (even €0); a replay shows them
1653
1679
  // only when it's a free-spins replay (freeSpins.total > 0).
1654
1680
  const showFsBlocks = isFS || (state.mode === 'replay' && state.freeSpins.total > 0);
1655
- // Replay is a read-only historical round — there's no real balance to show, so hide it.
1656
- const balance = state.mode === 'replay'
1681
+ // Replay is a read-only historical round — there's no real balance to show, so hide it. Keyed on
1682
+ // the sticky `replay` flag (not `mode`) so it stays hidden through a replay's free-spins phase.
1683
+ const balance = state.replay
1657
1684
  ? null
1658
1685
  : readout('balance', shell.t('Balance'), fmt(state.balance));
1659
1686
  // With a feature active (e.g. Ante) the BET readout shows the effective stake, tinted with
@@ -1923,6 +1950,10 @@ function openSettingsModal(shell) {
1923
1950
  return root;
1924
1951
  }
1925
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
+
1926
1957
  const SVG_NS = 'http://www.w3.org/2000/svg';
1927
1958
  function openGameInfoModal(shell) {
1928
1959
  const { root, body } = createOverlay({
@@ -1939,8 +1970,19 @@ function openGameInfoModal(shell) {
1939
1970
  .map((s, i) => ({ s, i, k: base(s, i) }))
1940
1971
  .sort((a, b) => a.k - b.k || a.i - b.i)
1941
1972
  .forEach(({ s }) => body.appendChild(renderSection(shell, s)));
1973
+ body.appendChild(versionFooter(shell));
1942
1974
  return root;
1943
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
+ }
1944
1986
  function renderSection(shell, s) {
1945
1987
  switch (s.type) {
1946
1988
  case 'modes': return sectionModes(shell, s.modes, sec('info-modes', s.title, shell.t('Modes')));
@@ -2734,6 +2776,10 @@ class GameShell extends EventEmitter {
2734
2776
  this.observeLayout();
2735
2777
  if (typeof document !== 'undefined') {
2736
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);
2737
2783
  this.keysBound = true;
2738
2784
  }
2739
2785
  this.render();
@@ -2819,6 +2865,12 @@ class GameShell extends EventEmitter {
2819
2865
  * false, while a spin is running, while autoplay is active, outside base mode, when an
2820
2866
  * overlay/modal is open, or when an editable element is focused. `repeat` (held key) is
2821
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 */ } };
2822
2874
  handleKeyDown = (e) => {
2823
2875
  if (this.destroyed || e.code !== 'Space' || e.repeat)
2824
2876
  return;
@@ -2883,10 +2935,18 @@ class GameShell extends EventEmitter {
2883
2935
  setBalance(n) { this.state.balance = n; this.render(); }
2884
2936
  setWin(n) { this.state.win = n; this.render(); }
2885
2937
  setBet(n) { this.state.bet = n; this.render(); }
2886
- setMode(mode) { this.state.mode = mode; this.render(); }
2938
+ setMode(mode) {
2939
+ if (mode === 'replay')
2940
+ this.state.replay = true; // sticky: a replay stays a replay across modes
2941
+ this.state.mode = mode;
2942
+ this.render();
2943
+ }
2887
2944
  setBusy(busy) { this.state.busy = busy; this.render(); }
2888
2945
  setAutoplay(a) { this.state.autoplay = a; this.render(); }
2889
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); }
2890
2950
  setBuyBonusEnabled(enabled) { this.state.buyBonusEnabled = enabled; this.render(); }
2891
2951
  setFreeSpins(fs) { this.state.freeSpins = fs; this.render(); }
2892
2952
  showModal(el) {
@@ -2957,6 +3017,9 @@ class GameShell extends EventEmitter {
2957
3017
  /** Open a generic, externally-driven modal (title + body + optional action buttons).
2958
3018
  * Each action runs its `on` then closes; the ✕ shows when `availableClose` is true. */
2959
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 = ''; }
2960
3023
  /** Open the non-dismissable replay summary modal (START REPLAY → onReplay → reopen). */
2961
3024
  openReplay(opts) {
2962
3025
  if (this.destroyed)
@@ -2975,6 +3038,7 @@ class GameShell extends EventEmitter {
2975
3038
  this.ro = null;
2976
3039
  if (this.keysBound) {
2977
3040
  document.removeEventListener('keydown', this.handleKeyDown);
3041
+ document.removeEventListener('pointerdown', this.pullFocus, true);
2978
3042
  this.keysBound = false;
2979
3043
  }
2980
3044
  this.cancelMoneyAnims();