@energy8platform/platform-core 0.19.0 → 0.20.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/README.md +44 -1
- package/dist/dev-bridge.cjs.js +104 -0
- package/dist/dev-bridge.cjs.js.map +1 -1
- package/dist/dev-bridge.d.ts +64 -1
- package/dist/dev-bridge.esm.js +104 -0
- package/dist/dev-bridge.esm.js.map +1 -1
- package/dist/index.cjs.js +113 -0
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +71 -1
- package/dist/index.esm.js +113 -0
- package/dist/index.esm.js.map +1 -1
- package/package.json +2 -2
- package/src/PlatformSession.ts +10 -0
- package/src/dev-bridge/DevBridge.ts +160 -2
- package/src/dev-bridge/index.ts +1 -1
- package/src/index.ts +1 -1
package/src/PlatformSession.ts
CHANGED
|
@@ -93,6 +93,16 @@ export class PlatformSession extends EventEmitter<PlatformSessionEvents> {
|
|
|
93
93
|
return this.sdk?.currency ?? 'USD';
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
/**
|
|
97
|
+
* `true` when launched as a historical-round replay (the host set
|
|
98
|
+
* `config.replayMode`). Games read this to hide balance/bet/autoplay UI
|
|
99
|
+
* and surface a "Play / Play Again" CTA only. Falls back to the handshake
|
|
100
|
+
* config when no SDK is present.
|
|
101
|
+
*/
|
|
102
|
+
get isReplay(): boolean {
|
|
103
|
+
return this.sdk?.isReplay ?? this.initData?.config?.replayMode ?? false;
|
|
104
|
+
}
|
|
105
|
+
|
|
96
106
|
/**
|
|
97
107
|
* Send a play request through the SDK and resolve with the host result.
|
|
98
108
|
* Throws if the session was constructed with `sdk: false`.
|
|
@@ -10,6 +10,64 @@ import {
|
|
|
10
10
|
} from '@energy8platform/game-sdk';
|
|
11
11
|
import type { GameDefinition, BetLevelsConfig } from '../lua/types';
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* A detected replay launch — the host wants to re-play a historical round
|
|
15
|
+
* instead of placing live bets. `mode`/`roundId` are forwarded verbatim to
|
|
16
|
+
* {@link ReplayConfig.resolve}.
|
|
17
|
+
*/
|
|
18
|
+
export interface ReplayLaunch {
|
|
19
|
+
/** Game mode the round was recorded in (e.g. "BASE", "BONUS"). */
|
|
20
|
+
mode?: string;
|
|
21
|
+
/** Identifier of the recorded round to replay. */
|
|
22
|
+
roundId?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Opt-in replay support for the DevBridge mock host.
|
|
27
|
+
*
|
|
28
|
+
* In production the casino backend serves a recorded round; in dev the
|
|
29
|
+
* DevBridge IS the host. Provide a `resolve` callback that returns the
|
|
30
|
+
* recorded rounds — DevBridge stays agnostic about where they come from
|
|
31
|
+
* (fetch, static fixtures, localStorage, …). When a replay launch is
|
|
32
|
+
* detected, the bridge flips `config.replayMode = true` and feeds the
|
|
33
|
+
* recorded `PlayResultData[]` back on each play request, without touching
|
|
34
|
+
* the wallet.
|
|
35
|
+
*/
|
|
36
|
+
export interface ReplayConfig {
|
|
37
|
+
/**
|
|
38
|
+
* Resolve the recorded rounds for a replay launch. Receives the `mode`
|
|
39
|
+
* and `roundId` from {@link detect}. May be async (e.g. a fetch).
|
|
40
|
+
*/
|
|
41
|
+
resolve: (
|
|
42
|
+
mode: string | undefined,
|
|
43
|
+
roundId: string | undefined,
|
|
44
|
+
) => PlayResultData[] | Promise<PlayResultData[]>;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Decide whether this launch is a replay and extract its params.
|
|
48
|
+
* Defaults to reading `?replay=1&mode=…&event=…` from the browser URL.
|
|
49
|
+
* Return `null` for a normal (non-replay) launch.
|
|
50
|
+
*/
|
|
51
|
+
detect?: () => ReplayLaunch | null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Default replay detector — reads `?replay=…&mode=…&event=…` from the
|
|
56
|
+
* browser URL. Returns `null` outside the browser or when `replay` is absent
|
|
57
|
+
* or falsy, so the bridge falls back to normal (live) play.
|
|
58
|
+
*/
|
|
59
|
+
function defaultReplayDetect(): ReplayLaunch | null {
|
|
60
|
+
const loc = (globalThis as { location?: { search?: string } }).location;
|
|
61
|
+
if (!loc?.search) return null;
|
|
62
|
+
const params = new URLSearchParams(loc.search);
|
|
63
|
+
const replay = params.get('replay');
|
|
64
|
+
if (!replay || replay === '0' || replay === 'false') return null;
|
|
65
|
+
return {
|
|
66
|
+
mode: params.get('mode') ?? undefined,
|
|
67
|
+
roundId: params.get('event') ?? params.get('roundId') ?? undefined,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
13
71
|
/** Default session TTL when GameDefinition.session_ttl is omitted (24h). */
|
|
14
72
|
const DEFAULT_SESSION_TTL_MS = 24 * 60 * 60 * 1000;
|
|
15
73
|
|
|
@@ -82,9 +140,15 @@ export interface DevBridgeConfig {
|
|
|
82
140
|
gameDefinition?: GameDefinition;
|
|
83
141
|
/** RNG seed for deterministic Lua execution */
|
|
84
142
|
luaSeed?: number;
|
|
143
|
+
/**
|
|
144
|
+
* Opt-in historical-round replay. When the detector reports a replay
|
|
145
|
+
* launch, the bridge serves recorded rounds instead of running Lua/onPlay.
|
|
146
|
+
* See {@link ReplayConfig}.
|
|
147
|
+
*/
|
|
148
|
+
replay?: ReplayConfig;
|
|
85
149
|
}
|
|
86
150
|
|
|
87
|
-
const DEFAULT_CONFIG: Omit<Required<DevBridgeConfig>, 'luaScript' | 'gameDefinition' | 'luaSeed'> = {
|
|
151
|
+
const DEFAULT_CONFIG: Omit<Required<DevBridgeConfig>, 'luaScript' | 'gameDefinition' | 'luaSeed' | 'replay'> = {
|
|
88
152
|
balance: 10000,
|
|
89
153
|
currency: 'USD',
|
|
90
154
|
gameConfig: {
|
|
@@ -128,7 +192,7 @@ const DEFAULT_CONFIG: Omit<Required<DevBridgeConfig>, 'luaScript' | 'gameDefinit
|
|
|
128
192
|
* ```
|
|
129
193
|
*/
|
|
130
194
|
export class DevBridge {
|
|
131
|
-
private _config: Required<Pick<DevBridgeConfig, 'balance' | 'currency' | 'gameConfig' | 'assetsUrl' | 'session' | 'onPlay' | 'networkDelay' | 'debug'>> & Pick<DevBridgeConfig, 'luaScript' | 'gameDefinition' | 'luaSeed'>;
|
|
195
|
+
private _config: Required<Pick<DevBridgeConfig, 'balance' | 'currency' | 'gameConfig' | 'assetsUrl' | 'session' | 'onPlay' | 'networkDelay' | 'debug'>> & Pick<DevBridgeConfig, 'luaScript' | 'gameDefinition' | 'luaSeed' | 'replay'>;
|
|
132
196
|
private _balance: number;
|
|
133
197
|
private _roundCounter = 0;
|
|
134
198
|
private _bridge: Bridge | null = null;
|
|
@@ -141,12 +205,27 @@ export class DevBridge {
|
|
|
141
205
|
private _sessionExpiresAt: number | null = null;
|
|
142
206
|
/** Pre-parsed session TTL from gameDefinition.session_ttl. */
|
|
143
207
|
private _sessionTtlMs: number;
|
|
208
|
+
/** Detected replay launch, or null when not replaying. */
|
|
209
|
+
private _replayLaunch: ReplayLaunch | null = null;
|
|
210
|
+
/** Recorded rounds for the active replay (resolved lazily once). */
|
|
211
|
+
private _replayResults: Promise<PlayResultData[]> | null = null;
|
|
212
|
+
/** Cursor into the recorded rounds; wraps to 0 on "Play Again". */
|
|
213
|
+
private _replayCursor = 0;
|
|
144
214
|
|
|
145
215
|
constructor(config: DevBridgeConfig = {}) {
|
|
146
216
|
this._config = { ...DEFAULT_CONFIG, ...config };
|
|
147
217
|
this._balance = this._config.balance;
|
|
148
218
|
this._useLuaServer = !!(this._config.luaScript && this._config.gameDefinition);
|
|
149
219
|
this._sessionTtlMs = parseSessionTtl(this._config.gameDefinition?.session_ttl);
|
|
220
|
+
if (this._config.replay) {
|
|
221
|
+
const detect = this._config.replay.detect ?? defaultReplayDetect;
|
|
222
|
+
this._replayLaunch = detect();
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** True when this bridge was launched as a historical-round replay. */
|
|
227
|
+
get isReplay(): boolean {
|
|
228
|
+
return this._replayLaunch !== null;
|
|
150
229
|
}
|
|
151
230
|
|
|
152
231
|
/** Current mock balance */
|
|
@@ -218,6 +297,11 @@ export class DevBridge {
|
|
|
218
297
|
// ─── Message Handling ──────────────────────────────────
|
|
219
298
|
|
|
220
299
|
private handleGameReady(id?: string): void {
|
|
300
|
+
if (this._replayLaunch) {
|
|
301
|
+
this.handleReplayGameReady(id);
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
|
|
221
305
|
const initData: InitData = {
|
|
222
306
|
balance: this._balance,
|
|
223
307
|
currency: this._config.currency,
|
|
@@ -229,10 +313,84 @@ export class DevBridge {
|
|
|
229
313
|
this.delayedSend('INIT', initData, id);
|
|
230
314
|
}
|
|
231
315
|
|
|
316
|
+
/**
|
|
317
|
+
* Replay INIT: flip `config.replayMode = true` and take balance/currency
|
|
318
|
+
* from the recorded results (the wallet is never touched in replay).
|
|
319
|
+
*/
|
|
320
|
+
private handleReplayGameReady(id?: string): void {
|
|
321
|
+
this.resolveReplayResults()
|
|
322
|
+
.then((results) => {
|
|
323
|
+
const first = results[0];
|
|
324
|
+
if (first) {
|
|
325
|
+
this._balance = first.balanceAfter;
|
|
326
|
+
}
|
|
327
|
+
const initData: InitData = {
|
|
328
|
+
balance: this._balance,
|
|
329
|
+
currency: first?.currency ?? this._config.currency,
|
|
330
|
+
config: {
|
|
331
|
+
...this._config.gameConfig,
|
|
332
|
+
replayMode: true,
|
|
333
|
+
} as GameConfigData,
|
|
334
|
+
session: null,
|
|
335
|
+
assetsUrl: this._config.assetsUrl,
|
|
336
|
+
};
|
|
337
|
+
this.delayedSend('INIT', initData, id);
|
|
338
|
+
})
|
|
339
|
+
.catch((err) => {
|
|
340
|
+
console.error('[DevBridge] Replay resolve failed:', err);
|
|
341
|
+
this.sendError(id, 'ENGINE_ERROR', err?.message ?? 'replay resolve failed');
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/** Resolve (and cache) the recorded rounds for the active replay launch. */
|
|
346
|
+
private resolveReplayResults(): Promise<PlayResultData[]> {
|
|
347
|
+
if (!this._replayResults) {
|
|
348
|
+
const { mode, roundId } = this._replayLaunch ?? {};
|
|
349
|
+
this._replayResults = Promise.resolve(
|
|
350
|
+
this._config.replay!.resolve(mode, roundId),
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
return this._replayResults;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Replay PLAY_REQUEST: serve the recorded round at the cursor and advance.
|
|
358
|
+
* No wallet movement, no bet/session validation. The first spin past the
|
|
359
|
+
* end resets the cursor to 0 ("Play Again"). An empty record list behaves
|
|
360
|
+
* like an exhausted live session.
|
|
361
|
+
*/
|
|
362
|
+
private handleReplayPlay(id?: string): void {
|
|
363
|
+
this.resolveReplayResults()
|
|
364
|
+
.then((results) => {
|
|
365
|
+
if (!results || results.length === 0) {
|
|
366
|
+
this.sendError(id, 'NO_ACTIVE_SESSION', 'replay has no recorded rounds');
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
// Play Again: a spin past the end wraps back to the first round.
|
|
370
|
+
if (this._replayCursor >= results.length) {
|
|
371
|
+
this._replayCursor = 0;
|
|
372
|
+
}
|
|
373
|
+
const next = results[this._replayCursor++];
|
|
374
|
+
this._lastPlayResult = next;
|
|
375
|
+
// Mirror the recorded balance so GET_BALANCE / HUD stay consistent.
|
|
376
|
+
this._balance = next.balanceAfter;
|
|
377
|
+
this.delayedSend('PLAY_RESULT', next, id);
|
|
378
|
+
})
|
|
379
|
+
.catch((err) => {
|
|
380
|
+
console.error('[DevBridge] Replay resolve failed:', err);
|
|
381
|
+
this.sendError(id, 'ENGINE_ERROR', err?.message ?? 'replay resolve failed');
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
|
|
232
385
|
private handlePlayRequest(
|
|
233
386
|
payload: PlayParams,
|
|
234
387
|
id?: string,
|
|
235
388
|
): void {
|
|
389
|
+
if (this._replayLaunch) {
|
|
390
|
+
this.handleReplayPlay(id);
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
|
|
236
394
|
const { action, bet, params } = payload;
|
|
237
395
|
this._roundCounter++;
|
|
238
396
|
|
package/src/dev-bridge/index.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { DevBridge } from './DevBridge';
|
|
2
|
-
export type { DevBridgeConfig } from './DevBridge';
|
|
2
|
+
export type { DevBridgeConfig, ReplayConfig, ReplayLaunch } from './DevBridge';
|
package/src/index.ts
CHANGED
|
@@ -35,7 +35,7 @@ export type {
|
|
|
35
35
|
|
|
36
36
|
// ─── DevBridge ──────────────────────────────────────────
|
|
37
37
|
export { DevBridge } from './dev-bridge';
|
|
38
|
-
export type { DevBridgeConfig } from './dev-bridge';
|
|
38
|
+
export type { DevBridgeConfig, ReplayConfig, ReplayLaunch } from './dev-bridge';
|
|
39
39
|
|
|
40
40
|
// ─── Branded loading screen ─────────────────────────────
|
|
41
41
|
// Renderer-agnostic CSS preloader showing the Energy8 platform logo.
|