The first minute, for a stranger: what the game is, what to do, and how to join

The landing says the pitch before it asks for a name — steal treasure,
blast through walls, outwit your friends — and states the goal. The
coach says "select a card, then choose its target," which is what the
cards actually want. The turn banner counts treasures home beside the
moves left. The council speaks once when its three voices agree. A
friend arriving on an invite link meets "Join Eric's game," a name
field, and a seat, with the gallery an afterthought; the masthead
says "invite friends" beside the room code. The first-person view has
a toggle beside the board instead of a checkbox in preferences. How
to play opens with winning. And a shared moment's card is titled by
the turn's story — the treasure carried home, the blow that landed,
the spell cast — instead of whose turn it was.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jm2auWk6RP71CjaAb4FMoG
This commit is contained in:
Eric Wagoner
2026-09-15 23:03:17 -04:00
co-authored by Claude Fable 5.1
parent a071494f4a
commit 4cc0423846
3 changed files with 100 additions and 25 deletions
+34 -1
View File
@@ -36,6 +36,7 @@ import { randomBytes } from "node:crypto";
import { readFileSync, existsSync, realpathSync, statSync, createReadStream } from "node:fs";
import { extname, join, normalize, sep } from "node:path";
import { WebSocketServer, WebSocket } from "ws";
import { cardDef } from "@wizwar/engine";
import type { Command, PlayerId } from "@wizwar/engine";
import {
catchUpSteps,
@@ -198,11 +199,43 @@ function safeBase(rawHost: string, rawProto: string): string {
return `${proto}://${escapeHtml(rawHost)}`;
}
/** The turn's story in one line, for the share card: the deed a
* stranger would click to see, chosen by weight — a crown, a treasure
* carried home, a blow that landed, a spell cast — before the bare fact
* of whose turn it was. */
function headlineOf(data: ShareData): string | null {
const name = (id: string | null) => { if (!id) return "a spell"; try { return cardDef(id).name; } catch { return id; } };
let best: { rank: number; text: string } | null = null;
const offer = (rank: number, text: string) => { if (!best || rank > best.rank) best = { rank, text }; };
for (const step of data.steps) {
for (const e of step.events) {
switch (e.type) {
case "gameWon": offer(9, e.reason === "treasures" ? `${e.player} wins with two treasures home` : `${e.player} is the last wizard standing`); break;
case "playerEliminated": offer(8, e.reason === "killed" ? `${e.player} falls in the maze` : `${e.player} loses both treasures`); break;
case "treasureDropped": if (e.onHomeOf === e.player) offer(7, `${e.player} carries a stolen treasure home`); break;
case "treasurePickedUp": if (e.owner !== e.player) offer(5, `${e.player} snatches ${e.owner}'s treasure`); break;
case "attackResolved": if (e.damageDealt > 0) offer(6, `${e.attacker} hits ${e.defender} with ${name(e.attackCardId)} for ${e.damageDealt}`); else if (e.fullyStopped) offer(4, `${e.defender} stops ${e.attacker}'s ${name(e.attackCardId)} cold`); break;
case "punched": offer(3, `${e.attacker} punches ${e.target}`); break;
case "spellCast": offer(2, e.target ? `${e.caster} casts ${name(e.cardId)} at ${e.target}` : `${e.caster} casts ${name(e.cardId)}`); break;
case "jumpedPit": offer(3, `${e.player} leaps the pit`); break;
case "fellInPit": offer(3, `${e.player} falls into the pit`); break;
case "wardSprung": offer(4, `${e.owner}'s warded treasure bites ${e.victim}`); break;
case "slimeTrapSprung": offer(4, `the slime springs ${name(e.cardId)} on ${e.victim}`); break;
default: break;
}
}
}
return best ? (best as { text: string }).text : null;
}
function shareHtml(id: string, data: ShareData, rawHost: string, rawProto: string): string {
const base = safeBase(rawHost, rawProto);
const headline = data.whole ? null : headlineOf(data);
const title = data.whole
? "The whole tale — a game of Wiz-War, replayed"
: `${escapeHtml(data.actor)}'s turn — a Wiz-War instant replay`;
: headline
? `${escapeHtml(headline)} — a Wiz-War instant replay`
: `${escapeHtml(data.actor)}'s turn — a Wiz-War instant replay`;
const desc = data.whole
? `A full game of Wiz-War, magical combat in a stone labyrinth — every turn through its wizard's own eyes${data.actor ? `, to ${escapeHtml(data.actor)}'s triumph` : ""}. Watch it all, then deal yourself in.`
: `Round ${data.round || "?"} of a game of Wiz-War, magical combat in a stone labyrinth. ` +