Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e30f3f0903 | ||
|
|
db0eeebcd5 |
@@ -1034,9 +1034,11 @@ function selfCare(view: GameView, style: AutomatonStyle, tier: TierTraits): Comm
|
||||
const myFloorTreasure = tier.guardGold
|
||||
? view.treasures.find((t) => t.owner === view.you && t.position && !t.carriedBy)
|
||||
: undefined;
|
||||
if (myFloorTreasure && enemyNear) {
|
||||
// Both spells demand line of sight to the gold; a blind cast would be
|
||||
// refused and cost the whole turn.
|
||||
if (myFloorTreasure && enemyNear && sightedCellsFor(view).has(cellKey(myFloorTreasure.position!))) {
|
||||
const safe = inHand(view, "safe");
|
||||
if (safe) {
|
||||
if (safe && !view.squareContents[cellKey(myFloorTreasure.position!)]) {
|
||||
return { type: "cast", instanceId: safe.instanceId, target: { kind: "cell", cell: myFloorTreasure.position! } };
|
||||
}
|
||||
const glue = inHand(view, "glue");
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
type PlayerId,
|
||||
} from "../src/game";
|
||||
import { cellKey, edgeKey } from "../src/board";
|
||||
import { viewFor } from "../src/view";
|
||||
import { sightedCellsFor, viewFor } from "../src/view";
|
||||
import { automatonCommand, automatonFallback, type AutomatonStyle, type AutomatonTier } from "../src/automaton";
|
||||
|
||||
/** Whose input does the maze want right now? */
|
||||
@@ -170,6 +170,59 @@ describe("the clockwork does not waste counters on pointless targets", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("the clockwork guards gold only within sight", () => {
|
||||
// Game X2XN: the bot blind-cast SAFE at a treasure it could not see, the
|
||||
// engine refused it, and the fallback burned the whole turn.
|
||||
function goldOnTheFloor() {
|
||||
let { state } = createGame({ playerIds: ["bot", "foe"], seed: 7, sets: ["basic", "expansion1"], deckRev: 36 });
|
||||
while (actingSeat(state) !== "bot") {
|
||||
const r = applyCommand(state, actingSeat(state), { type: "endTurn", draw: 0 });
|
||||
if (!r.ok) throw new Error(r.error);
|
||||
state = r.state;
|
||||
}
|
||||
const bot = state.players.find((p) => p.id === "bot")!;
|
||||
const foe = state.players.find((p) => p.id === "foe")!;
|
||||
foe.position = { ...bot.position };
|
||||
bot.hand = [{ instanceId: "safe#T", cardId: "safe" }];
|
||||
const gold = state.treasures.find((t) => t.owner === "bot")!;
|
||||
gold.carriedBy = null;
|
||||
return { state, bot, gold };
|
||||
}
|
||||
|
||||
it("never blind-casts SAFE at a treasure out of sight", () => {
|
||||
const { state, bot, gold } = goldOnTheFloor();
|
||||
const sighted = sightedCellsFor(viewFor(state, "bot"));
|
||||
const hidden = Object.keys(state.board.cells).find(
|
||||
(k) => !sighted.has(k) && !state.squareContents[k],
|
||||
)!;
|
||||
const [x, y] = hidden.split(",").map(Number);
|
||||
gold.position = { x: x!, y: y! };
|
||||
|
||||
const cmd = automatonCommand(viewFor(state, "bot"), "hunter", "archmage");
|
||||
// Whatever the brain picks, the engine must accept it — a refusal
|
||||
// forfeits the bot's turn.
|
||||
const chosen = cmd ?? automatonFallback(viewFor(state, "bot"), "archmage");
|
||||
expect(applyCommand(state, "bot", chosen).ok).toBe(true);
|
||||
});
|
||||
|
||||
it("still locks up gold it can see", () => {
|
||||
const { state, bot, gold } = goldOnTheFloor();
|
||||
const sighted = sightedCellsFor(viewFor(state, "bot"));
|
||||
const seen = [...sighted].find(
|
||||
(k) => k !== cellKey(bot.position) && !state.squareContents[k],
|
||||
)!;
|
||||
const [x, y] = seen.split(",").map(Number);
|
||||
gold.position = { x: x!, y: y! };
|
||||
|
||||
const cmd = automatonCommand(viewFor(state, "bot"), "hunter", "archmage");
|
||||
expect(cmd).toEqual({
|
||||
type: "cast", instanceId: "safe#T",
|
||||
target: { kind: "cell", cell: gold.position },
|
||||
});
|
||||
expect(applyCommand(state, "bot", cmd!).ok).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("a clogged hand gets shed, not hoarded", () => {
|
||||
it("the bot discards dead weight so the end-of-turn draw has room", () => {
|
||||
let { state } = createGame({ playerIds: ["bot", "other"], seed: 42, sets: ["basic"], deckRev: 13 });
|
||||
|
||||
@@ -52,10 +52,16 @@
|
||||
let fxCancels: (() => void)[] = [];
|
||||
/** A trap drawn by YOU earns a modal; buried log lines get missed. */
|
||||
let trapNotice = $state<string | null>(null);
|
||||
/** A Ward sprung on YOU earns the same courtesy. */
|
||||
let wardBite = $state<{ owner: string; amount: number } | null>(null);
|
||||
function playFx(events: Parameters<typeof scheduleFx>[0]) {
|
||||
if (!view) return;
|
||||
for (const e of events) {
|
||||
if (e.type === "trapSprung" && e.player === view.you && e.cardId) trapNotice = e.cardId;
|
||||
if (e.type === "wardSprung" && e.victim === view.you) {
|
||||
const dmg = events.find((d) => d.type === "damaged" && d.player === e.victim && d.source === "warded treasure");
|
||||
wardBite = { owner: e.owner, amount: dmg?.type === "damaged" ? dmg.amount : 0 };
|
||||
}
|
||||
}
|
||||
if (!prefs.flourishes) return;
|
||||
fxCancels.push(scheduleFx(
|
||||
@@ -1339,6 +1345,21 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if wardBite}
|
||||
<div class="scrim attack-scrim" role="alertdialog" aria-label="a warded treasure">
|
||||
<div class="attack-notice">
|
||||
<div class="attack-headline">The treasure bites back!</div>
|
||||
<div class="attack-card"><Card card={{ instanceId: "ward-notice", cardId: "ward" }} /></div>
|
||||
<div class="attack-power">
|
||||
{wardBite.amount > 0
|
||||
? `${wardBite.owner}'s treasure was warded — it bites you for ${wardBite.amount} point${wardBite.amount === 1 ? "" : "s"}.`
|
||||
: `${wardBite.owner}'s treasure was warded — but its bite finds no purchase.`}
|
||||
</div>
|
||||
<button class="stamp big" onclick={() => (wardBite = null)}>Grit your teeth</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if view && view.outOfTurnWindow?.playerId === view.you && !momentSeen}
|
||||
<div class="scrim attack-scrim" role="alertdialog" aria-label="your interruption">
|
||||
<div class="attack-notice">
|
||||
|
||||
Reference in New Issue
Block a user