- 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>
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 aticket_id.GET /matchmaking/queue/status/{ticket_id}— poll this; once matched it returns the assigned server'sserver_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)
- Add/extend a model in
app/models.pyif it needs its own table. - Add request/response shapes to
app/schemas.py. - Add a router file under
app/routers/,include_router()it inapp/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:
callsignis the only player identity, matching the game client's current state (no accounts yet). Real identity arrives with the GodotSteam auth checklist item inoverview/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.