@energy8platform/create-slot 0.2.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.
package/README.md ADDED
@@ -0,0 +1,48 @@
1
+ # @energy8platform/create-slot
2
+
3
+ Scaffolder for a new PixiJS slot game on the Energy8 `@energy8platform/game-engine` framework,
4
+ ready to run locally, test in the Stake dev harness, generate its math, and build for Stake Engine.
5
+
6
+ ```bash
7
+ npm create @energy8platform/slot@latest my-game
8
+ # or
9
+ npx @energy8platform/create-slot my-game
10
+ ```
11
+
12
+ It asks a few questions (id, title, mechanic, grid, …) and writes a complete, type-checking project.
13
+
14
+ ## What it generates
15
+
16
+ | File | Role |
17
+ |---|---|
18
+ | `src/game.spec.ts` | **Single source of truth.** `defineGame(spec)` derives the Lua game definition, the `modeMap`, the math modes, the paytable, the shell's buy-bonus cards AND the Game Info per-mode table. Edit modes / symbols / bet levels / `rtp` / `maxWin` here. |
19
+ | `src/scenes/GameScene.ts` | The render-only scene: `present(result, ctx)` + optional `onBonusEnter`/`onBonusExit`. The host owns the play loop; the scene never calls play/ack. |
20
+ | `src/scenes/IntroScene.ts` | Tap-to-start splash (skipped on replay). |
21
+ | `src/game/script.logic.lua` | The game's math logic (returns a bet-multiplier `total_win`). |
22
+ | `src/game/normalize.ts` + `schema.ts` | Map the raw play result → the scene's typed `SpinData`. |
23
+ | `src/stake/adapter.ts` | The Stake book adapter (`createGameAdapter`) — slices a round-book into segments. |
24
+ | `math.config.ts` | Per-mode sim + curate tuning for the math pipeline (independent of the spec's declared rtp/maxWin). |
25
+ | `CLAUDE.md` | Guidance for Claude Code on how the project is wired and what the framework already handles. |
26
+ | `vite.config.ts` | Dev (DevBridge), the Stake harness, and the `build:stake` frontend target. |
27
+
28
+ ## Workflow
29
+
30
+ ```bash
31
+ npm install
32
+ npm run dev # local dev via Energy8 DevBridge (config + Lua)
33
+ npm run stake # Stake dev harness: iframe wrapper + dev-RGS backed by curated books
34
+ npm run math:pool # Stage A — honest large simulation (the pool)
35
+ npm run math:curate # Stage B — compress into the publishable stake-math/ bundle
36
+ npm run build:stake # the Stake frontend build → dist-stake/
37
+ ```
38
+
39
+ The framework handles the Stake integration end-to-end (RGS bridge, shell, play loop, bonus
40
+ segment-drain, resume, autoplay, social mode, jurisdiction, money formatting, the math pipeline).
41
+ You write the spec, the Lua math, and the rendering — see the generated `CLAUDE.md` for the full map.
42
+
43
+ ## Related packages
44
+
45
+ - `@energy8platform/game-engine` — the PixiJS engine + host the game runs on.
46
+ - `@energy8platform/platform-core` — renderer-agnostic platform (Lua engine, shell, SDK session).
47
+ - `@energy8platform/stake-kit` — the Stake book adapter + dev harness used by the scaffold.
48
+ - `@energy8platform/stake-math-tools` — the `e8-math` simulation + curation pipeline.
package/dist/cli.js ADDED
@@ -0,0 +1,784 @@
1
+ #!/usr/bin/env node
2
+ import { stdout, stdin, exit, argv } from 'node:process';
3
+ import { resolve, join } from 'node:path';
4
+ import { createInterface } from 'node:readline/promises';
5
+ import { mkdirSync, cpSync, existsSync, renameSync, writeFileSync, rmSync, readdirSync, statSync, readFileSync } from 'node:fs';
6
+ import { fileURLToPath } from 'node:url';
7
+
8
+ const DEFAULT_GRID = {
9
+ lines: { cols: 5, rows: 3 },
10
+ ways: { cols: 5, rows: 3 },
11
+ cluster: { cols: 7, rows: 7 },
12
+ anywhere: { cols: 5, rows: 4 },
13
+ custom: { cols: 6, rows: 6 },
14
+ };
15
+ function titleCase(id) {
16
+ return id.split(/[-_]/).filter(Boolean).map((w) => w[0].toUpperCase() + w.slice(1)).join(' ');
17
+ }
18
+ function parseFlags(argv) {
19
+ // normalize --flag=value → --flag value
20
+ const args = [];
21
+ for (const tok of argv) {
22
+ if (tok.startsWith('--') && tok.includes('=')) {
23
+ const i = tok.indexOf('=');
24
+ args.push(tok.slice(0, i), tok.slice(i + 1));
25
+ }
26
+ else {
27
+ args.push(tok);
28
+ }
29
+ }
30
+ const out = {};
31
+ for (let i = 0; i < args.length; i++) {
32
+ const a = args[i];
33
+ if (a === '--id')
34
+ out.id = args[++i];
35
+ else if (a === '--title')
36
+ out.title = args[++i];
37
+ else if (a === '--mechanic')
38
+ out.mechanic = args[++i];
39
+ else if (a === '--grid') {
40
+ const [c, r] = args[++i].split('x').map(Number);
41
+ out.grid = { cols: c, rows: r };
42
+ }
43
+ else if (a === '--cascades')
44
+ out.cascades = true;
45
+ else if (a === '--no-cascades')
46
+ out.cascades = false;
47
+ else if (a === '--stake')
48
+ out.stake = true;
49
+ else if (a === '--no-stake')
50
+ out.stake = false;
51
+ }
52
+ return out;
53
+ }
54
+ function applyDefaults(partial) {
55
+ const mechanic = partial.mechanic ?? 'cluster';
56
+ return {
57
+ id: partial.id ?? '',
58
+ title: partial.title ?? titleCase(partial.id ?? ''),
59
+ mechanic,
60
+ grid: partial.grid ?? DEFAULT_GRID[mechanic],
61
+ stake: partial.stake ?? true,
62
+ cascades: partial.cascades ?? (mechanic !== 'lines'),
63
+ };
64
+ }
65
+ function validate(a) {
66
+ if (!/^[a-z][a-z0-9-]*$/.test(a.id))
67
+ throw new Error(`invalid id (must be kebab-case): "${a.id}"`);
68
+ if (!['lines', 'ways', 'cluster', 'anywhere', 'custom'].includes(a.mechanic))
69
+ throw new Error(`invalid mechanic: "${a.mechanic}"`);
70
+ if (a.grid.cols <= 0 || a.grid.rows <= 0)
71
+ throw new Error('grid dimensions must be > 0');
72
+ }
73
+ /** Flags that consume the next token as their value — used to skip those tokens during positional scan. */
74
+ const VALUE_FLAGS = new Set(['--id', '--title', '--mechanic', '--grid']);
75
+ /**
76
+ * Build an answers seed from argv: flags, plus a lone positional used as
77
+ * the id when --id is absent. Correctly skips tokens that are values of
78
+ * known value-taking flags (e.g. `--mechanic cluster` — `cluster` is NOT
79
+ * treated as a positional even when no --id is supplied).
80
+ */
81
+ function seedFromArgv(argv) {
82
+ // Normalize --flag=value to ['--flag', 'value'] for uniform processing
83
+ const args = [];
84
+ for (const tok of argv) {
85
+ if (tok.startsWith('--') && tok.includes('=')) {
86
+ const i = tok.indexOf('=');
87
+ args.push(tok.slice(0, i), tok.slice(i + 1));
88
+ }
89
+ else {
90
+ args.push(tok);
91
+ }
92
+ }
93
+ // Collect indices that are consumed as flag values so we don't
94
+ // accidentally treat them as positionals.
95
+ const valueIndices = new Set();
96
+ for (let i = 0; i < args.length; i++) {
97
+ if (VALUE_FLAGS.has(args[i]) && i + 1 < args.length) {
98
+ valueIndices.add(i + 1);
99
+ }
100
+ }
101
+ const flags = parseFlags(argv);
102
+ if (!flags.id) {
103
+ const positional = args.find((a, idx) => !a.startsWith('--') && !valueIndices.has(idx));
104
+ if (positional)
105
+ flags.id = positional;
106
+ }
107
+ return flags;
108
+ }
109
+
110
+ /** Ask the 5 questions interactively, applying defaults for blank answers. */
111
+ async function prompt(seed) {
112
+ const rl = createInterface({ input: stdin, output: stdout });
113
+ try {
114
+ const id = seed.id ?? (await rl.question('Game id (kebab-case): ')).trim();
115
+ const titleInput = (await rl.question(`Title [${applyDefaults({ id }).title}]: `)).trim();
116
+ const title = seed.title ?? (titleInput || undefined);
117
+ const mechanic = (seed.mechanic ?? ((await rl.question('Mechanic (lines|ways|cluster|anywhere|custom) [cluster]: ')).trim() || 'cluster'));
118
+ const gridStr = (await rl.question('Grid colsxrows [default for mechanic]: ')).trim();
119
+ const grid = seed.grid ?? (gridStr ? { cols: Number(gridStr.split('x')[0]), rows: Number(gridStr.split('x')[1]) } : undefined);
120
+ const stakeAns = seed.stake ?? ((await rl.question('Stake integration? (Y/n): ')).trim().toLowerCase() !== 'n');
121
+ return applyDefaults({ id, title, mechanic, grid, stake: stakeAns });
122
+ }
123
+ finally {
124
+ rl.close();
125
+ }
126
+ }
127
+
128
+ /** Emit a game.spec.ts with a sensible default symbol set + actions; author edits it. */
129
+ function genGameSpec(a) {
130
+ return `import { defineGame, type GameSpec } from '@energy8platform/platform-core/game-spec';
131
+
132
+ // Single source of truth. Edit symbols / paytable / bet levels / actions to design your game.
133
+ export const spec: GameSpec = {
134
+ id: '${a.id}',
135
+ type: 'slot',
136
+ mechanic: '${a.mechanic}',
137
+ grid: { cols: ${a.grid.cols}, rows: ${a.grid.rows} },
138
+ betLevels: [0.01, 0.1, 0.2, 0.5, 1, 2, 5, 10, 20, 50, 100, 200, 500, 1000, 10000, 100000, 1000000],
139
+ defaultBet: 1,
140
+ maxWin: 5000,
141
+ currency: 'EUR',
142
+ symbols: [
143
+ { id: 'H1', name: 'High 1', kind: 'high', pay: { 3: 10, 4: 25, 5: 100 } },
144
+ { id: 'H2', name: 'High 2', kind: 'high', pay: { 3: 8, 4: 20, 5: 80 } },
145
+ { id: 'H3', name: 'High 3', kind: 'high', pay: { 3: 6, 4: 15, 5: 60 } },
146
+ { id: 'H4', name: 'High 4', kind: 'high', pay: { 3: 5, 4: 12, 5: 50 } },
147
+ { id: 'L1', name: 'Low 1', kind: 'low', pay: { 3: 1, 4: 2, 5: 5 } },
148
+ { id: 'L2', name: 'Low 2', kind: 'low', pay: { 3: 0.8, 4: 1.5, 5: 4 } },
149
+ { id: 'L3', name: 'Low 3', kind: 'low', pay: { 3: 0.6, 4: 1.2, 5: 3 } },
150
+ { id: 'L4', name: 'Low 4', kind: 'low', pay: { 3: 0.5, 4: 1, 5: 2.5 } },
151
+ { id: 'WILD', name: 'Wild', kind: 'wild' },
152
+ { id: 'SCATTER', name: 'Scatter', kind: 'scatter' },
153
+ ],
154
+ // All player-facing spec copy is socialized automatically in social mode — symbol names shown
155
+ // in the paytable AND the action title/description below (e.g. 'BUY BONUS' -> 'GET BONUS',
156
+ // 'Pay more...' -> 'Win more...'). Write normal casino wording here; it stays compliant in social.
157
+ //
158
+ // Modes are declared ONCE here. \`rtp\` (target RTP, 0..1) and \`maxWin\` (per-mode cap; defaults to
159
+ // the game-level maxWin) feed the Game Info per-mode table automatically. NOTE: these are the
160
+ // DECLARED/displayed values — the math pipeline's targets live in math.config.ts and may differ
161
+ // (e.g. while tuning). Keep the declared values honest against the published math.
162
+ actions: {
163
+ spin: { role: 'base', rtp: 0.96 },
164
+ ante: { role: 'feature', cost: 1.5, rtp: 0.96, title: 'ANTE BET', description: 'Pay more for a boosted chance' },
165
+ free_spin: { role: 'free' },
166
+ buy_bonus: { role: 'buy', cost: 100, rtp: 0.96, maxWin: 5000, title: 'BUY BONUS', description: 'Buy the feature', feature: { spins: 10 } },
167
+ },
168
+ };
169
+
170
+ export const model = defineGame(spec);
171
+ `;
172
+ }
173
+
174
+ /** Generate CLAUDE.md for a scaffolded game — guidance for Claude Code (and humans) on how the
175
+ * project is wired, what to edit, and what the framework already handles. */
176
+ function genClaudeMd(a) {
177
+ const cascade = a.cascades === true;
178
+ return `# CLAUDE.md
179
+
180
+ Guidance for Claude Code (claude.ai/code) working in this repository — a slot game built on the
181
+ Energy8 \`@energy8platform/game-engine\` framework, targeting Stake Engine.
182
+
183
+ ## What this project is
184
+
185
+ \`${a.id}\` — a PixiJS slot game. The framework (\`@energy8platform/*\`) owns the boot sequence, the
186
+ Stake RGS bridge, the shell (control bar / modals / Game Info), the play loop, and the math
187
+ pipeline. This repo contains only the GAME: its spec, math logic, rendering, and assets.
188
+
189
+ ## Single source of truth: \`src/game.spec.ts\`
190
+
191
+ \`defineGame(spec)\` derives everything from one file: the Lua \`GameDefinition\`, the \`modeMap\`
192
+ (action → Stake mode), the math modes, the paytable, AND the shell's buy-bonus cards + Game Info
193
+ per-mode table. **Edit modes/symbols/bet levels/RTP/maxWin there — never duplicate them elsewhere.**
194
+ Each \`actions[key]\` carries \`role\` (base/feature/buy/free), \`cost\`, \`rtp\`, \`maxWin\`, \`title\`,
195
+ \`description\`, \`feature\`. \`rtp\`/\`maxWin\` are the DECLARED (displayed) values; the math pipeline's
196
+ TARGETS live in \`math.config.ts\` and may legitimately differ while tuning.
197
+
198
+ ## The scene contract (\`src/scenes/GameScene.ts\`)
199
+
200
+ The host owns the entire play loop (play → present → ack → drain). The scene only RENDERS:
201
+ - \`present(result, ctx)\` — draw ONE segment (a spin, or one free spin). All pacing lives here.
202
+ - \`onBonusEnter(trigger, ctx)\` / \`onBonusExit(last, ctx)\` — optional bonus intro / summary.
203
+
204
+ \`ctx\` = \`{ bet, action, mode, formatAmount(v), turbo }\`. The scene NEVER calls play/ack/roundId,
205
+ never touches the balance/win readouts (the host does, post-present), and never runs the FS loop —
206
+ a bonus is one round the host drains segment-by-segment.${cascade ? '\n\nThis game uses a CASCADE mechanic: `present` runs `result.steps` through the CascadeController and reflects `result.multiplier`.' : ''}
207
+
208
+ ## Commands
209
+
210
+ \`\`\`bash
211
+ npm run dev # local dev via Energy8 DevBridge (config + Lua)
212
+ npm run stake # the Stake dev harness — iframe wrapper + dev-RGS backed by curated books
213
+ npm run build:stake # the Stake frontend build → dist-stake/ (base './', no DevBridge)
214
+ npm run math:pool # Stage A — honest large simulation (the pool); see math.config.ts
215
+ npm run math:curate # Stage B — compress the pool into the publishable stake-math/ bundle
216
+ \`\`\`
217
+
218
+ ## Math pipeline
219
+
220
+ \`math.config.ts\` declares per-mode sim + curate tuning (iterations, CV, hit-rate, nRowsOut,
221
+ tolerances). The pipeline (\`e8-math\`) runs the native Go simulator, builds a pool, then curates a
222
+ ~Stake-canonical book bundle (\`stake-math/\`: \`books_<MODE>.jsonl.zst\` + \`lookUpTable_<MODE>_0.csv\`
223
+ + \`index.json\`). Book events are canonical \`{ type, spin }\`. The harness replays from these books.
224
+
225
+ ## What the framework already handles — do NOT reimplement
226
+
227
+ Segment-drain of bonuses, \`roundId\` forwarding, free-spins counter (with retriggers), resume of an
228
+ unfinished round, win/balance HUD timing, social-mode vocabulary, the legal disclaimer, currency
229
+ formatting, jurisdiction → feature restrictions, the insufficient-funds guard, the play-error modal
230
+ + reconnect overlay, autoplay, the bet ladder + default bet from \`/wallet/authenticate\`, the
231
+ open-redirect \`rgs_url\` guard, and spacebar handling. Write player-facing copy normally — it is
232
+ socialized automatically in social mode.
233
+
234
+ ## Conventions
235
+
236
+ - Keep \`game.spec.ts\` the source of truth; regenerate / reason from it.
237
+ - The math TARGETS (math.config.ts) and the DECLARED rtp/maxWin (game.spec) are separate — keep the
238
+ declared values honest against the published math before submitting.
239
+ - Tests: \`npm test\` (vitest). Typecheck: \`npx tsc --noEmit\`.
240
+ `;
241
+ }
242
+
243
+ function genPackageJson(a, v) {
244
+ const scripts = {
245
+ dev: 'vite',
246
+ build: 'tsc --noEmit && vite build',
247
+ postbuild: `rm -f ${a.id}.zip && cd dist && zip -r ../${a.id}.zip .`,
248
+ typecheck: 'tsc --noEmit',
249
+ smoke: 'tsx smoke.ts',
250
+ sim: 'e8-math sim --config ./math.config.ts',
251
+ pool: 'e8-math pool --config ./math.config.ts',
252
+ curate: 'e8-math curate --config ./math.config.ts',
253
+ math: 'e8-math all --config ./math.config.ts',
254
+ };
255
+ if (a.stake) {
256
+ scripts['dev:stake'] = 'BUILD_TARGET=stake vite';
257
+ scripts['build:stake'] = 'BUILD_TARGET=stake vite build';
258
+ scripts['stake'] = 'BUILD_TARGET=stake-harness vite';
259
+ scripts['stake:bundle'] =
260
+ `rm -rf dist-stake stake-math ${a.id}-stake.zip stake-math.zip && npm run build:stake && npm run math && cd dist-stake && zip -r ../${a.id}-stake.zip . && cd ../stake-math && zip -r ../stake-math.zip . && cd .. && echo 'Stake artifacts: ${a.id}-stake.zip + stake-math.zip'`;
261
+ }
262
+ const pkg = {
263
+ name: a.id,
264
+ private: true,
265
+ type: 'module',
266
+ scripts,
267
+ dependencies: {
268
+ '@energy8platform/platform-core': v['platform-core'],
269
+ '@energy8platform/game-engine': v['game-engine'],
270
+ ...(a.stake ? { '@energy8platform/stake-kit': v['stake-kit'], '@energy8platform/stake-bridge': v['stake-bridge'] } : { '@energy8platform/stake-kit': v['stake-kit'] }),
271
+ 'pixi.js': '^8.16.0',
272
+ zod: '^3.23.0',
273
+ },
274
+ devDependencies: {
275
+ '@energy8platform/stake-math-tools': v['stake-math-tools'],
276
+ '@types/node': '^20.0.0',
277
+ tsx: '^4.21.0',
278
+ typescript: '^5.6.0',
279
+ vite: '^6.0.0',
280
+ },
281
+ };
282
+ return JSON.stringify(pkg, null, 2) + '\n';
283
+ }
284
+
285
+ function genGameScene(a) {
286
+ const cascade = a.cascades === true;
287
+ const ctrl = cascade ? 'CascadeController' : 'ReelSpinController';
288
+ const present = cascade
289
+ ? ` /** Render one normalized result. Tune MultiplierAccumulator policy/reset() to your mechanic. */
290
+ async present(result: SpinData, ctx: RenderContext): Promise<void> {
291
+ const turbo = ctx.turbo > 0;
292
+ if (typeof result.multiplier === 'number') this.multiplier.set(result.multiplier);
293
+ for (const step of result.steps) await this.controller.run(step, { turbo });
294
+ if (result.totalWin > 0) await this.overlay.show(result.totalWin, ctx.bet, ctx.formatAmount);
295
+ }`
296
+ : ` /** Render one normalized result (one spin, or one free spin of a bonus). */
297
+ async present(result: SpinData, ctx: RenderContext): Promise<void> {
298
+ const turbo = ctx.turbo > 0;
299
+ await this.controller.run({ targetGrid: result.targetGrid }, { turbo });
300
+ if (result.totalWin > 0) await this.overlay.show(result.totalWin, ctx.bet, ctx.formatAmount);
301
+ }`;
302
+ const multiplierImport = cascade ? ', MultiplierAccumulator' : '';
303
+ const multiplierField = cascade
304
+ ? ` private readonly multiplier = new MultiplierAccumulator({ policy: 'session' });\n` : '';
305
+ return `import { Scene } from '@energy8platform/game-engine/core';
306
+ import { ReelGrid, ${ctrl}, BigWinOverlay${multiplierImport} } from '@energy8platform/game-engine/slot';
307
+ import type { SlotSceneController, RenderContext } from '@energy8platform/game-engine/host';
308
+ import { model } from '../game.spec';
309
+ import { resolveSymbol } from '../slot/symbols';
310
+ import type { SpinData } from '../game/normalize';
311
+
312
+ /**
313
+ * The host owns the play loop (play -> present -> ack -> drain). This scene only RENDERS:
314
+ * - present(result, ctx): draw ONE segment (a spin, or one free spin). Put all pacing here.
315
+ * - onBonusEnter(trigger, ctx): fires right before the first free spin (bonus intro).
316
+ * - onBonusExit(last, ctx): fires after the last free spin (bonus summary).
317
+ * ctx gives you { bet, action, mode, formatAmount(value), turbo } — turbo is live (0..3).
318
+ */
319
+ export class GameScene extends Scene implements SlotSceneController<SpinData> {
320
+ private grid!: ReelGrid;
321
+ private controller!: ${ctrl};
322
+ private overlay!: BigWinOverlay;
323
+ ${multiplierField}
324
+ private _vw = 1920;
325
+ private _vh = 1080;
326
+
327
+ async onEnter(): Promise<void> {
328
+ const { cols, rows } = model.spec.grid;
329
+ this.grid = new ReelGrid({ cols, rows, cellSize: 110, gap: 6, resolve: resolveSymbol });
330
+ this.container.addChild(this.grid);
331
+ this.controller = new ${ctrl}(this.grid);
332
+ this.overlay = new BigWinOverlay({
333
+ tiers: [
334
+ { id: 'big', minMultiplier: 10, title: 'BIG WIN', accentColor: 0xffd24a },
335
+ { id: 'mega', minMultiplier: 50, title: 'MEGA WIN', accentColor: 0x7ad7ff },
336
+ ],
337
+ formatMoney: (v) => v.toFixed(2),
338
+ width: 1920, height: 1080,
339
+ });
340
+ this.container.addChild(this.overlay);
341
+ this.layout(this._vw, this._vh);
342
+ }
343
+
344
+ ${present}
345
+
346
+ /** Bonus starting — show an intro. trigger.freeSpins?.total = how many free spins were awarded. */
347
+ async onBonusEnter(trigger: SpinData, _ctx: RenderContext): Promise<void> {
348
+ // TODO: show a bonus intro (e.g. "10 FREE SPINS"). Defaults to nothing.
349
+ void trigger;
350
+ }
351
+
352
+ /** Bonus finished — show a summary. ctx.formatAmount(last.totalWin) = the bonus total win. */
353
+ async onBonusExit(last: SpinData, ctx: RenderContext): Promise<void> {
354
+ // TODO: show a bonus summary. Defaults to nothing.
355
+ void last; void ctx;
356
+ }
357
+
358
+ onResize(width: number, height: number): void {
359
+ this._vw = width;
360
+ this._vh = height;
361
+ this.layout(width, height);
362
+ }
363
+
364
+ private layout(w: number, h: number): void {
365
+ this._vw = w; this._vh = h;
366
+ if (!this.grid) return;
367
+ const cols = model.spec.grid.cols, rows = model.spec.grid.rows;
368
+ const cellSize = 110, gap = 6; // must match the ReelGrid constructor above
369
+ const gridW = cols * cellSize + (cols - 1) * gap;
370
+ const gridH = rows * cellSize + (rows - 1) * gap;
371
+ const fit = Math.min((w * 0.92) / gridW, (h * 0.78) / gridH);
372
+ this.grid.scale.set(fit);
373
+ this.grid.x = Math.round((w - gridW * fit) / 2);
374
+ this.grid.y = Math.round((h - gridH * fit) / 2);
375
+ this.overlay?.resize?.(w, h);
376
+ }
377
+ }
378
+ `;
379
+ }
380
+
381
+ function genLuaLogic(a) {
382
+ const cascade = a.cascades === true;
383
+ const ret = cascade
384
+ ? ` return {
385
+ total_win = win, -- bet-multiplier; the platform multiplies by the actual bet
386
+ cascades = {}, -- TODO: emit cascade steps { winning, removed, new, grid }
387
+ free_spins = free_spins_result,
388
+ -- The engine reads these VARIABLES (not the nested free_spins table) to open / retrigger the
389
+ -- free-spins SESSION. Without them, free_spin would fail "requires an active session".
390
+ variables = { free_spins_awarded = fs_awarded, retrigger_spins = retrigger_awarded },
391
+ }`
392
+ : ` return {
393
+ total_win = win, -- bet-multiplier
394
+ matrix = grid, -- 2D array of SYM.* ids
395
+ wins = {}, -- TODO: emit line/way wins
396
+ free_spins = free_spins_result,
397
+ -- The engine reads these VARIABLES (not the nested free_spins table) to open / retrigger the
398
+ -- free-spins SESSION. Without them, free_spin would fail "requires an active session".
399
+ variables = { free_spins_awarded = fs_awarded, retrigger_spins = retrigger_awarded },
400
+ }`;
401
+ return `-- Game logic. The spec-derived prelude (SPEC/SYMBOLS/SYM/PAYTABLE) is injected above this file.
402
+ -- Reel weights / RTP tuning live here. Implement your mechanic; return a bet-multiplier in total_win.
403
+ function execute(state)
404
+ -- state.action: 'spin' | 'ante' | 'buy_bonus' | 'free_spin'
405
+ -- state.bet, state.action_config.feature_data
406
+ local action = state.action or 'spin'
407
+ local grid = {}
408
+ for c = 1, SPEC.cols do
409
+ grid[c] = {}
410
+ for r = 1, SPEC.rows do
411
+ grid[c][r] = engine.random(1, #SYMBOLS)
412
+ end
413
+ end
414
+
415
+ -- Placeholder random payout (replace with your real mechanic).
416
+ local win = 0
417
+ local free_spins_result = nil
418
+ local fs_awarded = 0 -- engine VARIABLE: spins this round awards (opens the session)
419
+ local retrigger_awarded = 0 -- engine VARIABLE: extra spins a free_spin retrigger awards
420
+
421
+ if action == 'buy_bonus' then
422
+ -- Player bought free spins: always award them, no base win.
423
+ fs_awarded = 10
424
+ free_spins_result = { awarded = fs_awarded, total = fs_awarded }
425
+
426
+ elseif action == 'free_spin' then
427
+ -- Free-spin round: random payout + small retrigger chance (~5%).
428
+ local roll = engine.random(1, 1000)
429
+ if roll <= 400 then -- ~40% small win (higher hit-rate in bonus)
430
+ win = engine.random(1, 8) * 0.3
431
+ elseif roll <= 440 then -- ~4% medium win
432
+ win = engine.random(10, 50)
433
+ elseif roll <= 442 then -- ~0.2% large win
434
+ win = engine.random(100, 800)
435
+ end
436
+ -- Retrigger: ~5% chance to award extra spins (read via retrigger_spins).
437
+ local retrigger_roll = engine.random(1, 100)
438
+ if retrigger_roll <= 5 then
439
+ retrigger_awarded = 5
440
+ free_spins_result = { awarded = retrigger_awarded, total = retrigger_awarded }
441
+ end
442
+
443
+ elseif action == 'ante' then
444
+ -- Ante (paid-boost) spin: same payouts as base but higher free-spins trigger (~5%).
445
+ local roll = engine.random(1, 1000)
446
+ if roll <= 250 then -- ~25% small win
447
+ win = engine.random(1, 5) * 0.2
448
+ elseif roll <= 270 then -- ~2% medium win
449
+ win = engine.random(5, 30)
450
+ elseif roll <= 272 then -- ~0.2% large win
451
+ win = engine.random(50, 500)
452
+ end
453
+ local fs_roll = engine.random(1, 100)
454
+ if fs_roll <= 5 then -- ~5% free-spins trigger
455
+ fs_awarded = 8
456
+ free_spins_result = { awarded = fs_awarded, total = fs_awarded }
457
+ end
458
+
459
+ else
460
+ -- Base spin ('spin'): random payout + small free-spins trigger (~2%).
461
+ local roll = engine.random(1, 1000)
462
+ if roll <= 250 then -- ~25% small win
463
+ win = engine.random(1, 5) * 0.2
464
+ elseif roll <= 270 then -- ~2% medium win
465
+ win = engine.random(5, 30)
466
+ elseif roll <= 272 then -- ~0.2% large win
467
+ win = engine.random(50, 500)
468
+ end
469
+ local fs_roll = engine.random(1, 100)
470
+ if fs_roll <= 2 then -- ~2% free-spins trigger
471
+ fs_awarded = 8
472
+ free_spins_result = { awarded = fs_awarded, total = fs_awarded }
473
+ end
474
+ end
475
+
476
+ ${ret}
477
+ end
478
+ `;
479
+ }
480
+
481
+ /** Generate src/stake/adapter.ts. The spin schema is the shared src/game/schema.ts (one schema for all games). */
482
+ function genStakeAdapter(_a) {
483
+ const adapter = `import { createGameAdapter, type SegmentCore } from '@energy8platform/stake-kit';
484
+ import { model } from '../game.spec';
485
+ import { spinSchema, type SpinDataRaw } from '../game/schema';
486
+
487
+ export const adapter = createGameAdapter<SpinDataRaw>({
488
+ model,
489
+ schema: spinSchema,
490
+ segmentOf: ({ event, payload, round }) => {
491
+ // Canonical Stake book events are { type, spin }; a free spin is type 'free_spin'.
492
+ const isFs = (event as { type?: string }).type === 'free_spin';
493
+ const core: SegmentCore<SpinDataRaw> = {
494
+ action: isFs ? 'free_spin' : round.triggerAction,
495
+ winX: payload.total_win ?? 0,
496
+ session: { roundId: round.roundId },
497
+ };
498
+ const awarded = payload.free_spins?.awarded ?? 0;
499
+ if (!isFs && awarded > 0) {
500
+ core.bonusFreeSpin = { grantId: 1, remainingSpins: awarded };
501
+ }
502
+ return core;
503
+ },
504
+ });
505
+
506
+ export default adapter;
507
+ `;
508
+ return { adapter };
509
+ }
510
+
511
+ function genMainTs(a) {
512
+ const stakeImport = a.stake ? `import adapter from './stake/adapter';\n` : '';
513
+ const stakeOpt = a.stake ? ` stake: { adapter },\n` : '';
514
+ return `import { createSlotGame } from '@energy8platform/game-engine/host';
515
+ import { ScaleMode } from '@energy8platform/game-engine';
516
+ import { model } from './game.spec';
517
+ import { GameScene } from './scenes/GameScene';
518
+ import { IntroScene } from './scenes/IntroScene';
519
+ import { normalize } from './game/normalize';
520
+ ${stakeImport}
521
+ createSlotGame({
522
+ model,
523
+ normalize,
524
+ // Scenes in order — the first eligible one starts. 'intro' is skipped on a replay launch, so a
525
+ // replay opens directly on the game scene.
526
+ scenes: [
527
+ { key: 'intro', scene: IntroScene, skipOnReplay: true },
528
+ { key: 'game', scene: GameScene },
529
+ ],
530
+ manifest: { bundles: [] },
531
+ design: { width: 1920, height: 1080 },
532
+ scaleMode: ScaleMode.FILL,
533
+ fonts: ['400 24px "Inter"'],
534
+ textureDefaults: true,
535
+ dev: (import.meta as any).env?.DEV ?? false,
536
+ ${stakeOpt} shell: {
537
+ // buy/ante cards + currency derive from the spec + initData.
538
+ // Base game-info sections. Wrap player-facing copy in t(...) so restricted gambling words are
539
+ // rewritten to social-casino vocabulary in social mode (t is the identity otherwise). The
540
+ // built-in sections (max win, paytable, controls, disclaimer) and the spec's buy/ante card copy
541
+ // are socialized automatically; t() is how YOUR custom copy joins in.
542
+ gameInfo: (t) => ({
543
+ sections: [
544
+ {
545
+ type: 'custom',
546
+ title: t('How to Play'),
547
+ html: \`<p>\${t('Spin the reels and match symbols to win. Buy the bonus to trigger free spins instantly.')}</p>\`,
548
+ },
549
+ ],
550
+ }),
551
+ },
552
+ }).catch((err) => { console.error('[${a.id}] failed to start', err); });
553
+ `;
554
+ }
555
+
556
+ /** Generate src/scenes/IntroScene.ts: a real game-owned Scene subclass (edit freely).
557
+ * The host injects goto() via scene start data; call goto('game') to advance. */
558
+ function genIntroScene(a) {
559
+ return `import { Container, Graphics, Text } from 'pixi.js';
560
+ import { Scene } from '@energy8platform/game-engine/core';
561
+ import { Tween } from '@energy8platform/game-engine/animation';
562
+
563
+ /** Start data the host injects into every scene (goto navigates between scenes). */
564
+ interface IntroData { goto?: (key: string, data?: unknown) => void; }
565
+
566
+ /** Full intro scene for ${a.title}. Edit this freely — background, logo, buttons, animation. */
567
+ export class IntroScene extends Scene {
568
+ private layer?: Container;
569
+
570
+ async onEnter(data?: unknown): Promise<void> {
571
+ const { goto } = (data ?? {}) as IntroData;
572
+ const layer = new Container();
573
+ this.layer = layer;
574
+ this.container.addChild(layer);
575
+
576
+ // Background — replace with a texture sprite once you have art.
577
+ const bg = new Graphics().rect(0, 0, 1920, 1080).fill({ color: 0x0b0f1a });
578
+ layer.addChild(bg);
579
+
580
+ const title = new Text({ text: '${a.title}', style: { fill: 0xffffff, fontSize: 110, fontFamily: 'Inter', fontWeight: '700', align: 'center' } });
581
+ title.anchor.set(0.5); title.position.set(960, 420);
582
+ layer.addChild(title);
583
+
584
+ const subtitle = new Text({ text: 'Press PLAY to begin', style: { fill: 0x9fb3c8, fontSize: 34, fontFamily: 'Inter' } });
585
+ subtitle.anchor.set(0.5); subtitle.position.set(960, 520);
586
+ layer.addChild(subtitle);
587
+
588
+ // PLAY button (replace with the engine UI Button or your own art).
589
+ const btn = new Container(); btn.position.set(960, 660);
590
+ const bgBtn = new Graphics().roundRect(-150, -45, 300, 90, 16).fill({ color: 0xffd24a });
591
+ const label = new Text({ text: 'PLAY', style: { fill: 0x0b0f1a, fontSize: 40, fontFamily: 'Inter', fontWeight: '700' } });
592
+ label.anchor.set(0.5);
593
+ btn.addChild(bgBtn, label);
594
+ btn.eventMode = 'static'; btn.cursor = 'pointer';
595
+ btn.once('pointerdown', () => goto?.('game'));
596
+ layer.addChild(btn);
597
+
598
+ layer.alpha = 0;
599
+ await Tween.to(layer, { alpha: 1 }, 400);
600
+ }
601
+
602
+ onExit(): void {
603
+ this.layer?.destroy({ children: true });
604
+ this.layer = undefined;
605
+ }
606
+ }
607
+ `;
608
+ }
609
+
610
+ /** Generate src/game/normalize.ts: game-declared SpinData + the host normalizer.
611
+ * Coercion (Lua {} → []) is schema-driven via stake-kit's deriveArrayFields + coerceLuaArrays. */
612
+ function genNormalize(a) {
613
+ const cascade = a.cascades === true;
614
+ const dataShape = cascade
615
+ ? ` /** Cascade steps the scene animates via CascadeController. */
616
+ steps: CascadeStepData[];
617
+ /** Optional running multiplier the scene reflects. */
618
+ multiplier?: number;`
619
+ : ` /** Result grid by column for the reel spin. */
620
+ targetGrid: CellData[][];`;
621
+ const mapBody = cascade
622
+ ? ` steps: Array.isArray(d.cascades) ? d.cascades.map((step: any) => ({
623
+ winningCells: step.winning ?? [],
624
+ removedCells: step.removed ?? [],
625
+ newCells: step.new ?? [],
626
+ settledGrid: step.grid ?? [],
627
+ })) : [],
628
+ multiplier: d.multiplier,`
629
+ : ` targetGrid: d.matrix ?? [],`;
630
+ return `import type { SlotSpinResultBase, SlotResultNormalizer } from '@energy8platform/platform-core/slot-result';
631
+ import type { ${cascade ? 'CascadeStepData' : 'CellData'} } from '@energy8platform/game-engine/slot';
632
+ import { deriveArrayFields, coerceLuaArrays } from '@energy8platform/stake-kit';
633
+ import { spinSchema, type SpinDataRaw } from './schema';
634
+
635
+ /** The game's typed play result. Extend with any fields your script.logic.lua returns. */
636
+ export interface SpinData extends SlotSpinResultBase {
637
+ ${dataShape}
638
+ }
639
+
640
+ // Array fields are derived from the schema once (Lua empty tables {} → []), so the
641
+ // scene-facing mapping below can rely on real arrays — no crashes from Lua empty tables.
642
+ const arrayFields = deriveArrayFields(spinSchema);
643
+
644
+ /** REQUIRED: map the raw play result into SpinData. The host calls this on every play. */
645
+ export const normalize: SlotResultNormalizer<SpinData> = (raw) => {
646
+ const r = (raw ?? {}) as { totalWin?: number; data?: unknown };
647
+ const coerced = coerceLuaArrays((r.data ?? {}) as Record<string, unknown>, arrayFields);
648
+ const parsed = spinSchema.safeParse(coerced);
649
+ const d = (parsed.success ? parsed.data : coerced) as SpinDataRaw;
650
+ return {
651
+ totalWin: r.totalWin ?? 0,
652
+ freeSpins: d.free_spins ? { awarded: d.free_spins.awarded, total: d.free_spins.total } : undefined,
653
+ ${mapBody}
654
+ };
655
+ };
656
+ `;
657
+ }
658
+
659
+ /** Generate src/game/schema.ts: the zod schema for the inner Lua data table.
660
+ * Array fields are plain z.array(...) so stake-kit's deriveArrayFields() finds them
661
+ * and coerceLuaArrays() can turn Lua {} into [] (used by normalize AND the stake adapter). */
662
+ function genSchema(a) {
663
+ const cascade = a.cascades === true;
664
+ const arrayFields = cascade
665
+ ? ` cascades: z.array(z.object({}).passthrough()).optional(),`
666
+ : ` matrix: z.array(z.array(z.number())).optional(),
667
+ wins: z.array(z.object({}).passthrough()).optional(),`;
668
+ return `import { z } from 'zod';
669
+
670
+ /** The inner Lua "data" table your script.logic.lua returns (the engine nests it under result.data).
671
+ * Edit these fields to match your math. Array fields MUST stay z.array(...) so Lua {} coerces to []. */
672
+ export const spinSchema = z.object({
673
+ total_win: z.number().optional(),
674
+ ${arrayFields}
675
+ multiplier: z.number().optional(),
676
+ free_spins: z.object({ awarded: z.number(), total: z.number() }).optional(),
677
+ });
678
+ export type SpinDataRaw = z.infer<typeof spinSchema>;
679
+ `;
680
+ }
681
+
682
+ function genMathConfig(_a) {
683
+ return `import { readFileSync } from 'node:fs';
684
+ import { buildLuaScript } from '@energy8platform/platform-core/game-spec';
685
+ import { model } from './src/game.spec';
686
+ import type { MathConfig } from '@energy8platform/stake-math-tools';
687
+
688
+ // node-only (the e8-math CLI runs in node) — reads the Lua via node:fs, not Vite raw imports.
689
+ const logic = readFileSync(new URL('./src/game/script.logic.lua', import.meta.url), 'utf8');
690
+
691
+ export default {
692
+ model,
693
+ luaScript: buildLuaScript(model, logic),
694
+ // One block per Stake mode key. Tune sim iterations + curate targets per mode.
695
+ // Missing modes use seeded defaults (see resolveModes in stake-math-tools).
696
+ modes: {
697
+ BASE: {
698
+ sim: { iterations: 100_000, bet: 1, rng: 'provably-fair' },
699
+ curate: {
700
+ capMaxWin: model.spec.maxWin * 100, // cents (bet-multiplier × 100)
701
+ algorithm: 'tiered',
702
+ targetRTP: 0.96,
703
+ toleranceRTP: 0.01,
704
+ targetCV: 5,
705
+ toleranceCV: 2,
706
+ targetHitRate: 0.25,
707
+ toleranceHitRate: 0.05,
708
+ nRowsOut: 50_000, // keep nRowsOut < iterations
709
+ },
710
+ },
711
+ // Feature modes. Only sim.iterations is set by default — the remaining sim params and all
712
+ // curate targets fall back to seeded defaults (capMaxWin from spec.maxWin); tune as needed.
713
+ BUY_BONUS: { sim: { iterations: 100_000 } },
714
+ ANTE: { sim: { iterations: 100_000 } },
715
+ },
716
+ } satisfies MathConfig;
717
+ `;
718
+ }
719
+
720
+ const TEMPLATE_DIR = resolve(fileURLToPath(new URL('.', import.meta.url)), '../template');
721
+ function substituteTree(dir, vars) {
722
+ // NOTE: template is text-only. If binary placeholders are ever added under template/,
723
+ // add a file-extension allowlist here — readFileSync(p,'utf8') would corrupt them.
724
+ for (const name of readdirSync(dir)) {
725
+ const p = join(dir, name);
726
+ if (statSync(p).isDirectory()) {
727
+ substituteTree(p, vars);
728
+ continue;
729
+ }
730
+ let text = readFileSync(p, 'utf8');
731
+ for (const [k, val] of Object.entries(vars))
732
+ text = text.split('${' + k + '}').join(val);
733
+ writeFileSync(p, text);
734
+ }
735
+ }
736
+ async function generate(a, targetDir, versions) {
737
+ validate(a);
738
+ mkdirSync(targetDir, { recursive: true });
739
+ // 1) copy fixed template
740
+ cpSync(TEMPLATE_DIR, targetDir, { recursive: true });
741
+ // _gitignore → .gitignore
742
+ if (existsSync(join(targetDir, '_gitignore')))
743
+ renameSync(join(targetDir, '_gitignore'), join(targetDir, '.gitignore'));
744
+ // 2) substitute ${id}/${title}
745
+ substituteTree(targetDir, { id: a.id, title: a.title });
746
+ // 3) codegen files
747
+ mkdirSync(join(targetDir, 'src/game'), { recursive: true });
748
+ mkdirSync(join(targetDir, 'src/scenes'), { recursive: true });
749
+ writeFileSync(join(targetDir, 'src/game.spec.ts'), genGameSpec(a));
750
+ writeFileSync(join(targetDir, 'CLAUDE.md'), genClaudeMd(a));
751
+ writeFileSync(join(targetDir, 'package.json'), genPackageJson(a, versions));
752
+ writeFileSync(join(targetDir, 'math.config.ts'), genMathConfig());
753
+ writeFileSync(join(targetDir, 'src/scenes/GameScene.ts'), genGameScene(a));
754
+ writeFileSync(join(targetDir, 'src/main.ts'), genMainTs(a));
755
+ writeFileSync(join(targetDir, 'src/scenes/IntroScene.ts'), genIntroScene(a));
756
+ writeFileSync(join(targetDir, 'src/game/script.logic.lua'), genLuaLogic(a));
757
+ writeFileSync(join(targetDir, 'src/game/normalize.ts'), genNormalize(a));
758
+ writeFileSync(join(targetDir, 'src/game/schema.ts'), genSchema(a));
759
+ if (a.stake) {
760
+ mkdirSync(join(targetDir, 'src/stake'), { recursive: true });
761
+ const { adapter } = genStakeAdapter();
762
+ writeFileSync(join(targetDir, 'src/stake/adapter.ts'), adapter);
763
+ }
764
+ else if (existsSync(join(targetDir, 'src/stake'))) {
765
+ rmSync(join(targetDir, 'src/stake'), { recursive: true, force: true });
766
+ }
767
+ }
768
+
769
+ // Dependency versions written into a scaffolded game's package.json. Keep in lock-step with the
770
+ // published @energy8platform/* versions (a create-slot test asserts these match the workspace).
771
+ const PUBLISHED = {
772
+ 'platform-core': '^0.25.0', 'game-engine': '^0.18.0', 'stake-kit': '^0.2.0', 'stake-bridge': '^0.3.0',
773
+ 'stake-math-tools': '^0.8.0',
774
+ };
775
+ async function main() {
776
+ const seed = seedFromArgv(argv.slice(2));
777
+ const yes = argv.includes('--yes');
778
+ const answers = yes ? applyDefaults(seed) : await prompt(seed);
779
+ const dir = resolve(process.cwd(), answers.id);
780
+ await generate(answers, dir, PUBLISHED);
781
+ console.log(`\n✓ Created ${answers.id} at ${dir}\n cd ${answers.id} && npm install && npm run dev\n`);
782
+ }
783
+ main().catch((err) => { console.error(err.message); exit(1); });
784
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sources":["../src/answers.ts","../src/prompts.ts","../src/codegen/gameSpec.ts","../src/codegen/claudeMd.ts","../src/codegen/packageJson.ts","../src/codegen/gameScene.ts","../src/codegen/luaLogic.ts","../src/codegen/stakeAdapter.ts","../src/codegen/mainTs.ts","../src/codegen/introScene.ts","../src/codegen/normalize.ts","../src/codegen/schema.ts","../src/codegen/mathConfig.ts","../src/generate.ts","../src/cli.ts"],"sourcesContent":[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "@energy8platform/create-slot",
3
+ "version": "0.2.0",
4
+ "type": "module",
5
+ "bin": { "create-slot": "dist/cli.js" },
6
+ "files": ["dist", "template"],
7
+ "scripts": {
8
+ "build": "rollup -c rollup.config.ts --configPlugin @rollup/plugin-typescript",
9
+ "test": "vitest run",
10
+ "typecheck": "tsc --noEmit"
11
+ },
12
+ "devDependencies": {
13
+ "@rollup/plugin-typescript": "^12.1.0",
14
+ "@types/node": "^20.0.0",
15
+ "rollup": "^4.24.0",
16
+ "tslib": "^2.8.0",
17
+ "typescript": "^5.6.0",
18
+ "vitest": "^2.0.0"
19
+ }
20
+ }
@@ -0,0 +1,15 @@
1
+ # ${title}
2
+
3
+ Generated with `npm create @energy8platform/slot`. Built on @energy8platform game-spec / host / stake-kit / slot.
4
+
5
+ ## Develop
6
+ - `npm install`
7
+ - `npm run dev` — runs the game in a browser (Vite + in-process DevBridge running your Lua)
8
+ - Edit `src/game.spec.ts` (symbols/paytable/bet levels/actions) and `src/game/script.logic.lua` (math).
9
+ - Swap placeholder art in `public/assets/` (see NAMING.md) and wire it in `src/slot/symbols.ts`.
10
+
11
+ ## Verify
12
+ - `npm run typecheck`
13
+ - `npm run smoke` — proves spec → export artifacts
14
+
15
+ See the `slot-game-creator` skill for the full mechanics → math → art → UI workflow.
@@ -0,0 +1,3 @@
1
+ node_modules
2
+ dist
3
+ dist-stake
@@ -0,0 +1,13 @@
1
+ import { buildLuaScript } from '@energy8platform/platform-core/game-spec';
2
+ import { model } from './src/game.spec';
3
+ import logic from './src/game/script.logic.lua?raw';
4
+
5
+ export default {
6
+ balance: 100000,
7
+ currency: model.spec.currency ?? 'EUR',
8
+ networkDelay: 80,
9
+ debug: true,
10
+ gameDefinition: model.gameDefinition,
11
+ luaScript: buildLuaScript(model, logic),
12
+ luaSeed: 12345,
13
+ };
@@ -0,0 +1,17 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
6
+ <title>${title}</title>
7
+ <style>
8
+ html, body { margin: 0; padding: 0; height: 100%; overflow: hidden; background: #0a0a12; }
9
+ #game { position: fixed; inset: 0; }
10
+ canvas { display: block; }
11
+ </style>
12
+ </head>
13
+ <body>
14
+ <div id="game"></div>
15
+ <script type="module" src="/src/main.ts"></script>
16
+ </body>
17
+ </html>
@@ -0,0 +1,3 @@
1
+ # Asset naming
2
+ Name each symbol sprite after its spec symbol id, lowercased: `h1.webp`, `wild.webp`, `scatter.webp`.
3
+ Backgrounds: `bg-base.webp`, `bg-fs.webp`. Audio: `bgm-base.mp3`, `sfx-spin.mp3`. VFX: spritesheets per effect.
@@ -0,0 +1,3 @@
1
+ # Asset naming
2
+ Name each symbol sprite after its spec symbol id, lowercased: `h1.webp`, `wild.webp`, `scatter.webp`.
3
+ Backgrounds: `bg-base.webp`, `bg-fs.webp`. Audio: `bgm-base.mp3`, `sfx-spin.mp3`. VFX: spritesheets per effect.
@@ -0,0 +1,3 @@
1
+ # Asset naming
2
+ Name each symbol sprite after its spec symbol id, lowercased: `h1.webp`, `wild.webp`, `scatter.webp`.
3
+ Backgrounds: `bg-base.webp`, `bg-fs.webp`. Audio: `bgm-base.mp3`, `sfx-spin.mp3`. VFX: spritesheets per effect.
@@ -0,0 +1,3 @@
1
+ # Asset naming
2
+ Name each symbol sprite after its spec symbol id, lowercased: `h1.webp`, `wild.webp`, `scatter.webp`.
3
+ Backgrounds: `bg-base.webp`, `bg-fs.webp`. Audio: `bgm-base.mp3`, `sfx-spin.mp3`. VFX: spritesheets per effect.
@@ -0,0 +1,6 @@
1
+ import { Texture } from 'pixi.js';
2
+ import { AnimatedSymbol, type SymbolResolver } from '@energy8platform/game-engine/slot';
3
+
4
+ // Placeholder resolver: every symbol is a blank tile. Swap in real textures from public/assets/symbols.
5
+ export const resolveSymbol: SymbolResolver = (id: string) =>
6
+ new AnimatedSymbol({ textures: { base: id ? Texture.WHITE : Texture.EMPTY }, size: 110 });
@@ -0,0 +1,3 @@
1
+ export const DESIGN_W = 1920;
2
+ export const DESIGN_H = 1080;
3
+ export const COLORS = { bg: 0x0a0a12, accent: 0xffd24a };
@@ -0,0 +1,13 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "Bundler",
6
+ "lib": ["ES2022", "DOM"],
7
+ "strict": true,
8
+ "noEmit": true,
9
+ "skipLibCheck": true,
10
+ "types": ["node"]
11
+ },
12
+ "include": ["src/**/*.ts"]
13
+ }
@@ -0,0 +1,19 @@
1
+ import { defineGameConfig } from '@energy8platform/game-engine/vite';
2
+ import { stakeHarnessPlugin } from '@energy8platform/stake-kit/harness';
3
+
4
+ const target = process.env.BUILD_TARGET;
5
+ const isStake = target === 'stake';
6
+ const isHarness = target === 'stake-harness';
7
+
8
+ export default defineGameConfig({
9
+ base: './',
10
+ // Stake builds and harness run inside the Stake RGS shell — no local DevBridge.
11
+ devBridge: !isStake && !isHarness,
12
+ devBridgeConfig: './dev.config',
13
+ vite: {
14
+ server: { port: 5173 },
15
+ optimizeDeps: { include: ['pixi.js'], exclude: ['fengari'] },
16
+ ...(isStake ? { build: { outDir: 'dist-stake' } } : {}),
17
+ ...(isHarness ? { plugins: [stakeHarnessPlugin({ config: './math.config.ts', booksDir: 'stake-math' })] } : {}),
18
+ },
19
+ });