Files
gioco-mobile/Blueprint_Client.md
2026-06-09 22:40:43 +02:00

18 KiB
Raw Permalink Blame History

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 lequilibrio.


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:

  1. fermare i container;
  2. fare un dump PostgreSQL;
  3. copiare repository, .env e cartella data/backups;
  4. 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:

  1. setup Fastify + TypeScript;
  2. Docker Compose con PostgreSQL e Redis;
  3. Prisma schema;
  4. seed città e item;
  5. register/login JWT;
  6. player profile;
  7. inventory;
  8. market current;
  9. buy/sell;
  10. missioni disponibili;
  11. start/claim mission;
  12. scheduler prezzi;
  13. leaderboard;
  14. 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 delleconomia.