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:
co-authored by
Claude Fable 5
parent
4c7633097b
commit
9af7bd80ce
@@ -22,7 +22,7 @@ import {
|
||||
type PlayerId,
|
||||
CURRENT_RULES_REV,
|
||||
} from "@wizwar/engine";
|
||||
import { appendLine, ensureDataDir, readAllRooms, roomFileExists, type RoomLine } from "./store";
|
||||
import { appendLine, archiveRoomFile, ensureDataDir, readAllRooms, roomFileExists, type RoomLine } from "./store";
|
||||
import { recordRoom } from "./stats";
|
||||
|
||||
export interface LoggedCommand {
|
||||
@@ -153,6 +153,33 @@ export function joinRoom(
|
||||
return { token: fresh };
|
||||
}
|
||||
|
||||
/** The host shows an unclaimed (or unwanted) seat the door — lobby only:
|
||||
* once the deal happens, every seat is a player in the game's record. */
|
||||
export function kickSeat(room: Room, byId: PlayerId, name: PlayerId): string | null {
|
||||
if (byId !== room.hostId) return "only the host may clear a seat";
|
||||
if (room.state) return "the game has started — seats are settled";
|
||||
if (name === room.hostId) return "the host cannot kick themselves — abandon the room instead";
|
||||
if (!room.players.includes(name)) return "no such seat";
|
||||
room.players = room.players.filter((p) => p !== name);
|
||||
room.tokens.delete(name);
|
||||
room.bots.delete(name);
|
||||
room.colorChoices.delete(name);
|
||||
appendLine(room.id, { kind: "kick", name });
|
||||
recordRoom(room);
|
||||
return null;
|
||||
}
|
||||
|
||||
/** The host dissolves the room: a lobby that never dealt, or a finished
|
||||
* game done being remembered. The ledger is archived, never deleted. */
|
||||
export function abandonRoom(room: Room, byId: PlayerId): string | null {
|
||||
if (byId !== room.hostId) return "only the host may abandon the room";
|
||||
if (room.state && room.state.phase === "playing") return "the game is still being played";
|
||||
appendLine(room.id, { kind: "abandon", by: byId, at: new Date().toISOString() });
|
||||
archiveRoomFile(room.id);
|
||||
rooms.delete(room.id);
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Everyone gets their chosen standee; the undecided get the first free one. */
|
||||
export function resolveColors(room: Room): number[] {
|
||||
const taken = new Set<number>();
|
||||
@@ -601,8 +628,14 @@ export function loadPersistedRooms(): void {
|
||||
chat: [],
|
||||
bots: new Map(),
|
||||
};
|
||||
if (lines.some((l) => l.kind === "abandon")) continue;
|
||||
for (const line of lines.slice(1)) {
|
||||
if (line.kind === "join") {
|
||||
if (line.kind === "kick") {
|
||||
room.players = room.players.filter((p) => p !== line.name);
|
||||
room.tokens.delete(line.name);
|
||||
room.bots.delete(line.name);
|
||||
room.colorChoices.delete(line.name);
|
||||
} else if (line.kind === "join") {
|
||||
if (line.bot) {
|
||||
room.players.push(line.name);
|
||||
room.bots.set(line.name, {
|
||||
|
||||
Reference in New Issue
Block a user