@energy8platform/game-engine 0.10.7 → 0.10.9
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/dist/debug.cjs.js +11 -7
- package/dist/debug.cjs.js.map +1 -1
- package/dist/debug.esm.js +11 -7
- package/dist/debug.esm.js.map +1 -1
- package/dist/index.cjs.js +11 -7
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.esm.js +11 -7
- package/dist/index.esm.js.map +1 -1
- package/dist/lua.cjs.js +151 -108
- package/dist/lua.cjs.js.map +1 -1
- package/dist/lua.d.ts +29 -27
- package/dist/lua.esm.js +151 -108
- package/dist/lua.esm.js.map +1 -1
- package/package.json +1 -1
- package/src/debug/DevBridge.ts +12 -8
- package/src/lua/LuaEngine.ts +94 -81
- package/src/lua/SessionManager.ts +74 -29
package/dist/lua.d.ts
CHANGED
|
@@ -113,19 +113,9 @@ interface SimulationResult {
|
|
|
113
113
|
/**
|
|
114
114
|
* Runs Lua game scripts locally, replicating the platform's server-side execution.
|
|
115
115
|
*
|
|
116
|
-
* Implements the full lifecycle
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
* @example
|
|
120
|
-
* ```ts
|
|
121
|
-
* const engine = new LuaEngine({
|
|
122
|
-
* script: luaSource,
|
|
123
|
-
* gameDefinition: { id: 'my-slot', type: 'SLOT', actions: { ... } },
|
|
124
|
-
* });
|
|
125
|
-
*
|
|
126
|
-
* const result = engine.execute({ action: 'spin', bet: 1.0 });
|
|
127
|
-
* // result.data.matrix, result.totalWin, result.nextActions, etc.
|
|
128
|
-
* ```
|
|
116
|
+
* Implements the full lifecycle matching `casino_platform/internal/usecase/game_usecase.go`:
|
|
117
|
+
* action routing → state assembly → Lua execute() → result extraction →
|
|
118
|
+
* transition evaluation → session management.
|
|
129
119
|
*/
|
|
130
120
|
declare class LuaEngine {
|
|
131
121
|
private L;
|
|
@@ -137,18 +127,13 @@ declare class LuaEngine {
|
|
|
137
127
|
private variables;
|
|
138
128
|
private simulationMode;
|
|
139
129
|
constructor(config: LuaEngineConfig);
|
|
140
|
-
/** Current session data (if any) */
|
|
141
130
|
get session(): SessionData | null;
|
|
142
|
-
/** Current persistent state values */
|
|
143
131
|
get persistentVars(): Record<string, number>;
|
|
144
132
|
/**
|
|
145
|
-
* Execute a play action —
|
|
146
|
-
* This is what DevBridge calls on each PLAY_REQUEST.
|
|
133
|
+
* Execute a play action — replicates server's Play() function.
|
|
147
134
|
*/
|
|
148
135
|
execute(params: PlayParams): LuaPlayResult;
|
|
149
|
-
/** Reset all state (sessions, persistent vars, variables) */
|
|
150
136
|
reset(): void;
|
|
151
|
-
/** Destroy the Lua VM */
|
|
152
137
|
destroy(): void;
|
|
153
138
|
private loadScript;
|
|
154
139
|
private callLuaExecute;
|
|
@@ -210,20 +195,37 @@ declare class ActionRouter {
|
|
|
210
195
|
declare function evaluateCondition(condition: string, variables: Record<string, number>): boolean;
|
|
211
196
|
|
|
212
197
|
/**
|
|
213
|
-
* Manages session lifecycle
|
|
214
|
-
*
|
|
215
|
-
*
|
|
198
|
+
* Manages session lifecycle matching the platform server behavior:
|
|
199
|
+
* - createSession: initial spin counted (spinsPlayed=1, totalWin=spinWin)
|
|
200
|
+
* - updateSession: accumulates win, decrements spins, checks max win cap on session level
|
|
201
|
+
* - completeSession: returns cumulative totalWin, cleans up session vars
|
|
202
|
+
* - Safety cap: 200 spins max per session
|
|
216
203
|
*/
|
|
217
204
|
declare class SessionManager {
|
|
218
205
|
private session;
|
|
219
206
|
get isActive(): boolean;
|
|
220
207
|
get current(): SessionData | null;
|
|
221
208
|
get sessionTotalWin(): number;
|
|
222
|
-
/**
|
|
223
|
-
|
|
224
|
-
/**
|
|
209
|
+
/** Get the fixed bet amount from the session (server uses session bet, not request bet) */
|
|
210
|
+
get sessionBet(): number | undefined;
|
|
211
|
+
/** Get spinsVarName to restore free_spins_remaining into variables */
|
|
212
|
+
get spinsVarName(): string | undefined;
|
|
213
|
+
get spinsRemaining(): number;
|
|
214
|
+
/**
|
|
215
|
+
* Create a new session from a transition rule.
|
|
216
|
+
* Server behavior: initial spin is already counted (spinsPlayed=1, totalWin includes spinWin).
|
|
217
|
+
*/
|
|
218
|
+
createSession(rule: TransitionRule, variables: Record<string, number>, bet: number, spinWin: number, maxWinCap: number | undefined): SessionData;
|
|
219
|
+
/**
|
|
220
|
+
* Update session after a bonus spin.
|
|
221
|
+
* Server behavior: accumulate win, decrement spins, check retrigger, check max win cap,
|
|
222
|
+
* safety cap at 200 spins.
|
|
223
|
+
*/
|
|
225
224
|
updateSession(rule: TransitionRule, variables: Record<string, number>, spinWin: number): SessionData;
|
|
226
|
-
/**
|
|
225
|
+
/**
|
|
226
|
+
* Complete the session explicitly.
|
|
227
|
+
* Returns cumulative totalWin and list of session-scoped var names to clean up.
|
|
228
|
+
*/
|
|
227
229
|
completeSession(): {
|
|
228
230
|
totalWin: number;
|
|
229
231
|
session: SessionData;
|
|
@@ -233,7 +235,7 @@ declare class SessionManager {
|
|
|
233
235
|
markMaxWinReached(): void;
|
|
234
236
|
/** Store _persist_* data extracted from Lua result */
|
|
235
237
|
storePersistData(data: Record<string, unknown>): void;
|
|
236
|
-
/** Get
|
|
238
|
+
/** Get persistent params to inject into next execute() call */
|
|
237
239
|
getPersistentParams(): Record<string, unknown>;
|
|
238
240
|
/** Reset all session state */
|
|
239
241
|
reset(): void;
|
package/dist/lua.esm.js
CHANGED
|
@@ -369,10 +369,13 @@ function evaluateComparison(expr, variables) {
|
|
|
369
369
|
}
|
|
370
370
|
}
|
|
371
371
|
|
|
372
|
+
const MAX_SESSION_SPINS = 200;
|
|
372
373
|
/**
|
|
373
|
-
* Manages session lifecycle
|
|
374
|
-
*
|
|
375
|
-
*
|
|
374
|
+
* Manages session lifecycle matching the platform server behavior:
|
|
375
|
+
* - createSession: initial spin counted (spinsPlayed=1, totalWin=spinWin)
|
|
376
|
+
* - updateSession: accumulates win, decrements spins, checks max win cap on session level
|
|
377
|
+
* - completeSession: returns cumulative totalWin, cleans up session vars
|
|
378
|
+
* - Safety cap: 200 spins max per session
|
|
376
379
|
*/
|
|
377
380
|
class SessionManager {
|
|
378
381
|
session = null;
|
|
@@ -387,36 +390,57 @@ class SessionManager {
|
|
|
387
390
|
get sessionTotalWin() {
|
|
388
391
|
return this.session?.totalWin ?? 0;
|
|
389
392
|
}
|
|
390
|
-
/**
|
|
391
|
-
|
|
392
|
-
|
|
393
|
+
/** Get the fixed bet amount from the session (server uses session bet, not request bet) */
|
|
394
|
+
get sessionBet() {
|
|
395
|
+
return this.session?.bet;
|
|
396
|
+
}
|
|
397
|
+
/** Get spinsVarName to restore free_spins_remaining into variables */
|
|
398
|
+
get spinsVarName() {
|
|
399
|
+
return this.session?.spinsVarName;
|
|
400
|
+
}
|
|
401
|
+
get spinsRemaining() {
|
|
402
|
+
return this.session?.spinsRemaining ?? 0;
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Create a new session from a transition rule.
|
|
406
|
+
* Server behavior: initial spin is already counted (spinsPlayed=1, totalWin includes spinWin).
|
|
407
|
+
*/
|
|
408
|
+
createSession(rule, variables, bet, spinWin, maxWinCap) {
|
|
409
|
+
let spinsRemaining = -1;
|
|
410
|
+
let spinsVarName;
|
|
393
411
|
if (rule.session_config?.total_spins_var) {
|
|
394
|
-
|
|
395
|
-
spinsRemaining = variables[
|
|
412
|
+
spinsVarName = rule.session_config.total_spins_var;
|
|
413
|
+
spinsRemaining = variables[spinsVarName] ?? -1;
|
|
396
414
|
}
|
|
415
|
+
const persistentVarNames = rule.session_config?.persistent_vars ?? [];
|
|
397
416
|
const persistentVars = {};
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
persistentVars[varName] = variables[varName] ?? 0;
|
|
401
|
-
}
|
|
417
|
+
for (const varName of persistentVarNames) {
|
|
418
|
+
persistentVars[varName] = variables[varName] ?? 0;
|
|
402
419
|
}
|
|
403
420
|
this.session = {
|
|
404
421
|
spinsRemaining,
|
|
405
|
-
spinsPlayed:
|
|
406
|
-
totalWin:
|
|
422
|
+
spinsPlayed: 1, // initial spin counts
|
|
423
|
+
totalWin: spinWin, // initial spin win included
|
|
407
424
|
completed: false,
|
|
408
425
|
maxWinReached: false,
|
|
409
426
|
bet,
|
|
427
|
+
maxWinCap,
|
|
428
|
+
spinsVarName,
|
|
429
|
+
persistentVarNames,
|
|
410
430
|
persistentVars,
|
|
411
431
|
persistentData: {},
|
|
412
432
|
};
|
|
413
433
|
return this.toSessionData();
|
|
414
434
|
}
|
|
415
|
-
/**
|
|
435
|
+
/**
|
|
436
|
+
* Update session after a bonus spin.
|
|
437
|
+
* Server behavior: accumulate win, decrement spins, check retrigger, check max win cap,
|
|
438
|
+
* safety cap at 200 spins.
|
|
439
|
+
*/
|
|
416
440
|
updateSession(rule, variables, spinWin) {
|
|
417
441
|
if (!this.session)
|
|
418
442
|
throw new Error('No active session');
|
|
419
|
-
// Accumulate win
|
|
443
|
+
// Accumulate win and count spin
|
|
420
444
|
this.session.totalWin += spinWin;
|
|
421
445
|
this.session.spinsPlayed++;
|
|
422
446
|
// Decrement spins (only for non-unlimited sessions)
|
|
@@ -430,29 +454,39 @@ class SessionManager {
|
|
|
430
454
|
this.session.spinsRemaining += extraSpins;
|
|
431
455
|
}
|
|
432
456
|
}
|
|
433
|
-
//
|
|
434
|
-
if (this.session.
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
457
|
+
// Safety cap: server limits sessions to 200 spins
|
|
458
|
+
if (this.session.spinsPlayed >= MAX_SESSION_SPINS) {
|
|
459
|
+
this.session.spinsRemaining = 0;
|
|
460
|
+
}
|
|
461
|
+
// Update session persistent vars from current variables
|
|
462
|
+
for (const varName of this.session.persistentVarNames) {
|
|
463
|
+
if (varName in variables) {
|
|
464
|
+
this.session.persistentVars[varName] = variables[varName];
|
|
439
465
|
}
|
|
440
466
|
}
|
|
441
|
-
//
|
|
442
|
-
if (this.session.
|
|
467
|
+
// Check max win cap (on session level, not per spin)
|
|
468
|
+
if (this.session.maxWinCap !== undefined && this.session.totalWin >= this.session.maxWinCap) {
|
|
469
|
+
this.session.totalWin = this.session.maxWinCap;
|
|
470
|
+
this.session.spinsRemaining = 0;
|
|
471
|
+
this.session.maxWinReached = true;
|
|
472
|
+
}
|
|
473
|
+
// Auto-complete if spins exhausted or explicit complete
|
|
474
|
+
if (this.session.spinsRemaining === 0 || rule.complete_session) {
|
|
443
475
|
this.session.completed = true;
|
|
444
476
|
}
|
|
445
477
|
return this.toSessionData();
|
|
446
478
|
}
|
|
447
|
-
/**
|
|
479
|
+
/**
|
|
480
|
+
* Complete the session explicitly.
|
|
481
|
+
* Returns cumulative totalWin and list of session-scoped var names to clean up.
|
|
482
|
+
*/
|
|
448
483
|
completeSession() {
|
|
449
484
|
if (!this.session)
|
|
450
485
|
throw new Error('No active session to complete');
|
|
451
486
|
this.session.completed = true;
|
|
452
487
|
const totalWin = this.session.totalWin;
|
|
453
488
|
const session = this.toSessionData();
|
|
454
|
-
const sessionVarNames =
|
|
455
|
-
// Clear session state so it doesn't leak into the next round
|
|
489
|
+
const sessionVarNames = [...this.session.persistentVarNames];
|
|
456
490
|
this.session = null;
|
|
457
491
|
return { totalWin, session, sessionVarNames };
|
|
458
492
|
}
|
|
@@ -474,16 +508,16 @@ class SessionManager {
|
|
|
474
508
|
}
|
|
475
509
|
}
|
|
476
510
|
}
|
|
477
|
-
/** Get
|
|
511
|
+
/** Get persistent params to inject into next execute() call */
|
|
478
512
|
getPersistentParams() {
|
|
479
513
|
if (!this.session)
|
|
480
514
|
return {};
|
|
481
515
|
const params = {};
|
|
482
|
-
// Session persistent vars (float64)
|
|
516
|
+
// Session persistent vars (float64) → state.variables
|
|
483
517
|
for (const [key, value] of Object.entries(this.session.persistentVars)) {
|
|
484
518
|
params[key] = value;
|
|
485
519
|
}
|
|
486
|
-
// _persist_ complex data → _ps_*
|
|
520
|
+
// _persist_ complex data → _ps_* in state.params
|
|
487
521
|
for (const [key, value] of Object.entries(this.session.persistentData)) {
|
|
488
522
|
params[`_ps_${key}`] = value;
|
|
489
523
|
}
|
|
@@ -581,22 +615,20 @@ class PersistentState {
|
|
|
581
615
|
|
|
582
616
|
const { lua, lauxlib, lualib } = fengari;
|
|
583
617
|
const { to_luastring, to_jsstring } = fengari;
|
|
618
|
+
/** Default engine variables matching the server's NewGameState() */
|
|
619
|
+
const DEFAULT_VARIABLES = {
|
|
620
|
+
multiplier: 1,
|
|
621
|
+
total_multiplier: 1,
|
|
622
|
+
global_multiplier: 1,
|
|
623
|
+
last_win_amount: 0,
|
|
624
|
+
free_spins_awarded: 0,
|
|
625
|
+
};
|
|
584
626
|
/**
|
|
585
627
|
* Runs Lua game scripts locally, replicating the platform's server-side execution.
|
|
586
628
|
*
|
|
587
|
-
* Implements the full lifecycle
|
|
588
|
-
*
|
|
589
|
-
*
|
|
590
|
-
* @example
|
|
591
|
-
* ```ts
|
|
592
|
-
* const engine = new LuaEngine({
|
|
593
|
-
* script: luaSource,
|
|
594
|
-
* gameDefinition: { id: 'my-slot', type: 'SLOT', actions: { ... } },
|
|
595
|
-
* });
|
|
596
|
-
*
|
|
597
|
-
* const result = engine.execute({ action: 'spin', bet: 1.0 });
|
|
598
|
-
* // result.data.matrix, result.totalWin, result.nextActions, etc.
|
|
599
|
-
* ```
|
|
629
|
+
* Implements the full lifecycle matching `casino_platform/internal/usecase/game_usecase.go`:
|
|
630
|
+
* action routing → state assembly → Lua execute() → result extraction →
|
|
631
|
+
* transition evaluation → session management.
|
|
600
632
|
*/
|
|
601
633
|
class LuaEngine {
|
|
602
634
|
L;
|
|
@@ -610,20 +642,16 @@ class LuaEngine {
|
|
|
610
642
|
constructor(config) {
|
|
611
643
|
this.gameDefinition = config.gameDefinition;
|
|
612
644
|
this.simulationMode = config.simulationMode ?? false;
|
|
613
|
-
// Set up RNG
|
|
614
645
|
const rng = config.seed !== undefined
|
|
615
646
|
? createSeededRng(config.seed)
|
|
616
647
|
: undefined;
|
|
617
|
-
// Initialize sub-managers
|
|
618
648
|
this.api = new LuaEngineAPI(config.gameDefinition, rng, config.logger);
|
|
619
649
|
this.actionRouter = new ActionRouter(config.gameDefinition);
|
|
620
650
|
this.sessionManager = new SessionManager();
|
|
621
651
|
this.persistentState = new PersistentState(config.gameDefinition.persistent_state);
|
|
622
|
-
// Create Lua state and load standard libraries
|
|
623
652
|
this.L = lauxlib.luaL_newstate();
|
|
624
653
|
lualib.luaL_openlibs(this.L);
|
|
625
|
-
// Polyfill Lua 5.1/5.2 functions removed in 5.3
|
|
626
|
-
// Platform server may use gopher-lua (5.1) — these ensure compatibility.
|
|
654
|
+
// Polyfill Lua 5.1/5.2 functions removed in 5.3
|
|
627
655
|
lauxlib.luaL_dostring(this.L, to_luastring(`
|
|
628
656
|
math.pow = function(a, b) return a ^ b end
|
|
629
657
|
math.atan2 = math.atan2 or function(y, x) return math.atan(y, x) end
|
|
@@ -641,32 +669,36 @@ class LuaEngine {
|
|
|
641
669
|
loadstring = loadstring or load
|
|
642
670
|
table.getn = table.getn or function(t) return #t end
|
|
643
671
|
`));
|
|
644
|
-
// Register engine.* API
|
|
645
672
|
this.api.register(this.L);
|
|
646
|
-
// Load and compile the script
|
|
647
673
|
this.loadScript(config.script);
|
|
648
674
|
}
|
|
649
|
-
/** Current session data (if any) */
|
|
650
675
|
get session() {
|
|
651
676
|
return this.sessionManager.current;
|
|
652
677
|
}
|
|
653
|
-
/** Current persistent state values */
|
|
654
678
|
get persistentVars() {
|
|
655
679
|
return { ...this.variables };
|
|
656
680
|
}
|
|
657
681
|
/**
|
|
658
|
-
* Execute a play action —
|
|
659
|
-
* This is what DevBridge calls on each PLAY_REQUEST.
|
|
682
|
+
* Execute a play action — replicates server's Play() function.
|
|
660
683
|
*/
|
|
661
684
|
execute(params) {
|
|
662
|
-
const { action: actionName,
|
|
663
|
-
// 1. Resolve
|
|
685
|
+
const { action: actionName, params: clientParams } = params;
|
|
686
|
+
// 1. Resolve action
|
|
664
687
|
const action = this.actionRouter.resolveAction(actionName, this.sessionManager.isActive);
|
|
665
|
-
// 2.
|
|
666
|
-
|
|
688
|
+
// 2. Determine bet — server uses session bet for session actions
|
|
689
|
+
let bet = params.bet;
|
|
690
|
+
if (this.sessionManager.isActive && this.sessionManager.sessionBet !== undefined) {
|
|
691
|
+
bet = this.sessionManager.sessionBet;
|
|
692
|
+
}
|
|
693
|
+
// 3. Build state.variables (matching server's NewGameState + restore)
|
|
694
|
+
const stateVars = {
|
|
695
|
+
...DEFAULT_VARIABLES,
|
|
696
|
+
...this.variables,
|
|
697
|
+
bet,
|
|
698
|
+
};
|
|
667
699
|
// Load cross-spin persistent state
|
|
668
700
|
this.persistentState.loadIntoVariables(stateVars);
|
|
669
|
-
// Load session persistent vars
|
|
701
|
+
// Load session persistent vars + restore spinsRemaining
|
|
670
702
|
if (this.sessionManager.isActive) {
|
|
671
703
|
const sessionParams = this.sessionManager.getPersistentParams();
|
|
672
704
|
for (const [k, v] of Object.entries(sessionParams)) {
|
|
@@ -674,8 +706,14 @@ class LuaEngine {
|
|
|
674
706
|
stateVars[k] = v;
|
|
675
707
|
}
|
|
676
708
|
}
|
|
709
|
+
// Restore spinsRemaining into the variable the script reads
|
|
710
|
+
if (this.sessionManager.spinsVarName) {
|
|
711
|
+
stateVars[this.sessionManager.spinsVarName] = this.sessionManager.spinsRemaining;
|
|
712
|
+
}
|
|
713
|
+
// Also set free_spins_remaining for convenience
|
|
714
|
+
stateVars.free_spins_remaining = this.sessionManager.spinsRemaining;
|
|
677
715
|
}
|
|
678
|
-
//
|
|
716
|
+
// 4. Build state.params
|
|
679
717
|
const stateParams = { ...clientParams };
|
|
680
718
|
stateParams._action = actionName;
|
|
681
719
|
// Inject session _ps_* persistent data
|
|
@@ -703,16 +741,18 @@ class LuaEngine {
|
|
|
703
741
|
if (clientParams?.ante_bet && this.gameDefinition.ante_bet) {
|
|
704
742
|
stateParams.ante_bet = true;
|
|
705
743
|
}
|
|
706
|
-
//
|
|
707
|
-
const luaResult = this.callLuaExecute(action.stage, stateParams, stateVars);
|
|
708
|
-
//
|
|
744
|
+
// 5. Execute Lua (server: executor.Execute(stage, state))
|
|
745
|
+
const luaResult = this.callLuaExecute(action.stage, actionName, stateParams, stateVars);
|
|
746
|
+
// 6. Process result (server: ApplyLuaResult)
|
|
709
747
|
const totalWinMultiplier = typeof luaResult.total_win === 'number' ? luaResult.total_win : 0;
|
|
710
748
|
const resultVariables = (luaResult.variables ?? {});
|
|
711
|
-
const
|
|
712
|
-
// Merge
|
|
749
|
+
const spinWin = Math.round(totalWinMultiplier * bet * 100) / 100;
|
|
750
|
+
// Merge ONLY Lua return variables into engine state (not the whole stateVars).
|
|
751
|
+
// On the server, state.Variables is a temporary object rebuilt each call.
|
|
752
|
+
// Only the Lua result's `variables` table persists between calls.
|
|
753
|
+
Object.assign(this.variables, resultVariables);
|
|
754
|
+
// Also update stateVars for transition evaluation below
|
|
713
755
|
Object.assign(stateVars, resultVariables);
|
|
714
|
-
this.variables = { ...stateVars };
|
|
715
|
-
delete this.variables.bet; // bet is per-spin, not persistent
|
|
716
756
|
// Build client data (everything except special keys)
|
|
717
757
|
const data = {};
|
|
718
758
|
for (const [key, value] of Object.entries(luaResult)) {
|
|
@@ -720,22 +760,11 @@ class LuaEngine {
|
|
|
720
760
|
data[key] = value;
|
|
721
761
|
}
|
|
722
762
|
}
|
|
723
|
-
// 6. Apply max win cap
|
|
724
|
-
let cappedWin = totalWin;
|
|
725
|
-
if (this.gameDefinition.max_win) {
|
|
726
|
-
const cap = this.calculateMaxWinCap(bet);
|
|
727
|
-
if (cap !== undefined && totalWin > cap) {
|
|
728
|
-
cappedWin = cap;
|
|
729
|
-
this.variables.max_win_reached = 1;
|
|
730
|
-
data.max_win_reached = true;
|
|
731
|
-
this.sessionManager.markMaxWinReached();
|
|
732
|
-
}
|
|
733
|
-
}
|
|
734
763
|
// 7. Handle _persist_* and _persist_game_* keys
|
|
735
764
|
this.sessionManager.storePersistData(data);
|
|
736
765
|
this.persistentState.storeGameData(data);
|
|
737
|
-
// Save cross-spin persistent state
|
|
738
|
-
this.persistentState.saveFromVariables(
|
|
766
|
+
// Save cross-spin persistent state (from stateVars which has Lua result merged)
|
|
767
|
+
this.persistentState.saveFromVariables(stateVars);
|
|
739
768
|
// Add exposed persistent vars to client data
|
|
740
769
|
const exposedVars = this.persistentState.getExposedVars();
|
|
741
770
|
if (exposedVars) {
|
|
@@ -747,36 +776,56 @@ class LuaEngine {
|
|
|
747
776
|
delete data[key];
|
|
748
777
|
}
|
|
749
778
|
}
|
|
750
|
-
// 8. Evaluate transitions
|
|
751
|
-
const { rule, nextActions } = this.actionRouter.evaluateTransitions(action,
|
|
779
|
+
// 8. Evaluate transitions (server uses state.Variables which is stateVars)
|
|
780
|
+
const { rule, nextActions } = this.actionRouter.evaluateTransitions(action, stateVars);
|
|
781
|
+
// 9. Determine credit behavior (server: creditNow logic)
|
|
752
782
|
let creditDeferred = action.credit === 'defer' || rule.credit_override === 'defer';
|
|
783
|
+
// 10. Session lifecycle (server: create/update/complete session)
|
|
753
784
|
let session = this.sessionManager.current;
|
|
754
|
-
|
|
785
|
+
let resultTotalWin = spinWin;
|
|
786
|
+
let sessionCompleted = false;
|
|
787
|
+
// Calculate max win cap for session
|
|
788
|
+
const maxWinCap = this.calculateMaxWinCap(bet);
|
|
755
789
|
if (rule.creates_session && !this.sessionManager.isActive) {
|
|
756
|
-
|
|
790
|
+
// CREATE SESSION — initial spin counted (server: createSession includes spinWin)
|
|
791
|
+
session = this.sessionManager.createSession(rule, stateVars, bet, spinWin, maxWinCap);
|
|
757
792
|
creditDeferred = true;
|
|
758
|
-
|
|
759
|
-
//
|
|
793
|
+
resultTotalWin = spinWin;
|
|
794
|
+
// Clear the trigger variable — it was consumed to set spinsRemaining
|
|
760
795
|
if (rule.session_config?.total_spins_var) {
|
|
761
796
|
delete this.variables[rule.session_config.total_spins_var];
|
|
762
797
|
}
|
|
763
798
|
}
|
|
764
|
-
// Handle session update
|
|
765
799
|
else if (this.sessionManager.isActive) {
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
if (
|
|
800
|
+
// UPDATE SESSION — accumulate win, check completion
|
|
801
|
+
session = this.sessionManager.updateSession(rule, stateVars, spinWin);
|
|
802
|
+
if (session?.completed) {
|
|
803
|
+
// SESSION COMPLETED — server returns session.TotalWin as result.TotalWin
|
|
769
804
|
const completed = this.sessionManager.completeSession();
|
|
770
805
|
session = completed.session;
|
|
806
|
+
resultTotalWin = completed.totalWin;
|
|
807
|
+
sessionCompleted = true;
|
|
771
808
|
creditDeferred = false;
|
|
772
|
-
// Clean up session-scoped variables
|
|
809
|
+
// Clean up session-scoped variables
|
|
773
810
|
for (const varName of completed.sessionVarNames) {
|
|
774
811
|
delete this.variables[varName];
|
|
775
812
|
}
|
|
776
813
|
}
|
|
814
|
+
else {
|
|
815
|
+
// Mid-session: totalWin = spinWin, credit deferred
|
|
816
|
+
resultTotalWin = spinWin;
|
|
817
|
+
creditDeferred = true;
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
// No session: resultTotalWin = spinWin (already set)
|
|
821
|
+
// Apply max win cap for non-session spins
|
|
822
|
+
if (!this.sessionManager.isActive && !sessionCompleted && maxWinCap !== undefined && resultTotalWin > maxWinCap) {
|
|
823
|
+
resultTotalWin = maxWinCap;
|
|
824
|
+
this.variables.max_win_reached = 1;
|
|
825
|
+
data.max_win_reached = true;
|
|
777
826
|
}
|
|
778
827
|
return {
|
|
779
|
-
totalWin:
|
|
828
|
+
totalWin: Math.round(resultTotalWin * 100) / 100,
|
|
780
829
|
data,
|
|
781
830
|
nextActions,
|
|
782
831
|
session,
|
|
@@ -784,13 +833,11 @@ class LuaEngine {
|
|
|
784
833
|
creditDeferred,
|
|
785
834
|
};
|
|
786
835
|
}
|
|
787
|
-
/** Reset all state (sessions, persistent vars, variables) */
|
|
788
836
|
reset() {
|
|
789
837
|
this.variables = {};
|
|
790
838
|
this.sessionManager.reset();
|
|
791
839
|
this.persistentState.reset();
|
|
792
840
|
}
|
|
793
|
-
/** Destroy the Lua VM */
|
|
794
841
|
destroy() {
|
|
795
842
|
if (this.L) {
|
|
796
843
|
lua.lua_close(this.L);
|
|
@@ -805,7 +852,6 @@ class LuaEngine {
|
|
|
805
852
|
lua.lua_pop(this.L, 1);
|
|
806
853
|
throw new Error(`Failed to load Lua script: ${err}`);
|
|
807
854
|
}
|
|
808
|
-
// Verify that execute() function exists
|
|
809
855
|
lua.lua_getglobal(this.L, to_luastring('execute'));
|
|
810
856
|
if (lua.lua_type(this.L, -1) !== lua.LUA_TFUNCTION) {
|
|
811
857
|
lua.lua_pop(this.L, 1);
|
|
@@ -813,21 +859,22 @@ class LuaEngine {
|
|
|
813
859
|
}
|
|
814
860
|
lua.lua_pop(this.L, 1);
|
|
815
861
|
}
|
|
816
|
-
callLuaExecute(stage, params, variables) {
|
|
817
|
-
// Push the execute function
|
|
862
|
+
callLuaExecute(stage, action, params, variables) {
|
|
818
863
|
lua.lua_getglobal(this.L, to_luastring('execute'));
|
|
819
|
-
// Build
|
|
820
|
-
lua.lua_createtable(this.L, 0,
|
|
864
|
+
// Build state table: {stage, action, params, variables}
|
|
865
|
+
lua.lua_createtable(this.L, 0, 4);
|
|
821
866
|
// state.stage
|
|
822
867
|
lua.lua_pushstring(this.L, to_luastring(stage));
|
|
823
868
|
lua.lua_setfield(this.L, -2, to_luastring('stage'));
|
|
869
|
+
// state.action (server sets this at top level)
|
|
870
|
+
lua.lua_pushstring(this.L, to_luastring(action));
|
|
871
|
+
lua.lua_setfield(this.L, -2, to_luastring('action'));
|
|
824
872
|
// state.params
|
|
825
873
|
pushJSValue(this.L, params);
|
|
826
874
|
lua.lua_setfield(this.L, -2, to_luastring('params'));
|
|
827
875
|
// state.variables
|
|
828
876
|
pushJSValue(this.L, variables);
|
|
829
877
|
lua.lua_setfield(this.L, -2, to_luastring('variables'));
|
|
830
|
-
// Call execute(state) → 1 result
|
|
831
878
|
const status = lua.lua_pcall(this.L, 1, 1, 0);
|
|
832
879
|
if (status !== lua.LUA_OK) {
|
|
833
880
|
const err = to_jsstring(lua.lua_tostring(this.L, -1));
|
|
@@ -835,21 +882,17 @@ class LuaEngine {
|
|
|
835
882
|
throw new Error(`Lua execute() failed: ${err}`);
|
|
836
883
|
}
|
|
837
884
|
if (this.simulationMode) {
|
|
838
|
-
// Fast path: extract only total_win
|
|
885
|
+
// Fast path: extract only total_win, variables, _persist_* keys
|
|
839
886
|
const result = {};
|
|
840
|
-
// result.total_win
|
|
841
887
|
lua.lua_getfield(this.L, -1, to_luastring('total_win'));
|
|
842
888
|
result.total_win = lua.lua_type(this.L, -1) === lua.LUA_TNUMBER
|
|
843
|
-
? lua.lua_tonumber(this.L, -1)
|
|
844
|
-
: 0;
|
|
889
|
+
? lua.lua_tonumber(this.L, -1) : 0;
|
|
845
890
|
lua.lua_pop(this.L, 1);
|
|
846
|
-
// result.variables
|
|
847
891
|
lua.lua_getfield(this.L, -1, to_luastring('variables'));
|
|
848
892
|
if (lua.lua_type(this.L, -1) === lua.LUA_TTABLE) {
|
|
849
893
|
result.variables = luaToJS(this.L, -1);
|
|
850
894
|
}
|
|
851
895
|
lua.lua_pop(this.L, 1);
|
|
852
|
-
// result._persist_* keys (needed for session state)
|
|
853
896
|
lua.lua_pushnil(this.L);
|
|
854
897
|
while (lua.lua_next(this.L, -2) !== 0) {
|
|
855
898
|
if (lua.lua_type(this.L, -2) === lua.LUA_TSTRING) {
|
|
@@ -860,10 +903,10 @@ class LuaEngine {
|
|
|
860
903
|
}
|
|
861
904
|
lua.lua_pop(this.L, 1);
|
|
862
905
|
}
|
|
863
|
-
lua.lua_pop(this.L, 1);
|
|
906
|
+
lua.lua_pop(this.L, 1);
|
|
864
907
|
return result;
|
|
865
908
|
}
|
|
866
|
-
// Full path
|
|
909
|
+
// Full path
|
|
867
910
|
const result = luaToJS(this.L, -1);
|
|
868
911
|
lua.lua_pop(this.L, 1);
|
|
869
912
|
if (!result || typeof result !== 'object' || Array.isArray(result)) {
|