---
id: server-architecture/007
title: Stage 3 — Godot client with prediction and reconciliation; two clients
epic: server-architecture
state: done
priority: 2
blocked_by: ["server-architecture/006"]
estimate: L
created: 2026-08-02
updated: 2026-08-04
claimed_by: null
claimed_at: null
delivers: [6]
review_artifact: null
---

## What to do

Add the client side: a Godot client that connects to stage 2's world server, predicts its own movement locally for responsiveness, and reconciles against the server's authoritative state as it arrives — the netcode decision `overview.md` calls out as "not deferred... retrofitting prediction onto a netcode layer that didn't have it from the start is close to a rewrite."

1. A Godot client project using the `EntityNode` rendering half of the split stage 2's server already established for `Entity` data — same codebase family, client-specific concerns only (rendering, local input, prediction).
2. Client-side prediction: on local input, move immediately and locally, without waiting for the server round-trip.
3. Server reconciliation: when the server's authoritative position for this client's own entity arrives (per stage 2's protocol), reconcile — correct the local predicted position if it's diverged, per the standard prediction/reconciliation pattern this decision is named after.
4. **Two clients, simultaneously connected to the same world instance, each seeing the other move** — this is the concrete proof this stage exists to deliver, not just "one client works."
5. **Portal routing:** a player crosses a portal from world A to world B — membership enforced at the destination, the destination world spawned/hydrated on demand if not already running, and the player's entity state transferred through the director with no loss or duplication. See `overview.md`'s "Not yet decided: the portal-transfer mechanism itself" note — the actual transfer mechanism (candidate: Postgres-as-transfer-medium, snapshotting out of world A and hydrating into world B) and its related snapshot-boundary question are open design questions, not settled ones; confirm an approach with Kris before locking in the implementation, don't just build the candidate silently.
6. Automated tests where feasible (reconciliation logic, message handling) plus a headless two-client test-client script that connects two instances of the test client (not two humans clicking) and asserts each sees the other's position update correctly — consistent with every other stage's "no listening port to click on" constraint. Extend this (or add a companion script) to cover the portal-routing case: a client crosses a portal and ends up correctly placed in, and only in, the destination world.
7. Handoff note: `land-of-lor/pocs/multiplayer-poc/handoff/stage-3.md`, including explicitly what a human should look at when actually running two real Godot client windows on the droplet/locally (this is the one stage where an actual visual check by Kris is genuinely the most meaningful verification, even though the automated headless version has to exist too).
8. **Web export, deployed to the droplet — this stage's close-out, and how Kris human-verifies it.** Godot's HTML5/web export target for the same client from step 1, built and served from `lor-game-server-01` (deliverable #4/#5), connected to a live world. Verification is two browser tabs open to that URL, each seeing the other move — the same bar as the headless two-client test, just witnessed directly instead of read from a script's assertions. This is `deliverable #6` (`deliverables/006-play-in-your-browser.md`); it does not flip to `delivered` until this link is live and that two-tab check has actually been done. **Known prerequisite, not yet resolved:** there's no deploy key set up for getting build output onto the droplet yet (see deliverable #5's server notes) — sort that out as part of this step, don't assume it exists. **Known constraint, not a blocker:** the droplet currently serves plain HTTP with no domain/TLS, so this ships as `ws://` from an `http://` page — that's fine for the PoC, but flag in the handoff note that a later move to a real domain + TLS turns this into `wss://`, a client connection-code change, not just a hosting one.

## Definition of done

Automated/headless-scripted verification passes and demonstrates two clients observing each other's movement, plus a portal crossing between two world instances with membership enforced, on-demand destination hydration, and lossless entity-state transfer; the handoff note exists. **Additionally, per the `delivers: [6]` gate (see `epics/README.md`'s "Promised deliverables" section): this task cannot move to `done` until deliverable #6 is `delivered` with its live link in place** — the web export running on the droplet and the two-browser-tab check, not just the headless proof. The review→done gate this extends already required Kris's sign-off; this makes explicit that his sign-off on deliverable #6 specifically is part of it. Set `state: review`, not `done`, once the automated/headless portion lands — `done` still waits on the web-export deploy and Kris's check.

## Where the work lands

`land-of-lor/pocs/multiplayer-poc/`.

## Docs to read first

`server-architecture/overview.md`'s "Netcode" section (prediction/reconciliation, the reason it's day-one not deferred). Stage 2's handoff note for the protocol/movement contract this stage's client talks to.

## Status (2026-08-03)

**State: `review`.** All of steps 1-8 are built and automated-verified; deliverable #6 is `delivered` (live link in place, verified externally per the gate in "Definition of done" above) — so the `delivers: [6]` gate is satisfied. `state: review` (not `done`) regardless, per this epic's build-plan gating: only Kris moves a stage to `done`, same rule every prior stage followed.

**Built, across three sessions (server protocol, client, deploy):**
- Portal transfer ("Postgres as the transfer medium, via the director" — closed 2026-08-03, see `overview.md`'s "Persistence" section) and `seq`-based move reconciliation protocol (`intent_ack`/`intent_rejected`), director + world-server.
- A real Godot client (`world-server/client/`) reusing `movement.gd`/`grid.gd`/`entity.gd`/`protocol.gd` byte-for-byte: client-side prediction against the shared `Movement.try_move()`, server reconciliation on `intent_ack`, interpolated rendering of remote entities, `EntityNode` (`ColorRect`-based, per Kris's own "we see each other's balls moving" bar), full connect/join/hello flow with remembered identity, and portal traversal (direct handoff + a proven 2-second crash-safety fallback — see handoff note's Deviation 3 for why the fallback, not the direct handoff, is the actually-reliable path in this Godot build).
- Automated headless verification: 28 vitest + 12 + 15 CLI-driven checks (portal transfer incl. real `SIGKILL` crash-safety, mutual visibility, reconciliation-correction proof, illegal-intent refusal, portal crossing incl. crash-safety) + 41 Godot pure-logic checks — all independently re-run and confirmed by the orchestrating session, not just trusted from the building agents' own reports.
- Web export (Godot 4.7.1 HTML5, `world-server/client/web-build/`, committed to the repo) deployed to `lor-game-server-01`: Node 22 installed, director built and running as `lor-director.service` (replacing a placeholder stub), the obsolete placeholder `lor-world@1.service` stopped/disabled (the director spawns world processes itself, on demand — no per-world systemd unit), Caddy serving the web export at `/play/*`, firewall opened for direct game-channel connections (`32768:60999/tcp`, the ephemeral range `allocateFreePort()` actually uses).
- **Live, external verification from the container**: `director/cli-test-client/verify-live-droplet.ts` (`npm run verify-live-droplet`) proves, over the public internet against the real running droplet (not a local stand-in), two accounts joining the same live-spawned world, both connecting directly to its real `gamePort`, mutual movement visibility both directions, and a live illegal-intent refusal — **9/9 passed**. The web client URL and its `.wasm`/`.pck` assets independently confirmed to return HTTP 200 with correct sizes.

**Known gap, flagged plainly, not silently worked around:** a GitHub deploy key for the droplet (deliverable #5's outstanding follow-up) could not be registered via API this session — the container's token lacks the required "Administration" repo permission (403). A read-only keypair was generated on the droplet (public key in `deliverables/006-play-in-your-browser.md`) and needs a human with GitHub admin access to add it. Code was still gotten onto the droplet, via the same `git bundle` transfer method deliverable #5 originally used — not blocking, just not the cleaner `git pull` workflow deliverable #5 described as the goal.

**What's still a human check, not provable from this container:** actually opening `http://209.38.29.22/play/index.html?director=ws://209.38.29.22/director/` in two browser tabs and watching the canvas render — no display/browser exists in this container. The live-verification script proves the underlying protocol traffic works end-to-end; the visual confirmation is Kris's own remaining step, same as it would be for the desktop two-client check.

Full detail: `land-of-lor/pocs/multiplayer-poc/handoff/stage-3.md` (and `handoff/stage-3-server-side.md` for the protocol-only first half).

## Verified and closed (2026-08-04)

Kris performed the human verification bar directly: the web client at `https://lor-server.cocreations.com.au/play/index.html?director=wss://lor-server.cocreations.com.au/director/` — two browser tabs, each seeing the other's player move in the live commons world. (His words on seeing it work are not printable on a family-friendly board, but they were an emphatic yes.) Getting the browser path to this point required the same-day HTTPS wiring and the public-commons-world work recorded in deliverable #6's 2026-08-04 update. The `delivers: [6]` gate was already satisfied; with the visual check done, this stage is `done`.