c0b5f421c5
- In-game chat: T=all, Y=team (race doubles as team), server-relayed and team-filtered via new ChatManager autoload; last 10 messages shown bottom-left (chat/chat_box.gd). - Bot fill for casual (bots/): keeps each of the match's 2 offered races at a minimum of 7 total humans+bots, spawning/despawning reactively as players join/leave. Bots always fly their race's fighter and use negative peer_ids so they ride the existing networked-ship stack (spawning, loadout sync, health/position sync, bullet attribution) with no special-casing. Casual matchmaking now forms with just 1 real player queued instead of waiting for a second (matchmaking-api/). - Borderless fullscreen with stretch scaling disabled instead of scaling the Steam-Deck-matched 1280x800 canvas up to fill PC monitors (which read as zoomed in) — PC monitors now reveal more world/HUD at native size instead. Reworked main_menu/team_select/chat_box to position via anchors relative to the real window instead of hardcoded coordinates. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
87 lines
4.0 KiB
Markdown
87 lines
4.0 KiB
Markdown
# Spacewar Matchmaking API
|
||
|
||
FastAPI service for matchmaking, stats, and ranks (chat planned — see below).
|
||
One process, one Postgres database, no Redis: matchmaking tickets live
|
||
in-memory in the API process, which is enough at this scale — add Redis
|
||
later only if the API needs to run as more than one instance.
|
||
|
||
## Run it
|
||
|
||
```
|
||
cp .env.example .env # already done for local dev; edit if needed
|
||
docker compose up -d
|
||
```
|
||
|
||
API is at `http://localhost:8100`, interactive docs at `/docs`. Postgres is
|
||
exposed on host port `5433` (mapped from its usual `5432`, to avoid clashing
|
||
with any other local Postgres) for local inspection.
|
||
|
||
```
|
||
docker compose down # stop
|
||
docker compose down -v # stop + wipe the postgres volume
|
||
```
|
||
|
||
Code in `app/` is bind-mounted with `--reload`, so edits take effect without
|
||
rebuilding the image. Rebuild (`docker compose up -d --build`) only when
|
||
`requirements.txt` changes.
|
||
|
||
## How matchmaking works right now
|
||
|
||
- `POST /matchmaking/queue/join` `{callsign, mode, mmr?}` — creates the
|
||
player row if it doesn't exist yet, returns a `ticket_id`.
|
||
- `GET /matchmaking/queue/status/{ticket_id}` — poll this; once matched it
|
||
returns the assigned server's `server_ip`/`server_port`/`team`.
|
||
- `POST /matchmaking/queue/leave?ticket_id=...` — cancel a queued ticket.
|
||
|
||
A background loop (`app/queue_manager.py`) runs every 2s and forms a match
|
||
once enough players are queued for a mode, then splits them into two teams
|
||
and assigns the first available `GameServer` for that mode. Casual and
|
||
ranked differ here: casual has bot fill (`spacewar/bots/bot_manager.gd`
|
||
pads both teams up to `GameConfig.bot_min_team_size` in-game), so 1 queued
|
||
player is enough to form a casual match — `CASUAL_TEAM_SIZE` just caps how
|
||
many real players can group into one match, it no longer gates formation.
|
||
Ranked has no bots, so it still waits for the full `RANKED_TEAM_SIZE × 2`
|
||
real players before forming.
|
||
|
||
There's no real game-server pool yet — one dev server is auto-seeded on API
|
||
startup from `DEV_SEED_SERVER_IP`/`DEV_SEED_SERVER_PORT` (defaults to
|
||
`127.0.0.1:7777`, i.e. a Godot server run on the host via
|
||
`godot4 --headless --path spacewar -- --server`). That IP is handed straight
|
||
to game clients to connect to, so it must be reachable from wherever the
|
||
*client* runs, not the API container — `host.docker.internal` would resolve
|
||
inside the api container but not on the client's machine, which is why this
|
||
isn't a docker-internal address. Real servers should eventually call
|
||
`POST /servers/register` on boot and periodically as a heartbeat — that
|
||
endpoint exists but nothing calls it yet.
|
||
|
||
## Client integration
|
||
|
||
The Godot client is wired up (`spacewar/autoload/matchmaking_client.gd`):
|
||
CASUAL/RANKED on the main menu calls `POST /matchmaking/queue/join`, polls
|
||
`GET /matchmaking/queue/status/{ticket_id}` every 1.5s, and once `matched`
|
||
connects `NetworkManager` directly to the returned `server_ip`/`server_port`.
|
||
Point it at a non-default API with `godot4 ... -- --matchmaking-api=http://host:port`.
|
||
|
||
## Adding a new domain (stats/ranks did this; chat will too)
|
||
|
||
1. Add/extend a model in `app/models.py` if it needs its own table.
|
||
2. Add request/response shapes to `app/schemas.py`.
|
||
3. Add a router file under `app/routers/`, `include_router()` it in
|
||
`app/main.py`.
|
||
|
||
Chat isn't scaffolded yet (no data model or requirements decided), but it
|
||
fits the same shape — likely a WebSocket router using FastAPI's native
|
||
support, in this same service.
|
||
|
||
## Not set up yet, on purpose
|
||
|
||
- **Migrations**: tables are created via `Base.metadata.create_all()` on
|
||
startup. Fine while the schema is still moving; switch to Alembic before
|
||
this holds real data.
|
||
- **Auth**: `callsign` is the only player identity, matching the game
|
||
client's current state (no accounts yet). Real identity arrives with the
|
||
GodotSteam auth checklist item in `overview/tech.md`.
|
||
- **Ranked MMR-window widening / bot-fill timeouts**: current matching is a
|
||
simple threshold (enough players queued → form a match). Refine once
|
||
there's real queue volume to tune against.
|