Ask the automatons: the council of three offers hints from your seat

The owner's idea for teaching novices: a hints button runs all three
temperaments' brains against YOUR view — the same cards and board you
see, nothing hidden — and each says what it would do in your robes.
The Hunter (eyes on the gold), the Berserker (eyes on the blood), the
Worrier (eyes on the exits). Available on your turn and while under
fire (counter advice included); purely advisory, computed client-side,
nothing dispatched. hints.ts translates any Command into plain words.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0138A8CjeQRpvzKxuMfz1Bqc
This commit is contained in:
Eric Wagoner
2026-08-18 02:19:32 -04:00
co-authored by Claude Fable 5
parent 5ac1c9ed70
commit b1f03fee81
2 changed files with 136 additions and 0 deletions
+36
View File
@@ -949,6 +949,14 @@
illusionPrompt = null;
}
// The council of three: what each temperament would do from your seat.
let councilHints = $state<import("./hints").TableHint[] | null>(null);
async function askCouncil() {
if (!view) return;
const { tableHints } = await import("./hints");
councilHints = tableHints(view);
}
let pickChoice = $state(false);
function pickUp() {
if (treasuresHere.length > 1) { pickChoice = true; return; }
@@ -1630,6 +1638,21 @@
<div class="peek-note">{creatureStats(commandedCreature)}</div>
</div>
{/if}
{#if councilHints}
<div class="slip council">
<div class="council-head">The council considers your position…
<button class="hint-cancel" onclick={() => (councilHints = null)}>enough</button>
</div>
{#each councilHints as h (h.name)}
<div class="council-row">
<span class="council-name">{h.glyph} {h.name}</span>
<span class="council-mood">({h.mood})</span>
{h.advice}
</div>
{/each}
<button class="stamp tiny" onclick={askCouncil}>ask again</button>
</div>
{/if}
{#if actionsSpent}
{#if view.turn.drawForbidden}
<div class="slip urgent">Your own bolt has left you reeling — the rest of this turn is lost, and no cards come with it.</div>
@@ -1766,6 +1789,7 @@
{/if}
</span>
<button class="stamp tiny" onclick={pass}>let it resolve</button>
<button class="stamp tiny" onclick={askCouncil}>🎓 hints</button>
{/if}
{#if youMustDiscard}
<span class="hint-alert">Hand over the limit — mark cards and discard.</span>
@@ -1939,6 +1963,7 @@
<button class="stamp" class:primary={punchWallMode} disabled={view.turn.attackUsed && !punchWallMode}
onclick={() => (punchWallMode = !punchWallMode)}>
{punchWallMode ? "Click the wall to punch — or cancel" : "Punch a wall…"}</button>
<button class="stamp" onclick={askCouncil}>🎓 Ask the automatons</button>
<button class="stamp" onclick={startDiscardMode}>Discard cards</button>
<label class="inline draw-pick">
draw
@@ -2354,6 +2379,17 @@
.stamp.big { font-size: 0.95rem; padding: 0.6rem 1.3rem; }
.stamp.tiny { font-size: 0.62rem; padding: 0.22rem 0.5rem; box-shadow: 1px 1px 0 rgba(0,0,0,0.3); }
.stamp.discard-one { background: #8a7a5e; }
.slip.council { background: #ece4f1; border-color: #7a6f9a; }
.council-head {
font-weight: 700;
display: flex;
justify-content: space-between;
align-items: baseline;
margin-bottom: 0.25rem;
}
.council-row { margin: 0.2rem 0; }
.council-name { font-weight: 700; }
.council-mood { color: #8a7a5e; font-size: 0.72rem; }
.stamp:focus-visible { outline: 2px solid #d8b23a; outline-offset: 2px; }
.game {
+100
View File
@@ -0,0 +1,100 @@
// The council of three: each automaton temperament reads YOUR view — the
// same cards and board you see, nothing more — and says what it would do
// in your robes. Purely advisory; nothing is dispatched.
import { automatonCommand, cardDef, type Command, type GameView } from "@wizwar/engine";
import type { AutomatonStyle } from "@wizwar/engine";
const DIRECTION: Record<string, string> = { N: "north", S: "south", E: "east", W: "west" };
const VOICES: Record<AutomatonStyle, { name: string; glyph: string; mood: string }> = {
hunter: { name: "The Hunter", glyph: "🏹", mood: "eyes on the gold" },
berserker: { name: "The Berserker", glyph: "🔥", mood: "eyes on the blood" },
worrier: { name: "The Worrier", glyph: "🛡", mood: "eyes on the exits" },
};
function nameOf(view: GameView, instanceId: string): string {
const c = view.yourHand.find((c) => c.instanceId === instanceId);
return c ? cardDef(c.cardId).name : "a card";
}
function describe(view: GameView, cmd: Command): string {
switch (cmd.type) {
case "move":
return `step ${DIRECTION[cmd.direction]}`;
case "playNumberForMovement":
return `play the ${nameOf(view, cmd.instanceId)} for extra movement`;
case "cast": {
const card = nameOf(view, cmd.instanceId);
const at =
cmd.target?.kind === "player" ? ` at ${cmd.target.playerId}`
: cmd.target?.kind === "cell" ? ` at the square (${cmd.target.cell.x}, ${cmd.target.cell.y})`
: cmd.target?.kind === "edge" ? ` on the wall line beside (${cmd.target.cell.x}, ${cmd.target.cell.y})`
: cmd.target?.kind === "creature" ? " on the monster"
: "";
const num = cmd.numberInstanceIds?.length
? ` with ${cmd.numberInstanceIds.map((id) => nameOf(view, id)).join(" and ")}`
: "";
return `cast ${card}${at}${num}`;
}
case "counteract":
return `answer with ${nameOf(view, cmd.instanceId)}`;
case "pass":
return "let it resolve — nothing in hand is worth spending on this";
case "punch":
return `punch ${cmd.targetId}`;
case "punchWall":
return "punch the wall";
case "pickUpTreasure":
return "pick up the treasure here (that ends the turn's actions)";
case "pickUpObject":
return "pick up the object here (that ends the turn's actions)";
case "dropTreasure":
return "drop the treasure here";
case "dropObject":
return `drop ${nameOf(view, cmd.instanceId)} here`;
case "warpStep":
return "step through the warp underfoot";
case "testIllusion":
return "test that shimmering wall — doubting it is free";
case "moveCreature":
return `march the monster ${DIRECTION[cmd.direction]}`;
case "creatureAttack":
return `set the monster on ${cmd.targetId}`;
case "setAmbush":
return `lay an ambush with ${nameOf(view, cmd.instanceId)}`;
case "armWard":
return cmd.armed ? "arm the Ward over the gold" : "stand the Ward down";
case "discard":
return `shed ${cmd.instanceIds.map((id) => nameOf(view, id)).join(", ")} to make room`;
case "endTurn":
return cmd.draw > 0 ? `end the turn and draw ${cmd.draw}` : "end the turn";
default:
return "bide";
}
}
export interface TableHint {
name: string;
glyph: string;
mood: string;
advice: string;
}
/** What each temperament would do from this seat, phrased for a novice. */
export function tableHints(view: GameView): TableHint[] {
const styles: AutomatonStyle[] = ["hunter", "berserker", "worrier"];
const out: TableHint[] = [];
for (const s of styles) {
const v = VOICES[s];
let advice: string;
try {
const cmd = automatonCommand(view, s, "archmage");
advice = cmd ? `I would ${describe(view, cmd)}.` : "Nothing calls for us right now — wait for your moment.";
} catch {
advice = "…I have no counsel here.";
}
out.push({ name: v.name, glyph: v.glyph, mood: v.mood, advice });
}
return out;
}