<!-- Generated by apps/dreamboard-cli/scripts/sync-skill-docs.ts. -->
<!-- Source: docs/reference/game-interface.mdx -->

# Game interface

Reference for authoring Dreamboard game interfaces.

`@dreamboard/ui-sdk` is Dreamboard's game interface package. Use
it to create UI for the game using projected game view, handle player actions and render prompts
windows.

## Package boundary

Use `@dreamboard/ui-sdk` for:

- runtime bootstrap and loading/error shell
- reducer-view reads for the controlling seat
- typed action, prompt, and window submission
- lobby and session metadata
- presentational UI and board primitives

## Import surface

Game UIs should import from `@dreamboard/ui-sdk` only.

```tsx
import {
  ErrorBoundary,
  GameSkeleton,
  PluginRuntime,
  ToastProvider,
  useActions,
  useGameSelector,
  useGameView,
  useGameplayPrompts,
  useGameplayWindows,
  useLobby,
  useMe,
  usePlayerInfo,
  usePluginSession,
} from "@dreamboard/ui-sdk";
```

## Runtime shell

### `PluginRuntime`

`PluginRuntime` waits for the first reducer-native state-sync snapshot and then
provides runtime, session, and plugin-state context to authored hooks.

| Prop | Required | Notes |
| --- | --- | --- |
| `children` | Yes | Rendered after runtime initialization succeeds |
| `timeout` | No | Milliseconds to wait for first state-sync snapshot; defaults to `10000` |
| `loadingComponent` | No | Replaces the default loading shell |
| `errorComponent` | No | Receives the runtime boot error string |

```tsx
import { PluginRuntime } from "@dreamboard/ui-sdk";
import { createRoot } from "react-dom/client";

createRoot(document.getElementById("root")!).render(
  <PluginRuntime timeout={15000}>
    <App />
  </PluginRuntime>,
);
```

### `ErrorBoundary`

Wrap the root UI so authored render failures stay inside the plugin instead of
blanking the whole frame.

```tsx
import { ErrorBoundary, PluginRuntime } from "@dreamboard/ui-sdk";

<ErrorBoundary>
  <PluginRuntime>
    <App />
  </PluginRuntime>
</ErrorBoundary>;
```

### `ToastProvider` and `useToast()`

Use `ToastProvider` once near the root. Use `useToast()` from children when the
UI needs transient feedback that is not reducer state.

```tsx
import { ToastProvider, useToast } from "@dreamboard/ui-sdk";

function EndTurnButton() {
  const { success } = useToast();

  return (
    <button onClick={() => success("Turn submitted")}>
      End turn
    </button>
  );
}
```

`useToast()` returns:

- `toasts`
- `show(message, type?, duration?)`
- `dismiss(id)`
- `success(message, duration?)`
- `error(message, duration?)`
- `info(message, duration?)`
- `warning(message, duration?)`

### `GameSkeleton`

Use `GameSkeleton` while the plugin session is still loading or when the UI
needs a fallback shell.

| Prop | Required | Notes |
| --- | --- | --- |
| `variant` | No | One of `default`, `cards`, `players`, or `minimal` |
| `message` | No | Loading or placeholder text |
| `className` | No | Additional container classes |

```tsx
import { GameSkeleton, usePluginSession } from "@dreamboard/ui-sdk";

function AppLoader() {
  const { status } = usePluginSession();

  if (status === "loading") {
    return <GameSkeleton variant="default" message="Loading game..." />;
  }

  return <App />;
}
```

## View and session hooks

### `useGameView()`

Reads the full reducer-projected view for the controlling seat.

```ts
const view = useGameView();
```

Use this when the component genuinely needs the whole view object. Otherwise,
prefer `useGameSelector(...)`.

### `useGameSelector(selector, equalityFn?)`

Reads one derived slice from the projected reducer view and only re-renders
when that slice changes according to `equalityFn`.

```ts
const legalCardIds = useGameSelector(
  (view) => new Set(view.legalCardIds),
  (left, right) =>
    left.size === right.size && [...left].every((cardId) => right.has(cardId)),
);
```

Use selector hooks for:

- compact status text
- legal-target hints
- small card or resource subsets
- board-specific render inputs

### `usePluginSession()`

Returns runtime session metadata for the current plugin.

| Field | Type | Notes |
| --- | --- | --- |
| `status` | `"loading" \| "ready"` | Session initialization status |
| `sessionId` | `string \| null` | Current session ID |
| `controllablePlayerIds` | `string[]` | Seats the user can control |
| `controllingPlayerId` | `string \| null` | Currently selected seat |
| `userId` | `string \| null` | Current user ID |

```ts
const { controllingPlayerId, controllablePlayerIds, status } =
  usePluginSession();
```

### `useLobby()`

Returns the current lobby snapshot from state-sync.

```ts
const lobby = useLobby();
const seatCount = lobby.seats.length;
```

Use this for seat order, display names, colours, and host markers.

### `useMe()`

Returns the player currently being controlled by this user.

```ts
const me = useMe();
// me.playerId
// me.name
// me.color
```

This throws if there is no controlling player, so it is for playable seats, not
spectator-only UI.

### `usePlayerInfo()`

Builds a `Map<PlayerId, Player>` from the current lobby snapshot.

```ts
const players = usePlayerInfo();
const judge = players.get("player-1");
```

Use it when the reducer view contains player IDs and the UI needs labels,
colours, or host markers.

## Actions, prompts, and windows

### `useActions()`

`useActions()` is the typed reducer action surface for the current phase.

| Field | Notes |
| --- | --- |
| `phase` | Current runtime phase |
| `commands` | Typed reducer action factories for the current phase |
| `availableActions` | `ReadonlySet` of currently available action names |
| `can(name)` | Whether an action is currently available |
| `dispatch(command)` | Submit one action |
| `validate(command)` | Validate one action without submitting it |
| `respondToPrompt(prompt, response)` | Submit a prompt response |
| `submitWindowAction(window, command)` | Submit a gameplay-window action |

```ts
const phase = useActions();

if (phase.phase === "placeThing") {
  await phase.dispatch(
    phase.commands.placeThing({
      cardId: "a-dog",
      ringId: "ring-1",
    }),
  );
}
```

Use `phase.commands.<action>(...)` instead of hand-writing `{ type, params }`
objects. The command and phase types come from the generated UI contract.

`dispatch(...)`, `validate(...)`, `respondToPrompt(...)`, and
`submitWindowAction(...)` reject when runtime validation or submission fails.

### `useGameplayPrompts(promptId?)`

Returns active prompts for the controlling seat. Pass a prompt ID to narrow the
result to one prompt family.

```ts
const judgePrompts = useGameplayPrompts("judge-placement");

if (judgePrompts[0]) {
  await phase.respondToPrompt(judgePrompts[0], "ring-2");
}
```

Use prompts for reducer-owned deferred input, not ad hoc local modal state.

Typed prompt response example:

```tsx
import type { PromptResponse } from "@dreamboard/ui-contract";
import { useActions, useGameplayPrompts } from "@dreamboard/ui-sdk";

export function JudgePromptActions() {
  const phase = useActions();
  const prompts = useGameplayPrompts("judge-placement");
  const activePrompt = prompts[0];

  if (!activePrompt) {
    return null;
  }

  const chooseRing = async (ringId: "ring-1" | "ring-2" | "ring-3") => {
    const response: PromptResponse<"judge-placement"> = ringId;
    await phase.respondToPrompt(activePrompt, response);
  };

  return (
    <>
      <button onClick={() => void chooseRing("ring-1")}>Ring 1</button>
      <button onClick={() => void chooseRing("ring-2")}>Ring 2</button>
      <button onClick={() => void chooseRing("ring-3")}>Ring 3</button>
    </>
  );
}
```

### `useGameplayWindows(windowId?)`

Returns active gameplay windows for the controlling seat. Pass a window ID to
filter the result.

```ts
const tradeWindows = useGameplayWindows("trade-offer");

if (tradeWindows[0]) {
  await phase.submitWindowAction(tradeWindows[0], {
    type: "acceptTrade",
  });
}
```

Window actions are still reducer-defined and typed from the generated contract.

Typed window action example:

```tsx
import { windowCommands } from "../shared/generated/ui-contract";
import { useActions, useGameplayWindows } from "@dreamboard/ui-sdk";

export function TradeWindowActions() {
  const phase = useActions();
  const windows = useGameplayWindows("trade-offer");
  const activeWindow = windows[0];

  if (!activeWindow) {
    return null;
  }

  const acceptTrade = async () => {
    await phase.submitWindowAction(
      activeWindow,
      windowCommands["trade-offer"].acceptTrade({
        offerId: "offer-1",
      }),
    );
  };

  const declineTrade = async () => {
    await phase.submitWindowAction(
      activeWindow,
      windowCommands["trade-offer"].declineTrade(),
    );
  };

  return (
    <>
      <button onClick={() => void acceptTrade()}>Accept</button>
      <button onClick={() => void declineTrade()}>Decline</button>
    </>
  );
}
```

Import `windowCommands` from your workspace's generated
`shared/generated/ui-contract.ts` file.

## Presentational primitives

These components are presentational. Feed them from reducer view data and
explicit props.

| Export | Use it for |
| --- | --- |
| `Card` | One authored card face |
| `Hand` | Responsive hand layout with render props |
| `PlayArea` | Shared played-card or centre-table region |
| `PlayerInfo` | Player badges, scores, and active-seat markers |
| `ActionButton` | One action with availability, loading, and cost state |
| `ActionPanel` / `ActionGroup` | Grouped action regions |
| `ResourceCounter` | Resource totals |
| `CostDisplay` | Cost breakdowns |
| `PhaseIndicator` | Current phase or step status |
| `GameEndDisplay` | Final rankings and scores |
| `DiceRoller` | Reducer-driven dice results |
| `Drawer` and related exports | Mobile overflow or detail panels |

`DiceRoller` is display-only. In reducer-native games, dice values are
typically driven by authored die state updated through reducer runtime effects
such as `effects.rollDie(...)`.

### `Card`

`Card` renders one `CardItem`. Use `renderContent` when the default face is not
enough.

```tsx
<Card
  card={card}
  selected={selectedIds.includes(card.id)}
  onCardClick={(cardId) => setSelectedIds([cardId])}
  renderContent={(item) => <ThingCardFace card={item} />}
/>
```

### `Hand`

`Hand` is a render-prop primitive. You supply `renderCard`, `renderDrawer`, and
`renderEmpty`.

```tsx
<Hand
  cards={view.handCards}
  selectedIds={selectedIds}
  renderCard={({ card, x, y, zIndex, isSelected }) => (
    <div
      key={card.id}
      style={{
        position: "absolute",
        left: x,
        transform: `translateY(${y}px)`,
        zIndex,
      }}
    >
      <Card
        card={card}
        selected={isSelected}
        onCardClick={(cardId) => setSelectedIds([cardId])}
      />
    </div>
  )}
  renderDrawer={({ cards }) => <button>View {cards.length} cards</button>}
  renderEmpty={() => <p>No cards in hand</p>}
/>
```

Use `Hand` when you want SDK layout behaviour but game-specific card faces.

### `PlayArea`

`PlayArea` renders a shared card region from an array of `CardItem` values.

```tsx
<PlayArea
  cards={view.trickCards}
  layout="row"
  renderCard={(card) => <ThingCardFace card={card} />}
/>
```

### `ActionButton`

Use `ActionButton` for one reducer action. It can reflect availability,
resource affordability, and loading state.

```tsx
<ActionButton
  label="Build road"
  cost={{ brick: 1, lumber: 1 }}
  currentResources={view.resources}
  resourceDefs={resourceDefs}
  available={phase.can("buildRoad")}
  onClick={() => phase.dispatch(phase.commands.buildRoad())}
/>
```

### `ActionPanel` and `ActionGroup`

Use these to keep the action surface grouped by phase or intent.

```tsx
<ActionPanel title="Your turn" state={phase.phase}>
  <ActionGroup title="Build">
    <ActionButton
      label="Build road"
      available={phase.can("buildRoad")}
      onClick={() => phase.dispatch(phase.commands.buildRoad())}
    />
  </ActionGroup>
</ActionPanel>
```

### `PlayerInfo`

`PlayerInfo` is a display component. Pass display-ready values, not raw lobby
records.

```tsx
<PlayerInfo
  playerId={me.playerId}
  name={me.name}
  color={me.color}
  isCurrentPlayer
  isActive={view.activePlayerId === me.playerId}
  score={view.scores[me.playerId] ?? 0}
/>
```

## Board primitives

Use board primitives when the reducer view already contains render-ready board
data.

| Export | Use it for |
| --- | --- |
| `TrackBoard` | Linear, circular, or branching tracks |
| `SquareGrid` | Chess-like or tile-grid boards |
| `HexGrid` | Hex maps with tiles, edges, and vertices |
| `NetworkGraph` | Nodes, connections, and route graphs |
| `ZoneMap` | Region or area control boards |
| `SlotSystem` | Worker-placement and slot occupancy layouts |

```tsx
<TrackBoard
  spaces={view.track.spaces}
  pieces={view.track.pieces}
  type="circular"
  renderSpace={(space, pieces) => (
    <g aria-label={space.name ?? space.id}>
      <circle cx={space.position.x} cy={space.position.y} r={24} />
      <text x={space.position.x} y={space.position.y}>
        {pieces.length}
      </text>
    </g>
  )}
/>
```

Board renderers are still UI code. The reducer should provide:

- legal targets
- ownership markers
- highlight states
- labels and annotations
- grouped piece and card data in the shape the component expects

## Exported types

Use these root exports when the UI needs to type reducer-owned runtime data.

| Type | Meaning |
| --- | --- |
| `PhaseActions<Phase>` | Current phase action API returned by `useActions()` |
| `ActionDefinition` | One available action from gameplay state |
| `GameplayPromptInstance` | One typed active prompt instance |
| `GameplayWindowInstance` | One typed active window instance |
| `GameplaySnapshot` | Transport state that sits alongside the projected view |
| `PluginStateSnapshot` | Complete state-sync payload delivered to the plugin |
| `LobbyState` | Current lobby seats and host metadata |
| `Player` | Simplified player metadata from the lobby |
