Add the command/dialog split to the state-model and interactions docs, declare role dialog and zone facing in the format spec, and let setups declare which dialogs power open interactions.
133 lines
5.8 KiB
Markdown
133 lines
5.8 KiB
Markdown
# 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:** Design. Proposes the operation set, the command/dialog split, and
|
||
> the deck-pick-up dialog before the interaction half of `@tts/tabletop` is
|
||
> built.
|
||
|
||
## 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. |