The host clears seats and closes rooms

Two lobby-lifecycle gaps from tonight's duels (a token lost to a
client bug left an unkickable ghost seat holding a chair in VQJW).
kickSeat: the host removes any other seat from an unstarted room —
the kick is a ledger line so restarts replay it, and a kicked live
socket is set adrift with notice. abandonRoom: the host dissolves a
lobby or a finished game; every member is notified and detached, and
the ledger is archived to rooms-abandoned/ — moved, never deleted.
The lobby shows hosts a ✕ per seat and an abandon-room control.
Proven live: kick with notice, kick surviving a server restart, and
abandonment archiving its ledger.

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-26 11:05:42 -04:00
co-authored by Claude Fable 5
parent 4c7633097b
commit 9af7bd80ce
5 changed files with 130 additions and 4 deletions
+24 -2
View File
@@ -3,7 +3,7 @@
// command. Because the engine is deterministic, replaying a file rebuilds
// the exact game state — server restarts lose nothing.
import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync } from "node:fs";
import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync } from "node:fs";
import { join } from "node:path";
export interface RoomMetaLine {
@@ -58,7 +58,19 @@ export interface ChatLine {
at: string;
}
export type RoomLine = RoomMetaLine | JoinLine | StartLine | CommandLine | ChatLine;
export interface KickLine {
kind: "kick";
/** The seat the host removed from an unstarted room. */
name: string;
}
export interface AbandonLine {
kind: "abandon";
by: string;
at: string;
}
export type RoomLine = RoomMetaLine | JoinLine | StartLine | CommandLine | ChatLine | KickLine | AbandonLine;
const DATA_DIR = process.env.WIZWAR_DATA_DIR ?? join(process.cwd(), "data", "rooms");
@@ -103,3 +115,13 @@ export function readAllRooms(): Map<string, RoomLine[]> {
}
return rooms;
}
/** Retire an abandoned room's ledger to the graveyard — never deleted,
* only moved out of the living rooms directory. */
export function archiveRoomFile(roomId: string): void {
const src = join(DATA_DIR, `${roomId}.jsonl`);
if (!existsSync(src)) return;
const graveyard = join(DATA_DIR, "..", "rooms-abandoned");
mkdirSync(graveyard, { recursive: true });
renameSync(src, join(graveyard, `${roomId}.${Date.now()}.jsonl`));
}