# Games — Cookbook

> Short, copy-pasteable recipes for the games modality: roles, ticks, inputs, state, events, reconnect, and namespaces.

Task-oriented snippets for the games modality. Each assumes you've imported the
client:

**TypeScript:**
```ts
import { Games } from "@clutchcall/sdk/games";
```

**Python:**
```python
from clutchcall.games import Games
```

## Join a room as a player

Construct with a `playerId` to take the player role.

**TypeScript:**
```ts
const me = new Games({
  relayHost: "relay.clutchcall.dev",
  token:     await fetchRoomToken("duel-42", "alice"),
  roomId:    "duel-42",
  playerId:  "alice",
});
```

**Python:**
```python
me = Games(
    relay_host="relay.clutchcall.dev",
    token=await fetch_room_token("duel-42", "alice"),
    room_id="duel-42",
    player_id="alice",
)
```

## Run the authoritative server

Omit `playerId` to take the authority role — it publishes state and reads every
player's input.

**TypeScript:**
```ts
const auth = new Games({ relayHost, token, roomId: "duel-42" }); // no playerId
```

**Python:**
```python
auth = Games(token=tok, room_id="duel-42")   # no player_id
```

## Broadcast world state on a tick

Open the state publisher once, then write a self-contained snapshot per tick.
You drive the clock.

**TypeScript:**
```ts
const state = await auth.publishState({ tickHz: 30 });
setInterval(() => state.write(serializeWorld(world)), 1000 / 30);
```

**Python:**
```python
import asyncio
state = auth.publish_state(tick_hz=30)
async def loop():
    while True:
        state.write(serialize_world(world))
        await asyncio.sleep(1 / 30)
```

## Render incoming state on the client

Subscribe to state; each callback is one snapshot. Latest-wins — never block on
an older one.

**TypeScript:**
```ts
await me.subscribeState((bytes) => render(deserializeState(bytes)));
```

**Python:**
```python
me.subscribe_state(lambda data: render(deserialize_state(data)))
```

## Send player input each tick

`publishInput` returns a publisher bound to this player's id — no need to attach
your own `from`.

**TypeScript:**
```ts
const input = await me.publishInput();
addEventListener("tick", () => input.write(serializeInput(localInput)));
```

**Python:**
```python
inp = me.publish_input()
inp.write(serialize_input(local_input))
```

## Fan in every player's input on the server

One callback receives all players; the SDK decodes the `from` header for you.

**TypeScript:**
```ts
await auth.subscribeInputs((playerId, bytes) => {
  applyInput(playerId, deserializeInput(bytes));
});
```

**Python:**
```python
auth.subscribe_inputs(lambda pid, data: apply_input(pid, deserialize_input(data)))
```

## Send a reliable chat event

Events ride a reliable, ordered stream — use them for anything that must not be
dropped.

**TypeScript:**
```ts
const chat = await me.publishEvent({ channel: "chat" });
chat.write(serialize({ text: "gg" }));
```

**Python:**
```python
chat = me.publish_event(channel="chat")
chat.write(serialize({"text": "gg"}))
```

## Receive events with the sender id

`subscribeEvents` hands you the decoded sender and the payload.

**TypeScript:**
```ts
await me.subscribeEvents({ channel: "chat" }, (fromPlayerId, bytes) => {
  appendChat(fromPlayerId, deserialize(bytes));
});
```

**Python:**
```python
me.subscribe_events(
    channel="chat",
    on_event=lambda pid, data: append_chat(pid, deserialize(data)),
)
```

## Use multiple event channels

Each channel is an isolated reliable track — split chat from gameplay RPCs.

**TypeScript:**
```ts
const chat   = await me.publishEvent({ channel: "chat" });
const rpc    = await me.publishEvent({ channel: "rpc.use_item" });

await me.subscribeEvents({ channel: "chat" },        onChat);
await me.subscribeEvents({ channel: "rpc.use_item" }, onItemRpc);
```

**Python:**
```python
chat = me.publish_event(channel="chat")
rpc  = me.publish_event(channel="rpc.use_item")

me.subscribe_events(channel="chat",         on_event=on_chat)
me.subscribe_events(channel="rpc.use_item", on_event=on_item_rpc)
```

## Emit a server-authoritative event

The authority can publish events too — they're stamped `from = "_authority"`, so
clients can trust them as coming from the server.

**TypeScript:**
```ts
const settle = await auth.publishEvent({ channel: "match.result" });
settle.write(serialize({ winner: "alice", score: [3, 1] }));
```

**Python:**
```python
settle = auth.publish_event(channel="match.result")
settle.write(serialize({"winner": "alice", "score": [3, 1]}))
```

## Override priority on a write

state/input default to `100`, events to `50`. Bump a critical event above the
rest of its lane.

**TypeScript:**
```ts
const events = await me.publishEvent({ channel: "chat" });
events.write(serialize(urgentLine), 90);   // higher than default 50
```

**Python:**
```python
events = me.publish_event(channel="chat")
events.write(serialize(urgent_line), priority=90)
```

## Track connection state

Pass `onState` to watch the link. The session reconnects and replays your
channels automatically — no manual re-subscribe.

**TypeScript:**
```ts
const me = new Games({
  relayHost, token, roomId: "duel-42", playerId: "alice",
  onState: (s) => {
    if (s === 1) showOnline();
    if (s === 2) showReconnecting();
  },
});
```

**Python:**
```python
me = Games(
    token=tok, room_id="duel-42", player_id="alice",
    on_state=lambda s, reason=None: (
        show_online() if s == 1 else show_reconnecting() if s == 2 else None
    ),
)
```

## Stop one channel without leaving the room

Every `subscribe*` returns a handle you can `close()` while keeping other
channels open.

**TypeScript:**
```ts
const sub = await me.subscribeEvents({ channel: "chat" }, onChat);
sub.close();   // stop receiving chat; input/state stay live
```

**Python:**
```python
sub = me.subscribe_events(channel="chat", on_event=on_chat)
sub.close()
```

## Leave the room

`close()` tears down the shared session and every channel on it.

**TypeScript:**
```ts
await me.close();
```

**Python:**
```python
me.close()
```

## Validate a player id before joining

Player ids must encode to ≤ 255 UTF-8 bytes — the wire header is one length
byte. Guard untrusted ids client-side.

**TypeScript:**
```ts
if (new TextEncoder().encode(playerId).length > 255) {
  throw new Error("player id too long");
}
```

**Python:**
```python
if len(player_id.encode("utf-8")) > 255:
    raise ValueError("player id too long")
```

> **TIP:**
> Picking between channels: if dropping the message is fine because a newer one is
> coming, use **state** or **input** (datagram, latest-wins). If a player would
> notice it missing, use an **event** (reliable, ordered).
