server: project config and infrastructure setup
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>
This commit is contained in:
@@ -0,0 +1,7 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
data
|
||||||
|
.env
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
README.md
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
.env
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Dati persistenti dei container (bind mount locali)
|
||||||
|
data/postgres/*
|
||||||
|
data/redis/*
|
||||||
|
data/backups/*
|
||||||
|
!data/postgres/.gitkeep
|
||||||
|
!data/redis/.gitkeep
|
||||||
|
!data/backups/.gitkeep
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# Immagine base: dipendenze complete + client Prisma generato
|
||||||
|
FROM node:22-alpine AS base
|
||||||
|
WORKDIR /app
|
||||||
|
RUN apk add --no-cache openssl
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm ci
|
||||||
|
COPY prisma ./prisma
|
||||||
|
RUN npx prisma generate
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Stage di sviluppo: usato da docker-compose.yml (hot reload con tsx)
|
||||||
|
FROM base AS dev
|
||||||
|
ENV NODE_ENV=development
|
||||||
|
EXPOSE 3000
|
||||||
|
CMD ["npm", "run", "dev"]
|
||||||
|
|
||||||
|
# Stage di build: compila TypeScript in dist/
|
||||||
|
FROM base AS build
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# Stage di produzione: solo runtime, senza dipendenze dev
|
||||||
|
FROM node:22-alpine AS production
|
||||||
|
WORKDIR /app
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
RUN apk add --no-cache openssl
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm ci --omit=dev
|
||||||
|
COPY --from=build /app/dist ./dist
|
||||||
|
COPY --from=build /app/prisma ./prisma
|
||||||
|
COPY --from=build /app/node_modules/.prisma ./node_modules/.prisma
|
||||||
|
COPY --from=build /app/node_modules/@prisma/client ./node_modules/@prisma/client
|
||||||
|
EXPOSE 3000
|
||||||
|
CMD ["node", "dist/server.js"]
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
# 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
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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](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.5` XP.
|
||||||
|
- **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`):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
# in .env usa localhost al posto di db/redis
|
||||||
|
npx prisma migrate dev
|
||||||
|
npx prisma db seed
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## Backup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 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.
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
services:
|
||||||
|
api:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
target: dev
|
||||||
|
container_name: contrabbandieri-api
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
redis:
|
||||||
|
condition: service_healthy
|
||||||
|
volumes:
|
||||||
|
- .:/app
|
||||||
|
- /app/node_modules
|
||||||
|
command: sh -c "npx prisma migrate deploy && 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}
|
||||||
|
# Sottodirectory del bind mount: initdb richiede una directory vuota
|
||||||
|
PGDATA: /var/lib/postgresql/data/pgdata
|
||||||
|
volumes:
|
||||||
|
- ./data/postgres:/var/lib/postgresql/data
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 12
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: redis:7
|
||||||
|
container_name: contrabbandieri-redis
|
||||||
|
restart: unless-stopped
|
||||||
|
command: ["redis-server", "--appendonly", "yes"]
|
||||||
|
ports:
|
||||||
|
- "6379:6379"
|
||||||
|
volumes:
|
||||||
|
- ./data/redis:/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "redis-cli", "ping"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 12
|
||||||
Generated
+2245
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"name": "contrabbandieri-server",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"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"
|
||||||
|
},
|
||||||
|
"prisma": {
|
||||||
|
"seed": "tsx prisma/seed.ts"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@fastify/cors": "^11.0.1",
|
||||||
|
"@fastify/jwt": "^9.1.0",
|
||||||
|
"@fastify/rate-limit": "^10.3.0",
|
||||||
|
"@prisma/client": "^6.8.0",
|
||||||
|
"argon2": "^0.41.1",
|
||||||
|
"dotenv": "^16.5.0",
|
||||||
|
"fastify": "^5.3.0",
|
||||||
|
"ioredis": "^5.6.0",
|
||||||
|
"node-cron": "^3.0.3",
|
||||||
|
"socket.io": "^4.8.1",
|
||||||
|
"zod": "^3.24.4"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^22.15.0",
|
||||||
|
"@types/node-cron": "^3.0.11",
|
||||||
|
"prisma": "^6.8.0",
|
||||||
|
"tsx": "^4.19.4",
|
||||||
|
"typescript": "^5.8.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"lib": ["ES2023"],
|
||||||
|
"module": "NodeNext",
|
||||||
|
"moduleResolution": "NodeNext",
|
||||||
|
"outDir": "dist",
|
||||||
|
"rootDir": "src",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"sourceMap": true
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user