Idiot lifts the moment its march turns impossible
The all-carried check ran only on the victim's own step, so a curse whose last floor treasure was hauled off mid-turn kept its grip until the idiot happened to move. The lift check now also runs at the pickup that empties the floor, and at attach for a victim already standing on their chest. 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
dd6ba59220
commit
7ee3c01e44
+23
-13
@@ -2575,6 +2575,8 @@ const CARD_EFFECTS: Record<string, AttackEffect | NeutralEffect | CounterEffect>
|
||||
const carried = ctx.state.treasures.find((t) => t.id === ctx.defender.carriedTreasureId);
|
||||
if (carried?.owner === ctx.defender.id) return;
|
||||
attachSustained(ctx.state, ctx.events, "idiot", ctx.attacker.id, ctx.defender.id, PERMANENT_TURNS);
|
||||
// Already standing on their own treasure: the march is over as it begins.
|
||||
liftIdiotIfSatisfied(ctx.state, ctx.events, ctx.defender);
|
||||
},
|
||||
},
|
||||
"big-man": {
|
||||
@@ -4400,19 +4402,7 @@ function doMove(prev: GameState, direction: Side, over = false): CommandResult {
|
||||
state.turn.actionsEnded = true;
|
||||
springSlimeTrap(state, events, p);
|
||||
}
|
||||
// IDIOT lifts when the victim reaches their own treasure.
|
||||
if (sustainedOn(state, p.id, "idiot").length > 0) {
|
||||
const onOwn = state.treasures.some(
|
||||
(t) => t.owner === p.id && t.position && cellKey(t.position) === cellKey(p.position),
|
||||
);
|
||||
const allCarried = !state.treasures.some((t) => t.owner === p.id && t.position);
|
||||
if (onOwn || allCarried) {
|
||||
for (const fx of sustainedOn(state, p.id, "idiot")) {
|
||||
events.push({ type: "spellExpired", effectId: fx.id, cardId: "idiot", target: p.id });
|
||||
}
|
||||
state.sustained = state.sustained.filter((fx) => !(fx.cardId === "idiot" && fx.targetId === p.id));
|
||||
}
|
||||
}
|
||||
liftIdiotIfSatisfied(state, events, p);
|
||||
|
||||
// Armed ambushes may spring on this step.
|
||||
checkAmbushes(state, events, p, { movedFrom: from });
|
||||
@@ -4561,6 +4551,22 @@ function castingBlocked(
|
||||
* treasure on their own home, or drop their own treasure underfoot for an
|
||||
* instant cure.
|
||||
*/
|
||||
/** 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. */
|
||||
function liftIdiotIfSatisfied(state: GameState, events: GameEvent[], p: PlayerState): void {
|
||||
if (sustainedOn(state, p.id, "idiot").length === 0) return;
|
||||
const onOwn = state.treasures.some(
|
||||
(t) => t.owner === p.id && t.position && cellKey(t.position) === cellKey(p.position),
|
||||
);
|
||||
const allCarried = !state.treasures.some((t) => t.owner === p.id && t.position);
|
||||
if (!onOwn && !allCarried) return;
|
||||
for (const fx of sustainedOn(state, p.id, "idiot")) {
|
||||
events.push({ type: "spellExpired", effectId: fx.id, cardId: "idiot", target: p.id });
|
||||
}
|
||||
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)";
|
||||
@@ -6400,6 +6406,10 @@ function doPickUpTreasure(prev: GameState, treasureId?: string): CommandResult {
|
||||
const events: GameEvent[] = [
|
||||
{ type: "treasurePickedUp", player: p.id, treasureId: t.id, owner: t.owner, at: p.position },
|
||||
];
|
||||
// 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);
|
||||
// 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);
|
||||
|
||||
@@ -165,6 +165,34 @@ describe("expansion combat cards", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("idiot lifts the moment the victim's last floor treasure is carried off", () => {
|
||||
let { state } = createGame({ playerIds: ["alice", "bob", "carol"], seed: 42, sets: ["basic"] });
|
||||
while (state.turn.round < 2) {
|
||||
state = must(state, activePlayer(state).id, { type: "endTurn", draw: 0 });
|
||||
}
|
||||
const { attacker, defender } = faceOff(state);
|
||||
const third = state.players.find((p) => p.id !== attacker && p.id !== defender)!;
|
||||
const id = giveCard(state, attacker, "idiot");
|
||||
state = castAt(state, attacker, defender, id);
|
||||
expect(sustainedOn(state, defender, "idiot").length).toBe(1);
|
||||
|
||||
// A third wizard hauls off one of the victim's chests: one destination
|
||||
// remains, so the march goes on.
|
||||
const [first, second] = state.treasures.filter((t) => t.owner === defender);
|
||||
const hauler = state.players.find((p) => p.id === third.id)!;
|
||||
first!.carriedBy = hauler.id;
|
||||
first!.position = null;
|
||||
hauler.carriedTreasureId = first!.id;
|
||||
expect(sustainedOn(state, defender, "idiot").length).toBe(1);
|
||||
|
||||
// The caster takes the last one off the floor — nothing left to march
|
||||
// to, and the curse lifts on the spot.
|
||||
const att = state.players.find((p) => p.id === attacker)!;
|
||||
att.position = { ...second!.position! };
|
||||
state = must(state, attacker, { type: "pickUpTreasure", treasureId: second!.id });
|
||||
expect(sustainedOn(state, defender, "idiot").length).toBe(0);
|
||||
});
|
||||
|
||||
it("idiot forbids item handling and punches but allows counteractions", () => {
|
||||
let { state } = createGame({
|
||||
playerIds: ["alice", "bob"], seed: 42, sets: ["basic", "expansion1"],
|
||||
|
||||
Reference in New Issue
Block a user