The clockworks study three lost duels
Six instincts learned playing Kestrel live, taught to the automatons. Bank guard: enemy gold banked at home is a scoreboard anyone can rob — an empty-handed raider within two of a stocked bank pulls the walker home (the predicate reads only enemy positions, the thief-chase shuttle lesson), and pathDenial blockades the raider's road. Repossession: the bot's own treasure banked by an enemy one delivery from winning becomes a march goal, and the underfoot grab learns the owner exception. Infrastructure: holding DIMENSIONAL WARP near home in the early rounds, the bot anchors one mouth at its doorstep and one beside the richest distant gold — the home-anchored superhighway that won its teacher two games. Turn theft: a carrier one delivery from winning eats LIGHTNING BLAST's stun over any bigger point total. The teleport delivery: carrying with home a blink away but farther by boot, it jumps the ambush and drops next command. Last-counter thrift: a lone remaining counteraction at healthy life waits for something big. A/B vs the prior brain across 100 mirrors: parity (44-52, historical noise band) — the patterns are anti-human, and mirrors don't play like humans. Three scenario tests pin the rungs; 287 tests; 23 ledgers verified. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015RCWSTnb1KYTPyL4GmhGnF
This commit is contained in:
co-authored by
Claude Fable 5
parent
9af7bd80ce
commit
6d88b35516
@@ -9,6 +9,7 @@
|
||||
import { cardDef, type CardInstance } from "./cards";
|
||||
import { cellKey, edgeKey, neighbor, opposite, stepTarget, SIDES, type Cell, type Side } from "./board";
|
||||
import { bentSightFor, sightedCellsFor, type GameView } from "./view";
|
||||
import { wallIgnoringDistance } from "./game";
|
||||
import type { AmbushTrigger, Command, PlayerId } from "./game";
|
||||
|
||||
export type AutomatonStyle = "hunter" | "berserker" | "worrier";
|
||||
@@ -293,6 +294,17 @@ function pathDenial(view: GameView): Command | null {
|
||||
if (d <= 6) threats.push({ enemy: e.position, goal: floorGold.position! });
|
||||
}
|
||||
}
|
||||
// Enemy gold banked at MY home: my score, snatchable by anyone. A
|
||||
// raider closing on the bank gets the same road-lengthening treatment.
|
||||
const me2 = me(view);
|
||||
const banked = view.treasures.find(
|
||||
(t) => t.owner !== view.you && t.position && cellKey(t.position) === cellKey(me2.home));
|
||||
if (banked) {
|
||||
for (const e of livingEnemies(view)) {
|
||||
const d = Math.abs(e.position.x - me2.home.x) + Math.abs(e.position.y - me2.home.y);
|
||||
if (d <= 6) threats.push({ enemy: e.position, goal: me2.home });
|
||||
}
|
||||
}
|
||||
if (threats.length === 0) return null;
|
||||
|
||||
const sighted = sightedCellsFor(view);
|
||||
@@ -630,7 +642,16 @@ function treasureGoals(view: GameView): Set<string> {
|
||||
}
|
||||
for (const t of view.treasures) {
|
||||
if (!t.position || t.carriedBy) continue;
|
||||
if (t.owner === view.you) continue;
|
||||
if (t.owner === view.you) {
|
||||
// REPOSSESSION: my own gold banked at an enemy's home is a point on
|
||||
// THEIR scoreboard. Marching to take it back is worth the detour
|
||||
// only when that enemy is one delivery from winning — constant
|
||||
// re-stealing farms nothing and stalls the whole table.
|
||||
const banker = view.players.find(
|
||||
(p) => p.id !== view.you && cellKey(p.home) === cellKey(t.position!));
|
||||
if (banker && deliveryWins(view, banker)) goals.add(cellKey(t.position));
|
||||
continue;
|
||||
}
|
||||
if (cellKey(t.position) === cellKey(self.home)) continue;
|
||||
goals.add(cellKey(t.position));
|
||||
}
|
||||
@@ -731,7 +752,16 @@ function respond(view: GameView, style: AutomatonStyle, tier: TierTraits): Comma
|
||||
return { type: "pass" };
|
||||
}
|
||||
|
||||
const flinch = (style === "worrier" ? 1 : 0) - tier.counterThrift;
|
||||
// The LAST counter in hand is a treasure of its own: at a healthy life
|
||||
// total it waits for something big rather than answering every jab —
|
||||
// patience the clockwork's best opponent taught it, three counters at
|
||||
// a time. (Lethal blows override this below.)
|
||||
const counterCount = view.yourHand.filter((c) => {
|
||||
const t = cardDef(c.cardId).cardType;
|
||||
return t === "counteraction" || t === "neutral/counteraction";
|
||||
}).length;
|
||||
const lastCounterHold = counterCount <= 1 && me(view).life >= 10 ? 2 : 0;
|
||||
const flinch = (style === "worrier" ? 1 : 0) - tier.counterThrift - lastCounterHold;
|
||||
const attackId = stack.attackCard?.cardId ?? null;
|
||||
const atk = attackId ? ATTACKS[attackId] : null;
|
||||
const affliction = attackId ? AFFLICTIONS[attackId] != null : false;
|
||||
@@ -830,6 +860,24 @@ function respond(view: GameView, style: AutomatonStyle, tier: TierTraits): Comma
|
||||
/** The best attack available against a visible target, numbers and amplify included. */
|
||||
function bestAttack(view: GameView, targetId: PlayerId, tier: TierTraits): { cmd: Command; damage: number } | null {
|
||||
const numbers = numbersInHand(view);
|
||||
// TURN THEFT: against a carrier one delivery from winning, a
|
||||
// LIGHTNING BLAST's stolen turn outranks any point total — the number
|
||||
// rides along so soaks cannot zero the damage and void the stun.
|
||||
{
|
||||
const carrier = view.players.find((p) => p.id === targetId);
|
||||
const bolt = inHand(view, "lightning-blast");
|
||||
const biggestNum = numbers[numbers.length - 1];
|
||||
if (carrier && bolt && biggestNum && deliveryWins(view, carrier)) {
|
||||
return {
|
||||
damage: cardDef(biggestNum.cardId).value ?? 1,
|
||||
cmd: {
|
||||
type: "cast", instanceId: bolt.instanceId,
|
||||
target: { kind: "player", playerId: targetId },
|
||||
numberInstanceIds: [biggestNum.instanceId],
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
const biggest = numbers[numbers.length - 1];
|
||||
const biggestValue = biggest ? (cardDef(biggest.cardId).value ?? 0) : 0;
|
||||
const target = view.players.find((p) => p.id === targetId)!;
|
||||
@@ -1078,6 +1126,43 @@ function selfCare(view: GameView, style: AutomatonStyle, tier: TierTraits): Comm
|
||||
}
|
||||
}
|
||||
}
|
||||
// INFRASTRUCTURE: a wormhole anchored beside home turns every future
|
||||
// delivery into a three-move stroll — the pattern that won its best
|
||||
// opponent two games. Placed early, from home's doorstep, far mouth
|
||||
// by the richest distant gold.
|
||||
{
|
||||
const dwarp = inHand(view, "dimensional-warp");
|
||||
const nearHome = Math.abs(self.position.x - self.home.x) + Math.abs(self.position.y - self.home.y) <= 1;
|
||||
if (dwarp && nearHome && view.dimWarps.length === 0 && view.turn.round <= 6) {
|
||||
const legal = (c: Cell) =>
|
||||
view.board.cells[cellKey(c)] !== undefined &&
|
||||
!view.board.homes.some((h) => h.x === c.x && h.y === c.y) &&
|
||||
!view.squareContents[cellKey(c)] &&
|
||||
!view.treasures.some((t) => t.position && cellKey(t.position) === cellKey(c)) &&
|
||||
(view.groundObjects[cellKey(c)] ?? []).length === 0;
|
||||
const goals = treasureGoals(view);
|
||||
if (legal(self.position) && goals.size > 0) {
|
||||
const dHome = distancesFrom(view, [self.home], false);
|
||||
const sighted = sightedCellsFor(view);
|
||||
let far: { cell: Cell; worth: number } | null = null;
|
||||
for (const g of goals) {
|
||||
const [gx, gy] = g.split(",").map(Number) as [number, number];
|
||||
for (const side of SIDES) {
|
||||
const c = neighbor({ x: gx, y: gy }, side);
|
||||
if (!legal(c) || !sighted.has(cellKey(c))) continue;
|
||||
const worth = dHome.get(cellKey(c)) ?? 0;
|
||||
if (worth >= 8 && (far === null || worth > far.worth)) far = { cell: c, worth };
|
||||
}
|
||||
}
|
||||
if (far) {
|
||||
return {
|
||||
type: "cast", instanceId: dwarp.instanceId,
|
||||
params: { cell: self.position }, target: { kind: "cell", cell: far.cell },
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Guard the gold on the floor: a SAFE locks it, GLUE sticks it down.
|
||||
const myFloorTreasure = tier.guardGold
|
||||
? view.treasures.find((t) => t.owner === view.you && t.position && !t.carriedBy)
|
||||
@@ -1170,10 +1255,13 @@ export function automatonCommand(
|
||||
if (self.carriedTreasureId && here === cellKey(self.home)) return { type: "dropTreasure" };
|
||||
if (!self.carriedTreasureId) {
|
||||
const prize = view.treasures.find(
|
||||
(t) => t.position && !t.carriedBy && t.owner !== you && cellKey(t.position) === here &&
|
||||
here !== cellKey(self.home),
|
||||
(t) => t.position && !t.carriedBy && cellKey(t.position) === here &&
|
||||
here !== cellKey(self.home) &&
|
||||
(t.owner !== you ||
|
||||
// Repossessing my own gold off an enemy's home square.
|
||||
view.players.some((p) => p.id !== you && cellKey(p.home) === here)),
|
||||
);
|
||||
if (prize) return { type: "pickUpTreasure" };
|
||||
if (prize) return { type: "pickUpTreasure", treasureId: prize.id };
|
||||
}
|
||||
|
||||
// Wounded clockwork teleports clear of visible hunters.
|
||||
@@ -1411,6 +1499,24 @@ export function automatonCommand(
|
||||
}
|
||||
}
|
||||
|
||||
// TELEPORT DELIVERY: carrying with home four wall-ignoring spaces away
|
||||
// but farther by boot — blink in and drop next command. The jump beats
|
||||
// any ambush camped on the walking road.
|
||||
if (self.carriedTreasureId && !view.turn.actionsEnded) {
|
||||
const tpHome = inHand(view, "teleport");
|
||||
if (tpHome) {
|
||||
const blink = wallIgnoringDistance(view.board, self.position, self.home);
|
||||
const movesLeft = view.turn.movementAllowance - view.turn.movementUsed;
|
||||
const dWalk = blink >= 1 && blink <= 4
|
||||
? distancesFrom(view, [self.position], UNLOCKS.some((id) => inHand(view, id)))
|
||||
.get(cellKey(self.home)) ?? Infinity
|
||||
: 0;
|
||||
if (blink <= 4 && blink >= 1 && dWalk > movesLeft) {
|
||||
return { type: "cast", instanceId: tpHome.instanceId, target: { kind: "cell", cell: { ...self.home } } };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// March. The thief-chase outranks everything; the berserker hunts wizards
|
||||
// over gold; the worrier keeps its distance; the hunter goes to the gold.
|
||||
if (view.turn.movementUsed < view.turn.movementAllowance) {
|
||||
@@ -1430,13 +1536,26 @@ export function automatonCommand(
|
||||
const strikeLive = !view.turn.attackUsed && !view.turn.attackForbidden &&
|
||||
!view.turn.actionsEnded && view.yourHand.some((c) => ATTACKS[c.cardId] != null);
|
||||
const chasing = thief !== null && strikeLive;
|
||||
// BANK GUARD: enemy gold delivered to my home is my scoreboard, and
|
||||
// anyone may snatch it off the floor. A raider nearer my bank than I
|
||||
// am, with the bank stocked, outranks the next grab — run home.
|
||||
const bankedCount = view.treasures.filter(
|
||||
(t) => t.owner !== you && t.position && cellKey(t.position) === cellKey(self.home)).length;
|
||||
// The predicate reads only ENEMY positions — my own steps must not
|
||||
// flip it mid-march or the walker shuttles (the thief-chase lesson).
|
||||
const bankThreatened = bankedCount > 0 && !self.carriedTreasureId &&
|
||||
livingEnemies(view).some((p) =>
|
||||
!p.carriedTreasureId &&
|
||||
Math.abs(p.position.x - self.home.x) + Math.abs(p.position.y - self.home.y) <= 2);
|
||||
const objectives = cursedIdiot && ownGoldCells.size > 0
|
||||
? ownGoldCells
|
||||
: chasing
|
||||
? new Set([cellKey(thief.position)])
|
||||
: style === "berserker" && !self.carriedTreasureId
|
||||
? (enemyCells.size > 0 ? enemyCells : gold)
|
||||
: gold;
|
||||
: 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 }) ??
|
||||
|
||||
@@ -3493,7 +3493,7 @@ function walkingDistance(state: GameState, from: Cell, to: Cell): number {
|
||||
}
|
||||
|
||||
/** BFS steps between cells ignoring walls (teleport distance). */
|
||||
function wallIgnoringDistance(board: AssembledBoard, from: Cell, to: Cell): number {
|
||||
export function wallIgnoringDistance(board: AssembledBoard, from: Cell, to: Cell): number {
|
||||
if (cellKey(from) === cellKey(to)) return 0;
|
||||
const seen = new Map<string, number>([[cellKey(from), 0]]);
|
||||
const queue: Cell[] = [from];
|
||||
|
||||
Reference in New Issue
Block a user