Forced drops bank honestly; half-dead sockets confess

DROP OBJECT's forced drop hardcoded onHomeOf null and skipped the
victory check — a treasure shaken loose over a home square neither
reported the banking nor triggered scoring or the captured-treasures
clause until something else prodded the engine. It now lands like any
drop: homeOwnerAt reported, checkVictory immediate.

And the stuck-game mystery from 38YP: a deploy restart left the
browser holding a half-dead socket that never fired onclose, so the
existing reconnect loop and masthead banner never engaged and a live
game wore a frozen face. The client now pings every 15s and force-
closes a socket silent for 45s, which wakes the reconnect machinery;
the server answers pong.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015RCWSTnb1KYTPyL4GmhGnF
This commit is contained in:
Eric Wagoner
2026-08-27 23:45:26 -04:00
co-authored by Claude Fable 5
parent 5eb5312637
commit ca0a4ef2e3
4 changed files with 58 additions and 1 deletions
+16
View File
@@ -277,6 +277,11 @@ function saveSeats(seats: Seat[]): void {
class Net {
status = $state<"disconnected" | "connected">("disconnected");
/** When the socket last spoke. A server restart can leave a half-dead
* socket that never fires onclose; the watchdog closes it by hand so
* the reconnect loop (and the masthead banner) actually engage. */
private lastHeard = Date.now();
private watchdog: ReturnType<typeof setInterval> | null = null;
roomId = $state<string | null>(null);
players = $state<string[]>([]);
hostId = $state<string | null>(null);
@@ -339,6 +344,14 @@ class Net {
if (this.ws) return;
const ws = new WebSocket(SERVER_URL);
this.ws = ws;
this.lastHeard = Date.now();
if (!this.watchdog) {
this.watchdog = setInterval(() => {
if (!this.ws || this.status !== "connected") return;
this.send({ type: "ping" });
if (Date.now() - this.lastHeard > 45_000) this.ws.close();
}, 15_000);
}
ws.onopen = () => {
this.watching = null; // the first state after (re)connecting may carry a gap
this.status = "connected";
@@ -367,7 +380,10 @@ class Net {
};
ws.onmessage = (raw) => {
const msg = JSON.parse(raw.data as string);
this.lastHeard = Date.now();
switch (msg.type) {
case "pong":
break;
case "seat": {
this.token = msg.token;
const seatRoom = (this.roomIdPending ?? this.roomId ?? "").toUpperCase();