Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
805fb09782 | ||
|
|
168e57ab91 |
@@ -1374,19 +1374,25 @@ export function automatonCommand(
|
||||
const ownGoldCells = new Set(view.treasures
|
||||
.filter((t) => t.owner === view.you && t.position && !t.carriedBy)
|
||||
.map((t) => cellKey(t.position!)));
|
||||
// A thief already underfoot is not a destination — with the chase
|
||||
// moot, march on the real goals (deliver, grab) instead of standing
|
||||
// on them forever.
|
||||
const thiefAfar = thief && cellKey(thief.position) !== here ? thief : null;
|
||||
// The chase exists to deliver a blow, so it claims the march only
|
||||
// while that blow is still live: an attack in hand, this turn's
|
||||
// attack unspent, casting not yet closed. Those gates hold still as
|
||||
// the clockwork walks — a goal keyed to the thief's nearness flips
|
||||
// underfoot and shuttles the walker on and off their square. Caught
|
||||
// with the strike still live, it stands its ground (a goal underfoot
|
||||
// ends the march); otherwise the gold has its legs for the turn.
|
||||
const strikeLive = !view.turn.attackUsed && !view.turn.attackForbidden &&
|
||||
!view.turn.actionsEnded && view.yourHand.some((c) => ATTACKS[c.cardId] != null);
|
||||
const chasing = thief !== null && strikeLive;
|
||||
const objectives = cursedIdiot && ownGoldCells.size > 0
|
||||
? ownGoldCells
|
||||
: thiefAfar
|
||||
? new Set([cellKey(thiefAfar.position)])
|
||||
: chasing
|
||||
? new Set([cellKey(thief.position)])
|
||||
: style === "berserker" && !self.carriedTreasureId
|
||||
? (enemyCells.size > 0 ? enemyCells : gold)
|
||||
: gold;
|
||||
const path =
|
||||
pathToward(view, self.position, objectives, { avoidNearEnemies: style === "worrier" && !thiefAfar, canUnlock }) ??
|
||||
pathToward(view, self.position, objectives, { avoidNearEnemies: style === "worrier" && !chasing, canUnlock }) ??
|
||||
pathToward(view, self.position, objectives, { canUnlock }) ??
|
||||
pathToward(view, self.position, enemyCells, { canUnlock }) ??
|
||||
// No clean road to anything: grit the teeth and wade the hazards,
|
||||
|
||||
@@ -4933,6 +4933,28 @@ function doCast(prev: GameState, cmd: Extract<Command, { type: "cast" }>): Comma
|
||||
}
|
||||
return { ok: true, state, events: events2 };
|
||||
}
|
||||
// CHAOS aims at no one: cast bare, it sweeps every other living
|
||||
// wizard into the pile, each offered their FULL SHIELD sit-out in
|
||||
// turn order. (Full shields only exclude; nothing in the set cancels
|
||||
// a zero-point storm, so the queue is the whole defense.) The older
|
||||
// player-targeted form is still accepted below, so stored games
|
||||
// replay unchanged.
|
||||
if (inHand.cardId === "chaos" && (!cmd.target || cmd.target.kind !== "player")) {
|
||||
consumeCast(state, caster, inHand, mods, false);
|
||||
if (state.turn.attackUsed) state.turn.secondAttackUsed = true;
|
||||
state.turn.attackUsed = true;
|
||||
state.lastSpellUsed[caster.id] = inHand.cardId;
|
||||
const events: GameEvent[] = [{
|
||||
type: "spellCast", caster: caster.id, card: inHand, cardId: inHand.cardId,
|
||||
numberCards: mods.numbers, numberValue: mods.magnitude.numberValue,
|
||||
from: caster.position, target: null, targetCell: null,
|
||||
}];
|
||||
const queue = turnOrderFrom(state, caster.id).filter((id) =>
|
||||
id !== caster.id && state.players.find((p) => p.id === id)!.alive);
|
||||
state.chaosPending = { casterId: caster.id, excluded: [], queue };
|
||||
finishChaosIfReady(state, events);
|
||||
return { ok: true, state, events };
|
||||
}
|
||||
if (!cmd.target || cmd.target.kind !== "player") return err("attack spells target a player");
|
||||
if (cmd.target.playerId === caster.id) return err("you cannot attack yourself");
|
||||
const target = state.players.find((p) => p.id === (cmd.target as { playerId: PlayerId }).playerId);
|
||||
|
||||
@@ -14,6 +14,7 @@ import { pushSustained } from "./helpers";
|
||||
/** Whose input does the maze want right now? */
|
||||
function actingSeat(state: GameState): PlayerId {
|
||||
return (
|
||||
state.wardPending?.ownerId ??
|
||||
state.stack?.waitingOn ??
|
||||
state.pendingDiscard ??
|
||||
state.chaosPending?.queue[0] ??
|
||||
@@ -735,3 +736,42 @@ describe("the clockwork flees the dread", () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("the thief-chase holds one goal per turn", () => {
|
||||
it("a clockwork beside its thief never shuttles on and off their square", () => {
|
||||
let { state } = createGame({ playerIds: ["thief", "bot"], seed: 42, sets: ["basic", "expansion1"] });
|
||||
// Round 2, thief's turn burned; the bot acts with a full allowance.
|
||||
while (state.turn.round < 2 || state.players[state.turn.activeIndex]!.id !== "bot") {
|
||||
const r = applyCommand(state, actingSeat(state), { type: "endTurn", draw: 0 });
|
||||
if (!r.ok) throw new Error(r.error);
|
||||
state = r.state;
|
||||
}
|
||||
const thief = state.players.find((p) => p.id === "thief")!;
|
||||
const bot = state.players.find((p) => p.id === "bot")!;
|
||||
// The thief carries the bot's gold and stands one step away.
|
||||
const mine = state.treasures.find((t) => t.owner === "bot")!;
|
||||
mine.carriedBy = "thief";
|
||||
mine.position = null;
|
||||
thief.position = { x: 2, y: 4 };
|
||||
bot.position = { x: 2, y: 5 };
|
||||
state.edgeOverrides[edgeKey({ x: 2, y: 4 }, "S")] = "open";
|
||||
// A number card and no attacks: the blow the chase serves cannot land.
|
||||
bot.hand = [];
|
||||
bot.hand.push({ cardId: "number-2", instanceId: "N2" } as never);
|
||||
const visited = [cellKey(bot.position)];
|
||||
for (let guard = 0; guard < 40; guard++) {
|
||||
const view = viewFor(state, "bot");
|
||||
const cmd = automatonCommand(view, "hunter", "archmage") ?? automatonFallback(view, "archmage");
|
||||
if (cmd.type === "endTurn") break;
|
||||
const r = applyCommand(state, "bot", cmd);
|
||||
if (!r.ok) break;
|
||||
state = r.state;
|
||||
const at = cellKey(state.players.find((p) => p.id === "bot")!.position);
|
||||
if (cmd.type === "move" && at !== visited[visited.length - 1]) visited.push(at);
|
||||
}
|
||||
// No step may return to the square just departed: A-B-A is the shuttle.
|
||||
for (let i = 2; i < visited.length; i++) {
|
||||
expect(visited[i]).not.toBe(visited[i - 2]);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -402,6 +402,35 @@ describe("the ward window and chaos shields", () => {
|
||||
expect(state.players.find((p) => p.id === owner.id)!.hand.some((c) => c.cardId === "ward")).toBe(false);
|
||||
});
|
||||
|
||||
it("chaos casts bare — no target, every other wizard queued for a shield", () => {
|
||||
let { state } = threeGame();
|
||||
state = toRound2(state);
|
||||
state = must(state, activePlayer(state).id, { type: "endTurn", draw: 0 });
|
||||
const caster = activePlayer(state);
|
||||
const others = state.players.filter((p) => p.id !== caster.id).map((p) => p.id);
|
||||
const chaos = giveCard(state, caster.id, "chaos", "C", 0);
|
||||
const shielder = others[0]!;
|
||||
giveCard(state, shielder, "full-shield", "S", 0);
|
||||
const shielderHand = () => state.players.find((p) => p.id === shielder)!.hand.map((c) => c.instanceId).sort();
|
||||
const kept = shielderHand().filter((id) => id !== "full-shield#S").sort();
|
||||
|
||||
state = must(state, caster.id, { type: "cast", instanceId: chaos.instanceId });
|
||||
// No stack: the queue opens at once, in turn order from the caster.
|
||||
expect(state.stack).toBeNull();
|
||||
expect(state.chaosPending?.queue.length).toBe(2);
|
||||
const first = state.chaosPending!.queue[0]!;
|
||||
state = first === shielder
|
||||
? must(state, first, { type: "counteract", instanceId: "full-shield#S" })
|
||||
: must(state, first, { type: "pass" });
|
||||
const second = state.chaosPending!.queue[0]!;
|
||||
state = second === shielder
|
||||
? must(state, second, { type: "counteract", instanceId: "full-shield#S" })
|
||||
: must(state, second, { type: "pass" });
|
||||
expect(state.chaosPending).toBeNull();
|
||||
// The shielded hand rode out the storm untouched.
|
||||
expect(shielderHand()).toEqual(kept);
|
||||
});
|
||||
|
||||
it("chaos: bystanders may shield out, reflections are refused", () => {
|
||||
let { state } = threeGame();
|
||||
state = toRound2(state);
|
||||
|
||||
@@ -226,6 +226,7 @@ const NOTABLE_EVENTS = new Set([
|
||||
"firewallBurned", "objectThrown", "gameWon", "positionsSwapped",
|
||||
"teleported", "stonesDestroyed", "thumbOfGod", "stoneTurnedToWater",
|
||||
"waterwallCrashes", "sectorRotated", "sectorRelocated", "homeBasesSwapped",
|
||||
"handsScrambled",
|
||||
]);
|
||||
|
||||
const SEAT_KEY = "wizwar-seat";
|
||||
|
||||
Reference in New Issue
Block a user