18 KiB
Blueprint Server — Contrabbandieri MMO Mobile
1. Obiettivo
Realizzare un backend funzionante per un gioco mobile multiplayer asincrono, persistente e trasferibile tra server Linux tramite Docker.
Il server deve gestire:
- autenticazione utenti;
- profilo giocatore;
- denaro, reputazione e livello;
- inventario;
- mercato dinamico;
- missioni generate dal server;
- città e prezzi locali;
- eventi globali;
- classifiche;
- notifiche realtime opzionali;
- deploy tramite Docker Compose.
Il client mobile non deve mai essere considerato affidabile. Tutte le regole di gioco vivono sul server.
2. Stack consigliato
Backend
- Node.js 22 LTS
- TypeScript
- Fastify
- Prisma ORM
- PostgreSQL
- Redis
- Socket.IO
- Zod
- JWT
- Docker Compose
Perché questo stack
Fastify è veloce, semplice e adatto a API REST. Prisma semplifica schema, migrazioni e accesso al database. PostgreSQL è affidabile per dati persistenti. Redis serve per cache, classifiche, cooldown e notifiche realtime. Socket.IO consente eventi live verso client mobile.
3. Architettura generale
Mobile Client
|
| HTTPS REST / WebSocket
v
Nginx Reverse Proxy
|
v
Game API Node.js
| |
| v
| Redis
v
PostgreSQL
4. Struttura repository
contrabbandieri-server/
├── docker-compose.yml
├── docker-compose.prod.yml
├── .env.example
├── README.md
├── data/
│ ├── postgres/
│ ├── redis/
│ └── backups/
├── package.json
├── tsconfig.json
├── prisma/
│ ├── schema.prisma
│ └── seed.ts
├── src/
│ ├── app.ts
│ ├── server.ts
│ ├── config/
│ │ └── env.ts
│ ├── db/
│ │ ├── prisma.ts
│ │ └── redis.ts
│ ├── modules/
│ │ ├── auth/
│ │ ├── players/
│ │ ├── market/
│ │ ├── inventory/
│ │ ├── missions/
│ │ ├── cities/
│ │ ├── events/
│ │ └── leaderboard/
│ ├── realtime/
│ │ └── socket.ts
│ ├── jobs/
│ │ ├── scheduler.ts
│ │ ├── updateMarketPrices.job.ts
│ │ ├── generateMissions.job.ts
│ │ └── generateWorldEvent.job.ts
│ ├── shared/
│ │ ├── authGuard.ts
│ │ ├── errors.ts
│ │ ├── result.ts
│ │ └── validators.ts
│ └── types/
└── nginx/
└── nginx.conf
5. Modello di dominio MVP
Player
Rappresenta il giocatore.
Campi principali:
- id
- userId
- displayName
- money
- reputation
- level
- experience
- currentCityId
- createdAt
- updatedAt
City
Ogni città ha mercato e missioni locali.
Esempi iniziali:
- Milano
- Napoli
- Palermo
Campi:
- id
- name
- riskLevel
- policePressure
- economyModifier
Item
Merce commerciabile.
Esempi:
- Gioielli
- Componenti elettronici
- Auto rubate
- Informazioni riservate
- Armi leggere
Campi:
- id
- name
- basePrice
- rarity
- illegalLevel
- weight
InventoryItem
Quantità posseduta da un player.
Campi:
- playerId
- itemId
- quantity
MarketPrice
Prezzo locale per item e città.
Campi:
- cityId
- itemId
- buyPrice
- sellPrice
- demand
- supply
- updatedAt
Mission
Missione generata dal server.
Campi:
- id
- cityId
- title
- type
- difficulty
- durationSeconds
- rewardMoney
- rewardXp
- risk
- requiredItemId
- requiredItemQuantity
- expiresAt
PlayerMission
Missione iniziata da un player.
Campi:
- id
- playerId
- missionId
- status
- startedAt
- completesAt
- resolvedAt
WorldEvent
Evento globale o locale che altera economia/rischio.
Campi:
- id
- title
- description
- type
- cityId nullable
- priceModifier
- policeModifier
- startsAt
- endsAt
6. Schema Prisma MVP
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
enum MissionStatus {
STARTED
COMPLETED
FAILED
}
enum MissionType {
DELIVERY
THEFT
SMUGGLING
INTEL
}
enum EventType {
MARKET_BOOM
POLICE_RAID
SHORTAGE
BLACKOUT
}
model User {
id String @id @default(uuid())
email String @unique
passwordHash String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
player Player?
}
model Player {
id String @id @default(uuid())
userId String @unique
displayName String @unique
money Int @default(1000)
reputation Int @default(0)
level Int @default(1)
experience Int @default(0)
currentCityId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id])
currentCity City @relation(fields: [currentCityId], references: [id])
inventory InventoryItem[]
missions PlayerMission[]
}
model City {
id String @id @default(uuid())
name String @unique
riskLevel Int
policePressure Float @default(1.0)
economyModifier Float @default(1.0)
createdAt DateTime @default(now())
players Player[]
prices MarketPrice[]
missions Mission[]
events WorldEvent[]
}
model Item {
id String @id @default(uuid())
name String @unique
basePrice Int
rarity Int
illegalLevel Int
weight Int @default(1)
createdAt DateTime @default(now())
inventory InventoryItem[]
prices MarketPrice[]
missions Mission[]
}
model InventoryItem {
playerId String
itemId String
quantity Int
player Player @relation(fields: [playerId], references: [id])
item Item @relation(fields: [itemId], references: [id])
@@id([playerId, itemId])
}
model MarketPrice {
cityId String
itemId String
buyPrice Int
sellPrice Int
demand Float @default(1.0)
supply Float @default(1.0)
updatedAt DateTime @updatedAt
city City @relation(fields: [cityId], references: [id])
item Item @relation(fields: [itemId], references: [id])
@@id([cityId, itemId])
}
model Mission {
id String @id @default(uuid())
cityId String
title String
type MissionType
difficulty Int
durationSeconds Int
rewardMoney Int
rewardXp Int
risk Float
requiredItemId String?
requiredItemQuantity Int?
expiresAt DateTime
createdAt DateTime @default(now())
city City @relation(fields: [cityId], references: [id])
requiredItem Item? @relation(fields: [requiredItemId], references: [id])
playerMissions PlayerMission[]
}
model PlayerMission {
id String @id @default(uuid())
playerId String
missionId String
status MissionStatus @default(STARTED)
startedAt DateTime @default(now())
completesAt DateTime
resolvedAt DateTime?
player Player @relation(fields: [playerId], references: [id])
mission Mission @relation(fields: [missionId], references: [id])
}
model WorldEvent {
id String @id @default(uuid())
title String
description String
type EventType
cityId String?
priceModifier Float @default(1.0)
policeModifier Float @default(1.0)
startsAt DateTime
endsAt DateTime
createdAt DateTime @default(now())
city City? @relation(fields: [cityId], references: [id])
}
7. API REST MVP
Base path:
/api/v1
Auth
POST /auth/register
POST /auth/login
GET /auth/me
POST /auth/register
Request:
{
"email": "player@example.com",
"password": "password123",
"displayName": "ShadowFox"
}
Response:
{
"accessToken": "jwt",
"player": {
"id": "...",
"displayName": "ShadowFox",
"money": 1000,
"level": 1
}
}
Player
GET /player/me
POST /player/travel
POST /player/travel
Request:
{
"cityId": "..."
}
Regola server:
- verifica che la città esista;
- calcola costo viaggio;
- verifica soldi disponibili;
- aggiorna città corrente;
- scala il denaro.
Cities
GET /cities
GET /cities/:cityId
Market
GET /market/current
POST /market/buy
POST /market/sell
POST /market/buy
Request:
{
"itemId": "...",
"quantity": 3
}
Regole server:
- il player compra nel mercato della città corrente;
- il prezzo lo decide il server;
- il costo è
buyPrice * quantity; - se il player non ha soldi, errore;
- aggiorna inventario;
- aggiorna domanda/offerta.
POST /market/sell
Request:
{
"itemId": "...",
"quantity": 2
}
Regole server:
- verifica inventario;
- paga
sellPrice * quantity; - riduce inventario;
- aggiorna domanda/offerta.
Inventory
GET /inventory
Missions
GET /missions/available
POST /missions/:missionId/start
POST /missions/:playerMissionId/claim
GET /missions/active
Avvio missione
Il server calcola completesAt = now + durationSeconds.
Claim missione
Regole:
- se
now < completesAt, errore; - se già risolta, errore;
- calcola successo in base a rischio, livello e reputazione;
- assegna premio o fallimento;
- aggiorna XP, soldi, reputazione.
Events
GET /events/current
Leaderboard
GET /leaderboard/money
GET /leaderboard/reputation
8. Realtime Socket.IO
Endpoint:
/ws
Eventi server → client:
market:updated
mission:completed
worldEvent:started
worldEvent:ended
player:notification
leaderboard:updated
Eventi client → server:
client:ping
Nel prototipo, le azioni importanti restano REST. Socket.IO serve solo per notificare.
9. Regole mercato dinamico
Ogni città ha prezzi indipendenti.
Formula base:
rawPrice = basePrice * city.economyModifier * demand / max(supply, 0.25) * eventModifier
Prezzo acquisto:
buyPrice = round(rawPrice * 1.12)
Prezzo vendita:
sellPrice = round(rawPrice * 0.88)
Limiti:
minPrice = basePrice * 0.35
maxPrice = basePrice * 3.50
Ogni acquisto:
demand += quantity * 0.02
supply -= quantity * 0.01
Ogni vendita:
demand -= quantity * 0.01
supply += quantity * 0.02
Ogni ciclo scheduler:
demand = demand * 0.98 + 1.0 * 0.02
supply = supply * 0.98 + 1.0 * 0.02
Questo fa tornare lentamente il mercato verso l’equilibrio.
10. Scheduler
Implementare un processo interno nel backend.
Ogni minuto
- aggiornare prezzi di mercato;
- completare notifiche missioni;
- cancellare missioni scadute;
- generare nuove missioni se sotto soglia.
Ogni 15 minuti
- generare possibile evento locale;
- aggiornare classifiche Redis.
Ogni ora
- generare possibile evento globale;
- pulizia dati temporanei.
Librerie consigliate:
node-cron
Per versione avanzata:
BullMQ + Redis
Per MVP basta node-cron.
11. Sicurezza
Principi
- mai fidarsi del client;
- validare ogni payload con Zod;
- usare JWT con scadenza breve;
- password con Argon2 o bcrypt;
- rate limit su login e register;
- usare HTTPS tramite Nginx;
- non esporre PostgreSQL e Redis su internet;
- loggare azioni economiche importanti.
Variabili segrete
Nel file .env, non committare mai:
DATABASE_URL
JWT_SECRET
POSTGRES_PASSWORD
REDIS_URL
12. File .env.example
NODE_ENV=development
PORT=3000
DATABASE_URL=postgresql://gameuser:gamepassword@db:5432/game
REDIS_URL=redis://redis:6379
JWT_SECRET=change_me_in_production
JWT_EXPIRES_IN=7d
POSTGRES_DB=game
POSTGRES_USER=gameuser
POSTGRES_PASSWORD=gamepassword
13. Docker Compose sviluppo
Regola sui dati persistenti
Il server deve salvare tutti i dati persistenti in volumi montati nella stessa directory del progetto, non in volumi Docker anonimi o nominati.
Struttura dati locale:
contrabbandieri-server/
└── data/
├── postgres/ # database PostgreSQL persistente
├── redis/ # snapshot Redis, se abilitati
└── backups/ # dump manuali o automatici
Vantaggi:
- puoi copiare l'intera cartella del progetto su un altro server;
- i dati sono visibili e controllabili dal filesystem Linux;
- backup e restore sono più semplici;
- Docker rimane solo il runtime, non il proprietario nascosto dei dati.
Prima del primo avvio:
mkdir -p data/postgres data/redis data/backups
Permessi consigliati se PostgreSQL ha problemi di scrittura:
sudo chown -R 999:999 data/postgres
sudo chown -R 999:999 data/redis
services:
api:
build: .
container_name: contrabbandieri-api
restart: unless-stopped
ports:
- "3000:3000"
env_file:
- .env
depends_on:
- db
- redis
volumes:
- .:/app
- /app/node_modules
command: npm run dev
db:
image: postgres:16
container_name: contrabbandieri-db
restart: unless-stopped
environment:
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- ./data/postgres:/var/lib/postgresql/data
ports:
- "5432:5432"
redis:
image: redis:7
container_name: contrabbandieri-redis
restart: unless-stopped
ports:
- "6379:6379"
volumes:
- ./data/redis:/data
13.1 Backup e migrazione server
Poiché i volumi sono bind-mount locali, la migrazione minima consiste in:
- fermare i container;
- fare un dump PostgreSQL;
- copiare repository,
.enve cartelladata/backups; - riavviare Docker Compose sul nuovo host.
Backup PostgreSQL:
docker compose exec db pg_dump -U "$POSTGRES_USER" "$POSTGRES_DB" > ./data/backups/game_$(date +%Y%m%d_%H%M).sql
Restore su nuovo server:
cat ./data/backups/game.sql | docker compose exec -T db psql -U "$POSTGRES_USER" "$POSTGRES_DB"
Spegnimento controllato prima di copiare dati grezzi:
docker compose down
tar -czf contrabbandieri-data.tar.gz data .env docker-compose.yml
Nota: il dump SQL è preferibile alla copia a caldo di data/postgres, perché evita inconsistenze se il database è in esecuzione.
14. Dockerfile backend
FROM node:22-alpine AS base
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npx prisma generate
RUN npm run build
FROM node:22-alpine AS production
WORKDIR /app
ENV NODE_ENV=production
COPY package*.json ./
RUN npm ci --omit=dev
COPY --from=base /app/dist ./dist
COPY --from=base /app/prisma ./prisma
COPY --from=base /app/node_modules/.prisma ./node_modules/.prisma
EXPOSE 3000
CMD ["node", "dist/server.js"]
15. Package.json base
{
"name": "contrabbandieri-server",
"version": "0.1.0",
"type": "module",
"scripts": {
"dev": "tsx watch src/server.ts",
"build": "tsc",
"start": "node dist/server.js",
"prisma:generate": "prisma generate",
"prisma:migrate": "prisma migrate dev",
"prisma:deploy": "prisma migrate deploy",
"prisma:seed": "tsx prisma/seed.ts"
},
"dependencies": {
"@fastify/cors": "latest",
"@fastify/jwt": "latest",
"@fastify/rate-limit": "latest",
"@prisma/client": "latest",
"argon2": "latest",
"dotenv": "latest",
"fastify": "latest",
"ioredis": "latest",
"node-cron": "latest",
"socket.io": "latest",
"zod": "latest"
},
"devDependencies": {
"@types/node": "latest",
"prisma": "latest",
"tsx": "latest",
"typescript": "latest"
}
}
16. Deploy su Linux
Installazione base
sudo apt update
sudo apt install -y ca-certificates curl git ufw
Docker
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER
Disconnettersi e riconnettersi.
Avvio
git clone <repo>
cd contrabbandieri-server
cp .env.example .env
nano .env
docker compose up -d --build
Migrazioni
docker compose exec api npx prisma migrate deploy
docker compose exec api npx prisma db seed
17. Backup e migrazione
Backup database
docker exec contrabbandieri-db pg_dump -U gameuser game > backup.sql
Restore database
cat backup.sql | docker exec -i contrabbandieri-db psql -U gameuser game
File da portare su altro server
.env
backup.sql
docker-compose.yml
nginx/nginx.conf
Il codice può essere recuperato da Git.
18. MVP funzionale minimo
Ordine di implementazione:
- setup Fastify + TypeScript;
- Docker Compose con PostgreSQL e Redis;
- Prisma schema;
- seed città e item;
- register/login JWT;
- player profile;
- inventory;
- market current;
- buy/sell;
- missioni disponibili;
- start/claim mission;
- scheduler prezzi;
- leaderboard;
- Socket.IO notifiche.
19. Seed iniziale
Città:
Milano: economia alta, rischio medio
Napoli: economia media, rischio alto
Palermo: economia bassa, rischio alto
Torino: economia media, rischio basso
Bari: economia bassa, rischio medio
Item:
Gioielli, base 500
Componenti elettronici, base 350
Documenti falsi, base 700
Auto rubate, base 1800
Informazioni riservate, base 1200
Armi leggere, base 1500
Missioni template:
Consegna discreta
Recupero merce
Scambio al porto
Infiltrazione uffici
Trasporto notturno
20. Estensioni successive
Dopo MVP:
- clan/gang;
- territori;
- PvP asincrono;
- aste tra giocatori;
- crafting oggetti;
- stagioni competitive;
- anti-cheat comportamentale;
- pannello admin web;
- log economico completo;
- bilanciamento automatico dell’economia.