Rev 16: BUTT-HEAD's charge is measured as far as its legs reach (K4T3)
Reuben, a goat with a 5 played and eight legs, aimed at a wizard eight corridor steps away and was told he had no wings. The charge measures the walk through real corridors, as rev 10 made it, but the walk was searched only six steps deep and anything past that came back as unreachable. From rev 16 the search runs as far as the legs do; older games keep the six-step reading. A test charges seven or eight steps under both revisions. 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
0911312f98
commit
3b52a92cdc
@@ -238,7 +238,7 @@ export interface CastParams {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** The revision new games are dealt under; GameConfig.deckRev pins it per game. */
|
/** The revision new games are dealt under; GameConfig.deckRev pins it per game. */
|
||||||
export const CURRENT_RULES_REV = 15;
|
export const CURRENT_RULES_REV = 16;
|
||||||
|
|
||||||
/** Every rulings revision since the baseline, newest last — the entries a
|
/** Every rulings revision since the baseline, newest last — the entries a
|
||||||
* game's deckRev freezes it before or after. Shown to players as the house
|
* game's deckRev freezes it before or after. Shown to players as the house
|
||||||
@@ -258,6 +258,7 @@ export const RULES_REVISIONS: { rev: number; note: string }[] = [
|
|||||||
{ rev: 13, note: "SLOW DEATH's per-draw bites land as ONE blow that pauses for a victim holding ABSORB or BLUNT — countered against the total (absorb soaks up to 3, blunt halves rounding up); older games bit instantly." },
|
{ rev: 13, note: "SLOW DEATH's per-draw bites land as ONE blow that pauses for a victim holding ABSORB or BLUNT — countered against the total (absorb soaks up to 3, blunt halves rounding up); older games bit instantly." },
|
||||||
{ rev: 14, note: "Crossing a pit on a 2, 3, or 4 means edging around its rim: the walker lands on an open square beside the pit — the only one if there is one, otherwise the one they name by clicking it — and a pit with no way off cannot be entered. Older games bounced the walker back and charged the stride." },
|
{ rev: 14, note: "Crossing a pit on a 2, 3, or 4 means edging around its rim: the walker lands on an open square beside the pit — the only one if there is one, otherwise the one they name by clicking it — and a pit with no way off cannot be entered. Older games bounced the walker back and charged the stride." },
|
||||||
{ rev: 15, note: "BIG MAN at a fork: a pushed wizard leaves by any open side but the way the giant came — the only one if there is one, otherwise the side they choose, the giant's stride hanging until they do (FAQ: at an intersection the other player decides). Older games shove straight ahead whenever that way is open; rounding a corner was never possible before and is allowed in every game." },
|
{ rev: 15, note: "BIG MAN at a fork: a pushed wizard leaves by any open side but the way the giant came — the only one if there is one, otherwise the side they choose, the giant's stride hanging until they do (FAQ: at an intersection the other player decides). Older games shove straight ahead whenever that way is open; rounding a corner was never possible before and is allowed in every game." },
|
||||||
|
{ rev: 16, note: "BUTT-HEAD's charge is measured as far as the goat's legs reach. Older games searched only six steps of corridor and refused a longer ram as unreachable, even with the legs to make it." },
|
||||||
];
|
];
|
||||||
|
|
||||||
export interface GameConfig {
|
export interface GameConfig {
|
||||||
@@ -2361,7 +2362,11 @@ const CARD_EFFECTS: Record<string, AttackEffect | NeutralEffect | CounterEffect>
|
|||||||
if ((state.config.deckRev ?? 1) < 10 || !target) return null;
|
if ((state.config.deckRev ?? 1) < 10 || !target) return null;
|
||||||
const caster = activePlayer(state);
|
const caster = activePlayer(state);
|
||||||
const legs = state.turn.movementAllowance - state.turn.movementUsed;
|
const legs = state.turn.movementAllowance - state.turn.movementUsed;
|
||||||
const d = walkingDistance(state, caster.position, target.position);
|
// Rev 16: the charge is measured as far as the legs reach. Before it,
|
||||||
|
// the search stopped at six steps and called anything past that
|
||||||
|
// unreachable — a goat with eight legs refused a seven-step ram.
|
||||||
|
const reach = (state.config.deckRev ?? 1) >= 16 ? legs : 6;
|
||||||
|
const d = walkingDistance(state, caster.position, target.position, reach);
|
||||||
if (d > legs) return "the goat charges on legs, not wings — you cannot walk that far this turn";
|
if (d > legs) return "the goat charges on legs, not wings — you cannot walk that far this turn";
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
@@ -2369,7 +2374,8 @@ const CARD_EFFECTS: Record<string, AttackEffect | NeutralEffect | CounterEffect>
|
|||||||
if (ctx.fullyStopped || !ctx.defender.alive || !ctx.attacker.alive) return;
|
if (ctx.fullyStopped || !ctx.defender.alive || !ctx.attacker.alive) return;
|
||||||
if ((ctx.state.config.deckRev ?? 1) >= 10) {
|
if ((ctx.state.config.deckRev ?? 1) >= 10) {
|
||||||
const legs = ctx.state.turn.movementAllowance - ctx.state.turn.movementUsed;
|
const legs = ctx.state.turn.movementAllowance - ctx.state.turn.movementUsed;
|
||||||
const d = walkingDistance(ctx.state, ctx.attacker.position, ctx.defender.position);
|
const reach = (ctx.state.config.deckRev ?? 1) >= 16 ? legs : 6;
|
||||||
|
const d = walkingDistance(ctx.state, ctx.attacker.position, ctx.defender.position, reach);
|
||||||
if (d === 0) return;
|
if (d === 0) return;
|
||||||
if (d > legs) {
|
if (d > legs) {
|
||||||
// The target slipped beyond the charge (a counter-teleport, say):
|
// The target slipped beyond the charge (a counter-teleport, say):
|
||||||
@@ -3677,7 +3683,9 @@ function attachSustained(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** BFS steps between cells respecting walls (MENTAL FORCE's 3 moved spaces). */
|
/** BFS steps between cells respecting walls (MENTAL FORCE's 3 moved spaces). */
|
||||||
function walkingDistance(state: GameState, from: Cell, to: Cell): number {
|
/** Steps along real corridors from one cell to another, searched out to
|
||||||
|
* `limit` steps; beyond that the answer is Infinity. */
|
||||||
|
export function walkingDistance(state: GameState, from: Cell, to: Cell, limit = 6): number {
|
||||||
if (cellKey(from) === cellKey(to)) return 0;
|
if (cellKey(from) === cellKey(to)) return 0;
|
||||||
const view = boardView(state);
|
const view = boardView(state);
|
||||||
const seen = new Map<string, number>([[cellKey(from), 0]]);
|
const seen = new Map<string, number>([[cellKey(from), 0]]);
|
||||||
@@ -3685,7 +3693,7 @@ function walkingDistance(state: GameState, from: Cell, to: Cell): number {
|
|||||||
while (queue.length > 0) {
|
while (queue.length > 0) {
|
||||||
const cur = queue.shift()!;
|
const cur = queue.shift()!;
|
||||||
const d = seen.get(cellKey(cur))!;
|
const d = seen.get(cellKey(cur))!;
|
||||||
if (d >= 6) break;
|
if (d >= limit) break;
|
||||||
for (const side of SIDES) {
|
for (const side of SIDES) {
|
||||||
const step = stepTarget(view, cur, side);
|
const step = stepTarget(view, cur, side);
|
||||||
if (step.kind === "blocked") continue;
|
if (step.kind === "blocked") continue;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { applyCommand, activePlayer, boardView, createGame, gameLos, sustainedOn } from "../src/game";
|
import { applyCommand, activePlayer, boardView, createGame, gameLos, sustainedOn, walkingDistance, type GameState } from "../src/game";
|
||||||
import { cellKey, edgeKey, stepTarget } from "../src/board";
|
import { cellKey, edgeKey, stepTarget, type Cell } from "../src/board";
|
||||||
import type { CardInstance } from "../src/cards";
|
import type { CardInstance } from "../src/cards";
|
||||||
import { newExpansionGame as newGame, must, drain, giveCard, toRound2, faceOff, castAt } from "./helpers";
|
import { newExpansionGame as newGame, must, drain, giveCard, toRound2, faceOff, castAt } from "./helpers";
|
||||||
|
|
||||||
@@ -812,3 +812,34 @@ describe("the goat charges on legs, not wings (rev 10)", () => {
|
|||||||
expect(rams).toBeGreaterThan(0);
|
expect(rams).toBeGreaterThan(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("butt-head's charge reaches as far as its legs (K4T3)", () => {
|
||||||
|
/** Two empty cells seven or eight corridor steps apart on the seed-42 board. */
|
||||||
|
function longRun(state: GameState): { from: Cell; to: Cell; steps: number } {
|
||||||
|
const view = boardView(state);
|
||||||
|
const cells = Object.keys(view.cells).map((k) => { const [x, y] = k.split(",").map(Number) as [number, number]; return { x, y }; })
|
||||||
|
.filter((c) => !state.squareContents[cellKey(c)] && !view.homes.some((h) => cellKey(h) === cellKey(c)));
|
||||||
|
for (const from of cells) for (const to of cells) {
|
||||||
|
const d = walkingDistance(state, from, to, 12);
|
||||||
|
if (d === 7 || d === 8) return { from, to, steps: d };
|
||||||
|
}
|
||||||
|
throw new Error("no seven-step corridor on this board");
|
||||||
|
}
|
||||||
|
for (const [deckRev, ok] of [[16, true], [15, false]] as const) {
|
||||||
|
it(`a goat with eight legs and a target seven or eight corridor steps off: rev ${deckRev} ${ok ? "charges" : "refuses"}`, () => {
|
||||||
|
let { state } = createGame({ playerIds: ["goat", "mark"], seed: 42, sets: ["basic", "expansion1"], deckRev });
|
||||||
|
state = toRound2(state);
|
||||||
|
const goat = activePlayer(state);
|
||||||
|
const mark = state.players.find((p) => p.id !== goat.id)!;
|
||||||
|
const run = longRun(state);
|
||||||
|
goat.position = { ...run.from };
|
||||||
|
mark.position = { ...run.to };
|
||||||
|
state.turn.movementAllowance = 8;
|
||||||
|
state.turn.movementUsed = 0;
|
||||||
|
const card = giveCard(state, goat.id, "butt-head");
|
||||||
|
const r = applyCommand(state, goat.id, { type: "cast", instanceId: card.instanceId, target: { kind: "player", playerId: mark.id } });
|
||||||
|
expect(r.ok).toBe(ok);
|
||||||
|
if (!r.ok) expect(r.error).toMatch(/legs, not wings/);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user