Credibility pass: the splice marks sanded from the rev-11 batch
The extracted rebuildRoom loop reflowed to the file's indent and its doc comments unstacked; liftIdiotIfSatisfied moves below idiotBlocked so each keeps its own comment; the pickup's duplicated owner lookup folds to one; the cloned grab-refusal chain hoists to grabRefused; a one-off #8a7a5c rejoins the #8a7a5e palette; an inert overflow-x line and a duplicate import go; span 6 and the two cache strategies say why; the reversed walking-dead test moves beside its biting sibling. Also: the replay harness now honors lobby kicks, which VVZU (five bots seated, two kicked) was the first production ledger to exercise. 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
e83d70116f
commit
a1fdb37c54
@@ -4568,6 +4568,13 @@ function castingBlocked(
|
||||
* treasure on their own home, or drop their own treasure underfoot for an
|
||||
* instant cure.
|
||||
*/
|
||||
function idiotBlocked(state: GameState, playerId: PlayerId): string | null {
|
||||
if (sustainedOn(state, playerId, "idiot").length > 0) {
|
||||
return "What am I doing here...? (you can do nothing but head for your treasure)";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** IDIOT lifts when the march is done — the victim stands on their own
|
||||
* treasure — or turns unsatisfiable: every one of their treasures is in
|
||||
* someone's arms, leaving nothing to stand on. */
|
||||
@@ -4584,13 +4591,6 @@ function liftIdiotIfSatisfied(state: GameState, events: GameEvent[], p: PlayerSt
|
||||
state.sustained = state.sustained.filter((fx) => !(fx.cardId === "idiot" && fx.targetId === p.id));
|
||||
}
|
||||
|
||||
function idiotBlocked(state: GameState, playerId: PlayerId): string | null {
|
||||
if (sustainedOn(state, playerId, "idiot").length > 0) {
|
||||
return "What am I doing here...? (you can do nothing but head for your treasure)";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** "One cannot attack or be attacked while in a bush"; mist-bodies neither. */
|
||||
function attackBlockedByStatus(state: GameState, attacker: PlayerState, target: PlayerState): string | null {
|
||||
if (sustainedOn(state, target.id, "big-man").length > 0 &&
|
||||
@@ -6423,13 +6423,12 @@ function doPickUpTreasure(prev: GameState, treasureId?: string): CommandResult {
|
||||
const events: GameEvent[] = [
|
||||
{ type: "treasurePickedUp", player: p.id, treasureId: t.id, owner: t.owner, at: p.position },
|
||||
];
|
||||
const owner = state.players.find((q) => q.id === t.owner);
|
||||
// This grab may have lifted the owner's IDIOT: with every one of their
|
||||
// treasures now carried, there is nothing left to march to.
|
||||
const cursedOwner = state.players.find((q) => q.id === t.owner);
|
||||
if (cursedOwner) liftIdiotIfSatisfied(state, events, cursedOwner);
|
||||
if (owner) liftIdiotIfSatisfied(state, events, owner);
|
||||
// WARD: "you may play at that time (out of turn) this card on him" —
|
||||
// literally: the grab hangs while the owner decides.
|
||||
const owner = state.players.find((q) => q.id === t.owner);
|
||||
if (owner && owner.alive && owner.id !== p.id) {
|
||||
const holdsWard = owner.hand.some((c) => c.cardId === "ward");
|
||||
if (holdsWard) {
|
||||
|
||||
@@ -412,6 +412,8 @@ describe("big man", () => {
|
||||
});
|
||||
|
||||
it("a shrunk wizard slips between the giant's boots", () => {
|
||||
// bigRig() makes the ACTIVE player the giant; here the mover must act,
|
||||
// so the giant takes the other seat.
|
||||
let { state } = createGame({ playerIds: ["alice", "bob"], seed: 42, sets: ["basic", "expansion1"] });
|
||||
state = toRound2(state);
|
||||
const mover = activePlayer(state);
|
||||
|
||||
@@ -58,6 +58,32 @@ describe("expansion combat cards", () => {
|
||||
expect(state.players.find((p) => p.id === defender)!.life).toBe(lifeStart - 1);
|
||||
});
|
||||
|
||||
it("a reversed walking dead heals half a point per space walked (rev 11)", () => {
|
||||
let { state } = newGame();
|
||||
state = toRound2(state);
|
||||
const { attacker, defender } = faceOff(state);
|
||||
const wd = giveCard(state, attacker, "walking-dead");
|
||||
giveCard(state, defender, "reverse", "RV", 0);
|
||||
const r = applyCommand(state, attacker, {
|
||||
type: "cast", instanceId: wd.instanceId, target: { kind: "player", playerId: defender },
|
||||
});
|
||||
if (!r.ok) throw new Error(r.error);
|
||||
state = must(r.state, defender, { type: "counteract", instanceId: "reverse#RV" });
|
||||
state = drain(state);
|
||||
expect(sustainedOn(state, defender, "walking-dead").length).toBe(1);
|
||||
|
||||
state = must(state, attacker, { type: "endTurn", draw: 0 });
|
||||
// Two spaces walked: one point gained, not bled.
|
||||
let steps = 0;
|
||||
for (const dir of ["N", "S", "E", "W", "N", "S", "E", "W"] as const) {
|
||||
if (steps >= 2) break;
|
||||
const mv = applyCommand(state, defender, { type: "move", direction: dir });
|
||||
if (mv.ok) { state = mv.state; steps++; }
|
||||
}
|
||||
expect(steps).toBe(2);
|
||||
expect(state.players.find((p) => p.id === defender)!.life).toBe(16);
|
||||
});
|
||||
|
||||
it("mental swap trades entire hands", () => {
|
||||
let { state } = newGame();
|
||||
state = toRound2(state);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { applyCommand, activePlayer, displays, handLimit, sustainedOn, type GameState, type PlayerId } from "../src/game";
|
||||
import { applyCommand, activePlayer, createGame, displays, handLimit, sustainedOn, type GameState, type PlayerId } from "../src/game";
|
||||
import type { CardInstance } from "../src/cards";
|
||||
import { createGame } from "../src/game";
|
||||
import { newGame, must, drain, giveCard, toRound2, faceOff, castAt } from "./helpers";
|
||||
|
||||
/** Display a stone for a player during their turn. */
|
||||
@@ -170,31 +169,6 @@ describe("slow death", () => {
|
||||
expect(state.players.find((p) => p.id === defender)!.life).toBe(17);
|
||||
});
|
||||
|
||||
it("a reversed walking dead heals half a point per space walked (rev 11)", () => {
|
||||
let { state } = newGame();
|
||||
state = toRound2(state);
|
||||
const { attacker, defender } = faceOff(state);
|
||||
const wd = giveCard(state, attacker, "walking-dead");
|
||||
giveCard(state, defender, "reverse", "RV", 0);
|
||||
const r = applyCommand(state, attacker, {
|
||||
type: "cast", instanceId: wd.instanceId, target: { kind: "player", playerId: defender },
|
||||
});
|
||||
if (!r.ok) throw new Error(r.error);
|
||||
state = must(r.state, defender, { type: "counteract", instanceId: "reverse#RV" });
|
||||
state = drain(state);
|
||||
expect(sustainedOn(state, defender, "walking-dead").length).toBe(1);
|
||||
|
||||
state = must(state, attacker, { type: "endTurn", draw: 0 });
|
||||
// Two spaces walked: one point gained, not bled.
|
||||
let steps = 0;
|
||||
for (const dir of ["N", "S", "E", "W", "N", "S", "E", "W"] as const) {
|
||||
if (steps >= 2) break;
|
||||
const mv = applyCommand(state, defender, { type: "move", direction: dir });
|
||||
if (mv.ok) { state = mv.state; steps++; }
|
||||
}
|
||||
expect(steps).toBe(2);
|
||||
expect(state.players.find((p) => p.id === defender)!.life).toBe(16);
|
||||
});
|
||||
|
||||
it("a full reflection returns the curse onto its caster (rev 11)", () => {
|
||||
let { state } = newGame();
|
||||
|
||||
Reference in New Issue
Block a user