Seat transfer phrases: carry your games to another device

While seated, "transfer seat" in the masthead mints a spoken-word
one-time phrase from the game's own vocabulary (ember-troll-dagger),
good for ten minutes. Typing it into the lobby's "claim a transferred
seat" box on any other device hands over the seat's token, adds the
game to that browser's ledger, and sits you straight down at the
table. Phrases are single-use, expire, are voided by a server restart
(seats never are), and both devices keep the seat afterward — the
phone on the couch and the desktop upstairs can play the same wizard.
Verified end to end: mint, claim, rejoin, and a second claim refused.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Eric Wagoner
2026-08-15 23:58:52 -04:00
co-authored by Claude Fable 5
parent bc8013863b
commit 2184721ab4
4 changed files with 137 additions and 0 deletions
+19
View File
@@ -173,6 +173,8 @@ class Net {
notificationsEnabled = $state(
typeof Notification !== "undefined" && Notification.permission === "granted",
);
/** A transfer phrase we minted, to show the user. */
transferCode = $state<{ code: string; expiresAt: number } | null>(null);
private lastYourTurn = new Map<string, boolean>();
private pollTimer: ReturnType<typeof setInterval> | null = null;
@@ -237,6 +239,16 @@ class Net {
if (line) this.log = [...this.log, line];
}
break;
case "transferCode":
this.transferCode = { code: msg.code, expiresAt: msg.expiresAt };
break;
case "transferClaimed": {
const seat = msg.seat as Seat & { roomId: string };
this.rememberSeat({ name: seat.name, roomId: seat.roomId, token: seat.token });
this.refreshGames();
this.resume({ name: seat.name, roomId: seat.roomId, token: seat.token });
break;
}
case "games": {
this.games = msg.games;
for (const g of msg.games as GameSummary[]) {
@@ -299,6 +311,13 @@ class Net {
this.games = this.games.filter((g) => g.roomId !== roomId);
}
requestTransferCode(): void {
this.send({ type: "makeTransfer" });
}
claimTransfer(code: string): void {
this.send({ type: "claimTransfer", code });
}
/** Ask the server how all our games are doing. */
refreshGames(): void {
if (this.seats.length > 0) this.send({ type: "myGames", seats: $state.snapshot(this.seats) });