BIG MAN pushes around corners; at a fork the pushed wizard chooses
Eric, a giant on his first turn, could only shove a wizard in a straight line: the engine shoved in the giant's own direction or refused. The FAQ says otherwise — "If Big Man pushes someone to an intersection, then the other player gets to decide which direction to go." Now a pushed wizard leaves by any open side but the way the giant came. One open side is taken. Several are theirs to choose: the stride hangs in a pushPending window like the Ward's, the pushed wizard answers with pushChoice (a dialog offers the ways; an automaton steps out of the giant's line), and the stride resumes with the answer — a blinded giant's roll is not rolled twice. Rounding a corner was a refusal before, so every game may do it now. Only the case that used to resolve — straight ahead open — keeps its old reading in games dealt before rev 15; from rev 15 the pushed wizard chooses there too. Tests pin the corner, the fork's window and its refusals, and the older reading; 84 ledgers replay. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jm2auWk6RP71CjaAb4FMoG
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
c6913ea1bc
commit
e724267b0a
@@ -1,7 +1,7 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
type CreatureState, applyCommand, activePlayer, boardView, creatureAt, type GameState, type PlayerId, createGame } from "../src/game";
|
||||
import { cellKey, edgeKey, SIDES, stepTarget, type Cell, type Side } from "../src/board";
|
||||
import { cellKey, edgeKey, neighbor, opposite, SIDES, stepTarget, type Cell, type Side } from "../src/board";
|
||||
import type { CardInstance } from "../src/cards";
|
||||
import { newExpansionGame as newGame, must, drain, giveCard, toRound2, emptyNeighborCell, pushSustained } from "./helpers";
|
||||
|
||||
@@ -392,23 +392,112 @@ describe("big man", () => {
|
||||
expect(cellKey(state.players.find((p) => p.id === rig.giant.id)!.position)).toBe(cellKey(run.mid));
|
||||
});
|
||||
|
||||
/** From a giant's square: a step into a square whose exits (all sides but
|
||||
* the way back) number exactly `want` — a corner (1, not straight) or a
|
||||
* fork (2+). Returns the giant's start, the pushee's square, and the exits. */
|
||||
function pushSite(state: GameState, want: "corner" | "fork") {
|
||||
const view = boardView(state);
|
||||
for (const key of Object.keys(view.cells)) {
|
||||
const [x, y] = key.split(",").map(Number) as [number, number];
|
||||
if (view.homes.some((h) => cellKey(h) === key)) continue;
|
||||
for (const side of SIDES) {
|
||||
const t1 = stepTarget(view, { x, y }, side);
|
||||
if (t1.kind !== "step") continue;
|
||||
const exits = SIDES.filter((d) => d !== opposite(side) && stepTarget(view, t1.to, d).kind === "step");
|
||||
if (want === "corner" && exits.length === 1 && exits[0] !== side) return { at: { x, y }, side, mid: t1.to, exits };
|
||||
if (want === "fork" && exits.length >= 2) return { at: { x, y }, side, mid: t1.to, exits };
|
||||
}
|
||||
}
|
||||
throw new Error(`no ${want} on this board`);
|
||||
}
|
||||
|
||||
it("pushes a wizard around a corner when that is the only way (rev 15)", () => {
|
||||
const rig = bigRig();
|
||||
let state = rig.state;
|
||||
const site = pushSite(state, "corner");
|
||||
const giant = state.players.find((p) => p.id === rig.giant.id)!;
|
||||
const victim = state.players.find((p) => p.id !== rig.giant.id)!;
|
||||
giant.position = { ...site.at };
|
||||
victim.position = { ...site.mid };
|
||||
state = must(state, rig.giant.id, { type: "move", direction: site.side });
|
||||
const after = state.players.find((p) => p.id !== rig.giant.id)!;
|
||||
expect(cellKey(after.position)).toBe(cellKey(neighbor(site.mid, site.exits[0]!)));
|
||||
expect(cellKey(state.players.find((p) => p.id === rig.giant.id)!.position)).toBe(cellKey(site.mid));
|
||||
});
|
||||
|
||||
it("at a fork the pushed wizard chooses, and the stride hangs until they do", () => {
|
||||
const rig = bigRig();
|
||||
let state = rig.state;
|
||||
const site = pushSite(state, "fork");
|
||||
const giant = state.players.find((p) => p.id === rig.giant.id)!;
|
||||
const victim = state.players.find((p) => p.id !== rig.giant.id)!;
|
||||
giant.position = { ...site.at };
|
||||
victim.position = { ...site.mid };
|
||||
const used = state.turn.movementUsed;
|
||||
const r = applyCommand(state, rig.giant.id, { type: "move", direction: site.side });
|
||||
if (!r.ok) throw new Error(r.error);
|
||||
state = r.state;
|
||||
expect(state.pushPending?.pusheeId).toBe(victim.id);
|
||||
expect(state.turn.movementUsed).toBe(used);
|
||||
expect(cellKey(state.players.find((p) => p.id === rig.giant.id)!.position)).toBe(cellKey(site.at));
|
||||
// Only the pushed wizard may answer, and only with pushChoice, and only an open way.
|
||||
expect(applyCommand(state, rig.giant.id, { type: "move", direction: site.side }).ok).toBe(false);
|
||||
expect(applyCommand(state, victim.id, { type: "pass" }).ok).toBe(false);
|
||||
expect(applyCommand(state, victim.id, { type: "pushChoice", direction: opposite(site.side) }).ok).toBe(false);
|
||||
const pick = site.exits[site.exits.length - 1]!;
|
||||
state = must(state, victim.id, { type: "pushChoice", direction: pick });
|
||||
expect(state.pushPending).toBeNull();
|
||||
expect(cellKey(state.players.find((p) => p.id !== rig.giant.id)!.position)).toBe(cellKey(neighbor(site.mid, pick)));
|
||||
expect(cellKey(state.players.find((p) => p.id === rig.giant.id)!.position)).toBe(cellKey(site.mid));
|
||||
expect(state.turn.movementUsed).toBe(used + 1);
|
||||
});
|
||||
|
||||
it("an older game shoves straight ahead when it can, and now rounds a corner when it cannot", () => {
|
||||
let { state } = createGame({ playerIds: ["alice", "bob"], seed: 42, sets: ["basic", "expansion1"], deckRev: 14 });
|
||||
state = toRound2(state);
|
||||
const giant = activePlayer(state);
|
||||
state.sustained.push({ id: "fx-big", cardId: "big-man", casterId: giant.id, targetId: giant.id, remainingTurns: 9, data: {} });
|
||||
const victim = state.players.find((p) => p.id !== giant.id)!;
|
||||
// A fork where straight ahead is open: no window, straight it is.
|
||||
const fork = pushSite(state, "fork");
|
||||
if (fork.exits.includes(fork.side)) {
|
||||
giant.position = { ...fork.at };
|
||||
victim.position = { ...fork.mid };
|
||||
const r = applyCommand(state, giant.id, { type: "move", direction: fork.side });
|
||||
if (!r.ok) throw new Error(r.error);
|
||||
expect(r.state.pushPending).toBeNull();
|
||||
expect(cellKey(r.state.players.find((p) => p.id !== giant.id)!.position)).toBe(cellKey(neighbor(fork.mid, fork.side)));
|
||||
}
|
||||
// A corner: refused before, rounded now.
|
||||
const corner = pushSite(state, "corner");
|
||||
giant.position = { ...corner.at };
|
||||
victim.position = { ...corner.mid };
|
||||
const r = applyCommand(state, giant.id, { type: "move", direction: corner.side });
|
||||
if (!r.ok) throw new Error(r.error);
|
||||
expect(cellKey(r.state.players.find((p) => p.id !== giant.id)!.position)).toBe(cellKey(neighbor(corner.mid, corner.exits[0]!)));
|
||||
});
|
||||
|
||||
it("no room to shove means no way forward", () => {
|
||||
const rig = bigRig();
|
||||
const state = rig.state;
|
||||
const victim = state.players.find((p) => p.id !== rig.giant.id)!;
|
||||
const view = boardView(state);
|
||||
// Find a step whose far side is walled: victim there cannot be pushed.
|
||||
for (const side of SIDES) {
|
||||
const t1 = stepTarget(view, rig.giant.position, side);
|
||||
if (t1.kind !== "step") continue;
|
||||
const t2 = stepTarget(view, t1.to, side);
|
||||
if (t2.kind === "step") continue;
|
||||
victim.position = { ...t1.to };
|
||||
const r = applyCommand(state, rig.giant.id, { type: "move", direction: side });
|
||||
expect(r.ok).toBe(false);
|
||||
return;
|
||||
// A dead-end pocket beside some square: a victim there cannot be pushed anywhere.
|
||||
const giant = state.players.find((p) => p.id === rig.giant.id)!;
|
||||
for (const key of Object.keys(view.cells)) {
|
||||
const [x, y] = key.split(",").map(Number) as [number, number];
|
||||
for (const side of SIDES) {
|
||||
const t1 = stepTarget(view, { x, y }, side);
|
||||
if (t1.kind !== "step") continue;
|
||||
if (SIDES.some((d) => d !== opposite(side) && stepTarget(view, t1.to, d).kind === "step")) continue;
|
||||
giant.position = { x, y };
|
||||
victim.position = { ...t1.to };
|
||||
const r = applyCommand(state, rig.giant.id, { type: "move", direction: side });
|
||||
expect(r.ok).toBe(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
throw new Error("setup: seed offered no walled pocket beside the home");
|
||||
throw new Error("setup: no dead-end pocket on this board");
|
||||
});
|
||||
|
||||
it("a shrunk wizard slips between the giant's boots", () => {
|
||||
|
||||
Reference in New Issue
Block a user