Lernprojekt für Code-Reviews zu Design und Architektur. Der Server implementiert ein Gefangenendilemma mit TCP-Bots, browserbasierten Code-Bots und einem Web-UI.
Monorepo (npm Workspaces) mit drei Paketen:
| Paket | Beschreibung |
|---|---|
server/ |
Express-Backend, WebSocket, TCP-Server, Code-Bot-Ausführung |
client/ |
React SPA (Vite) |
example-bot/ |
Beispiel-TCP-Bots (Tit-for-Tat, Random) |
| Dienst | Port | Protokoll |
|---|---|---|
| HTTP / REST / WebSocket | 3000 | HTTP, WS (/ws) |
| TCP-Bots | 4000 | Line-Delimited JSON |
| Frontend (Dev) | 5173 | Vite Dev-Server mit Proxy auf 3000 |
In Produktion liefert der Server das gebaute Frontend selbst aus (client/dist).
npm install
npm run devStartet Backend (Port 3000) und Frontend (Port 5173). Web-UI: http://localhost:5173
npm run build
npm startWeb-UI und API dann unter http://localhost:3000
Externe Bots verbinden sich per TCP mit Line-Delimited JSON (eine JSON-Zeile pro Nachricht, mit \n abgeschlossen).
npm run bot # Tit-for-Tat
npm run bot:random # Zufallsstrategie (50/50 C/D)Umgebungsvariablen:
| Variable | Standard | Beschreibung |
|---|---|---|
SERVER_HOST |
localhost |
Hostname des Game-Servers |
SERVER_PORT |
4000 |
TCP-Port des Game-Servers |
BOT_NAME |
TitForTat_Bot |
Anzeigename bei der Registrierung |
Beispiel gegen den Server auf snug:
SERVER_HOST=snug.meqo.de SERVER_PORT=9998 BOT_NAME=MyBot npm run botRegistrierung (Bot → Server):
{"type": "register", "name": "TitForTatBot"}Spielzug-Anfrage (Server → Bot):
{"type": "request_move", "gameId": "g-123", "opponent": "human", "ownScore": 12, "oppScore": 9, "history": [["C", "D"], ["C", "C"]]}Antwort (Bot → Server):
{"move": "C"}Code-Bots werden im Browser-Editor oder per HTTP-API verwaltet. Der Code wird serverseitig ausgeführt und muss 'C' oder 'D' zurückgeben.
Verfügbare Variablen im Bot-Code: gameId, opponent, ownScore, oppScore, history
Basis-URL lokal: http://localhost:3000/api/bots/code
curl http://localhost:3000/api/bots/codecurl -X POST http://localhost:3000/api/bots/code \
-H 'Content-Type: application/json' \
-d '{"name":"MyBot","code":"return \"C\";"}'curl http://localhost:3000/api/bots/code/{id}
curl -X PUT http://localhost:3000/api/bots/code/{id} \
-H 'Content-Type: application/json' \
-d '{"name":"MyBot_v2","code":"return \"D\";"}'
curl -X DELETE http://localhost:3000/api/bots/code/{id}| Zug 1 | Zug 2 | Punkte 1 | Punkte 2 |
|---|---|---|---|
| C | C | 3 | 3 |
| C | D | 0 | 5 |
| D | C | 5 | 0 |
| D | D | 1 | 1 |
Round-Robin: Jeder Bot spielt einmal gegen jeden anderen. Pro Paarung werden 80–120 Runden zufällig gewählt. Bei Fehlern/Timeouts wird der Bot disqualifiziert.
| Methode | Pfad | Beschreibung |
|---|---|---|
| GET | /api/bots |
Alle Bots (TCP + Code, ohne Quellcode) |
| GET | /api/bots/code |
Alle Code-Bots inkl. Quellcode |
| POST | /api/bots/code |
Code-Bot anlegen |
| GET | /api/bots/code/:id |
Einzelnen Code-Bot abrufen |
| PUT | /api/bots/code/:id |
Code-Bot ändern |
| DELETE | /api/bots/code/:id |
Code-Bot löschen |
| POST | /api/bots/presets/random |
Random Code-Bot erstellen |
| DELETE | /api/bots/:id |
Bot entfernen (TCP oder Code) |
| POST | /api/game/human/start |
Human-Spiel starten |
| POST | /api/game/human/move |
Zug senden |
| POST | /api/tournament/start |
Turnier starten |
WebSocket für Live-Updates: /ws (relativ zur Web-UI-URL, z. B. wss://snug.meqo.de:9999/ws)
| Datei | Zweck |
|---|---|
Dockerfile |
Multi-Stage-Build (Client + Server, Node 20 Alpine) |
docker-compose.yaml |
Traefik (SSL) + App |
traefik.yml |
Traefik-Entrypoints und Docker-Provider |
dynamic.yml |
TLS-Zertifikate (crt.pem, key.pem aus /certs) |
.dockerignore |
Schließt node_modules und lokale Builds aus |
docker build -t prisoners-dilemma-game .
docker run -d --name pd-game -p 13000:3000 -p 14000:4000 prisoners-dilemma-game
# Web-UI: http://localhost:13000Mit Compose (nur App, ohne SSL):
TCP_PORT=14000 docker compose up -d app
docker compose exec app wget -qO- http://127.0.0.1:3000/api/botsDer Stack startet zwei Container:
- app — Node-Server (HTTP 3000, TCP 4000 im Container)
- traefik — SSL-Terminierung, Routing zur App
Auf dem Server laufen mehrere Docker-Stacks mit Traefik über denselben Docker-Socket. Alle nutzen oft denselben Hostnamen (snug.meqo.de). Ohne separaten Entrypoint würde Traefik Anfragen an den falschen Container weiterleiten (504 Gateway Timeout). Der Game-Stack verwendet deshalb:
- Entrypoint
gamesecure(intern Port 8443) → extern HTTPS 9999 - Label
traefik.constraint=game— Traefik ignoriert Container anderer Stacks
APP_HOST=snug.meqo.de
SSL_CERT_DIR=/etc/ssl/private/meqo.de
TRAEFIK_HTTPS_PORT=9999
TRAEFIK_HTTP_PORT=9980
TCP_PORT=9998| Variable | Standard | Beschreibung |
|---|---|---|
APP_HOST |
game.example.com |
Host-Header für Traefik-Routing |
SSL_CERT_DIR |
/etc/ssl/private/example.com |
Verzeichnis mit crt.pem und key.pem |
TRAEFIK_HTTPS_PORT |
9999 |
Externer HTTPS-Port (→ intern 8443) |
TRAEFIK_HTTP_PORT |
9980 |
Externer HTTP-Port (→ intern 80) |
TCP_PORT |
9998 |
Externer TCP-Port für Bots (→ intern 4000) |
docker compose up -d --build# Projekt nach /home/cpm/services/game kopieren (ohne node_modules)
rsync -avz --exclude node_modules --exclude .git \
-e cpm-ssh ./ snug:/home/cpm/services/game/
# Auf dem Server
cd /home/cpm/services/game
docker compose up -d --buildErreichbar unter:
- Web-UI: https://snug.meqo.de:9999/
- TCP-Bots: snug.meqo.de:9998
docker compose logs -f
docker compose ps
docker compose up -d --build # nach Code-Updates
docker compose down| Variable | Standard | Beschreibung |
|---|---|---|
HTTP_PORT |
3000 |
HTTP/WebSocket-Port im Container |
TCP_PORT |
4000 |
TCP-Port im Container |