@energy8platform/game-engine 0.10.6 → 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.d.ts +2 -0
- package/dist/index.esm.js +11 -7
- package/dist/index.esm.js.map +1 -1
- package/dist/lua.cjs.js +182 -111
- package/dist/lua.cjs.js.map +1 -1
- package/dist/lua.d.ts +32 -27
- package/dist/lua.esm.js +182 -111
- 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 +120 -67
- package/src/lua/SessionManager.ts +74 -29
- package/src/lua/SimulationRunner.ts +11 -19
- package/src/lua/types.ts +2 -0
package/dist/lua.cjs.js
CHANGED
|
@@ -371,10 +371,13 @@ function evaluateComparison(expr, variables) {
|
|
|
371
371
|
}
|
|
372
372
|
}
|
|
373
373
|
|
|
374
|
+
const MAX_SESSION_SPINS = 200;
|
|
374
375
|
/**
|
|
375
|
-
* Manages session lifecycle
|
|
376
|
-
*
|
|
377
|
-
*
|
|
376
|
+
* Manages session lifecycle matching the platform server behavior:
|
|
377
|
+
* - createSession: initial spin counted (spinsPlayed=1, totalWin=spinWin)
|
|
378
|
+
* - updateSession: accumulates win, decrements spins, checks max win cap on session level
|
|
379
|
+
* - completeSession: returns cumulative totalWin, cleans up session vars
|
|
380
|
+
* - Safety cap: 200 spins max per session
|
|
378
381
|
*/
|
|
379
382
|
class SessionManager {
|
|
380
383
|
session = null;
|
|
@@ -389,36 +392,57 @@ class SessionManager {
|
|
|
389
392
|
get sessionTotalWin() {
|
|
390
393
|
return this.session?.totalWin ?? 0;
|
|
391
394
|
}
|
|
392
|
-
/**
|
|
393
|
-
|
|
394
|
-
|
|
395
|
+
/** Get the fixed bet amount from the session (server uses session bet, not request bet) */
|
|
396
|
+
get sessionBet() {
|
|
397
|
+
return this.session?.bet;
|
|
398
|
+
}
|
|
399
|
+
/** Get spinsVarName to restore free_spins_remaining into variables */
|
|
400
|
+
get spinsVarName() {
|
|
401
|
+
return this.session?.spinsVarName;
|
|
402
|
+
}
|
|
403
|
+
get spinsRemaining() {
|
|
404
|
+
return this.session?.spinsRemaining ?? 0;
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Create a new session from a transition rule.
|
|
408
|
+
* Server behavior: initial spin is already counted (spinsPlayed=1, totalWin includes spinWin).
|
|
409
|
+
*/
|
|
410
|
+
createSession(rule, variables, bet, spinWin, maxWinCap) {
|
|
411
|
+
let spinsRemaining = -1;
|
|
412
|
+
let spinsVarName;
|
|
395
413
|
if (rule.session_config?.total_spins_var) {
|
|
396
|
-
|
|
397
|
-
spinsRemaining = variables[
|
|
414
|
+
spinsVarName = rule.session_config.total_spins_var;
|
|
415
|
+
spinsRemaining = variables[spinsVarName] ?? -1;
|
|
398
416
|
}
|
|
417
|
+
const persistentVarNames = rule.session_config?.persistent_vars ?? [];
|
|
399
418
|
const persistentVars = {};
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
persistentVars[varName] = variables[varName] ?? 0;
|
|
403
|
-
}
|
|
419
|
+
for (const varName of persistentVarNames) {
|
|
420
|
+
persistentVars[varName] = variables[varName] ?? 0;
|
|
404
421
|
}
|
|
405
422
|
this.session = {
|
|
406
423
|
spinsRemaining,
|
|
407
|
-
spinsPlayed:
|
|
408
|
-
totalWin:
|
|
424
|
+
spinsPlayed: 1, // initial spin counts
|
|
425
|
+
totalWin: spinWin, // initial spin win included
|
|
409
426
|
completed: false,
|
|
410
427
|
maxWinReached: false,
|
|
411
428
|
bet,
|
|
429
|
+
maxWinCap,
|
|
430
|
+
spinsVarName,
|
|
431
|
+
persistentVarNames,
|
|
412
432
|
persistentVars,
|
|
413
433
|
persistentData: {},
|
|
414
434
|
};
|
|
415
435
|
return this.toSessionData();
|
|
416
436
|
}
|
|
417
|
-
/**
|
|
437
|
+
/**
|
|
438
|
+
* Update session after a bonus spin.
|
|
439
|
+
* Server behavior: accumulate win, decrement spins, check retrigger, check max win cap,
|
|
440
|
+
* safety cap at 200 spins.
|
|
441
|
+
*/
|
|
418
442
|
updateSession(rule, variables, spinWin) {
|
|
419
443
|
if (!this.session)
|
|
420
444
|
throw new Error('No active session');
|
|
421
|
-
// Accumulate win
|
|
445
|
+
// Accumulate win and count spin
|
|
422
446
|
this.session.totalWin += spinWin;
|
|
423
447
|
this.session.spinsPlayed++;
|
|
424
448
|
// Decrement spins (only for non-unlimited sessions)
|
|
@@ -432,29 +456,39 @@ class SessionManager {
|
|
|
432
456
|
this.session.spinsRemaining += extraSpins;
|
|
433
457
|
}
|
|
434
458
|
}
|
|
435
|
-
//
|
|
436
|
-
if (this.session.
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
459
|
+
// Safety cap: server limits sessions to 200 spins
|
|
460
|
+
if (this.session.spinsPlayed >= MAX_SESSION_SPINS) {
|
|
461
|
+
this.session.spinsRemaining = 0;
|
|
462
|
+
}
|
|
463
|
+
// Update session persistent vars from current variables
|
|
464
|
+
for (const varName of this.session.persistentVarNames) {
|
|
465
|
+
if (varName in variables) {
|
|
466
|
+
this.session.persistentVars[varName] = variables[varName];
|
|
441
467
|
}
|
|
442
468
|
}
|
|
443
|
-
//
|
|
444
|
-
if (this.session.
|
|
469
|
+
// Check max win cap (on session level, not per spin)
|
|
470
|
+
if (this.session.maxWinCap !== undefined && this.session.totalWin >= this.session.maxWinCap) {
|
|
471
|
+
this.session.totalWin = this.session.maxWinCap;
|
|
472
|
+
this.session.spinsRemaining = 0;
|
|
473
|
+
this.session.maxWinReached = true;
|
|
474
|
+
}
|
|
475
|
+
// Auto-complete if spins exhausted or explicit complete
|
|
476
|
+
if (this.session.spinsRemaining === 0 || rule.complete_session) {
|
|
445
477
|
this.session.completed = true;
|
|
446
478
|
}
|
|
447
479
|
return this.toSessionData();
|
|
448
480
|
}
|
|
449
|
-
/**
|
|
481
|
+
/**
|
|
482
|
+
* Complete the session explicitly.
|
|
483
|
+
* Returns cumulative totalWin and list of session-scoped var names to clean up.
|
|
484
|
+
*/
|
|
450
485
|
completeSession() {
|
|
451
486
|
if (!this.session)
|
|
452
487
|
throw new Error('No active session to complete');
|
|
453
488
|
this.session.completed = true;
|
|
454
489
|
const totalWin = this.session.totalWin;
|
|
455
490
|
const session = this.toSessionData();
|
|
456
|
-
const sessionVarNames =
|
|
457
|
-
// Clear session state so it doesn't leak into the next round
|
|
491
|
+
const sessionVarNames = [...this.session.persistentVarNames];
|
|
458
492
|
this.session = null;
|
|
459
493
|
return { totalWin, session, sessionVarNames };
|
|
460
494
|
}
|
|
@@ -476,16 +510,16 @@ class SessionManager {
|
|
|
476
510
|
}
|
|
477
511
|
}
|
|
478
512
|
}
|
|
479
|
-
/** Get
|
|
513
|
+
/** Get persistent params to inject into next execute() call */
|
|
480
514
|
getPersistentParams() {
|
|
481
515
|
if (!this.session)
|
|
482
516
|
return {};
|
|
483
517
|
const params = {};
|
|
484
|
-
// Session persistent vars (float64)
|
|
518
|
+
// Session persistent vars (float64) → state.variables
|
|
485
519
|
for (const [key, value] of Object.entries(this.session.persistentVars)) {
|
|
486
520
|
params[key] = value;
|
|
487
521
|
}
|
|
488
|
-
// _persist_ complex data → _ps_*
|
|
522
|
+
// _persist_ complex data → _ps_* in state.params
|
|
489
523
|
for (const [key, value] of Object.entries(this.session.persistentData)) {
|
|
490
524
|
params[`_ps_${key}`] = value;
|
|
491
525
|
}
|
|
@@ -583,22 +617,20 @@ class PersistentState {
|
|
|
583
617
|
|
|
584
618
|
const { lua, lauxlib, lualib } = fengari;
|
|
585
619
|
const { to_luastring, to_jsstring } = fengari;
|
|
620
|
+
/** Default engine variables matching the server's NewGameState() */
|
|
621
|
+
const DEFAULT_VARIABLES = {
|
|
622
|
+
multiplier: 1,
|
|
623
|
+
total_multiplier: 1,
|
|
624
|
+
global_multiplier: 1,
|
|
625
|
+
last_win_amount: 0,
|
|
626
|
+
free_spins_awarded: 0,
|
|
627
|
+
};
|
|
586
628
|
/**
|
|
587
629
|
* Runs Lua game scripts locally, replicating the platform's server-side execution.
|
|
588
630
|
*
|
|
589
|
-
* Implements the full lifecycle
|
|
590
|
-
*
|
|
591
|
-
*
|
|
592
|
-
* @example
|
|
593
|
-
* ```ts
|
|
594
|
-
* const engine = new LuaEngine({
|
|
595
|
-
* script: luaSource,
|
|
596
|
-
* gameDefinition: { id: 'my-slot', type: 'SLOT', actions: { ... } },
|
|
597
|
-
* });
|
|
598
|
-
*
|
|
599
|
-
* const result = engine.execute({ action: 'spin', bet: 1.0 });
|
|
600
|
-
* // result.data.matrix, result.totalWin, result.nextActions, etc.
|
|
601
|
-
* ```
|
|
631
|
+
* Implements the full lifecycle matching `casino_platform/internal/usecase/game_usecase.go`:
|
|
632
|
+
* action routing → state assembly → Lua execute() → result extraction →
|
|
633
|
+
* transition evaluation → session management.
|
|
602
634
|
*/
|
|
603
635
|
class LuaEngine {
|
|
604
636
|
L;
|
|
@@ -608,22 +640,20 @@ class LuaEngine {
|
|
|
608
640
|
persistentState;
|
|
609
641
|
gameDefinition;
|
|
610
642
|
variables = {};
|
|
643
|
+
simulationMode;
|
|
611
644
|
constructor(config) {
|
|
612
645
|
this.gameDefinition = config.gameDefinition;
|
|
613
|
-
|
|
646
|
+
this.simulationMode = config.simulationMode ?? false;
|
|
614
647
|
const rng = config.seed !== undefined
|
|
615
648
|
? createSeededRng(config.seed)
|
|
616
649
|
: undefined;
|
|
617
|
-
// Initialize sub-managers
|
|
618
650
|
this.api = new LuaEngineAPI(config.gameDefinition, rng, config.logger);
|
|
619
651
|
this.actionRouter = new ActionRouter(config.gameDefinition);
|
|
620
652
|
this.sessionManager = new SessionManager();
|
|
621
653
|
this.persistentState = new PersistentState(config.gameDefinition.persistent_state);
|
|
622
|
-
// Create Lua state and load standard libraries
|
|
623
654
|
this.L = lauxlib.luaL_newstate();
|
|
624
655
|
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.
|
|
656
|
+
// Polyfill Lua 5.1/5.2 functions removed in 5.3
|
|
627
657
|
lauxlib.luaL_dostring(this.L, to_luastring(`
|
|
628
658
|
math.pow = function(a, b) return a ^ b end
|
|
629
659
|
math.atan2 = math.atan2 or function(y, x) return math.atan(y, x) end
|
|
@@ -641,32 +671,36 @@ class LuaEngine {
|
|
|
641
671
|
loadstring = loadstring or load
|
|
642
672
|
table.getn = table.getn or function(t) return #t end
|
|
643
673
|
`));
|
|
644
|
-
// Register engine.* API
|
|
645
674
|
this.api.register(this.L);
|
|
646
|
-
// Load and compile the script
|
|
647
675
|
this.loadScript(config.script);
|
|
648
676
|
}
|
|
649
|
-
/** Current session data (if any) */
|
|
650
677
|
get session() {
|
|
651
678
|
return this.sessionManager.current;
|
|
652
679
|
}
|
|
653
|
-
/** Current persistent state values */
|
|
654
680
|
get persistentVars() {
|
|
655
681
|
return { ...this.variables };
|
|
656
682
|
}
|
|
657
683
|
/**
|
|
658
|
-
* Execute a play action —
|
|
659
|
-
* This is what DevBridge calls on each PLAY_REQUEST.
|
|
684
|
+
* Execute a play action — replicates server's Play() function.
|
|
660
685
|
*/
|
|
661
686
|
execute(params) {
|
|
662
|
-
const { action: actionName,
|
|
663
|
-
// 1. Resolve
|
|
687
|
+
const { action: actionName, params: clientParams } = params;
|
|
688
|
+
// 1. Resolve action
|
|
664
689
|
const action = this.actionRouter.resolveAction(actionName, this.sessionManager.isActive);
|
|
665
|
-
// 2.
|
|
666
|
-
|
|
690
|
+
// 2. Determine bet — server uses session bet for session actions
|
|
691
|
+
let bet = params.bet;
|
|
692
|
+
if (this.sessionManager.isActive && this.sessionManager.sessionBet !== undefined) {
|
|
693
|
+
bet = this.sessionManager.sessionBet;
|
|
694
|
+
}
|
|
695
|
+
// 3. Build state.variables (matching server's NewGameState + restore)
|
|
696
|
+
const stateVars = {
|
|
697
|
+
...DEFAULT_VARIABLES,
|
|
698
|
+
...this.variables,
|
|
699
|
+
bet,
|
|
700
|
+
};
|
|
667
701
|
// Load cross-spin persistent state
|
|
668
702
|
this.persistentState.loadIntoVariables(stateVars);
|
|
669
|
-
// Load session persistent vars
|
|
703
|
+
// Load session persistent vars + restore spinsRemaining
|
|
670
704
|
if (this.sessionManager.isActive) {
|
|
671
705
|
const sessionParams = this.sessionManager.getPersistentParams();
|
|
672
706
|
for (const [k, v] of Object.entries(sessionParams)) {
|
|
@@ -674,8 +708,14 @@ class LuaEngine {
|
|
|
674
708
|
stateVars[k] = v;
|
|
675
709
|
}
|
|
676
710
|
}
|
|
711
|
+
// Restore spinsRemaining into the variable the script reads
|
|
712
|
+
if (this.sessionManager.spinsVarName) {
|
|
713
|
+
stateVars[this.sessionManager.spinsVarName] = this.sessionManager.spinsRemaining;
|
|
714
|
+
}
|
|
715
|
+
// Also set free_spins_remaining for convenience
|
|
716
|
+
stateVars.free_spins_remaining = this.sessionManager.spinsRemaining;
|
|
677
717
|
}
|
|
678
|
-
//
|
|
718
|
+
// 4. Build state.params
|
|
679
719
|
const stateParams = { ...clientParams };
|
|
680
720
|
stateParams._action = actionName;
|
|
681
721
|
// Inject session _ps_* persistent data
|
|
@@ -703,16 +743,16 @@ class LuaEngine {
|
|
|
703
743
|
if (clientParams?.ante_bet && this.gameDefinition.ante_bet) {
|
|
704
744
|
stateParams.ante_bet = true;
|
|
705
745
|
}
|
|
706
|
-
//
|
|
707
|
-
const luaResult = this.callLuaExecute(action.stage, stateParams, stateVars);
|
|
708
|
-
//
|
|
746
|
+
// 5. Execute Lua (server: executor.Execute(stage, state))
|
|
747
|
+
const luaResult = this.callLuaExecute(action.stage, actionName, stateParams, stateVars);
|
|
748
|
+
// 6. Process result (server: ApplyLuaResult)
|
|
709
749
|
const totalWinMultiplier = typeof luaResult.total_win === 'number' ? luaResult.total_win : 0;
|
|
710
750
|
const resultVariables = (luaResult.variables ?? {});
|
|
711
|
-
const
|
|
751
|
+
const spinWin = Math.round(totalWinMultiplier * bet * 100) / 100;
|
|
712
752
|
// Merge result variables into engine variables
|
|
713
753
|
Object.assign(stateVars, resultVariables);
|
|
714
754
|
this.variables = { ...stateVars };
|
|
715
|
-
delete this.variables.bet;
|
|
755
|
+
delete this.variables.bet;
|
|
716
756
|
// Build client data (everything except special keys)
|
|
717
757
|
const data = {};
|
|
718
758
|
for (const [key, value] of Object.entries(luaResult)) {
|
|
@@ -720,17 +760,6 @@ 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);
|
|
@@ -747,31 +776,56 @@ class LuaEngine {
|
|
|
747
776
|
delete data[key];
|
|
748
777
|
}
|
|
749
778
|
}
|
|
750
|
-
// 8. Evaluate transitions
|
|
779
|
+
// 8. Evaluate transitions (server: evaluateTransitions)
|
|
751
780
|
const { rule, nextActions } = this.actionRouter.evaluateTransitions(action, this.variables);
|
|
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, this.variables, bet, spinWin, maxWinCap);
|
|
757
792
|
creditDeferred = true;
|
|
793
|
+
resultTotalWin = spinWin;
|
|
794
|
+
// Clear the trigger variable — it was consumed to set spinsRemaining
|
|
795
|
+
if (rule.session_config?.total_spins_var) {
|
|
796
|
+
delete this.variables[rule.session_config.total_spins_var];
|
|
797
|
+
}
|
|
758
798
|
}
|
|
759
|
-
// Handle session update
|
|
760
799
|
else if (this.sessionManager.isActive) {
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
if (
|
|
800
|
+
// UPDATE SESSION — accumulate win, check completion
|
|
801
|
+
session = this.sessionManager.updateSession(rule, this.variables, spinWin);
|
|
802
|
+
if (session?.completed) {
|
|
803
|
+
// SESSION COMPLETED — server returns session.TotalWin as result.TotalWin
|
|
764
804
|
const completed = this.sessionManager.completeSession();
|
|
765
805
|
session = completed.session;
|
|
806
|
+
resultTotalWin = completed.totalWin;
|
|
807
|
+
sessionCompleted = true;
|
|
766
808
|
creditDeferred = false;
|
|
767
|
-
// Clean up session-scoped variables
|
|
809
|
+
// Clean up session-scoped variables
|
|
768
810
|
for (const varName of completed.sessionVarNames) {
|
|
769
811
|
delete this.variables[varName];
|
|
770
812
|
}
|
|
771
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;
|
|
772
826
|
}
|
|
773
827
|
return {
|
|
774
|
-
totalWin:
|
|
828
|
+
totalWin: Math.round(resultTotalWin * 100) / 100,
|
|
775
829
|
data,
|
|
776
830
|
nextActions,
|
|
777
831
|
session,
|
|
@@ -779,13 +833,11 @@ class LuaEngine {
|
|
|
779
833
|
creditDeferred,
|
|
780
834
|
};
|
|
781
835
|
}
|
|
782
|
-
/** Reset all state (sessions, persistent vars, variables) */
|
|
783
836
|
reset() {
|
|
784
837
|
this.variables = {};
|
|
785
838
|
this.sessionManager.reset();
|
|
786
839
|
this.persistentState.reset();
|
|
787
840
|
}
|
|
788
|
-
/** Destroy the Lua VM */
|
|
789
841
|
destroy() {
|
|
790
842
|
if (this.L) {
|
|
791
843
|
lua.lua_close(this.L);
|
|
@@ -800,7 +852,6 @@ class LuaEngine {
|
|
|
800
852
|
lua.lua_pop(this.L, 1);
|
|
801
853
|
throw new Error(`Failed to load Lua script: ${err}`);
|
|
802
854
|
}
|
|
803
|
-
// Verify that execute() function exists
|
|
804
855
|
lua.lua_getglobal(this.L, to_luastring('execute'));
|
|
805
856
|
if (lua.lua_type(this.L, -1) !== lua.LUA_TFUNCTION) {
|
|
806
857
|
lua.lua_pop(this.L, 1);
|
|
@@ -808,28 +859,54 @@ class LuaEngine {
|
|
|
808
859
|
}
|
|
809
860
|
lua.lua_pop(this.L, 1);
|
|
810
861
|
}
|
|
811
|
-
callLuaExecute(stage, params, variables) {
|
|
812
|
-
// Push the execute function
|
|
862
|
+
callLuaExecute(stage, action, params, variables) {
|
|
813
863
|
lua.lua_getglobal(this.L, to_luastring('execute'));
|
|
814
|
-
// Build
|
|
815
|
-
lua.lua_createtable(this.L, 0,
|
|
864
|
+
// Build state table: {stage, action, params, variables}
|
|
865
|
+
lua.lua_createtable(this.L, 0, 4);
|
|
816
866
|
// state.stage
|
|
817
867
|
lua.lua_pushstring(this.L, to_luastring(stage));
|
|
818
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'));
|
|
819
872
|
// state.params
|
|
820
873
|
pushJSValue(this.L, params);
|
|
821
874
|
lua.lua_setfield(this.L, -2, to_luastring('params'));
|
|
822
875
|
// state.variables
|
|
823
876
|
pushJSValue(this.L, variables);
|
|
824
877
|
lua.lua_setfield(this.L, -2, to_luastring('variables'));
|
|
825
|
-
// Call execute(state) → 1 result
|
|
826
878
|
const status = lua.lua_pcall(this.L, 1, 1, 0);
|
|
827
879
|
if (status !== lua.LUA_OK) {
|
|
828
880
|
const err = to_jsstring(lua.lua_tostring(this.L, -1));
|
|
829
881
|
lua.lua_pop(this.L, 1);
|
|
830
882
|
throw new Error(`Lua execute() failed: ${err}`);
|
|
831
883
|
}
|
|
832
|
-
|
|
884
|
+
if (this.simulationMode) {
|
|
885
|
+
// Fast path: extract only total_win, variables, _persist_* keys
|
|
886
|
+
const result = {};
|
|
887
|
+
lua.lua_getfield(this.L, -1, to_luastring('total_win'));
|
|
888
|
+
result.total_win = lua.lua_type(this.L, -1) === lua.LUA_TNUMBER
|
|
889
|
+
? lua.lua_tonumber(this.L, -1) : 0;
|
|
890
|
+
lua.lua_pop(this.L, 1);
|
|
891
|
+
lua.lua_getfield(this.L, -1, to_luastring('variables'));
|
|
892
|
+
if (lua.lua_type(this.L, -1) === lua.LUA_TTABLE) {
|
|
893
|
+
result.variables = luaToJS(this.L, -1);
|
|
894
|
+
}
|
|
895
|
+
lua.lua_pop(this.L, 1);
|
|
896
|
+
lua.lua_pushnil(this.L);
|
|
897
|
+
while (lua.lua_next(this.L, -2) !== 0) {
|
|
898
|
+
if (lua.lua_type(this.L, -2) === lua.LUA_TSTRING) {
|
|
899
|
+
const key = to_jsstring(lua.lua_tostring(this.L, -2));
|
|
900
|
+
if (key.startsWith('_persist_')) {
|
|
901
|
+
result[key] = luaToJS(this.L, -1);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
lua.lua_pop(this.L, 1);
|
|
905
|
+
}
|
|
906
|
+
lua.lua_pop(this.L, 1);
|
|
907
|
+
return result;
|
|
908
|
+
}
|
|
909
|
+
// Full path
|
|
833
910
|
const result = luaToJS(this.L, -1);
|
|
834
911
|
lua.lua_pop(this.L, 1);
|
|
835
912
|
if (!result || typeof result !== 'object' || Array.isArray(result)) {
|
|
@@ -891,7 +968,8 @@ class SimulationRunner {
|
|
|
891
968
|
script,
|
|
892
969
|
gameDefinition,
|
|
893
970
|
seed,
|
|
894
|
-
logger: () => { },
|
|
971
|
+
logger: () => { },
|
|
972
|
+
simulationMode: true,
|
|
895
973
|
});
|
|
896
974
|
const spinCost = this.calculateSpinCost(startAction, bet, gameDefinition, params);
|
|
897
975
|
let totalWagered = 0;
|
|
@@ -908,36 +986,29 @@ class SimulationRunner {
|
|
|
908
986
|
for (let i = 0; i < iterations; i++) {
|
|
909
987
|
totalWagered += spinCost;
|
|
910
988
|
let roundWin = 0;
|
|
989
|
+
let roundBonusWin = 0;
|
|
911
990
|
// Execute the starting action
|
|
912
|
-
|
|
991
|
+
let result = engine.execute({
|
|
913
992
|
action: startAction,
|
|
914
993
|
bet,
|
|
915
994
|
params,
|
|
916
995
|
});
|
|
917
996
|
const baseWin = result.totalWin;
|
|
918
997
|
roundWin += baseWin;
|
|
919
|
-
// If a
|
|
998
|
+
// If a session was created, play through it using nextActions from the engine
|
|
920
999
|
if (result.session && !result.session.completed) {
|
|
921
1000
|
bonusTriggered++;
|
|
922
|
-
// Find the bonus action from nextActions (different from startAction)
|
|
923
|
-
const bonusAction = result.nextActions.find(a => a !== startAction)
|
|
924
|
-
?? result.nextActions[0];
|
|
925
|
-
// Play bonus spins until session completes
|
|
926
|
-
let bonusSessionWin = 0;
|
|
927
1001
|
let safetyLimit = 10_000;
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
bet,
|
|
933
|
-
});
|
|
934
|
-
bonusSessionWin += lastResult.totalWin;
|
|
1002
|
+
while (result.session && !result.session.completed && safetyLimit-- > 0) {
|
|
1003
|
+
const nextAction = result.nextActions[0];
|
|
1004
|
+
result = engine.execute({ action: nextAction, bet });
|
|
1005
|
+
roundBonusWin += result.totalWin;
|
|
935
1006
|
bonusSpinsPlayed++;
|
|
936
1007
|
}
|
|
937
|
-
|
|
938
|
-
roundWin += bonusSessionWin;
|
|
1008
|
+
roundWin += roundBonusWin;
|
|
939
1009
|
}
|
|
940
1010
|
baseGameWin += baseWin;
|
|
1011
|
+
bonusWin += roundBonusWin;
|
|
941
1012
|
totalWon += roundWin;
|
|
942
1013
|
if (roundWin > 0)
|
|
943
1014
|
hits++;
|