@energy8platform/game-engine 0.10.7 → 0.10.8
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 +144 -103
- package/dist/lua.cjs.js.map +1 -1
- package/dist/lua.d.ts +29 -27
- package/dist/lua.esm.js +144 -103
- 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 +87 -76
- 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,16 @@ 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
|
|
749
|
+
const spinWin = Math.round(totalWinMultiplier * bet * 100) / 100;
|
|
712
750
|
// Merge result variables into engine variables
|
|
713
751
|
Object.assign(stateVars, resultVariables);
|
|
714
752
|
this.variables = { ...stateVars };
|
|
715
|
-
delete this.variables.bet;
|
|
753
|
+
delete this.variables.bet;
|
|
716
754
|
// Build client data (everything except special keys)
|
|
717
755
|
const data = {};
|
|
718
756
|
for (const [key, value] of Object.entries(luaResult)) {
|
|
@@ -720,17 +758,6 @@ class LuaEngine {
|
|
|
720
758
|
data[key] = value;
|
|
721
759
|
}
|
|
722
760
|
}
|
|
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
761
|
// 7. Handle _persist_* and _persist_game_* keys
|
|
735
762
|
this.sessionManager.storePersistData(data);
|
|
736
763
|
this.persistentState.storeGameData(data);
|
|
@@ -747,36 +774,56 @@ class LuaEngine {
|
|
|
747
774
|
delete data[key];
|
|
748
775
|
}
|
|
749
776
|
}
|
|
750
|
-
// 8. Evaluate transitions
|
|
777
|
+
// 8. Evaluate transitions (server: evaluateTransitions)
|
|
751
778
|
const { rule, nextActions } = this.actionRouter.evaluateTransitions(action, this.variables);
|
|
779
|
+
// 9. Determine credit behavior (server: creditNow logic)
|
|
752
780
|
let creditDeferred = action.credit === 'defer' || rule.credit_override === 'defer';
|
|
781
|
+
// 10. Session lifecycle (server: create/update/complete session)
|
|
753
782
|
let session = this.sessionManager.current;
|
|
754
|
-
|
|
783
|
+
let resultTotalWin = spinWin;
|
|
784
|
+
let sessionCompleted = false;
|
|
785
|
+
// Calculate max win cap for session
|
|
786
|
+
const maxWinCap = this.calculateMaxWinCap(bet);
|
|
755
787
|
if (rule.creates_session && !this.sessionManager.isActive) {
|
|
756
|
-
|
|
788
|
+
// CREATE SESSION — initial spin counted (server: createSession includes spinWin)
|
|
789
|
+
session = this.sessionManager.createSession(rule, this.variables, bet, spinWin, maxWinCap);
|
|
757
790
|
creditDeferred = true;
|
|
758
|
-
|
|
759
|
-
//
|
|
791
|
+
resultTotalWin = spinWin;
|
|
792
|
+
// Clear the trigger variable — it was consumed to set spinsRemaining
|
|
760
793
|
if (rule.session_config?.total_spins_var) {
|
|
761
794
|
delete this.variables[rule.session_config.total_spins_var];
|
|
762
795
|
}
|
|
763
796
|
}
|
|
764
|
-
// Handle session update
|
|
765
797
|
else if (this.sessionManager.isActive) {
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
if (
|
|
798
|
+
// UPDATE SESSION — accumulate win, check completion
|
|
799
|
+
session = this.sessionManager.updateSession(rule, this.variables, spinWin);
|
|
800
|
+
if (session?.completed) {
|
|
801
|
+
// SESSION COMPLETED — server returns session.TotalWin as result.TotalWin
|
|
769
802
|
const completed = this.sessionManager.completeSession();
|
|
770
803
|
session = completed.session;
|
|
804
|
+
resultTotalWin = completed.totalWin;
|
|
805
|
+
sessionCompleted = true;
|
|
771
806
|
creditDeferred = false;
|
|
772
|
-
// Clean up session-scoped variables
|
|
807
|
+
// Clean up session-scoped variables
|
|
773
808
|
for (const varName of completed.sessionVarNames) {
|
|
774
809
|
delete this.variables[varName];
|
|
775
810
|
}
|
|
776
811
|
}
|
|
812
|
+
else {
|
|
813
|
+
// Mid-session: totalWin = spinWin, credit deferred
|
|
814
|
+
resultTotalWin = spinWin;
|
|
815
|
+
creditDeferred = true;
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
// No session: resultTotalWin = spinWin (already set)
|
|
819
|
+
// Apply max win cap for non-session spins
|
|
820
|
+
if (!this.sessionManager.isActive && !sessionCompleted && maxWinCap !== undefined && resultTotalWin > maxWinCap) {
|
|
821
|
+
resultTotalWin = maxWinCap;
|
|
822
|
+
this.variables.max_win_reached = 1;
|
|
823
|
+
data.max_win_reached = true;
|
|
777
824
|
}
|
|
778
825
|
return {
|
|
779
|
-
totalWin:
|
|
826
|
+
totalWin: Math.round(resultTotalWin * 100) / 100,
|
|
780
827
|
data,
|
|
781
828
|
nextActions,
|
|
782
829
|
session,
|
|
@@ -784,13 +831,11 @@ class LuaEngine {
|
|
|
784
831
|
creditDeferred,
|
|
785
832
|
};
|
|
786
833
|
}
|
|
787
|
-
/** Reset all state (sessions, persistent vars, variables) */
|
|
788
834
|
reset() {
|
|
789
835
|
this.variables = {};
|
|
790
836
|
this.sessionManager.reset();
|
|
791
837
|
this.persistentState.reset();
|
|
792
838
|
}
|
|
793
|
-
/** Destroy the Lua VM */
|
|
794
839
|
destroy() {
|
|
795
840
|
if (this.L) {
|
|
796
841
|
lua.lua_close(this.L);
|
|
@@ -805,7 +850,6 @@ class LuaEngine {
|
|
|
805
850
|
lua.lua_pop(this.L, 1);
|
|
806
851
|
throw new Error(`Failed to load Lua script: ${err}`);
|
|
807
852
|
}
|
|
808
|
-
// Verify that execute() function exists
|
|
809
853
|
lua.lua_getglobal(this.L, to_luastring('execute'));
|
|
810
854
|
if (lua.lua_type(this.L, -1) !== lua.LUA_TFUNCTION) {
|
|
811
855
|
lua.lua_pop(this.L, 1);
|
|
@@ -813,21 +857,22 @@ class LuaEngine {
|
|
|
813
857
|
}
|
|
814
858
|
lua.lua_pop(this.L, 1);
|
|
815
859
|
}
|
|
816
|
-
callLuaExecute(stage, params, variables) {
|
|
817
|
-
// Push the execute function
|
|
860
|
+
callLuaExecute(stage, action, params, variables) {
|
|
818
861
|
lua.lua_getglobal(this.L, to_luastring('execute'));
|
|
819
|
-
// Build
|
|
820
|
-
lua.lua_createtable(this.L, 0,
|
|
862
|
+
// Build state table: {stage, action, params, variables}
|
|
863
|
+
lua.lua_createtable(this.L, 0, 4);
|
|
821
864
|
// state.stage
|
|
822
865
|
lua.lua_pushstring(this.L, to_luastring(stage));
|
|
823
866
|
lua.lua_setfield(this.L, -2, to_luastring('stage'));
|
|
867
|
+
// state.action (server sets this at top level)
|
|
868
|
+
lua.lua_pushstring(this.L, to_luastring(action));
|
|
869
|
+
lua.lua_setfield(this.L, -2, to_luastring('action'));
|
|
824
870
|
// state.params
|
|
825
871
|
pushJSValue(this.L, params);
|
|
826
872
|
lua.lua_setfield(this.L, -2, to_luastring('params'));
|
|
827
873
|
// state.variables
|
|
828
874
|
pushJSValue(this.L, variables);
|
|
829
875
|
lua.lua_setfield(this.L, -2, to_luastring('variables'));
|
|
830
|
-
// Call execute(state) → 1 result
|
|
831
876
|
const status = lua.lua_pcall(this.L, 1, 1, 0);
|
|
832
877
|
if (status !== lua.LUA_OK) {
|
|
833
878
|
const err = to_jsstring(lua.lua_tostring(this.L, -1));
|
|
@@ -835,21 +880,17 @@ class LuaEngine {
|
|
|
835
880
|
throw new Error(`Lua execute() failed: ${err}`);
|
|
836
881
|
}
|
|
837
882
|
if (this.simulationMode) {
|
|
838
|
-
// Fast path: extract only total_win
|
|
883
|
+
// Fast path: extract only total_win, variables, _persist_* keys
|
|
839
884
|
const result = {};
|
|
840
|
-
// result.total_win
|
|
841
885
|
lua.lua_getfield(this.L, -1, to_luastring('total_win'));
|
|
842
886
|
result.total_win = lua.lua_type(this.L, -1) === lua.LUA_TNUMBER
|
|
843
|
-
? lua.lua_tonumber(this.L, -1)
|
|
844
|
-
: 0;
|
|
887
|
+
? lua.lua_tonumber(this.L, -1) : 0;
|
|
845
888
|
lua.lua_pop(this.L, 1);
|
|
846
|
-
// result.variables
|
|
847
889
|
lua.lua_getfield(this.L, -1, to_luastring('variables'));
|
|
848
890
|
if (lua.lua_type(this.L, -1) === lua.LUA_TTABLE) {
|
|
849
891
|
result.variables = luaToJS(this.L, -1);
|
|
850
892
|
}
|
|
851
893
|
lua.lua_pop(this.L, 1);
|
|
852
|
-
// result._persist_* keys (needed for session state)
|
|
853
894
|
lua.lua_pushnil(this.L);
|
|
854
895
|
while (lua.lua_next(this.L, -2) !== 0) {
|
|
855
896
|
if (lua.lua_type(this.L, -2) === lua.LUA_TSTRING) {
|
|
@@ -860,10 +901,10 @@ class LuaEngine {
|
|
|
860
901
|
}
|
|
861
902
|
lua.lua_pop(this.L, 1);
|
|
862
903
|
}
|
|
863
|
-
lua.lua_pop(this.L, 1);
|
|
904
|
+
lua.lua_pop(this.L, 1);
|
|
864
905
|
return result;
|
|
865
906
|
}
|
|
866
|
-
// Full path
|
|
907
|
+
// Full path
|
|
867
908
|
const result = luaToJS(this.L, -1);
|
|
868
909
|
lua.lua_pop(this.L, 1);
|
|
869
910
|
if (!result || typeof result !== 'object' || Array.isArray(result)) {
|