Credibility pass: the fast-edit shavings swept from the ops-and-rev-13 batch
Server: turnFacts joins actingSeat's full priority order (a hanging ward or slow-death window now reads as the lobby's attention, stubs included), summarize and stubOf share one baseSummary, the stranded doc comment returns to summarize, the protocol contract names feedbackList, Sentry init moves below the imports it cannot precede, randomBytes joins its group, the feedback path hoists once with the operator-fields note. Engine: the safe's sight check trusts castSight alone, the locked-safe predicate becomes lockedSafeAt, the doomed-grab test drives both seats and must reach endTurn. Web: the twin safe handlers merge, paPoints joins the reset ritual with a derived burnable range, five decorative empty handlers and a dead class go, the hand earns real list semantics, fitHandCards cites Card.svelte's tokens, two wrap-sites reindent, feedbackReports moves home as a named type, and the chronicle's stay-mounted reason is stated. feedback-reply.sh adopts its siblings' set/usage conventions and speaks Node. Two test screenshots leave the repo root. 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
d37bc62c28
commit
afa0e17483
@@ -27,17 +27,12 @@
|
||||
// {type:"chat", player, text, at} one line of table talk
|
||||
// {type:"watching", roomId} you are seated in the gallery
|
||||
// {type:"audience", count} how many watch from the gallery
|
||||
// {type:"transferCode"|"transferClaimed"|"catchUp"|"games"|"stats"|"feedbackReceived"}
|
||||
// {type:"transferCode"|"transferClaimed"|"catchUp"|"games"|"stats"|"feedbackReceived"|"feedbackList"}
|
||||
// {type:"error", message}
|
||||
|
||||
import * as Sentry from "@sentry/node";
|
||||
// Errors only, no tracing: the box is small and the ledgers are the real
|
||||
// telemetry. Without a DSN in the environment this is entirely inert.
|
||||
if (process.env.SENTRY_DSN) {
|
||||
Sentry.init({ dsn: process.env.SENTRY_DSN, environment: "production", tracesSampleRate: 0 });
|
||||
}
|
||||
|
||||
import { createServer } from "node:http";
|
||||
import { randomBytes } from "node:crypto";
|
||||
import { readFileSync, existsSync, realpathSync } from "node:fs";
|
||||
import { extname, join, normalize, sep } from "node:path";
|
||||
import { WebSocketServer, WebSocket } from "ws";
|
||||
@@ -71,11 +66,16 @@ import {
|
||||
} from "./rooms";
|
||||
import { engagementStats, recordHotseat } from "./stats";
|
||||
import { appendFeedback, readFeedback } from "./store";
|
||||
import { randomBytes } from "node:crypto";
|
||||
import { getShare, loadShares, mintShare } from "./shares";
|
||||
import { renderSharePng } from "./ogimage";
|
||||
import { BOT_LINES, type BanterTrigger } from "./banter";
|
||||
|
||||
// Errors only, no tracing: the box is small and the ledgers are the real
|
||||
// telemetry.
|
||||
if (process.env.SENTRY_DSN) {
|
||||
Sentry.init({ dsn: process.env.SENTRY_DSN, environment: "production", tracesSampleRate: 0 });
|
||||
}
|
||||
|
||||
// --- Abuse limits: this is a public server on a small box. -----------------
|
||||
const MAX_SOCKETS = 300; // concurrent connections
|
||||
const MAX_ROOMS = 5000; // total rooms on the server
|
||||
|
||||
@@ -184,17 +184,22 @@ function stubOf(room: Room): RoomStub {
|
||||
turnHolder,
|
||||
waitKind,
|
||||
playing: room.state?.phase === "playing",
|
||||
base: {
|
||||
roomId: room.id,
|
||||
players: [...room.players],
|
||||
started: room.state !== null,
|
||||
finished: room.state?.phase === "finished",
|
||||
winner: room.state?.winner ?? null,
|
||||
activePlayerId: active,
|
||||
round: room.state?.turn.round ?? null,
|
||||
lastMoveAt: room.log.length > 0 ? room.log[room.log.length - 1]!.at : null,
|
||||
chatCount: room.chat.length,
|
||||
},
|
||||
base: baseSummary(room, active),
|
||||
};
|
||||
}
|
||||
|
||||
/** The per-room half of a summary — everything except whose turn it is. */
|
||||
function baseSummary(room: Room, active: PlayerId | null): Omit<GameSummary, "name" | "yourTurn" | "attention"> {
|
||||
return {
|
||||
roomId: room.id,
|
||||
players: [...room.players],
|
||||
started: room.state !== null,
|
||||
finished: room.state?.phase === "finished",
|
||||
winner: room.state?.winner ?? null,
|
||||
activePlayerId: active,
|
||||
round: room.state?.turn.round ?? null,
|
||||
lastMoveAt: room.log.length > 0 ? room.log[room.log.length - 1]!.at : null,
|
||||
chatCount: room.chat.length,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -452,16 +457,21 @@ export interface GameSummary {
|
||||
chatCount: number;
|
||||
}
|
||||
|
||||
/** A seat-holder's one-line view of a room, for the lobby ledger. */
|
||||
/** Who holds the table's attention, and why — the summary's turn facts. */
|
||||
/** Who holds the table's attention, and why — the summary's turn facts.
|
||||
* The priority order is actingSeat's: a hanging ward or slow-death window
|
||||
* outranks the stack, which outranks the turn itself. */
|
||||
function turnFacts(s: GameState | null): {
|
||||
active: PlayerId | null;
|
||||
turnHolder: PlayerId | null;
|
||||
waitKind: "counteract" | "discard" | "interrupt" | null;
|
||||
} {
|
||||
const active = s && s.phase === "playing" ? s.players[s.turn.activeIndex]!.id : null;
|
||||
const waitingOn = s?.stack?.waitingOn ?? s?.pendingDiscard ?? s?.chaosPending?.queue[0] ?? s?.outOfTurnWindow?.playerId ?? null;
|
||||
const waitingOn = s?.wardPending?.ownerId ?? s?.slowDeathPending?.playerId ??
|
||||
s?.stack?.waitingOn ?? s?.pendingDiscard ?? s?.chaosPending?.queue[0] ??
|
||||
s?.outOfTurnWindow?.playerId ?? null;
|
||||
const waitKind = s == null ? null
|
||||
: s.wardPending != null ? "counteract" as const
|
||||
: s.slowDeathPending != null ? "counteract" as const
|
||||
: s.stack?.waitingOn != null ? "counteract" as const
|
||||
: s.pendingDiscard != null ? "discard" as const
|
||||
: s.chaosPending?.queue[0] != null ? "counteract" as const
|
||||
@@ -470,23 +480,15 @@ function turnFacts(s: GameState | null): {
|
||||
return { active, turnHolder: waitingOn ?? active, waitKind };
|
||||
}
|
||||
|
||||
/** A seat-holder's one-line view of a room, for the lobby ledger. */
|
||||
export function summarize(room: Room, playerId: PlayerId): GameSummary {
|
||||
const s = room.state;
|
||||
const { active, turnHolder, waitKind } = turnFacts(s);
|
||||
const yourTurn = s?.phase === "playing" && turnHolder === playerId;
|
||||
const { active, turnHolder, waitKind } = turnFacts(room.state);
|
||||
const yourTurn = room.state?.phase === "playing" && turnHolder === playerId;
|
||||
return {
|
||||
roomId: room.id,
|
||||
...baseSummary(room, active),
|
||||
name: playerId,
|
||||
players: [...room.players],
|
||||
started: s !== null,
|
||||
finished: s?.phase === "finished",
|
||||
winner: s?.winner ?? null,
|
||||
activePlayerId: active,
|
||||
yourTurn,
|
||||
attention: yourTurn ? (waitKind ?? "turn") : null,
|
||||
round: s?.turn.round ?? null,
|
||||
lastMoveAt: room.log.length > 0 ? room.log[room.log.length - 1]!.at : null,
|
||||
chatCount: room.chat.length,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -152,10 +152,14 @@ export function readAllRooms(): Map<string, RoomLine[]> {
|
||||
* beside the rooms directory — a report line carries roomId/seq pinning
|
||||
* its moment; a reply line carries reportId/status/text and folds onto
|
||||
* its report when read. Legacy reports without an id answer to their
|
||||
* timestamp. */
|
||||
* timestamp. A report line may carry fields (deckRev, player context)
|
||||
* that only the operator's raw read uses; readFeedback keeps the
|
||||
* player-facing subset. */
|
||||
const feedbackFile = () => join(DATA_DIR, "..", "feedback.jsonl");
|
||||
|
||||
export function appendFeedback(entry: Record<string, unknown>): void {
|
||||
ensureDataDir();
|
||||
appendFileSync(join(DATA_DIR, "..", "feedback.jsonl"), JSON.stringify(entry) + "\n", "utf8");
|
||||
appendFileSync(feedbackFile(), JSON.stringify(entry) + "\n", "utf8");
|
||||
}
|
||||
|
||||
export interface FeedbackReport {
|
||||
@@ -171,7 +175,7 @@ export interface FeedbackReport {
|
||||
}
|
||||
|
||||
export function readFeedback(): FeedbackReport[] {
|
||||
const file = join(DATA_DIR, "..", "feedback.jsonl");
|
||||
const file = feedbackFile();
|
||||
if (!existsSync(file)) return [];
|
||||
const reports = new Map<string, FeedbackReport>();
|
||||
for (const raw of readFileSync(file, "utf8").split("\n")) {
|
||||
|
||||
Reference in New Issue
Block a user