@energy8platform/game-engine 0.9.2 → 0.10.1

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