From a60467c6fc218fadfe97b6614e09562747e189f9 Mon Sep 17 00:00:00 2001 From: Eric Wagoner Date: Sun, 16 Aug 2026 22:23:25 -0400 Subject: [PATCH] The missed-moves banner stops counting games watched live MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Once a gap opened — a backgrounded tab during the bots' turns was enough — the mark-seen condition (exactly one move behind) could never hold again, so the banner grew with every move a player watched happen in front of them. Now only the first state after arriving (or reconnecting) announces a gap; while present and visible, play marks itself seen, an announced gap stays frozen until watched or skipped, and a hidden tab still accumulates its gap honestly. Co-Authored-By: Claude Fable 5 --- packages/web/src/net.svelte.ts | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/packages/web/src/net.svelte.ts b/packages/web/src/net.svelte.ts index b3e39d4..fece490 100644 --- a/packages/web/src/net.svelte.ts +++ b/packages/web/src/net.svelte.ts @@ -255,6 +255,9 @@ class Net { openingRolls = $state<{ rolls: Record; first: string; players: string[] } | null>(null); catchUp = $state<{ seq: number; actor: string; events: GameEvent[]; view: GameView; chat?: { player: string; text: string }[] }[] | null>(null); private seen: Record = loadSeen(); + /** Room whose live stream this connection has already shown once: states + * after the first mark themselves seen while the tab is visible. */ + private watching: string | null = null; private currentSeq = 0; private lastYourTurn = new Map(); private pollTimer: ReturnType | null = null; @@ -268,6 +271,7 @@ class Net { const ws = new WebSocket(SERVER_URL); this.ws = ws; ws.onopen = () => { + this.watching = null; // the first state after (re)connecting may carry a gap this.status = "connected"; // Mid-game reconnects (the socket dropped, not the page) walk straight // back to the table; otherwise the lobby ledger is the front door. @@ -317,13 +321,19 @@ class Net { this.view = msg.view; if (typeof msg.seq === "number" && this.roomId) { this.currentSeq = msg.seq; - const last = this.seen[this.roomId] ?? 0; - this.missedMoves = Math.max(0, msg.seq - last); - // Watching live counts as seeing; only a fresh arrival has a gap. - if (this.missedMoves === 0 || document.visibilityState === "visible") { - // A live update while present marks itself seen. - if (last >= msg.seq - 1) this.markSeen(); + // Only the FIRST state after arriving carries a gap worth + // announcing. Later states were watched live: a caught-up + // watcher stays caught up, and an announced gap stays FROZEN + // (not grown, not wiped) until watched or skipped. A hidden + // tab accumulates its gap honestly. + if (this.watching === this.roomId && document.visibilityState === "visible") { + if (this.missedMoves === 0) this.markSeen(); + } else { + const last = this.seen[this.roomId] ?? 0; + this.missedMoves = Math.max(0, msg.seq - last); + if (this.missedMoves === 0) this.markSeen(); } + if (document.visibilityState === "visible") this.watching = this.roomId; } break; }