Skip to content

feat: S5 โ€” Scry & Hex reactions - #9

Merged
anchildress1 merged 6 commits into
mainfrom
feat/s5-reactions
Jun 7, 2026
Merged

anchildress1 merged 6 commits into
mainfrom
feat/s5-reactions

Conversation

@anchildress1

Copy link
Copy Markdown
Owner

S5 โ€” Scry & Hex reactions

Builds the one-use reaction system (v1-implementation-checklist.md S5). The engine owns the state; a dedicated policy module owns the rules; a component carries the human prompt. Live trigger lights up in S6 when Skรถll Asks through the same interface โ€” same staging as the dormant Skรถll turn pill.

What lands

  • Engine (engine.ts) โ€” one Scry + one Hex per player per round, plus an interrupt window: a resolved Ask opens it (owned by the asker), a resolved Cast closes it, a reaction/decline closes it. Charges + window reset with the round.
  • Reactions policy (reactions.ts, new) โ€” resolveReaction: Pass lets the Ask stand; Scry/Hex need an open window owned by the rival and an unspent charge, then resolve as shareAnswer / killAnswer.
  • Action interface (actions.ts) โ€” React routes through resolveReaction, carrying the outcome.
  • UI (ReactionPrompt.svelte, new) โ€” "Skรถll asks. Answer it?" โ†’ Scry / Hex / Let it pass; spent reactions vanish (no "spent" copy).

Invariants held structurally (not by convention)

  • Cast sacredness โ€” a Cast leaves no window, so a winning cast can never be Hexed/stolen.
  • One reaction per window โ€” first reaction closes it; once Hex silences a question, nothing's left for Scry.
  • Trigger-on-Ask-only โ€” only an Ask opens a window.
  • Hex = no answer to anyone, turn still spent โ€” engine spends the turn at Ask time; the action layer withholds the answer.

Scope note

The live trigger is S6 (Skรถll has no Ask yet). The prompt is built + tested now, lit up when the Gemini opponent Asks. No dormant/uncovered page branches.

Tests & gates

  • 376 tests pass; e2e 13/13.
  • engine.ts 100% line/branch ยท reactions.ts 100% line/branch (floor 95/90).
  • Closes the Reactions 95/90, Cast sacredness, and reactions-suite-lands gate items.

๐Ÿค– Generated with Claude Code

anchildress1 and others added 4 commits June 7, 2026 13:34
Each player holds one Scry and one Hex per round, spent permanently. A
resolved Ask opens a reaction window owned by the asker; a resolved Cast
closes it, so a Cast is structurally never interruptible (the win check
stays sacred); consuming or declining a reaction closes it, so at most
one reaction lands per window. Charges and the window reset with the
round. Exposes reactionAvailable, reactionWindow, consumeReaction, and
declineReaction as the primitives the reaction policy resolves over.

Generated-by: Claude Opus 4.8

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Ashley Childress <6563688+anchildress1@users.noreply.github.com>
reactions.ts owns the reaction policy over the engine's window/charge
primitives: Pass lets the Ask stand and spends nothing; Scry/Hex need an
open window owned by the rival and an unspent charge, then spend it and
resolve as shareAnswer (Scry overhears the answer) or killAnswer (Hex
silences the question). Hex's no-answer-to-anyone is the action layer
withholding the asker's answer โ€” the engine still spent the turn at Ask
time. Route React through resolveReaction; add the 95/90 coverage floor.

Generated-by: Claude Opus 4.8

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Ashley Childress <6563688+anchildress1@users.noreply.github.com>
ReactionPrompt renders the ux-copy ยง3 interrupt โ€” "Skรถll asks. Answer
it?" with Scry / Hex / Let it pass. Only held reactions are offered, so a
spent one simply vanishes (no "spent" copy). Built and tested now; the
live trigger lights up in S6 when Skรถll Asks through the same interface,
the same way the Skรถll turn pill is staged.

Generated-by: Claude Opus 4.8

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Ashley Childress <6563688+anchildress1@users.noreply.github.com>
Check off S5 in the implementation checklist with the engine-state /
reactions-policy split and the S6 live-trigger note, and the section-5
reaction test rows in the test checklist. Skรถll's live Ask/reaction stays
open for S6.

Generated-by: Claude Opus 4.8

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Ashley Childress <6563688+anchildress1@users.noreply.github.com>
Copilot AI review requested due to automatic review settings June 7, 2026 17:42

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Implements the S5 โ€œone-use reactionsโ€ mechanic (Scry/Hex) by adding per-round reaction charges and an Ask-triggered interrupt window to the engine, a dedicated reaction policy module for resolving choices, and a new UI prompt component for human-side reactions.

Changes:

  • Add reaction state to the engine (per-player Scry/Hex charges + interrupt window) and wire a new React action result that carries a reaction outcome.
  • Introduce resolveReaction policy (reactions.ts) and add ReactionPrompt UI + component tests.
  • Update coverage thresholds and checklists/tests to reflect S5 completion.

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
vite.config.ts Adds coverage gate for the new reactions module.
src/lib/server/engine/engine.ts Adds reaction charges + reaction window state and APIs to consume/decline reactions.
src/lib/server/engine/reactions.ts New policy module to validate/resolve Scry/Hex/Pass against engine state.
src/lib/server/engine/actions.ts Routes React through resolveReaction and returns structured outcome.
src/lib/components/ReactionPrompt.svelte New UI prompt offering Scry/Hex/Pass based on held charges.
tests/lib/server/engine/engine.test.ts Adds unit coverage for reaction charges/window behavior and round reset.
tests/lib/server/engine/reactions.test.ts New unit tests for resolveReaction policy outcomes and constraints.
tests/lib/server/engine/actions.test.ts Updates integration tests for React routing/outcomes.
tests/routes/api/action/server.test.ts Updates API response expectations for React to include outcome.
tests/lib/components/ReactionPrompt.svelte.test.ts New component tests for prompt copy/choices and spent-reaction hiding.
docs/v1-implementation-checklist.md Marks S5 items complete and documents the implementation approach.
docs/test-checklist.md Marks S5 reaction test gates complete and updates related gating notes.

๐Ÿ’ก Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/lib/server/engine/reactions.ts Resolved
anchildress1 and others added 2 commits June 7, 2026 13:49
Pass unconditionally closed the window, so the asker could send
{reaction:'Pass'} to slam their own window shut and deny the rival a
reaction โ€” violating "only the rival may respond." Gate it like Scry/Hex:
Pass closes the window only when it is open and owned by the other player;
otherwise it is a harmless no-op. Caught in review (PR #9).

Generated-by: Claude Opus 4.8

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Ashley Childress <6563688+anchildress1@users.noreply.github.com>
The reaction window was a side effect of a *resolved* Ask โ€” it only
existed after engine.ask had already computed and returned the answer, so
a Hex's killAnswer claimed to kill something already handed back. Reframe
the window as a *pending* Ask: openReactionWindow marks it before the Ask
is answered, decoupled from ask itself. Now a Hex resolves first and the
orchestration simply never asks for the answer (S6 wires Skรถll's Ask:
open window -> react -> ask-and-deliver on Pass/Scry, or spend his turn
unanswered on Hex). ask no longer touches the window; a resolved Cast
still closes it (sacredness). Caught in review (PR #9, P1).

Generated-by: Claude Opus 4.8

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Ashley Childress <6563688+anchildress1@users.noreply.github.com>
@sonarqubecloud

sonarqubecloud Bot commented Jun 7, 2026

Copy link
Copy Markdown

@anchildress1
anchildress1 merged commit 63b6e6d into main Jun 7, 2026
5 checks passed
@anchildress1
anchildress1 deleted the feat/s5-reactions branch June 7, 2026 18:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants