Rev 23: POWER DRAIN drains the number played; the troll punches walls; the first-person stack fits

POWER DRAIN's gain is the number played, BLUNTed or ABSORBed or not
(FAQ: the counter blunts the damage done, not the drain), and a wall
drained for its points gives them up too. Older games gave only what
the opponent lost and nothing from a wall, and replay so.

"This rock-hard beast can punch a player (or a wall, etc.)": a commanded
troll now punches a wall line beside it for a D4, once a turn — a new
command, so no old game changes.

Under the first-person pane the board strip could run into the dock on
a short or tall window; the pane now takes no more height than the row
can spare and the strip shrinks beneath it.

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-17 00:07:34 -04:00
co-authored by Claude Fable 5.1
parent 803c51415e
commit 80dd7a7865
6 changed files with 145 additions and 11 deletions
@@ -1,8 +1,8 @@
import { describe, expect, it } from "vitest";
import { applyCommand, activePlayer, boardView, createGame, sustainedOn, type GameState } from "../src/game";
import { cellKey, edgeKey, neighbor, opposite, type Side } from "../src/board";
import { cellKey, edgeKey, neighbor, opposite, SIDES, type Side } from "../src/board";
import type { CardInstance } from "../src/cards";
import { newGame, must, giveCard, toRound2, faceOff, castAt } from "./helpers";
import { newGame, must, giveCard, toRound2, faceOff, castAt, drain } from "./helpers";
describe("duration spells", () => {
it("slow reduces movement to 1, blocks number cards, and halves attacks", () => {
@@ -567,3 +567,44 @@ describe("the displayed MASTER KEY", () => {
expect(r.ok).toBe(false);
});
});
describe("POWER DRAIN drains the number played (rev 23)", () => {
it("gains the number even when the blow is blunted", () => {
let { state } = newGame();
state = toRound2(state);
const { attacker, defender } = faceOff(state);
const pd = giveCard(state, attacker, "power-drain");
giveCard(state, attacker, "number-4", "N", 1);
const d = state.players.find((p) => p.id === defender)!;
d.hand[0] = { instanceId: "blunt#T", cardId: "blunt" };
state = must(state, attacker, { type: "cast", instanceId: pd.instanceId, target: { kind: "player", playerId: defender }, numberInstanceIds: ["number-4#N"] });
state = must(state, defender, { type: "counteract", instanceId: "blunt#T" });
state = drain(state);
expect(state.players.find((p) => p.id === defender)!.life).toBe(13);
expect(state.players.find((p) => p.id === attacker)!.life).toBe(19);
});
it("drains a wall for its points", () => {
let { state } = newGame();
state = toRound2(state);
const me = activePlayer(state);
const board = boardView(state);
const side = SIDES.find((s) => board.edges[edgeKey(me.position, s)] === "wall")!;
const pd = giveCard(state, me.id, "power-drain");
giveCard(state, me.id, "number-3", "N", 1);
state = must(state, me.id, { type: "cast", instanceId: pd.instanceId, target: { kind: "edge", cell: me.position, side }, numberInstanceIds: ["number-3#N"] });
expect(state.wallDamage[edgeKey(me.position, side)]).toBe(3);
expect(state.players.find((p) => p.id === me.id)!.life).toBe(18);
});
it("older games gave only what the opponent lost, and nothing from a wall", () => {
let state = toRound2(createGame({ playerIds: ["alice", "bob"], seed: 42, sets: ["basic"], deckRev: 22 }).state);
const me = activePlayer(state);
const board = boardView(state);
const side = SIDES.find((s) => board.edges[edgeKey(me.position, s)] === "wall")!;
const pd = giveCard(state, me.id, "power-drain");
giveCard(state, me.id, "number-3", "N", 1);
state = must(state, me.id, { type: "cast", instanceId: pd.instanceId, target: { kind: "edge", cell: me.position, side }, numberInstanceIds: ["number-3#N"] });
expect(state.players.find((p) => p.id === me.id)!.life).toBe(15);
});
});