@energy8platform/game-engine 0.10.1 → 0.10.3

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/index.cjs.js CHANGED
@@ -3965,841 +3965,6 @@ class ScrollContainer extends ui.ScrollBox {
3965
3965
  }
3966
3966
  }
3967
3967
 
3968
- const fengari$1 = require('fengari');
3969
- const { lua: lua$1, lauxlib: lauxlib$1 } = fengari$1;
3970
- const { to_luastring: to_luastring$1, to_jsstring: to_jsstring$1 } = fengari$1;
3971
- /**
3972
- * Seeded xoshiro128** PRNG for deterministic simulation/replay.
3973
- * Period: 2^128 - 1
3974
- */
3975
- function createSeededRng(seed) {
3976
- let s0 = (seed >>> 0) | 1;
3977
- let s1 = (seed * 1103515245 + 12345) >>> 0;
3978
- let s2 = (seed * 6364136223846793005 + 1442695040888963407) >>> 0;
3979
- let s3 = (seed * 1442695040888963407 + 6364136223846793005) >>> 0;
3980
- return () => {
3981
- const result = (((s1 * 5) << 7) * 9) >>> 0;
3982
- const t = s1 << 9;
3983
- s2 ^= s0;
3984
- s3 ^= s1;
3985
- s1 ^= s2;
3986
- s0 ^= s3;
3987
- s2 ^= t;
3988
- s3 = ((s3 << 11) | (s3 >>> 21)) >>> 0;
3989
- return result / 4294967296;
3990
- };
3991
- }
3992
- /**
3993
- * Implements and registers all platform `engine.*` functions into a Lua state.
3994
- */
3995
- class LuaEngineAPI {
3996
- rng;
3997
- logger;
3998
- gameDefinition;
3999
- constructor(gameDefinition, rng, logger) {
4000
- this.gameDefinition = gameDefinition;
4001
- this.rng = rng ?? Math.random;
4002
- this.logger = logger ?? ((level, msg) => {
4003
- const fn = level === 'error' ? console.error
4004
- : level === 'warn' ? console.warn
4005
- : level === 'debug' ? console.debug
4006
- : console.log;
4007
- fn(`[Lua:${level}] ${msg}`);
4008
- });
4009
- }
4010
- /** Register `engine` global table on the Lua state */
4011
- register(L) {
4012
- // Create the `engine` table
4013
- lua$1.lua_newtable(L);
4014
- this.registerFunction(L, 'random', (LS) => {
4015
- const min = lauxlib$1.luaL_checkinteger(LS, 1);
4016
- const max = lauxlib$1.luaL_checkinteger(LS, 2);
4017
- const result = this.random(Number(min), Number(max));
4018
- lua$1.lua_pushinteger(LS, result);
4019
- return 1;
4020
- });
4021
- this.registerFunction(L, 'random_float', (LS) => {
4022
- lua$1.lua_pushnumber(LS, this.randomFloat());
4023
- return 1;
4024
- });
4025
- this.registerFunction(L, 'random_weighted', (LS) => {
4026
- lauxlib$1.luaL_checktype(LS, 1, lua$1.LUA_TTABLE);
4027
- const weights = [];
4028
- const len = lua$1.lua_rawlen(LS, 1);
4029
- for (let i = 1; i <= len; i++) {
4030
- lua$1.lua_rawgeti(LS, 1, i);
4031
- weights.push(lua$1.lua_tonumber(LS, -1));
4032
- lua$1.lua_pop(LS, 1);
4033
- }
4034
- const result = this.randomWeighted(weights);
4035
- lua$1.lua_pushinteger(LS, result);
4036
- return 1;
4037
- });
4038
- this.registerFunction(L, 'shuffle', (LS) => {
4039
- lauxlib$1.luaL_checktype(LS, 1, lua$1.LUA_TTABLE);
4040
- const arr = [];
4041
- const len = lua$1.lua_rawlen(LS, 1);
4042
- for (let i = 1; i <= len; i++) {
4043
- lua$1.lua_rawgeti(LS, 1, i);
4044
- arr.push(luaToJS(LS, -1));
4045
- lua$1.lua_pop(LS, 1);
4046
- }
4047
- const shuffled = this.shuffle(arr);
4048
- pushJSArray(LS, shuffled);
4049
- return 1;
4050
- });
4051
- this.registerFunction(L, 'log', (LS) => {
4052
- const level = to_jsstring$1(lauxlib$1.luaL_checkstring(LS, 1));
4053
- const msg = to_jsstring$1(lauxlib$1.luaL_checkstring(LS, 2));
4054
- this.logger(level, msg);
4055
- return 0;
4056
- });
4057
- this.registerFunction(L, 'get_config', (LS) => {
4058
- const config = this.getConfig();
4059
- pushJSObject(LS, config);
4060
- return 1;
4061
- });
4062
- // Set the table as global `engine`
4063
- lua$1.lua_setglobal(L, to_luastring$1('engine'));
4064
- }
4065
- // ─── engine.* implementations ─────────────────────────
4066
- random(min, max) {
4067
- return Math.floor(this.rng() * (max - min + 1)) + min;
4068
- }
4069
- randomFloat() {
4070
- return this.rng();
4071
- }
4072
- randomWeighted(weights) {
4073
- const totalWeight = weights.reduce((a, b) => a + b, 0);
4074
- let roll = this.rng() * totalWeight;
4075
- for (let i = 0; i < weights.length; i++) {
4076
- roll -= weights[i];
4077
- if (roll < 0)
4078
- return i + 1; // 1-based index
4079
- }
4080
- return weights.length; // fallback to last
4081
- }
4082
- shuffle(arr) {
4083
- const copy = [...arr];
4084
- for (let i = copy.length - 1; i > 0; i--) {
4085
- const j = Math.floor(this.rng() * (i + 1));
4086
- [copy[i], copy[j]] = [copy[j], copy[i]];
4087
- }
4088
- return copy;
4089
- }
4090
- getConfig() {
4091
- const def = this.gameDefinition;
4092
- let betLevels = [];
4093
- if (Array.isArray(def.bet_levels)) {
4094
- betLevels = def.bet_levels;
4095
- }
4096
- else if (def.bet_levels && 'levels' in def.bet_levels && def.bet_levels.levels) {
4097
- betLevels = def.bet_levels.levels;
4098
- }
4099
- return {
4100
- id: def.id,
4101
- type: def.type,
4102
- bet_levels: betLevels,
4103
- };
4104
- }
4105
- // ─── Helpers ──────────────────────────────────────────
4106
- registerFunction(L, name, fn) {
4107
- lua$1.lua_pushcfunction(L, fn);
4108
- lua$1.lua_setfield(L, -2, to_luastring$1(name));
4109
- }
4110
- }
4111
- // ─── Lua ↔ JS marshalling ───────────────────────────────
4112
- /** Read a Lua value at the given stack index and return its JS equivalent */
4113
- function luaToJS(L, idx) {
4114
- const type = lua$1.lua_type(L, idx);
4115
- switch (type) {
4116
- case lua$1.LUA_TNIL:
4117
- return null;
4118
- case lua$1.LUA_TBOOLEAN:
4119
- return lua$1.lua_toboolean(L, idx);
4120
- case lua$1.LUA_TNUMBER:
4121
- if (lua$1.lua_isinteger(L, idx)) {
4122
- return Number(lua$1.lua_tointeger(L, idx));
4123
- }
4124
- return lua$1.lua_tonumber(L, idx);
4125
- case lua$1.LUA_TSTRING:
4126
- return to_jsstring$1(lua$1.lua_tostring(L, idx));
4127
- case lua$1.LUA_TTABLE:
4128
- return luaTableToJS(L, idx);
4129
- default:
4130
- return null;
4131
- }
4132
- }
4133
- /** Convert a Lua table to a JS object or array */
4134
- function luaTableToJS(L, idx) {
4135
- // Normalize index to absolute
4136
- if (idx < 0)
4137
- idx = lua$1.lua_gettop(L) + idx + 1;
4138
- // Check if it's an array (sequential integer keys starting at 1)
4139
- const len = lua$1.lua_rawlen(L, idx);
4140
- if (len > 0) {
4141
- // Verify it's a pure array by checking key 1 exists
4142
- lua$1.lua_rawgeti(L, idx, 1);
4143
- const hasFirst = lua$1.lua_type(L, -1) !== lua$1.LUA_TNIL;
4144
- lua$1.lua_pop(L, 1);
4145
- if (hasFirst) {
4146
- // Check if there are also string keys (mixed table)
4147
- let hasStringKeys = false;
4148
- lua$1.lua_pushnil(L);
4149
- while (lua$1.lua_next(L, idx) !== 0) {
4150
- lua$1.lua_pop(L, 1); // pop value
4151
- if (lua$1.lua_type(L, -1) === lua$1.LUA_TSTRING) {
4152
- hasStringKeys = true;
4153
- lua$1.lua_pop(L, 1); // pop key
4154
- break;
4155
- }
4156
- }
4157
- if (!hasStringKeys) {
4158
- // Pure array
4159
- const arr = [];
4160
- for (let i = 1; i <= len; i++) {
4161
- lua$1.lua_rawgeti(L, idx, i);
4162
- arr.push(luaToJS(L, -1));
4163
- lua$1.lua_pop(L, 1);
4164
- }
4165
- return arr;
4166
- }
4167
- }
4168
- }
4169
- // Object (or mixed table)
4170
- const obj = {};
4171
- lua$1.lua_pushnil(L);
4172
- while (lua$1.lua_next(L, idx) !== 0) {
4173
- const keyType = lua$1.lua_type(L, -2);
4174
- let key;
4175
- if (keyType === lua$1.LUA_TSTRING) {
4176
- key = to_jsstring$1(lua$1.lua_tostring(L, -2));
4177
- }
4178
- else if (keyType === lua$1.LUA_TNUMBER) {
4179
- key = String(lua$1.lua_tonumber(L, -2));
4180
- }
4181
- else {
4182
- lua$1.lua_pop(L, 1);
4183
- continue;
4184
- }
4185
- obj[key] = luaToJS(L, -1);
4186
- lua$1.lua_pop(L, 1);
4187
- }
4188
- return obj;
4189
- }
4190
- /** Push a JS value onto the Lua stack */
4191
- function pushJSValue(L, value) {
4192
- if (value === null || value === undefined) {
4193
- lua$1.lua_pushnil(L);
4194
- }
4195
- else if (typeof value === 'boolean') {
4196
- lua$1.lua_pushboolean(L, value ? 1 : 0);
4197
- }
4198
- else if (typeof value === 'number') {
4199
- if (Number.isInteger(value)) {
4200
- lua$1.lua_pushinteger(L, value);
4201
- }
4202
- else {
4203
- lua$1.lua_pushnumber(L, value);
4204
- }
4205
- }
4206
- else if (typeof value === 'string') {
4207
- lua$1.lua_pushstring(L, to_luastring$1(value));
4208
- }
4209
- else if (Array.isArray(value)) {
4210
- pushJSArray(L, value);
4211
- }
4212
- else if (typeof value === 'object') {
4213
- pushJSObject(L, value);
4214
- }
4215
- else {
4216
- lua$1.lua_pushnil(L);
4217
- }
4218
- }
4219
- /** Push a JS array as a Lua table (1-based) */
4220
- function pushJSArray(L, arr) {
4221
- lua$1.lua_createtable(L, arr.length, 0);
4222
- for (let i = 0; i < arr.length; i++) {
4223
- pushJSValue(L, arr[i]);
4224
- lua$1.lua_rawseti(L, -2, i + 1);
4225
- }
4226
- }
4227
- /** Push a JS object as a Lua table */
4228
- function pushJSObject(L, obj) {
4229
- const keys = Object.keys(obj);
4230
- lua$1.lua_createtable(L, 0, keys.length);
4231
- for (const key of keys) {
4232
- pushJSValue(L, obj[key]);
4233
- lua$1.lua_setfield(L, -2, to_luastring$1(key));
4234
- }
4235
- }
4236
-
4237
- /**
4238
- * Replicates the platform's action dispatch and transition evaluation.
4239
- * Routes play requests to the correct action, evaluates transition conditions
4240
- * against current variables to determine next actions and session operations.
4241
- */
4242
- class ActionRouter {
4243
- actions;
4244
- constructor(gameDefinition) {
4245
- this.actions = gameDefinition.actions;
4246
- }
4247
- /** Look up action by name and validate prerequisites */
4248
- resolveAction(actionName, hasSession) {
4249
- const action = this.actions[actionName];
4250
- if (!action) {
4251
- throw new Error(`Unknown action: "${actionName}". Available: ${Object.keys(this.actions).join(', ')}`);
4252
- }
4253
- if (action.requires_session && !hasSession) {
4254
- throw new Error(`Action "${actionName}" requires an active session`);
4255
- }
4256
- return action;
4257
- }
4258
- /** Evaluate transitions in order, return the first matching rule */
4259
- evaluateTransitions(action, variables) {
4260
- for (const rule of action.transitions) {
4261
- if (evaluateCondition(rule.condition, variables)) {
4262
- return { rule, nextActions: rule.next_actions };
4263
- }
4264
- }
4265
- throw new Error(`No matching transition for action with stage "${action.stage}". ` +
4266
- `Variables: ${JSON.stringify(variables)}`);
4267
- }
4268
- }
4269
- // ─── Condition Evaluator ────────────────────────────────
4270
- /**
4271
- * Evaluates a transition condition expression against variables.
4272
- *
4273
- * Supports:
4274
- * - "always" → true
4275
- * - Simple comparisons: "var > 0", "var == 1", "var >= 10", "var != 0", "var < 5", "var <= 3"
4276
- * - Logical connectives: "expr && expr", "expr || expr"
4277
- *
4278
- * This covers all patterns used by the platform's govaluate conditions.
4279
- */
4280
- function evaluateCondition(condition, variables) {
4281
- const trimmed = condition.trim();
4282
- if (trimmed === 'always')
4283
- return true;
4284
- // Handle || (OR) — lowest precedence
4285
- if (trimmed.includes('||')) {
4286
- const parts = splitOnOperator(trimmed, '||');
4287
- return parts.some(part => evaluateCondition(part, variables));
4288
- }
4289
- // Handle && (AND)
4290
- if (trimmed.includes('&&')) {
4291
- const parts = splitOnOperator(trimmed, '&&');
4292
- return parts.every(part => evaluateCondition(part, variables));
4293
- }
4294
- // Single comparison: "variable op value"
4295
- return evaluateComparison(trimmed, variables);
4296
- }
4297
- function splitOnOperator(expr, operator) {
4298
- const parts = [];
4299
- let depth = 0;
4300
- let current = '';
4301
- for (let i = 0; i < expr.length; i++) {
4302
- if (expr[i] === '(')
4303
- depth++;
4304
- else if (expr[i] === ')')
4305
- depth--;
4306
- if (depth === 0 && expr.substring(i, i + operator.length) === operator) {
4307
- parts.push(current);
4308
- current = '';
4309
- i += operator.length - 1;
4310
- }
4311
- else {
4312
- current += expr[i];
4313
- }
4314
- }
4315
- parts.push(current);
4316
- return parts;
4317
- }
4318
- function evaluateComparison(expr, variables) {
4319
- // Match: variable_name operator value
4320
- const match = expr.trim().match(/^\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*(>=|<=|!=|==|>|<)\s*(-?\d+(?:\.\d+)?)\s*$/);
4321
- if (!match) {
4322
- throw new Error(`Cannot parse condition: "${expr}"`);
4323
- }
4324
- const [, varName, op, valueStr] = match;
4325
- const left = variables[varName] ?? 0;
4326
- const right = parseFloat(valueStr);
4327
- switch (op) {
4328
- case '>': return left > right;
4329
- case '>=': return left >= right;
4330
- case '<': return left < right;
4331
- case '<=': return left <= right;
4332
- case '==': return left === right;
4333
- case '!=': return left !== right;
4334
- default: return false;
4335
- }
4336
- }
4337
-
4338
- /**
4339
- * Manages session lifecycle: creation, spin counting, retriggers, and completion.
4340
- * Handles both slot sessions (fixed spin count) and table game sessions (unlimited).
4341
- * Also manages _persist_ data roundtrip between Lua calls.
4342
- */
4343
- class SessionManager {
4344
- session = null;
4345
- get isActive() {
4346
- return this.session !== null && !this.session.completed;
4347
- }
4348
- get current() {
4349
- if (!this.session)
4350
- return null;
4351
- return this.toSessionData();
4352
- }
4353
- get sessionTotalWin() {
4354
- return this.session?.totalWin ?? 0;
4355
- }
4356
- /** Create a new session from a transition rule */
4357
- createSession(rule, variables, bet) {
4358
- let spinsRemaining = -1; // unlimited by default
4359
- if (rule.session_config?.total_spins_var) {
4360
- const varName = rule.session_config.total_spins_var;
4361
- spinsRemaining = variables[varName] ?? -1;
4362
- }
4363
- const persistentVars = {};
4364
- if (rule.session_config?.persistent_vars) {
4365
- for (const varName of rule.session_config.persistent_vars) {
4366
- persistentVars[varName] = variables[varName] ?? 0;
4367
- }
4368
- }
4369
- this.session = {
4370
- spinsRemaining,
4371
- spinsPlayed: 0,
4372
- totalWin: 0,
4373
- completed: false,
4374
- maxWinReached: false,
4375
- bet,
4376
- persistentVars,
4377
- persistentData: {},
4378
- };
4379
- return this.toSessionData();
4380
- }
4381
- /** Update session after a spin: decrement counter, accumulate win, handle retrigger */
4382
- updateSession(rule, variables, spinWin) {
4383
- if (!this.session)
4384
- throw new Error('No active session');
4385
- // Accumulate win
4386
- this.session.totalWin += spinWin;
4387
- this.session.spinsPlayed++;
4388
- // Decrement spins (only for non-unlimited sessions)
4389
- if (this.session.spinsRemaining > 0) {
4390
- this.session.spinsRemaining--;
4391
- }
4392
- // Handle retrigger (add_spins_var)
4393
- if (rule.add_spins_var) {
4394
- const extraSpins = variables[rule.add_spins_var] ?? 0;
4395
- if (extraSpins > 0 && this.session.spinsRemaining >= 0) {
4396
- this.session.spinsRemaining += extraSpins;
4397
- }
4398
- }
4399
- // Update persistent vars
4400
- if (this.session.persistentVars) {
4401
- for (const key of Object.keys(this.session.persistentVars)) {
4402
- if (key in variables) {
4403
- this.session.persistentVars[key] = variables[key];
4404
- }
4405
- }
4406
- }
4407
- // Auto-complete if spins exhausted
4408
- if (this.session.spinsRemaining === 0) {
4409
- this.session.completed = true;
4410
- }
4411
- return this.toSessionData();
4412
- }
4413
- /** Complete the session, return accumulated totalWin */
4414
- completeSession() {
4415
- if (!this.session)
4416
- throw new Error('No active session to complete');
4417
- this.session.completed = true;
4418
- const totalWin = this.session.totalWin;
4419
- const session = this.toSessionData();
4420
- return { totalWin, session };
4421
- }
4422
- /** Mark max win reached — stops the session */
4423
- markMaxWinReached() {
4424
- if (this.session) {
4425
- this.session.maxWinReached = true;
4426
- this.session.completed = true;
4427
- }
4428
- }
4429
- /** Store _persist_* data extracted from Lua result */
4430
- storePersistData(data) {
4431
- if (!this.session)
4432
- return;
4433
- for (const key of Object.keys(data)) {
4434
- if (key.startsWith('_persist_')) {
4435
- const cleanKey = key.slice('_persist_'.length);
4436
- this.session.persistentData[cleanKey] = data[key];
4437
- }
4438
- }
4439
- }
4440
- /** Get _ps_* params to inject into next execute() call */
4441
- getPersistentParams() {
4442
- if (!this.session)
4443
- return {};
4444
- const params = {};
4445
- // Session persistent vars (float64)
4446
- for (const [key, value] of Object.entries(this.session.persistentVars)) {
4447
- params[key] = value;
4448
- }
4449
- // _persist_ complex data → _ps_*
4450
- for (const [key, value] of Object.entries(this.session.persistentData)) {
4451
- params[`_ps_${key}`] = value;
4452
- }
4453
- return params;
4454
- }
4455
- /** Reset all session state */
4456
- reset() {
4457
- this.session = null;
4458
- }
4459
- toSessionData() {
4460
- if (!this.session)
4461
- throw new Error('No session');
4462
- return {
4463
- spinsRemaining: this.session.spinsRemaining,
4464
- spinsPlayed: this.session.spinsPlayed,
4465
- totalWin: Math.round(this.session.totalWin * 100) / 100,
4466
- completed: this.session.completed,
4467
- maxWinReached: this.session.maxWinReached,
4468
- betAmount: this.session.bet,
4469
- };
4470
- }
4471
- }
4472
-
4473
- /**
4474
- * Manages cross-spin persistent state — variables that survive between base game spins.
4475
- * Separate from session-scoped persistence (handled by SessionManager).
4476
- *
4477
- * Handles two mechanisms:
4478
- * 1. Numeric vars declared in `persistent_state.vars` — stored in state.variables
4479
- * 2. Complex data with `_persist_game_*` prefix — stored separately, injected as `_ps_*`
4480
- */
4481
- class PersistentState {
4482
- config;
4483
- vars = {};
4484
- gameData = {};
4485
- constructor(config) {
4486
- this.config = config;
4487
- }
4488
- /** Load persistent vars into variables map before execute() */
4489
- loadIntoVariables(variables) {
4490
- if (!this.config)
4491
- return;
4492
- for (const varName of this.config.vars) {
4493
- if (varName in this.vars) {
4494
- variables[varName] = this.vars[varName];
4495
- }
4496
- }
4497
- }
4498
- /** Save persistent vars from variables map after execute() */
4499
- saveFromVariables(variables) {
4500
- if (!this.config)
4501
- return;
4502
- for (const varName of this.config.vars) {
4503
- if (varName in variables) {
4504
- this.vars[varName] = variables[varName];
4505
- }
4506
- }
4507
- }
4508
- /** Extract _persist_game_* keys from Lua return data, store them */
4509
- storeGameData(data) {
4510
- for (const key of Object.keys(data)) {
4511
- if (key.startsWith('_persist_game_')) {
4512
- const cleanKey = key.slice('_persist_game_'.length);
4513
- this.gameData[cleanKey] = data[key];
4514
- delete data[key]; // remove from client data
4515
- }
4516
- }
4517
- }
4518
- /** Get _ps_* params for next execute() call */
4519
- getGameDataParams() {
4520
- const params = {};
4521
- for (const [key, value] of Object.entries(this.gameData)) {
4522
- params[`_ps_${key}`] = value;
4523
- }
4524
- return params;
4525
- }
4526
- /** Get exposed vars for client data.persistent_state */
4527
- getExposedVars() {
4528
- if (!this.config?.exposed_vars?.length)
4529
- return undefined;
4530
- const exposed = {};
4531
- for (const varName of this.config.exposed_vars) {
4532
- if (varName in this.vars) {
4533
- exposed[varName] = this.vars[varName];
4534
- }
4535
- }
4536
- return exposed;
4537
- }
4538
- /** Reset all state */
4539
- reset() {
4540
- this.vars = {};
4541
- this.gameData = {};
4542
- }
4543
- }
4544
-
4545
- const fengari = require('fengari');
4546
- const { lua, lauxlib, lualib } = fengari;
4547
- const { to_luastring, to_jsstring } = fengari;
4548
- /**
4549
- * Runs Lua game scripts locally, replicating the platform's server-side execution.
4550
- *
4551
- * Implements the full lifecycle: action routing → state assembly → Lua execute() →
4552
- * result extraction → transition evaluation → session management.
4553
- *
4554
- * @example
4555
- * ```ts
4556
- * const engine = new LuaEngine({
4557
- * script: luaSource,
4558
- * gameDefinition: { id: 'my-slot', type: 'SLOT', actions: { ... } },
4559
- * });
4560
- *
4561
- * const result = engine.execute({ action: 'spin', bet: 1.0 });
4562
- * // result.data.matrix, result.totalWin, result.nextActions, etc.
4563
- * ```
4564
- */
4565
- class LuaEngine {
4566
- L;
4567
- api;
4568
- actionRouter;
4569
- sessionManager;
4570
- persistentState;
4571
- gameDefinition;
4572
- variables = {};
4573
- constructor(config) {
4574
- this.gameDefinition = config.gameDefinition;
4575
- // Set up RNG
4576
- const rng = config.seed !== undefined
4577
- ? createSeededRng(config.seed)
4578
- : undefined;
4579
- // Initialize sub-managers
4580
- this.api = new LuaEngineAPI(config.gameDefinition, rng, config.logger);
4581
- this.actionRouter = new ActionRouter(config.gameDefinition);
4582
- this.sessionManager = new SessionManager();
4583
- this.persistentState = new PersistentState(config.gameDefinition.persistent_state);
4584
- // Create Lua state and load standard libraries
4585
- this.L = lauxlib.luaL_newstate();
4586
- lualib.luaL_openlibs(this.L);
4587
- // Register engine.* API
4588
- this.api.register(this.L);
4589
- // Load and compile the script
4590
- this.loadScript(config.script);
4591
- }
4592
- /** Current session data (if any) */
4593
- get session() {
4594
- return this.sessionManager.current;
4595
- }
4596
- /** Current persistent state values */
4597
- get persistentVars() {
4598
- return { ...this.variables };
4599
- }
4600
- /**
4601
- * Execute a play action — the main entry point.
4602
- * This is what DevBridge calls on each PLAY_REQUEST.
4603
- */
4604
- execute(params) {
4605
- const { action: actionName, bet, params: clientParams } = params;
4606
- // 1. Resolve the action definition
4607
- const action = this.actionRouter.resolveAction(actionName, this.sessionManager.isActive);
4608
- // 2. Build state.variables
4609
- const stateVars = { ...this.variables, bet };
4610
- // Load cross-spin persistent state
4611
- this.persistentState.loadIntoVariables(stateVars);
4612
- // Load session persistent vars
4613
- if (this.sessionManager.isActive) {
4614
- const sessionParams = this.sessionManager.getPersistentParams();
4615
- for (const [k, v] of Object.entries(sessionParams)) {
4616
- if (typeof v === 'number') {
4617
- stateVars[k] = v;
4618
- }
4619
- }
4620
- }
4621
- // 3. Build state.params
4622
- const stateParams = { ...clientParams };
4623
- stateParams._action = actionName;
4624
- // Inject session _ps_* persistent data
4625
- if (this.sessionManager.isActive) {
4626
- const sessionParams = this.sessionManager.getPersistentParams();
4627
- for (const [k, v] of Object.entries(sessionParams)) {
4628
- if (typeof v !== 'number') {
4629
- stateParams[k] = v;
4630
- }
4631
- }
4632
- }
4633
- // Inject cross-spin _ps_* game data
4634
- const gameDataParams = this.persistentState.getGameDataParams();
4635
- Object.assign(stateParams, gameDataParams);
4636
- // Handle buy bonus
4637
- if (action.buy_bonus_mode && this.gameDefinition.buy_bonus) {
4638
- const mode = this.gameDefinition.buy_bonus.modes[action.buy_bonus_mode];
4639
- if (mode) {
4640
- stateParams.buy_bonus = true;
4641
- stateParams.buy_bonus_mode = action.buy_bonus_mode;
4642
- stateParams.forced_scatter_count = this.pickFromDistribution(mode.scatter_distribution);
4643
- }
4644
- }
4645
- // Handle ante bet
4646
- if (clientParams?.ante_bet && this.gameDefinition.ante_bet) {
4647
- stateParams.ante_bet = true;
4648
- }
4649
- // 4. Build the state table and call Lua execute()
4650
- const luaResult = this.callLuaExecute(action.stage, stateParams, stateVars);
4651
- // 5. Extract special fields from Lua result
4652
- const totalWinMultiplier = typeof luaResult.total_win === 'number' ? luaResult.total_win : 0;
4653
- const resultVariables = (luaResult.variables ?? {});
4654
- const totalWin = Math.round(totalWinMultiplier * bet * 100) / 100;
4655
- // Merge result variables into engine variables
4656
- Object.assign(stateVars, resultVariables);
4657
- this.variables = { ...stateVars };
4658
- delete this.variables.bet; // bet is per-spin, not persistent
4659
- // Build client data (everything except special keys)
4660
- const data = {};
4661
- for (const [key, value] of Object.entries(luaResult)) {
4662
- if (key !== 'total_win' && key !== 'variables') {
4663
- data[key] = value;
4664
- }
4665
- }
4666
- // 6. Apply max win cap
4667
- let cappedWin = totalWin;
4668
- if (this.gameDefinition.max_win) {
4669
- const cap = this.calculateMaxWinCap(bet);
4670
- if (cap !== undefined && totalWin > cap) {
4671
- cappedWin = cap;
4672
- this.variables.max_win_reached = 1;
4673
- data.max_win_reached = true;
4674
- this.sessionManager.markMaxWinReached();
4675
- }
4676
- }
4677
- // 7. Handle _persist_* and _persist_game_* keys
4678
- this.sessionManager.storePersistData(data);
4679
- this.persistentState.storeGameData(data);
4680
- // Save cross-spin persistent state
4681
- this.persistentState.saveFromVariables(this.variables);
4682
- // Add exposed persistent vars to client data
4683
- const exposedVars = this.persistentState.getExposedVars();
4684
- if (exposedVars) {
4685
- data.persistent_state = exposedVars;
4686
- }
4687
- // Remove _persist_* keys from client data
4688
- for (const key of Object.keys(data)) {
4689
- if (key.startsWith('_persist_')) {
4690
- delete data[key];
4691
- }
4692
- }
4693
- // 8. Evaluate transitions
4694
- const { rule, nextActions } = this.actionRouter.evaluateTransitions(action, this.variables);
4695
- let creditDeferred = action.credit === 'defer' || rule.credit_override === 'defer';
4696
- let session = this.sessionManager.current;
4697
- // Handle session creation
4698
- if (rule.creates_session && !this.sessionManager.isActive) {
4699
- session = this.sessionManager.createSession(rule, this.variables, bet);
4700
- creditDeferred = true;
4701
- }
4702
- // Handle session update
4703
- else if (this.sessionManager.isActive) {
4704
- session = this.sessionManager.updateSession(rule, this.variables, cappedWin);
4705
- // Handle session completion
4706
- if (rule.complete_session || session?.completed) {
4707
- const completed = this.sessionManager.completeSession();
4708
- session = completed.session;
4709
- creditDeferred = false;
4710
- }
4711
- }
4712
- return {
4713
- totalWin: cappedWin,
4714
- data,
4715
- nextActions,
4716
- session,
4717
- variables: { ...this.variables },
4718
- creditDeferred,
4719
- };
4720
- }
4721
- /** Reset all state (sessions, persistent vars, variables) */
4722
- reset() {
4723
- this.variables = {};
4724
- this.sessionManager.reset();
4725
- this.persistentState.reset();
4726
- }
4727
- /** Destroy the Lua VM */
4728
- destroy() {
4729
- if (this.L) {
4730
- lua.lua_close(this.L);
4731
- this.L = null;
4732
- }
4733
- }
4734
- // ─── Private ──────────────────────────────────────────
4735
- loadScript(source) {
4736
- const status = lauxlib.luaL_dostring(this.L, to_luastring(source));
4737
- if (status !== lua.LUA_OK) {
4738
- const err = to_jsstring(lua.lua_tostring(this.L, -1));
4739
- lua.lua_pop(this.L, 1);
4740
- throw new Error(`Failed to load Lua script: ${err}`);
4741
- }
4742
- // Verify that execute() function exists
4743
- lua.lua_getglobal(this.L, to_luastring('execute'));
4744
- if (lua.lua_type(this.L, -1) !== lua.LUA_TFUNCTION) {
4745
- lua.lua_pop(this.L, 1);
4746
- throw new Error('Lua script must define a global `execute(state)` function');
4747
- }
4748
- lua.lua_pop(this.L, 1);
4749
- }
4750
- callLuaExecute(stage, params, variables) {
4751
- // Push the execute function
4752
- lua.lua_getglobal(this.L, to_luastring('execute'));
4753
- // Build and push the state table
4754
- lua.lua_createtable(this.L, 0, 3);
4755
- // state.stage
4756
- lua.lua_pushstring(this.L, to_luastring(stage));
4757
- lua.lua_setfield(this.L, -2, to_luastring('stage'));
4758
- // state.params
4759
- pushJSValue(this.L, params);
4760
- lua.lua_setfield(this.L, -2, to_luastring('params'));
4761
- // state.variables
4762
- pushJSValue(this.L, variables);
4763
- lua.lua_setfield(this.L, -2, to_luastring('variables'));
4764
- // Call execute(state) → 1 result
4765
- const status = lua.lua_pcall(this.L, 1, 1, 0);
4766
- if (status !== lua.LUA_OK) {
4767
- const err = to_jsstring(lua.lua_tostring(this.L, -1));
4768
- lua.lua_pop(this.L, 1);
4769
- throw new Error(`Lua execute() failed: ${err}`);
4770
- }
4771
- // Marshal result table to JS
4772
- const result = luaToJS(this.L, -1);
4773
- lua.lua_pop(this.L, 1);
4774
- if (!result || typeof result !== 'object' || Array.isArray(result)) {
4775
- throw new Error('Lua execute() must return a table');
4776
- }
4777
- return result;
4778
- }
4779
- calculateMaxWinCap(bet) {
4780
- const mw = this.gameDefinition.max_win;
4781
- if (!mw)
4782
- return undefined;
4783
- const caps = [];
4784
- if (mw.multiplier !== undefined)
4785
- caps.push(bet * mw.multiplier);
4786
- if (mw.fixed !== undefined)
4787
- caps.push(mw.fixed);
4788
- return caps.length > 0 ? Math.min(...caps) : undefined;
4789
- }
4790
- pickFromDistribution(distribution) {
4791
- const entries = Object.entries(distribution);
4792
- const totalWeight = entries.reduce((sum, [, w]) => sum + w, 0);
4793
- let roll = this.api.randomFloat() * totalWeight;
4794
- for (const [value, weight] of entries) {
4795
- roll -= weight;
4796
- if (roll < 0)
4797
- return parseInt(value, 10);
4798
- }
4799
- return parseInt(entries[entries.length - 1][0], 10);
4800
- }
4801
- }
4802
-
4803
3968
  const DEFAULT_CONFIG = {
4804
3969
  balance: 10000,
4805
3970
  currency: 'USD',
@@ -4823,11 +3988,11 @@ const DEFAULT_CONFIG = {
4823
3988
  * `CasinoGameSDK` via a shared in-memory `MemoryChannel`, removing
4824
3989
  * the need for postMessage and iframes.
4825
3990
  *
4826
- * This allows games to be developed and tested without a real backend.
3991
+ * When `luaScript` is set, play requests are sent to the Vite dev server
3992
+ * which runs LuaEngine in Node.js — no fengari in the browser.
4827
3993
  *
4828
3994
  * @example
4829
3995
  * ```ts
4830
- * // In your dev entry point or vite plugin
4831
3996
  * import { DevBridge } from '@energy8platform/game-engine/debug';
4832
3997
  *
4833
3998
  * const devBridge = new DevBridge({
@@ -4836,10 +4001,7 @@ const DEFAULT_CONFIG = {
4836
4001
  * gameConfig: { id: 'my-slot', type: 'slot', betLevels: [0.2, 0.5, 1, 2] },
4837
4002
  * onPlay: ({ action, bet }) => ({
4838
4003
  * totalWin: Math.random() > 0.5 ? bet * (Math.random() * 20) : 0,
4839
- * data: {
4840
- * matrix: generateRandomMatrix(5, 3, 10),
4841
- * win_lines: [],
4842
- * },
4004
+ * data: { matrix: [[1,2,3],[4,5,6],[7,8,9]] },
4843
4005
  * }),
4844
4006
  * });
4845
4007
  * devBridge.start();
@@ -4850,11 +4012,11 @@ class DevBridge {
4850
4012
  _balance;
4851
4013
  _roundCounter = 0;
4852
4014
  _bridge = null;
4853
- _luaEngine = null;
4015
+ _useLuaServer;
4854
4016
  constructor(config = {}) {
4855
4017
  this._config = { ...DEFAULT_CONFIG, ...config };
4856
4018
  this._balance = this._config.balance;
4857
- this.initLuaEngine();
4019
+ this._useLuaServer = !!(this._config.luaScript && this._config.gameDefinition);
4858
4020
  }
4859
4021
  /** Current mock balance */
4860
4022
  get balance() {
@@ -4885,7 +4047,8 @@ class DevBridge {
4885
4047
  this.handleOpenDeposit();
4886
4048
  });
4887
4049
  if (this._config.debug) {
4888
- console.log('[DevBridge] Started listening via Bridge (devMode)');
4050
+ const mode = this._useLuaServer ? 'Lua (server-side)' : 'onPlay callback';
4051
+ console.log(`[DevBridge] Started — mode: ${mode}`);
4889
4052
  }
4890
4053
  }
4891
4054
  /** Stop listening */
@@ -4906,22 +4069,6 @@ class DevBridge {
4906
4069
  /** Destroy the dev bridge */
4907
4070
  destroy() {
4908
4071
  this.stop();
4909
- if (this._luaEngine) {
4910
- this._luaEngine.destroy();
4911
- this._luaEngine = null;
4912
- }
4913
- }
4914
- initLuaEngine() {
4915
- if (!this._config.luaScript || !this._config.gameDefinition)
4916
- return;
4917
- this._luaEngine = new LuaEngine({
4918
- script: this._config.luaScript,
4919
- gameDefinition: this._config.gameDefinition,
4920
- seed: this._config.luaSeed,
4921
- });
4922
- if (this._config.debug) {
4923
- console.log('[DevBridge] LuaEngine initialized');
4924
- }
4925
4072
  }
4926
4073
  // ─── Message Handling ──────────────────────────────────
4927
4074
  handleGameReady(id) {
@@ -4939,32 +4086,25 @@ class DevBridge {
4939
4086
  // Deduct bet
4940
4087
  this._balance -= bet;
4941
4088
  this._roundCounter++;
4942
- let result;
4943
- if (this._luaEngine) {
4944
- // Use LuaEngine for real Lua execution
4945
- const luaResult = this._luaEngine.execute({ action, bet, roundId, params });
4946
- const totalWin = luaResult.creditDeferred ? 0 : luaResult.totalWin;
4947
- this._balance += totalWin;
4948
- result = {
4949
- roundId: roundId ?? `dev-round-${this._roundCounter}`,
4950
- action,
4951
- balanceAfter: this._balance,
4952
- totalWin: Math.round(luaResult.totalWin * 100) / 100,
4953
- data: luaResult.data,
4954
- nextActions: luaResult.nextActions,
4955
- session: luaResult.session,
4956
- creditPending: luaResult.creditDeferred,
4957
- bonusFreeSpin: null,
4958
- currency: this._config.currency,
4959
- gameId: this._config.gameConfig?.id ?? 'dev-game',
4960
- };
4089
+ if (this._useLuaServer) {
4090
+ // Send to Vite dev server for Lua execution
4091
+ this.executeLuaOnServer({ action, bet, roundId, params })
4092
+ .then((result) => {
4093
+ this._bridge?.send('PLAY_RESULT', result, id);
4094
+ })
4095
+ .catch((err) => {
4096
+ console.error('[DevBridge] Lua server error:', err);
4097
+ // Refund bet on error
4098
+ this._balance += bet;
4099
+ this._bridge?.send('PLAY_RESULT', this.buildFallbackResult(action, bet, roundId), id);
4100
+ });
4961
4101
  }
4962
4102
  else {
4963
4103
  // Fallback to onPlay callback
4964
4104
  const customResult = this._config.onPlay({ action, bet, roundId, params });
4965
4105
  const totalWin = customResult.totalWin ?? (Math.random() > 0.6 ? bet * (1 + Math.random() * 10) : 0);
4966
4106
  this._balance += totalWin;
4967
- result = {
4107
+ const result = {
4968
4108
  roundId: roundId ?? `dev-round-${this._roundCounter}`,
4969
4109
  action,
4970
4110
  balanceAfter: this._balance,
@@ -4977,8 +4117,50 @@ class DevBridge {
4977
4117
  currency: this._config.currency,
4978
4118
  gameId: this._config.gameConfig?.id ?? 'dev-game',
4979
4119
  };
4120
+ this.delayedSend('PLAY_RESULT', result, id);
4980
4121
  }
4981
- this.delayedSend('PLAY_RESULT', result, id);
4122
+ }
4123
+ async executeLuaOnServer(params) {
4124
+ const response = await fetch('/__lua-play', {
4125
+ method: 'POST',
4126
+ headers: { 'Content-Type': 'application/json' },
4127
+ body: JSON.stringify(params),
4128
+ });
4129
+ if (!response.ok) {
4130
+ const err = await response.json().catch(() => ({ error: 'Unknown error' }));
4131
+ throw new Error(err.error ?? `HTTP ${response.status}`);
4132
+ }
4133
+ const luaResult = await response.json();
4134
+ const totalWin = luaResult.creditDeferred ? 0 : luaResult.totalWin;
4135
+ this._balance += totalWin;
4136
+ return {
4137
+ roundId: params.roundId ?? `dev-round-${this._roundCounter}`,
4138
+ action: params.action,
4139
+ balanceAfter: this._balance,
4140
+ totalWin: Math.round(luaResult.totalWin * 100) / 100,
4141
+ data: luaResult.data,
4142
+ nextActions: luaResult.nextActions,
4143
+ session: luaResult.session,
4144
+ creditPending: luaResult.creditDeferred,
4145
+ bonusFreeSpin: null,
4146
+ currency: this._config.currency,
4147
+ gameId: this._config.gameConfig?.id ?? 'dev-game',
4148
+ };
4149
+ }
4150
+ buildFallbackResult(action, bet, roundId) {
4151
+ return {
4152
+ roundId: roundId ?? `dev-round-${this._roundCounter}`,
4153
+ action,
4154
+ balanceAfter: this._balance,
4155
+ totalWin: 0,
4156
+ data: { error: 'Lua execution failed' },
4157
+ nextActions: ['spin'],
4158
+ session: null,
4159
+ creditPending: false,
4160
+ bonusFreeSpin: null,
4161
+ currency: this._config.currency,
4162
+ gameId: this._config.gameConfig?.id ?? 'dev-game',
4163
+ };
4982
4164
  }
4983
4165
  handlePlayAck(_payload) {
4984
4166
  if (this._config.debug) {
@@ -4993,7 +4175,7 @@ class DevBridge {
4993
4175
  }
4994
4176
  handleOpenDeposit() {
4995
4177
  if (this._config.debug) {
4996
- console.log('[DevBridge] 💰 Open deposit requested (mock: adding 1000)');
4178
+ console.log('[DevBridge] Open deposit requested (mock: adding 1000)');
4997
4179
  }
4998
4180
  this._balance += 1000;
4999
4181
  this._bridge?.send('BALANCE_UPDATE', { balance: this._balance });
@@ -5026,7 +4208,6 @@ Object.defineProperty(exports, "TimeoutError", {
5026
4208
  enumerable: true,
5027
4209
  get: function () { return gameSdk.TimeoutError; }
5028
4210
  });
5029
- exports.ActionRouter = ActionRouter;
5030
4211
  exports.AssetManager = AssetManager;
5031
4212
  exports.AudioManager = AudioManager;
5032
4213
  exports.BalanceDisplay = BalanceDisplay;
@@ -5040,16 +4221,12 @@ exports.InputManager = InputManager;
5040
4221
  exports.Label = Label;
5041
4222
  exports.Layout = Layout;
5042
4223
  exports.LoadingScene = LoadingScene;
5043
- exports.LuaEngine = LuaEngine;
5044
- exports.LuaEngineAPI = LuaEngineAPI;
5045
4224
  exports.Modal = Modal;
5046
4225
  exports.Panel = Panel;
5047
- exports.PersistentState = PersistentState;
5048
4226
  exports.ProgressBar = ProgressBar;
5049
4227
  exports.Scene = Scene;
5050
4228
  exports.SceneManager = SceneManager;
5051
4229
  exports.ScrollContainer = ScrollContainer;
5052
- exports.SessionManager = SessionManager;
5053
4230
  exports.SpineHelper = SpineHelper;
5054
4231
  exports.SpriteAnimation = SpriteAnimation;
5055
4232
  exports.StateMachine = StateMachine;
@@ -5058,6 +4235,4 @@ exports.Toast = Toast;
5058
4235
  exports.Tween = Tween;
5059
4236
  exports.ViewportManager = ViewportManager;
5060
4237
  exports.WinDisplay = WinDisplay;
5061
- exports.createSeededRng = createSeededRng;
5062
- exports.evaluateCondition = evaluateCondition;
5063
4238
  //# sourceMappingURL=index.cjs.js.map