# Recipes

Copy-paste patterns for things you'll actually build. Each recipe is
a short, working snippet plus a sentence on why the pieces are where
they are. New to the language? Read the
[language guide](./language-guide.md) first; this doc assumes you
know what `var`, `cstate`, `host`, `plyr`, `lstn`, and `cpnt` mean.

## Contents

- [Name your game's types (aliases & enums)](#name-your-games-types-aliases--enums)
- [A start screen with a Start button](#a-start-screen-with-a-start-button)
- [Move to a new state when the host hits Start](#move-to-a-new-state-when-the-host-hits-start)
- [List players with a kick button each](#list-players-with-a-kick-button-each)
- [Text input with a `cstate` draft](#text-input-with-a-cstate-draft)
- [Pre-fill a `cstate` from server data with `init`](#pre-fill-a-cstate-from-server-data-with-init)
- [Instant UI with the client prelude & epilogue](#instant-ui-with-the-client-prelude--epilogue)
- [Score the round and move on](#score-the-round-and-move-on)
- [Show different UI per player ("you've answered")](#show-different-ui-per-player-youve-answered)
- [Build a list of buttons from a list of options](#build-a-list-of-buttons-from-a-list-of-options)
- [Factor a reusable component that reads state & forwards actions](#factor-a-reusable-component-that-reads-state--forwards-actions)
- [Conditional CSS classes](#conditional-css-classes)
- [Pop a notification toast](#pop-a-notification-toast)
- [Play a UI sound on tap](#play-a-ui-sound-on-tap)
- [Show a custom image or play custom audio](#show-a-custom-image-or-play-custom-audio)
- [Time-out a state with `func.currentTime`](#time-out-a-state-with-funccurrenttime)
- [Track who has answered with a player-keyed record](#track-who-has-answered-with-a-player-keyed-record)

---

## Name your game's types (aliases & enums)

Repeating a shape like `{ [key: string]: { name: string, score: number } }`
across every `var`, `func`, and `state.params` is error-prone. Declare it
once in `globals.banto` with a `type` alias and reference the name:

```banto
// globals.banto
type Player = { name: string, score: number }
type Phase  = "lobby" | "playing" | "results"   // a named enum

var players { type: { [key: string]: Player }, default: {} }
var phase   { type: Phase, default: "lobby" }
```

```banto
// question.banto — reference aliases in params and func signatures too
state question { params: { question: Question } }

func topScore {
    params: { ps: Player[] },
    returnType: number,
    action: {
        let best = 0;
        params.ps.forEach((p) => { if (p.score > best) { best = p.score; } });
        return best;
    }
}
```

Why: aliases are **compile-time only** (they expand to their real shape
before anything runs), so there's zero runtime cost — they exist purely to
keep your types readable and consistent. A named string-union like `Phase`
is also the *only* way to declare an enum; the compiler rejects an inline
`"a" | "b"` in a block annotation. Aliases live in `globals.banto` only,
can reference each other, and can't be recursive.

---

## A start screen with a Start button

The host sees a Start button; clicking it flips a `var`.

```banto
// start.banto
state start {}

var started {
    type: boolean,
    default: false
}

host default {
    condition: true,
    child: cpnt.button({
        child: "Start the game",
        class: [],
        onClick: () => { var.started = true; }
    })
}

plyr default {
    condition: true,
    child: "Waiting for the host to start…"
}
```

Why this works: `var.started` starts at `false`. The button's
`onClick` flips it. A listener (next recipe) watches `var.started`
and acts when it becomes `true`.

---

## Move to a new state when the host hits Start

Pair the previous recipe with a `lstn`:

```banto
// start.banto (continued)
lstn onStart {
    condition: var.started,
    next: { state: "question", inputs: { questionId: 0 } }
}
```

The next state must exist (`question.banto`) and declare matching
params:

```banto
// question.banto
state question {
    params: { questionId: number }
}
```

Why: a listener fires the moment its `condition` becomes true. `next`
hands off to the named state, passing values that match its `params`.
You can include `actions: { … }` too, to mutate vars before the
transition.

---

## List players with a kick button each

```banto
host default {
    condition: true,
    child: cpnt.HostLobby({
        roomCode: var.roomCode,
        onStart: () => { var.started = true; },
        playerButtons: Object.entries(var.players).map(([sessionId, p]) => ({
            name: p.name,
            sessionId: sessionId,
            onClick: (sId) => { func.kickPlayer(sId); }
        }))
    })
}
```

Two things to notice:

1. The `onClick` is `action(string)` — it receives the kicked
    player's `sessionId` as the arrow argument. (`HostLobby`
    happens to pass the sessionId through; for components where
    the action is `action`, you'd write `() => …` and capture
    `sessionId` from the `.map`.)
2. `func.kickPlayer` returns `action`, so it can stand alone as a
    statement. The runtime disconnects the player and removes them
    from `var.players`.

---

## Text input with a `cstate` draft

The classic pattern: keep typing-in-progress on the client, only
flush to a `var` on submit.

```banto
cstate draft {
    type: string,
    default: ""
}

plyr default {
    condition: true,
    child: cpnt.textInput({
        value: cstate.draft,
        hint: "Your funniest answer…",
        onChange: (next) => { cstate.draft = next; },
        onSubmit: (text) => {
            var.answers[player.sessionId] = text;
            cstate.draft = "";
        }
    })
}
```

Why `cstate` and not `var`: every keystroke would otherwise round-trip
to the server. `cstate.draft` is browser-local, so typing is
instant; the server only learns the final string when the player
submits.

> The compiler is strict about where you can write to `cstate`. Top-level
> statements only — not inside `if` branches or iterator bodies. The
> error message tells you exactly where to lift the assignment to.

---

## Pre-fill a `cstate` from server data with `init`

A `cstate` `default` is a static literal — it can't read `player` or
`var`. When each viewer's draft should *start* from their own
server-side data (an editable profile, a previously-submitted answer, a
per-player board), seed it with an `init` block on the view.

```banto
cstate board {
    type: { [key: string]: string },
    default: {}
}

plyr default {
    condition: true,
    init: {
        cstate.board = var.playerBoards[player.sessionId];
    },
    child: cpnt.PlayerScreen({
        name: player.name,
        score: player.score,
        roomCode: var.roomCode,
        child: cpnt.Board({ cells: cstate.board })   // edits stay local
    })
}
```

`init` runs **once on the server** when the viewer enters this state (or
first connects), reads whatever server state it needs, and ships only
the resulting `cstate` to that client. Edits from there on are local
until you flush them to a `var` on submit — same as the plain draft
recipe above. `init` is skipped on incremental re-renders, so a later
`var` change won't clobber the viewer's in-progress edits.

> `init` writes `cstate` only — it can't mutate `var` (that's a
> listener's job). Its right-hand sides may read `var`, `player`,
> `state.params`, `data`, and `func.X(…)`.

---

## Instant UI with the client prelude & epilogue

You write one action body, but the compiler splits any handler that
touches `cstate` into up to three phases that run **in this order**:

```text
client prelude  →   server body   →  client epilogue
(cstate writes      (var writes,      (cstate writes
 before the first    func.X side-      after the last
 server write)       effects)          server write)
```

The split is purely by **source order** — you don't annotate anything.
`cstate.*` writes (and the `let`s that only feed them) sitting *before*
your first `var`/server statement become the **prelude**; ones sitting
*after* your last server statement become the **epilogue**.

**Why this matters: no round-trip.** Both client phases run **in the
browser, immediately**, on the same frame as the tap/keystroke. The
server body is fired off in parallel and its fresh view streams back
whenever the server is done. So anything you express as a `cstate` write
paints **instantly**, while authoritative `var` state catches up
asynchronously. The epilogue does **not** wait for the server response —
it runs right after the trigger is emitted.

Two things this buys you:

- **Zero-latency screen updates.** Move a cursor, fill a grid cell, flip
   a local toggle — drive it through `cstate` and it updates without
   waiting a round-trip for the server to echo a new view.
- **Sequencing `cstate` around `var` writes.** Snapshot or adjust client
   state *before* the server sees it (prelude), hand work to the server
   (body), then reset or advance client state *after* (epilogue) — all in
   one handler.

**Crossword grid, typing a letter.** The player's grid must update the
instant they press a key; the server grades asynchronously. One handler:

```banto
onChange: (t) => {
    // ── prelude (client, instant) ──
    let curr = cstate.activeGrid;
    cstate.board[curr.r][curr.c] = t;          // fill the cell now
    cstate.activeGrid.c = curr.c + 1;          // advance the cursor now

    // ── server body (async) ──
    var.pendingLetters.push({                  // let the server grade it
        sessionId: player.sessionId,
        r: curr.r, c: curr.c, letter: t
    });
}
```

The cell fills and the cursor advances on the same frame the key is
pressed. Meanwhile `var.pendingLetters` reaches the server, a
`lstn` grades the move, and the corrected board streams back — without
ever blocking the typing.

> **Two rules keep the split clean.** (1) The client work must stay
> contiguous — a prelude prefix and/or an epilogue suffix — with all your
> server writes grouped in the middle. A server write that lands *after*
> an epilogue `cstate` write is rejected as interleaved
> (`cstate-write-interleaved`); move it up. (2) `cstate` writes may live
> inside `if`/`forEach` branches (the whole block lifts to the client, as
> the real crossword handler does), but such a branch can't *also* contain
> a `var`/server write — one branch is either all-client or all-server
> (`client-phase-mixed-branch`). Split a mixed branch into a client one
> and a server one. See the [gotchas](./gotchas.md#actions-client-vs-server).

---

## Score the round and move on

When everyone has answered (or time runs out), grade and transition.
This snippet assumes a `var.players` map and a `var.playerData` map
keyed by sessionId.

```banto
lstn onDone {
    condition: var.skipped
        || (Object.keys(var.players).length > 0
            && Object.keys(var.players).length == Object.keys(var.playerData).length)
        || (func.currentTime() - state.params.initTime) >= var.questionTime,
    actions: {
        Object.entries(var.players).forEach(([sessionId, p]) => {
            if (!Object.keys(var.playerData).includes(sessionId)) { return; }
            if (state.params.question.correctOptions.includes(
                var.playerData[sessionId].selection
            )) {
                var.players[sessionId].score = p.score + 100;
            }
        });
    },
    next: { state: "results" }
}
```

`return;` inside `.forEach` skips to the next iteration — that's how
you bail out early per-iteration without an extra `if` wrapping the
whole body.

---

## Show different UI per player ("you've answered")

Multiple `plyr` blocks, evaluated top-to-bottom; the first true one
wins.

```banto
plyr hasAnswered {
    condition: Object.keys(var.playerData).includes(player.sessionId),
    child: cpnt.PlayerScreen({
        name: player.name,
        score: player.score,
        roomCode: var.roomCode,
        child: "Answer locked in. Waiting for everyone else…"
    })
}

plyr default {
    condition: true,
    child: cpnt.QuestionScreen({ /* ... */ })
}
```

The `default` block must be last and must be `condition: true`.

---

## Build a list of buttons from a list of options

```banto
plyr default {
    condition: true,
    child: cpnt.container({
        class: [css.answer-column],
        child: state.params.question.options.map((option, i) =>
            cpnt.button({
                child: option,
                class: [],
                onClick: () => {
                    var.playerData[player.sessionId] = {
                        selection: i,
                        selectionTime: func.currentTime()
                    };
                }
            })
        )
    })
}
```

`cpnt.container`'s `child` accepts a list, so mapping options straight
into it is all you need — lay them out with a flex class in
`styles.banto.css`.

Each button's `onClick` captures **its own** `i` and `option` from
the `.map`. The compiler snapshots those at render time, so when the
player taps option 2, the action knows it's option 2 — even though
all four buttons share the same arrow.

---

## Factor a reusable component that reads state & forwards actions

A custom `cpnt` can read **global** state (global `var`s, the inherent
`roomCode`/`players`/`currentTime`, and `data.*`) and can **forward an
action** — either by invoking an `action` param with a value, or by
letting an action ride through a `.map` into a nested cpnt. That's enough
to factor a grid into `Cell` / `Row` pieces instead of inlining it.

```banto
// globals.banto
var roundScore { type: number, default: 0 }

// Reads a global var directly — no need to thread it through params.
cpnt ScoreHud {
    params: { label: string },
    child: cpnt.container({ class: [css.hud], child: params.label + ": " + var.roundScore })
}

// Invokes its `onPick` action param with the tapped value.
cpnt Choice {
    params: { label: string, value: string, onPick: action(string) },
    child: cpnt.button({
        child: params.label,
        class: [css.choice],
        onClick: () => { params.onPick(params.value); }
    })
}

// Forwards a per-cell action through a `.map` into `Choice`.
cpnt ChoiceRow {
    params: { choices: { label: string, value: string, onPick: action(string) }[] },
    child: cpnt.container({ class: [css.row], child: params.choices.map((c) =>
        cpnt.Choice({ label: c.label, value: c.value, onPick: c.onPick })) })
}
```

Call it and handle the forwarded value at the call site:

```banto
plyr default {
    condition: true,
    child: cpnt.ChoiceRow({ choices: state.params.options.map((o) => ({
        label: o.text,
        value: o.id,
        onPick: (id) => { var.answers[player.sessionId] = id; }
    })) })
}
```

Two rules to remember: a cpnt body is **read-only** and can't see
state-scoped vars, `state.params`, or `cstate` (pass those in via
`params`); and an **invoked** action (`params.onPick(v)`) must do server
work only — put any `cstate`/`client.*` work in the call-site handler.

---

## Conditional CSS classes

`&&` and `||` follow JavaScript semantics — they return one of their
operands. The runtime drops non-string entries from `style` arrays
before joining, which lets you write:

```banto
class: [
    css.option-button,
    var.isCorrect && css.correct,
    var.isWrong && css.wrong,
    player.sessionId == state.params.activePlayer && css.is-you
]
```

When a guard is false, that entry becomes `false`, gets dropped, and
the rendered `class` attribute only contains the survivors.

---

## Pop a notification toast

```banto
lstn onTimeUp {
    condition: !var.allAnswered
        && (func.currentTime() - state.params.initTime) > var.questionTime,
    actions: {
        func.notify({ to: "all", message: "Time's up!", type: "standard" });
    },
    next: { state: "results" }
}
```

`to` accepts `"host"`, `"all"`, or a player `sessionId`. `type` is
optional (`"error" | "success" | "standard"`, defaulting to
`"standard"`). Notifications are fire-and-forget — unknown
sessionIds silently drop, no error.

---

## Play a UI sound on tap

```banto
cpnt.button({
    child: "Buzz in!",
    class: [],
    onClick: () => {
        client.playSound("ding");
        var.buzzedPlayers.push(player.sessionId);
    }
})
```

`client.playSound(sound)` runs on the player's browser. The compiler
optimizes statically-known calls so the sound fires instantly on
click without waiting for the server round-trip.

Allowed built-in sounds: `"tick"`, `"ding"`, `"whoosh"`, `"chching"`,
`"applause"`, `"countdown"`, `"anticipation"`, `"waiting"`. To play one
of **your own** clips, pass an audio asset instead — see the next recipe.

---

## Show a custom image or play custom audio

Upload your media once, then reference it as `asst.<name>`. Drop files
in an `assets/` directory, title them in `banto.config.json`, and run
`banto assets push`:

```text
my-game/assets/logo.png
my-game/assets/victory-fanfare.mp3
```

```json
{ "assets": { "logo": { "title": "Logo" }, "victory-fanfare": { "title": "Fanfare" } } }
```

Then use them from any view or action:

```banto
// an image on the host screen
child: cpnt.image({ src: asst.logo, alt: "Logo", fit: "contain", class: [css.logo] })

// ambient background music
child: cpnt.audio({ src: asst.victory-fanfare, autoplay: true, loop: true })

// a one-shot cue on a tap (client.playSound accepts an audio asset)
onClick: () => { client.playSound(asst.victory-fanfare); }
```

`asst.<name>` uses the file's base name (no extension) and resolves at
build time. The compiler enforces kind: an image asset won't fit an
audio slot, or vice versa. See the [language guide](./language-guide.md#custom-image--audio-assets)
for the full flow.

---

## Time-out a state with `func.currentTime`

Two pieces — store the entry time as a state param, then check the
elapsed time in a listener.

```banto
// previous state's lstn.next:
next: {
    state: "question",
    inputs: {
        question: data.questionSet[var.questionIndex],
        initTime: func.currentTime()
    }
}
```

```banto
// question.banto
state question {
    params: { question: ..., initTime: number }
}

var questionTime {
    type: number,
    default: 60000   // 60 seconds in milliseconds
}

lstn onTimeout {
    condition: (func.currentTime() - state.params.initTime) >= var.questionTime,
    next: { state: "results" }
}
```

`func.currentTime()` returns milliseconds since the Unix epoch.

---

## Track who has answered with a player-keyed record

A `{ [sessionId: string]: <something> }` map is the workhorse for
"per-player state during a round". Declare it locally to the state so
it resets when the state ends:

```banto
var playerData {
    type: { [key: string]: { selection: number, selectionTime: number } },
    default: {}
}
```

Write to it from a player's action:

```banto
onClick: () => {
    var.playerData[player.sessionId] = {
        selection: i,
        selectionTime: func.currentTime()
    };
}
```

Read out with `Object.keys`, `Object.entries`, `Object.values`:

```banto
condition: Object.keys(var.playerData).length == Object.keys(var.players).length
```

To remove a key cleanly (from a non-mutating perspective), use
`func.removeKey`:

```banto
var.playerData = func.removeKey({ obj: var.playerData, key: sessionId });
```