Files
client/matchmaking-api
anekdotin c0b5f421c5 Add in-game chat, bot fill for casual, and borderless fullscreen
- 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>
2026-07-13 17:02:12 -04:00
..

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.