SLOT ENGINE

Specification · algorithms · administration · development guide

Next.js 15 + TS strict Pixi 7 legacy (canvas-fallback) PostgreSQL 16 + Prisma RTP target 95% reference 66 unit · 25 e2e
⚠️ Demo/social mode only. All balances are virtual credits. No deposits, withdrawals, payments or crypto in the code and never will be (legal boundary of the project, see SECURITY.md). 18+

01Quick start and links

🌐 Pages

URLWhat is this
/Lobby: theme cards, 🏆 daily leaders, 💎 jackpot
/demoGame, default theme — Wild Patrik (classic)
/demo?theme=centavraCentavra — sci-fi theme with cascades
ℹ️ Centavra here — second theme of the same engine (catalog themes/centavra/: own art + own math v2.0.0), not a separate project. Do not confuse with the eponymous project centavra-mission-spin (bottle/, ports :3005/:5002) — that one is not related to slot-engine.
/adminAdmin panel (password, see section 02)
/spec.htmlThis document
/production.htmlProduction & promo: standards for ad assets, briefs for generation, roadmap
/investors.htmlFor investors: two-model pipeline (Opus 4.8 × Fable 5), who built what, remaining Roadmap
/strategy.htmlExit strategy: market, monetization (USDT vs license), blockchain/provably-fair, geography, phased de-risking
/investor-deck.htmlInvestor deck: 3 screens 16:9 (PDF: /slot-engine-investor-deck.pdf)

🖥 Environments

Prod (monitored by operator)
http://127.0.0.1:3006 — pm2 slot-engine, catalog .next
Dev / e2e
http://127.0.0.1:3007 — npm run dev, catalog .next-dev
DB
PostgreSQL 16, database slot_engine (password in .env)
Secrets
.env — outside git; do not read, do not print

Never occupy :3006 dev-server and do not delete .next while pm2 is running. Restart of production — pm2 restart slot-engine only after npm run build.

⌨️ Commands

cd slot-engine
npm run dev          # dev server :3007
npm run ci           # lint + 66 unit + production build (green = can commit)
npm run e2e          # 25 Playwright tests (auto-starts dev server)
npm run build && pm2 restart slot-engine     # deploy on :3006

npm run simulate:rtp -- --config wild-patrik --spins 1000000 --seed 777          # RTP report
npm run simulate:rtp -- --config centavra --spins 200000 --seed 777 --force-bonus # bonus price
npm run validate:assets -- --theme wild-patrik                                    # asset contract

02Admin panel

Login: open /admin → redirect to /admin/login → enter password. Password is stored in the variable ADMIN_PASSWORD file .env on the server (not in this document or in git — it's a secret). After login, a signed HMAC cookie is set; middleware closes all /admin/* and /api/math/*; if the password is not set in .env — admin panel responds with 503.

SectionWhat it can do
/adminDashboard: daily statistics (spins, bets, wins, actual RTP), top wins
/admin/roundsAll rounds with pagination: bet, win, scatters, FS, cap
/admin/rounds/[id]Dispute resolution: full round outcome with grid images — base window, each line, free spin chain, cascades
/admin/simulationsRTP simulation history + «Run simulation» button (theme + number of spins, live result)
/admin/configsMath versions of themes (what is recorded in the DB during spins)
/admin/validationAsset validator reports
/admin/errorsError log from audit_logs (type=error)

⚠️ Known gotcha: parallel IDE agents broke twice src/middleware.ts, inserting node:crypto — middleware runs in edge-runtime, only available crypto.subtle. If build fails with «Unhandled scheme node:» — check this file first.

03API

EndpointPurpose
POST/api/sessionNew anonymous session, starting balance 1000 demo credits
GET/api/session?sessionId=State: balance, purchased unlocks
POST/api/spinSpin: {sessionId, bet, themeId, buyBonus?} → window, lines, free spins, cascades, jackpot, balance. Single source of outcomes.
POST/api/gambleRisk game ×2: {sessionId, roundId, action: red|black|collect}
POST/api/unlockFeature purchase with demo credits (e.g. premium_sound = 300)
GET/api/history?sessionId=Player's recent rounds (modal 🕘)
GET/api/leaderboard?sessionId=🏆 Top 10 of the day + your position (sessionId optional)
GET/api/jackpot💎 Current promo pool for ticker
GET/api/themesTheme list: bets, default bet
ADMIN/api/math/simulateLaunch simulation from admin panel (middleware closed)

All mutations are behind rate-limit (token-bucket: 3 spins/s per session, 10/s per IP). Extra fields in request body are discarded by zod schemas — it's impossible to "pass desired outcome" from the client (there is an e2e test exactly for this).

04Win algorithms

🎲 Randomness generator

  • Production: crypto.randomInt (OS CSPRNG, no modulo-bias). No Math.random in outcomes.
  • Tests/simulations: deterministic mulberry32 seeded — its creation in production build is forbidden by code (throws an error).
  • Server-authoritative principle: outcome is fully computed on the server in a single transaction (deduction → draw → credit → journal). Client only renders. Math is in src/server/math with package server-only — frontend physically cannot import it.
  • Audit: each round writes Round + full RoundResult.detail (reels, lines, freespins, cascades) — any dispute is resolved in admin panel via screenshot.

🎰 Base mechanics (both themes)

Field
5 reels × 3 rows; each reel is a closed-loop symbol strip (reel strip), stop = independent random strip index
Lines
10 fixed, pay strictly left to right; bet per line = bet / 10
Wild (W)
substitutes for everything except scatter; line is read in two ways (pure W series and first non-W symbol series with substitution) — the higher payout is awarded
Scatter (S)
pays from total bet: 3/4/5 pieces → ×3/×15/×75; and gives 12/20/30 free spins
Free spins
all wins ×2, retriggers work, cap of 100 FS per round; may spin on separate reels (reelsFs.json, optional)
Cap
maximum round win — 5000× total bet
Frequencies
win ~every 3.7th spin (hit 27.3%), free spins ~1 time per 119 spins
Line win = paytable[symbol][series length] × (bet/10) × (free spins ? 2 : 1)

RTP is adjusted by reel strip composition (how many times each symbol appears on each reel — the COUNTS table in scripts/calibrate-math.ts) and the paytable values. The probabilities honestly follow from the reels — no "tweaking" of the outcome after generation.

⛓ Cascades — Centavra feature (math v2.0.0)

  • After any line win, the winning symbols disappear, survivors drop down, new ones arrive from the same reel above the stop point — refill is deterministic from stops, no separate RNG, the entire chain is reproducible and stored in the log.
  • Multiplier ladder by chain steps: ×1 → ×2 → ×3 → ×5 (then ×5).
  • Scatters and free spins are counted only on the first window; during free spins, cascades also work, their ×2 is multiplied on top of the ladder.
  • Safeguard: maximum 50 chain steps.
Step win = Σ step lines × ladder multiplier × (free spins ? 2 : 1)

Enabling cascades raised the "raw" RTP to 133.78% — Centavra's paytable recalibrated (×0.71) back to target: 94.85% @1M. Wild Patrik untouched: control run exactly 95.20%.

05Game features

🃏 Risk game ×2

After any win: guess the card color — win doubles, wrong guess — lost. Up to 5 steps (then auto-collect). Fair 50/50 on crypto-RNG, EV-neutral — does not change game RTP, only variance. The outcome is decided by the server (POST /api/gamble); the card (suit/rank) is a derivative of the outcome for display. A new spin = silent collect. Race protection — optimistic guard by step number.

💰 Buy Bonus

Instant entry into free spins for 27× bet. Implementation — rejection-sampling: the server re-rolls stops until 3+ scatters, which preserves the natural conditional distribution of 3/4/5 scatters. Price calibrated by simulator: Buy EV ≈ 25.5× bet → Buy RTP 94.61% (classic) / 94.11% (cascades) — within target band. Button with confirmation, price visible before purchase.

💎 Demo Jackpot

Shared progressive promo pool on top of RTP themes: grows by 2% of each bet from all players (casino accounting — nothing deducted from the player), seed 500, cap 5000. Drop chance increases with pool: p = 1/D, D = clamp(CAP·100/pool, 2, 2000); at cap — guaranteed. Atomic claim prevents double payout on simultaneous spins. Win not included in totalWin or leaderboard.

🏆 Daily Leaderboard + Social Layer

Top-10 for the day by best round multiplier: score = (totalWin + risk game) / (bet | bonus price); one player — one row; own position outside top comes as a separate row. Nicknames — anonymous, stable, from userId hash («Sheriff-742»), no personal data. Reset at midnight. Plus: round history 🕘, achievement toasts, premium sound unlock for demo credits, autoplay with stop conditions, turbo, fullscreen, mobile portrait.

06RTP and calibration

ThemeMathMechanicsRTP @1M (seed 777)HitVolatilityBuy-bonus RTP
Wild Patrikv1.1.0classic, 10 lines95.20%27.33%4.19 (medium)94.61% @27×
Centavrav2.0.0cascades ×1/2/3/594.85%27.33%4.32 (medium)94.11% @27×
  • The same math module is used by /api/spin and the CLI simulator — the report is guaranteed to match production outcomes.
  • Acceptance criterion: RTP within 95% ± 1.5 p.p. over 1M seeded spins; CLI exits with FAIL if out of range. For bonus price — a separate criterion (--force-bonus).
  • Reports are written to reports/rtp-*.json|md (multiplier distribution, top wins, FS frequency, σ).
  • Volatility = σ of round multiplier: <4 low, 4–9 medium, >9 high.
  • Iron rule: after any edit themes/<theme>/math/*.json — both runs for this theme are mandatory (see AGENTS.md).

07What can be changed (configuration points)

📐 Theme math — themes/<id>/math/*.json

File / fieldWhat regulatesAfter editing
reels.jsonReel strips = symbol probabilities (main RTP/volatility regulator)both RTP runs
mandatory
paytable.jsonPayouts for symbol series (line bet multipliers)
gameConfig.json: scatterPays, freeSpins, freeSpinMultiplier, maxWinX, maxFreeSpinsPerRoundScatters, free spins, cap
gameConfig.json: buyBonus.priceX · cascades.multipliersBonus buy price · cascade ladder (remove field = disable feature)
gameConfig.json: allowedBets, defaultBetBet line (each must be divisible by 10 lines)ci is enough
reelsFs.json (optional)Separate 'weighted' strips for free spinsboth RTP runs

When changing math, bump version — it is written into each round (mathVersion), allowing old disputes to be resolved by old rules.

⚙️ Constants in code

spin/service.ts
START_BALANCE (1000), UNLOCK_PRICES (premium_sound 300)
math/gamble.ts
GAMBLE_MAX_STEPS (5)
server/jackpot.ts
JACKPOT_SEED (500), JACKPOT_CAP (5000), 2% fee
server/rateLimit.ts
spin/IP/risk-game limits
server/leaderboard.ts
top-10, cash 15s, nickname dictionary

🎨 Themes and presentation

  • New theme = catalog themes/<id>/ with manifest + math + art; lobby and ?theme= pick up automatically; assets are checked validate:assets.
  • Sound: free synth pack; samples — themes/<id>/sounds/*.ogg (CC0) for unlock.
  • Symbol atlas and webp backgrounds are collected scripts/build-atlas.ts (12 files → 1 request).
  • File structure/names themes/wild-patrik/ do not change — external certification contract (113 files).

08Player “strategies” — honest analysis

Slot is a game with a fixed negative mathematical expectation for the player (RTP ≈ 95% < 100%): there is no strategy that beats the distance. The outcome of each spin is independent, there are no “hot/cold” streaks. What the player actually controls:

DecisionEffect on mathematical expectationEffect on variance / experience
Bet sizedoes not change RTPscales both wins and losses
Risk game ×2neutral (fair 50/50)sharply increases variance; ladder up to ×32
Buy Bonuspurchase RTP ~94.1–94.6% — slightly below base gameguaranteed event, concentrated variance
Theme selection95.20% vs 94.85% — practically equalcascades yield more chain-like wins
Autoplay stopsdoes not changediscipline: stop on free spins / big win
Jackpot spin timingpool near ceiling → chance increases (promo pool)the only '+EV moment', and it is deliberately transparent in the ticker

This section is also a product honesty checklist: everything listed is visible to the player (prices, odds, ticker), nothing is hidden. In demo, loss = zero real money; a new session gives 1000 credits for free.

09Tests and quality guarantees

  • Green state benchmark: build ✓ · 66/66 unit · 25/25 e2e · lint clean. Any edit — run until report.
  • What is checked: credit conservation (balance = bet/price − deductions + wins, at each step of each feature); determinism of outcomes from seed; consistency of cascade chains (step N+1 window = step N refill, final without wins); impossibility of fake win from client; guarantee of jackpot drop at ceiling; races (parallel gamble/jackpot claims); fairness of 50/50 risk game and bonus price vs live config.
  • Isolation from production: e2e go to :3007 and don't touch prod; dev and prod are built into different directories (.next-dev/.next) — the dev server cannot 'eat' prod's BUILD_ID.
  • If something is red before your edit — stop and report, do not silently fix others' code (AGENTS.md rule).

10Project structure

slot-engine/
├─ src/server/math/        # PURE math: rng, reels, paylines, evaluateSpin,
│                          # cascade, gamble, playRound, simulateRtp (server-only)
├─ src/server/             # spin/service (transactions), jackpot, leaderboard, rateLimit, db
├─ src/app/api/            # session, spin, gamble, unlock, history, leaderboard, jackpot, themes
├─ src/app/demo/           # game page (React, outcome presentation)
├─ src/app/admin/          # admin (dashboard, rounds, simulations, configs, errors)
├─ src/game/engine/        # Pixi: SlotRenderer, ReelController, AnimationController, SymbolMap
├─ src/game/sound/         # WebAudio: synthesis + sample pack
├─ src/middleware.ts       # HMAC-protected admin (edge-runtime! only crypto.subtle)
├─ themes/<id>/            # manifest, math/*.json, art, sounds — theme contract
├─ scripts/                # simulate-rtp, calibrate-math, validate-assets, build-atlas, sync-themes
├─ prisma/                 # schema + migrations (users/sessions/balances/rounds/audit/jackpot)
├─ tests/  e2e/            # vitest + Playwright
└─ README ARCHITECTURE MATH_MODEL SECURITY ACCEPTANCE DEPLOYMENT AGENTS LICENSES

Source documents: AGENTS.md (rules for agents and standards), MATH_MODEL.md (model and simulations), SECURITY.md (demo boundaries), ACCEPTANCE.md (acceptance criteria 12/12).

11How to improve the game further

Extended version with 'feature → which assets to generate' mapping, standards for ad formats (push networks like Kadam, banners, video) and spec-prompts — in the paired document /production.html.

🎮 Gameplay

  • Third theme for cascade math (engine is ready — only art pack needed).
  • Ways-pays / Megaways: payouts 'by ways' instead of lines; variable reel height.
  • Cluster pays (5+ adjacent symbols) — natural extension of cascades.
  • Level jackpot mini/major/grand with different thresholds.
  • Time-based tournaments: window 1 hour, points = multipliers, prizes in demo credits (leaderboard already exists).
  • Missions/quests: "catch 3 freespins per day" → credits; permanent achievements in profile (currently — session toasts).
  • Risk game "suit ladder" (guess the suit = ×4) as a second gamble mode.

✨ Presentation and UX

  • Polish cascade animation for live impression (physics of falling sprites, particles on popping).
  • Round replay via link: RoundResult already stores everything — page "watch how I got ×212" + sharing on leaderboard.
  • Recorded music and expanded sample pack (phase 2 — currently synthesis + CC0 samples).
  • Ante bet (bet +25% → FS chance ×2) — standard genre option, calculated by the same simulator.
  • PWA: install on phone, offline splash screen.
  • Localization EN (currently RU-only).

🛠 Tech

  • Valkey/out-of-process mechanism for rate-limit and caches when scaling to multiple instances (interfaces already extracted).
  • A/B test of math: two parallel MathConfig versions, comparison of actual RTP in admin panel (mathVersion already written to each round).
  • Client error collection (Sentry class) and FPS render metrics.
  • Captcha/mechanism for session creation if demo goes fully public on the internet.

🧙 Principles for any development

  • Outcomes — server only; src/game/** does not import @/server/**.
  • Money/crypto — never (demo boundary from SECURITY.md); «paid» = demo credits.
  • New mechanic = new math version + recalibration + both RTP runs + tests to baseline.
  • Thin verifiable slices: feature → ci+e2e green → commit → deploy → smoke.