Card wave 2: durations, doors, teleports, modifiers; harden room auth

Duration system: sustained effects expire at the start of the caster's
turns; SLOW (movement 1, no number cards, attack every other turn),
NO SPELL, MEDUSA (paralysis + damage immunity), INVISIBLE (1-in-4 hit
roll), SHRINK (50% miss, movement 2). Doors: PICK LOCK and MASTER KEY
(displayed, reusable) unlock adjacent doors until end of turn, REMOVE
LOCK is permanent, JAM LOCK seals a door for everyone. Movement:
TELEPORT (4 spaces through walls, ends movement), PASS THROUGH WALL
charges, POWER RUN (life for spaces), SWAP (consumes movement),
GO AWAY (knockback + lost turn), TELEPORT OPPONENT. Card warfare:
CARD ERASURE (named), THOUGHT-STEAL (2 random via seeded RNG),
TELEPATH (private hand reveal), POWER DRAIN (damage feeds the caster),
SUDDEN DEATH, STONE DEAD, WIZARDBLADE (same-square, number-powered,
stays displayed). Cast modifiers: AMPLIFY doubles power/duration
(stackable x2), ADD permits two number cards, EXTEND doubles duration;
REVERSE heals instead of harms but keeps secondary effects. Counters
now also halve durations (BLUNT) and split them (REFLECTION).

Security (from review findings): room codes and game seeds now come
from node:crypto, and every seat gets a secret token — reclaiming a
name in a room requires its token, closing the impersonation hole.

29 cards implemented; 51 tests passing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Eric Wagoner
2026-08-15 20:04:44 -04:00
co-authored by Claude Fable 5
parent 36b3ffe9a6
commit 67743dd17e
6 changed files with 1272 additions and 192 deletions
+5 -3
View File
@@ -84,9 +84,10 @@ wss.on("connection", (socket) => {
case "create": {
const name = String(msg.name ?? "").trim();
if (!name) return send(socket, { type: "error", message: "name required" });
const room = createRoom(name);
const { room, token } = createRoom(name);
session.playerId = name;
session.roomId = room.id;
send(socket, { type: "seat", playerId: name, token });
broadcastRoomState(room);
break;
}
@@ -96,10 +97,11 @@ wss.on("connection", (socket) => {
if (!name || !roomId) return send(socket, { type: "error", message: "name and roomId required" });
const room = getRoom(roomId);
if (!room) return send(socket, { type: "error", message: "no such room" });
const problem = joinRoom(room, name);
if (problem) return send(socket, { type: "error", message: problem });
const result = joinRoom(room, name, typeof msg.token === "string" ? msg.token : null);
if ("error" in result) return send(socket, { type: "error", message: result.error });
session.playerId = name;
session.roomId = room.id;
send(socket, { type: "seat", playerId: name, token: result.token });
broadcastRoomState(room);
break;
}