Files
client/matchmaking-api/README.md
T
anekdotin edcda92812 Add ENet multiplayer (movement/combat sync) and matchmaking API
Networked play, end to end: connect, get matched, spawn, fight, respawn.

Godot side:
- Replace the single hardcoded Player with per-peer networked ships
  (MultiplayerSpawner + custom spawn_function), driven by client-side
  prediction with server reconciliation for movement.
- Server-authoritative combat: bullets, health, death/respawn all decided
  by the server and broadcast to every peer over RPC.
- Fix a Camera2D bug where Godot auto-promotes the first camera to ever
  enter the scene tree as the active one regardless of its own `current`
  value — with MultiplayerSpawner catch-up replication that was almost
  never the local player's own ship. `make_current()` on the owner's
  camera fixes it; property assignment doesn't reliably override the
  auto-claim.
- Fix two "late joiner never learns already-established state" gaps
  (health, visibility) by folding both into the existing per-tick
  position broadcast instead of relying on one-shot RPCs that only reach
  peers already connected when they fire.
- Make the 2 offered factions in team select match for every player in
  a match: the server decides once and clients either read it directly
  (server's own instance) or request it over RPC, rather than each
  client rolling its own random pair.
- New MatchmakingClient autoload wires the main menu's CASUAL/RANKED
  buttons to the matchmaking API instead of connecting directly.

New matchmaking-api/ (FastAPI + Postgres, Docker Compose):
- Queue/match/stats/ranks endpoints. In-memory matchmaking queue in a
  single API process — no Redis until there's an actual reason to shard
  the queue across instances.
- Background loop forms a match once enough players are queued for a
  mode, assigns the first available registered game server.
- No real server pool yet: one dev server is auto-seeded from
  DEV_SEED_SERVER_IP/PORT; POST /servers/register exists for real
  servers to self-register later but nothing calls it yet.

Docs: CLAUDE.md and overview/tech.md updated to match — networking
checklist mostly checked off, matchmaking backend section rewritten to
describe what was actually built vs. the original Go/Redis plan, and
Current Tasks reordered around what's actually left (bot fill, real
server pool, ranked/MMR refinement, lag compensation).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 22:37:59 -04:00

3.6 KiB
Raw Blame History

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 once enough players are queued for a mode (CASUAL_TEAM_SIZE/RANKED_TEAM_SIZE × 2) it forms a match, splits them into two teams, and assigns the first available GameServer for that mode.

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.