Scenario — the game loop [Flow]
Source path: knowledge-base/diagrams/flows/scenario-loop.md
# Scenario — the game loop `[Flow]`
A game is a **branch**; a `GameRoom` runs the turn loop as a phase machine. The player-facing loop is **Brief → Prepare → Commit → Consequence**. Generation alternates with deliberate player action: the engine briefs the seat, the seat prepares its reading, the seat commits, then the world plays the consequence forward. Structural phase identifiers remain `read → write → play → resolution → graph → arc → scoring → game-theory` for persisted state and protocol compatibility. Vocabulary: [LANGUAGE.md](../../knowledge/vocabulary/index.md). The screen layout is in [ui/scenario-ui](../ui/scenario-ui.md).
## The turn state machine
```mermaid
stateDiagram-v2
[*] --> Setup
Setup --> Turn: computer mode · Director proxies all seats
state Turn {
[*] --> PerspectiveGen
PerspectiveGen --> Read: Brief — public + private views (off-clock)
Read --> Write: Prepare — open Streams + add Priors
Write --> StreamGen: preparation closes (write clock)
StreamGen --> Play: GEN — candidate Streams seeded · cards priced + dealt
Play --> Showdown: Commit — back moves with conviction (play clock)
Showdown --> ArcGen: reveal + realism verdicts · Director vetoes / dictates
ArcGen --> FateScoring: GEN — Merge + continuation · settle
FateScoring --> GameTheoryReview: Impact lands
GameTheoryReview --> PerspectiveGen: Strategy read · next round
}
Turn --> [*]: Director ends → Report
```
- **Generation alternates with player phases.** *Perspective Gen* writes the public account + each seat's private view off the canon (players never see canon; the Director sees all). *Stream & Intuition Gen* seeds each seat's candidate streams, then **prices and deals** the hand. *Arc Gen* folds the Merge into the continuation, then **settles** (decay→income) and **scores**.
- **Streams are perspective-owned** — one per seat, no shared belief pool. Prepare poses questions + adds Priors; a Prior that moves a Thread your way **cheapens its cards** (`−ln p` falls) and earns conviction.
- **The Merge is the only place separate seats' streams meet.** Each contested thread settles per `RESOLVE_BIAS` — `random` (seeded draw over conviction-shaped odds) · `highest-cost` (rarest action forced through) · `realism` (impartial AI judge). A **universal realism pass** then interprets reality *around* the chosen winner, producing a Director-editable **telling · reasoning · closure** that rides onto the merge and is injected into the continuation prompt.
- **Attribution is intrinsic:** each Arc Gen decomposes the realized stance shift per Thread — **Aumann–Shapley on Fate/KL, conserving exactly** — into running **Impact** plus neutral **External**. Goals are optional personal trackers that never affect attribution.
## The engine data-flow
`useScenario.advance()` is the one-click progression: per phase it runs the right pure-engine side-effects + AI calls, then commits to the store.
```mermaid
flowchart LR
Adv["advance() — one click per phase"]
Adv -->|read| AP["generateArcPerspective"]
Adv -->|write| GS["game-streams — seed + deal"]
Adv -->|play| AG["game-agent — LLM turn"] --> ECO["economy — cost / settle"]
Adv -->|play→showdown| DC["detectConflicts"] --> SET["settlement — draw / rule pick"]
SET -->|winner per group| CR["game-realism — impartial judge"]
CR -->|telling · reasoning · closes| Merge["MergeResolution (auditable)"]
Adv -->|resolve / Arc Gen| Merge --> GEN["generateScenes → BULK_ADD_SCENES"]
GEN --> SCO["scoring — Aumann–Shapley"]
AP & GS & SCO & ECO --> Store(["store — UPSERT_GAME_ROOM ·<br/>CREATE_MERGE · COMMIT/CLOSE_STREAM"])
```
- **Showdown is the Realism review.** The impartial judge (`ai/game-realism`) explains why one action prevails and what happens next directly beside the revealed cards. The Director may set another winner or open **Refine consequence** to steer and re-run the interpretation without leaving the table.
- **Fast commitment, deliberate consequence.** Card choice, seat-Stream seeding, perspective delivery, and Impact attribution use no thinking budget. Reasoning is reserved for durable judgement and authored output: conflict/Realism settlement, the causal graph + continuation Arc, and the Strategy read.
- **Pure engine** (`lib/client/game/`): `engine` (phase machine + deal) · `economy` (pricing/settle) · `scoring` (Impact) · `settlement` (contested draws) · `agent` (deterministic fallback) · `attribution`. **AI** (`lib/engine/ai/`): `game-narration` · `game-streams` · `game-agent` · `game-conflicts` · `game-realism` · `game-cast`.
> **Shipped boundary:** joined `/scenario` clients and Showdown use the same canonical `GameRoom` as the Director studio. The blind `director` and `lowest-cost` biases were removed; Realism plus Director editing is the supported consequence path.
## Driving a game headless (MCP)
The same table is fully drivable over MCP — a Director (or an AI seat) runs the loop with no studio open. The headless surface splits the one-click studio `advance()` into ops that mark **where the AI must run**, because a headless caller cannot silently do a generative step wrong. The full loop and its parts (all `Scenario`-group ops, in [ops catalog](../../../src/lib/server/ops/catalog.ts)):
```mermaid
flowchart TD
create["**create-game**<br/>stand up the table · mints a guest-pass<br/>token per non-director seat (`passes`)"]
create -->|affordance| rr1["read-scenario<br/>see the table + each seat's pass"]
create --> play
play["**game-play-round** — the player half, on-rails<br/>deliver perspectives · seed + deal ·<br/>auto-drive every AGENT seat"]
play -->|"`stoppedFor: seatId`<br/>(a human/guest is up)"| cmd["**game-command** — one sync move<br/>play · endTurn/lockIn · chat · move ·<br/>streamEdit · table controls"]
cmd -.->|"resume the rails"| play
play -->|"`stoppedFor: null`<br/>`readyToResolve: true`"| resolve["**game-resolve** — the world half<br/>settle contests · merge-CRG · continuation<br/>arc · score Impact · open next round"]
resolve -->|affordance| arc["read-arc<br/>the continuation the resolve wrote to canon"]
resolve -->|"next round open (`read`)"| play
play -.->|"lower-level alternative"| adv["game-advance — ONE phase transition<br/>(read→write deal → write→play)"]
adv -.->|"generative continuation<br/>is studio-only"| studio(["`needsStudio` — no-op<br/>RESOLVE→GRAPH→ARC not run headless"])
```
- **Two ways to walk a round.** `game-play-round` is the **on-rails** driver — one call runs a round's whole player half (perspectives → deal → every agent seat's play) and **stops at the first human seat** (`stoppedFor`); an all-agent table plays its player-half in a single call. `game-advance` is the **lower-level** one-phase-at-a-time transition that mirrors the studio's phase machine. Both are generative on the READ/WRITE deal; both refuse the continuation.
- **`game-command` is the only non-generative driver** — one synchronous `GameCommand` through the pure `gameReducer` (play a card, `endTurn`/`lockIn`, `chat`, `move`, `streamEdit`, `setContested`/`veto`/`addSeats`/`endGame`/…). `applied:false` means it was a no-op (wrong phase, not the seat's turn, insufficient conviction). It is how a GM makes the human move at a `stoppedFor` stop, then hands back to `game-play-round`.
- **`game-resolve` is the world half, headless** — it runs the SAME full pipeline the studio's Arc Gen does (settle → merge-CRG → continuation arc → Impact scoring → economy) and commits the SAME artifact set (`CREATE_REASONING_GRAPH` · `BULK_ADD_SCENES` · `CREATE_MERGE` · `COMMIT`/`CLOSE_STREAM` · `SET_ARC_SCORE_FEEDBACK`), then opens the next round. `noContinuation:true` settles + scores without the graph/arc.
- **Ending early closes the unfinished questions.** `endGame` closes every still-open Stream owned by a participating seat on that branch, including a card already played but not yet folded into a Merge. It preserves the Stream in the record as closed rather than deleting the unfinished Scenario evidence.
- **The `needsStudio` boundary is deliberate.** Only `game-play-round`/`game-resolve` run the generative continuation headless; a bare `game-advance` from `play`/`resolution`/… returns `needsStudio` rather than doing it wrong. Prefer `game-play-round → game-resolve` for a fully headless game.
- **Headless commits echo live.** Every op commits `UPSERT_GAME_ROOM` on the record stream, so an open studio and any joined `/scenario` client (via a minted guest-pass token) update in real time — the same single-writer path as the studio.
> The studio and MCP are the same room, one writer: `game-play-round` ≈ `advance()`'s READ+WRITE+PLAY player half, `game-resolve` ≈ its Arc Gen world half. See the MCP practice in [mcp-chains](mcp-chains.md).
Scenario closes the shipped **play loop**; it does not yet close the cross-session training loop. The
product-level [Gameplay loop](gameplay-loop.md) preserves this state machine as the inner loop and maps
the missing **Consequence → Debrief → Adapt → next Brief** bridge.
Open on GitHubRaw Markdown source