@energy8platform/game-engine 0.34.0 → 0.34.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@energy8platform/game-engine",
3
- "version": "0.34.0",
3
+ "version": "0.34.1",
4
4
  "description": "Universal casino game engine built on PixiJS v8 and @energy8platform/game-sdk",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs.js",
@@ -186,7 +186,7 @@ export async function createSlotGame<T extends SlotSpinResultBase = SlotSpinResu
186
186
  jurisdiction?: import('./shellConfig').JurisdictionRestrictions;
187
187
  betLevels?: number[];
188
188
  defaultBet?: number;
189
- stake?: { defaultBetLevel?: number };
189
+ stake?: { defaultBetLevel?: number; minBet?: number; maxBet?: number };
190
190
  };
191
191
  lang?: string;
192
192
  } | null;
@@ -218,7 +218,20 @@ export async function createSlotGame<T extends SlotSpinResultBase = SlotSpinResu
218
218
  // absent on dev/devBridge → buildShellConfig falls back to the spec.
219
219
  betLevels: config?.betLevels,
220
220
  defaultBet: config?.stake?.defaultBetLevel ?? config?.defaultBet,
221
+ // Hard stake window; the bridge rejects anything outside it before /bet/play.
222
+ minBet: config?.stake?.minBet,
223
+ maxBet: config?.stake?.maxBet,
221
224
  };
225
+ // On a real Stake launch the ladder is CURRENCY-SPECIFIC and mandatory. Falling back to the
226
+ // spec's (EUR-shaped) ladder here would put the game on bets the wallet can't honour — every
227
+ // spin rejected on a high-denomination currency (ARS minBet 50), or silently mispriced. Fail
228
+ // where the cause is visible instead of at the first spin.
229
+ if (isStakeNow && !runtime.betLevels?.length) {
230
+ fatal('Could not load the bet levels for your currency. Please relaunch the game.');
231
+ throw new Error(
232
+ 'createSlotGame: Stake launch returned no config.betLevels — refusing to fall back to the spec ladder',
233
+ );
234
+ }
222
235
  if (opts.dev) {
223
236
  // Dev-only diagnostic. Logged as PLAIN STRINGS (not collapsed objects) so the values are
224
237
  // readable in the console without expanding. If the shown symbol is a bare code ("EUR")
@@ -238,6 +251,12 @@ export async function createSlotGame<T extends SlotSpinResultBase = SlotSpinResu
238
251
  app: game.app,
239
252
  parent: game.uiLayer,
240
253
  };
254
+ // Adopt the bet the shell is about to display. `currentBet` was seeded from the SPEC above,
255
+ // which is only ever right by accident: the authoritative default is the per-currency one from
256
+ // /wallet/authenticate. The shell re-syncs us on `betChange`, but that fires ONLY when the
257
+ // player moves the bet — so without this the FIRST spin (and any bonus buy before it) plays at
258
+ // the spec's bet while the bar shows the RGS one.
259
+ currentBet = pixiShellCfg.currentBet ?? currentBet;
241
260
  // The game may swap in its own shell (a custom renderer over the same core) via shellFactory;
242
261
  // default is the built-in Pixi shell. The host drives whichever it gets through the Shell contract.
243
262
  shell = (opts.shellFactory ?? createPixiShell)(pixiShellCfg);
@@ -73,6 +73,40 @@ export interface ShellRuntime {
73
73
  /** Per-currency default bet from `/wallet/authenticate` (the bridge surfaces it as
74
74
  * `config.stake.defaultBetLevel`). Stake requires the selector to start here on every entry. */
75
75
  defaultBet?: number;
76
+ /** Hard per-currency stake bounds from `/wallet/authenticate` (`config.stake.minBet/maxBet`,
77
+ * major units). The bridge REJECTS any bet outside them before `/bet/play`, so the resolved
78
+ * default is clamped into this window — see `resolveDefaultBet`. */
79
+ minBet?: number;
80
+ maxBet?: number;
81
+ }
82
+
83
+ /**
84
+ * Resolve the bet the game must START on — the single source of truth for BOTH the shell's
85
+ * selector and the host's `currentBet` (they must never disagree; if they do, the player is
86
+ * charged a bet they never chose).
87
+ *
88
+ * The `preferred` bet is the RGS default when there is one, else the spec's. Neither is trusted
89
+ * blindly: the result is always an actual rung of `betLevels`, inside `[minBet, maxBet]`.
90
+ *
91
+ * Why snapping matters beyond the bounds check: the shell's `stepBet()` locates the current bet
92
+ * with `availableBets.indexOf(state.bet)`. An off-ladder default makes that return -1, so the very
93
+ * first +/- press jumps the bet to `availableBets[0]` instead of stepping.
94
+ */
95
+ export function resolveDefaultBet(
96
+ betLevels: number[],
97
+ preferred: number | undefined,
98
+ bounds?: { minBet?: number; maxBet?: number },
99
+ ): number {
100
+ if (!betLevels.length) return preferred ?? 0; // no ladder to snap to — nothing better to offer
101
+ // Drop rungs the RGS would reject outright. If the bounds exclude the WHOLE ladder the data is
102
+ // self-contradictory; keep the ladder rather than hand back a bet that isn't even selectable.
103
+ const inBounds = betLevels.filter(
104
+ (l) => (bounds?.minBet == null || l >= bounds.minBet) && (bounds?.maxBet == null || l <= bounds.maxBet),
105
+ );
106
+ const pool = inBounds.length ? inBounds : betLevels;
107
+ if (preferred == null) return pool[0];
108
+ // Nearest rung; ties go to the cheaper one (`<` keeps the earlier, ascending-ladder entry).
109
+ return pool.reduce((best, l) => (Math.abs(l - preferred) < Math.abs(best - preferred) ? l : best), pool[0]);
76
110
  }
77
111
 
78
112
  /** The subset of Stake's jurisdiction flags the shell can enforce via `ShellFeatures`. */
@@ -389,8 +423,19 @@ export function buildShellConfig(
389
423
  ): Omit<PixiShellConfig, 'app' | 'parent' | 'gameInfo'> & { gameInfo: GameInfoContent } {
390
424
  // Prefer the currency-specific ladder from /wallet/authenticate; fall back to the spec (dev/devBridge).
391
425
  const betLevels = runtime.betLevels?.length ? runtime.betLevels : model.spec.betLevels;
392
- // Stake requires the default to come from authenticate on every entry; spec default is the dev fallback.
393
- const defaultBet = runtime.defaultBet ?? model.spec.defaultBet ?? betLevels[0];
426
+ // Stake requires the default to come from authenticate on every entry; spec default is the dev
427
+ // fallback. Snapped onto the ladder + clamped to the RGS bounds so the selector and the host's
428
+ // `currentBet` both start on a bet the server will actually accept.
429
+ // EXCEPT in replay: there the bridge reports the replayed round's OWN stake, a historical amount
430
+ // that need not be a rung of today's ladder. Snapping it would misreport what was actually bet.
431
+ const preferredBet = runtime.defaultBet ?? model.spec.defaultBet;
432
+ const defaultBet =
433
+ runtime.mode === 'replay'
434
+ ? (preferredBet ?? betLevels[0])
435
+ : resolveDefaultBet(betLevels, preferredBet, {
436
+ minBet: runtime.minBet,
437
+ maxBet: runtime.maxBet,
438
+ });
394
439
  // runtime.currency is the resolved CurrencyConfig (derived from initData.config.currency by the
395
440
  // host); opts.currency still wins. Fall back to the spec code, then a neutral euro.
396
441
  const currency =