# bgm Interactions The free-interaction layer (layer 3 of the layering) — how a player interacts with a bgm game that has no rules yet: a sandbox. It builds on the state model: see [`state-model.md`](./state-model.md) for components-vs-setup, path→stack × facing, and the anchoring scope. > **Status:** Implemented (layer 3, sandbox). The operation set, the > command/dialog split, and the deck-pick-up dialog are built in > `@tts/tabletop` (`interactions.ts`, `dialog.tsx`). Rule-enforced play (layer > 4) is not yet wired — the rule seam is a no-op filter over free interaction. ## 1. The operation set is closed and tiny Two assumptions from the state model do most of the work: 1. **Parts are never created or destroyed** — the set of parts is fixed (the setup's parts). 2. **State is only path, stack, and facing** — there is nowhere to store a free position in space. Together they collapse the *entire* space of free interaction into three operations: 1. **`move(id, path, index?)`** — relocate a part to a path, at a stack position (default: top of stack). 2. **`setFacing(id, facing)`** — change facing, within the part's physical affordance. 3. **reorder** — a `move` with an explicit `index`. That's it. There is no arbitrary positioning and no free placement in 3D because the state model has nowhere to store one. "Free" means *unconstrained over these three operations*, not "free in space." ## 2. Free interaction and rule play are the same operations This is the payoff. Rule-enforced play (layer 4) is **the same three operations, gated by a legality check**: - The interaction layer produces an **intent** — "player wants `move(card, /discard)`." - **Sandbox mode**: apply it directly. - **Rule mode**: validate the intent against the setup's shape + the rule script first; reject if illegal. So the rule engine needs no interaction vocabulary of its own — it is a **filter over free interaction**. `move`/`setFacing` are the shared primitives; rules decide which are legal in the current state. Drag-and-drop and a scripted move both funnel through the same store mutation. That is the seam between layer 3 and layer 4. ## 3. Commands vs the dialog stack There are two channels, and they do not mix: - **Commands** — intent to *change state* (`move`, `setFacing`). They go through the rule seam and mutate the store. - **The dialog stack** — transient UI contexts (the deck pick-up, a "confirm discard," a hint prompt). Opening/closing a dialog **never issues a command** and never touches state; it is pure UI. This simplifies the model. The dialog stack is **UI state, hosted by the layer-3 shell, not by the game-state store** — dialogs don't belong in path/stack/facing. How they connect — one direction only: - **Player-initiated**: a click on a deck pushes the deck dialog; the dialog's insert button issues a `move` command. - **Script-initiated**: a rule script pushes the same dialog (e.g. to force a discard) and awaits the player's `move` through it. The script's open/close is tied to the dialog stack; it can `pushDialog`/`popDialog` without mutating game state. So the deck dialog is **one implementation, driven either way** — by a player click or by a rule script. Only the *trigger* differs. Dialogs are **authored in the manifest**, not hardcoded. A `role: dialog` definition declares the title, body, action buttons, and an optional widget (a stack of parts). Setups declare which dialogs are the tool for which interactions via `interactions:` (see [`format.md`](./format.md) §3). The rule seam stays clean because a dialog's buttons issue commands while its open/close is stack-only. ## 4. Compound interactions: the deck pick-up dialog A dialog is where compound, multi-step manipulation lives, because it owns the transient sub-state that the store must not. The prime example — inserting a card into the middle of a deck: 1. pick up the deck 2. scroll through it to find the place 3. insert the card at the cursor 4. put the deck back (even 0: put down your held hand of cards first). The dialog is an **alternate view of the stack**: the deck is "lifted" off the table into the dialog (it stays on its path; the rest of the table becomes backdrop). Its contents are shown **in order**, with an **insertion cursor** between cards that you scroll. The cursor *is* the index: `move(id, path, index)`'s index is discovered by scrolling the visible deck, not typed. This is a real interaction *mode*, owned by the dialog: - The scroll position is state. - Everything else pauses while it's open (or the held part is kept visible). - It only exists for **stacks**; single parts just snap onto a path. We treat it as a **reusable pattern** — a stack-inspector dialog — not a one-off hack for one command, so `deal`, `draw`, and `look-at-the-top` can reuse the same lifted-deck view. ## 5. The primitives map to concrete gestures - **Move** — pick up a part → it leaves its stack (transient "in hand"); drag → resolve the nearest path anchor within a threshold; drop → `move` (drop on nothing returns to origin). - **Facing** — click cycles the part through its physical affordance (`face → back → standing`, or the declared list). - **Reorder / insert** — the deck dialog above. The "in hand" state is transient and illegal, so it lives **outside the store** (a UI-level held part); only the committed outcome (drop) mutates the store. ## Open questions - **Held source for insertion** — single held card (fits the physical model; the dialog inserts it at the cursor), vs the dialog owns the source (you cursor a card *from* the deck to lift). Lean single-held-card, but confirm it doesn't fight the hand step. - **Multi-part ops** — picking up a whole stack, dealing N cards. Deferred; the single-part primitives are the foundation. - **Scroll window** — a window over a subset of the deck can return with the cursor's scroll position; exact widget is a render concern, not a state one.