The archmage cuts off a winning delivery and aims at the leader when no kill is on the table
Reuben's three wins today were walked home unseen: the clockwork fought whoever stood in front of it. Now a delivery that would end the game pulls every automaton toward that wizard's home (where the banked gold is lifted off the doorstep) or the road to it, and among visible targets the thief still dies first, a kill is still taken, and only then does the aim shift to whoever has more treasures banked. Measured with deploy/brain-trial.mjs, which seats the working tree's brain against an older checkout's at one table. 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
1b18dffd28
commit
be96dd6047
@@ -730,6 +730,52 @@ function deliveryWins(view: GameView, p: { id: PlayerId; home: Cell; carriedTrea
|
||||
(t) => t.owner !== p.id && t.position && cellKey(t.position) === cellKey(p.home));
|
||||
}
|
||||
|
||||
/** Treasures a wizard has delivered: the scoreboard the table reads. */
|
||||
function bankedBy(view: GameView, p: { id: PlayerId; home: Cell }): number {
|
||||
return view.treasures.filter(
|
||||
(t) => t.owner !== p.id && t.position && !t.carriedBy && cellKey(t.position) === cellKey(p.home)).length;
|
||||
}
|
||||
|
||||
/** The enemy whose delivery in progress would end the game, if any. */
|
||||
function winningCarrier(view: GameView) {
|
||||
return livingEnemies(view).find((p) => deliveryWins(view, p)) ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Where to cut off a carrier. Their home square first, when the clockwork
|
||||
* can get there no later than they can: it is the one square they must
|
||||
* reach, and the gold already banked there is free for the taking, which
|
||||
* unmakes the win outright. Otherwise the nearest square on their
|
||||
* shortest road home that the clockwork reaches before they do; failing
|
||||
* that, the home square anyway. Ties break toward their home, so the goal
|
||||
* holds still as the clockwork walks. The carrier's road is measured as
|
||||
* if every door opened for them.
|
||||
*/
|
||||
export function interceptCell(
|
||||
view: GameView, carrier: { position: Cell; home: Cell }, from: Cell, canUnlock: boolean,
|
||||
): Cell {
|
||||
const fromHome = distancesFrom(view, [carrier.home], true);
|
||||
const fromCarrier = distancesFrom(view, [carrier.position], true);
|
||||
const fromMe = distancesFrom(view, [from], canUnlock);
|
||||
const road = fromCarrier.get(cellKey(carrier.home));
|
||||
if (road == null) return carrier.home;
|
||||
const mineToHome = fromMe.get(cellKey(carrier.home));
|
||||
if (mineToHome != null && mineToHome <= road) return carrier.home;
|
||||
let best: { cell: Cell; mine: number; toHome: number } | null = null;
|
||||
for (const k of Object.keys(view.board.cells)) {
|
||||
const theirs = fromCarrier.get(k);
|
||||
const toHome = fromHome.get(k);
|
||||
const mine = fromMe.get(k);
|
||||
if (theirs == null || toHome == null || mine == null) continue;
|
||||
if (theirs + toHome !== road || mine > theirs) continue;
|
||||
if (!best || mine < best.mine || (mine === best.mine && toHome < best.toHome)) {
|
||||
const [x, y] = k.split(",").map(Number);
|
||||
best = { cell: { x: x!, y: y! }, mine, toHome };
|
||||
}
|
||||
}
|
||||
return best?.cell ?? carrier.home;
|
||||
}
|
||||
|
||||
/** The wizard making off with MY gold, if any. */
|
||||
function thiefOfMine(view: GameView) {
|
||||
const carriers = new Set(
|
||||
@@ -1455,9 +1501,22 @@ export function automatonCommand(
|
||||
.filter((p) => sighted.has(cellKey(p.position)) &&
|
||||
!shieldedFromMe(p.id) && !myPact(p.id) && !bushOrMist(view, p.id));
|
||||
if (visible.length > 0) {
|
||||
const target = thief && visible.some((p) => p.id === thief.id)
|
||||
? thief
|
||||
: visible.sort((a, b) => a.life - b.life)[0]!;
|
||||
// The thief of my gold dies first, and a kill on the table is
|
||||
// taken. Otherwise the aim shifts to whoever is nearest to winning:
|
||||
// a delivery in progress that would end the game, then a wizard
|
||||
// with more treasures banked than I have. The clockwork still
|
||||
// plays its own game — it hunts the leader only when the weakest
|
||||
// would not have died anyway.
|
||||
const weakest = [...visible].sort((a, b) => a.life - b.life)[0]!;
|
||||
const finisher = bestAttack(view, weakest.id, tier);
|
||||
const runner = visible.find((p) => deliveryWins(view, p));
|
||||
const myBank = bankedBy(view, self);
|
||||
const leader = [...visible]
|
||||
.filter((p) => bankedBy(view, p) > myBank)
|
||||
.sort((a, b) => bankedBy(view, b) - bankedBy(view, a) || a.life - b.life)[0];
|
||||
const target = thief && visible.some((p) => p.id === thief.id) ? thief
|
||||
: finisher && finisher.damage >= weakest.life ? weakest
|
||||
: runner ?? leader ?? weakest;
|
||||
// DROP OBJECT shakes the treasure out of the hands that matter:
|
||||
// the thief of my gold, or any carrier whose delivery wins.
|
||||
{
|
||||
@@ -1698,15 +1757,25 @@ export function automatonCommand(
|
||||
livingEnemies(view).some((p) =>
|
||||
!p.carriedTreasureId &&
|
||||
Math.abs(p.position.x - self.home.x) + Math.abs(p.position.y - self.home.y) <= 2);
|
||||
// INTERCEPTION: a delivery that would end the game is every
|
||||
// clockwork's business, whatever its temperament — the road to that
|
||||
// wizard's home is the only march that matters, and the post, once
|
||||
// reached, is held (the strike above fires the moment they walk into
|
||||
// sight). My own delivery and my own stolen gold still come first.
|
||||
const runner = !self.carriedTreasureId && !chasing && !cursedIdiot ? winningCarrier(view) : null;
|
||||
const cutoff = runner ? interceptCell(view, runner, self.position, canUnlock) : null;
|
||||
if (cutoff && cellKey(cutoff) === here) return endTurnDrawing(view, tier, style);
|
||||
const objectives = cursedIdiot && ownGoldCells.size > 0
|
||||
? ownGoldCells
|
||||
: chasing
|
||||
? new Set([cellKey(thief.position)])
|
||||
: bankThreatened
|
||||
? new Set([cellKey(self.home)])
|
||||
: style === "berserker" && !self.carriedTreasureId
|
||||
? (enemyCells.size > 0 ? enemyCells : gold)
|
||||
: gold;
|
||||
: cutoff
|
||||
? new Set([cellKey(cutoff)])
|
||||
: bankThreatened
|
||||
? new Set([cellKey(self.home)])
|
||||
: style === "berserker" && !self.carriedTreasureId
|
||||
? (enemyCells.size > 0 ? enemyCells : gold)
|
||||
: gold;
|
||||
const path =
|
||||
pathToward(view, self.position, objectives, { avoidNearEnemies: style === "worrier" && !chasing, canUnlock }) ??
|
||||
pathToward(view, self.position, objectives, { canUnlock }) ??
|
||||
|
||||
Reference in New Issue
Block a user