From ef921a0c2ba04cf3f47b986c1a24433980d0fd56 Mon Sep 17 00:00:00 2001 From: Eric Wagoner Date: Sun, 23 Aug 2026 11:04:05 -0400 Subject: [PATCH] Three more cautions: the un-grabbed chest, the biting ground, the starved draw MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ending a turn on a treasure nobody picked up now asks first (unless the auto-grab preference is about to take it, or the grab would be refused — laden arms, weakness, spent actions, glue, a locked safe, or your own home where the chest already scores; the view learns gluedCells and openSafes so the check is honest). Stepping onto ground that bites — thornbush, tacks, pit, ooze — warns with the exact price, unless you are mist. And the sticky draw picker no longer quietly starves anyone: a draw below what would fit asks too. Co-Authored-By: Claude Fable 5 --- packages/engine/src/view.ts | 6 ++++++ packages/web/src/App.svelte | 38 ++++++++++++++++++++++++++++++++++++- 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/packages/engine/src/view.ts b/packages/engine/src/view.ts index 459a941..5203394 100644 --- a/packages/engine/src/view.ts +++ b/packages/engine/src/view.ts @@ -57,6 +57,10 @@ export interface GameView { /** Duration spells in play (public knowledge). */ sustained: SustainedEffect[]; squareContents: Record; + /** Cells whose contents are glued down (public: the cast was seen). */ + gluedCells: Record; + /** Safes standing open (their combination entered this turn). */ + openSafes: string[]; groundObjects: Record; doorStates: Record; /** Accumulated attack damage per edge (public — cracks show). */ @@ -144,6 +148,8 @@ export function viewFor(state: GameState, playerId: PlayerId): GameView { pendingDiscard: state.pendingDiscard, sustained: state.sustained.map((s) => ({ ...s })), squareContents: { ...state.squareContents }, + gluedCells: { ...state.gluedCells }, + openSafes: [...state.openSafes], groundObjects: Object.fromEntries( Object.entries(state.groundObjects).map(([k, v]) => [k, [...v]]), ), diff --git a/packages/web/src/App.svelte b/packages/web/src/App.svelte index afe60c0..12c929d 100644 --- a/packages/web/src/App.svelte +++ b/packages/web/src/App.svelte @@ -63,7 +63,8 @@ if (!prefs.cautions || reasons.length === 0) go(); else caution = { reasons, go }; } - /** A move with the delivery check: stepping off your home still laden. */ + /** A move with its regret checks: stepping off your home still laden, + * or stepping onto ground that bites. */ function tryMove(direction: Side, over?: boolean) { const reasons: string[] = []; if (view && me && me.carriedTreasureId && cellKey(me.position) === cellKey(me.home)) { @@ -72,6 +73,22 @@ ? `you stand on your home with ${t.owner}'s treasure in your arms — drop it first and it scores` : "you stand on your home with your own treasure in your arms — drop it first and it is safe"); } + if (view && me) { + const misted = view.sustained.some((e) => e.cardId === "mist-body" && e.targetId === view.you); + const steps = over ? 2 : 1; + const dest = { x: me.position.x + (direction === "E" ? steps : direction === "W" ? -steps : 0), + y: me.position.y + (direction === "S" ? steps : direction === "N" ? -steps : 0) }; + const ground = view.squareContents[cellKey(dest)]?.kind; + if (!misted && ground) { + const bite: Record = { + thornbush: "the thorns end your turn, cost you the NEXT one, and prick for 1", + tacks: "the tacks bite for 3", + pit: "the pit: roll a 1 and you fall in, hurt and stuck", + ooze: "the ooze burns 1 — and may drop you flat, treasure and all", + }; + if (bite[ground]) reasons.push(bite[ground]); + } + } withCaution(reasons, () => dispatch({ type: "move", direction, ...(over ? { over: true } : {}) })); } function playFx(events: Parameters[0]) { @@ -919,6 +936,25 @@ ? `${t.owner}'s treasure is still in your arms — drop it here and it scores` : "your own treasure is still in your arms — drop it here and it is safe"); } + // Treasure underfoot un-grabbed — unless the auto-grab preference (or + // this caution's own "do it anyway") is about to handle it, or the + // grab would be refused (laden, weakened, actions spent, glue, safe, + // or your own home where a stolen chest is already scoring). + const here = cellKey(me.position); + const autoWillGrab = prefs.autoGrab && treasuresHere.length === 1; + const weakened = view.sustained.some((e) => e.cardId === "weakness" && e.targetId === view.you); + const safed = view.squareContents[here]?.kind === "safe" && + view.squareContents[here]!.createdBy !== view.you && !view.openSafes.includes(here); + if (treasuresHere.length > 0 && !autoWillGrab && !me.carriedTreasureId && + !view.turn.actionsEnded && !weakened && !view.gluedCells[here] && !safed && + here !== cellKey(me.home)) { + reasons.push(`a treasure lies at your feet, un-grabbed`); + } + // The draw picker is sticky: a 0 or 1 chosen turns ago quietly starves. + const drawRoom = Math.min(2, room); + if (drawCount < drawRoom) { + reasons.push(`drawing only ${drawCount} when ${drawRoom} would fit`); + } } withCaution(reasons, () => { // Preference: a lone grabbable treasure underfoot is picked up on the