docs(adr): T2 polish architecture decisions
This commit is contained in:
parent
728ad76a5e
commit
3557aa7cb4
1 changed files with 83 additions and 0 deletions
|
|
@ -373,3 +373,86 @@ requiring no changes to the registry contract itself.
|
|||
and straightforward to reason about.
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
## T2-ADR-1: Turn-boundary queue
|
||||
|
||||
### Decision
|
||||
Hot-swap updates are enqueued server-side (`room.pendingProfile`) and applied at the next turn boundary (after `applyMove()` succeeds), not immediately on receipt.
|
||||
|
||||
### Rationale
|
||||
T1 shipped an immediate-apply simplification (profile swap took effect the moment the server received `modifier-profile.update`). The retrospective noted this was acceptable because WS messages serialize per-socket — a client can't interleave its own move and swap. But:
|
||||
|
||||
- The **opponent** can still have a move in-flight when the swap lands, causing move validation to run against a profile they didn't agree to.
|
||||
- Observers/spectators (future T3+ concern) see inconsistent ordering.
|
||||
- Determinism goal: the game state at turn N is fully determined by {profile at turn N, moves 1..N}.
|
||||
|
||||
Enqueuing eliminates the race.
|
||||
|
||||
### Semantics
|
||||
- On `modifier-profile.update` (or on `consent=approve` per T2-ADR-2): server validates shape + stores in `room.pendingProfile`, acks sender with `modifier-profile.queued`.
|
||||
- After the next move applies successfully: server runs `validateProfile(pending, layout, session)`. If valid, `reconcileProfileSwap(session, room.profile, pending, layout)`; broadcast `modifier-profile.updated`; clear pending.
|
||||
- If invalid at apply time: NACK to original sender with specific error code, clear pending, no broadcast.
|
||||
- **Last-write-wins**: rapid successive updates replace the pending slot. Only the most recent pending fires on the next boundary.
|
||||
|
||||
### Rejected Alternatives
|
||||
- Per-socket move-boundary lock — forces sender to wait synchronously for their own next move before the swap is acked. Poor UX; WS doesn't guarantee reply ordering anyway.
|
||||
- Queue per sender — multiple pending profiles per room, applied in order. Nondeterministic interaction if both players queue updates simultaneously. Last-write-wins is simpler and sufficient.
|
||||
|
||||
---
|
||||
|
||||
## T2-ADR-2: Two-player consent
|
||||
|
||||
### Decision
|
||||
In multiplayer rooms, a profile swap requires both players' agreement. Host sends `modifier-profile.propose`; opponent reviews and sends `modifier-profile.consent` with approve/reject. Approve promotes the proposal to the pending-queue (T2-ADR-1). Reject clears and notifies the host. 60s timeout → auto-reject.
|
||||
|
||||
### Rationale
|
||||
T1 granted host unilateral authority to change rules mid-game. This is an unfair advantage model; chess variants are a social contract. Both players must opt-in to rule changes.
|
||||
|
||||
Solo mode and single-player "vs. bot" contexts bypass consent — the sole participant is trivially the sole consenter. `modifier-profile.update` remains valid in solo mode as a shortcut.
|
||||
|
||||
### Semantics
|
||||
- Host → server: `{type: "modifier-profile.propose", roomCode, candidate: ModifierProfile}`.
|
||||
- Server validates shape + stores `room.proposalState = {profile, proposedBy, proposedAt, timeoutHandle}`.
|
||||
- Server → opponent: `{type: "modifier-profile.proposal-pending", profile, expiresAt}`.
|
||||
- Opponent → server: `{type: "modifier-profile.consent", roomCode, decision: "approve" | "reject"}`.
|
||||
- On approve: clear proposalState, promote to `room.pendingProfile` (T2-ADR-1), server → host: `modifier-profile.consent-received`.
|
||||
- On reject or 60s timeout: clear proposalState, server → both: `modifier-profile.rejected`.
|
||||
- Only the non-proposer can consent. Self-consent is rejected.
|
||||
- Stale proposals (after a newer `propose` replaces them): old timeout cancelled, old state overwritten.
|
||||
|
||||
### Rejected Alternatives
|
||||
- Majority-vote model for 3+ player scenarios — not applicable (chess is 2-player).
|
||||
- Silent auto-approve with opt-out grace period — violates the social-contract principle.
|
||||
- Host-only with explicit "I agree to changes" checkbox at game start — inflexible; players may change their minds after seeing how a rule plays out.
|
||||
|
||||
---
|
||||
|
||||
## T2-ADR-3: Editor undo/redo
|
||||
|
||||
### Decision
|
||||
The Modifier Profile Editor maintains a client-side snapshot stack of working-profile states. Every meaningful user action (add/delete/edit modifier, change bound layout) pushes a snapshot. Cmd/Ctrl+Z undoes, Cmd/Ctrl+Shift+Z redoes. Stack capped at 50 snapshots (oldest dropped). History cleared on Save or Cancel.
|
||||
|
||||
### Rationale
|
||||
Users compose profiles iteratively; mistakes are common (added wrong modifier, wrong color, wrong square). Manual deletion is destructive and loses intermediate states. Undo/redo is standard for structured editors.
|
||||
|
||||
Capped at 50 snapshots to bound memory. Cleared on Save because the saved state is the new baseline (undoing past Save would revert persisted data, which is surprising).
|
||||
|
||||
### Semantics
|
||||
- Editor state: `{history: ModifierProfile[], historyIndex: number, clipboard: Modifier[]}`.
|
||||
- `currentProfile = history[historyIndex]`.
|
||||
- Every mutation: `pushSnapshot(newProfile)`:
|
||||
1. Truncate history forward of `historyIndex` (redo branches lost).
|
||||
2. Append newProfile.
|
||||
3. If `history.length > 50`, drop the oldest entry and decrement historyIndex.
|
||||
4. Set `historyIndex = history.length - 1`.
|
||||
- `undo()`: `historyIndex = max(0, historyIndex - 1)`.
|
||||
- `redo()`: `historyIndex = min(history.length - 1, historyIndex + 1)`.
|
||||
- Toolbar buttons disabled at stack ends.
|
||||
- Save or Cancel: `setHistory([currentProfile]); setHistoryIndex(0)` — fresh single-entry stack for a new session.
|
||||
|
||||
### Rejected Alternatives
|
||||
- Infinite history — memory unbounded. 50 is generous for a single editing session.
|
||||
- Per-modifier undo (like per-field undo in some editors) — over-granular for structured data edits.
|
||||
- Persist history to localStorage — adds complexity for marginal benefit; users expect editor state to reset between sessions.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue