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);
|
const carried = ctx.state.treasures.find((t) => t.id === ctx.defender.carriedTreasureId);
|
||||||
if (carried?.owner === ctx.defender.id) return;
|
if (carried?.owner === ctx.defender.id) return;
|
||||||
attachSustained(ctx.state, ctx.events, "idiot", ctx.attacker.id, ctx.defender.id, PERMANENT_TURNS);
|
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": {
|
"big-man": {
|
||||||
@@ -4400,19 +4402,7 @@ function doMove(prev: GameState, direction: Side, over = false): CommandResult {
|
|||||||
state.turn.actionsEnded = true;
|
state.turn.actionsEnded = true;
|
||||||
springSlimeTrap(state, events, p);
|
springSlimeTrap(state, events, p);
|
||||||
}
|
}
|
||||||
// IDIOT lifts when the victim reaches their own treasure.
|
liftIdiotIfSatisfied(state, events, p);
|
||||||
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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Armed ambushes may spring on this step.
|
// Armed ambushes may spring on this step.
|
||||||
checkAmbushes(state, events, p, { movedFrom: from });
|
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
|
* treasure on their own home, or drop their own treasure underfoot for an
|
||||||
* instant cure.
|
* 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 {
|
function idiotBlocked(state: GameState, playerId: PlayerId): string | null {
|
||||||
if (sustainedOn(state, playerId, "idiot").length > 0) {
|
if (sustainedOn(state, playerId, "idiot").length > 0) {
|
||||||
return "What am I doing here...? (you can do nothing but head for your treasure)";
|
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[] = [
|
const events: GameEvent[] = [
|
||||||
{ type: "treasurePickedUp", player: p.id, treasureId: t.id, owner: t.owner, at: p.position },
|
{ 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" —
|
// WARD: "you may play at that time (out of turn) this card on him" —
|
||||||
// literally: the grab hangs while the owner decides.
|
// literally: the grab hangs while the owner decides.
|
||||||
const owner = state.players.find((q) => q.id === t.owner);
|
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", () => {
|
it("idiot forbids item handling and punches but allows counteractions", () => {
|
||||||
let { state } = createGame({
|
let { state } = createGame({
|
||||||
playerIds: ["alice", "bob"], seed: 42, sets: ["basic", "expansion1"],
|
playerIds: ["alice", "bob"], seed: 42, sets: ["basic", "expansion1"],
|
||||||
|
|||||||
Reference in New Issue
Block a user