SPB Git forge

spb/spinza

Public
8commits 1branches 0releases
1.6 MBsize
maindefault branch
16 days agolast push
TypeScript 97.6% SQL 1.4% JavaScript 0.5%
6.0 KB

# Spinza — repository guide

Spinza (www.spinza.dev) is a premium fictional social casino. Virtual credits only: no deposits, no withdrawals, no purchases, no crypto, no cash value, no prizes. The full product brief is in docs/SPEC-original.md; this file is the condensed working guide.

# Layout (pnpm workspaces + Turborepo)

Path Package Role
packages/shared @spinza/shared Economy constants (10,000 SC start, bet levels, daily rewards, XP curve), zod schemas, formatting (formatSC — never a currency sign), API DTO types.
packages/game-core @spinza/game-core Also: src/crash/ (crash engine: P(crash ≥ x) = rtp/x, SHA-256 commit/reveal, curves exp/power/steps, pacing events, multiplierAt shared with the browser via crash/curve.ts) and src/arcade/ (instant resolvers + ladder engine m_k = rtp·Π(1/p_i) — strategy-independent RTP).
Server-only game engine: CryptoRng (crypto.randomBytes, rejection sampling — Math.random is banned for outcomes), reels, ways/lines evaluation, wild/scatter/cascade/hold-respin/pick-bonus/meter/heat/mystery/quantum/dynamic-grid features, runSpin(), simulate(), certify(), validateDefinition(), defineGame(). ./client export = types only (safe for the browser).
games/ @spinza/games 20 slot definitions (src/<slug>/index.ts, registry.ts), 10 Risk Games (src/crash/index.ts, cash-out/crash engine), 5 Spinza Originals — Beyond Slots (src/arcade/index.ts: Dropzone, Grid//Break, Orbit = instant; The Vault, Escape 99 = ladder), calibration.json (payScale per slug+version), certifications/<slug>.json (PASS/FAIL reports for all 35 games).
packages/database @spinza/database Drizzle schema (25 tables, snake_case casing), migrations in drizzle/, seed.ts (games sync, achievements, missions, levels, flags, settings), sync-games.ts.
apps/api @spinza/api Fastify on 127.0.0.1:8231. Auth (argon2id, opaque 256-bit sessions hashed, recovery codes), atomic spin (services/spin.ts), wallet ledger (services/wallet.ts), progression, rewards, leaderboards, admin (TOTP), health.
apps/simulator @spinza/simulator CLI `pnpm sim <validate
apps/web @spinza/web Next 16 (port 8230). /api/* is rewritten to the Fastify service. Game screen = src/components/game/* (PixiJS renderer, procedural symbols, WebAudio sound). Admin console under /admin.

# Game families & API

Family Kind Play API Client
Slots (20) slot POST /api/games/:slug/spin components/game/* (PixiJS renderer)
Risk Games (10) crash POST /api/crash/:slug/start → poll GET /api/crash/:slug/rounds/:idPOST /api/crash/:slug/cashout (auto cash-out settles server-side; crash_rounds table; lazy settlement) components/crash/* (2D canvas scenes)
Beyond Slots instant (3) arcade POST /api/arcade/:slug/play {bet, clientRoundId, input} components/arcade/{dropzone,gridbreak,orbit}.tsx
Beyond Slots ladder (2) arcade POST /api/arcade/:slug/startPOST /api/arcade/:slug/act {roundId, action} (arcade_sessions) components/arcade/{vault,escape}.tsx

Every settled round of any family goes through apps/api/src/services/settle.ts (settleRound: game_rounds row, stats, XP, achievements, missions, leaderboards). Lobby categories come from summary.kind/category written by sync-games.ts.

# Non-negotiable rules

  • Outcomes are decided only in apps/api via runSpin. The browser animates result.steps; it never computes wins.
  • Every balance change goes through lockWalletapplyCredit (ledger row) → saveWallet inside one Postgres transaction. sum(credit_transactions.amount) == wallets.balance is an invariant checked in admin.
  • Spin idempotency: unique (user_id, client_round_id); duplicates (even concurrent) return the stored round with replayed: true.
  • A game is published only if games/certifications/<slug>.json is PASS for its exact version. Changing pays/weights ⇒ bump version, re-run pnpm sim calibrate <slug> then pnpm sim certify <slug>.
  • Never log passwords, hashes, recovery codes, session tokens (pino redact list in apps/api/src/app.ts).
  • No payments, no $/ anywhere in UI copy. Credits are formatted 10,000 SC.

# Commands

bash
pnpm install
createdb spinza && pnpm db:migrate && pnpm db:seed          # local Postgres 17 + Redis required
pnpm dev:api   # 8231     pnpm dev:web   # 8230
pnpm sim validate | pnpm sim run <slug> --spins 1000000 | pnpm sim calibrate all|arcade --spins 10000000 --threads 26 | pnpm sim certify all|crash|arcade --spins 10000000
pnpm --filter @spinza/game-core test    # engine unit tests
pnpm --filter @spinza/api test          # integration tests (needs local DB/Redis)
pnpm admin:create <username> [password] # prints TOTP secret once

# Deployment (MacLustr)

mld deploy spinza from the laptop (~/Desktop/cluster-skill/mld). Manifest with secrets: M1M32:~/dispatch/apps/spinza.json (copy: deploy/spinza.mld.json, gitignored). Node M3U96a, PM2 spinza-api (8231 loopback) + spinza-web (8230) + spinza-ngrok (www.spinza.dev). Postgres db spinza + Redis local to the node. Hooks: install → migrate + seed → next build. Backups: deploy/backup.sh (daily launchd on the node).

# Gotchas

  • drizzle-orm ≥0.44 wraps pg errors: use pgCode(e) (apps/api/src/lib/pg.ts), not e.code.
  • Way/line pays are in bet units (betDivisor: 25 for 5-reel ways, 50 for 6+ reels, lines.length for line games); scatter/bonus/coin values are × total bet. payScale from calibration.json scales all non-jackpot wins.
  • Certification tolerance is statistical: max(0.4%, 3σ/√n) capped at 1.5% — high-volatility games need ≥10M spins.
  • Turbopack: import workspace packages without .js extensions; browser code imports @spinza/game-core/client only.
  • PixiJS renderer must be created client-side only (GameClient is "use client"; Application.init is async).