@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.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;
|
|
@@ -606,22 +638,20 @@ class LuaEngine {
|
|
|
606
638
|
persistentState;
|
|
607
639
|
gameDefinition;
|
|
608
640
|
variables = {};
|
|
641
|
+
simulationMode;
|
|
609
642
|
constructor(config) {
|
|
610
643
|
this.gameDefinition = config.gameDefinition;
|
|
611
|
-
|
|
644
|
+
this.simulationMode = config.simulationMode ?? false;
|
|
612
645
|
const rng = config.seed !== undefined
|
|
613
646
|
? createSeededRng(config.seed)
|
|
614
647
|
: undefined;
|
|
615
|
-
// Initialize sub-managers
|
|
616
648
|
this.api = new LuaEngineAPI(config.gameDefinition, rng, config.logger);
|
|
617
649
|
this.actionRouter = new ActionRouter(config.gameDefinition);
|
|
618
650
|
this.sessionManager = new SessionManager();
|
|
619
651
|
this.persistentState = new PersistentState(config.gameDefinition.persistent_state);
|
|
620
|
-
// Create Lua state and load standard libraries
|
|
621
652
|
this.L = lauxlib.luaL_newstate();
|
|
622
653
|
lualib.luaL_openlibs(this.L);
|
|
623
|
-
// Polyfill Lua 5.1/5.2 functions removed in 5.3
|
|
624
|
-
// Platform server may use gopher-lua (5.1) — these ensure compatibility.
|
|
654
|
+
// Polyfill Lua 5.1/5.2 functions removed in 5.3
|
|
625
655
|
lauxlib.luaL_dostring(this.L, to_luastring(`
|
|
626
656
|
math.pow = function(a, b) return a ^ b end
|
|
627
657
|
math.atan2 = math.atan2 or function(y, x) return math.atan(y, x) end
|
|
@@ -639,32 +669,36 @@ class LuaEngine {
|
|
|
639
669
|
loadstring = loadstring or load
|
|
640
670
|
table.getn = table.getn or function(t) return #t end
|
|
641
671
|
`));
|
|
642
|
-
// Register engine.* API
|
|
643
672
|
this.api.register(this.L);
|
|
644
|
-
// Load and compile the script
|
|
645
673
|
this.loadScript(config.script);
|
|
646
674
|
}
|
|
647
|
-
/** Current session data (if any) */
|
|
648
675
|
get session() {
|
|
649
676
|
return this.sessionManager.current;
|
|
650
677
|
}
|
|
651
|
-
/** Current persistent state values */
|
|
652
678
|
get persistentVars() {
|
|
653
679
|
return { ...this.variables };
|
|
654
680
|
}
|
|
655
681
|
/**
|
|
656
|
-
* Execute a play action —
|
|
657
|
-
* This is what DevBridge calls on each PLAY_REQUEST.
|
|
682
|
+
* Execute a play action — replicates server's Play() function.
|
|
658
683
|
*/
|
|
659
684
|
execute(params) {
|
|
660
|
-
const { action: actionName,
|
|
661
|
-
// 1. Resolve
|
|
685
|
+
const { action: actionName, params: clientParams } = params;
|
|
686
|
+
// 1. Resolve action
|
|
662
687
|
const action = this.actionRouter.resolveAction(actionName, this.sessionManager.isActive);
|
|
663
|
-
// 2.
|
|
664
|
-
|
|
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
|
+
};
|
|
665
699
|
// Load cross-spin persistent state
|
|
666
700
|
this.persistentState.loadIntoVariables(stateVars);
|
|
667
|
-
// Load session persistent vars
|
|
701
|
+
// Load session persistent vars + restore spinsRemaining
|
|
668
702
|
if (this.sessionManager.isActive) {
|
|
669
703
|
const sessionParams = this.sessionManager.getPersistentParams();
|
|
670
704
|
for (const [k, v] of Object.entries(sessionParams)) {
|
|
@@ -672,8 +706,14 @@ class LuaEngine {
|
|
|
672
706
|
stateVars[k] = v;
|
|
673
707
|
}
|
|
674
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;
|
|
675
715
|
}
|
|
676
|
-
//
|
|
716
|
+
// 4. Build state.params
|
|
677
717
|
const stateParams = { ...clientParams };
|
|
678
718
|
stateParams._action = actionName;
|
|
679
719
|
// Inject session _ps_* persistent data
|
|
@@ -701,16 +741,16 @@ class LuaEngine {
|
|
|
701
741
|
if (clientParams?.ante_bet && this.gameDefinition.ante_bet) {
|
|
702
742
|
stateParams.ante_bet = true;
|
|
703
743
|
}
|
|
704
|
-
//
|
|
705
|
-
const luaResult = this.callLuaExecute(action.stage, stateParams, stateVars);
|
|
706
|
-
//
|
|
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)
|
|
707
747
|
const totalWinMultiplier = typeof luaResult.total_win === 'number' ? luaResult.total_win : 0;
|
|
708
748
|
const resultVariables = (luaResult.variables ?? {});
|
|
709
|
-
const
|
|
749
|
+
const spinWin = Math.round(totalWinMultiplier * bet * 100) / 100;
|
|
710
750
|
// Merge result variables into engine variables
|
|
711
751
|
Object.assign(stateVars, resultVariables);
|
|
712
752
|
this.variables = { ...stateVars };
|
|
713
|
-
delete this.variables.bet;
|
|
753
|
+
delete this.variables.bet;
|
|
714
754
|
// Build client data (everything except special keys)
|
|
715
755
|
const data = {};
|
|
716
756
|
for (const [key, value] of Object.entries(luaResult)) {
|
|
@@ -718,17 +758,6 @@ class LuaEngine {
|
|
|
718
758
|
data[key] = value;
|
|
719
759
|
}
|
|
720
760
|
}
|
|
721
|
-
// 6. Apply max win cap
|
|
722
|
-
let cappedWin = totalWin;
|
|
723
|
-
if (this.gameDefinition.max_win) {
|
|
724
|
-
const cap = this.calculateMaxWinCap(bet);
|
|
725
|
-
if (cap !== undefined && totalWin > cap) {
|
|
726
|
-
cappedWin = cap;
|
|
727
|
-
this.variables.max_win_reached = 1;
|
|
728
|
-
data.max_win_reached = true;
|
|
729
|
-
this.sessionManager.markMaxWinReached();
|
|
730
|
-
}
|
|
731
|
-
}
|
|
732
761
|
// 7. Handle _persist_* and _persist_game_* keys
|
|
733
762
|
this.sessionManager.storePersistData(data);
|
|
734
763
|
this.persistentState.storeGameData(data);
|
|
@@ -745,31 +774,56 @@ class LuaEngine {
|
|
|
745
774
|
delete data[key];
|
|
746
775
|
}
|
|
747
776
|
}
|
|
748
|
-
// 8. Evaluate transitions
|
|
777
|
+
// 8. Evaluate transitions (server: evaluateTransitions)
|
|
749
778
|
const { rule, nextActions } = this.actionRouter.evaluateTransitions(action, this.variables);
|
|
779
|
+
// 9. Determine credit behavior (server: creditNow logic)
|
|
750
780
|
let creditDeferred = action.credit === 'defer' || rule.credit_override === 'defer';
|
|
781
|
+
// 10. Session lifecycle (server: create/update/complete session)
|
|
751
782
|
let session = this.sessionManager.current;
|
|
752
|
-
|
|
783
|
+
let resultTotalWin = spinWin;
|
|
784
|
+
let sessionCompleted = false;
|
|
785
|
+
// Calculate max win cap for session
|
|
786
|
+
const maxWinCap = this.calculateMaxWinCap(bet);
|
|
753
787
|
if (rule.creates_session && !this.sessionManager.isActive) {
|
|
754
|
-
|
|
788
|
+
// CREATE SESSION — initial spin counted (server: createSession includes spinWin)
|
|
789
|
+
session = this.sessionManager.createSession(rule, this.variables, bet, spinWin, maxWinCap);
|
|
755
790
|
creditDeferred = true;
|
|
791
|
+
resultTotalWin = spinWin;
|
|
792
|
+
// Clear the trigger variable — it was consumed to set spinsRemaining
|
|
793
|
+
if (rule.session_config?.total_spins_var) {
|
|
794
|
+
delete this.variables[rule.session_config.total_spins_var];
|
|
795
|
+
}
|
|
756
796
|
}
|
|
757
|
-
// Handle session update
|
|
758
797
|
else if (this.sessionManager.isActive) {
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
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
|
|
762
802
|
const completed = this.sessionManager.completeSession();
|
|
763
803
|
session = completed.session;
|
|
804
|
+
resultTotalWin = completed.totalWin;
|
|
805
|
+
sessionCompleted = true;
|
|
764
806
|
creditDeferred = false;
|
|
765
|
-
// Clean up session-scoped variables
|
|
807
|
+
// Clean up session-scoped variables
|
|
766
808
|
for (const varName of completed.sessionVarNames) {
|
|
767
809
|
delete this.variables[varName];
|
|
768
810
|
}
|
|
769
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;
|
|
770
824
|
}
|
|
771
825
|
return {
|
|
772
|
-
totalWin:
|
|
826
|
+
totalWin: Math.round(resultTotalWin * 100) / 100,
|
|
773
827
|
data,
|
|
774
828
|
nextActions,
|
|
775
829
|
session,
|
|
@@ -777,13 +831,11 @@ class LuaEngine {
|
|
|
777
831
|
creditDeferred,
|
|
778
832
|
};
|
|
779
833
|
}
|
|
780
|
-
/** Reset all state (sessions, persistent vars, variables) */
|
|
781
834
|
reset() {
|
|
782
835
|
this.variables = {};
|
|
783
836
|
this.sessionManager.reset();
|
|
784
837
|
this.persistentState.reset();
|
|
785
838
|
}
|
|
786
|
-
/** Destroy the Lua VM */
|
|
787
839
|
destroy() {
|
|
788
840
|
if (this.L) {
|
|
789
841
|
lua.lua_close(this.L);
|
|
@@ -798,7 +850,6 @@ class LuaEngine {
|
|
|
798
850
|
lua.lua_pop(this.L, 1);
|
|
799
851
|
throw new Error(`Failed to load Lua script: ${err}`);
|
|
800
852
|
}
|
|
801
|
-
// Verify that execute() function exists
|
|
802
853
|
lua.lua_getglobal(this.L, to_luastring('execute'));
|
|
803
854
|
if (lua.lua_type(this.L, -1) !== lua.LUA_TFUNCTION) {
|
|
804
855
|
lua.lua_pop(this.L, 1);
|
|
@@ -806,28 +857,54 @@ class LuaEngine {
|
|
|
806
857
|
}
|
|
807
858
|
lua.lua_pop(this.L, 1);
|
|
808
859
|
}
|
|
809
|
-
callLuaExecute(stage, params, variables) {
|
|
810
|
-
// Push the execute function
|
|
860
|
+
callLuaExecute(stage, action, params, variables) {
|
|
811
861
|
lua.lua_getglobal(this.L, to_luastring('execute'));
|
|
812
|
-
// Build
|
|
813
|
-
lua.lua_createtable(this.L, 0,
|
|
862
|
+
// Build state table: {stage, action, params, variables}
|
|
863
|
+
lua.lua_createtable(this.L, 0, 4);
|
|
814
864
|
// state.stage
|
|
815
865
|
lua.lua_pushstring(this.L, to_luastring(stage));
|
|
816
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'));
|
|
817
870
|
// state.params
|
|
818
871
|
pushJSValue(this.L, params);
|
|
819
872
|
lua.lua_setfield(this.L, -2, to_luastring('params'));
|
|
820
873
|
// state.variables
|
|
821
874
|
pushJSValue(this.L, variables);
|
|
822
875
|
lua.lua_setfield(this.L, -2, to_luastring('variables'));
|
|
823
|
-
// Call execute(state) → 1 result
|
|
824
876
|
const status = lua.lua_pcall(this.L, 1, 1, 0);
|
|
825
877
|
if (status !== lua.LUA_OK) {
|
|
826
878
|
const err = to_jsstring(lua.lua_tostring(this.L, -1));
|
|
827
879
|
lua.lua_pop(this.L, 1);
|
|
828
880
|
throw new Error(`Lua execute() failed: ${err}`);
|
|
829
881
|
}
|
|
830
|
-
|
|
882
|
+
if (this.simulationMode) {
|
|
883
|
+
// Fast path: extract only total_win, variables, _persist_* keys
|
|
884
|
+
const result = {};
|
|
885
|
+
lua.lua_getfield(this.L, -1, to_luastring('total_win'));
|
|
886
|
+
result.total_win = lua.lua_type(this.L, -1) === lua.LUA_TNUMBER
|
|
887
|
+
? lua.lua_tonumber(this.L, -1) : 0;
|
|
888
|
+
lua.lua_pop(this.L, 1);
|
|
889
|
+
lua.lua_getfield(this.L, -1, to_luastring('variables'));
|
|
890
|
+
if (lua.lua_type(this.L, -1) === lua.LUA_TTABLE) {
|
|
891
|
+
result.variables = luaToJS(this.L, -1);
|
|
892
|
+
}
|
|
893
|
+
lua.lua_pop(this.L, 1);
|
|
894
|
+
lua.lua_pushnil(this.L);
|
|
895
|
+
while (lua.lua_next(this.L, -2) !== 0) {
|
|
896
|
+
if (lua.lua_type(this.L, -2) === lua.LUA_TSTRING) {
|
|
897
|
+
const key = to_jsstring(lua.lua_tostring(this.L, -2));
|
|
898
|
+
if (key.startsWith('_persist_')) {
|
|
899
|
+
result[key] = luaToJS(this.L, -1);
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
lua.lua_pop(this.L, 1);
|
|
903
|
+
}
|
|
904
|
+
lua.lua_pop(this.L, 1);
|
|
905
|
+
return result;
|
|
906
|
+
}
|
|
907
|
+
// Full path
|
|
831
908
|
const result = luaToJS(this.L, -1);
|
|
832
909
|
lua.lua_pop(this.L, 1);
|
|
833
910
|
if (!result || typeof result !== 'object' || Array.isArray(result)) {
|
|
@@ -889,7 +966,8 @@ class SimulationRunner {
|
|
|
889
966
|
script,
|
|
890
967
|
gameDefinition,
|
|
891
968
|
seed,
|
|
892
|
-
logger: () => { },
|
|
969
|
+
logger: () => { },
|
|
970
|
+
simulationMode: true,
|
|
893
971
|
});
|
|
894
972
|
const spinCost = this.calculateSpinCost(startAction, bet, gameDefinition, params);
|
|
895
973
|
let totalWagered = 0;
|
|
@@ -906,36 +984,29 @@ class SimulationRunner {
|
|
|
906
984
|
for (let i = 0; i < iterations; i++) {
|
|
907
985
|
totalWagered += spinCost;
|
|
908
986
|
let roundWin = 0;
|
|
987
|
+
let roundBonusWin = 0;
|
|
909
988
|
// Execute the starting action
|
|
910
|
-
|
|
989
|
+
let result = engine.execute({
|
|
911
990
|
action: startAction,
|
|
912
991
|
bet,
|
|
913
992
|
params,
|
|
914
993
|
});
|
|
915
994
|
const baseWin = result.totalWin;
|
|
916
995
|
roundWin += baseWin;
|
|
917
|
-
// If a
|
|
996
|
+
// If a session was created, play through it using nextActions from the engine
|
|
918
997
|
if (result.session && !result.session.completed) {
|
|
919
998
|
bonusTriggered++;
|
|
920
|
-
// Find the bonus action from nextActions (different from startAction)
|
|
921
|
-
const bonusAction = result.nextActions.find(a => a !== startAction)
|
|
922
|
-
?? result.nextActions[0];
|
|
923
|
-
// Play bonus spins until session completes
|
|
924
|
-
let bonusSessionWin = 0;
|
|
925
999
|
let safetyLimit = 10_000;
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
bet,
|
|
931
|
-
});
|
|
932
|
-
bonusSessionWin += lastResult.totalWin;
|
|
1000
|
+
while (result.session && !result.session.completed && safetyLimit-- > 0) {
|
|
1001
|
+
const nextAction = result.nextActions[0];
|
|
1002
|
+
result = engine.execute({ action: nextAction, bet });
|
|
1003
|
+
roundBonusWin += result.totalWin;
|
|
933
1004
|
bonusSpinsPlayed++;
|
|
934
1005
|
}
|
|
935
|
-
|
|
936
|
-
roundWin += bonusSessionWin;
|
|
1006
|
+
roundWin += roundBonusWin;
|
|
937
1007
|
}
|
|
938
1008
|
baseGameWin += baseWin;
|
|
1009
|
+
bonusWin += roundBonusWin;
|
|
939
1010
|
totalWon += roundWin;
|
|
940
1011
|
if (roundWin > 0)
|
|
941
1012
|
hits++;
|