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
+31
View File
@@ -1265,3 +1265,34 @@ describe("the tear is an attack (rev 6)", () => {
expect(r.events.some((e) => e.type === "treasureTorn" || e.type === "tearResisted")).toBe(true);
});
});
describe("a forced drop lands like any drop", () => {
it("DROP OBJECT onto a home square reports the banking and checks victory", () => {
let { state } = createGame({ playerIds: ["att", "def"], seed: 42, sets: ["basic", "expansion1"] });
state = toRound2(state);
while (activePlayer(state).id !== "att") {
state = must(state, activePlayer(state).id, { type: "endTurn", draw: 0 });
}
const att = state.players.find((p) => p.id === "att")!;
const def = state.players.find((p) => p.id === "def")!;
// The defender stands on their OWN home carrying the attacker's gold —
// the forced drop banks it for them, cruel as that is for the caster.
def.position = { ...def.home };
att.position = { ...def.home };
const gold = state.treasures.find((t) => t.owner === "att" && t.position)!;
gold.position = null;
gold.carriedBy = "def";
def.carriedTreasureId = gold.id;
const dob = giveCard(state, "att", "drop-object", "D", 0);
let r = applyCommand(state, "att", {
type: "cast", instanceId: dob.instanceId,
target: { kind: "player", playerId: "def" }, params: { cardId: "treasure" },
});
if (!r.ok) throw new Error(r.error);
r = applyCommand(r.state, "def", { type: "pass" });
if (!r.ok) throw new Error(r.error);
const drop = r.events.find((e) => e.type === "treasureDropped");
expect(drop).toBeTruthy();
if (drop?.type === "treasureDropped") expect(drop.onHomeOf).toBe("def");
});
});