Signals

<game-shell> exposes all game state as individual Signal.State instances from the TC39 Signals proposal (via signal-polyfill). Each signal can be read with .get() and written with .set(). When a signal's value changes, any effect() that read it automatically re-runs.

Game components access signals via this.shell and observe them by implementing effectCallback. The shell is passed as the argument and you destructure only the signals you need:

effectCallback({ scene, round }) {
  // Re-runs whenever scene or round changes
  if (scene.get() === "playing") {
    this.#setupRound(round.get());
  }
}

effectCallback is automatically called when the component connects and re-called whenever any signal read inside it changes. It is automatically cleaned up when the component disconnects.

Built-in Signals

Signal Type Description
scene string Current game scene name
round number Current round number (1-indexed)
rounds number | null Total configured rounds
score number Accumulated score
roundScores number[] Per-round score array
roundScore number Score of the most recently completed round (computed)
bestRoundScore number Highest score across all completed rounds (computed)
worstRoundScore number Lowest score across all completed rounds (computed)
scoreOrder "asc" | "desc" Whether lower (asc) or higher (desc) scores are better
lastRoundPassed boolean | null Result of the most recent round
lastFeedback string | null Feedback string from the most recent round event
passStreak number Current consecutive pass streak
failStreak number Current consecutive fail streak
peakPassStreak number Highest pass streak reached this game
peakFailStreak number Highest fail streak reached this game
difficulty object | null Difficulty object from the active progression director
stats object Arbitrary { key: value } stats map, updated via GameStatUpdateEvent
storageKey string localStorage persistence key
gameId string Game identifier
betweenDelay number Milliseconds between rounds
encodedResult string | null Compact encoded result for sharing or challenges
groupId string | null Group identifier, or null when not in group play
groupName string | null Group display name, or null
challenge object | null Opponent's decoded result for challenge mode
formatScoreSignal function | null Score formatting function, set via shell.formatScore
spriteSheet string URL of the SVG sprite sheet, mirrors the sprite-sheet attribute

Effect Scheduling

Effects are batched via microtasks. When a signal value changes, the watcher schedules a microtask to process all pending computed signals. This means multiple signal writes in the same synchronous block coalesce into a single effect run.

Writing Signals

The shell writes signals in response to game events. You can also write them directly in JS for advanced use cases:

const shell = document.querySelector("game-shell");

// Directly update a stat signal
shell.stats.set({ ...shell.stats.get(), combo: 5 });

// Or use GameStatUpdateEvent (preferred — keeps logic in the component):
this.dispatchEvent(new GameStatUpdateEvent("combo", 5));

Prefer dispatching events from game components over directly writing shell signals — it keeps mechanics decoupled from the shell.

Relationship to Context

Game signals are not distributed via the Context Protocol. They are properties directly on <game-shell>, accessed via the this.shell DOM traversal. The Context Protocol is used for data that flows from a custom parent to its descendants — for example, <game-word-source> distributes the current word via gameWordContext.

See Context Protocol for details.