@energy8platform/platform-core 0.16.1 → 0.16.2

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.
@@ -1,5 +1,5 @@
1
1
  import type { PlayParams, SessionData } from '@energy8platform/game-sdk';
2
- import type { LuaEngineConfig, LuaPlayResult, GameDefinition } from './types';
2
+ import type { LuaEngineConfig, LuaPlayResult, GameDefinition, ActionDefinition } from './types';
3
3
  import { LuaEngineAPI, createSeededRng, luaToJS, pushJSValue, cachedToLuastring } from './LuaEngineAPI';
4
4
  import { ActionRouter } from './ActionRouter';
5
5
  import { SessionManager } from './SessionManager';
@@ -193,6 +193,12 @@ export class LuaEngine {
193
193
  }
194
194
  }
195
195
 
196
+ // Apply MapState parity — server's state_mapper.go injects these
197
+ // variable-derived keys into the client data so scripts don't have to
198
+ // surface them manually. Lua-provided values take precedence (server
199
+ // also overwrites variable-derived keys with state.Data on merge).
200
+ this.applyMapStateInjection(stateVars, data);
201
+
196
202
  // 7. Handle _persist_* and _persist_game_* keys
197
203
  this.sessionManager.storePersistData(data);
198
204
  this.persistentState.storeGameData(data);
@@ -214,7 +220,8 @@ export class LuaEngine {
214
220
  }
215
221
 
216
222
  // 8. Evaluate transitions (server uses state.Variables which is stateVars)
217
- const { rule, nextActions } = this.actionRouter.evaluateTransitions(action, stateVars);
223
+ const { rule } = this.actionRouter.evaluateTransitions(action, stateVars);
224
+ let nextActions = rule.next_actions;
218
225
 
219
226
  // 9. Determine credit behavior (server: creditNow logic)
220
227
  let creditDeferred = action.credit === 'defer' || rule.credit_override === 'defer';
@@ -227,9 +234,15 @@ export class LuaEngine {
227
234
  // Calculate max win cap for session
228
235
  const maxWinCap = this.calculateMaxWinCap(bet);
229
236
 
237
+ // Snapshot the round data for history (matches server's MapStateForHistory:
238
+ // strip _persist_* keys, but those are already removed below before
239
+ // returning — at this point in the flow they may still be in `data`,
240
+ // so we filter inline).
241
+ const roundData = stripPersistKeys(data);
242
+
230
243
  if (rule.creates_session && !this.sessionManager.isActive) {
231
244
  // CREATE SESSION — initial spin counted (server: createSession includes spinWin)
232
- session = this.sessionManager.createSession(rule, stateVars, bet, spinWin, maxWinCap);
245
+ session = this.sessionManager.createSession(rule, stateVars, bet, spinWin, maxWinCap, roundData);
233
246
  creditDeferred = true;
234
247
  resultTotalWin = spinWin;
235
248
 
@@ -239,16 +252,23 @@ export class LuaEngine {
239
252
  }
240
253
  } else if (this.sessionManager.isActive) {
241
254
  // UPDATE SESSION — accumulate win, check completion
242
- session = this.sessionManager.updateSession(rule, stateVars, spinWin);
255
+ session = this.sessionManager.updateSession(rule, stateVars, spinWin, roundData);
243
256
 
244
257
  if (session?.completed) {
245
- // SESSION COMPLETED — server returns session.TotalWin as result.TotalWin
258
+ // SESSION COMPLETED — server returns session.TotalWin as result.TotalWin,
259
+ // and pulls next_actions from the explicit completion transition
260
+ // (findCompletionNextActions) rather than the matched 'continue' rule.
246
261
  const completed = this.sessionManager.completeSession();
247
262
  session = completed.session;
248
263
  resultTotalWin = completed.totalWin;
249
264
  sessionCompleted = true;
250
265
  creditDeferred = false;
251
266
 
267
+ const completionNext = findCompletionNextActions(action);
268
+ if (completionNext) {
269
+ nextActions = completionNext;
270
+ }
271
+
252
272
  // Clean up session-scoped variables
253
273
  for (const varName of completed.sessionVarNames) {
254
274
  delete this.variables[varName];
@@ -386,6 +406,35 @@ export class LuaEngine {
386
406
  return result as Record<string, unknown>;
387
407
  }
388
408
 
409
+ /**
410
+ * Mirror server's state_mapper.go MapState — surface variable-derived
411
+ * fields so scripts that don't manually echo them in the result table
412
+ * still produce a server-shaped data map. Lua keys win on conflict.
413
+ */
414
+ private applyMapStateInjection(
415
+ vars: Record<string, number>,
416
+ data: Record<string, unknown>,
417
+ ): void {
418
+ const m = vars.multiplier;
419
+ if (typeof m === 'number' && m > 1 && data.multiplier === undefined) {
420
+ data.multiplier = m;
421
+ }
422
+
423
+ const gm = vars.global_multiplier;
424
+ if (typeof gm === 'number' && gm > 1 && data.global_multiplier === undefined) {
425
+ data.global_multiplier = gm;
426
+ }
427
+
428
+ const fs = vars.free_spins_remaining;
429
+ if (typeof fs === 'number' && fs > 0 && data.free_spins_total === undefined) {
430
+ data.free_spins_total = Math.trunc(fs);
431
+ }
432
+
433
+ if (vars.max_win_reached === 1 && data.max_win_reached === undefined) {
434
+ data.max_win_reached = true;
435
+ }
436
+ }
437
+
389
438
  private calculateMaxWinCap(bet: number): number | undefined {
390
439
  const mw = this.gameDefinition.max_win;
391
440
  if (!mw) return undefined;
@@ -410,3 +459,37 @@ export class LuaEngine {
410
459
  return parseInt(entries[entries.length - 1][0], 10);
411
460
  }
412
461
  }
462
+
463
+ // ─── Module helpers ─────────────────────────────────────
464
+
465
+ /**
466
+ * Strip _persist_* and _persist_game_* keys from a data map — matches
467
+ * server's MapStateForHistory used when recording session round history.
468
+ */
469
+ function stripPersistKeys(data: Record<string, unknown>): Record<string, unknown> {
470
+ const out: Record<string, unknown> = {};
471
+ for (const k of Object.keys(data)) {
472
+ if (k.startsWith('_persist_') || k.startsWith('_persist_game_')) continue;
473
+ out[k] = data[k];
474
+ }
475
+ return out;
476
+ }
477
+
478
+ /**
479
+ * Mirror server's findCompletionNextActions: when a session naturally
480
+ * completes, the matched 'continue' rule's next_actions are NOT what
481
+ * the client should see — the explicit complete_session transition wins,
482
+ * with a fallback to the 'always' transition.
483
+ */
484
+ function findCompletionNextActions(action: ActionDefinition): string[] | null {
485
+ let alwaysFallback: string[] | null = null;
486
+ for (const t of action.transitions) {
487
+ if (t.complete_session && t.next_actions && t.next_actions.length > 0) {
488
+ return t.next_actions;
489
+ }
490
+ if (t.condition.trim() === 'always' && t.next_actions && t.next_actions.length > 0 && alwaysFallback === null) {
491
+ alwaysFallback = t.next_actions;
492
+ }
493
+ }
494
+ return alwaysFallback;
495
+ }
@@ -1,6 +1,13 @@
1
1
  import type { SessionData } from '@energy8platform/game-sdk';
2
2
  import type { TransitionRule } from './types';
3
3
 
4
+ /** Per-round history entry — wire shape mirrors server SessionInfo.History. */
5
+ export interface SessionRound {
6
+ spinIndex: number;
7
+ win: number;
8
+ data: Record<string, unknown>;
9
+ }
10
+
4
11
  interface SessionState {
5
12
  spinsRemaining: number;
6
13
  spinsPlayed: number;
@@ -13,6 +20,7 @@ interface SessionState {
13
20
  persistentVarNames: string[];
14
21
  persistentVars: Record<string, number>;
15
22
  persistentData: Record<string, unknown>;
23
+ history: SessionRound[];
16
24
  }
17
25
 
18
26
  const MAX_SESSION_SPINS = 200;
@@ -56,7 +64,8 @@ export class SessionManager {
56
64
 
57
65
  /**
58
66
  * Create a new session from a transition rule.
59
- * Server behavior: initial spin is already counted (spinsPlayed=1, totalWin includes spinWin).
67
+ * Server behavior: initial spin is already counted (spinsPlayed=1, totalWin includes spinWin),
68
+ * and history[0] holds the trigger spin's data.
60
69
  */
61
70
  createSession(
62
71
  rule: TransitionRule,
@@ -64,6 +73,7 @@ export class SessionManager {
64
73
  bet: number,
65
74
  spinWin: number,
66
75
  maxWinCap: number | undefined,
76
+ triggerData: Record<string, unknown>,
67
77
  ): SessionData {
68
78
  let spinsRemaining = -1;
69
79
  let spinsVarName: string | undefined;
@@ -90,6 +100,7 @@ export class SessionManager {
90
100
  persistentVarNames,
91
101
  persistentVars,
92
102
  persistentData: {},
103
+ history: [{ spinIndex: 0, win: spinWin, data: triggerData }],
93
104
  };
94
105
 
95
106
  return this.toSessionData();
@@ -104,9 +115,18 @@ export class SessionManager {
104
115
  rule: TransitionRule,
105
116
  variables: Record<string, number>,
106
117
  spinWin: number,
118
+ roundData: Record<string, unknown>,
107
119
  ): SessionData {
108
120
  if (!this.session) throw new Error('No active session');
109
121
 
122
+ // Append history with PRE-increment spin index (matches server's
123
+ // updateSession: append round, THEN increment SpinsPlayed).
124
+ this.session.history.push({
125
+ spinIndex: this.session.spinsPlayed,
126
+ win: spinWin,
127
+ data: roundData,
128
+ });
129
+
110
130
  // Accumulate win and count spin
111
131
  this.session.totalWin += spinWin;
112
132
  this.session.spinsPlayed++;
@@ -222,6 +242,8 @@ export class SessionManager {
222
242
  completed: this.session.completed,
223
243
  maxWinReached: this.session.maxWinReached,
224
244
  betAmount: this.session.bet,
245
+ // Snapshot the array so downstream mutation can't corrupt internal state.
246
+ history: [...this.session.history],
225
247
  };
226
248
  }
227
249
  }
package/src/lua/types.ts CHANGED
@@ -11,6 +11,12 @@ export interface GameDefinition {
11
11
  buy_bonus?: BuyBonusConfig;
12
12
  ante_bet?: AnteBetConfig;
13
13
  persistent_state?: PersistentStateConfig;
14
+ /**
15
+ * Session expiry duration as a Go-style duration string ("24h", "2h", "5ms").
16
+ * Mirrors the platform's GameDefinition.SessionTTL — defaults to 24h on
17
+ * the server when absent. Used by DevBridge to surface SESSION_EXPIRED.
18
+ */
19
+ session_ttl?: string;
14
20
  }
15
21
 
16
22
  export interface BetLevelsConfig {