Add package.json (Fastify, Prisma, Redis, JWT), TypeScript config, Dockerfile, docker-compose with Postgres and Redis services, and .gitignore with data directory rules. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Contrabbandieri MMO — Server
Backend per il gioco mobile multiplayer asincrono "Contrabbandieri". Tutte le regole di gioco vivono sul server: il client non è mai considerato affidabile.
Stack
Node.js 22 · TypeScript · Fastify · Prisma · PostgreSQL · Redis · Socket.IO · Zod · JWT · Docker Compose
Avvio rapido
cp .env.example .env # poi cambia JWT_SECRET e POSTGRES_PASSWORD
mkdir -p data/postgres data/redis data/backups
docker compose up -d --build
Al primo avvio il container api applica automaticamente le migrazioni (prisma migrate deploy). Poi esegui il seed (città, item, listini):
docker compose exec api npx prisma db seed
Il server è su http://localhost:3000 (health check: GET /healthz).
Dati persistenti
Tutti i dati vivono in bind-mount locali dentro data/ (postgres/, redis/, backups/): copiare la cartella del progetto su un altro server trasferisce tutto. Per il backup vedi §Backup.
Struttura
src/
├── app.ts # Fastify, plugin, error handler, registrazione route
├── server.ts # entrypoint: HTTP + Socket.IO + scheduler
├── config/
│ ├── env.ts # variabili d'ambiente validate con Zod
│ └── gameBalance.ts # ★ TUTTI i parametri di bilanciamento del gioco
├── db/ # client Prisma e Redis
├── modules/ # un modulo per dominio: routes + service
│ ├── auth/ players/ cities/ market/ inventory/
│ ├── missions/ events/ leaderboard/
├── jobs/ # scheduler node-cron + job periodici
├── realtime/socket.ts # Socket.IO (path /ws)
└── shared/ # authGuard, errori, validazione, utility
Bilanciamento: ogni costante di gioco (costi viaggio, formule mercato, probabilità di successo missioni, eventi, XP) sta in src/config/gameBalance.ts, commentata. Nessun numero "magico" altrove.
API REST (/api/v1)
Tutte le route tranne register/login richiedono Authorization: Bearer <jwt>.
| Metodo | Path | Descrizione |
|---|---|---|
| POST | /auth/register |
crea utente+giocatore, ritorna JWT (rate limit 5/min) |
| POST | /auth/login |
login, ritorna JWT (rate limit 5/min) |
| GET | /auth/me |
utente + giocatore correnti |
| GET | /player/me |
profilo, città corrente, XP per il prossimo livello |
| POST | /player/travel |
{cityId} — viaggia (costo scalato dal server) |
| GET | /cities · /cities/:cityId |
città con costo viaggio |
| GET | /market/current |
listino della città corrente + eventi attivi |
| POST | /market/buy · /market/sell |
{itemId, quantity} — prezzi decisi dal server |
| GET | /inventory |
inventario con peso totale |
| GET | /missions/available |
missioni nella città corrente |
| POST | /missions/:missionId/start |
avvia (consuma subito gli item richiesti) |
| POST | /missions/:playerMissionId/claim |
riscuote: successo/fallimento calcolato dal server |
| GET | /missions/active |
missioni in corso |
| GET | /events/current |
eventi mondo attivi |
| GET | /leaderboard/money · /leaderboard/reputation |
classifiche (Redis), ?limit=N |
Gli errori hanno sempre la forma { "error": { "code": "...", "message": "..." } }.
Realtime (Socket.IO)
Path /ws, handshake con auth: { token: "<jwt>" }.
Eventi server→client: market:updated, mission:completed, worldEvent:started, worldEvent:ended, player:notification, leaderboard:updated. Client→server: client:ping.
Regole di gioco (formule)
I parametri citati sono tutti in gameBalance.ts.
- Mercato (
rawPrice = basePrice * economyModifier * demand / max(supply, 0.25) * eventModifier): buy = raw×1.12, sell = raw×0.88, limiti [0.35×, 3.5×] del prezzo base. Acquisti/vendite spostano domanda/offerta; ogni minuto il mercato decade verso l'equilibrio (fattore 0.98). - Viaggio:
costo = 50 + riskLevel(destinazione) × 25. - Missioni:
successo = 0.95 − risk×0.6 + livello×0.01 + reputazione×0.0005 − (pressione polizia−1)×0.05, limitato a [5%, 95%]. Successo: denaro + XP + reputazione (+2×difficoltà). Fallimento: −3 reputazione (mai sotto 0). Massimo 3 missioni attive per giocatore. - XP: per passare dal livello L a L+1 servono
100 × L^1.5XP. - Eventi: ogni 15 min possibile evento locale (25%), ogni ora possibile evento globale (20%); modificano prezzi e pressione di polizia finché attivi.
Scheduler
- Ogni minuto: ricalcolo prezzi, notifica missioni completate, pulizia missioni scadute, generazione missioni (minimo 5 attive per città).
- Ogni 15 minuti: possibile evento locale, refresh classifiche Redis.
- Ogni ora: possibile evento globale, notifica eventi terminati.
Sviluppo senza Docker
Servono PostgreSQL e Redis raggiungibili (puoi usare solo i container db e redis):
npm install
# in .env usa localhost al posto di db/redis
npx prisma migrate dev
npx prisma db seed
npm run dev
Backup
# dump (a caldo, sicuro)
docker compose exec db pg_dump -U gameuser game > ./data/backups/game_$(date +%Y%m%d_%H%M).sql
# restore
cat ./data/backups/game_XXX.sql | docker compose exec -T db psql -U gameuser game
# migrazione completa su altro host: stop, archivio, riavvio
docker compose down
tar -czf contrabbandieri-data.tar.gz data .env docker-compose.yml
Produzione
Il Dockerfile ha uno stage production (build TypeScript, solo dipendenze runtime, node dist/server.js). Prima del deploy: cambia JWT_SECRET e POSTGRES_PASSWORD, non esporre le porte 5432/6379, metti un reverse proxy HTTPS (es. Nginx) davanti alla porta 3000.