05 — Multiplayer
This document specifies every way Every Last City is played with more than one human, and it specifies exactly one thing about each: how order records and observation streams travel. The turn itself — the Orders phase, the Cascade, the Reckoning — is owned entirely by 10-turn-model.md and is identical in every mode. What changes between hot seat, live play, correspondence play and file play is the schedule and the carrier, never the rules.
Covered here: the four transport modes and their seat model; the common event-sourced foundation; concurrent order submission and how a phase closes; the wire protocol; the two authority modes; hot seat; live play over the Internet and LAN; correspondence play, which this turn model suits far better than sequential turn-taking ever did; the signed turn-bundle file for players with no infrastructure at all; switching a game between modes; notifications; deadlines, absence and AI substitution; spectating and replay; fog enforcement and anti-cheat; and self-hosted servers.
Status: Draft v0.2 · Owner: unassigned · Depends on: docs/design/00-direction.md, docs/design/01-decision-turn-model.md, 01-game-rules.md, 03-architecture.md, 04-ui-ux.md, 06-ai.md, 07-modding-content.md, 08-services-platform.md, 10-turn-model.md, 13-command.md, 14-victory.md
What this document is not. It defines no rule that changes what the simulation computes. Every requirement below is about transport, scheduling, storage, trust, or presentation. Where a requirement here appears to describe game behaviour, it is restating an obligation 10-turn-model.md owns, and 10-turn-model.md governs.
1. Multiplayer modes and definitions
MP-010 The game MUST support the following transport modes, all operating on the same game entity (immutable initial state + append-only order log per 03-architecture.md AR-250):
| Mode | How orders travel | Cascade runs on |
|---|---|---|
| Hot seat | Two or more humans share one device; each takes the Orders phase in sequence behind an occlusion screen (10-turn-model.md TM-460) | The local client |
| Live | All players connected concurrently over LAN or the Internet; orders stream to the authority as they are made | The authority (server or designated host client) |
| Correspondence | Server-held game; each player opens it whenever they like during the Orders phase, submits, and leaves | The server |
| Turn-bundle | The game serialized to signed, portable files passed by any out-of-band channel — email, forum, USB stick, airgapped network | The turn's closer (§10.4) |
Partially held 2026-09-28: the shared-entity half stands in core and two of the four modes stand with their sections' own pins; correspondence and turn-bundle wait on surfaces that do not exist yet. All modes ride one game entity — immutable initial state plus append-only log per AR-250, pinned at packages/core/test/view/view-delivery.test.ts. Hot seat stands at packages/client/test/handoff.test.ts, which titles the occlusion hand-off (MP-390, MP-400); live play stands over real sockets in the server suites (packages/server/test/wire.test.ts and lobby-transport.test.ts) on the two committed example games in packages/server/examples/. What waits: correspondence — nothing under packages/server names one, and its section's rows wait with 08-services-platform.md — and turn-bundle, whose client path is absent by MP-790's own flagged gap.
MP-020 A seat is a slot in the game's player roster, identified by the stable player index
01-game-rules.md GR-020 assigns at setup. Every seat has a controller binding: local-human (a
profile on this device), remote-human (an account, per 08-services-platform.md), bundle-human (a
signing key, §10), ai (an AI plugin reference, per 06-ai.md), or vacant. Transport mode is a
property of how seats are bound and carried — never of the game state itself, and never of the player
index, which does not change for any reason including AI substitution (GR-020).
no-test-needed: a definition of vocabulary, and the controller bindings it names (local-human, remote-human, bundle-human, ai, vacant) are a client and services concern the core never models. Its one normative clause — a player index that changes for no reason, including AI substitution — is 01-game-rules.md GR-020 and is held there.
MP-030 Any combination of seat controller types MUST be permitted in one game (for example two local humans, one remote human, and two AIs). Rationale: a seat is a slot in a roster and a controller is a way of filling it; there is no reason for the two to constrain each other, and every restriction of the form "the organizer must be local" or "email games may have only one connected player" is an artifact of a transport, not a property of a game.
Partially held 2026-09-28: mixed human/AI rosters stand end-to-end through the deployed lobby; what this row still owes is the rest of the combination space. The witness pin plays two local humans against one Constraint Floor AI team over real WebSockets — packages/server/test/humans-vs-floor.smoke.test.ts, which reads examples/human-vs-ai.json, the configuration the deployed lobby serves itself, and commits five turns through both close gates; and packages/server/test/lobby.test.ts's MP-460 surface sets an AI seat while humans sit beside it. What waits: no test assembles a roster mixing remote humans with local humans and AIs in one game — this row's own example shape — so the universal quantifier stands by witness configurations only, and should be widened when a mixed-roster live game lands.
MP-040 All modes MUST be cross-play across every supported platform (Windows, macOS, Linux, iOS, Android, and the browser), guaranteed by the deterministic core (03-architecture.md AR-070). A game started on any platform MUST be joinable and resumable from any other.
no-test-needed: cross-play across six platforms presupposes platform builds this tree does not contain — there is no iOS or Android target here and no packaged browser build — so a game started on one platform cannot yet be joined from another for the simple reason that most platforms are absent; the clause that binds code today is the deterministic core it names as its guarantee (03-architecture.md AR-070, pinned at packages/client/test/save-container.test.ts), which stands by that document's own pins. It becomes testable when a second platform ships: same setup and seed, the game carried across.
MP-050 Offline modes (hot seat, LAN live play, turn-bundle) MUST NOT require an account or any
network service. Correspondence and Internet live play through an @everylastcity/server instance
require an account on that instance (official or self-hosted); guest accounts scoped to a single
server instance MAY be offered by that instance.
no-test-needed: no account machinery exists anywhere in this tree — nothing under any package implements identity or login — so the prohibition has no subject to bind, and offline modes run in packages/client with no transport at all while every server-backed game in the suites goes through the lobby explicitly (the humans-vs-floor smoke joins by code on purpose). When instance accounts land per 08-services-platform.md the testable shape is a local game constructing without an account object.
MP-060 The rules computed in every mode MUST be identical: same core version, same seed, same order log ⇒ same result, regardless of mode, transport, or how many times the game switched modes (10-turn-model.md TM-2410).
2. Mode capability parity
MP-070 Every mode MUST provide the capabilities marked required below. A mode MUST NOT be shipped with a required capability missing, and no mode MAY offer a capability marked unavailable:
| Capability | Hot seat | Live | Correspondence | Turn-bundle |
|---|---|---|---|---|
| The three-phase turn of TM-020, unmodified | required | required | required | required |
| Concurrent Orders phase (no player blocks another) | by occlusion, sequentially | required | required | required (§10.4) |
| Persisted orders carry an absent player (TM-280, TM-530) | required | required | required | required |
| Orders deadline and reserve bank (§13) | optional | required | required | soft only (MP-1040) |
| Server-enforced fog | unavailable | mode-dependent (§6) | required by default (MP-380) | unavailable (MP-790) |
| Compacted post-Cascade observation stream (TM-2150) | required | required | required | required |
| In-game chat (MP-185) | local only | required | required | carried in the bundle |
| AI substitution for an absent seat (MP-1010) | optional | required | required | optional |
| Spectating (§14) | unavailable live | policy-dependent | policy-dependent | post-game replay only |
| Rated play (MP-1160) | unavailable | authoritative only | authoritative only | unavailable |
| Mode switching in any direction (§11) | required | required | required | required |
no-test-needed: a shipping gate whose cells are each owned and held elsewhere — the three-phase turn at its rows in 10-turn-model.md, the occlusion hand-off at MP-400's pin in packages/client/test/handoff.test.ts, the deadline and reserve bank at packages/server/test/deadline.test.ts (TM-520 through TM-570), the compaction at packages/core/test/view/view-stream.test.ts (TM-2150), chat at MP-185's pin in packages/client/test/live-chat.test.ts, mode switching at packages/core/test/net/mode.test.ts. No mode has shipped from this tree yet — there is no distribution surface beyond local play and the operator-run server — so the gate has nothing to bite; when a mode ships, these pins are what it checks against.
MP-080 The ability to play a complete game with zero central infrastructure — turn-bundle, LAN, and hot seat — is a permanent requirement and MUST NOT be traded away for any server-side convenience. Rationale: a game whose multiplayer depends on a service the publisher operates is a game with an expiry date, and the correspondence games this design is built for have a lifetime measured in years (MP-630). The file must outlive the service, and it must outlive us.
no-test-needed: a standing product commitment about trade-offs nobody has yet proposed, which no engine behaviour can exhibit. What it protects is held by rows that exist: MP-750 container and the section 10 turn cycle, both of which run with no server present.
3. Common foundations
MP-090 Every multiplayer game MUST be represented as the event-sourced entity defined in 03-architecture.md: immutable initial state (setup, map or generator+seed, content hashes) plus an append-only, sequence-numbered order log. Saves, resume, reconnection, spectating, replays, mode switching, and desync detection MUST all be derived from that log. The Cascade and the Reckoning MUST NOT be logged (10-turn-model.md TM-310); they are recomputed on replay.
Implementation note, 2026-09-12. MP-090's log is tamper-evident now. This requirement makes
"saves, resume, reconnection, spectating, replays, mode switching, and desync detection" all derive from
the order log, and until today the log had no integrity chain to derive them from: 03-architecture.md
AR-260's ChainHash existed only as a comment naming a future user of XXH3. It is built, with a
verifyChain that names the first broken link — a chain is sequential, so a verifier returning a
list would report the consequences alongside the cause — and with rows for the three ways a log is
actually tampered with: a payload edited in place, a record removed from the middle, a record inserted
into it. A chain nobody checks is a field, which is why the check shipped in the same slice as the
chain.
The readings that decided the envelope's shape are recorded at AR-340, and one of them is this
document's: MP-140 is why Issuer is not hashed. "A seat's human controller MAY change device,
platform, connection, or mode binding at any time without affecting game state" — so a substituted turn
must hash exactly like an unsubstituted one, and the field that records who actually issued a record has
to stay outside the digest to keep that true.
MP-100 Every game MUST carry a globally unique GameId (UUIDv7) assigned at creation, stable
across mode switches, exports, and imports.
MP-110 Every game MUST be pinned at creation to: a core EngineVersion, a RulesVersion, a
protocol/view schema version (03-architecture.md AR-880, AR-920), the content hashes of its unit set,
map/scenario or generator plugin, and any behavioural mods (07-modding-content.md), and every ruleset
value that changes what the Cascade computes — including the Initiative Tier of 10-turn-model.md
TM-1030, which is fixed for the life of the game (TM-1070). All participants MUST run the pinned core
version; clients MUST support side-by-side core versions so old games stay playable.
MP-120 Content auto-distribution: when a joining client lacks pinned content, the lobby/server MUST offer to fetch it — data content (unit sets, maps, scenarios, assets) automatically; sandboxed WASM plugins (AI, generators) after a one-time per-plugin consent prompt. Hash mismatch MUST block joining.
3.1 There is no active seat
MP-130 There is no active seat. At any instant the game is in exactly one phase (10-turn-model.md TM-090), and during an Orders phase every seat may append order records concurrently. What MUST be exclusive is the authority: exactly one node (server, designated host client, or the turn's bundle closer) owns the right to assign log sequence numbers and append. All modes MUST enforce single-authority append; concurrent divergent appends MUST be impossible by construction (server sequencing) or detected and rejected (bundle sequence check, §10.6).
MP-135 Concurrency MUST be safe without ordering guarantees between players, because an order record can only mutate state belonging to its issuing player and any two records from different players therefore commute (10-turn-model.md TM-240). The transport MUST NOT add an ordering constraint of its own, MUST NOT hold one player's submission until another's has arrived, and MUST NOT let arrival order be observable to any player.
Rationale: this is the single most important consequence of the turn model for this document, and it is why the old possession machinery is gone. Under sequential turns, transport had to enforce "one player at a time" because the rules required it; the file that vanished from the sender's game list and the server that refused out-of-turn orders were both implementations of that rule. Under Orders and the Cascade the rule does not exist. The authority still sequences the log — that is a storage property — but it sequences a set of records whose result does not depend on the sequence, and a test proves it (TM-2360 hazard 7). Nothing about a fast connection, an early submission, or a lucky arrival order can buy anything.
MP-140 Player identity in a game is the seat, not the transport: a seat's human controller MAY change device, platform, connection, or mode binding at any time without affecting game state, stats attribution, or colours. Per-seat display colours personalized by a player MUST be stored per player per game.
MP-150 Losing and leaving: 14-victory.md owns what it means to leave a game and what placing a
departing player receives (VC-080). This document owns notification and access: a player who
capitulates, concedes, or is eliminated MUST be notified in-game and, in correspondence games, via
notification (§12), and MUST be offered spectator access to the remainder of the game under the
game's spectating policy (§14). A Concede order (14-victory.md VC-320, VC-2840) MUST travel as an
ordinary logged order like any other.
no-test-needed: notification and spectator access belong to the services platform, per section 12 and section 14. The one clause the core carries — a Concede travelling as an ordinary logged order — is 14-victory.md VC-320 and VC-2840, and belongs to that document.
MP-160 Auto-persistence: in every connected mode the authority MUST durably persist the log after
every accepted order record and at every CloseOrders. A crash MUST lose at most order records whose
results were never acknowledged (MP-290).
Held 2026-09-29 — the durable-before-acknowledged half closed with this slice. Both directions now stand in packages/server/test/durable.test.ts: persistence per accepted record, mid-phase as well as at each close (AR-255's partial-turn container included), and the strict ordering itself — pinned over a registered handler exactly as transport.ts observes it, no wire carrying an OrderResults for recorded records before their rewrite has landed. What remains on this row is custody past one process: section 5's store (SVC-410/412) owns clocks and membership that outlive the host.
MP-170 Clean lifecycle: no client action (window close, crash, network drop, sleep) may hang or corrupt a game for other players. Leaving is always safe, resuming is always possible from the persisted log, and an absent player never blocks anyone (10-turn-model.md TM-560).
no-test-needed: a liveness property of a client and a server that do not exist yet. Its two core clauses are held elsewhere: an absent player blocking nobody is 10-turn-model.md TM-560, and resuming from the persisted log is TM-2410 replay over the AR-260 chain.
MP-180 The complete wire protocol, bundle format, and server API MUST be publicly documented and versioned, sufficient for third-party clients and self-hosted servers to interoperate.
3.2 Chat and presence
MP-185 All connected modes (live, correspondence, spectators) MUST provide in-game text chat: a
game-wide channel, plus optional private player-to-player messages. In correspondence games chat MUST
be persistent, turn-annotated, and delivered with turn notifications. Chat history is part of the
game record but MUST NOT enter the order log (it can never affect simulation state). Because it is
game record, in-game chat travels as the Chat / ChatRelay protocol messages of MP-270 and MUST
NOT ride a room framework's ephemeral state-sync channel (MP-265; 08-services-platform.md SVC-402
states the same boundary from the server side). Pre-launch lobby chat is the separate, expendable
capability of MP-465 and carries none of this requirement's obligations.
MP-186 Connected clients MUST see per-seat presence, and presence MUST consist of exactly:
whether the seat is connected; whether it has issued CommitOrders for the current Orders phase;
time remaining on the deadline and in that seat's reserve bank; and, in correspondence games, time
since the seat's last action. Presence MUST be ephemeral, MUST NOT contribute to any hash, and MUST
NOT carry any fact about the game world (10-turn-model.md TM-440, TM-2070).
4. Turn handling: Orders, the Cascade, the Reckoning
MP-190 Turn handling is the three-phase turn of 10-turn-model.md TM-020 — a concurrent Orders phase, an input-free Cascade, and an input-free Reckoning — in every mode, with no variants, no options, and no per-mode differences. There is no sequential turn-taking anywhere in this specification, no seat is prompted "when it is their turn", and no player waits on another player's units.
Rationale: this is the change that reshaped this document. The previous draft specified sequential turns as the model and treated concurrent play as a variant that would need sealed batches and reveal-after-commit machinery. The reality is the reverse: the model is concurrent by construction, the sealing machinery is unnecessary because no client is ever sent another client's orders (MP-230), and the transport's job shrank from enforcing possession to relaying two things — order records up, and one observation stream per player down.
MP-192 The phases, their ordering, what may happen in each, and every rule about deadlines, absence and substitution are owned by 10-turn-model.md. This document specifies only how the records that drive them are carried, measured, and stored. Where the two appear to conflict, 10-turn-model.md governs.
no-test-needed: a documentation duty. It assigns the phase rules to 10-turn-model.md and names which document governs a conflict, so there is no behaviour here to assert.
MP-195 Exactly one node MUST run the Cascade and the Reckoning for a given turn: the server in authoritative mode, the designated host client in relay-only mode, the local client in hot seat and solo play, and the turn's closer in bundle mode (§10.4). Every other node MUST obtain the turn's outcome by replaying the turn record locally (relay-only, LAN, bundle) or by receiving its own observation stream (authoritative). No node MAY treat a Cascade it did not compute or replay as authoritative.
4.1 Order submission
MP-200 During an Orders phase a connected client MUST receive no information about the game world at all. The board is stable (10-turn-model.md TM-340) and every player sees their knowledge view exactly as the previous Cascade left it (TM-2050, TM-2060). The only thing the transport MAY deliver during an Orders phase is presence (MP-186) and chat (MP-185).
Rationale: under sequential turns this document had to specify a live fog-filtered feed to the players who were waiting, because things were happening that they could legitimately see. Now nothing is happening. The Orders phase transmits nothing, which is simpler, cheaper, and closes a whole class of leak by having no channel to leak through.
MP-205 A client MUST be able to submit order records incrementally throughout the Orders phase, in whatever order the player works, batched or one at a time (MP-280). The authority MUST validate and append each accepted record immediately (MP-300) and MUST NOT hold records until the player commits.
MP-210 Order records for a unit and stack level MUST resolve last-write-wins by ascending log sequence number (10-turn-model.md TM-230). A client MUST be able to revise any order any number of times before the phase closes, at no cost and with no record beyond the log itself (TM-360).
MP-215 CommitOrders MUST mark the issuing seat ready without freezing its orders and without
conferring any advantage (10-turn-model.md TM-390). RevokeCommit MUST be accepted while the phase
is open and rejected after CloseOrders (TM-420). Neither MUST be required: a seat that submits
nothing at all has a well-defined turn (TM-290).
MP-220 The Orders phase MUST close by exactly one authority-issued CloseOrders record, after
every seat has committed, timed out, or been marked absent or substituted (10-turn-model.md TM-430).
It MUST close immediately when every seat expected to act has committed, without waiting for the
deadline (TM-400). No other mechanism MAY close a phase.
MP-225 The authority MUST reject, with a typed rejection (03-architecture.md AR-320), any order
record naming a turn other than the current one or arriving outside that turn's Orders phase
(10-turn-model.md TM-260) — PhaseForbidden is the rejection for the latter. A late record MUST NOT
be queued for the next phase.
4.2 What the transport must not carry
MP-230 The authority MUST NOT relay any order record, or any derivative of one, to any client other than its issuer. It MUST NOT disclose another seat's order count, order kinds, unit selection, camera position, edit activity, or the timing of any of these (10-turn-model.md TM-450). Presence (MP-186) is the complete list of what a player may learn about another player during an Orders phase.
Rationale: sealed-envelope cryptography is unnecessary here and would have been theatre. The protection is structural — the server simply never sends one player's orders to another — and it is stronger than a seal, because a seal protects a payload that was nonetheless transmitted. The one mode where the payload genuinely must travel past other seats is bundle play, and §10.4 handles that with a commitment rather than by pretending the file is private.
MP-235 No client MAY submit anything during the Cascade or the Reckoning, and the authority MUST reject anything that arrives (10-turn-model.md TM-050). The transport MUST NOT offer a mid-Cascade prompt, confirmation, or interrupt of any kind, in any mode.
4.3 Delivering the result
MP-240 After the Cascade completes and the Reckoning closes, the authority MUST deliver to each seat exactly one observation stream: the compacted, ordered list of events that seat observed (10-turn-model.md TM-2150), carrying no gaps, placeholders, activation indices, per-event timestamps, or any field whose value varies with unobserved activity (TM-2160).
MP-245 In a game with two or more human players the stream MUST be transmitted as a single payload after the Cascade has fully completed (10-turn-model.md TM-2170) and MUST NOT be streamed incrementally. In solo and hot seat games it MAY be delivered incrementally (TM-2180). Playback pacing is client state and MUST NOT reach the authority (TM-2330).
MP-247 Pre-Cascade agreement: every node holding full state MUST compute the activation-order
hash of 10-turn-model.md TM-2370 at CloseOrders and exchange it before the Cascade runs; the
authority MUST compare and MUST report any mismatch as a desync immediately (TM-2380, MP-1150). The
hash travels on its own channel and MUST NOT be folded into the canonical state hash (TM-2390).
Rationale: this is the cheapest desync check in the system and the only one that fires before any
state has diverged. A few hundred bytes exchanged between CloseOrders and the first activation
catches an initiative modifier applied in a different order, a tier term contributing on one build
and not another, or a sort without a comparator — and it names the bug, where the same divergence
found three turns later at the state hash names nothing.
4.4 Hot seat scheduling and rolling planning
MP-249 In hot seat play the Orders phases of the players sharing a machine MUST be taken in sequence with full occlusion between them (10-turn-model.md TM-460, §7 below), and the resulting turn record and every hash MUST be identical to the same orders submitted concurrently. Where consecutive seats are all resolved by the same client — local humans in hot seat, or AI seats resolved by the authority — there MUST be no artificial pause between them beyond the occlusion hand-off itself.
MP-251 Where the client offers rolling planning (10-turn-model.md TM-2550), orders staged for turn T+1 while turn T plays back MUST remain entirely client-side. The transport MUST NOT transmit them, and the authority MUST reject them, until the Orders phase for T+1 has opened (TM-2570). Rolling planning MUST NOT be enabled in a mode where a player could act on the current Cascade before another player has received the same stream; where MP-245 holds, that is satisfied by construction (TM-2600).
5. Wire protocol (conceptual)
MP-250 The wire protocol is the network projection of the core's order/event model (03-architecture.md): order records flow up from client to authority; validated results flow down from authority to clients. Clients MUST never exchange raw game state peer-to-peer. What a client may legitimately receive and reconstruct depends on the game's authority mode (§6):
| Relay-only (incl. direct-LAN host and peers) | Authoritative (fog-enforced) | |
|---|---|---|
| What flows down | The canonical order log | Only the seat's compacted observation stream (TM-2150) and its own view deltas |
| What the client rebuilds | Full game state by replaying the log | Its own PlayerView only (AR-410, AR-420) |
| Fast-start object | A full-state snapshot (AR-650), derived from and SHA-256-verified against the log (AR-570) | An AR-500 ViewCheckpoint over that seat's filtered view — never full state, never the log |
| Who computes the Cascade | The designated host client (MP-195) | The server |
Authoritative-mode clients MUST NOT receive the canonical order log, RefereeView data, or
full-state snapshots under any circumstance, including fast start and reconnection (MP-290), even on
explicit request. Rationale: "snapshot" in 03-architecture.md's vocabulary (AR-650) is the complete
canonical state; serving one to a fog-enforced client would hand it every opponent's hidden position,
defeating MP-1120 information starvation and violating AR-410/AR-590.
MP-260 Transport: the primary transport MUST be WebSocket over TLS (browser-compatible), carrying
a versioned message envelope in a binary or JSON encoding. LAN play uses the identical message
protocol over a local socket. Message boundaries MUST be unambiguous to the receiver on every
supported transport, and a receiver MUST reject a malformed, truncated, or over-long message with a
typed rejection (03-architecture.md AR-320) rather than parsing it partially. On a message-oriented
transport — WebSocket, which is every transport this document currently specifies, including the
direct-LAN path (MP-440(b), MP-500) — RFC 6455 framing discharges that obligation and the protocol
MUST NOT add a length prefix of its own. A stream-oriented transport added later MUST define its
framing in the protocol reference (MP-180), exactly as MP-265 already requires of a room framework's
framing. The maximum accepted message size MUST be stated in that reference and derived from the
largest legitimate payload class — an observation stream (MP-240) or an AR-500 ViewCheckpoint —
rather than left as an unstated implementation constant. All messages carry GameId, the protocol
version Hello negotiates (MP-270, MP-1220), and a monotonically increasing per-game sequence number
for downstream messages, which Resume(lastMsgSeq) consumes (MP-270, MP-290).
Rationale: the obligation is that a receiver can find message boundaries and refuse a message it cannot trust; framing is only the means, and on WebSocket the transport already supplies it — RFC 6455 delivers each send as one discrete, length-carrying message, whole or not at all. A length prefix inside a frame that is already length-prefixed delimits nothing, and it is not free: MP-1110's threat model puts every one of these parsers at a boundary where clients are never trusted, and a hand-rolled length prefix there reintroduces the allocate-from-attacker-controlled-length class of bug that using the transport's own framing eliminates outright. The typed rejection and the stated size cap are what actually defend that boundary, so they are what this requirement states. 03-architecture.md AR-620's varint lengths are the canonical on-disk and hashing encoding — a different artifact, not wire framing.
MP-265 Room framework and the state-transport boundary. The live server MAY be built on a room
framework (the binding stack selects Colyseus) for rooms, matchmaking, lobby presence, connection
lifecycle, and reconnection. Such a framework's automatic state-synchronization — mutate server-side
objects, ship computed binary deltas — MUST NOT be used as the transport for game state. Order
records travel up and validated results travel down (MP-250); every bulk payload a client
legitimately receives is the log tail, a full-state snapshot, an AR-500 ViewCheckpoint, or the
observation stream of MP-240, and never a framework-computed delta over server-held state. Framework
state sync MAY carry only ephemeral, non-authoritative data that can neither affect simulation nor
reveal fog-hidden state: connection status, commit markers and clocks (MP-186), and pre-launch lobby
chat (MP-465). In-game chat is not such data, and neither is the observation stream, the
activation-order hash, or any order record. Whatever the sync channel does carry is additionally
bound by 08-services-platform.md SVC-402: it MUST be reconstructible from the durable store and the
log, and losing all of it at once MUST cost nothing but a reconnect. Where such a framework supplies
its own message framing, that framing is part of the protocol surface MP-180 requires to be
documented and versioned.
Rationale: three independent reasons, each sufficient. (1) Replays, spectating, mode switching, resume, and desync detection all derive from the append-only log (MP-090); a second, snapshot-shaped state channel would make the log stop being the truth — the same argument 03-architecture.md AR-500 makes from the core's side. (2) Such frameworks synchronize server state, not a per-seat filtered view — adopting one for game state would hand a fog-enforced client the referee's copy and silently defeat MP-1120. (3) The observation stream must arrive as one payload at one moment (MP-245); a delta-sync channel pushes changes as they are computed, which is precisely the timing channel 10-turn-model.md TM-2170 exists to close.
MP-270 Message families (conceptual; exact schemas in the protocol reference, MP-180):
| Direction | Message | Purpose |
|---|---|---|
| up | Hello / Auth |
Protocol + core version negotiation (AR-920); account token, guest token, or LAN identity |
| up | JoinGame, ClaimSeat, LeaveGame |
Enter a game/lobby; bind a seat; leave (never destructive) |
| up | Resume(lastMsgSeq) |
Reconnect and request what was missed — the seat's own accepted order records and any completed observation stream in authoritative mode, the raw log tail in relay-only mode (MP-290) |
| up | SubmitOrders(turn, seat, orders[], clientBatchId) |
A transport batch of AR-260 order records, each carrying its own ClientTag; clientBatchId correlates the reply only (MP-280) |
| up | CommitOrders(turn, seat) / RevokeCommit(turn, seat) |
Mark the seat ready for this turn; withdraw that mark (MP-215) |
| up | ActivationOrderHash(turn, hash) |
This node's TM-2370 activation-order hash, sent by every node holding full state, before the Cascade (MP-247) |
| up | TurnHash(turn, hash) |
The reporting node's own AR-130 TurnHash for the Reckoning just closed. Sent by every connected node that legitimately holds full state — the relay-only host client, direct-LAN peers, and any client entitled to the whole log — per MP-1150. Fog-filtered authoritative-mode clients never send this: they hold no canonical state and instead compare the ViewHash carried on ViewCheckpoint |
| up | RequestCheckpoint (authoritative) / RequestSnapshot (relay-only) |
Ask for an AR-500 ViewCheckpoint of this seat's view; or, for a client already entitled to the whole log, a full-state snapshot. A RequestSnapshot from an authoritative-mode client MUST be answered with a ViewCheckpoint, never a snapshot (MP-250) |
| up | Chat, Ping, Concede |
Ancillary; Concede is an ordinary logged order (14-victory.md VC-320) |
| down | GameInfo |
Roster, mode, authority mode (§6) and how it was set (MP-905/MP-906), pinned versions/hashes including Initiative Tier (MP-110), policies, spectating rules |
| down | PhaseBegin(turn, phase) |
The game entered Orders, Cascade, or Reckoning (TM-090). The only phase notification; there is no per-seat turn begin |
| down | OrdersClosed(turn, commitMarkers[]) |
The phase closed, with each seat's terminal marker — Committed, TimedOut, Absent or Substituted (TM-300) |
| down | Observations(turn, events[]) |
The seat's complete compacted observation stream for the Cascade just computed, as one payload (MP-240, MP-245) |
| down | ViewCheckpoint / ViewDelta (AR-500) |
The seat's fog-filtered view: periodic full checkpoint of its PlayerView plus deltas, carrying the ViewHash of MP-1150 |
| down | OrderResults(clientBatchId, results[]) |
One result per submitted ClientTag: the assigned log Seq, a typed AR-320 rejection, or NotAttempted (MP-285) |
| down | ActivationOrderHash(turn, hash) / TurnHash(turn, hash) |
The authority's digests for comparison (§15, AR-530). TurnHash is AR-130's 64-bit XXH3, explicitly not the SHA-256 StateHash, which is reserved for snapshots and manifests |
| down | Presence, ChatRelay, Control |
Presence and clocks (MP-186); chat; substitution / host-migration / mode-switch / rollback notices |
Implementation note, 2026-09-12, the first of this document. §5's contract is built and no
transport is, which is the order the document's own obligations imply: MP-250's authority matrix is a
security property, MP-260's framing rules are a codec, MP-270's families are a discriminated union, and
MP-285's batch semantics are pure logic. All four are right or wrong before a socket exists. They live
in packages/core/src/net/, which 03-architecture.md AR-020 settles — the graph is exactly client→core
and server→core with nothing in core depending on either, so a contract both sides must agree on has
one legal home. @everylastcity/server is named in that graph, does not exist, and will speak this.
Three readings recorded.
1. MP-250's matrix is implemented as a predicate every downstream message passes through, not as a
convention. The clause is absolute — "under any circumstance, including fast start and reconnection,
even on explicit request" — and a claim about every circumstance is a claim about every code path. One
function is the only way to make that checkable, and the test enumerates the whole downstream union
rather than sampling it, so a message added later that ships full state fails there rather than passing
by not having been thought of. MP-270's own special case is a second function for the same reason: an
authority answering RequestSnapshot must be unable to answer the question the client asked instead of
the question the mode allows.
2. The size cap is checked before the parse, and the order is the requirement's priority rather than
an optimisation. MP-260 wants the maximum "derived from the largest legitimate payload class … rather
than left as an unstated implementation constant", and MP-1110 puts every one of these parsers "at a
boundary where clients are never trusted". A codec that parsed first and measured second would already
have paid the cost the cap exists to refuse. The derivation is written where the number is, the byte
count is a UTF-8 count rather than a code-unit count — a message of astral characters is four bytes per
character and length would admit four times the cap — and it is counted rather than encoded, because
allocating the bytes to measure them is the allocation the cap exists to avoid.
3. MP-280's and MP-285's interaction is the part neither states, and it is what makes whole-batch
retry converge. A retried batch that partly landed must come back in the same shape: the tags that
landed answered from the index, the order that was rejected answered with its recorded rejection,
and the suffix still NotAttempted. That last is the subtle one — AR-340 returns the original result
and a rejected order's original result is a rejection, so an authority that re-attempted it would sail
past the stopping point and apply a suffix the client was told was never tried. An accepted tag
mid-batch, by contrast, does not stop the batch. Both directions are pinned.
One shortfall, stated rather than worked around: AR-260's order envelope is unbuilt. The shipped
OrderRecord carries no ClientTag, ChainHash or Issuer, so AR-340's "a ClientTag already
present in the log" has no field to key on and the index here is authority-side transport state. An
authority rebuilt from the log would forget it and could re-apply a retried order. applyBatch takes
the index as an argument for that reason — the day the field lands the caller passes a view over the
log and nothing in this module changes.
MP-280 Idempotency is per order, not per batch. A SubmitOrders batch is a transport grouping
only: each element is an AR-260 order envelope carrying its own ClientTag, and resubmitting a
ClientTag already present in the log MUST return that order's original result without re-applying
it (AR-340). clientBatchId is transport correlation — it names the OrderResults reply and makes
whole-batch retry safe — and MUST NOT be used as the idempotency key.
MP-285 Batch outcome semantics: a batch is not atomic. The authority applies its orders in
array order, each one atomically, and MUST stop at the first rejection — orders before it are applied
and enter the log with their assigned Seq; the rejected order carries its typed AR-320 reason;
every order after it MUST NOT be applied and is reported NotAttempted. OrderResults returns
exactly one entry per submitted ClientTag so the client can reconcile without guessing.
Rationale: prefix application matches the incremental flow of MP-205 — a batch is only a network
optimization over records the player made one at a time, so a late rejection must neither undo
earlier accepted revisions nor let later ones apply against a state the player never saw. Because
order records are revisions of the player's own plan and not moves in a shared world, a partially
applied batch leaves a perfectly coherent Orders phase that the player can inspect and finish.
MP-290 Reconnection. What resuming means depends on the phase the game is in when the client returns:
- Mid-Orders — the common case, and the one correspondence play depends on. The client MUST
recover every order record it had accepted, identified by
ClientTag(MP-280), and MUST be able to continue editing and commit normally. Uncommitted client-side staging is client state and is restored by the client where it offers staging (10-turn-model.md TM-470; 04-ui-ux.md owns staging). Records whose results were never received MAY be resubmitted with no risk of double application. - Mid-Cascade or mid-Reckoning — there is nothing to resume, because neither phase takes input (TM-050). The client MUST wait and MUST receive the complete observation stream when the Cascade finishes, exactly as a connected client would (MP-245).
- Gap too large to ship as a tail — the mode decides the fast-start object (MP-250).
Authoritative: an AR-500
ViewCheckpointof that seat'sPlayerViewplus the subsequent deltas and any observation streams it missed. A reconnecting client MUST NOT be sent a full-state snapshot or any segment of the canonical log, even if it asks (AR-410, AR-590, MP-1120). Relay-only / direct LAN: a full-state snapshot (AR-650) plus the log tail, verified by SHA-256 against the manifest (AR-570) — permissible only because such clients are already entitled to the whole log.
Reconnect MUST be automatic. Disconnection MUST NOT commit a seat, MUST NOT clear its orders, and MUST NOT close a phase; a seat that never returns before the deadline is handled by §13.
no-test-needed: reconnection needs a client and a live transport, neither of which exists. The one clause the core can state — that a reconnecting client is never sent a full-state snapshot or a segment of the canonical log — is MP-1120, held at core/test/net/fog.test.ts.
MP-300 Validation: the authority MUST validate every order record against the pinned core's legality rules for the submitting seat before appending it (10-turn-model.md TM-250). Rejected records never enter the log. Clients SHOULD pre-validate locally against the same core so rejections are rare and instant — which they can do completely for their own orders, because an Orders phase mutates only per-player state (TM-350).
MP-310 Randomness: all random draws are made by the authority's core PRNG (03-architecture.md). Clients in authoritative mode never draw randomness; in relay/host mode only the host's core draws.
MP-320 Timing targets. Order-record acknowledgment SHOULD be ≤ 250 ms server-side processing at the 95th percentile in live games. The Cascade itself is bounded by 10-turn-model.md TM-2480 (200 ms for 1,200 activations). Observation-stream dispatch MUST begin within 100 ms of the Reckoning closing and MUST satisfy the simultaneity requirement of MP-1125. Correspondence games have no latency requirement beyond notification delivery (§12).
Rationale: latency stopped being a fairness question under this model and became purely a comfort question. There is no race to submit (MP-135), so a 300 ms connection costs a player nothing but the delay before their own edit is confirmed. The number that now matters is the one in MP-1125 — whether every player receives the turn's result at the same moment — because that is the only remaining place where a timing difference could mean anything.
MP-330 The protocol MUST carry no client-authored derived state: no computed combat results, no visibility claims, no initiative values, no activation indices. Everything a client asserts is either an order record, a digest for comparison (MP-247, MP-1150), or ancillary metadata.
6. Authority modes: relay-only vs authoritative
MP-340 Every server-connected game runs in exactly one authority mode at any instant, chosen at
creation or — for a game that enters a server mode by conversion from hot seat, turn-bundle, or
drop-box play — at that conversion (§11, MP-905). The mode in force MUST be carried in GameInfo and
shown to all joiners:
| Relay-only | Authoritative | |
|---|---|---|
| Who runs the Cascade and Reckoning | One designated client (the host) runs the full core; the server relays and stores | @everylastcity/server runs the headless core |
| Server sees | The canonical order log and metadata (needed for persistence, resume, mode switching); it does not interpret it | Full game state (it computes it) |
| Server cannot see | Nothing is hidden from the server unless E2E encryption is enabled (MP-370) | n/a |
| Fog enforcement | Client-side only; the host client necessarily holds full state — trust-based among friends | Server-side: each seat receives only its own observation stream (§15) |
| Randomness | Host's core PRNG | Server's core PRNG |
| Order validation | Host core validates; server checks only log integrity and sequencing | Server core validates every record |
| Eligible for rated play | No | Yes (08-services-platform.md) |
| Cost profile | Near-zero server compute | Server compute per game |
MP-350 Default: casual games default to relay-only; rated, tournament, and "fog-enforced" flagged games MUST be authoritative. The lobby MUST display the mode and its trust implications in plain language before players join.
Partially held 2026-09-28: both authority modes stand with their default, each mode's behaviour pinned; what waits is the rating gate and the pre-join trust display. Relay-only is the default in packages/server/src/config.ts exactly as this row makes it ("relay-only by default — it needs no fog machinery to play"); relay mode stands at MP-247's digest cross-check in packages/server/test/host.test.ts, and authoritative server-side execution at AI-480's pins in packages/server/test/ai-seat.test.ts. What waits: rated, tournament and flagged games forcing the authoritative mode — no rating surface exists anywhere in this tree — and the lobby stating a mode's trust implications in plain language before players join; the public listing (MP-480's pin shows exactly its named fields) carries neither.
MP-360 In relay-only mode the server MUST still: sequence and durably store the log; verify
hash-chain integrity (§15); collect and cross-check per-node ActivationOrderHash (MP-247) and
TurnHash (AR-130) values and flag mismatches; and support resume, mode switch and spectate from the
stored log.
Partially held 2026-09-28: sequencing, digest cross-check with mismatch recording, durable storage and resume from it stand at their pins; mode switch — and spectate from the stored log — wait. The server sequences the relay's log (per-seat strictly increasing envelopes pinned at packages/server/test/host.test.ts under MP-260/MP-100) and collects and cross-checks digests: MP-247's pin there shows the close waiting for every full-state seat, then recording each mismatch once; resume stands within one session at wire.test.ts (MP-170/MP-550, MP-570). The word "durably" has landed at MP-160's pins below: every accepted record rewrites an AR-550 container, and a restarted process resumes the game from it; what waits is mode switch from the stored log (MP-600) and spectating from it.
MP-370 Optional E2E-encrypted relay: a relay-only game MAY be created with end-to-end encryption (participants hold the key; server stores ciphertext). This forfeits every server-side feature that reads the log — server-rendered spectating, AI substitution, web replay, and server-side deadline enforcement beyond a wall-clock timer — and the creation UI MUST state these trade-offs. If it ships, it ships behind a feature flag (open question 2).
no-test-needed: an option this build never exercises — the row opens with MAY, and no encryption surface exists anywhere on the wire path of packages/server or packages/core (the one discussion of encryption in the tree, core/src/net/bundle.ts, names what is deliberately absent); every feature it would forfeit is itself unbuilt, so there is nothing a test could confirm being lost. If it ships behind its flag, its pin must show ciphertext at rest on the server plus the creation UI naming each forfeited feature in turn.
MP-380 Correspondence games SHOULD default to authoritative mode: fog enforcement matters most when an Orders phase sits open on a server for days and no other player is watching. Relay-style correspondence (server stores; the host client validates and runs the Cascade on its next connection) MAY be offered for mod content the server cannot execute, and MUST be labelled as unenforced.
no-test-needed: both halves — the authoritative default and the labelled relay-style option for mod content — bind a correspondence mode that does not exist yet (nothing under packages/server names one, and its rows wait with section 9), so there is no game whose default this could check or label. The machinery beneath it stands: relay versus authoritative with their respective digest gates at MP-350's pins above. When correspondence lands, the row tests as its default plus the unenforced label.
7. Hot seat
MP-390 A game with two or more local-human seats on one device is a hot seat game. It MUST work
fully offline with no account; each local seat MAY be bound to a local profile (for options, keybinds
and stats) or played as an unnamed guest seat.
MP-400 Occlusion hand-off: between local humans' Orders phases the client MUST display a neutral hand-off screen showing the next player's name and colours, with the map and all reports hidden, requiring explicit confirmation ("I am <name>") before revealing that seat's fog-filtered view (10-turn-model.md TM-460). Design decision: blanking is mandatory and not configurable. A shared screen is the one place where fog has no technical enforcement at all, and the cost of the rule is one keypress per hand-off against the certainty that an accidental glance cannot happen. There is no case in which a player benefits from the option to skip it.
MP-410 Optional per-profile PIN: a local profile MAY set a PIN required at its hand-off. Default off.
MP-420 Everything rendered between hand-off confirmations MUST be restricted to the active seat's fog-filtered view and its own observation stream — including reports, graphs, chat, the Dispatch (13-command.md), and combat detail. In particular the client MUST NOT render, for the seat currently at the machine, any part of another seat's observation stream from the previous Cascade.
MP-430 Playback in hot seat: the Cascade playback belongs to no single seat, so the client MUST either play it once per seat behind the occlusion screen, each seat seeing only its own stream, or offer a combined post-Cascade summary restricted to the union of nothing — that is, no combined view at all. A single shared playback MUST NOT be offered in a game with two or more human seats.
Rationale: this is the one genuinely new problem the turn model creates for hot seat, and it needs saying rather than discovering. Under sequential turns each player watched their own turn and there was nothing to share. Now a single Cascade produces a different, incompatible truth for each seat, and the tempting shortcut — show everyone the whole thing on the shared screen — is a total fog collapse. Per-seat playback behind the occlusion screen costs a few seconds per turn and preserves the game.
MP-435 A hot seat game MUST be switchable to any other mode and vice versa (§11). A hot seat game on a device with a signed-in account MAY be uploaded to become a correspondence game, with the other local seats invited out to remote players.
8. Live play (Internet and LAN)
8.1 Session topologies
MP-440 Live play MUST support two topologies with the same protocol (§5):
(a) Server-relayed: all clients connect to an @everylastcity/server instance (official or
self-hosted); no player opens ports; the server is relay-only or authoritative per §6.
(b) Direct LAN: one client hosts on the local network; peers connect directly; the host is the
authority (relay-only semantics). Hosting in (b) requires a client build that can open a listening
socket — the desktop and mobile builds. A browser-tab client MAY join a direct-LAN game but MUST NOT
be required to host one, and the UI MUST NOT offer hosting on a build that cannot. Rationale: (a)
removes port forwarding and external-IP configuration, which is the single largest source of failure
in player-hosted networking. (b) preserves infrastructure-free play (MP-080). The hosting caveat is a
platform capability limit, not a rules or mode limit: a browser-tab participant plays the identical
game over the identical protocol.
MP-450 Direct hosting over the open Internet MUST remain possible for users who want it (manual
host:port entry, MP-500), but is never required and never the default.
8.2 Lobbies, invites, discovery
MP-460 Creating a live game opens a lobby: the creator configures setup (owned by 04-ui-ux.md and 01-game-rules.md), and the lobby displays the roster, pinned content, rules summary, Initiative Tier, deadline and reserve defaults (§13), authority mode, and policies. The creator holds the lobby admin role: assigning joiners to seats, reordering seats, setting AI seats, kicking, and launching.
MP-465 Lobby chat is a distinct, deliberately expendable capability: a lobby MUST provide a text
channel among its current occupants before launch, for arranging setup, seats, and start time. It is
not in-game chat (MP-185): it is not part of the game record, never enters the order log, and is
not retained past launch, so it MAY be carried on a room framework's ephemeral state-sync channel
(MP-265; 08-services-platform.md SVC-402). At Launch Game (MP-520) — or, for a correspondence game
created under these lobby semantics, at the deferred start of MP-660 — the lobby channel closes and
all further conversation is in-game chat over the MP-270 Chat / ChatRelay messages. The client
MUST make the distinction visible, so no player assumes a pre-launch message is preserved in the game
record.
MP-470 Private lobbies are joined by invite link or short join code, generated automatically and never empty. The admin MAY regenerate the code at any time.
MP-480 Public listing: the admin MAY list the lobby publicly under a game name. The listing MUST show name, ruleset, unit set, map size, seats free, deadline preset, authority mode, and host region, and MUST NOT expose the join code. A listed lobby MAY additionally be marked open (join without code).
MP-490 LAN discovery: client builds with access to link-local discovery — the desktop and mobile builds — MUST discover LAN lobbies on the same network automatically (mDNS/UDP broadcast). A browser-tab client has no access to mDNS or UDP broadcast and MUST fall back to manual connection (MP-500) or an invite link; the absence of discovery MUST be stated in the UI rather than presented as an empty lobby list.
MP-500 Manual connection: entering host:port (or an invite URL) directly MUST always work, for
VPNs, airgapped LANs, and direct Internet hosting (MP-450). A browser-tab client served over HTTPS
cannot open a plaintext ws:// connection to a LAN host, which constrains the supported LAN path for
that build (open question 8).
MP-510 Friends/invites: with an account, players MAY invite friends directly; invitees receive a push notification deep-linking into the lobby (08-services-platform.md).
no-test-needed: an invite with a push notification deep-linking into a lobby presupposes accounts and channel infrastructure, both owned by 08-services-platform.md — one covered requirement of its one hundred twenty-six in this tree — and nothing under packages/server or packages/client implements either. The code-based join (MP-470's pins) is the in-repo half that exists; account-bound invites become testable when identity lands.
8.3 Launch
MP-520 Launch requires every seat resolved: each human seat bound to a present, connected player
and each remaining seat set to AI or removed. The admin's Launch Game starts the game; map
generation (if procedural) occurs at launch on the authority; turn 1's Orders phase opens once every
starting unit has a complete order stack (10-turn-model.md TM-070).
MP-530 The admin need not occupy a playing seat; an adminless-seat game is the dedicated-server case.
MP-540 Seat assignment is by the admin or automatic. There MUST be no requirement that the host occupy any particular player index. Rationale: under this turn model the player index is a tie-break input to nothing a player can feel — activation order is Initiative with a creation-sequence tiebreak (TM-1150), not player order — so index carries no advantage and no rule may give it one.
8.4 Join, leave, and absence during play
MP-550 After launch, clients MAY disconnect and reconnect freely without affecting other players (MP-290). A disconnected seat does not stop the game: its persisted orders execute (10-turn-model.md TM-530, TM-610), the Orders phase closes on the deadline and reserve schedule of §13, and the Cascade runs. All connected clients MUST see which seats have not committed and how long remains (MP-186).
Rationale: this is the requirement that most visibly improves under the new model. Under sequential turns, an absent player froze the game at their seat and every other player sat and waited; the old draft's answer was "the game waits at that seat's turn". Now an absent player's army continues a coherent plan and the turn resolves on schedule. Absence degraded from a stall into a disadvantage borne by the absent player alone.
MP-560 Leaving is uniform and safe: a single "Leave game" action disconnects with no ceremony — the game state lives with the authority.
8.5 Resume and host migration
MP-570 Server-relayed games: resuming requires no clients present — any participant reopens the game from their game list; the server restores the session and notifies the roster. A correspondence game does not need resuming at all, because it was never suspended (MP-615).
MP-580 Server-relayed relay-only games: if the designated host client disconnects, the server
MUST migrate the host (authority) role to the next connected client by player index within 10
seconds, resuming from the canonical log; if none is connected, the game holds at the current phase
boundary and resumes when any participant returns. Migration MUST be announced via Control, MUST
occur only at a phase boundary, and MUST NOT be performed mid-Cascade or mid-Reckoning
(10-turn-model.md TM-620). No order record, digest, or PRNG state may be lost or replayed
differently.
no-test-needed: no client holds an authority role in this build — relay-only clients are log-entitled replayers of the canonical log (the bootstrap carrier in packages/server/src/host.ts) and the server is a single process — so there is no host role to migrate between connected clients, and no Control migration notice is ever emitted from packages/server/src although the frame family exists for it (core/src/net/protocol.ts, MP-270). It becomes testable when designated-host clients land: the ten-second bound, phase-boundary-only announcement through Control, and no PRNG state lost or replayed differently.
MP-590 Direct LAN games: the canonical log lives on the host, and every client MUST retain its
received log prefix. Any machine holding the full log MAY restart the game as the new host: it opens
the game from its saved log, re-hosts, and other clients rejoin by discovery or address. Design
decision for a host lost mid-turn: the game resumes from the last completed Reckoning boundary
present on the recovering machine, with an explicit on-screen statement of which turn was lost, and
every seat's order records for the abandoned Orders phase are discarded rather than partially
replayed. A phase is not a resumable unit; a turn is. Rationale: an Orders phase is only meaningful
as a complete set of commitments closed by one CloseOrders, and replaying a fragment of one would
produce a turn nobody submitted. Rewinding one turn is a small, comprehensible loss; a
half-reconstructed turn is an incomprehensible one.
Partially held 2026-09-28: the client-retention half stands, and restarting from a saved log exists now on the server authority — any machine holding it still waits. A fresh relay-only client receives setup plus seed as its bootstrap carrier (packages/server/src/host.ts) and rebuilds canonical state from the server's LogTails turn for turn exactly as the humans-vs-floor smoke demonstrates, with resume over a dropped socket pinned at wire.test.ts (MP-170/MP-550); MP-450's pin keeps an explicitly bound host:port reachable by any client that knows it. Restarting from a saved log now stands on this row's own terms in one reading only: the server process re-hosts its persisted log through the same single door every live order uses, to an identical state on the same GameId (MP-160's pins at packages/server/test/durable.test.ts). Flagged for the spec owner: that resume continues the open Orders phase from exact replay rather than this row's Reckoning-boundary discard — a client re-hosting a fragment its peers never all saw is what that rule targets, and an authority re-reading its own complete log is not. Rejoining by discovery still waits — no discovery surface exists in this tree, so a rejoinder must name the address. Restarting from a saved log on any other machine no longer waits as of 2026-09-29: --import /slot-dir opens an already-playing slot from its directory alone (the server process door at rehost.ts) — identity and every rules-read value from initial.bin plus the manifest, this process’s serving choices at v1 defaults where no operator file exists (flagged reading for the spec owner). Pinned in packages/server/test/durable.test.ts — an imported slot replays to the crash point, keeps its own identity, continues one gapless chain, and none/unreadable/finished pass through named — with the flag surface pinned in packages/server/test/config-surface.test.ts.
MP-600 A resumed live game MAY be resumed into a different mode instead (§11). The intended hybrid is explicit: play live for the first session, then continue as correspondence when people disperse, with no interruption to the game and no requirement that any particular seat be the one to convert it (MP-930).
Partially held 2026-09-28: the mode switch itself stands — consent-gated with seed and pins untouched — at its core pins; resuming a live game into another mode waits on the server. Switching modes between turns is built in the core, with its consent gates pinned at packages/core/test/net/mode.test.ts per this document's own note under that cluster (MP-900, MP-920, MP-940), the seed and pinned versions ride through untouched, and the Control family already names mode switch on the wire (core/src/net/protocol.ts). What waits: the live seam — nothing in packages/server/src executes a resume into another mode, so the intended hybrid, live now and correspondence after dispersal, has no server flow to test.
9. Correspondence play
Delivery phase: Phase 5. This is the server-hosted asynchronous mode: the durable service owns the log and players exchange no files. The 2026-09-19 scheduling ruling that defers signed file play does not defer this section.
9.1 Model
MP-610 A correspondence game lives on an @everylastcity/server instance. The server holds the
log, opens each Orders phase, accepts each seat's order records whenever that player connects, closes
the phase when every seat has committed or the deadline and reserve schedule expires (§13), runs the
Cascade and the Reckoning, delivers every seat its observation stream (MP-240), opens the next Orders
phase, and notifies everyone (§12). There are no user-visible files, directories, position numbers,
mail settings, or polling screens.
Partially held 2026-09-28: the phase machinery this row describes stands on the server today — per-seat commits, deadline and reserve close, Cascade and Reckoning execution, per-seat observation dispatch; what waits is a service holding it across sessions for months. packages/server/test/deadline.test.ts pins the host closing an Orders phase on its schedule (TM-520 through TM-570) with the cascade matching a timely commit, and wire.test.ts pins each seat's observation stream as one payload at the boundary (MP-240/MP-245). What waits: the durable service itself — packages/server now holds its log across restarts at MP-160's pins but no correspondence service wraps it, and no phase ever opens for an absent player, and none of the "no user-visible files" prohibitions has a UI to bind yet.
MP-615 Correspondence play MUST NOT depend on a live session. The authority for a correspondence game is the durable service and its persisted log (MP-160); a live room, if the deployment uses one (MP-265), is materialized only while a participant is connected and disposed when the last one leaves — an event that MUST NOT end, pause, stall, or otherwise alter the game. Every correspondence operation MUST be performable with no room in existence and no other participant online: claiming a seat (MP-650); submitting order records and committing (MP-205, MP-215); closing the Orders phase, running the Cascade and the Reckoning, and dispatching every seat's observation stream (MP-220, MP-240); deadline and reserve expiry (§13, MP-1000); rollback (MP-740); and archival and revival (MP-720). No obligation of §12 or §13, and no phase transition, may be implemented as behaviour of a room object.
Rationale: rooms in the chosen framework are ephemeral and in-memory, while correspondence play is a primary mode with a design lifetime of months to years (MP-630). Binding a years-long game to a process-lifetime object would be an availability regression against plain file passing, where the turn outlives every machine that touches it. The Cascade is explicitly on this list because it is the one operation that must run for a game nobody is watching: the deadline expires at 03:00 and the turn resolves, or correspondence play does not work.
Partially held 2026-09-28: one game keeps running with zero connected clients, pinned; the rest of this row — every operation doable with no room and nobody online — waits on durability. wire.test.ts pins MP-570 directly: resuming needs no clients present, the session survives total absence, a late claim re-notifies the roster — but that is one process holding memory. Log survival across restart no longer waits — packages/server/test/durable.test.ts pins it at MP-160's row, a resumed process re-hosting its open phase from the persisted log; and deadline-driven close while unattended now runs, with one honest gap (a resume re-arms the phase clock from the operator configuration rather than recomputing remaining time from storage — SVC-412). What waits: seat claiming and submission with nobody connected, and this row's remainder beyond a running log — rollback, archival and revival.
MP-620 A correspondence Orders phase MUST support multi-sitting work: the player connects from any device, revises some orders, leaves, returns, revises more, and commits when ready. Accepted records persist server-side across sittings (MP-290); uncommitted client staging is restored per device where the client offers it (10-turn-model.md TM-470).
Partially held 2026-09-28: multi-sitting stands within one running session — accepted records survive a client leaving and returning, and per-device staging restore is pinned; the sitting survives no restart yet. A dropped socket keeps its accepted orders (wire.test.ts, MP-170/MP-550) and revision before commit costs nothing extra (deadline.test.ts, TM-540), so connect-revise-leave-return-commit plays out today over real sockets; the uncommitted staging half stands at UX-1430's pin in packages/client/test/background-staging.test.ts. The persistence that blocked them has landed at MP-160's pins (packages/server/test/durable.test.ts): accepted records survive the server's own restart, and the resumed process continues the open phase they sit in; what waits is custody past one instance (§5's store).
MP-625 One submission per player per turn. A correspondence turn MUST cost each player exactly one round-trip — open the game, issue orders against a stable board, commit — regardless of how many units they own, and MUST NOT require any further exchange before the turn resolves.
Rationale: this is the property that made the turn model worth adopting and it belongs in the transport document as a requirement, not as an emergent hope. Under interactive per-unit activation a correspondence turn needs one round-trip per unit activation: six players with thirty units each is 180 sequential round-trips, potentially hours apart, which is not a slow game but a broken one. Under Orders and the Cascade the count is one per player per turn and it does not grow with army size — a player with 200 units submits once, exactly as a player with 20 does. Everything else in this section is bookkeeping around that single fact.
Partially held 2026-09-28: a turn costs exactly one commit per seat and nothing more, as far as this tree can play it; what waits is proving the round-trip count across connections that do not exist yet. The close gate counts commitments, not units — the server's phase closes on each present seat having committed (wire.test.ts plays two seats against one AI team with every turn closing on exactly those commitments), and once closed the Cascade and Reckoning run at the boundary with no further exchange in connected play (deadline.test.ts). What waits: this row's actual subject — a player who opens, issues orders, commits and leaves, months apart, needing nothing more — which presupposes section 9's durable service.
MP-630 Correspondence games are long-lived: the design lifetime is months to years. Server retention policy MUST NOT silently destroy games (MP-720).
no-test-needed: a design lifetime of months to years is an obligation on retention, and retention does not exist yet — the dormancy and archive machinery belongs to MP-720 and the instance's data-retention policy under 08-services-platform.md, neither of which appears anywhere in this tree; while no durable store exists there is also nothing that could silently destroy a game. It becomes testable with the archive/revive surface: a stalled game frozen past its dormancy period, delisted yet revivable by one action.
9.2 Creation and joining
MP-640 Creation: any account holder creates the game (setup per §8.2 lobby semantics, minus live presence), marks human seats as invited (by account, contact link, or open code), and publishes it. The creator need not hold any particular player index (MP-540).
no-test-needed: game creation by an account holder, with invites by account, contact link or open code, is the services platform. Nothing in it reaches the core.
MP-650 Joining: invitees receive a link or code; opening it shows the game summary and lets them claim their designated seat (or pick a free seat if the invite is open). Seat claiming MUST be server-transacted, so a mistyped identifier cannot corrupt the roster.
Partially held 2026-09-28: a seat claim is already server-transacted over the wire; what waits are the correspondents' invitations — links, codes and open seats — that have no endpoint yet. ClaimSeat crosses the live boundary as an ordinary transaction (wire.test.ts, MP-260/MP-110), and the lobby's seating surface enforces exactly its seats, refusing double-seating and taken slots (lobby.test.ts, MP-460) — a mistyped identifier cannot corrupt a roster it does not name. What waits: invite links or codes for a correspondence game designating or opening a seat; no such endpoint exists under packages/server.
MP-660 Deferred start: by default the game starts — map generated, turn 1's Orders phase opened — when all human seats are claimed. The creator MAY alternatively start immediately with unclaimed seats held by stand-in AI until claimed. All claimants MUST be notified at start.
Partially held 2026-09-28: the launch gate — every seat resolved before play begins, unclaimable gaps resolvable to AI — stands in the lobby; what waits is deferral of the start itself and the notice to claimants. packages/server/test/lobby-auto.test.ts pins the automatic face: no launch into an empty room, setup-designated AI seats pinned, joiners seated in order onto free slots, dark occupants released so the gate stays reachable; lobby.test.ts's MP-520 names the first failing seat that blocks launch. What waits: a game that does not start until every human seat is claimed — stand-in AIs holding the unclaimed meanwhile — and notification of claimants at start, both service territory.
9.3 Running the game
MP-670 Phase advance: when the last seat commits, or the deadline and reserve schedule expires
(§13), the server MUST issue CloseOrders, run the Cascade and the Reckoning, dispatch every seat's
observation stream under MP-245, open the next Orders phase, and notify every player — not only some
next player, because there is no next player.
Partially held 2026-09-28: the phase advance itself stands — last commit or deadline expiry closes the phase, the Cascade and Reckoning run, every seat gets its observation stream at the boundary; what waits is telling the players who are not connected. deadline.test.ts pins the schedule side (TM-520/MP-1030: an absent seat times out at expiry and the live game proceeds), wire.test.ts pins each seat's observation stream delivered once, in one payload, at the boundary (MP-240/MP-245), and both run with no notion of a next player — dispatch is per-seat. What waits: notifying every player who is offline about the new phase; that is section 12's service, which does not exist here.
MP-680 The player's game list MUST show, for every correspondence game: the turn number; whether you have committed; how many seats have committed out of how many; the deadline remaining and your reserve bank remaining; whether a Cascade result is waiting to be watched; and unread chat. An "open my next game awaiting orders" action MUST cycle the player through games where they have not yet committed.
Rationale: the old game list answered "is it my turn?", which was the only question sequential play allowed. The question now is "have I committed?", and it has a different shape: it is never exclusive, several games can want you at once, and the answer for a game you have committed to is "nothing, relax" rather than "wait for six other people in order".
no-test-needed: a game list showing commit status with an action cycling through uncommitted games — all of it client UI over correspondence state no endpoint serves; the only game list in this tree answers a different question (the local Load Game list, 04-ui-ux.md UX-080). It becomes testable when section 9 lands: the list's fields asserted against a service holding committed and uncommitted seats alike.
MP-690 Multiple local humans MAY share one device within a correspondence game: each claims their seat from that device, and the occlusion hand-off of §7 applies between them.
no-test-needed: a permissive arrangement whose only normative content is that section 7's occlusion rules apply between the local players — and those rules stand titled in packages/client/test/handoff.test.ts (MP-390, MP-400), where no argument makes a hot-seat hand-off go away. The seat claiming this would ride on belongs to section 9's unbuilt service.
MP-700 The server transactionally owns phase state and possession. There MUST be no user-visible retry, hold, re-upload, or registry-repair mechanism of any kind, and no user action may leave a game in a state requiring manual repair. A client crash mid-Orders loses at most unacknowledged order records (MP-290).
Partially held 2026-09-28: within one running session no user action can leave a game requiring manual repair — a retry converges on the original results without re-applying — and a crash mid-Orders loses at most unacknowledged records; what waits is transactional ownership across restarts. host.test.ts pins MP-285's convergence (a retry of the same batch converges on the original results without re-applying, MP-280/AR-340) — there is no hold, re-upload or registry-repair mechanism because idempotence makes them unnecessary; wire.test.ts pins MP-290's verbatim resume as the loss bound. What waits: phase state surviving a process restart at all, which needs the store MP-160 records as unbuilt.
MP-710 Contact changes: a player changing accounts or notification addresses updates their own account (08-services-platform.md); a game admin MAY re-issue an invite to rebind an unclaimed or abandoned seat.
no-test-needed: contact changes ride on account machinery that does not exist in this tree — the player half belongs to 08-services-platform.md, and the game-admin invite rebind presupposes both accounts and invites; nothing under packages/server has either. It becomes testable when identity and invitation endpoints land.
9.4 Retention
MP-720 Inactivity: a fully stalled correspondence game (no orders from anyone) is nudged per §12, then after a server-configurable dormancy period (default 180 days, minimum 90) archived — frozen, delisted from active lists, but retained and revivable by any participant with one action. Hard deletion only per the instance's data-retention policy (08-services-platform.md). A game MUST NEVER be destroyed by timeout.
no-test-needed: no dormancy, archive or revival machinery exists — nothing under packages/server names one, nudges included — and the retention policy it defers to lives in 08-services-platform.md; while there is also no durable store, there is nothing a timeout could destroy either. Its sharpest clause (a game never destroyed by timeout) holds today by the absence of any such code, and the row becomes testable when the archive/revive surface lands: frozen past its dormancy period, delisted, revivable by one action.
MP-730 Personal backups, scoped by authority mode (§6). What a participant may export from a live game is bounded by what that participant is already entitled to hold:
- Games without fog enforcement (relay-only, bundle, drop-box, hot seat): every participant MAY at any time export the game as a turn-bundle (§10) or replay file. This adds no exposure — such participants already hold the full log by construction (MP-250, MP-790).
- Authoritative fog-enforced games (the MP-380 correspondence default): a participant's mid-game export MUST be limited to their own fog-filtered view cache — the AR-590 client artifact of view checkpoints, deltas and past observation streams for their seat. It restores that player's own view on a new device and is useless for continuing the game alone. A full bundle or replay of a live fog-enforced game MUST NOT be offered to any participant.
A participant obtains the full container of a fog-enforced game only (a) after the game ends, via the replay of MP-1100, or (b) through a consent-gated mode switch out of authoritative fog-enforced mode — server → bundle (MP-920), server → hot seat (MP-940), or any other such switch, all governed by the gate in MP-900 — which requires all active human participants' consent precisely because it relocates full state onto a player device. No notification-only or admin-unilateral path to that switch exists. Operator-side export is a separate right (MP-1230) and MUST NOT be exposed as a per-participant path around this rule.
9.5 Rollback as an audited admin action
no-test-needed: what a participant may export is a client product rule over artifacts the core does not hold. The consent gate it defers to is tested: MP-900, MP-920 and MP-940 at core/test/net/mode.test.ts.
MP-740 The server retains full turn history, so rollback is: the game admin proposes a rollback to
a named prior point — any Reckoning boundary, never a point inside a phase — all active human
participants are notified and MUST consent (default), or the game's pre-agreed policy allows
admin-only rollback (declared at creation); on execution the log is truncated to that boundary, a
RollbackExecuted record with actor, timestamp and reason is appended to the audit log
(03-architecture.md AR-563), and all clients resync. Rollbacks MUST be visible forever in the game's
history view.
Rationale: the boundary restriction is new and it is forced by the model. A turn's outcome is a pure
function of the state at CloseOrders and the records of that phase, so a rollback to any point
inside an Orders phase would truncate a set of commitments into a set nobody submitted, and replaying
forward from it would produce a Cascade that never happened. Reckoning boundaries are the only points
at which the log describes a state that actually existed.
no-test-needed: an admin action on a server that does not exist, with its own notification and consent flow. The Reckoning-boundary restriction its rationale derives is a property of the turn model and is held by 10-turn-model.md.
10. Turn-bundle mode (signed file play)
Delivery phase: Phase 7 (owner ruling, 2026-09-19). This section remains normative and the existing container, signature, encryption and replay groundwork remains supported; the ruling changes priority, not the feature contract. MP-750–MP-880 and any later obligation whose only observable behaviour is a turn-bundle import, export, packet, drop-box, or mode switch MUST NOT be selected as implementation work before the Phase 5 exit is satisfied. The exception is shared container, replay, signing, encryption, or verification machinery that independently satisfies a requirement of the current phase. Server-hosted correspondence remains Phase 5 (§9).
10.1 Purpose and format
MP-750 A turn bundle is a single portable file containing everything needed to continue a game:
header (GameId, core/protocol versions, content hashes, seat roster with public keys, bundle
sequence number, lifecycle phase), the initial state, the full order log, chat history, and a
signature chain (03-architecture.md AR-565, AR-567). It preserves infrastructure-free play (MP-080)
for airgapped, forum and file-passing players.
MP-755 An order packet is the small companion file this mode needs under a concurrent Orders
phase: one seat's order records for exactly one turn, plus that seat's CommitOrders, signed by that
seat's key and naming the GameId, the turn number, and the bundleSeq of the turn-opening bundle it
answers. A packet MUST NOT contain game state, MUST NOT be applicable to any other turn, and MUST be
rejected if its bundleSeq does not match the open turn.
MP-760 Container: the bundle uses the documented save/replay container of 03-architecture.md,
compressed, with extension .elcturn; order packets use the same container framing with extension
.elcorders. Both formats MUST be fully documented (MP-180).
Held 2026-09-23 as packages/core/test/net/bundle-trust.test.ts. The clause worth testing is "uses
the documented container" rather than the extensions, and the code states it in the strong form: the
.elcturn role is not a different file format but the same manifest carrying a roster, which
isBundle reads, with bundleSeq and activeSeat riding there too. One container, profiled by its
fields.
The row that would catch the drift is the parser count: exactly one parseContainer in the
workspace, and none in the core — a second one is how a parallel format begins, and it begins for a
good local reason every time, taking the AR-570 verification path with it. A core-side parser would be
a layering break as well (AR-010: the core has no I/O). Three mutants killed.
MP-770 Integrity and identity: each seat owns a keypair generated automatically at seat claim (invisible to the user; exportable for multi-device use). Every bundle hand-off and every order packet carries the sender's signature (AR-565). A tampered log, a forged hand-off, or a packet signed by a seat that does not own it MUST fail verification on import.
Built 2026-09-17, the hand-off half. Ed25519 keypairs (cli/src/keys.ts) and AR-565’s signature
chain (cli/src/handoff.ts), at the container boundary — which is the ruling this document had been
waiting on, since WebCrypto is asynchronous and the core is not, and the reasoning is recorded at
AR-565 rather than repeated here. The three refusals this requirement names in its own words map to
named faults: a tampered log to handoff:log (the chain signed the old ChainHash head) or to
AR-560’s entry table, whichever the tamperer left inconsistent; a forged hand-off to
handoff:signature; and a packet signed by a seat that does not own it to handoff:seat, which
is kept distinct from forgery because a valid signature by a seat whose turn it was not is what an
honest player hits by exporting twice from the wrong machine. elc replay --verify runs the chain
with AR-570’s other checks, and an ordinary save answers nothing there — the profile is optional, and
a check that failed for every save ever written would say only that.
Not built, and named so it is not assumed: order packets (.elcorders, MP-750) carry no
signature yet, because nothing writes one — the packet half of §10 is import/export, which waits on
the same gate as the rest of it; and the key’s home is the host’s (a keyring, a profile
directory, a browser store), so a seat key is generated and exported by these modules and stored by
nobody yet. MP-780’s optional passphrase encryption is untouched.
MP-780 Privacy: a bundle MAY additionally be encrypted with a game passphrase agreed out-of-band (for posting on public forums). Encryption is optional; signatures are not.
Built 2026-09-17 (cli/src/passphrase.ts). PBKDF2-SHA-256 at 600,000 iterations (OWASP’s figure)
over the passphrase, AES-256-GCM over the container bytes, in a documented envelope: a magic
string, a four-byte header length, then a plaintext JSON header carrying the KDF, its iterations,
the salt and the IV. The header is plaintext on purpose — a reader that cannot tell "this is
sealed, ask for the passphrase" from "this download is corrupt" sends a player looking for a
problem they do not have — and it carries nothing about the game: no GameId, no seat, no
turn, because the point of sealing a file is that the thread it is posted in learns nothing from
it. Measured on Node 22.23.2: the derivation costs 55 ms, the cipher 2 ms to seal a
seven-megabyte bundle and 3 to open it, with a 16-byte tag. The whole cost is the derivation, once
per file.
The second sentence is the load-bearing one, and it is a row rather than a remark: what comes out of an opening is an ordinary container whose hand-off chain is verified exactly as AR-565 says, and sealing a bundle with a forged hand-off produces a sealed bundle with a forged hand-off. A reader that treated "it decrypted" as "it is genuine" would have replaced a signature with a passphrase — and a passphrase is shared by everybody in the game, so it identifies nobody.
What it protects against, stated so the UI can say it. It protects against the passer-by: somebody with the file and not the passphrase learns its size and nothing else. It does not make a weak passphrase secret — WebCrypto offers PBKDF2 and neither Argon2 nor scrypt, and PBKDF2-SHA-256 is cheap on a GPU — so a guessable passphrase on a publicly posted file is guessable. The screen that asks for one MUST say so in those terms rather than implying the file is safe because it is encrypted.
Two refusals worth naming. A header this build cannot honour (a KDF or cipher it does not have, an iteration count that is not one) is refused by name, never attempted, for AR-880’s reason: a file sealed by a build that knew better is not a file to pretend to have opened. And a wrong passphrase and an altered file are one answer, because an authenticated cipher fails identically for a key that is not the key and a byte that is not the byte; the sentence carries both, since the remedies are opposite — try again, or get the file again — and picking one would be a guess.
No passphrase flag on the CLI, deliberately: argv is visible in a process list, so a passphrase
belongs on stdin or in a prompt, and the bundle verbs that will need one do not exist yet. What
elc does today is recognize a sealed bundle and say what it is.
MP-790 Fog in bundle mode is trust-based: the file necessarily contains full game state, and a determined participant can extract hidden information. The client MUST still render only the importing seat's fog-filtered view and its own observation stream, and the UI MUST label bundle games "friendly trust level: fog is not cryptographically enforced". Rated play in bundle mode is prohibited (MP-350). The closer's additional sight (MP-845) is a disclosed part of this trust level, not an exception to it.
Flagged gap, 2026-09-23: neither half is built, and the blocking fact is recorded as a test row
— packages/client contains no bundle path at all: no import, no export, no trust label. The row
exists because "there is no client support yet" is exactly the kind of claim that stays in a document
after it stops being true; when somebody builds the import, it fails and sends them here.*
The clause most likely to be lost is the label, and it is worth saying why. Everything else in §10 announces its own absence by not working — an unbuilt import cannot be used by accident. The label is the one obligation a fully working bundle implementation would not notice missing, because the game plays correctly without it. And it is the honest half of this requirement: the fog guarantee here is trust, not cryptography, and a player who is not told that will reasonably assume otherwise, having been given a fog-enforced game everywhere else.*
The filtered-view clause is the other half and has the same blocker. Note what it will mean when there is an importer: the container necessarily holds everything, so rendering only the importing seat’s view is a discipline the importer must keep deliberately — it will be able to read the full state and must not — which is the opposite of every other fog surface in this project, where the filtering is structural and the data simply is not there.
no-test-needed: this row's own flagged gap (2026-09-23) already records the blocking fact in full — packages/client contains no bundle path at all, no import, no export, no trust label — and stays as the standing record that sends whoever builds the importer here; there is nothing further to annotate until an importer exists. When it does, the two halves test separately: the importing seat's fog-filtered view rendered although the container holds everything, and the trust label present — "the clause most likely to be lost," in this row's own words.
10.2 Creating and bootstrapping
MP-800 Creation: the organizer configures setup with human seats marked bundle-human,
optionally claims their own seat, and exports the initial bundle in the sign-in profile
(03-architecture.md AR-567). While seats remain unclaimed the game is in sign-in state: the bundle
circulates, each recipient imports it, claims a seat (name + auto-generated key), and re-exports for
the next player. When the last seat is claimed, the completing machine materializes the initial state
deterministically from the recorded seed and provenance, and turn 1's Orders phase opens.
no-test-needed: the sign-in circulation needs a client and a filesystem, which core/test/net/bundle.test.ts records as its reason for covering the turn cycle and not the claim cycle.
MP-810 Bootstrap shortcut: if the organizer already holds every participant's public key (prior games, or key exchange via a server), they MAY pre-authorize all seats and export a ready-to-play bundle, skipping the round-robin — the sign-in cycle exists for key establishment, not ceremony.
Flagged gap, 2026-09-23: the bootstrap shortcut needs the client that does not exist (see MP-790). Recorded with the clause that gives it its shape: "the sign-in cycle exists for key establishment, not ceremony." That is a statement about what may be skipped, so the test to write when it lands is an equivalence: a game bootstrapped from pre-authorised keys and the same game assembled by the round-robin must reach the same state, or the shortcut is a second setup path rather than a shortcut.
no-test-needed: this row's flagged gap (2026-09-23) already names the blocker — the bootstrap shortcut needs the client that does not exist, see MP-790 — and records the test to write when it lands as an equivalence: a game bootstrapped from pre-authorised keys and the same game assembled by the round-robin reach one state. Nothing further stands or waits until the importer exists.
10.3 Importing and playing
MP-820 Import: opening a bundle or packet (file association, drag-drop, or in-game "Import") verifies signatures, hashes and content availability (MP-120), then places or updates the game in the player's game list. There are no watched directories to pre-create; imported files are copied into managed storage and the source is left untouched.
MP-830 Any seat whose key is present on the importing device MAY issue order records for the open Orders phase and export an order packet for it. Only the turn's closer MAY close the phase and run the Cascade (MP-845). The client MUST refuse to produce a packet for a seat whose key it does not hold, and MUST refuse to close a turn for which this device is not the closer.
10.4 The turn cycle
MP-840 A bundle turn MUST proceed in exactly three steps:
- Open. The turn's closer exports a turn-opening bundle at the current Reckoning boundary and distributes it to every seat by any channel — a single broadcast, not a chain. It carries the closer's own sealed order packet and the commitment of MP-845.
- Order. Every other seat imports it, plays a full Orders phase offline against the stable board (10-turn-model.md TM-340), and returns a signed order packet (MP-755) to the closer. Seats work in parallel; no seat waits on any other, and no seat sees any other's packet.
- Close. When the closer holds a packet from every seat — or the game's soft deadline expires
(MP-1040) — the closer appends every packet to the log in ascending player index, appends
TurnTimeoutfor any missing seat, appendsCloseOrders, runs the Cascade and the Reckoning, and exports the turn-closing bundle, which is also the next turn's opening bundle if the closer role has not rotated.
MP-845 The closer is a per-turn role, MUST rotate to the next player index at each Reckoning (skipping seats that are not live), and MUST be recorded in the bundle header. Because the closer holds every other seat's packet before the Cascade runs, the closer MUST commit first: the turn-opening bundle MUST carry the closer's own order packet encrypted under a key the closer withholds, together with a hash commitment over the plaintext packet, and the closer MUST publish the key only in the turn-closing bundle. Every client MUST verify, on importing a turn-closing bundle, that the revealed packet matches the commitment, and MUST refuse the bundle and raise a tamper warning if it does not.
Rationale: this is the one place in the design where the concurrent Orders phase collides with a transport that has no simultaneous channel, and it deserves a real answer rather than a shrug at "friendly trust". The commitment costs one hash and one symmetric key, is verified automatically, and makes the closer's unavoidable early sight worthless — their own orders were fixed before anyone else's existed. Rotation keeps even that structural position from settling on one player. What remains is that a dishonest closer can stall or refuse to close, which is a social failure a file game cannot prevent and does not need to: any other seat can be made closer by agreement, and the whole mode is labelled friendly-trust (MP-790) for exactly this reason.
MP-847 The bundle mode's turn therefore costs one broadcast plus one packet per player, independent of army size, and every player's thinking happens in parallel. Rationale: this is the same one-submission-per-player-per-turn property as MP-625, achieved without any server at all, and it is a direct consequence of the Orders phase being stable and identical for everyone (TM-030, TM-2050). Under sequential turns a file game required a serial chain of N hand-offs per turn, each carrying a full interactive turn's thinking, with every player idle until the file reached them; the concurrent phase turns that chain into a fan-out and a fan-in.
MP-850 After sending its packet a seat enters committed, awaiting close state: it MUST NOT be
able to revise that turn's orders, and the client MUST say so plainly. "Re-send my packet" MUST
remain available for a lost transmission, and re-sending an identical packet MUST be idempotent at
the closer by ClientTag (MP-280).
10.5 Recovery and rollback
MP-860 Rollback by agreement: any participant MAY export a bundle from an earlier Reckoning
boundary of their retained history (MP-740's boundary restriction applies identically). Importing a
bundle whose sequence number is ≤ one already seen for that GameId MUST warn loudly ("this rewinds
the game to turn N; agreed with all players?") and record the divergence locally.
MP-870 Lost transmission: if a turn-opening or turn-closing bundle never arrives, the sender re-exports it. Bundles and packets are plain files and may be re-sent freely; there is no single-download constraint.
Held 2026-09-23, and it is a requirement that something must not be built — which makes it one of the more useful rows in this document, because what it forbids is exactly what a careful implementer adds on purpose. Marking a bundle consumed, burning a nonce, refusing a second import: each looks like an integrity measure, each would pass every functional test, and each breaks the transport. A bundle that arrives twice is the normal case, not an attack — the sender re-exports when a file did not arrive, and mail clients retry.
So the scan covers the bundle rules and the container for a consumed flag, a used mark, a download
count; and separately the BundleRefusal union, which is the likelier route — the constraint arriving
as an error message meaning "you have had this one already". Both mutants killed, along with one that
removes isResend from the public surface: MP-850’s idempotence rule is only useful if a host can
reach it, and one the core keeps to itself is one every host reimplements differently.
10.6 Relay drop-box (optional bridge)
MP-880 An @everylastcity/server instance MAY offer a drop-box for bundle games: participants
upload and download bundles and order packets by GameId, with server-tracked phase state and
notifications, while the game formally remains a bundle game (signatures, trust level, no server
simulation). A drop-box collapses the fan-out and fan-in of MP-840 into ordinary correspondence
ergonomics — each player submits once, whenever they like — without any server-side fog claim, and
one action converts such a game to full correspondence play (§11).
no-test-needed: a drop-box is an optional server feature, and this requirement is explicit that such a game formally remains a bundle game, so it adds no core obligation.
11. Switching a game between modes
MP-890 Any game MUST be convertible between hot seat, live, correspondence and turn-bundle modes, in any direction, any number of times. A switch rebinds seat transports (MP-020) and changes the authority; it MUST NOT touch game state, the log, the seed, or pinned versions (MP-060).
MP-900 Who may switch: the game admin in server modes, or the current closer in bundle mode, with
notification to all humans. Switches append a ModeSwitch record to the audit log
(03-architecture.md AR-563).
Notification alone suffices only for a switch that does not relocate full state onto a player
device. A switch that takes a live game out of authoritative fog-enforced mode — to turn-bundle
(MP-920), hot seat (MP-940), drop-box (MP-880), or relay-only, whose designated host client
necessarily holds full state (MP-340) — MUST additionally obtain the consent of all active human
participants before it executes, and MUST NOT be performable unilaterally by the admin or by any
other single participant. Consenting participants and any refusal MUST be recorded in the
ModeSwitch record and surfaced in the game's history view. Rationale: MP-730 permits a participant
to obtain the full container of a live fog-enforced game only through a consent-gated switch; a
notification-only admin path out of authoritative mode would reopen from the other end exactly the
leak MP-730 closes.
MP-903 A mode switch MUST take effect only at a Reckoning boundary, between the close of one turn and the opening of the next Orders phase. A switch proposed mid-Orders MUST be queued and applied at the next boundary, and a switch MUST NOT be applied during a Cascade or a Reckoning (10-turn-model.md TM-620). Rationale: a mode switch changes which node is the authority, and the authority is the node that closes the phase and runs the Cascade. Switching mid-phase would leave a set of order records addressed to a node that is no longer entitled to sequence them.
MP-905 Authority mode on conversion: a game entering a server mode by conversion — hot seat
uploaded to correspondence (MP-435), a drop-box bundle game converted (MP-880), or any later
re-switch (MP-890) — never chose an authority mode at creation, so the converting party (MP-900) MUST
select relay-only or authoritative as an explicit part of the switch. The selected mode MUST appear
in the ModeSwitch notice and in GameInfo (MP-340), and the switch UI MUST show the same trust
implications required of the lobby by MP-350. Absent an explicit choice the converted game MUST
default to relay-only. Rationale: authoritative is the stronger trust claim; defaulting to it
silently would advertise enforcement the game's own history cannot support.
The authority-mode choice MUST NOT be used to route around MP-900's consent gate. Selecting or defaulting to relay-only for a game that is currently authoritative and fog-enforced hands full state to the designated host client (MP-340) and is therefore itself a switch out of fog enforcement: it requires the consent of all active human participants exactly as a switch to bundle or hot seat does. The relay-only default above applies only where nothing is being taken away — a game that was never fog-enforced, or one already outside authoritative mode at the moment of the switch.
MP-906 Fog enforcement is never retroactive. When a game becomes authoritative after any phase in
which participants held full state — hot seat, turn-bundle, drop-box, or relay-only — the client MUST
state plainly, at the switch and in GameInfo, that every device that previously held the full
container (AR-590) still holds all opponents' history up to the switch point, and that enforcement
applies only from that point forward. Such a game MUST be permanently flagged
fog-enforced-from-turn-N and MUST remain ineligible for rated play (MP-1160), which requires
authoritative mode for a game's entire life.
MP-910 There MUST be no minimum-turns gate on switching: switching is legal from turn 1, subject only to the boundary restriction of MP-903.
MP-920 Correspondence → bundle: exporting a correspondence game to bundle mode suspends the
server copy (authority moves to the file); re-uploading the latest bundle (sequence-checked) resumes
it as a server game under the same GameId. Uploading a stale bundle follows rollback consent
(MP-740).
Consent gate: where the correspondence game is authoritative and fog-enforced, the export MUST NOT
proceed without the consent of all active human participants (MP-900), because the bundle is by
construction a full container (MP-750) and hands every participant who receives it all opponents'
hidden history. The consent prompt MUST state that fog enforcement ends at the switch and trust drops
to the friendly bundle level (MP-790), and consent MUST be recorded in the ModeSwitch record. A
correspondence game that is not fog-enforced needs no consent for this switch: it adds no exposure
(MP-730).
MP-930 Live ↔ correspondence on a server is a pure scheduling change — the same server, the same log, the same phases, and only the deadline configuration differs (§13). It MUST be achievable as a single admin toggle in both directions, applied at the next Reckoning boundary (MP-903).
Rationale: under this model the distinction between live and correspondence play is nothing but the value of one deadline. That is a real simplification and it is worth stating: the same game, the same protocol messages, and the same code path serve a ten-minute deadline and a forty-eight-hour one.
MP-940 Hot seat ↔ server modes: uploading binds local seats to accounts and invites (MP-435); downloading a server game for hot seat requires all active human participants' consent per MP-900, because full state will reside on one device and fog trust drops to the friendly level (MP-790).
12. Notifications
MP-950 When an Orders phase opens in a game a player is in, the service MUST notify them via their enabled channels: mobile/desktop push, in-client badge, and opt-in email with a deep link that opens directly into that game's Orders phase (channel infrastructure per 08-services-platform.md).
no-test-needed: every channel this row names — mobile and desktop push, in-client badge, opt-in email with deep link — is infrastructure of 08-services-platform.md (one covered requirement of its one hundred twenty-six in this tree), and no code here opens any of them; the only serviceWorker registration in packages/client serves offline caching, not event delivery. It becomes testable when channel dispatch lands: an Orders phase opening reaching the enabled channels with a deep link resolving into that game.
MP-960 Notification events MUST include: an Orders phase opened; your turn's result is ready to watch; deadline warnings at configurable fractions (default 50% and 90% elapsed); your reserve bank began drawing down; you were timed out; you were substituted or restored; a Verdict offer is open and closes in N turns (14-victory.md VC-2780); game started; you were invited; rollback or mode switch executed; game revived from archive; chat mentions (opt-in); game ended, with result.
Rationale: the notification set changed shape with the model. "It is your turn" was previously an exclusive, sequential fact about one player; the equivalent now — "an Orders phase is open" — goes to everyone at once and stops being urgent, so the urgency moved to the two facts that are genuinely time-bounded: the deadline and the reserve bank. Notifying a player that their reserve has started to drain is the single most useful message this system sends, because it is the only one that reports an irreversible cost accruing right now.
no-test-needed: a notification event set dispatched by a service that does not exist here — no push, email or inbox surface appears anywhere under packages/server; the one delivery path in the tree is the live connection itself, where phase-open already reaches connected seats (the PhaseBegin frame of core/src/net/protocol.ts, delivered at the boundary from host.ts). The urgency clauses this row defines — deadline warnings and reserve-bank draw-down notices — become testable with channel dispatch; until then the events exist only as turn-model state changes whose pins they cite.
MP-970 Per-player controls MUST include: per-game mute, global quiet hours, digest batching ("one summary for my 12 games"), and channel selection. Channel defaults are owned by 08-services-platform.md SVC-530 and are not restated divergently here: phase-open → push + desktop, with email fallback only if the seat has not committed for 24 h; invites → push + email; security email always (SVC-180) and exempt from unsubscribe (SVC-540). Where this document and SVC-530 disagree about defaults, SVC-530 governs.
no-test-needed: per-game mute, quiet hours, digest batching and channel selection are controls over channels this tree does not have, and the defaults clause belongs explicitly to 08-services-platform.md's SVC-530 — one covered requirement of its one hundred twenty-six; there is no surface here for any test to observe. It becomes testable with the notification service: a muted game silent, quiet hours batched into the digest, SVC-530 governing where this document and it disagree.
MP-980 Self-hosted instances MUST support notifications without proprietary dependencies:
standard Web Push and SMTP relay configuration at minimum (§16). Where no push channel is available to
a client, that client MUST nonetheless surface every MP-960 event: over its live connection while it
holds one, where PhaseBegin and the rest of the MP-270 downstream family already carry them, and
from the authoritative in-app inbox of 08-services-platform.md SVC-500(e) on its next connection for
anything missed. No "check for new turns" affordance may appear in the UI (MP-610).
A client that is running but can be neither pushed to nor told over a live connection — an instance
operated with rooms disabled (08-services-platform.md SVC-404) on a platform with no push channel —
MUST obtain those events by a periodic catch-up query, which at that point is the only mechanism
available because nothing on the instance can reach the client unbidden. Such a query MUST be bounded
so that it costs an idle correspondence player nothing measurable in battery, data, or server load
across a game whose design lifetime is months to years (MP-630): its interval MUST be derived from
that game's own deadline configuration (MP-990) rather than being a fixed constant, MUST back off
while the client is idle or backgrounded, and MUST stop for a seat that has already committed for the
open phase, which has nothing further to learn until the next PhaseBegin. The concrete interval,
back-off and channel mechanics are owned by 08-services-platform.md SVC-500 and are not restated here.
Rationale: the end is that a player learns an Orders phase opened on a platform where nothing can
push to them — a real obligation under MP-080 and MP-1210, which between them rule out depending on
APNs or FCM. Polling was written as the mechanism, but the mechanism is not the requirement and the
case for it is narrower than "push is unavailable": a running client already holds the connection
MP-260 gives it and is told by PhaseBegin, and a client that is not running cannot poll at all. In
the residual case a query really is the only answer, so it is mandated here with its reason. What was
missing is the bound: a 172800 s (48 h) correspondence deadline (MP-990, TM-490) does not need to be
asked about every minute, and an interval chosen for live play would drain a phone over a game
measured in years.
Partially held 2026-09-28: the running-connected-client half stands — a connected client is told over its live connection by the downstream family; what waits is everything for a client that cannot be pushed to or holds no connection. A client holding the wire learns an Orders phase opened from the stream itself (the PhaseBegin frame of core/src/net/protocol.ts, delivered at every boundary from host.ts), which settles this row's residual case in the affirmative; and no "check for new turns" affordance exists anywhere in packages/client. What waits: the authoritative in-app inbox of SVC-500(e) replaying missed events on next connection, and the bounded catch-up query — interval derived from the game's own deadline configuration, backing off while idle — whose mechanics SVC-500 owns.
13. Deadlines, absence, and substitution
MP-990 Every server-hosted game carries a stall policy, chosen at creation, displayed to all joiners, and changeable mid-game only with the consent of all active humans. The rules of deadlines, the reserve bank, and their consequences are owned by 10-turn-model.md §5; this table is the configuration surface and its defaults defer to TM-490:
| Policy element | Options (defaults bold) |
|---|---|
| Orders deadline | per TM-490 for the game's shape — None for solo, two-player and hot seat; 600 s live 3+; 90 s blitz; 172800 s (48 h) correspondence; or any custom whole-second value, or None |
| Reserve bank per player per game | per TM-490 — 1800 s live, 300 s blitz, 432000 s (5 days) correspondence; custom permitted; never refills (TM-510) |
| On reserve exhaustion | TurnTimeout and close (TM-520). There is no other option: persisted orders execute and the turn resolves |
| Escalation | after 3 consecutive missed turns (TM-570), AI substitute — by vote in casual play, automatically in rated play (TM-580) |
| Vacation | per-player pool of days/year (default 30) that suspends that player's deadline; announced to the game |
| Admin rollback without unanimous consent | off / on (MP-740) |
MP-995 The host MUST perform all deadline, reserve and vacation arithmetic outside the simulation
core, and MUST reach the core only through the TurnTimeout and CloseOrders records
(10-turn-model.md TM-190, TM-550). No wall-clock value may enter the core by any path, and a replay
MUST reproduce the turn from those two records alone without consulting any clock (TM-2430).
*Ruled 2026-09-12, against AR-563's required timestamp. This requirement's subject is the
simulation core, and an audit record's timestamp is outside it — made structurally so rather than
argued. 03-architecture.md AR-563 requires every audit record to carry a timestamp, and those
records are declared in packages/core; read flatly, "no wall-clock value may enter the core by any
path" forbids them. The two do not actually conflict: this requirement's preceding sentence scopes
itself to arithmetic "outside the simulation core", TM-2430 scopes the prohibition to what a
replay consults, and AR-563(a) already guarantees the audit log is not that — "LoadFrom,
replay, snapshot verification, TurnHash, and StateHash MUST ignore it entirely."
That argument is correct and it is not enough, because the next reader finds a count of milliseconds
in packages/core and either deletes it or starts computing with it. So the claim is made
structural: AuditInstant is opaque. The core can carry the value and hash it; it defines no
arithmetic, comparison or formatting on it anywhere, and cannot construct one except by being handed
it (asAuditInstant, the boundary this requirement draws for the deadline arithmetic itself). "No
wall-clock value enters the core" is then true in the sense that matters — the core holds no clock
semantics — and true by type rather than by a comment.
Built at client/src/host/deadline.ts, where this requirement puts it. AR-020's graph gives it no
better home yet: MP-1010 names the host client as the node that runs substitution in relay-only
games, and @everylastcity/server does not exist. The module reads no clock either — the client is
held to no-ambient-nondeterminism (AR-085) — and that is right rather than inconvenient, because
every interesting case in this section is a boundary: the instant the reserve reaches zero, the
instant a vacation starts, the third consecutive miss. A module that read its own clock could not be
put at any of them, and two hosts evaluating the same game at the same instant would not be
guaranteed to agree.*
MP-1000 Timeout resolution. When a seat's reserve reaches zero without a CommitOrders, the
authority MUST issue TurnTimeout for that seat and proceed to CloseOrders (TM-520). The seat's
units then execute their persisted order stacks and Stances normally (TM-530), automated units
resolve normally, units with no Posture adopt their Fallback Posture for that turn only, and Dispatch
items persist to the player's next session rather than being consumed (13-command.md CM-2010,
CM-2020). There MUST be no rules-visible penalty for a timeout — no forfeited movement, no production
penalty, no initiative penalty (TM-540). Host-issued records MUST carry Issuer = Authority(AutoTurn)
per 03-architecture.md AR-260 so the log records who generated them, and the seat's commit marker in
the turn record MUST be TimedOut (TM-300). A timed-out turn MUST be visible to every player through
the commit markers of OrdersClosed.
Rationale: the previous draft specified an elaborate auto-turn resolver that had to decide, on the absent player's behalf, what unordered units should do and what unspent resources should become. None of that machinery survives, and its disappearance is the clearest single illustration of what the turn model bought. Orders persist (TM-280), so an absent player's army is not idle — it is executing the plan its owner last wrote, including every conditional in it. There is nothing left for a resolver to invent, and "the absent player's army continues its campaign" is a far better failure mode than "the absent player's army stands still".
MP-1010 AI substitution: a substituting seat uses an AI configured at creation (default per
06-ai.md) running under the authority — server-side in correspondence and server-authoritative games
(06-ai.md AI-480), on the host client in relay-only games. Substitution MUST take effect only at a
turn boundary, MUST be recorded as a SubstitutePlayer order, and MUST NOT alter any unit's order
stack (10-turn-model.md TM-590): the AI takes over the position's existing orders. A substituted
player MUST be able to reclaim their seat on reconnection, recorded as ResumePlayer and taking
effect at the next turn boundary, with their Dispatch items intact (TM-600, CM-2010). The substitute
MUST receive only that seat's fog-filtered view (06-ai.md AI-490) and MUST commit within the same
Orders phase as any human seat, with no extra time, information, or order kinds (TM-410). Tenure MUST
continue to accrue for a substituted seat (14-victory.md VC-3060, VC-3070).
no-test-needed: a restatement for multiplayer of clauses each owned and held elsewhere: the turn-boundary effect and untouched order stack at 10-turn-model.md TM-590, the reclaim at TM-600, the fog-filtered view at 06-ai.md AI-490, the same Orders phase with no extra time at TM-410, and continuing Tenure at 14-victory.md VC-3060.
MP-1020 Seat replacement: an opened seat is offered by invite link; a replacement human inherits the seat (player index, forces, colours) but not the predecessor's account stats. Rated games MUST record the substitution for rating integrity (08-services-platform.md SVC-648).
no-test-needed: seat replacement by invite link, and its record for rating integrity, belong to the services platform per 08-services-platform.md SVC-648.
MP-1030 Live absence is not a special case: a disconnected seat in a live game is handled by exactly the schedule above at the live deadline defaults, and its units execute persisted orders (MP-550). No live game may stop for an absent seat.
MP-1040 Bundle games have no enforceable timers. The closer MAY apply a soft deadline: after
an agreed interval, close the turn with TurnTimeout for any seat whose packet has not arrived
(MP-840 step 3). The client MAY show local reminders. The drop-box bridge (MP-880) applies reminder
notifications only.
14. Spectating and replay
MP-1050 Spectating derives from the log: a spectator is a read-only subscriber to a stream built by the same observation rules as a player's (10-turn-model.md TM-2300). Per-game spectating policy is set at creation and changeable by the admin: disabled (default for private games), invited spectators, link-holders, or public.
Held 2026-09-23 as packages/core/test/net/spectating.test.ts, and the best answer this section
could have is a structural one: there is no spectator code path. compactedStream is the only
builder of a delivered stream in the core, and a seat-POV spectator is that same function called with
that seat’s number — so "built by the same observation rules as a player’s" is not a discipline the
implementation keeps, it is the same line of code. The row checks for the failure mode directly: a
second, parallel builder written for spectators, which would drift from the player’s and pass every
player-side fog test ever written, because none of them would call it. A mutant adding one is killed.
Flagged gap: per-game spectating policy — disabled, invited, link-holders, public — is not built; there is no spectator transport and no admin surface to set it on. It waits on the lobby’s admin surface (§8.2), which is where the setting belongs.
MP-1055 A live spectator's stream MUST be built and transmitted under the same rules as a player's: compacted (TM-2150), free of activation indices and per-event timestamps (TM-2160), and delivered as a single payload after the Cascade completes (TM-2170, MP-245). Rationale: a spectator receiving events incrementally is a timing oracle for anyone in contact with them, which is precisely the channel TM-2170 closes for players. A spectator stream that leaks it would make the player-side protection worthless in any game with an audience.
Held 2026-09-23 on the clause that is easiest to get wrong and hardest to notice: the per-viewer
sequence is dense. The rationale argues about delivery timing, and the same argument applies to
ordinals — a stream numbered by the referee’s position would leak, through its gaps, that something
happened where the viewer could not see. The fixture is built by hand rather than played out of a
scene so the visibility pattern can be chosen, including the case a played game makes rare: a seat
that observes only the last two of five events, whose ordinals must read [0, 1] and not [3, 4].
A mutant that numbers by referee position is killed.
The stripping of the activation index is checked twice, and the second row is the one that lasts: the
first tests the events this fixture produced, the second closes the union — a new StreamEvent
member declaring an activation index would satisfy the fixture and leak. Checked against the source
record, which does carry one, so the absence is a fact about the filter rather than about the fixture.
MP-1060 Spectator view scopes: (a) omniscient — full map, available only when all active humans consent or the game has ended; (b) seat POV — one player's observation stream, requiring that player's consent; (c) delayed — omniscient or POV delayed by N turns (default 1) to blunt live coaching. Rated and tournament games SHOULD default to delayed omniscient.
Flagged gap, 2026-09-23: the three view scopes and their consent gates are not built. This is the requirement 03-architecture.md’s AR-520 defers to — it was disclaimed there the same day precisely because the gate is this document’s to hold, not the core’s — and the core’s half of the bargain is met: it provides both surfaces and makes no eligibility decision of its own (held at AR-520). What is missing is everything above the core: unanimous-consent capture for (a), per-player consent for (b), and the N-turn delay buffer for (c).
The clause to build first is the delay, and not for the reason it looks like. (a) and (b) are consent bookkeeping and fail safe — absent consent, no stream. (c) is a buffer, and a buffer implemented as "send the stream, mark it delayed" is indistinguishable from a working one in every test that does not measure when bytes left the server. It is the scope most likely to ship broken and the one whose whole purpose (blunting live coaching) is defeated by the defect.
no-test-needed: this row's flagged gap (2026-09-23) already records what is missing — the three view scopes and their consent gates are not built, everything above the core — while the core's half of the bargain stands titled at packages/core/test/architecture/hosting-and-streams.test.ts (AR-520: spectating is a view stream, and the core makes no eligibility decision); there is nothing above that core for a test to hold yet. When it lands, build the delay first — this row's own warning being that a buffer implemented as "send the stream, mark it delayed" is indistinguishable from working in every test that does not measure when bytes left the server.
MP-1070 Enforcement: in authoritative mode the server constructs the spectator stream server-side. In relay-only and bundle games, live spectating beyond seat-POV-with-consent MUST be unavailable (the data to filter trustworthily does not exist server-side); post-game replay is always available.
no-test-needed: enforcement by a server that constructs the spectator stream server-side. Already disclaimed as prose at core/test/net/fog.test.ts, which holds the stream rules the core can state.
MP-1080 Detail withheld from players MUST be withheld from live spectators: a live spectator stream MUST NOT carry anything 10-turn-model.md TM-2120 denies a player — events in unobserved tiles, activation indices, roster size, any unit's orders or condition — nor exact enemy initiative beyond the coarse band of TM-1100, nor any Contact detail above the fidelity grade the viewed seat holds (01-game-rules.md GR-1260). Full detail unlocks for participants and spectators only after the game ends (MP-1100).
Held 2026-09-23 on four of its clauses. A seat sees nothing of an event it did not observe — not even that it happened: no redacted placeholder, which a mutant inserting one is killed for. A seat that observed nothing still receives a payload, an empty array rather than silence, which §15’s MP-1125(3) calls the subtle clause for the reason that applies here too — treating "no observations" as "nothing to send" turns silence into a signal. No delivered event declares a unit’s orders, condition, disorder or supply. And the Contact-grade clause (GR-1260) holds for free: a seat-POV spectator reads that seat’s own stream, so it cannot exceed that seat’s fidelity — there is no arithmetic to get wrong, which is itself the argument for the seat-POV scope being the safe one.
A finding that is not this section’s alone: the coarse initiative band has no channel. This
requirement permits enemy initiative "beyond the coarse band of TM-1100", which presupposes the band is
deliverable. It is not. initiativeBand is implemented and exported from core/src/turn/initiative.ts
and nothing calls it — pinned by a row, so building the channel becomes a deliberate act that
updates this note rather than a quiet addition to a projection. 04-ui-ux.md’s UX-320 needs the same
missing channel for the player’s own screen, found the same day: two requirements, one absent
channel.
The ruling belongs to 01-game-rules.md rather than to either of them, because it is a question about Contact fidelity: a Contact is fidelity-graded (GR-1200–GR-1260), so "the band of a unit you have seen" must say at what grade, and a Trace sighting that yielded an initiative band would leak more than the sighting itself does. The cheap reading recorded at UX-320 is that the band rides where the class already does, the band being mostly a function of class base plus stance. It is not implemented, because a field on a hashed projection is a re-emission and the placement should be ruled before it is paid for.
MP-1090 Players who have left the game (14-victory.md VC-080) become eligible spectators per MP-150; the admin MAY restrict them to a delayed view to prevent intelligence reaching remaining players.
Flagged gap, 2026-09-23: departed-player spectating is not built, because neither spectating nor the admin restriction exists. It waits on MP-1060’s scopes.*
The clause worth carrying forward is the reason for the delay, which is sharper than MP-1060’s: a departed player is not a neutral audience. They hold everything their seat knew at the moment it left — map knowledge no surviving player has — and "intelligence reaching remaining players" is therefore a risk the delay does not remove, only limits to what happens after their departure. A design that treats VC-080 departure as producing an ordinary spectator has already lost that distinction.
no-test-needed: this row's flagged gap (2026-09-23) records why it waits — departed-player spectating is not built because neither spectating nor the admin restriction exists, and it waits on MP-1060's scopes; its own note carries forward the sharper reason for the delay (a departed player holds everything their seat knew at departure). Nothing stands to test until those scopes exist.
MP-1100 Replay: any participant MAY export, and the server MUST retain, a full replay (initial state + log) of every finished game, viewable in the client with free camera, per-seat observation stream toggle, step-through of any Cascade, and full detail. A replay MUST reproduce every Cascade, every Reckoning, every hash and every player's observation stream bit-identically (10-turn-model.md TM-2410), MUST NOT invoke AI code (TM-2420, 06-ai.md AI-350), and MUST NOT consult any clock, deadline or reserve bank (TM-2430). Replays double as the bug-report artifact.
Held 2026-09-23 on the two absences, which are the clauses a working replay would satisfy by accident today and lose silently later. No AI code in the core at all — the strongest form of TM-2420/AI-350, and true because AR-060 puts the host in the client or the server; a replay that called an AI would produce a different game wherever that AI’s version had moved, and would do so with differing hashes whose natural reading is an engine defect. No clock read in the core — stated in this requirement’s own terms rather than left to the determinism lint, so a replay viewer built inside the core would fail here rather than in a lint message about nondeterminism. Both mutants killed.
The bit-identical clause is held at its root: the observation stream is derived, not stored, so
the same records and seat give the same stream with no state carried between calls. That is the
property that makes reproduction achievable; the replay of a container is AR-570’s check set, which
elc replay --verify already runs.
Flagged gap: the replay viewer — free camera, per-seat observation-stream toggle, step-through of any Cascade — is not built, and is the same capability 03-architecture.md’s AR-510 records as a gap from the other side. Note the one clause that is cheap now and awkward later: "the server MUST retain" a replay of every finished game. That is a retention obligation on whatever stores games, and retrofitting retention is how the first interesting bug report arrives without its artifact.
15. Fog enforcement, integrity, and anti-cheat
MP-1110 Threat model: clients are never trusted. Anti-cheat rests on (a) server authority and information starvation, (b) determinism and auditability, (c) log integrity — never on client-side scanning or kernel components (none SHALL exist).
no-test-needed: a threat model rather than a testable assertion, as core/test/net/codec.test.ts puts it: what it motivates is testable and what it declares is not. Its three grounds are held where they act, and its one prohibition names components this repository does not contain.
MP-1120 Information starvation (authoritative mode). The server MUST send each client only:
- during an Orders phase, nothing about the game world — presence and chat only (MP-200);
- during the Cascade and the Reckoning, nothing at all;
- after the Reckoning, exactly one compacted observation stream for that seat (MP-240): the ordered list of events that seat observed under 10-turn-model.md TM-2100 and TM-2110, and nothing else. The stream MUST NOT contain gaps, placeholders, activation indices, per-event timestamps, or any field whose value varies with unobserved activity (TM-2160).
Content within the stream MUST honour every disclosure limit the rules impose: an observed enemy unit's initiative appears only as the coarse three-band estimate of TM-1100 (TM-2130), and a Contact reports only what its fidelity grade admits (01-game-rules.md GR-1260) — a Trace says a domain and nothing more, and only an Assessment carries strength, Stance and condition. What was never sent cannot be map-hacked.
Rationale: compaction is not an optimization, it is the fog rule. An uncompacted per-activation feed leaks by omission: a client that receives a slot for every activation can infer from an empty slot that something happened where it could not see, and by correlating those slots with the initiative bands it legitimately knows, deduce roughly what class of unit acted and when. Over a few turns that is a reasonable census of an opponent's army, assembled entirely from things the player never observed. Emitting only observable events, with no ordinal that maps back to the roster, closes it completely.
Ruled 2026-09-12: an OrderResults is not world information, and the Orders phase may carry
one. MP-200 and this requirement both state the Orders-phase allowance as "presence and chat", and
read with nothing else that forbids the authority from answering a submission — which MP-205
requires it to validate and append "immediately", and MP-285 requires it to report with "exactly one
entry per submitted ClientTag so the client can reconcile without guessing". An Orders phase that
cannot say whether an order was accepted is not usable. The prohibition is on world information,
and an OrderResults reports the fate of records the client itself wrote and already holds. The
bound that keeps this from swallowing the rule: a permitted kind must be derivable from what the
receiving client already knows, and an OrderResults entry MUST NOT carry a rejection reason that
names another seat's state — a record naming an unobservable entity is refused as
not-observed (MP-1130) and says nothing about what is there. The permitted set is enumerated as
ORDERS_PHASE_KINDS in core/src/net/fog.ts; GameInfo is in it because MP-110's pins are
constant for the life of the game, Control because a rollback notice is a fact about the session
rather than the board, and OrdersClosed because MP-1127 permits the commit marker by name.
MP-1125 The timing channel. Because activation order is initiative order, when a client learns something would otherwise disclose what acted. The transport MUST therefore:
- transmit each player's stream as a single payload after the Cascade has fully completed, never incrementally (10-turn-model.md TM-2170), in every game with two or more human players;
- dispatch all players' streams for a turn together, so that no player learns the turn resolved measurably before another;
- not vary dispatch time with stream content or size — a player who observed nothing MUST receive an empty stream at the same moment as a player who observed a battle, and the transport MUST NOT skip or defer a dispatch because a stream is empty;
- send nothing at all between
CloseOrdersand that dispatch, so the interval carries no sub-structure to measure.
Rationale: this is the requirement the fog rules of 10-turn-model.md reach into this document to obtain, and every clause of it closes a channel that reopens the leak MP-1120 closed. Clause 3 is the subtle one: an implementation that treats "no observations" as "nothing to send" turns silence into a signal, and an empty payload costs nothing to send.
Implementation note, 2026-09-12. Clause 4 has a consequence worth stating where somebody will be
standing when they try to undo it: in an authoritative game PhaseBegin only ever names Orders.
MP-270 calls PhaseBegin "the only phase notification" and 10-turn-model.md TM-090's phases include
the Cascade, so the obvious implementation announces it — and clause 4 forbids exactly that, because
"the interval carries no sub-structure to measure" and a phase announcement is sub-structure. The
client already knows the turn is resolving: OrdersClosed was the last thing it received. So a
Cascade progress indicator cannot be fed from the wire, and the honest client-side version is a
spinner that knows nothing. refuseSend in core/src/net/fog.ts refuses every kind during the
Cascade and the Reckoning with no exception, and the silences are scoped to authoritative mode:
applying them in relay-only would refuse a LogTail to the one mode whose clients are entitled to
the whole log (MP-1070), which is a mode with no fog to protect.
MP-1127 Orders-phase silence. The only thing the transport MAY deliver during an Orders phase is presence (MP-186) and chat, and presence MUST NOT vary with any player's order content (10-turn-model.md TM-450, TM-2070). The client MUST NOT emit network traffic whose timing or volume varies with the content of its Dispatch (13-command.md CM-1860) or of its order edits in any way an opponent could observe, and the authority MUST NOT relay any derived signal of another seat's activity beyond the commit marker itself.
Ruled 2026-09-12, on a conflict with MP-186. The idle clock is quantized to the hour, and its
dispatch is on the cadence. MP-186 requires presence to consist of "exactly ... and, in
correspondence games, time since the seat's last action", and this requirement forbids the authority
to relay "any derived signal of another seat's activity beyond the commit marker itself". Time since
a seat's last action is precisely a derived signal of that seat's activity, so as written the two
cannot both hold. Neither is deleted, because the leak is fineness rather than existence: an idle
clock that resets the instant a seat revises an order lets an opponent watch them work, while one at
whole-hour resolution serves §9 completely — correspondence deadlines are in days (MP-640) — and
says nothing about an editing session. Both halves of the rule are needed, and each without the
other is the whole leak: a coarse value pushed on every accepted order leaks through its timing, and
a fine value on a slow cadence leaks through its content. So idleSeconds MUST be reported as a
multiple of 3600 seconds, and a presence dispatch MUST NOT be triggered by an order submission or
edit — IDLE_GRANULARITY_SECONDS, quantizeIdle, presenceFaults and presenceTriggerAllowed in
core/src/net/fog.ts, where the permitted triggers are the cadence, connection changes,
substitution, and the commit marker this requirement names.
MP-1130 Order legality: the authority validates every order record against the pinned core (MP-300, 10-turn-model.md TM-250); a record naming an entity the issuing player does not own or cannot observe is rejected and counted. Repeated impossible records from one client SHOULD be surfaced to the admin and the service as a tamper signal. An order that was legal when issued and merely unachievable at activation is not an error at any layer and MUST NOT be counted as one (TM-270).
MP-1140 Log integrity: the log is hash-chained per order record — each appended record's
ChainHash commits to its predecessor (03-architecture.md AR-260). That is the granularity AR-570(b)
verifies on import and that the AR-565 hand-off signature covers, so it is the only chaining rule;
this document adds none. A SubmitOrders batch is a transport grouping only (MP-280), is not atomic
(MP-285), and MUST NOT be treated as a chain unit. The log is additionally server-sequenced in server
modes (gapless Seq) and signature-chained in bundle mode (MP-770). Non-order control records —
rollbacks, mode switches, admin actions — live in the separate audit chain of AR-563.
MP-1150 Desync detection has two checkpoints per turn, and is scoped to what each node can legitimately compute — a fog-filtered client holds no canonical state and MUST NOT be sent any (MP-250), so it cannot be asked for a canonical digest:
- Before the Cascade. Every node holding full state MUST compute and exchange the activation-order hash of 10-turn-model.md TM-2370 (MP-247); the authority compares and reports any mismatch immediately, before any state has diverged.
- After the Reckoning, nodes holding full state — the authoritative server, the relay-only host
client, and any client entitled to the whole log — MUST compute AR-130's
TurnHash(64-bit XXH3) over the canonical state hash of TM-2400, and connected ones MUST report it (TurnHashmessage, §5; AR-530). - After the Reckoning, authoritative-mode clients report instead a per-seat
ViewHash: a digest computed independently by server and client over the canonical encoding (AR-620) of that seat'sPlayerView, carried on AR-500ViewCheckpointmessages. Contract: this obliges 03-architecture.md to defineViewHashas part of the AR-500 view-serialization contract; until it does, authoritative-mode clients report no digest and their desync exposure is covered only by the server's own snapshot verification (AR-670). Discharged 2026-09-12: 03-architecture.md AR-505 now defines it, so authoritative-mode clients report aViewHashand the fallback above no longer applies.
Per-turn digests MUST use the cheap XXH3 form; the SHA-256 StateHash is reserved for snapshots and
save manifests (AR-130). On mismatch the authority's log is truth: the mismatching client MUST resync
— from the log in relay-only mode, from a fresh ViewCheckpoint in authoritative mode (MP-290) — and
persistent mismatch is logged as a defect or tamper signal with the replay attached.
MP-1160 Rated integrity: rated games MUST run in authoritative mode on a server instance (MP-350), for the game's entire life (MP-906). Ratings, reporting and enforcement policy are specified in 08-services-platform.md. Relay-only and bundle games are unrated by construction and their UI says so. Where a rated game uses third-party AI seats, 06-ai.md AI-370's determinism and server-side-execution constraints apply.
MP-1170 Fair-visibility parity: any information the client UI derives — distance readouts, combat odds previews (13-command.md), a player's own units' initiative and relative activation order — MUST be computable from that seat's own view and committed state alone (10-turn-model.md TM-2080), so a modified client gains no information advantage, only convenience it already legitimately has. The client MUST NOT need, and the protocol MUST NOT supply, any value that would let a client reconstruct the global activation order (TM-1120).
Partially held 2026-09-28: the protocol half stands titled — no value that would let a client reconstruct the global activation order is ever shipped; what waits is pinning the UI-derivation parity across every readout this row names. TM-1120's pin says this row's second MUST in its title — "the global activation order is never shipped to a player" (packages/core/test/turn/initiative-contract.test.ts) — beside the view machinery at packages/core/test/net/fog.test.ts and the player-view tests. What waits: the first clause, that distance readouts, combat odds previews and initiative displays are computable from that seat's own view and committed state alone; TM-2080 pins the core half of it (packages/core/test/view/phase-knowledge.test.ts), but no test yet holds the parity assertion over values a client UI actually derives.
MP-1180 Disputes: because every game is a deterministic replay from the order log, the dispute artifact is the log itself. The service MUST be able to re-simulate any rated game headlessly and verify the recorded result before it enters ratings (TM-2410).
no-test-needed: the dispute resimulator is a service obligation — "the service MUST be able to re-simulate" — and no service exists in this tree to carry it; what stands independently is the mechanism the row leans on, deterministic replay from the order log, pinned at TM-2410 with recorded games under packages/core/test/corpus. It becomes testable when ratings land: a rated game resimulated headlessly reproducing its recorded result before entering standings.
16. Self-hosted servers
MP-1190 @everylastcity/server MUST be self-hostable as a single Docker image, providing the full
feature set of this document: lobbies, live relay and authoritative simulation, correspondence play,
drop-box, spectating, replays, notifications, and local accounts. The official hosted instance runs
the same software (08-services-platform.md).
no-test-needed: there is no Dockerfile anywhere in this tree and no other packaging surface, so there is nothing to containerize — and most of the feature set the image would ship (correspondence, drop-box, spectating, notifications, local accounts) belongs to the services platform standing at one covered requirement of its one hundred twenty-six. When the image lands it should run as a test subject against every section 16 row that has a pin by then.
MP-1200 Configuration surface (file/env, no GUI required): listen port and TLS, external URL, account policy (open/invite/guest), default stall policies (MP-990), dormancy period (MP-720), storage paths and quotas, Web Push keys, SMTP relay, content cache limits, and the scheduler that owns deadline expiry and Cascade execution for games with nobody connected (MP-615). Where an instance runs as more than one process or node, the configuration surface additionally covers the shared store used for room presence and matchmaking; a single-process instance MUST require no such store and MUST run against its database alone.
MP-1210 Instance independence: a self-hosted instance MUST function with zero connectivity to the official service — no license checks, no phone-home. Cross-instance identity and federation are NOT required in v1; a player uses per-instance accounts (open question 6).
no-test-needed: instance independence holds by construction — no code under packages/server or packages/client reaches any official service; there is no license check, no phone-home and no cross-instance identity anywhere in the tree to test against or forbid. The row's own v1 boundary (per-instance accounts only) becomes testable when accounts land, where an instance must play a full game with every external route severed.
MP-1220 Compatibility: server and client negotiate protocol and core versions (Hello, §5;
AR-920); an instance MUST advertise its supported version ranges, and games remain pinned (MP-110) so
a server upgrade never alters an in-flight game's rules. Self-hosted operators MUST be able to run
multiple core versions side by side.
MP-1230 Data portability: the instance operator MUST be able to export any game the instance
hosts as a bundle or replay, and an instance MUST support bulk export — no game is captive to an
instance (MP-080). Operator export carries no new exposure in authoritative mode, since the instance
already computes full state. Player export follows MP-730 exactly: unrestricted for games without
fog enforcement; limited mid-game to the player's own fog-filtered view cache (AR-590) in
authoritative fog-enforced games; full only after the game ends (MP-1100) or through the
consent-gated mode switch out of authoritative mode (MP-900, MP-905, MP-920, MP-940). Operator bulk
export MUST NOT be surfaced as a per-participant feature that circumvents MP-730, and an operator who
is also a participant in a live fog-enforced game on their own instance MUST be disclosed as such in
GameInfo — self-hosting is not a fog-enforcement claim.
Partially held 2026-09-28: the player-export regime this row defers to stands pinned — what a participant may export, with its consent gates; what waits is any operator-side surface, of which none exists. Player export follows MP-730 exactly, and that cluster's consent gates are pinned at packages/core/test/net/mode.test.ts per this document's own note under it (MP-900/MP-920/MP-940); the client-side container export the row anticipates is 03-architecture.md AR-540's, scoped to what a participant already legitimately holds. What waits: operator bulk export — no endpoint of that kind exists anywhere under packages/server — and the GameInfo disclosure naming an operator who sits in their own live fog-enforced game.
MP-1240 WASM plugin execution: an instance MAY execute sandboxed WASM AI and generator plugins (07-modding-content.md) for authoritative games with modded content, subject to operator-set resource limits; instances that decline MUST still relay such games (MP-380).
no-test-needed: both halves presuppose mod content that does not exist — WebAssembly appears nowhere under any package's src in this tree, so no instance can execute such a plugin and there are no "such games" to relay; the relay duty it would attach (MP-380) stands vacuously on a server whose only games run vanilla. It becomes testable when 07-modding-content.md's host lands: one instance executing under operator-set limits, its refusing twin still relaying.
17. Contracts on other documents
Obligations this specification places on subsystems it does not own, and the obligations it accepts.
MP-1250 This document accepts 10-turn-model.md TM-2700 in full and discharges it as follows:
transport of order records (§4.1, §5) and of observation streams (MP-240, MP-245); the presence
channel of TM-440 as ephemeral non-authoritative data (MP-186, MP-265); the deadline and reserve
clocks as host-side state reaching the core only as TurnTimeout and CloseOrders (MP-995,
MP-1000); the activation-order hash exchange of TM-2380 (MP-247, MP-1150); and the single-payload
transmission of TM-2170 (MP-245, MP-1125).
no-test-needed: a contract whose five discharges are each held by rows that already carry titled pins — order-record transport at the wire suite (packages/server/test/wire.test.ts), observation streams at MP-240/MP-245's one-payload pin there, the presence channel of TM-440 at MP-186's six-fields pin in packages/server/test/host.test.ts, deadline and reserve clocks reaching the core only as TurnTimeout and CloseOrders (host.ts raises both through applyOrder, pinned by packages/server/test/deadline.test.ts), and the activation-order hash exchange at MP-247's pin — so a test of this row would re-run cited pins rather than assert anything new. If a discharge regresses, its own named row fails first.
MP-1255 This document accepts 14-victory.md VC-3180 in full: AI substitution of a departed
seat without freezing its Tenure (MP-1010); transport of CastVerdictVote and Concede as ordinary
logged orders (MP-150, MP-270); and delivery of a Verdict offer within the VerdictWindow of VC-2780
in correspondence play (MP-960).
Partially held 2026-09-28: the orders this row accepts for transport stand in the core and cross the wire — CastVerdictVote and Concede are ordinary logged orders, and a Concede's journey is pinned; what waits is delivering a Verdict offer within its window to players who are not connected. Both kinds live in packages/core/src/turn/orders.ts with their apply paths and typed refusals (VC-2840), and host.test.ts pins MP-150 directly — a Concede crosses the wire as an ordinary record, landing in the log with no special reply; substitution of a departed seat is owned by MP-1010's own annotation. What waits: correspondence delivery — a Verdict offer reaching its players within VC-2780's window while they are offline, which needs sections 9 and 12.
MP-1260 03-architecture.md MUST provide: the order envelope and its gapless server-assigned
Seq and per-record ChainHash (AR-260); the Issuer enum with an Authority(reason) form covering
host-issued phase records — this document uses AutoTurn for TurnTimeout (MP-1000) and requires a
reason value for the authority's CloseOrders, which the AR-260 set does not yet name; typed
rejections including PhaseForbidden (AR-320); idempotent resubmission by ClientTag (AR-340);
PlayerView/RefereeView separation (AR-410, AR-420); ViewCheckpoint/ViewDelta with a defined
ViewHash (AR-500, MP-1150); the audit log for non-order control records (AR-563); the turn-bundle
and sign-in container profiles (AR-565, AR-567); and the three version axes of AR-880 with the wire
schema version of AR-920.
no-test-needed: an obligation on 03-architecture.md to provide named constructs, which is a documentation duty discharged by that document rather than behaviour a test can exercise.
MP-1265 06-ai.md MUST provide: AI seats that submit order records within the same Orders phase as human seats with no extra time, information, or order kinds (10-turn-model.md TM-410); server-side execution of AI seats for correspondence and server-authoritative games (AI-480); a substitute that takes over an absent seat's existing orders and sees only that seat's fog-filtered view (AI-490, MP-1010); and the single fault-and-fallback policy of AI-240, which this document cites rather than restating.
Partially held 2026-09-28: server-side execution of an AI seat in a server-authoritative game stands with its fairness pins — same phase, one enforced clock, fault fallback shaped like AI-240; what waits is the correspondence half and the substitute's fog-filtered sight. packages/server/test/ai-seat.test.ts (AI-480 titled) plays a designated AI seat against humans in authoritative mode — turn one committed before any human order lands, no second authority admissible, two hosts byte-identical — and its fairness seam pins absorbing refused plans into a commit anyway; floor-seat.test.ts adds memory across the seat's own turns (AI-460); config-surface.test.ts keeps the single clock under MP-1200/TM-480/MP-186. What waits: correspondence games, which no service holds, and AI-490's substitute — taking over an absent seat's existing orders while seeing only that seat's fog-filtered view.
MP-1270 08-services-platform.md MUST provide: account, notification-channel and quiet-hours infrastructure (§12) with SVC-530 owning channel defaults; the authoritative in-app inbox of SVC-500(e), and the interval and back-off of the no-push catch-up query, which MP-980 now states as an end and defers to SVC-500 rather than fixing as a mechanism here — SVC-500's own polling sentence currently cites MP-980 for that mechanism and MUST take ownership of it instead; the room-framework scope boundary of SVC-402 matching MP-265; rating policy and substitution recording (SVC-648, MP-1020); and the durable scheduler that runs deadline expiry, Cascade execution and notification dispatch for correspondence games with nobody connected (MP-615, MP-1200).
no-test-needed: an obligation owed by 08-services-platform.md, which stands at one covered requirement of its one hundred twenty-six in this tree — the account, channel, inbox and scheduler infrastructure it must provide is absent here, so the contract has no counterparty a test could hold; its self-contained clause (SVC-500 taking ownership of the catch-up mechanism from MP-980) is a documentation duty on that document. It becomes checkable as each SVC provision lands.
MP-1275 04-ui-ux.md MUST provide: the client-side staging restored on reconnection (MP-290); the correspondence game list of MP-680; the occlusion hand-off and per-seat playback of §7; the presence and clock display of MP-186; and the trust-level labelling MP-350, MP-790 and MP-906 require in plain language.
Partially held 2026-09-28: three of its five provisions stand pinned — staging restore, occlusion hand-off with per-seat playback, presence and clock display; what waits is a correspondence game list and trust-level labelling over surfaces that do not exist yet. Staging restored on reconnection stands at UX-1430's pin (packages/client/test/background-staging.test.ts); the section 7 hand-off and its per-seat-behind-occlusion playback policy are titled in packages/client/test/handoff.test.ts; presence and clocks stand where 04-ui-ux.md holds them (UX-300, UX-305) with MP-186's six-field pin at packages/server/test/host.test.ts. What waits: the game list of MP-680 over a service that does not exist, and plain-language trust labelling for authoritative, bundle and mode-switched games — there is no such label anywhere in packages/client today (MP-790's flagged gap names the bundle case).
Open questions
- Bundle closer stalling (MP-845). The commitment makes the closer's early sight harmless, but a closer who simply never closes still halts the game, and rotation does not help if the stalled turn is theirs. Should any seat be able to force a re-close after an agreed interval by assembling the packets themselves — which requires every seat to broadcast its packet to everyone rather than only to the closer, and therefore trades stall-resistance for the closer's exclusive sight becoming universal? Current draft keeps packets closer-only and treats the stall as social.
- E2E-encrypted relay (MP-370): is there real demand, given it forfeits server-side deadline enforcement, substitution, spectating and replay? Ship behind a flag or drop entirely.
- Hot seat playback cost (MP-430). Per-seat playback behind the occlusion screen is correct and it multiplies the least interesting part of the turn by the number of players. Is there an acceptable compressed form — a written per-seat after-action summary (TM-2280) with animation only on request — and does that change the feel enough to matter at a shared table?
- Reconnection during a very long Cascade (MP-290). The Cascade is bounded at 200 ms of computation (TM-2480) but the dispatch is bounded by MP-1125's simultaneity requirement across potentially slow links. What does a client see if it connects during that window — a "resolving" state that is honest but reveals nothing, or nothing at all?
- Vacation and the reserve bank (MP-990). Vacation suspends the deadline; the reserve bank is separate and never refills (TM-510). A player returning from vacation with an exhausted reserve is permanently on the clock for the rest of a multi-year game. Should the reserve refill on some schedule in correspondence play, or is a bank that only ever depletes the correct incentive?
- Federation between self-hosted instances (MP-1210): out of scope for v1 — revisit post-launch.
- Default dormancy period (MP-720): 180 days is a guess; tune against real correspondence cadence once games exist.
- LAN play from a browser tab (MP-440, MP-490, MP-500): the browser build can neither host, nor
discover by mDNS, nor reach a plaintext
ws://LAN host. Is LAN play formally a desktop/mobile feature, or do we invest in a locally trusted certificate or a loopback bridge? This is the one place the cross-platform goal and the zero-infrastructure goal do not overlap. - Unreachable-participant consent (MP-900): the gate out of fog-enforced mode needs consent from all active humans, but correspondence play is exactly where a participant may be unreachable for weeks. Does an unreachable seat block the switch indefinitely, does the §13 escalation ladder make it inactive first, or does a substituted seat drop out of the consent set? "Active human participants" is used throughout without a definition.
- Downgrading a live authoritative game to relay-only (MP-900, MP-905): the gate requires unanimous consent, but should the downgrade be offered at all? It permanently forfeits rated eligibility on any later re-upgrade (MP-906), so a simpler rule would be to forbid authoritative → relay-only outright and require the switch to go through bundle or hot seat.
ViewHashin the core view contract (MP-1150): does 03-architecture.md adopt a per-player view digest on AR-500ViewCheckpoint, or do we accept server-only desync detection for fog-filtered clients? This document asserts the obligation; 03 must accept or refuse it.- A reason value for authority-issued
CloseOrders(MP-1260): AR-260'sIssuerenum namesAutoTurn,AiSubstitutionandPluginFallback.CloseOrdersis always authority-issued and is not any of those. Proposed: addPhaseControl. 03-architecture.md owns the enum. - View-cache export and restore (MP-730): the fog-filtered view cache is the only mid-game backup a fog-enforced participant gets — is it re-importable on a second device, and does that require the seat key of MP-770 to stop a stolen cache from revealing a player's own view?
- Batch semantics for clients that want all-or-nothing (MP-285): prefix application is specified; do any UI flows — multi-unit orders issued as one gesture — need an opt-in atomic batch, or is per-order reconciliation always sufficient now that a batch only ever revises the issuer's own plan?
- Two presence sources (MP-265, MP-186): presence for connected clients MAY ride the room framework's ephemeral sync channel, but correspondence presence ("time since last action", commit markers) must be visible with no room running. Do we accept two implementations, and does the live channel ever expose a signal — "currently viewing" can imply attention on a map region — that MP-1127 would not otherwise permit?
- Lobby chat durability (MP-465): lobby chat is expendable by construction, so a late joiner sees none of the pre-launch conversation and nothing survives launch. Acceptable, or should the lobby channel be persisted for the lobby's lifetime — a durable write, never the sync channel — without becoming a second chat record competing with MP-185?
- Cross-platform digest agreement (MP-1150, MP-1180): the desync machinery compares
ActivationOrderHash,TurnHashandViewHashcomputed by a Node authority, a browser tab and a Tauri build of the same source. Agreement rests on the core's integer and iteration-order discipline, so a core defect surfaces here first as a false desync. Should rated play (MP-1160) be gated on a CI job proving digest agreement across all three runtimes? - Per-turn digest budget (MP-247, MP-1150): the authority plus every full-state node now
computes two digests per turn while authoritative-mode clients compute a per-seat
ViewHash. Has the aggregate been measured on a server carrying many concurrent games, or only for one game in isolation? - Rated-play substitution limits (MP-1020, MP-1160): how many timed-out turns or substitutions before a rated game is annulled rather than rated? The turn model makes a timed-out turn far less damaging than a skipped turn used to be, which may mean the threshold should be higher than instinct suggests.
Bundle passphrase and key exchange (MP-780, MP-810).Ruled 2026-09-17: no connected assist — the assist is local. The question answers itself out of MP-810, which already grants the shortcut "if the organizer already holds every participant’s public key (prior games, or key exchange via a server)" and says in as many words that "the sign-in cycle exists for key establishment, not ceremony". "Prior games" is the offline half of that, and it is the one to build: a client that remembers the public keys of seats it has played with, filled from the bundles it has imported, gives a returning group of friends MP-810’s ready-to-play export with nothing connected at all. A drop-box assist would buy the same convenience for the first game with a stranger and buy it by making the convenient path the connected one, which is what this mode exists not to require. The passphrase stays out-of-band in every case: it is a shared secret and a channel that distributes it is a channel that can leak it, so there is nothing to assist. Building the key book waits on the client’s import surface (§10), which is the same gate as the rest of bundle play in the browser.