@energy8platform/game-engine 0.10.2 → 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/bin/simulate.ts +0 -0
- package/dist/debug.cjs.js +64 -882
- package/dist/debug.cjs.js.map +1 -1
- package/dist/debug.d.ts +7 -9
- package/dist/debug.esm.js +64 -882
- package/dist/debug.esm.js.map +1 -1
- package/dist/index.cjs.js +64 -889
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +8 -165
- package/dist/index.esm.js +65 -883
- package/dist/index.esm.js.map +1 -1
- package/dist/lua.cjs.js +4 -4
- package/dist/lua.cjs.js.map +1 -1
- package/dist/lua.esm.js +4 -4
- package/dist/lua.esm.js.map +1 -1
- package/dist/vite.cjs.js +79 -35
- package/dist/vite.cjs.js.map +1 -1
- package/dist/vite.esm.js +79 -35
- package/dist/vite.esm.js.map +1 -1
- package/package.json +1 -1
- package/src/debug/DevBridge.ts +71 -54
- package/src/index.ts +3 -8
- package/src/lua/LuaEngine.ts +2 -3
- package/src/lua/LuaEngineAPI.ts +1 -4
- package/src/lua/fengari.d.ts +10 -0
- package/src/vite/index.ts +85 -37
package/dist/debug.cjs.js
CHANGED
|
@@ -3,841 +3,6 @@
|
|
|
3
3
|
var gameSdk = require('@energy8platform/game-sdk');
|
|
4
4
|
var pixi_js = require('pixi.js');
|
|
5
5
|
|
|
6
|
-
const fengari$1 = require('fengari');
|
|
7
|
-
const { lua: lua$1, lauxlib: lauxlib$1 } = fengari$1;
|
|
8
|
-
const { to_luastring: to_luastring$1, to_jsstring: to_jsstring$1 } = fengari$1;
|
|
9
|
-
/**
|
|
10
|
-
* Seeded xoshiro128** PRNG for deterministic simulation/replay.
|
|
11
|
-
* Period: 2^128 - 1
|
|
12
|
-
*/
|
|
13
|
-
function createSeededRng(seed) {
|
|
14
|
-
let s0 = (seed >>> 0) | 1;
|
|
15
|
-
let s1 = (seed * 1103515245 + 12345) >>> 0;
|
|
16
|
-
let s2 = (seed * 6364136223846793005 + 1442695040888963407) >>> 0;
|
|
17
|
-
let s3 = (seed * 1442695040888963407 + 6364136223846793005) >>> 0;
|
|
18
|
-
return () => {
|
|
19
|
-
const result = (((s1 * 5) << 7) * 9) >>> 0;
|
|
20
|
-
const t = s1 << 9;
|
|
21
|
-
s2 ^= s0;
|
|
22
|
-
s3 ^= s1;
|
|
23
|
-
s1 ^= s2;
|
|
24
|
-
s0 ^= s3;
|
|
25
|
-
s2 ^= t;
|
|
26
|
-
s3 = ((s3 << 11) | (s3 >>> 21)) >>> 0;
|
|
27
|
-
return result / 4294967296;
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
/**
|
|
31
|
-
* Implements and registers all platform `engine.*` functions into a Lua state.
|
|
32
|
-
*/
|
|
33
|
-
class LuaEngineAPI {
|
|
34
|
-
rng;
|
|
35
|
-
logger;
|
|
36
|
-
gameDefinition;
|
|
37
|
-
constructor(gameDefinition, rng, logger) {
|
|
38
|
-
this.gameDefinition = gameDefinition;
|
|
39
|
-
this.rng = rng ?? Math.random;
|
|
40
|
-
this.logger = logger ?? ((level, msg) => {
|
|
41
|
-
const fn = level === 'error' ? console.error
|
|
42
|
-
: level === 'warn' ? console.warn
|
|
43
|
-
: level === 'debug' ? console.debug
|
|
44
|
-
: console.log;
|
|
45
|
-
fn(`[Lua:${level}] ${msg}`);
|
|
46
|
-
});
|
|
47
|
-
}
|
|
48
|
-
/** Register `engine` global table on the Lua state */
|
|
49
|
-
register(L) {
|
|
50
|
-
// Create the `engine` table
|
|
51
|
-
lua$1.lua_newtable(L);
|
|
52
|
-
this.registerFunction(L, 'random', (LS) => {
|
|
53
|
-
const min = lauxlib$1.luaL_checkinteger(LS, 1);
|
|
54
|
-
const max = lauxlib$1.luaL_checkinteger(LS, 2);
|
|
55
|
-
const result = this.random(Number(min), Number(max));
|
|
56
|
-
lua$1.lua_pushinteger(LS, result);
|
|
57
|
-
return 1;
|
|
58
|
-
});
|
|
59
|
-
this.registerFunction(L, 'random_float', (LS) => {
|
|
60
|
-
lua$1.lua_pushnumber(LS, this.randomFloat());
|
|
61
|
-
return 1;
|
|
62
|
-
});
|
|
63
|
-
this.registerFunction(L, 'random_weighted', (LS) => {
|
|
64
|
-
lauxlib$1.luaL_checktype(LS, 1, lua$1.LUA_TTABLE);
|
|
65
|
-
const weights = [];
|
|
66
|
-
const len = lua$1.lua_rawlen(LS, 1);
|
|
67
|
-
for (let i = 1; i <= len; i++) {
|
|
68
|
-
lua$1.lua_rawgeti(LS, 1, i);
|
|
69
|
-
weights.push(lua$1.lua_tonumber(LS, -1));
|
|
70
|
-
lua$1.lua_pop(LS, 1);
|
|
71
|
-
}
|
|
72
|
-
const result = this.randomWeighted(weights);
|
|
73
|
-
lua$1.lua_pushinteger(LS, result);
|
|
74
|
-
return 1;
|
|
75
|
-
});
|
|
76
|
-
this.registerFunction(L, 'shuffle', (LS) => {
|
|
77
|
-
lauxlib$1.luaL_checktype(LS, 1, lua$1.LUA_TTABLE);
|
|
78
|
-
const arr = [];
|
|
79
|
-
const len = lua$1.lua_rawlen(LS, 1);
|
|
80
|
-
for (let i = 1; i <= len; i++) {
|
|
81
|
-
lua$1.lua_rawgeti(LS, 1, i);
|
|
82
|
-
arr.push(luaToJS(LS, -1));
|
|
83
|
-
lua$1.lua_pop(LS, 1);
|
|
84
|
-
}
|
|
85
|
-
const shuffled = this.shuffle(arr);
|
|
86
|
-
pushJSArray(LS, shuffled);
|
|
87
|
-
return 1;
|
|
88
|
-
});
|
|
89
|
-
this.registerFunction(L, 'log', (LS) => {
|
|
90
|
-
const level = to_jsstring$1(lauxlib$1.luaL_checkstring(LS, 1));
|
|
91
|
-
const msg = to_jsstring$1(lauxlib$1.luaL_checkstring(LS, 2));
|
|
92
|
-
this.logger(level, msg);
|
|
93
|
-
return 0;
|
|
94
|
-
});
|
|
95
|
-
this.registerFunction(L, 'get_config', (LS) => {
|
|
96
|
-
const config = this.getConfig();
|
|
97
|
-
pushJSObject(LS, config);
|
|
98
|
-
return 1;
|
|
99
|
-
});
|
|
100
|
-
// Set the table as global `engine`
|
|
101
|
-
lua$1.lua_setglobal(L, to_luastring$1('engine'));
|
|
102
|
-
}
|
|
103
|
-
// ─── engine.* implementations ─────────────────────────
|
|
104
|
-
random(min, max) {
|
|
105
|
-
return Math.floor(this.rng() * (max - min + 1)) + min;
|
|
106
|
-
}
|
|
107
|
-
randomFloat() {
|
|
108
|
-
return this.rng();
|
|
109
|
-
}
|
|
110
|
-
randomWeighted(weights) {
|
|
111
|
-
const totalWeight = weights.reduce((a, b) => a + b, 0);
|
|
112
|
-
let roll = this.rng() * totalWeight;
|
|
113
|
-
for (let i = 0; i < weights.length; i++) {
|
|
114
|
-
roll -= weights[i];
|
|
115
|
-
if (roll < 0)
|
|
116
|
-
return i + 1; // 1-based index
|
|
117
|
-
}
|
|
118
|
-
return weights.length; // fallback to last
|
|
119
|
-
}
|
|
120
|
-
shuffle(arr) {
|
|
121
|
-
const copy = [...arr];
|
|
122
|
-
for (let i = copy.length - 1; i > 0; i--) {
|
|
123
|
-
const j = Math.floor(this.rng() * (i + 1));
|
|
124
|
-
[copy[i], copy[j]] = [copy[j], copy[i]];
|
|
125
|
-
}
|
|
126
|
-
return copy;
|
|
127
|
-
}
|
|
128
|
-
getConfig() {
|
|
129
|
-
const def = this.gameDefinition;
|
|
130
|
-
let betLevels = [];
|
|
131
|
-
if (Array.isArray(def.bet_levels)) {
|
|
132
|
-
betLevels = def.bet_levels;
|
|
133
|
-
}
|
|
134
|
-
else if (def.bet_levels && 'levels' in def.bet_levels && def.bet_levels.levels) {
|
|
135
|
-
betLevels = def.bet_levels.levels;
|
|
136
|
-
}
|
|
137
|
-
return {
|
|
138
|
-
id: def.id,
|
|
139
|
-
type: def.type,
|
|
140
|
-
bet_levels: betLevels,
|
|
141
|
-
};
|
|
142
|
-
}
|
|
143
|
-
// ─── Helpers ──────────────────────────────────────────
|
|
144
|
-
registerFunction(L, name, fn) {
|
|
145
|
-
lua$1.lua_pushcfunction(L, fn);
|
|
146
|
-
lua$1.lua_setfield(L, -2, to_luastring$1(name));
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
// ─── Lua ↔ JS marshalling ───────────────────────────────
|
|
150
|
-
/** Read a Lua value at the given stack index and return its JS equivalent */
|
|
151
|
-
function luaToJS(L, idx) {
|
|
152
|
-
const type = lua$1.lua_type(L, idx);
|
|
153
|
-
switch (type) {
|
|
154
|
-
case lua$1.LUA_TNIL:
|
|
155
|
-
return null;
|
|
156
|
-
case lua$1.LUA_TBOOLEAN:
|
|
157
|
-
return lua$1.lua_toboolean(L, idx);
|
|
158
|
-
case lua$1.LUA_TNUMBER:
|
|
159
|
-
if (lua$1.lua_isinteger(L, idx)) {
|
|
160
|
-
return Number(lua$1.lua_tointeger(L, idx));
|
|
161
|
-
}
|
|
162
|
-
return lua$1.lua_tonumber(L, idx);
|
|
163
|
-
case lua$1.LUA_TSTRING:
|
|
164
|
-
return to_jsstring$1(lua$1.lua_tostring(L, idx));
|
|
165
|
-
case lua$1.LUA_TTABLE:
|
|
166
|
-
return luaTableToJS(L, idx);
|
|
167
|
-
default:
|
|
168
|
-
return null;
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
/** Convert a Lua table to a JS object or array */
|
|
172
|
-
function luaTableToJS(L, idx) {
|
|
173
|
-
// Normalize index to absolute
|
|
174
|
-
if (idx < 0)
|
|
175
|
-
idx = lua$1.lua_gettop(L) + idx + 1;
|
|
176
|
-
// Check if it's an array (sequential integer keys starting at 1)
|
|
177
|
-
const len = lua$1.lua_rawlen(L, idx);
|
|
178
|
-
if (len > 0) {
|
|
179
|
-
// Verify it's a pure array by checking key 1 exists
|
|
180
|
-
lua$1.lua_rawgeti(L, idx, 1);
|
|
181
|
-
const hasFirst = lua$1.lua_type(L, -1) !== lua$1.LUA_TNIL;
|
|
182
|
-
lua$1.lua_pop(L, 1);
|
|
183
|
-
if (hasFirst) {
|
|
184
|
-
// Check if there are also string keys (mixed table)
|
|
185
|
-
let hasStringKeys = false;
|
|
186
|
-
lua$1.lua_pushnil(L);
|
|
187
|
-
while (lua$1.lua_next(L, idx) !== 0) {
|
|
188
|
-
lua$1.lua_pop(L, 1); // pop value
|
|
189
|
-
if (lua$1.lua_type(L, -1) === lua$1.LUA_TSTRING) {
|
|
190
|
-
hasStringKeys = true;
|
|
191
|
-
lua$1.lua_pop(L, 1); // pop key
|
|
192
|
-
break;
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
if (!hasStringKeys) {
|
|
196
|
-
// Pure array
|
|
197
|
-
const arr = [];
|
|
198
|
-
for (let i = 1; i <= len; i++) {
|
|
199
|
-
lua$1.lua_rawgeti(L, idx, i);
|
|
200
|
-
arr.push(luaToJS(L, -1));
|
|
201
|
-
lua$1.lua_pop(L, 1);
|
|
202
|
-
}
|
|
203
|
-
return arr;
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
// Object (or mixed table)
|
|
208
|
-
const obj = {};
|
|
209
|
-
lua$1.lua_pushnil(L);
|
|
210
|
-
while (lua$1.lua_next(L, idx) !== 0) {
|
|
211
|
-
const keyType = lua$1.lua_type(L, -2);
|
|
212
|
-
let key;
|
|
213
|
-
if (keyType === lua$1.LUA_TSTRING) {
|
|
214
|
-
key = to_jsstring$1(lua$1.lua_tostring(L, -2));
|
|
215
|
-
}
|
|
216
|
-
else if (keyType === lua$1.LUA_TNUMBER) {
|
|
217
|
-
key = String(lua$1.lua_tonumber(L, -2));
|
|
218
|
-
}
|
|
219
|
-
else {
|
|
220
|
-
lua$1.lua_pop(L, 1);
|
|
221
|
-
continue;
|
|
222
|
-
}
|
|
223
|
-
obj[key] = luaToJS(L, -1);
|
|
224
|
-
lua$1.lua_pop(L, 1);
|
|
225
|
-
}
|
|
226
|
-
return obj;
|
|
227
|
-
}
|
|
228
|
-
/** Push a JS value onto the Lua stack */
|
|
229
|
-
function pushJSValue(L, value) {
|
|
230
|
-
if (value === null || value === undefined) {
|
|
231
|
-
lua$1.lua_pushnil(L);
|
|
232
|
-
}
|
|
233
|
-
else if (typeof value === 'boolean') {
|
|
234
|
-
lua$1.lua_pushboolean(L, value ? 1 : 0);
|
|
235
|
-
}
|
|
236
|
-
else if (typeof value === 'number') {
|
|
237
|
-
if (Number.isInteger(value)) {
|
|
238
|
-
lua$1.lua_pushinteger(L, value);
|
|
239
|
-
}
|
|
240
|
-
else {
|
|
241
|
-
lua$1.lua_pushnumber(L, value);
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
else if (typeof value === 'string') {
|
|
245
|
-
lua$1.lua_pushstring(L, to_luastring$1(value));
|
|
246
|
-
}
|
|
247
|
-
else if (Array.isArray(value)) {
|
|
248
|
-
pushJSArray(L, value);
|
|
249
|
-
}
|
|
250
|
-
else if (typeof value === 'object') {
|
|
251
|
-
pushJSObject(L, value);
|
|
252
|
-
}
|
|
253
|
-
else {
|
|
254
|
-
lua$1.lua_pushnil(L);
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
/** Push a JS array as a Lua table (1-based) */
|
|
258
|
-
function pushJSArray(L, arr) {
|
|
259
|
-
lua$1.lua_createtable(L, arr.length, 0);
|
|
260
|
-
for (let i = 0; i < arr.length; i++) {
|
|
261
|
-
pushJSValue(L, arr[i]);
|
|
262
|
-
lua$1.lua_rawseti(L, -2, i + 1);
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
/** Push a JS object as a Lua table */
|
|
266
|
-
function pushJSObject(L, obj) {
|
|
267
|
-
const keys = Object.keys(obj);
|
|
268
|
-
lua$1.lua_createtable(L, 0, keys.length);
|
|
269
|
-
for (const key of keys) {
|
|
270
|
-
pushJSValue(L, obj[key]);
|
|
271
|
-
lua$1.lua_setfield(L, -2, to_luastring$1(key));
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
/**
|
|
276
|
-
* Replicates the platform's action dispatch and transition evaluation.
|
|
277
|
-
* Routes play requests to the correct action, evaluates transition conditions
|
|
278
|
-
* against current variables to determine next actions and session operations.
|
|
279
|
-
*/
|
|
280
|
-
class ActionRouter {
|
|
281
|
-
actions;
|
|
282
|
-
constructor(gameDefinition) {
|
|
283
|
-
this.actions = gameDefinition.actions;
|
|
284
|
-
}
|
|
285
|
-
/** Look up action by name and validate prerequisites */
|
|
286
|
-
resolveAction(actionName, hasSession) {
|
|
287
|
-
const action = this.actions[actionName];
|
|
288
|
-
if (!action) {
|
|
289
|
-
throw new Error(`Unknown action: "${actionName}". Available: ${Object.keys(this.actions).join(', ')}`);
|
|
290
|
-
}
|
|
291
|
-
if (action.requires_session && !hasSession) {
|
|
292
|
-
throw new Error(`Action "${actionName}" requires an active session`);
|
|
293
|
-
}
|
|
294
|
-
return action;
|
|
295
|
-
}
|
|
296
|
-
/** Evaluate transitions in order, return the first matching rule */
|
|
297
|
-
evaluateTransitions(action, variables) {
|
|
298
|
-
for (const rule of action.transitions) {
|
|
299
|
-
if (evaluateCondition(rule.condition, variables)) {
|
|
300
|
-
return { rule, nextActions: rule.next_actions };
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
throw new Error(`No matching transition for action with stage "${action.stage}". ` +
|
|
304
|
-
`Variables: ${JSON.stringify(variables)}`);
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
// ─── Condition Evaluator ────────────────────────────────
|
|
308
|
-
/**
|
|
309
|
-
* Evaluates a transition condition expression against variables.
|
|
310
|
-
*
|
|
311
|
-
* Supports:
|
|
312
|
-
* - "always" → true
|
|
313
|
-
* - Simple comparisons: "var > 0", "var == 1", "var >= 10", "var != 0", "var < 5", "var <= 3"
|
|
314
|
-
* - Logical connectives: "expr && expr", "expr || expr"
|
|
315
|
-
*
|
|
316
|
-
* This covers all patterns used by the platform's govaluate conditions.
|
|
317
|
-
*/
|
|
318
|
-
function evaluateCondition(condition, variables) {
|
|
319
|
-
const trimmed = condition.trim();
|
|
320
|
-
if (trimmed === 'always')
|
|
321
|
-
return true;
|
|
322
|
-
// Handle || (OR) — lowest precedence
|
|
323
|
-
if (trimmed.includes('||')) {
|
|
324
|
-
const parts = splitOnOperator(trimmed, '||');
|
|
325
|
-
return parts.some(part => evaluateCondition(part, variables));
|
|
326
|
-
}
|
|
327
|
-
// Handle && (AND)
|
|
328
|
-
if (trimmed.includes('&&')) {
|
|
329
|
-
const parts = splitOnOperator(trimmed, '&&');
|
|
330
|
-
return parts.every(part => evaluateCondition(part, variables));
|
|
331
|
-
}
|
|
332
|
-
// Single comparison: "variable op value"
|
|
333
|
-
return evaluateComparison(trimmed, variables);
|
|
334
|
-
}
|
|
335
|
-
function splitOnOperator(expr, operator) {
|
|
336
|
-
const parts = [];
|
|
337
|
-
let depth = 0;
|
|
338
|
-
let current = '';
|
|
339
|
-
for (let i = 0; i < expr.length; i++) {
|
|
340
|
-
if (expr[i] === '(')
|
|
341
|
-
depth++;
|
|
342
|
-
else if (expr[i] === ')')
|
|
343
|
-
depth--;
|
|
344
|
-
if (depth === 0 && expr.substring(i, i + operator.length) === operator) {
|
|
345
|
-
parts.push(current);
|
|
346
|
-
current = '';
|
|
347
|
-
i += operator.length - 1;
|
|
348
|
-
}
|
|
349
|
-
else {
|
|
350
|
-
current += expr[i];
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
parts.push(current);
|
|
354
|
-
return parts;
|
|
355
|
-
}
|
|
356
|
-
function evaluateComparison(expr, variables) {
|
|
357
|
-
// Match: variable_name operator value
|
|
358
|
-
const match = expr.trim().match(/^\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*(>=|<=|!=|==|>|<)\s*(-?\d+(?:\.\d+)?)\s*$/);
|
|
359
|
-
if (!match) {
|
|
360
|
-
throw new Error(`Cannot parse condition: "${expr}"`);
|
|
361
|
-
}
|
|
362
|
-
const [, varName, op, valueStr] = match;
|
|
363
|
-
const left = variables[varName] ?? 0;
|
|
364
|
-
const right = parseFloat(valueStr);
|
|
365
|
-
switch (op) {
|
|
366
|
-
case '>': return left > right;
|
|
367
|
-
case '>=': return left >= right;
|
|
368
|
-
case '<': return left < right;
|
|
369
|
-
case '<=': return left <= right;
|
|
370
|
-
case '==': return left === right;
|
|
371
|
-
case '!=': return left !== right;
|
|
372
|
-
default: return false;
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
/**
|
|
377
|
-
* Manages session lifecycle: creation, spin counting, retriggers, and completion.
|
|
378
|
-
* Handles both slot sessions (fixed spin count) and table game sessions (unlimited).
|
|
379
|
-
* Also manages _persist_ data roundtrip between Lua calls.
|
|
380
|
-
*/
|
|
381
|
-
class SessionManager {
|
|
382
|
-
session = null;
|
|
383
|
-
get isActive() {
|
|
384
|
-
return this.session !== null && !this.session.completed;
|
|
385
|
-
}
|
|
386
|
-
get current() {
|
|
387
|
-
if (!this.session)
|
|
388
|
-
return null;
|
|
389
|
-
return this.toSessionData();
|
|
390
|
-
}
|
|
391
|
-
get sessionTotalWin() {
|
|
392
|
-
return this.session?.totalWin ?? 0;
|
|
393
|
-
}
|
|
394
|
-
/** Create a new session from a transition rule */
|
|
395
|
-
createSession(rule, variables, bet) {
|
|
396
|
-
let spinsRemaining = -1; // unlimited by default
|
|
397
|
-
if (rule.session_config?.total_spins_var) {
|
|
398
|
-
const varName = rule.session_config.total_spins_var;
|
|
399
|
-
spinsRemaining = variables[varName] ?? -1;
|
|
400
|
-
}
|
|
401
|
-
const persistentVars = {};
|
|
402
|
-
if (rule.session_config?.persistent_vars) {
|
|
403
|
-
for (const varName of rule.session_config.persistent_vars) {
|
|
404
|
-
persistentVars[varName] = variables[varName] ?? 0;
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
this.session = {
|
|
408
|
-
spinsRemaining,
|
|
409
|
-
spinsPlayed: 0,
|
|
410
|
-
totalWin: 0,
|
|
411
|
-
completed: false,
|
|
412
|
-
maxWinReached: false,
|
|
413
|
-
bet,
|
|
414
|
-
persistentVars,
|
|
415
|
-
persistentData: {},
|
|
416
|
-
};
|
|
417
|
-
return this.toSessionData();
|
|
418
|
-
}
|
|
419
|
-
/** Update session after a spin: decrement counter, accumulate win, handle retrigger */
|
|
420
|
-
updateSession(rule, variables, spinWin) {
|
|
421
|
-
if (!this.session)
|
|
422
|
-
throw new Error('No active session');
|
|
423
|
-
// Accumulate win
|
|
424
|
-
this.session.totalWin += spinWin;
|
|
425
|
-
this.session.spinsPlayed++;
|
|
426
|
-
// Decrement spins (only for non-unlimited sessions)
|
|
427
|
-
if (this.session.spinsRemaining > 0) {
|
|
428
|
-
this.session.spinsRemaining--;
|
|
429
|
-
}
|
|
430
|
-
// Handle retrigger (add_spins_var)
|
|
431
|
-
if (rule.add_spins_var) {
|
|
432
|
-
const extraSpins = variables[rule.add_spins_var] ?? 0;
|
|
433
|
-
if (extraSpins > 0 && this.session.spinsRemaining >= 0) {
|
|
434
|
-
this.session.spinsRemaining += extraSpins;
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
// Update persistent vars
|
|
438
|
-
if (this.session.persistentVars) {
|
|
439
|
-
for (const key of Object.keys(this.session.persistentVars)) {
|
|
440
|
-
if (key in variables) {
|
|
441
|
-
this.session.persistentVars[key] = variables[key];
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
// Auto-complete if spins exhausted
|
|
446
|
-
if (this.session.spinsRemaining === 0) {
|
|
447
|
-
this.session.completed = true;
|
|
448
|
-
}
|
|
449
|
-
return this.toSessionData();
|
|
450
|
-
}
|
|
451
|
-
/** Complete the session, return accumulated totalWin */
|
|
452
|
-
completeSession() {
|
|
453
|
-
if (!this.session)
|
|
454
|
-
throw new Error('No active session to complete');
|
|
455
|
-
this.session.completed = true;
|
|
456
|
-
const totalWin = this.session.totalWin;
|
|
457
|
-
const session = this.toSessionData();
|
|
458
|
-
return { totalWin, session };
|
|
459
|
-
}
|
|
460
|
-
/** Mark max win reached — stops the session */
|
|
461
|
-
markMaxWinReached() {
|
|
462
|
-
if (this.session) {
|
|
463
|
-
this.session.maxWinReached = true;
|
|
464
|
-
this.session.completed = true;
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
/** Store _persist_* data extracted from Lua result */
|
|
468
|
-
storePersistData(data) {
|
|
469
|
-
if (!this.session)
|
|
470
|
-
return;
|
|
471
|
-
for (const key of Object.keys(data)) {
|
|
472
|
-
if (key.startsWith('_persist_')) {
|
|
473
|
-
const cleanKey = key.slice('_persist_'.length);
|
|
474
|
-
this.session.persistentData[cleanKey] = data[key];
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
/** Get _ps_* params to inject into next execute() call */
|
|
479
|
-
getPersistentParams() {
|
|
480
|
-
if (!this.session)
|
|
481
|
-
return {};
|
|
482
|
-
const params = {};
|
|
483
|
-
// Session persistent vars (float64)
|
|
484
|
-
for (const [key, value] of Object.entries(this.session.persistentVars)) {
|
|
485
|
-
params[key] = value;
|
|
486
|
-
}
|
|
487
|
-
// _persist_ complex data → _ps_*
|
|
488
|
-
for (const [key, value] of Object.entries(this.session.persistentData)) {
|
|
489
|
-
params[`_ps_${key}`] = value;
|
|
490
|
-
}
|
|
491
|
-
return params;
|
|
492
|
-
}
|
|
493
|
-
/** Reset all session state */
|
|
494
|
-
reset() {
|
|
495
|
-
this.session = null;
|
|
496
|
-
}
|
|
497
|
-
toSessionData() {
|
|
498
|
-
if (!this.session)
|
|
499
|
-
throw new Error('No session');
|
|
500
|
-
return {
|
|
501
|
-
spinsRemaining: this.session.spinsRemaining,
|
|
502
|
-
spinsPlayed: this.session.spinsPlayed,
|
|
503
|
-
totalWin: Math.round(this.session.totalWin * 100) / 100,
|
|
504
|
-
completed: this.session.completed,
|
|
505
|
-
maxWinReached: this.session.maxWinReached,
|
|
506
|
-
betAmount: this.session.bet,
|
|
507
|
-
};
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
/**
|
|
512
|
-
* Manages cross-spin persistent state — variables that survive between base game spins.
|
|
513
|
-
* Separate from session-scoped persistence (handled by SessionManager).
|
|
514
|
-
*
|
|
515
|
-
* Handles two mechanisms:
|
|
516
|
-
* 1. Numeric vars declared in `persistent_state.vars` — stored in state.variables
|
|
517
|
-
* 2. Complex data with `_persist_game_*` prefix — stored separately, injected as `_ps_*`
|
|
518
|
-
*/
|
|
519
|
-
class PersistentState {
|
|
520
|
-
config;
|
|
521
|
-
vars = {};
|
|
522
|
-
gameData = {};
|
|
523
|
-
constructor(config) {
|
|
524
|
-
this.config = config;
|
|
525
|
-
}
|
|
526
|
-
/** Load persistent vars into variables map before execute() */
|
|
527
|
-
loadIntoVariables(variables) {
|
|
528
|
-
if (!this.config)
|
|
529
|
-
return;
|
|
530
|
-
for (const varName of this.config.vars) {
|
|
531
|
-
if (varName in this.vars) {
|
|
532
|
-
variables[varName] = this.vars[varName];
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
}
|
|
536
|
-
/** Save persistent vars from variables map after execute() */
|
|
537
|
-
saveFromVariables(variables) {
|
|
538
|
-
if (!this.config)
|
|
539
|
-
return;
|
|
540
|
-
for (const varName of this.config.vars) {
|
|
541
|
-
if (varName in variables) {
|
|
542
|
-
this.vars[varName] = variables[varName];
|
|
543
|
-
}
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
/** Extract _persist_game_* keys from Lua return data, store them */
|
|
547
|
-
storeGameData(data) {
|
|
548
|
-
for (const key of Object.keys(data)) {
|
|
549
|
-
if (key.startsWith('_persist_game_')) {
|
|
550
|
-
const cleanKey = key.slice('_persist_game_'.length);
|
|
551
|
-
this.gameData[cleanKey] = data[key];
|
|
552
|
-
delete data[key]; // remove from client data
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
/** Get _ps_* params for next execute() call */
|
|
557
|
-
getGameDataParams() {
|
|
558
|
-
const params = {};
|
|
559
|
-
for (const [key, value] of Object.entries(this.gameData)) {
|
|
560
|
-
params[`_ps_${key}`] = value;
|
|
561
|
-
}
|
|
562
|
-
return params;
|
|
563
|
-
}
|
|
564
|
-
/** Get exposed vars for client data.persistent_state */
|
|
565
|
-
getExposedVars() {
|
|
566
|
-
if (!this.config?.exposed_vars?.length)
|
|
567
|
-
return undefined;
|
|
568
|
-
const exposed = {};
|
|
569
|
-
for (const varName of this.config.exposed_vars) {
|
|
570
|
-
if (varName in this.vars) {
|
|
571
|
-
exposed[varName] = this.vars[varName];
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
return exposed;
|
|
575
|
-
}
|
|
576
|
-
/** Reset all state */
|
|
577
|
-
reset() {
|
|
578
|
-
this.vars = {};
|
|
579
|
-
this.gameData = {};
|
|
580
|
-
}
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
const fengari = require('fengari');
|
|
584
|
-
const { lua, lauxlib, lualib } = fengari;
|
|
585
|
-
const { to_luastring, to_jsstring } = fengari;
|
|
586
|
-
/**
|
|
587
|
-
* Runs Lua game scripts locally, replicating the platform's server-side execution.
|
|
588
|
-
*
|
|
589
|
-
* Implements the full lifecycle: action routing → state assembly → Lua execute() →
|
|
590
|
-
* result extraction → transition evaluation → session management.
|
|
591
|
-
*
|
|
592
|
-
* @example
|
|
593
|
-
* ```ts
|
|
594
|
-
* const engine = new LuaEngine({
|
|
595
|
-
* script: luaSource,
|
|
596
|
-
* gameDefinition: { id: 'my-slot', type: 'SLOT', actions: { ... } },
|
|
597
|
-
* });
|
|
598
|
-
*
|
|
599
|
-
* const result = engine.execute({ action: 'spin', bet: 1.0 });
|
|
600
|
-
* // result.data.matrix, result.totalWin, result.nextActions, etc.
|
|
601
|
-
* ```
|
|
602
|
-
*/
|
|
603
|
-
class LuaEngine {
|
|
604
|
-
L;
|
|
605
|
-
api;
|
|
606
|
-
actionRouter;
|
|
607
|
-
sessionManager;
|
|
608
|
-
persistentState;
|
|
609
|
-
gameDefinition;
|
|
610
|
-
variables = {};
|
|
611
|
-
constructor(config) {
|
|
612
|
-
this.gameDefinition = config.gameDefinition;
|
|
613
|
-
// Set up RNG
|
|
614
|
-
const rng = config.seed !== undefined
|
|
615
|
-
? createSeededRng(config.seed)
|
|
616
|
-
: undefined;
|
|
617
|
-
// Initialize sub-managers
|
|
618
|
-
this.api = new LuaEngineAPI(config.gameDefinition, rng, config.logger);
|
|
619
|
-
this.actionRouter = new ActionRouter(config.gameDefinition);
|
|
620
|
-
this.sessionManager = new SessionManager();
|
|
621
|
-
this.persistentState = new PersistentState(config.gameDefinition.persistent_state);
|
|
622
|
-
// Create Lua state and load standard libraries
|
|
623
|
-
this.L = lauxlib.luaL_newstate();
|
|
624
|
-
lualib.luaL_openlibs(this.L);
|
|
625
|
-
// Register engine.* API
|
|
626
|
-
this.api.register(this.L);
|
|
627
|
-
// Load and compile the script
|
|
628
|
-
this.loadScript(config.script);
|
|
629
|
-
}
|
|
630
|
-
/** Current session data (if any) */
|
|
631
|
-
get session() {
|
|
632
|
-
return this.sessionManager.current;
|
|
633
|
-
}
|
|
634
|
-
/** Current persistent state values */
|
|
635
|
-
get persistentVars() {
|
|
636
|
-
return { ...this.variables };
|
|
637
|
-
}
|
|
638
|
-
/**
|
|
639
|
-
* Execute a play action — the main entry point.
|
|
640
|
-
* This is what DevBridge calls on each PLAY_REQUEST.
|
|
641
|
-
*/
|
|
642
|
-
execute(params) {
|
|
643
|
-
const { action: actionName, bet, params: clientParams } = params;
|
|
644
|
-
// 1. Resolve the action definition
|
|
645
|
-
const action = this.actionRouter.resolveAction(actionName, this.sessionManager.isActive);
|
|
646
|
-
// 2. Build state.variables
|
|
647
|
-
const stateVars = { ...this.variables, bet };
|
|
648
|
-
// Load cross-spin persistent state
|
|
649
|
-
this.persistentState.loadIntoVariables(stateVars);
|
|
650
|
-
// Load session persistent vars
|
|
651
|
-
if (this.sessionManager.isActive) {
|
|
652
|
-
const sessionParams = this.sessionManager.getPersistentParams();
|
|
653
|
-
for (const [k, v] of Object.entries(sessionParams)) {
|
|
654
|
-
if (typeof v === 'number') {
|
|
655
|
-
stateVars[k] = v;
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
}
|
|
659
|
-
// 3. Build state.params
|
|
660
|
-
const stateParams = { ...clientParams };
|
|
661
|
-
stateParams._action = actionName;
|
|
662
|
-
// Inject session _ps_* persistent data
|
|
663
|
-
if (this.sessionManager.isActive) {
|
|
664
|
-
const sessionParams = this.sessionManager.getPersistentParams();
|
|
665
|
-
for (const [k, v] of Object.entries(sessionParams)) {
|
|
666
|
-
if (typeof v !== 'number') {
|
|
667
|
-
stateParams[k] = v;
|
|
668
|
-
}
|
|
669
|
-
}
|
|
670
|
-
}
|
|
671
|
-
// Inject cross-spin _ps_* game data
|
|
672
|
-
const gameDataParams = this.persistentState.getGameDataParams();
|
|
673
|
-
Object.assign(stateParams, gameDataParams);
|
|
674
|
-
// Handle buy bonus
|
|
675
|
-
if (action.buy_bonus_mode && this.gameDefinition.buy_bonus) {
|
|
676
|
-
const mode = this.gameDefinition.buy_bonus.modes[action.buy_bonus_mode];
|
|
677
|
-
if (mode) {
|
|
678
|
-
stateParams.buy_bonus = true;
|
|
679
|
-
stateParams.buy_bonus_mode = action.buy_bonus_mode;
|
|
680
|
-
stateParams.forced_scatter_count = this.pickFromDistribution(mode.scatter_distribution);
|
|
681
|
-
}
|
|
682
|
-
}
|
|
683
|
-
// Handle ante bet
|
|
684
|
-
if (clientParams?.ante_bet && this.gameDefinition.ante_bet) {
|
|
685
|
-
stateParams.ante_bet = true;
|
|
686
|
-
}
|
|
687
|
-
// 4. Build the state table and call Lua execute()
|
|
688
|
-
const luaResult = this.callLuaExecute(action.stage, stateParams, stateVars);
|
|
689
|
-
// 5. Extract special fields from Lua result
|
|
690
|
-
const totalWinMultiplier = typeof luaResult.total_win === 'number' ? luaResult.total_win : 0;
|
|
691
|
-
const resultVariables = (luaResult.variables ?? {});
|
|
692
|
-
const totalWin = Math.round(totalWinMultiplier * bet * 100) / 100;
|
|
693
|
-
// Merge result variables into engine variables
|
|
694
|
-
Object.assign(stateVars, resultVariables);
|
|
695
|
-
this.variables = { ...stateVars };
|
|
696
|
-
delete this.variables.bet; // bet is per-spin, not persistent
|
|
697
|
-
// Build client data (everything except special keys)
|
|
698
|
-
const data = {};
|
|
699
|
-
for (const [key, value] of Object.entries(luaResult)) {
|
|
700
|
-
if (key !== 'total_win' && key !== 'variables') {
|
|
701
|
-
data[key] = value;
|
|
702
|
-
}
|
|
703
|
-
}
|
|
704
|
-
// 6. Apply max win cap
|
|
705
|
-
let cappedWin = totalWin;
|
|
706
|
-
if (this.gameDefinition.max_win) {
|
|
707
|
-
const cap = this.calculateMaxWinCap(bet);
|
|
708
|
-
if (cap !== undefined && totalWin > cap) {
|
|
709
|
-
cappedWin = cap;
|
|
710
|
-
this.variables.max_win_reached = 1;
|
|
711
|
-
data.max_win_reached = true;
|
|
712
|
-
this.sessionManager.markMaxWinReached();
|
|
713
|
-
}
|
|
714
|
-
}
|
|
715
|
-
// 7. Handle _persist_* and _persist_game_* keys
|
|
716
|
-
this.sessionManager.storePersistData(data);
|
|
717
|
-
this.persistentState.storeGameData(data);
|
|
718
|
-
// Save cross-spin persistent state
|
|
719
|
-
this.persistentState.saveFromVariables(this.variables);
|
|
720
|
-
// Add exposed persistent vars to client data
|
|
721
|
-
const exposedVars = this.persistentState.getExposedVars();
|
|
722
|
-
if (exposedVars) {
|
|
723
|
-
data.persistent_state = exposedVars;
|
|
724
|
-
}
|
|
725
|
-
// Remove _persist_* keys from client data
|
|
726
|
-
for (const key of Object.keys(data)) {
|
|
727
|
-
if (key.startsWith('_persist_')) {
|
|
728
|
-
delete data[key];
|
|
729
|
-
}
|
|
730
|
-
}
|
|
731
|
-
// 8. Evaluate transitions
|
|
732
|
-
const { rule, nextActions } = this.actionRouter.evaluateTransitions(action, this.variables);
|
|
733
|
-
let creditDeferred = action.credit === 'defer' || rule.credit_override === 'defer';
|
|
734
|
-
let session = this.sessionManager.current;
|
|
735
|
-
// Handle session creation
|
|
736
|
-
if (rule.creates_session && !this.sessionManager.isActive) {
|
|
737
|
-
session = this.sessionManager.createSession(rule, this.variables, bet);
|
|
738
|
-
creditDeferred = true;
|
|
739
|
-
}
|
|
740
|
-
// Handle session update
|
|
741
|
-
else if (this.sessionManager.isActive) {
|
|
742
|
-
session = this.sessionManager.updateSession(rule, this.variables, cappedWin);
|
|
743
|
-
// Handle session completion
|
|
744
|
-
if (rule.complete_session || session?.completed) {
|
|
745
|
-
const completed = this.sessionManager.completeSession();
|
|
746
|
-
session = completed.session;
|
|
747
|
-
creditDeferred = false;
|
|
748
|
-
}
|
|
749
|
-
}
|
|
750
|
-
return {
|
|
751
|
-
totalWin: cappedWin,
|
|
752
|
-
data,
|
|
753
|
-
nextActions,
|
|
754
|
-
session,
|
|
755
|
-
variables: { ...this.variables },
|
|
756
|
-
creditDeferred,
|
|
757
|
-
};
|
|
758
|
-
}
|
|
759
|
-
/** Reset all state (sessions, persistent vars, variables) */
|
|
760
|
-
reset() {
|
|
761
|
-
this.variables = {};
|
|
762
|
-
this.sessionManager.reset();
|
|
763
|
-
this.persistentState.reset();
|
|
764
|
-
}
|
|
765
|
-
/** Destroy the Lua VM */
|
|
766
|
-
destroy() {
|
|
767
|
-
if (this.L) {
|
|
768
|
-
lua.lua_close(this.L);
|
|
769
|
-
this.L = null;
|
|
770
|
-
}
|
|
771
|
-
}
|
|
772
|
-
// ─── Private ──────────────────────────────────────────
|
|
773
|
-
loadScript(source) {
|
|
774
|
-
const status = lauxlib.luaL_dostring(this.L, to_luastring(source));
|
|
775
|
-
if (status !== lua.LUA_OK) {
|
|
776
|
-
const err = to_jsstring(lua.lua_tostring(this.L, -1));
|
|
777
|
-
lua.lua_pop(this.L, 1);
|
|
778
|
-
throw new Error(`Failed to load Lua script: ${err}`);
|
|
779
|
-
}
|
|
780
|
-
// Verify that execute() function exists
|
|
781
|
-
lua.lua_getglobal(this.L, to_luastring('execute'));
|
|
782
|
-
if (lua.lua_type(this.L, -1) !== lua.LUA_TFUNCTION) {
|
|
783
|
-
lua.lua_pop(this.L, 1);
|
|
784
|
-
throw new Error('Lua script must define a global `execute(state)` function');
|
|
785
|
-
}
|
|
786
|
-
lua.lua_pop(this.L, 1);
|
|
787
|
-
}
|
|
788
|
-
callLuaExecute(stage, params, variables) {
|
|
789
|
-
// Push the execute function
|
|
790
|
-
lua.lua_getglobal(this.L, to_luastring('execute'));
|
|
791
|
-
// Build and push the state table
|
|
792
|
-
lua.lua_createtable(this.L, 0, 3);
|
|
793
|
-
// state.stage
|
|
794
|
-
lua.lua_pushstring(this.L, to_luastring(stage));
|
|
795
|
-
lua.lua_setfield(this.L, -2, to_luastring('stage'));
|
|
796
|
-
// state.params
|
|
797
|
-
pushJSValue(this.L, params);
|
|
798
|
-
lua.lua_setfield(this.L, -2, to_luastring('params'));
|
|
799
|
-
// state.variables
|
|
800
|
-
pushJSValue(this.L, variables);
|
|
801
|
-
lua.lua_setfield(this.L, -2, to_luastring('variables'));
|
|
802
|
-
// Call execute(state) → 1 result
|
|
803
|
-
const status = lua.lua_pcall(this.L, 1, 1, 0);
|
|
804
|
-
if (status !== lua.LUA_OK) {
|
|
805
|
-
const err = to_jsstring(lua.lua_tostring(this.L, -1));
|
|
806
|
-
lua.lua_pop(this.L, 1);
|
|
807
|
-
throw new Error(`Lua execute() failed: ${err}`);
|
|
808
|
-
}
|
|
809
|
-
// Marshal result table to JS
|
|
810
|
-
const result = luaToJS(this.L, -1);
|
|
811
|
-
lua.lua_pop(this.L, 1);
|
|
812
|
-
if (!result || typeof result !== 'object' || Array.isArray(result)) {
|
|
813
|
-
throw new Error('Lua execute() must return a table');
|
|
814
|
-
}
|
|
815
|
-
return result;
|
|
816
|
-
}
|
|
817
|
-
calculateMaxWinCap(bet) {
|
|
818
|
-
const mw = this.gameDefinition.max_win;
|
|
819
|
-
if (!mw)
|
|
820
|
-
return undefined;
|
|
821
|
-
const caps = [];
|
|
822
|
-
if (mw.multiplier !== undefined)
|
|
823
|
-
caps.push(bet * mw.multiplier);
|
|
824
|
-
if (mw.fixed !== undefined)
|
|
825
|
-
caps.push(mw.fixed);
|
|
826
|
-
return caps.length > 0 ? Math.min(...caps) : undefined;
|
|
827
|
-
}
|
|
828
|
-
pickFromDistribution(distribution) {
|
|
829
|
-
const entries = Object.entries(distribution);
|
|
830
|
-
const totalWeight = entries.reduce((sum, [, w]) => sum + w, 0);
|
|
831
|
-
let roll = this.api.randomFloat() * totalWeight;
|
|
832
|
-
for (const [value, weight] of entries) {
|
|
833
|
-
roll -= weight;
|
|
834
|
-
if (roll < 0)
|
|
835
|
-
return parseInt(value, 10);
|
|
836
|
-
}
|
|
837
|
-
return parseInt(entries[entries.length - 1][0], 10);
|
|
838
|
-
}
|
|
839
|
-
}
|
|
840
|
-
|
|
841
6
|
const DEFAULT_CONFIG = {
|
|
842
7
|
balance: 10000,
|
|
843
8
|
currency: 'USD',
|
|
@@ -861,11 +26,11 @@ const DEFAULT_CONFIG = {
|
|
|
861
26
|
* `CasinoGameSDK` via a shared in-memory `MemoryChannel`, removing
|
|
862
27
|
* the need for postMessage and iframes.
|
|
863
28
|
*
|
|
864
|
-
*
|
|
29
|
+
* When `luaScript` is set, play requests are sent to the Vite dev server
|
|
30
|
+
* which runs LuaEngine in Node.js — no fengari in the browser.
|
|
865
31
|
*
|
|
866
32
|
* @example
|
|
867
33
|
* ```ts
|
|
868
|
-
* // In your dev entry point or vite plugin
|
|
869
34
|
* import { DevBridge } from '@energy8platform/game-engine/debug';
|
|
870
35
|
*
|
|
871
36
|
* const devBridge = new DevBridge({
|
|
@@ -874,10 +39,7 @@ const DEFAULT_CONFIG = {
|
|
|
874
39
|
* gameConfig: { id: 'my-slot', type: 'slot', betLevels: [0.2, 0.5, 1, 2] },
|
|
875
40
|
* onPlay: ({ action, bet }) => ({
|
|
876
41
|
* totalWin: Math.random() > 0.5 ? bet * (Math.random() * 20) : 0,
|
|
877
|
-
* data: {
|
|
878
|
-
* matrix: generateRandomMatrix(5, 3, 10),
|
|
879
|
-
* win_lines: [],
|
|
880
|
-
* },
|
|
42
|
+
* data: { matrix: [[1,2,3],[4,5,6],[7,8,9]] },
|
|
881
43
|
* }),
|
|
882
44
|
* });
|
|
883
45
|
* devBridge.start();
|
|
@@ -888,11 +50,11 @@ class DevBridge {
|
|
|
888
50
|
_balance;
|
|
889
51
|
_roundCounter = 0;
|
|
890
52
|
_bridge = null;
|
|
891
|
-
|
|
53
|
+
_useLuaServer;
|
|
892
54
|
constructor(config = {}) {
|
|
893
55
|
this._config = { ...DEFAULT_CONFIG, ...config };
|
|
894
56
|
this._balance = this._config.balance;
|
|
895
|
-
this.
|
|
57
|
+
this._useLuaServer = !!(this._config.luaScript && this._config.gameDefinition);
|
|
896
58
|
}
|
|
897
59
|
/** Current mock balance */
|
|
898
60
|
get balance() {
|
|
@@ -923,7 +85,8 @@ class DevBridge {
|
|
|
923
85
|
this.handleOpenDeposit();
|
|
924
86
|
});
|
|
925
87
|
if (this._config.debug) {
|
|
926
|
-
|
|
88
|
+
const mode = this._useLuaServer ? 'Lua (server-side)' : 'onPlay callback';
|
|
89
|
+
console.log(`[DevBridge] Started — mode: ${mode}`);
|
|
927
90
|
}
|
|
928
91
|
}
|
|
929
92
|
/** Stop listening */
|
|
@@ -944,22 +107,6 @@ class DevBridge {
|
|
|
944
107
|
/** Destroy the dev bridge */
|
|
945
108
|
destroy() {
|
|
946
109
|
this.stop();
|
|
947
|
-
if (this._luaEngine) {
|
|
948
|
-
this._luaEngine.destroy();
|
|
949
|
-
this._luaEngine = null;
|
|
950
|
-
}
|
|
951
|
-
}
|
|
952
|
-
initLuaEngine() {
|
|
953
|
-
if (!this._config.luaScript || !this._config.gameDefinition)
|
|
954
|
-
return;
|
|
955
|
-
this._luaEngine = new LuaEngine({
|
|
956
|
-
script: this._config.luaScript,
|
|
957
|
-
gameDefinition: this._config.gameDefinition,
|
|
958
|
-
seed: this._config.luaSeed,
|
|
959
|
-
});
|
|
960
|
-
if (this._config.debug) {
|
|
961
|
-
console.log('[DevBridge] LuaEngine initialized');
|
|
962
|
-
}
|
|
963
110
|
}
|
|
964
111
|
// ─── Message Handling ──────────────────────────────────
|
|
965
112
|
handleGameReady(id) {
|
|
@@ -977,32 +124,25 @@ class DevBridge {
|
|
|
977
124
|
// Deduct bet
|
|
978
125
|
this._balance -= bet;
|
|
979
126
|
this._roundCounter++;
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
nextActions: luaResult.nextActions,
|
|
993
|
-
session: luaResult.session,
|
|
994
|
-
creditPending: luaResult.creditDeferred,
|
|
995
|
-
bonusFreeSpin: null,
|
|
996
|
-
currency: this._config.currency,
|
|
997
|
-
gameId: this._config.gameConfig?.id ?? 'dev-game',
|
|
998
|
-
};
|
|
127
|
+
if (this._useLuaServer) {
|
|
128
|
+
// Send to Vite dev server for Lua execution
|
|
129
|
+
this.executeLuaOnServer({ action, bet, roundId, params })
|
|
130
|
+
.then((result) => {
|
|
131
|
+
this._bridge?.send('PLAY_RESULT', result, id);
|
|
132
|
+
})
|
|
133
|
+
.catch((err) => {
|
|
134
|
+
console.error('[DevBridge] Lua server error:', err);
|
|
135
|
+
// Refund bet on error
|
|
136
|
+
this._balance += bet;
|
|
137
|
+
this._bridge?.send('PLAY_RESULT', this.buildFallbackResult(action, bet, roundId), id);
|
|
138
|
+
});
|
|
999
139
|
}
|
|
1000
140
|
else {
|
|
1001
141
|
// Fallback to onPlay callback
|
|
1002
142
|
const customResult = this._config.onPlay({ action, bet, roundId, params });
|
|
1003
143
|
const totalWin = customResult.totalWin ?? (Math.random() > 0.6 ? bet * (1 + Math.random() * 10) : 0);
|
|
1004
144
|
this._balance += totalWin;
|
|
1005
|
-
result = {
|
|
145
|
+
const result = {
|
|
1006
146
|
roundId: roundId ?? `dev-round-${this._roundCounter}`,
|
|
1007
147
|
action,
|
|
1008
148
|
balanceAfter: this._balance,
|
|
@@ -1015,8 +155,50 @@ class DevBridge {
|
|
|
1015
155
|
currency: this._config.currency,
|
|
1016
156
|
gameId: this._config.gameConfig?.id ?? 'dev-game',
|
|
1017
157
|
};
|
|
158
|
+
this.delayedSend('PLAY_RESULT', result, id);
|
|
1018
159
|
}
|
|
1019
|
-
|
|
160
|
+
}
|
|
161
|
+
async executeLuaOnServer(params) {
|
|
162
|
+
const response = await fetch('/__lua-play', {
|
|
163
|
+
method: 'POST',
|
|
164
|
+
headers: { 'Content-Type': 'application/json' },
|
|
165
|
+
body: JSON.stringify(params),
|
|
166
|
+
});
|
|
167
|
+
if (!response.ok) {
|
|
168
|
+
const err = await response.json().catch(() => ({ error: 'Unknown error' }));
|
|
169
|
+
throw new Error(err.error ?? `HTTP ${response.status}`);
|
|
170
|
+
}
|
|
171
|
+
const luaResult = await response.json();
|
|
172
|
+
const totalWin = luaResult.creditDeferred ? 0 : luaResult.totalWin;
|
|
173
|
+
this._balance += totalWin;
|
|
174
|
+
return {
|
|
175
|
+
roundId: params.roundId ?? `dev-round-${this._roundCounter}`,
|
|
176
|
+
action: params.action,
|
|
177
|
+
balanceAfter: this._balance,
|
|
178
|
+
totalWin: Math.round(luaResult.totalWin * 100) / 100,
|
|
179
|
+
data: luaResult.data,
|
|
180
|
+
nextActions: luaResult.nextActions,
|
|
181
|
+
session: luaResult.session,
|
|
182
|
+
creditPending: luaResult.creditDeferred,
|
|
183
|
+
bonusFreeSpin: null,
|
|
184
|
+
currency: this._config.currency,
|
|
185
|
+
gameId: this._config.gameConfig?.id ?? 'dev-game',
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
buildFallbackResult(action, bet, roundId) {
|
|
189
|
+
return {
|
|
190
|
+
roundId: roundId ?? `dev-round-${this._roundCounter}`,
|
|
191
|
+
action,
|
|
192
|
+
balanceAfter: this._balance,
|
|
193
|
+
totalWin: 0,
|
|
194
|
+
data: { error: 'Lua execution failed' },
|
|
195
|
+
nextActions: ['spin'],
|
|
196
|
+
session: null,
|
|
197
|
+
creditPending: false,
|
|
198
|
+
bonusFreeSpin: null,
|
|
199
|
+
currency: this._config.currency,
|
|
200
|
+
gameId: this._config.gameConfig?.id ?? 'dev-game',
|
|
201
|
+
};
|
|
1020
202
|
}
|
|
1021
203
|
handlePlayAck(_payload) {
|
|
1022
204
|
if (this._config.debug) {
|
|
@@ -1031,7 +213,7 @@ class DevBridge {
|
|
|
1031
213
|
}
|
|
1032
214
|
handleOpenDeposit() {
|
|
1033
215
|
if (this._config.debug) {
|
|
1034
|
-
console.log('[DevBridge]
|
|
216
|
+
console.log('[DevBridge] Open deposit requested (mock: adding 1000)');
|
|
1035
217
|
}
|
|
1036
218
|
this._balance += 1000;
|
|
1037
219
|
this._bridge?.send('BALANCE_UPDATE', { balance: this._balance });
|