@energy8platform/platform-core 0.28.3 → 0.29.0

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.
Files changed (56) hide show
  1. package/README.md +127 -150
  2. package/bin/simulate.ts +35 -98
  3. package/dist/dev-bridge.cjs.js +3 -3
  4. package/dist/dev-bridge.cjs.js.map +1 -1
  5. package/dist/dev-bridge.d.ts +2 -2
  6. package/dist/dev-bridge.esm.js +3 -3
  7. package/dist/dev-bridge.esm.js.map +1 -1
  8. package/dist/game-spec.cjs.js +59 -59
  9. package/dist/game-spec.cjs.js.map +1 -1
  10. package/dist/game-spec.d.ts +24 -23
  11. package/dist/game-spec.esm.js +57 -56
  12. package/dist/game-spec.esm.js.map +1 -1
  13. package/dist/index.cjs.js +3 -3
  14. package/dist/index.cjs.js.map +1 -1
  15. package/dist/index.d.ts +21 -2
  16. package/dist/index.esm.js +3 -3
  17. package/dist/index.esm.js.map +1 -1
  18. package/dist/lua.cjs.js +0 -1234
  19. package/dist/lua.cjs.js.map +1 -1
  20. package/dist/lua.d.ts +1 -206
  21. package/dist/lua.esm.js +0 -1225
  22. package/dist/lua.esm.js.map +1 -1
  23. package/dist/simulation.cjs.js +48 -179
  24. package/dist/simulation.cjs.js.map +1 -1
  25. package/dist/simulation.d.ts +26 -60
  26. package/dist/simulation.esm.js +48 -178
  27. package/dist/simulation.esm.js.map +1 -1
  28. package/dist/vite.cjs.js +323 -109
  29. package/dist/vite.cjs.js.map +1 -1
  30. package/dist/vite.d.ts +19 -9
  31. package/dist/vite.esm.js +322 -109
  32. package/dist/vite.esm.js.map +1 -1
  33. package/package.json +6 -5
  34. package/scripts/install-e8.mjs +113 -0
  35. package/src/dev-bridge/DevBridge.ts +3 -3
  36. package/src/game-spec/defineGame.ts +2 -2
  37. package/src/game-spec/derive.ts +39 -16
  38. package/src/game-spec/export.ts +23 -39
  39. package/src/game-spec/index.ts +3 -3
  40. package/src/game-spec/types.ts +2 -1
  41. package/src/index.ts +6 -12
  42. package/src/lua/index.ts +4 -11
  43. package/src/simulation/NativeSimulationRunner.ts +71 -45
  44. package/src/simulation/index.ts +2 -4
  45. package/src/vite/index.ts +4 -121
  46. package/src/vite/spinPlugin.ts +338 -0
  47. package/scripts/install-simulate.mjs +0 -101
  48. package/src/lua/ActionRouter.ts +0 -132
  49. package/src/lua/LuaEngine.ts +0 -520
  50. package/src/lua/LuaEngineAPI.ts +0 -314
  51. package/src/lua/PersistentState.ts +0 -80
  52. package/src/lua/SessionManager.ts +0 -249
  53. package/src/lua/SimulationRunner.ts +0 -190
  54. package/src/lua/fengari.d.ts +0 -10
  55. package/src/simulation/ParallelSimulationRunner.ts +0 -156
  56. package/src/simulation/SimulationWorker.ts +0 -44
@@ -1,520 +0,0 @@
1
- import type { PlayParams, SessionData } from '@energy8platform/game-sdk';
2
- import type { LuaEngineConfig, LuaPlayResult, GameDefinition, ActionDefinition } from './types';
3
- import { LuaEngineAPI, createSeededRng, luaToJS, pushJSValue, cachedToLuastring } from './LuaEngineAPI';
4
- import { ActionRouter } from './ActionRouter';
5
- import { SessionManager } from './SessionManager';
6
- import { PersistentState } from './PersistentState';
7
-
8
- // fengari — Lua 5.3 in pure JavaScript
9
- import fengari from 'fengari';
10
-
11
- const { lua, lauxlib, lualib } = fengari;
12
- const { to_luastring, to_jsstring } = fengari;
13
-
14
- /** Default engine variables matching the server's NewGameState() */
15
- const DEFAULT_VARIABLES: Record<string, number> = {
16
- multiplier: 1,
17
- total_multiplier: 1,
18
- global_multiplier: 1,
19
- last_win_amount: 0,
20
- free_spins_awarded: 0,
21
- };
22
-
23
- /**
24
- * Runs Lua game scripts locally, replicating the platform's server-side execution.
25
- *
26
- * Implements the full lifecycle matching `casino_platform/internal/usecase/game_usecase.go`:
27
- * action routing → state assembly → Lua execute() → result extraction →
28
- * transition evaluation → session management.
29
- */
30
- export class LuaEngine {
31
- private L: any;
32
- private api: LuaEngineAPI;
33
- private actionRouter: ActionRouter;
34
- private sessionManager: SessionManager;
35
- private persistentState: PersistentState;
36
- private gameDefinition: GameDefinition;
37
- private variables: Record<string, number> = {};
38
- private simulationMode: boolean;
39
- private allowSessionlessActions: boolean;
40
- /** Reusable state objects to avoid per-iteration allocation */
41
- private _stateVars: Record<string, number> = {};
42
- private _stateParams: Record<string, unknown> = {};
43
-
44
- constructor(config: LuaEngineConfig) {
45
- this.gameDefinition = config.gameDefinition;
46
- this.simulationMode = config.simulationMode ?? false;
47
- this.allowSessionlessActions = config.allowSessionlessActions ?? false;
48
-
49
- const rng = config.seed !== undefined
50
- ? createSeededRng(config.seed)
51
- : undefined;
52
-
53
- this.api = new LuaEngineAPI(config.gameDefinition, rng, config.logger);
54
- this.actionRouter = new ActionRouter(config.gameDefinition);
55
- this.sessionManager = new SessionManager();
56
- this.persistentState = new PersistentState(config.gameDefinition.persistent_state);
57
-
58
- this.L = lauxlib.luaL_newstate();
59
- lualib.luaL_openlibs(this.L);
60
-
61
- // Polyfill Lua 5.1/5.2 functions removed in 5.3
62
- lauxlib.luaL_dostring(this.L, to_luastring(`
63
- math.pow = function(a, b) return a ^ b end
64
- math.atan2 = math.atan2 or function(y, x) return math.atan(y, x) end
65
- math.log10 = math.log10 or function(x) return math.log(x, 10) end
66
- math.cosh = math.cosh or function(x) return (math.exp(x) + math.exp(-x)) / 2 end
67
- math.sinh = math.sinh or function(x) return (math.exp(x) - math.exp(-x)) / 2 end
68
- math.tanh = math.tanh or function(x) return math.sinh(x) / math.cosh(x) end
69
- math.frexp = math.frexp or function(x)
70
- if x == 0 then return 0, 0 end
71
- local e = math.floor(math.log(math.abs(x), 2)) + 1
72
- return x / (2 ^ e), e
73
- end
74
- math.ldexp = math.ldexp or function(m, e) return m * (2 ^ e) end
75
- unpack = unpack or table.unpack
76
- loadstring = loadstring or load
77
- table.getn = table.getn or function(t) return #t end
78
- `));
79
-
80
- this.api.register(this.L);
81
- this.loadScript(config.script);
82
- }
83
-
84
- get session(): SessionData | null {
85
- return this.sessionManager.current;
86
- }
87
-
88
- get persistentVars(): Record<string, number> {
89
- return { ...this.variables };
90
- }
91
-
92
- /**
93
- * Execute a play action — replicates server's Play() function.
94
- */
95
- execute(params: PlayParams): LuaPlayResult {
96
- const { action: actionName, params: clientParams } = params;
97
-
98
- // 1. Resolve action. In the harness, `allowSessionlessActions` lets a standalone free_spin
99
- // (replayed after a books-path bonus buy that never created an engine session) run anyway.
100
- const action = this.actionRouter.resolveAction(
101
- actionName,
102
- this.sessionManager.isActive || this.allowSessionlessActions,
103
- );
104
-
105
- // 2. Determine bet — server uses session bet for session actions
106
- let bet = params.bet;
107
- if (this.sessionManager.isActive && this.sessionManager.sessionBet !== undefined) {
108
- bet = this.sessionManager.sessionBet;
109
- }
110
-
111
- // 3. Build state.variables (matching server's NewGameState + restore)
112
- // Reuse pooled object to avoid per-iteration allocation
113
- const stateVars = this._stateVars;
114
- // Clear previous keys
115
- for (const key in stateVars) delete stateVars[key];
116
- // Apply defaults, then engine vars, then bet
117
- Object.assign(stateVars, DEFAULT_VARIABLES, this.variables);
118
- stateVars.bet = bet;
119
-
120
- // Load cross-spin persistent state
121
- this.persistentState.loadIntoVariables(stateVars);
122
-
123
- // Load session persistent vars + restore spinsRemaining
124
- if (this.sessionManager.isActive) {
125
- const sessionParams = this.sessionManager.getPersistentParams();
126
- for (const [k, v] of Object.entries(sessionParams)) {
127
- if (typeof v === 'number') {
128
- stateVars[k] = v;
129
- }
130
- }
131
- // Restore spinsRemaining into the variable the script reads
132
- if (this.sessionManager.spinsVarName) {
133
- stateVars[this.sessionManager.spinsVarName] = this.sessionManager.spinsRemaining;
134
- }
135
- // Also set free_spins_remaining for convenience
136
- stateVars.free_spins_remaining = this.sessionManager.spinsRemaining;
137
- }
138
-
139
- // 4. Build state.params (reuse pooled object)
140
- const stateParams = this._stateParams;
141
- for (const key in stateParams) delete stateParams[key];
142
- if (clientParams) Object.assign(stateParams, clientParams);
143
- stateParams._action = actionName;
144
-
145
- // Inject session _ps_* persistent data
146
- if (this.sessionManager.isActive) {
147
- const sessionParams = this.sessionManager.getPersistentParams();
148
- for (const [k, v] of Object.entries(sessionParams)) {
149
- if (typeof v !== 'number') {
150
- stateParams[k] = v;
151
- }
152
- }
153
- }
154
-
155
- // Inject cross-spin _ps_* game data
156
- const gameDataParams = this.persistentState.getGameDataParams();
157
- Object.assign(stateParams, gameDataParams);
158
-
159
- // v5: forced scatter rolls come from the action's own feature_data.
160
- // Output of the roll stays in state.params (it's random per-call), while
161
- // the static config flows through state.action_config.feature_data.
162
- const scatterDist = readScatterDistribution(action.feature_data);
163
- if (scatterDist) {
164
- stateParams.forced_scatter_count = this.pickFromDistribution(scatterDist);
165
- }
166
-
167
- // 5. Execute Lua (server: executor.Execute(stage, state))
168
- const luaResult = this.callLuaExecute(action.stage, actionName, stateParams, stateVars, action);
169
-
170
- // 6. Process result (server: ApplyLuaResult)
171
- const totalWinMultiplier = typeof luaResult.total_win === 'number' ? luaResult.total_win : 0;
172
- const resultVariables = (luaResult.variables ?? {}) as Record<string, number>;
173
- const spinWin = Math.round(totalWinMultiplier * bet * 100) / 100;
174
-
175
- // Merge ONLY Lua return variables into engine state (not the whole stateVars).
176
- // On the server, state.Variables is a temporary object rebuilt each call.
177
- // Only the Lua result's `variables` table persists between calls.
178
- Object.assign(this.variables, resultVariables);
179
- // Also update stateVars for transition evaluation below
180
- Object.assign(stateVars, resultVariables);
181
-
182
- // Build client data (everything except special keys)
183
- const data: Record<string, unknown> = {};
184
- for (const [key, value] of Object.entries(luaResult)) {
185
- if (key !== 'total_win' && key !== 'variables') {
186
- data[key] = value;
187
- }
188
- }
189
-
190
- // Apply MapState parity — server's state_mapper.go injects these
191
- // variable-derived keys into the client data so scripts don't have to
192
- // surface them manually. Lua-provided values take precedence (server
193
- // also overwrites variable-derived keys with state.Data on merge).
194
- this.applyMapStateInjection(stateVars, data);
195
-
196
- // 7. Handle _persist_* and _persist_game_* keys
197
- this.sessionManager.storePersistData(data);
198
- this.persistentState.storeGameData(data);
199
-
200
- // Save cross-spin persistent state (from stateVars which has Lua result merged)
201
- this.persistentState.saveFromVariables(stateVars);
202
-
203
- // Add exposed persistent vars to client data
204
- const exposedVars = this.persistentState.getExposedVars();
205
- if (exposedVars) {
206
- data.persistent_state = exposedVars;
207
- }
208
-
209
- // Remove _persist_* keys from client data
210
- for (const key of Object.keys(data)) {
211
- if (key.startsWith('_persist_')) {
212
- delete data[key];
213
- }
214
- }
215
-
216
- // 8. Evaluate transitions (server uses state.Variables which is stateVars)
217
- const { rule } = this.actionRouter.evaluateTransitions(action, stateVars);
218
- let nextActions = rule.next_actions;
219
-
220
- // 9. Determine credit behavior (server: creditNow logic)
221
- let creditDeferred = action.credit === 'defer' || rule.credit_override === 'defer';
222
-
223
- // 10. Session lifecycle (server: create/update/complete session)
224
- let session = this.sessionManager.current;
225
- let resultTotalWin = spinWin;
226
- let sessionCompleted = false;
227
-
228
- // Calculate max win cap for session
229
- const maxWinCap = this.calculateMaxWinCap(bet);
230
-
231
- // Snapshot the round data for history (matches server's MapStateForHistory:
232
- // strip _persist_* keys, but those are already removed below before
233
- // returning — at this point in the flow they may still be in `data`,
234
- // so we filter inline).
235
- const roundData = stripPersistKeys(data);
236
-
237
- if (rule.creates_session && !this.sessionManager.isActive) {
238
- // CREATE SESSION — initial spin counted (server: createSession includes spinWin)
239
- session = this.sessionManager.createSession(rule, stateVars, bet, spinWin, maxWinCap, roundData);
240
- creditDeferred = true;
241
- resultTotalWin = spinWin;
242
-
243
- // Clear the trigger variable — it was consumed to set spinsRemaining
244
- if (rule.session_config?.total_spins_var) {
245
- delete this.variables[rule.session_config.total_spins_var];
246
- }
247
- } else if (this.sessionManager.isActive) {
248
- // UPDATE SESSION — accumulate win, check completion
249
- session = this.sessionManager.updateSession(rule, stateVars, spinWin, roundData);
250
-
251
- if (session?.completed) {
252
- // SESSION COMPLETED — server returns session.TotalWin as result.TotalWin,
253
- // and pulls next_actions from the explicit completion transition
254
- // (findCompletionNextActions) rather than the matched 'continue' rule.
255
- const completed = this.sessionManager.completeSession();
256
- session = completed.session;
257
- resultTotalWin = completed.totalWin;
258
- sessionCompleted = true;
259
- creditDeferred = false;
260
-
261
- const completionNext = findCompletionNextActions(action);
262
- if (completionNext) {
263
- nextActions = completionNext;
264
- }
265
-
266
- // Clean up session-scoped variables
267
- for (const varName of completed.sessionVarNames) {
268
- delete this.variables[varName];
269
- }
270
- } else {
271
- // Mid-session: totalWin = spinWin, credit deferred
272
- resultTotalWin = spinWin;
273
- creditDeferred = true;
274
- }
275
- }
276
- // No session: resultTotalWin = spinWin (already set)
277
-
278
- // Apply max win cap for non-session spins
279
- if (!this.sessionManager.isActive && !sessionCompleted && maxWinCap !== undefined && resultTotalWin > maxWinCap) {
280
- resultTotalWin = maxWinCap;
281
- this.variables.max_win_reached = 1;
282
- data.max_win_reached = true;
283
- }
284
-
285
- return {
286
- totalWin: Math.round(resultTotalWin * 100) / 100,
287
- data,
288
- nextActions,
289
- session,
290
- // In simulation mode, return reference directly (caller only reads, never mutates)
291
- variables: this.simulationMode ? this.variables : { ...this.variables },
292
- creditDeferred,
293
- };
294
- }
295
-
296
- reset(): void {
297
- this.variables = {};
298
- this.sessionManager.reset();
299
- this.persistentState.reset();
300
- }
301
-
302
- destroy(): void {
303
- if (this.L) {
304
- lua.lua_close(this.L);
305
- this.L = null;
306
- }
307
- }
308
-
309
- // ─── Private ──────────────────────────────────────────
310
-
311
- private loadScript(source: string): void {
312
- const status = lauxlib.luaL_dostring(this.L, to_luastring(source));
313
- if (status !== lua.LUA_OK) {
314
- const err = to_jsstring(lua.lua_tostring(this.L, -1));
315
- lua.lua_pop(this.L, 1);
316
- throw new Error(`Failed to load Lua script: ${err}`);
317
- }
318
-
319
- lua.lua_getglobal(this.L, cachedToLuastring('execute'));
320
- if (lua.lua_type(this.L, -1) !== lua.LUA_TFUNCTION) {
321
- lua.lua_pop(this.L, 1);
322
- throw new Error('Lua script must define a global `execute(state)` function');
323
- }
324
- lua.lua_pop(this.L, 1);
325
- }
326
-
327
- private callLuaExecute(
328
- stage: string,
329
- action: string,
330
- params: Record<string, unknown>,
331
- variables: Record<string, number>,
332
- actionDef: ActionDefinition,
333
- ): Record<string, unknown> {
334
- lua.lua_getglobal(this.L, cachedToLuastring('execute'));
335
-
336
- // Build state table: {stage, action, action_config, params, variables}
337
- lua.lua_createtable(this.L, 0, 5);
338
-
339
- // state.stage
340
- lua.lua_pushstring(this.L, cachedToLuastring(stage));
341
- lua.lua_setfield(this.L, -2, cachedToLuastring('stage'));
342
-
343
- // state.action (server sets this at top level)
344
- lua.lua_pushstring(this.L, cachedToLuastring(action));
345
- lua.lua_setfield(this.L, -2, cachedToLuastring('action'));
346
-
347
- // state.action_config — v5: { cost_multiplier, feature_data }.
348
- // Server's lua_runtime.go substitutes 1.0 when cost_multiplier is unset
349
- // so scripts never see 0; mirror that default.
350
- const mult = typeof actionDef.cost_multiplier === 'number' && actionDef.cost_multiplier > 0
351
- ? actionDef.cost_multiplier
352
- : 1;
353
- pushJSValue(this.L, {
354
- cost_multiplier: mult,
355
- feature_data: actionDef.feature_data ?? {},
356
- });
357
- lua.lua_setfield(this.L, -2, cachedToLuastring('action_config'));
358
-
359
- // state.params
360
- pushJSValue(this.L, params);
361
- lua.lua_setfield(this.L, -2, cachedToLuastring('params'));
362
-
363
- // state.variables
364
- pushJSValue(this.L, variables);
365
- lua.lua_setfield(this.L, -2, cachedToLuastring('variables'));
366
-
367
- const status = lua.lua_pcall(this.L, 1, 1, 0);
368
- if (status !== lua.LUA_OK) {
369
- const err = to_jsstring(lua.lua_tostring(this.L, -1));
370
- lua.lua_pop(this.L, 1);
371
- throw new Error(`Lua execute() failed: ${err}`);
372
- }
373
-
374
- if (this.simulationMode) {
375
- // Fast path: extract only total_win, variables, _persist_* keys
376
- const result: Record<string, unknown> = {};
377
-
378
- lua.lua_getfield(this.L, -1, cachedToLuastring('total_win'));
379
- result.total_win = lua.lua_type(this.L, -1) === lua.LUA_TNUMBER
380
- ? lua.lua_tonumber(this.L, -1) : 0;
381
- lua.lua_pop(this.L, 1);
382
-
383
- lua.lua_getfield(this.L, -1, cachedToLuastring('variables'));
384
- if (lua.lua_type(this.L, -1) === lua.LUA_TTABLE) {
385
- result.variables = luaToJS(this.L, -1);
386
- }
387
- lua.lua_pop(this.L, 1);
388
-
389
- // Scan for _persist_* keys (different stages may or may not have them)
390
- lua.lua_pushnil(this.L);
391
- while (lua.lua_next(this.L, -2) !== 0) {
392
- if (lua.lua_type(this.L, -2) === lua.LUA_TSTRING) {
393
- const key = to_jsstring(lua.lua_tostring(this.L, -2));
394
- if (key.startsWith('_persist_')) {
395
- result[key] = luaToJS(this.L, -1);
396
- }
397
- }
398
- lua.lua_pop(this.L, 1);
399
- }
400
-
401
- lua.lua_pop(this.L, 1);
402
- return result;
403
- }
404
-
405
- // Full path
406
- const result = luaToJS(this.L, -1);
407
- lua.lua_pop(this.L, 1);
408
-
409
- if (!result || typeof result !== 'object' || Array.isArray(result)) {
410
- throw new Error('Lua execute() must return a table');
411
- }
412
-
413
- return result as Record<string, unknown>;
414
- }
415
-
416
- /**
417
- * Mirror server's state_mapper.go MapState — surface variable-derived
418
- * fields so scripts that don't manually echo them in the result table
419
- * still produce a server-shaped data map. Lua keys win on conflict.
420
- */
421
- private applyMapStateInjection(
422
- vars: Record<string, number>,
423
- data: Record<string, unknown>,
424
- ): void {
425
- const m = vars.multiplier;
426
- if (typeof m === 'number' && m > 1 && data.multiplier === undefined) {
427
- data.multiplier = m;
428
- }
429
-
430
- const gm = vars.global_multiplier;
431
- if (typeof gm === 'number' && gm > 1 && data.global_multiplier === undefined) {
432
- data.global_multiplier = gm;
433
- }
434
-
435
- const fs = vars.free_spins_remaining;
436
- if (typeof fs === 'number' && fs > 0 && data.free_spins_total === undefined) {
437
- data.free_spins_total = Math.trunc(fs);
438
- }
439
-
440
- if (vars.max_win_reached === 1 && data.max_win_reached === undefined) {
441
- data.max_win_reached = true;
442
- }
443
- }
444
-
445
- private calculateMaxWinCap(bet: number): number | undefined {
446
- const mw = this.gameDefinition.max_win;
447
- if (!mw) return undefined;
448
-
449
- const caps: number[] = [];
450
- if (mw.multiplier !== undefined) caps.push(bet * mw.multiplier);
451
- if (mw.fixed !== undefined) caps.push(mw.fixed);
452
-
453
- return caps.length > 0 ? Math.min(...caps) : undefined;
454
- }
455
-
456
- private pickFromDistribution(distribution: Record<string, number>): number {
457
- const entries = Object.entries(distribution);
458
- const totalWeight = entries.reduce((sum, [, w]) => sum + w, 0);
459
- let roll = this.api.randomFloat() * totalWeight;
460
-
461
- for (const [value, weight] of entries) {
462
- roll -= weight;
463
- if (roll < 0) return parseInt(value, 10);
464
- }
465
-
466
- return parseInt(entries[entries.length - 1][0], 10);
467
- }
468
- }
469
-
470
- // ─── Module helpers ─────────────────────────────────────
471
-
472
- /**
473
- * Pull a `scatter_distribution` map out of an action's feature_data, if any.
474
- * Returns null when missing or wrong-shaped — caller falls through with no
475
- * forced_scatter_count injection (matches server behavior).
476
- */
477
- function readScatterDistribution(
478
- featureData: Record<string, unknown> | undefined,
479
- ): Record<string, number> | null {
480
- if (!featureData) return null;
481
- const dist = featureData['scatter_distribution'];
482
- if (!dist || typeof dist !== 'object') return null;
483
- const out: Record<string, number> = {};
484
- for (const [k, v] of Object.entries(dist as Record<string, unknown>)) {
485
- if (typeof v === 'number') out[k] = v;
486
- }
487
- return Object.keys(out).length > 0 ? out : null;
488
- }
489
-
490
- /**
491
- * Strip _persist_* and _persist_game_* keys from a data map — matches
492
- * server's MapStateForHistory used when recording session round history.
493
- */
494
- function stripPersistKeys(data: Record<string, unknown>): Record<string, unknown> {
495
- const out: Record<string, unknown> = {};
496
- for (const k of Object.keys(data)) {
497
- if (k.startsWith('_persist_') || k.startsWith('_persist_game_')) continue;
498
- out[k] = data[k];
499
- }
500
- return out;
501
- }
502
-
503
- /**
504
- * Mirror server's findCompletionNextActions: when a session naturally
505
- * completes, the matched 'continue' rule's next_actions are NOT what
506
- * the client should see — the explicit complete_session transition wins,
507
- * with a fallback to the 'always' transition.
508
- */
509
- function findCompletionNextActions(action: ActionDefinition): string[] | null {
510
- let alwaysFallback: string[] | null = null;
511
- for (const t of action.transitions) {
512
- if (t.complete_session && t.next_actions && t.next_actions.length > 0) {
513
- return t.next_actions;
514
- }
515
- if (t.condition.trim() === 'always' && t.next_actions && t.next_actions.length > 0 && alwaysFallback === null) {
516
- alwaysFallback = t.next_actions;
517
- }
518
- }
519
- return alwaysFallback;
520
- }