added docs
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
# 1. Architecture overview
|
||||
|
||||
## Components
|
||||
|
||||
| Component | Folder | Language / stack | Runs as | Responsibility |
|
||||
|-----------|--------|------------------|---------|----------------|
|
||||
| Website | `wizard_website/` | React 19, TypeScript, Vite, CSS modules | static files served by nginx (container port 8082) | UI: start page, lobby, game table |
|
||||
| Game server | `wizard_server/app/` | Python 3.13, FastAPI, uvicorn | one process (container port 8083) | sessions, lobbies, running games, asking the AI for moves |
|
||||
| Game engine | `wizard_server/game_logic/` (package `wizard_game`) | pure Python, no dependencies | library, installed into server and AI images | the rules of Wizard |
|
||||
| AI service | `wizard_ai/` | Python, `websockets` | one process, no listening port | chooses moves for AI seats; training tools |
|
||||
|
||||
The game engine is deliberately a separate installable package. The server
|
||||
uses it to run games, and the AI uses the *same* code to train, so a model
|
||||
learns exactly the rules it will later play by.
|
||||
|
||||
## Network layout (production)
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph internet
|
||||
U[Browser]
|
||||
end
|
||||
subgraph net1 ["network wizard_website"]
|
||||
CADDY1((Caddy))
|
||||
WEB["wizard_website :8082"]
|
||||
end
|
||||
subgraph net2 ["network wizard_api"]
|
||||
CADDY2((Caddy))
|
||||
SRV["wizard_server :8083"]
|
||||
end
|
||||
subgraph net3 ["network wizard_internal (no internet)"]
|
||||
SRV2[wizard_server]
|
||||
AI[wizard_ai]
|
||||
end
|
||||
U -->|"https://game.marvinkrausser.com/wizard/website/"| CADDY1 --> WEB
|
||||
U -->|"wss://game.marvinkrausser.com/wizard/api/ws"| CADDY2 --> SRV
|
||||
AI -->|"ws://wizard_server:8083/internal/ai"| SRV2
|
||||
```
|
||||
|
||||
- Caddy is one existing container attached to the external networks
|
||||
`wizard_website` and `wizard_api`; it is drawn twice only for readability.
|
||||
- `wizard_server` is attached to two networks (`wizard_api` and
|
||||
`wizard_internal`); `SRV` and `SRV2` are the same container.
|
||||
- Caddy **strips the path prefix**: the website container sees `/assets/…`,
|
||||
the server sees `/ws` and `/health`.
|
||||
- The AI service connects *to* the server. It has no open port, and its
|
||||
network (`wizard_internal`) has no internet access.
|
||||
|
||||
## How one move travels through the system
|
||||
|
||||
A human plays a card in a game that also has an AI player:
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant B as Browser (useWizard)
|
||||
participant S as Server (WizardService / Lobby)
|
||||
participant E as WizardGame (engine)
|
||||
participant A as AI service
|
||||
|
||||
B->>S: {"type":"action","action":{"type":"play","card":17}}
|
||||
S->>E: game.apply(seat, PlayCard(card 17))
|
||||
E-->>S: events (e.g. TrickCompleted)
|
||||
S-->>B: {"type":"state", lobby, game (personal observation), events}
|
||||
Note over S: next seat is an AI → Lobby._ai_loop
|
||||
S->>A: {"type":"decide","request_id":"…","observation":{…}}
|
||||
A->>A: agent.act(observation)
|
||||
A-->>S: {"type":"action","request_id":"…","action":{…}}
|
||||
Note over S: waits at least AI_MOVE_DELAY<br/>(and TRICK_PAUSE after a trick)
|
||||
S->>E: game.apply(ai_seat, action)
|
||||
S-->>B: {"type":"state", …}
|
||||
```
|
||||
|
||||
Important properties of this flow:
|
||||
|
||||
1. **The server is authoritative.** Browsers and the AI only *propose*
|
||||
actions; the engine validates every action (`WizardGame.apply` raises
|
||||
`IllegalActionError` for anything illegal).
|
||||
2. **Every state message is complete.** The server does not send diffs. Each
|
||||
`state` message contains the full lobby and the full personal view of the
|
||||
game. The frontend simply replaces its state, which makes reconnects and
|
||||
reloads trivial.
|
||||
3. **Each player receives their own view.** The server builds an
|
||||
`Observation` per seat, which never contains other players' hands (and hides
|
||||
bids while "hidden bids" is active).
|
||||
4. **The AI sees exactly what a human sees.** The AI service receives the same
|
||||
`Observation` JSON a browser gets for that seat.
|
||||
|
||||
## Repository layout
|
||||
|
||||
```
|
||||
.
|
||||
├── compose.yaml # all three containers + networks
|
||||
├── .env.example # template for .env (AI_SHARED_SECRET, …)
|
||||
├── README.md # short project readme
|
||||
├── docs/ # this documentation
|
||||
├── instructions.txt # original task description
|
||||
├── wizard_instructions/ # original rules (saved web page)
|
||||
│
|
||||
├── wizard_server/
|
||||
│ ├── Dockerfile
|
||||
│ ├── requirements.txt # fastapi, uvicorn
|
||||
│ ├── requirements-dev.txt # + pytest, httpx, editable game_logic
|
||||
│ ├── app/ # the FastAPI server
|
||||
│ │ ├── main.py # app factory + routes
|
||||
│ │ ├── config.py # Settings from environment variables
|
||||
│ │ ├── protocol.py # browser → server message schema (pydantic)
|
||||
│ │ ├── service.py # WizardService: dispatches messages, sessions, cleanup
|
||||
│ │ ├── sessions.py # Session, Connection (ordered send queue), SessionRegistry
|
||||
│ │ ├── lobby.py # Lobby, Seat, LobbyManager, AI turn loop
|
||||
│ │ └── ai_bridge.py # request/response with the AI service + fallback
|
||||
│ ├── tests/test_server.py # websocket-level tests of the server
|
||||
│ └── game_logic/ # installable package "wizard-game"
|
||||
│ ├── pyproject.toml
|
||||
│ ├── wizard_game/
|
||||
│ │ ├── cards.py # Card, Suit, deck, card ids
|
||||
│ │ ├── actions.py # Bid, PlayCard, JSON (de)serialization
|
||||
│ │ ├── rules.py # stateless rule functions
|
||||
│ │ ├── state.py # Phase, GameOptions, results, events, RoundState
|
||||
│ │ ├── observation.py # Observation (a player's view) + JSON
|
||||
│ │ └── game.py # WizardGame state machine
|
||||
│ └── tests/ # rules + full random games
|
||||
│
|
||||
├── wizard_ai/
|
||||
│ ├── Dockerfile # needs game_logic as extra build context
|
||||
│ ├── requirements.txt # websockets
|
||||
│ ├── models/ # trained checkpoints (mounted read-only)
|
||||
│ ├── wizard_ai/
|
||||
│ │ ├── __main__.py # CLI: serve | train | evaluate
|
||||
│ │ ├── config.py
|
||||
│ │ ├── agents/ # Agent interface, random, heuristic, model agent
|
||||
│ │ ├── model/ # encoding.py (features/actions), policy.py (model interface)
|
||||
│ │ ├── training/ # env.py, self_play.py, evaluate.py
|
||||
│ │ └── service/client.py # production websocket client
|
||||
│ └── tests/test_ai.py
|
||||
│
|
||||
└── wizard_website/
|
||||
├── Dockerfile # node build → nginx
|
||||
├── nginx.conf # listens on 8082, SPA fallback
|
||||
├── vite.config.ts # base './', dev proxy for /api
|
||||
└── src/
|
||||
├── main.tsx, App.tsx, App.module.css
|
||||
├── api/ # types.ts (protocol types), useWizard.ts (connection hook)
|
||||
├── components/ # Button, PlayingCard, Modal, Toasts
|
||||
├── screens/ # HomeScreen, LobbyScreen, GameScreen (+ its parts)
|
||||
└── util/cards.ts # sorting, labels, legal-action helpers
|
||||
```
|
||||
|
||||
## Key design decisions (and why)
|
||||
|
||||
| Decision | Reason |
|
||||
|----------|--------|
|
||||
| Game rules in a dependency-free package | The AI trains on the exact same rules the server enforces; the engine is fast and easy to test in isolation. |
|
||||
| Engine works with seat indices only | Keeps it independent of users, names and networking. The server maps indices ↔ players. |
|
||||
| One `Observation` type for browser *and* AI | A single, well-tested definition of "what a player may know"; no risk of the AI accidentally seeing hidden information. |
|
||||
| Full-state messages instead of diffs | Simple client, trivial reconnects, no sync bugs. Messages are small (a few KB). |
|
||||
| Server asks the AI (request/response), AI seats live in the server | The AI service is stateless per request, can be scaled to several instances (round robin), and a missing AI never blocks a game (random fallback). |
|
||||
| All state in memory, single process | No database needed for a casual game. Consequence: a server restart ends all games, and the server must run with **one worker**. |
|
||||
| Session token in `sessionStorage` | Survives page reloads (reconnect to the same seat) but is per tab, so you can test several players in several tabs. |
|
||||
| Per-connection send queue | Websocket messages are always sent in order and never concurrently (see `sessions.Connection`). |
|
||||
| Relative asset paths + relative API URL in the frontend | The app works under any path prefix (`/wizard/website/` in production, `/` in development) without rebuilding. |
|
||||
Reference in New Issue
Block a user