# Banto DSL — full documentation # Banto DSL — Developer Docs Welcome! These docs help you write your first Banto game and get comfortable with the `banto` CLI. They're written for developers who have done a little JavaScript or TypeScript, but **don't** assume you've built a multiplayer game before. Two things you'll install: the **`@bantohq/cli`** command-line tool (scaffold, build, publish) and the **Banto DSL** editor extension (highlighting and inline errors). You *write* games in `.banto` files and *play* them by publishing to [banto.tv](https://banto.tv). ## Where to start 1. **[Getting started](./getting-started.md)** — install the CLI and the editor extension, scaffold a project, and publish your first game. Aim for 5 minutes. 2. **[The Banto language](./language-guide.md)** — the friendly tour of the DSL: project layout, blocks, types, actions, components, assets. 3. **[CLI reference](./cli.md)** — every command, every flag, every config file. Skim once, then keep it open while you work. 4. **[Recipes](./recipes.md)** — copy-paste patterns for the things you'll actually build (kicking players, text input, conditional UI, scoring, state transitions, media). 5. **[Registry reference](./registry-reference.md)** — the components and functions Banto ships with, organized so you can find one quickly. 6. **[Gotchas](./gotchas.md)** — the common compile errors and their fixes, keyed to diagnostic codes. Read before your first build; it saves round-trips. ### Fast path (for agents / experienced devs) Skip the tour. Read **getting-started → registry-reference → recipes → gotchas**, consult **language-guide** only for a specific feature, and learn a pattern by reading the **one** most similar example game in [`examples/`](../examples/), not by surveying all of them. ## Where to go for the spec-level details The docs above are the readable, learning-oriented versions. The authoritative specs live at the repo root and are worth a read once you're past the basics: - [`SYNTAX-AND-USAGE.md`](../SYNTAX-AND-USAGE.md) — every rule the language enforces. - [`COMPILATION.md`](../COMPILATION.md) — what your `.banto` files compile to, for the curious. - [`CLIENT-DATA.md`](../CLIENT-DATA.md) — how the server and the browsers talk to each other at runtime. If something in this folder contradicts those root files, the root files win. Tell us — they're the source of truth and these docs should follow. --- # Getting started This walkthrough takes you from "no Banto installed" to "a real game I can host on banto.tv" in about five minutes. By the end you'll know the shape of a Banto project and the handful of commands you'll use every day. ## What you'll need - **Node.js 18 or newer** — `node --version` should print something like `v20.x.x`. - A terminal you're comfortable with (PowerShell, Bash, zsh — any). - **VS Code** (or a compatible fork). Banto ships an extension that gives you highlighting and inline error messages — it's the fastest way to write `.banto` files. ## 1. Install the CLI ```bash npm install -g @bantohq/cli ``` Verify it worked: ```bash banto --help ``` You should see a list of subcommands (`init`, `build`, `fmt`, `auth`, `publish`, `assets`). > **About the `-g` flag.** Installing globally makes `banto` > available from any directory. If you'd rather not, install it as a > dev dependency in your project (`npm install --save-dev > @bantohq/cli`) and use `npx banto …` instead. ## 2. Install the VS Code extension Search **"Banto DSL"** in the Extensions view (or install it from the [Marketplace](https://marketplace.visualstudio.com/) / [Open VSX](https://open-vsx.org/) if you're on Cursor, Windsurf, or another fork). It gives you: - **Syntax highlighting** for `.banto` files - **Inline diagnostics** — the same type errors `banto build` reports, shown as you type - **Completions and hover docs** for built-in components (`cpnt.*`), functions (`func.*` / `client.*`), and your assets (`asst.*`) - **Format Document** support Because the extension surfaces errors live, most mistakes never make it as far as a build. ## 3. Scaffold a project Pick a directory you don't mind creating, then: ```bash banto init my-game cd my-game ``` You should now have a folder that looks like this: ```text my-game/ ├── globals.banto # globals: data sources, custom components, shared state ├── start.banto # the first state every game enters ├── styles.banto.css # CSS classes you can reference from your views ├── banto.config.json # publish settings (title, description, datasets) └── .gitignore ``` That's a complete "empty lobby" game: the host sees a room code, each player sees a "you're in" message, and the host can kick players. No real game yet — but enough of a shell to start adding logic to. > Open `start.banto` and `globals.banto` in your editor. Don't worry > about every line yet — we'll come back. Just notice the shape: > top-level "blocks" with a name and a `{ … }` body. ## 4. Build it ```bash banto build ``` This compiles your project to a single `game.json` and typechecks it. A clean run prints something like: ```text Wrote ./game.json (487 bytes, 0 error(s), 0 warning(s)). ``` If there's a mistake, you get the file, line, and a short explanation — fix it and build again. (With the extension installed you'll usually have already seen the same error underlined in your editor.) ## 5. Publish it and play Banto games run on [banto.tv](https://banto.tv) — that's where they're hosted — but you can test yours without real phones using the **sandbox**. Publishing privately is how you preview your own work. First, make sure `"public": false` in `banto.config.json` while you iterate, then: ```bash banto publish ``` The first publish opens a browser to log you in (the same thing `banto auth` does), then uploads your game. Now open the sandbox: ```bash banto sandbox ``` This opens an owner-only harness in your browser with the host screen and simulated player windows side by side — click through a whole round yourself, use **+ Add player** to add players, and open the **Inspector** (top-right) to watch your `var.*` state and `func.log` output live. Then, for a real multi-device round: 1. Go to [banto.tv](https://banto.tv) and start your game as the host. 2. Join from a couple of phones or extra browser tabs using the room code. 3. Play a round. Because you published with `"public": false`, only you (and anyone you send a direct link) can see it — perfect for playtesting. > **Iterating.** Edit a `.banto` file, run `banto publish` again, and > refresh. Re-publishing **updates the same game in place** — same ID, > no duplicate listing. Flip `"public": true` when you're ready to list > it in the public catalog. ## 6. Make your first change Open `start.banto` and find the `plyr default` block. Change the welcome message: ```banto plyr default { condition: true, child: cpnt.PlayerScreen({ name: player.name, score: player.score, roomCode: var.roomCode, child: "Welcome — you made it in!" }) } ``` Save, run `banto publish` again, and refresh your game on banto.tv. The player screen shows your new copy. You just learned the core idea of Banto: **describe what each viewer should see, declaratively, in `.banto` files. The runtime takes care of getting it onto their screens.** ## 7. Where to go next You now know enough to wander. - **[The Banto language](./language-guide.md)** — read this next. It explains everything you saw in the starter (`var`, `host`, `plyr`, `cpnt`, the `child` field) plus the parts you didn't (state transitions, listeners, actions, custom components, custom assets). - **[Recipes](./recipes.md)** — once you have a feature in mind ("I want a text input", "I want to advance to the next state when everyone has answered"), check here first. - **[CLI reference](./cli.md)** — keep this handy. It documents every command, including `banto assets` for custom media and `banto publish` for shipping to banto.tv. One part of the starter you'll touch a lot: - **`banto.config.json`** — your game's identity on banto.tv. Title, description, default question/prompt set. Edit this before you `banto publish`. Have fun. The DSL is small enough that you'll have a feel for it within a few hours; ping the team if you get stuck on something the docs didn't answer. --- # The Banto language This is the friendly tour of the Banto DSL — the language you write in `.banto` files. It assumes you've followed [Getting started](./getting-started.md) and have a project to play with. The canonical, every-rule version is [`SYNTAX-AND-USAGE.md`](../SYNTAX-AND-USAGE.md) at the repo root. This guide is the readable version: same content, less reference-doc density, more examples. ## Contents 1. [Mental model](#1-mental-model) 2. [Project layout](#2-project-layout) 3. [Types](#3-types) 4. [Blocks: the building units](#4-blocks-the-building-units) 5. [State files: `state`, `var`, `cstate`](#5-state-files-state-var-cstate) 6. [Views: `host` and `plyr`](#6-views-host-and-plyr) 7. [Listeners: `lstn`](#7-listeners-lstn) 8. [Components: `cpnt`](#8-components-cpnt) 9. [Datasets: `data`](#9-datasets-data) 10. [Styling: `css` and the `style` type](#10-styling-css-and-the-style-type) 11. [Actions](#11-actions) 12. [The function registries: `func` and `client`](#12-the-function-registries-func-and-client) 13. [Comments](#13-comments) --- ## 1. Mental model Banto is a small language for building **multiplayer party games**. One person is the **host** (the screen everyone watches) and several are **players** (their phones). Your job is to describe what each viewer should see, and what should happen when they tap things. Three ideas to keep in mind: 1. **The server is in charge.** Game state lives on the server, not in anyone's browser. When a player taps a button, their browser sends a token back to the server; the server runs the code; everyone affected gets a fresh view. 2. **You write what each viewer sees, not how to update it.** When the data changes, the runtime works out which views need to refresh and ships them only the data they actually use. 3. **No `if` you, no `for` you in views.** Views are declarative. Use multiple ordered blocks for "if/else" (first true wins) and `.map` over an array to build a list. The rest of this guide is the vocabulary for doing that. --- ## 2. Project layout A Banto project is a flat directory of `.banto` files plus a single CSS file: ```text my-game/ ├── globals.banto # required: shared declarations ├── start.banto # required: the entry state ├── question.banto # optional: more states, named however you want ├── results.banto ├── styles.banto.css # optional: CSS classes for your views └── banto.config.json # publish settings ``` Two rules to remember: - Every project has exactly one `globals.banto` and exactly one `start.banto`. The latter is where every game starts. - Every other `.banto` file is a **state**. The file name (without `.banto`) is the state's name and must match the `state` block declared inside it: `question.banto` contains `state question { … }`. State files are how you carve a game into "phases" — lobby, ask a question, show results, final scores. Each state file describes what the host and players see during that phase, and how the game moves on. --- ## 3. Types Banto uses TypeScript-style type expressions. If you've written a TypeScript interface, this will be familiar: ```text string // primitive number boolean object any // opts out of checking; assignable both ways unknown // accepts any value, but can't be used where a concrete type is required string[] // array of strings { name: string, score: number } // object — note the commas, not semicolons { [key: string]: number } // dictionary / record { name: string, nickname?: string } // optional field ``` The only syntactic surprise is **commas** between fields, where TypeScript would let you use semicolons. ### Banto-specific primitives Three extra types appear throughout the language; you'll get to know them as you read about the blocks that use them: | Type | What it represents | |-----------|--------------------| | `element` | Something renderable on screen — a primitive, a component, or a list of those. | | `style` | One or more CSS class references — a single `css.X` or a list of them. Used by component params that take styling. | | `action` | A block of side-effects (assignments, registry calls). The thing that runs when a button is clicked. | | `image` | A reference to a custom image asset (`asst.`). See [Custom image & audio assets](#custom-image--audio-assets). | | `audio` | A reference to a custom audio asset (`asst.`). | ### String-enum types Some component or function params restrict a string to a fixed set of values, written as a TypeScript-style union: ```text "tick" | "ding" | "whoosh" ``` They behave like normal strings, but the compiler rejects any literal that isn't in the list. You'll see these in registry signatures, and you can declare your **own** enums by naming a string-union with a [type alias](#type-aliases-and-enums) (`type Phase = "lobby" | "playing"`). Writing one *inline* in a block annotation isn't allowed — name it first. ### Type aliases and enums Declare a named type once in `globals.banto` with `type Name = …`, then use that name anywhere a type is expected. Great for a shape you repeat (a player record, a board cell) and the only way to declare a reusable enum: ```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" } ``` Reference an alias in a `var`/`cstate` `type`, a `state`/`cpnt`/`func` `params`, a `func` `returnType`, or nested inside another type. Aliases are **compile-time only** — the compiler expands each reference to its real structure before anything runs, so a `type` declaration never reaches the runtime. A few rules: aliases live in **`globals.banto` only**, names must be unique, a referenced name must be declared, and an alias can't refer to itself (no recursive types). Aliases may reference other aliases in any order. See [gotchas](./gotchas.md#type-aliases) for the error codes. ### Operators Values are combined with JavaScript-style operators. The full set: | Category | Operators | |---|---| | Arithmetic | `+` `-` `*` `/` `%` — `+` also concatenates when either side is a string | | Unary | `-x` (negate), `!x` (logical not) | | Comparison | `<` `>` `<=` `>=` | | Equality | `===` `!==` (strict) and `==` `!=` (loose) | | Logical | `&&` `\|\|` — return **one of the operands**, not a coerced boolean (see [§10 Styling](#10-styling-css-and-the-style-type)) | | Ternary | `cond ? a : b` — a value, usable anywhere a value is expected | > **Subtraction needs spaces.** Names can contain hyphens (`css.welcome-screen`), > so `var.a-var.b` reads as one name, not a subtraction. Write `var.a - var.b` > with spaces around the `-`. ### Built-in methods Values carry JavaScript-style methods based on their type — `"HI".toLowerCase()`, `var.scores.filter((s) => s > 0)`, `var.names.join(", ")`. Banto recognizes a **fixed** set (there's no general JS passthrough — an unlisted method is a compile error). The common ones: - **String**: `.length`, `.toUpperCase()`, `.toLowerCase()`, `.trim()`, `.slice()`, `.substring()`, `.replace()`, `.charAt()`, `.split()`, `.includes()`, `.startsWith()`, `.endsWith()`, `.indexOf()`. - **Array**: `.length`, `.map()`, `.filter()`, `.find()`, `.findIndex()`, `.some()`, `.every()`, `.forEach()`, `.includes()`, `.indexOf()`, `.slice()`, `.concat()`, `.sort()`, `.reverse()`, `.join()`, `.push()`, `.pop()`, `.shift()`. - **Number**: `.toString()`. - **Object**: `Object.keys()`, `Object.values()`, `Object.entries()`, `Object.fromEntries()`. The value-returning array iterators (`.map`, `.filter`, `.find`, `.findIndex`, `.some`, `.every`) need an **expression** callback (`(x) => x.foo`), not a statement block `{ … }`. Only `.forEach` takes a statement body. The full table with return types is in [`SYNTAX-AND-USAGE.md`](../SYNTAX-AND-USAGE.md#type-specific-class-accessors-operators-and-functions). --- ## 4. Blocks: the building units Everything you write at the top level of a `.banto` file is a **block**. A block has the shape: ```banto blockKind blockName { attribute1: value, attribute2: value } ``` Single-line is fine too: ```banto var hasStarted { type: boolean, default: false } ``` There are nine block kinds. They split into three groups: | Where they go | Block kinds | |----------------------|----------------------------------------------| | State files only | `state`, `host`, `plyr`, `lstn`, `cstate` | | `globals.banto` only | `data`, `cpnt`, `func` | | Both | `var` | `func` is dual-purpose: `func.X(...)` calls a built-in registry function, and you can also **declare** your own `func` blocks in `globals.banto` (see §12). `client` (the client-only registry) is the one namespace you write like a block but never declare yourself. The next sections walk through every block kind. --- ## 5. State files: `state`, `var`, `cstate` ### `state` Every state file has exactly one `state` block. Its name must match the file: ```banto // start.banto state start {} ``` If a state needs incoming data — usually because another state transitioned into it with `lstn.next` — declare a `params` type: ```banto // question.banto state question { params: { questionId: number } } ``` Inside this state file, anywhere you can use a value, you can read those params: `state.params.questionId`. ### `var` A `var` is **server-side mutable state**. It has a type and a default value: ```banto var hasStarted { type: boolean, default: false } var players { type: { [key: string]: { name: string, score: number } }, default: {} } ``` `var`s declared in `globals.banto` live for the whole game. `var`s declared in a state file live as long as that state is active — when the game transitions away and back, they reset to `default`. Read a var with `var.`. You can read vars from `host`, `plyr`, and `lstn` blocks. Mutate them inside an `action` (covered in [§11](#11-actions)). ### `cstate` — per-client state A `cstate` is **per-client, browser-only state**: each viewer has their own copy, the server never holds it, and it resets to `default` on every state transition. ```banto cstate draft { type: string, default: "" } ``` Read it with `cstate.` from `host` or `plyr` blocks. The textbook use case is a text input in progress: the typing-in-progress text lives in `cstate.draft` so it stays smooth and local; only when the player presses Enter does it travel to the server. See [Recipes → text input](./recipes.md#text-input-with-a-cstate-draft). The `default` is a static literal. To seed a `cstate` from **server state** instead — e.g. pre-fill a per-player draft from that player's `var` record — use an `init` block on the `host`/`plyr` view. See [§6 → Seeding `cstate` with `init`](#seeding-cstate-with-init). | | `var` | `cstate` | |---|---|---| | Lives where? | Server | Each viewer's browser | | Lives how long? | Across state transitions | Wiped on every transition | | Who can write it? | `host`, `plyr`, `lstn` (via actions) | `host`, `plyr` only | | Who can read it? | Anyone server-side | Only the client that owns it | --- ## 6. Views: `host` and `plyr` `host` and `plyr` blocks describe what's on screen during a state. They look identical: ```banto host default { condition: true, child: cpnt.HostScreen({ … }) } plyr default { condition: true, child: cpnt.PlayerScreen({ … }) } ``` Two required attributes, plus one optional: - **`condition`** — a boolean expression. If true, this block "wins". - **`child`** — what to render. An `element` value: a string, a component call (`cpnt.X(…)`), or a list of those. - **`init`** (optional) — a server-side initializer that seeds this viewer's `cstate` from server state. Covered in [§6 → Seeding `cstate` with `init`](#seeding-cstate-with-init). You can declare multiple `host` blocks and multiple `plyr` blocks per state. They're evaluated **top to bottom**; the first one whose `condition` is true is the one rendered. Like a chain of `else if`s. The last block of each kind **must** be named `default` with `condition: true`, so something always renders: ```banto plyr hasAnswered { condition: var.playerData.includes(player.sessionId), child: cpnt.WaitingScreen({ … }) } plyr default { condition: true, child: cpnt.QuestionScreen({ … }) } ``` ### The `player` value Inside a `plyr` block you have access to a special value, `player`, representing **the specific viewer this render is for**: ```text player.sessionId // string, unique per viewer player.name // string, the player's display name player.score // number, their current score ``` Each player gets their own evaluation of the `plyr` blocks, with `player` bound to their record. The host gets one evaluation of the `host` blocks, with no `player` value (don't reference it there). ### Building the `child` `child` is whatever you want on screen. The easy cases: ```banto child: "Hello!" // a string child: var.message // a var that holds a string child: cpnt.button({ child: "Tap me", onClick: () => {}, class: [] }) child: [cpnt.container({…}), "footer", cpnt.button({…})] // a list of elements ``` For larger UIs, lean on the component registry — see [§8 Components](#8-components-cpnt) and the [Registry reference](./registry-reference.md). ### Seeding `cstate` with `init` A `cstate` gets its starting value from its own `default`, but that default is a static literal — it can't look at game state. When the initial value of a viewer's client state depends on **server data** (their `var` record, `player`, `state.params`, …), use the optional `init` block on the `host`/`plyr` block: ```banto cstate board { type: { [key: string]: string }, default: {} } plyr default { condition: true, init: { cstate.board = var.playerBoards[player.sessionId]; }, child: cpnt.PlayerScreen({ … }) // reads cstate.board } ``` How it behaves: - **Runs on the server, once per snapshot.** `init` fires when a viewer first connects to this state or when the game transitions *into* this state — the same moment the view is first built. It is **skipped on incremental view updates** (when a `var` changes and the already-rendered view refreshes). - **Only its `cstate.X = …` writes matter.** `init` reads server state (`var`, `player`, `state.params`, `data`, `func.X(…)`) freely and writes the results into `cstate`. The viewer receives only the resulting `cstate` values — never the `var`/`data` you read along the way. It cannot write `var` (that's what `lstn.actions` are for). - **Overrides the `cstate` `default`.** Whatever `init` writes replaces the declared `default` for that render. Leave a `cstate` out of `init` and it keeps its `default`. Reach for `init` whenever a text input, drawing surface, or other client-local widget should *start* pre-filled from server state rather than blank. Without it you'd have no way to seed per-client state from per-player data, since a `cstate` `default` can't read `player`. --- ## 7. Listeners: `lstn` A `lstn` block watches the game state and **fires when its condition becomes true**. It does two things: - Run an `actions` block (mutate vars, call `func.X` side effects). - Optionally, **transition to another state** via `next`. ```banto lstn hasStarted { condition: var.started, actions: {}, next: { state: "question", inputs: { questionId: 0 } } } ``` | Attribute | Required? | What it is | |-------------|-----------|------------| | `condition` | yes | A boolean. The listener fires when this becomes true. | | `actions` | no | An `action` block — server-side side effects (assignments, registry calls). | | `next` | no | `{ state: "", inputs?: }`. The state name must exist; `inputs` must match that state's `params` type. | Like `host` and `plyr`, multiple `lstn` blocks are evaluated in declared order — the first one whose condition is true wins. They're the workhorse for "when X happens, do Y and move on". A real example from the trivia starter: ```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: { // ... grade the answers, update scores ... }, next: { state: "results" } } ``` --- ## 8. Components: `cpnt` A **component** is a renderable thing — a button, a container, a custom screen layout. Components come from two places: 1. **The component registry** — components shipped with Banto. Reference them with `cpnt.(args)`. 2. **Your own globals** — declare a custom component with a `cpnt` block in `globals.banto`. ### Calling a registry component Pass an object whose fields match the component's params: ```banto cpnt.button({ child: "Start the game", class: [css.big-button], onClick: () => { var.started = true; } }) ``` The full list of registry components is in the [Registry reference](./registry-reference.md). The most common ones to get started: - `cpnt.HostLobby` — default lobby screen with a kick list. - `cpnt.PlayerScreen` — wrapper for player views, shows score + room code. - `cpnt.button` — a button. - `cpnt.container` — a styleable wrapper. - `cpnt.textInput` — a text input bound to a `cstate`. - `cpnt.container` — a styleable wrapper; lay out its children with CSS flex classes. - `cpnt.image` / `cpnt.audio` — render your own uploaded media (see below). ### Declaring your own component Declare it in `globals.banto`: ```banto cpnt MyButton { params: string, child: cpnt.button({ child: params, class: [css.my-button], onClick: () => {} }) } ``` Two attributes: - `params` (optional) — the input type. Inside the body, read it as `params` (or `params.foo` for an object). - `child` (required) — an `element` value. Same rules as in `host`/`plyr` blocks. Use it like any registry component: ```banto cpnt.MyButton("Hello") ``` A component with object params: ```banto cpnt myCard { params: { title: string, body: string }, child: cpnt.container({ class: [css.card], child: [params.title, params.body] }) } ``` #### Reading game state, and forwarding actions A custom component's body can do more than wire up its own `params`: - **Read global `var`s and datasets.** A `cpnt` body may read any `var` declared in `globals.banto` (plus the inherent `roomCode` / `players` / `currentTime`) and `data.*`. It **cannot** read state-scoped vars, `state.params`, `cstate`, or `player`, and it never writes — pass those in via `params`. ```banto var roundScore { type: number, default: 0 } cpnt ScoreHud { params: { label: string }, child: cpnt.container({ class: [css.hud], child: params.label + ": " + var.roundScore }) } ``` - **Invoke an `action` param with a value.** A `params` field typed `action(T)` can be called from the component's own handler, forwarding a value to the call site: ```banto cpnt SubmitRow { params: { draft: string, onSubmit: action(string) }, child: cpnt.button({ child: "Submit", class: [css.submit], onClick: () => { params.onSubmit(params.draft); } }) } ``` The invoked handler is inlined at compile time and must do **server work only** (`var` writes, `func.*`); one that writes `cstate` or calls `client.*` is a compile error — keep that at the call site. - **Forward actions through nested `.map`s.** An `action` carried in a data field survives a `.map` into a nested custom cpnt, so grids/lists can be factored into reusable `Cell` / `Row` / `Grid` components. ### Lists of elements The `element` type accepts a single element or a list. Both are valid: ```banto child: cpnt.button(...) // single child: [cpnt.a(...), cpnt.b(...), "footer"] // list, rendered in order child: [] // empty list ``` That makes `[…].map(item => cpnt.X(item))` the natural way to render a dynamic list of components. ### Custom image & audio assets Games can ship their own images and audio. The flow has two halves — an upload step in the CLI and an `asst.` reference in your `.banto` files. **1. Upload.** Put media files in an `assets/` directory at your project root, give them titles in `banto.config.json`, then push: ```text my-game/ ├── assets/ │ ├── logo.png │ └── victory-fanfare.mp3 └── banto.config.json ``` ```jsonc // banto.config.json { "assets": { "logo": { "title": "Game logo" }, "victory-fanfare": { "title": "Victory fanfare" } } } ``` ```bash banto assets push ``` **2. Reference.** `asst.` is a namespace (like `css.`); `` is the file's base name (no extension). It resolves at build time to the asset's id — there's no runtime `asst` lookup. ```banto // an image cpnt.image({ src: asst.logo, alt: "Our logo", fit: "contain", class: [css.badge] }) // background audio cpnt.audio({ src: asst.victory-fanfare, autoplay: true }) // a one-shot cue fired from an action onClick: () => { client.playSound(asst.victory-fanfare); } ``` `asst.` carries a **kind** — `image` or `audio`. The compiler checks it: an `image` asset in an `audio` slot (or vice versa) is a type error. A plain string is also accepted anywhere an `image`/`audio` is expected (a raw asset id or URL escape hatch), but it's resolved at runtime and renders nothing if it can't load. > **Moderation.** Freshly-pushed assets are **pending** — usable in > your own games immediately, but only listable to others once an admin > approves them. The editor lists your asset names for completion even > before you push; the build is what enforces that a referenced asset > actually exists. See the [CLI reference](./cli.md#banto-assets) for > `banto assets ls` / `rm` / `catalog`. --- ## 9. Datasets: `data` A `data` block declares a **dataset that the host picks at game start**. The most common case: a question list for a trivia game. `data` blocks are declared in `globals.banto` only. Exactly **three names** are allowed: `questionSet`, `promptSet`, or `dynamic`. ```banto data questionSet data promptSet data dynamic ``` You can have at most one `data` block in a project. The shape is fixed by Banto: | Block | Type | |------------------------|----------------------------------------------------------------------------| | `data questionSet` | `{ prompt: string, options: string[], correctOptions: number[] }[]` | | `data promptSet` | `string[]` | | `data dynamic` | `(string \| number \| (string \| number \| (string \| number)[])[])[]` | Read it like a var: `data.questionSet`, `data.promptSet`, `data.dynamic`. The `dynamic` block is the general-purpose escape hatch: at any position, of any list, at any depth, a value is a string, a number, or another list. Lists may nest to any depth (type `Dynamic[]` where `Dynamic = string | number | Dynamic[]`). When you publish, `banto.config.json` determines what real data source the host sees pre-selected — see the [CLI reference's `banto.config.json` section](./cli.md#bantoconfigjson-per-project). --- ## 10. Styling: `css` and the `style` type CSS classes live in `styles.banto.css`. Restrictions: - **Class selectors only** (`.my-class`) and pseudo-classes (`.my-class:hover`). No element selectors, no `@import`, no `@font-face`, no `@keyframes`. - The compiler will reject anything outside that subset. ```css .host-column { display: flex; flex-direction: column; gap: 12px; } .option-button:hover { transform: translateY(-2px); } ``` Reference a class from a `.banto` file as `css.`. The `style` type is a **list** of class references — but a `style` slot also accepts a single reference as shorthand for a one-item list: ```banto class: css.host-column // one class (shorthand) class: [css.host-column] // one class (list form) class: [css.host-column, css.with-shadow] // multiple class: [] // none ``` Use whichever reads best. The single-value form is handy for the common case of one class; reach for `[ ]` when you have several or want to mix in conditional entries (see below). ### Conditional classes with `&&` `&&` and `||` follow JavaScript semantics — they return one of their operands. The runtime drops non-string entries from `style` arrays before joining, so this just works: ```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 array entry becomes `false`, gets dropped, and the final class string only contains the survivors. The same applies to the single-value form — `class: var.isCorrect && css.correct` applies the class only when the guard is truthy. --- ## 11. Actions An **action** is a block of server-side side effects: changing var values, calling registry functions that have side effects. Anywhere the type `action` is expected, you can supply one. ### Action statements Inside an action, statements end with a **semicolon**. The available statements: - **Assignment**: `var.score = 0;` and the compound forms `+= -= *= /= %=`. - **Mutating method calls**: `var.players.push(p);`. - **Function calls** that return `action`: `func.kickPlayer(sId);`. - **Iterators** for side effects: `var.players.forEach((p) => { … });`. - **`if` / `else if` / `else`** — same syntax as JavaScript. - **`return;`** inside a `.forEach` body — works like `continue`, skipping to the next iteration. ```banto actions: { var.players[player.sessionId].score += 100; if (var.players[player.sessionId].score >= 1000) { var.winners.push(player.sessionId); } Object.entries(var.players).forEach(([sessionId, p]) => { if (p.score < 0) { return; } var.scoreBoard[sessionId] = p.score; }); } ``` > **No `for` or `while` loops.** All looping goes through array methods — > `.map` (when you want a new value back), `.forEach` (when you just > want side effects). ### Action values: `() => { … }` Anywhere a slot is typed `action`, you can write an arrow function whose body is an action block. This is how event handlers are wired up: ```banto cpnt.button({ child: "Start", onClick: () => { var.hasStarted = true; } }) ``` The arrow runs **on the server, when the user emits the event**. From inside it, you have full access to `var`, `state`, `data`, registry functions — the same things `lstn` actions can touch. ### Events that carry data: `action(T)` Some events deliver a value (a text input sends what was typed; a slider sends its new position). Those slots are typed `action(T)` — declare an arrow with one parameter to receive the value: ```banto cpnt.textInput({ value: cstate.draft, onChange: (next) => { cstate.draft = next; }, onSubmit: (text) => { var.messages.push(text); cstate.draft = ""; } }) ``` Rules: - A plain `action` slot accepts only zero-arg arrows. - An `action(T)` slot accepts zero- or one-arg arrows. - Two or more parameters is always an error. ### Closures: what's captured vs. what's live When you write an arrow inside a `.map` (for example, building a button per player), the arrow's body can refer to two kinds of names: | What | When is it read? | |---|---| | Iteration locals (`item`, `i`, destructured names) and `player` | **Captured by value at render time.** The action remembers which iteration produced it. | | `var.*`, `state.params`, `data.*`, registry calls | **Read live at click time.** The action sees the latest server state. | This means the player-mapping pattern below works correctly: ```banto items: data.questionSet[state.params.questionIndex].options.map((option, i) => cpnt.button({ child: option, class: [], onClick: () => { // `i` is captured: this button's onClick always uses ITS i. // `player.sessionId` is captured: it's this player's id. // `var.playerData` is live: we read the latest snapshot. var.playerData[player.sessionId] = { selection: i, selectionTime: func.currentTime() }; } }) ) ``` There's no "last-i" footgun here. Each button captures its own `i`. --- ## 12. The function registries: `func` and `client` Banto ships two read-only registries of utility functions. ### `func.X` — server-side Anything under `func.` runs on the server, in the same place as your action bodies. Grouped: - **Time**: `func.currentTime()`. - **RNG**: `func.randomInt({min, max})`, `func.randomFloat({min, max})`, `func.shuffle(arr)`, `func.pickRandom(arr)`, `func.pickUnused({total, used})`. - **Math**: `func.floor`, `func.ceil`, `func.round`, `func.abs`, `func.clamp`, `func.sum`, `func.avg`, `func.minOf`, `func.maxOf`. - **Collections**: `func.unique`, `func.first`, `func.last`, `func.size`, `func.isEmpty`, `func.hasKey`, `func.merge`, `func.removeKey`, `func.mapValues`, `func.groupBy`, `func.range`, `func.equals`. - **Game control** (`returns: action`): `func.kickPlayer(sessionId)`. - **UX side effects**: `func.notify({to, message, type?})`, `func.log({message, level?})`. A function whose return type is `action` can appear as a **statement** in an action block; the others are just expressions. The full catalog is in the [Registry reference](./registry-reference.md). ### Declaring your own functions Beyond the built-in registry you can define **reusable functions** in `globals.banto` and call them the same way — `func.(...)`. They're pure helpers: they take a single argument, compute, and `return` a value for an action body (or a view) to use. ```banto func scoreFor { params: { correct: boolean, streak: number }, returnType: number, action: { let base = params.correct ? 100 : 0; return base + params.streak * 10; } } ``` ```banto // In a listener or action body: var.players[player.sessionId].score += func.scoreFor({ correct: true, streak: var.streak }); ``` Declaration shape: - `params` — the type of the single argument, referenced inside the body as `params.` (or bare `params` when the type isn't an object). Omit for a no-argument function. - `returnType` — the type returned. **Optional**: omit it for a `void` helper that just computes side-effect-free intermediate values. When set, every path through the body must `return` a value of this type. - `action` — the body. It behaves like an action block: `let` bindings are allowed, and it may call other registry functions (`func.X`). The rules that keep them predictable: - **Params are read-only.** You can't assign to `params.x` or call a mutating method on it — build a new value and `return` it. - **Pure over `params`.** A `func` body can't read game state (`var`, `cstate`, `state`, `data`, `player`) or call `client.X`. Pass whatever it needs in as an argument. This is what lets the same function run on the server *and* the browser. - **No recursion.** A function may not call itself, directly or through another function. Functions run wherever they're needed: server-side in action bodies and listeners, and client-side when referenced from a view or a client-side cstate write. A function reached only from the client is shipped to the browser automatically. ### `client.X` — browser-side `client.X(...)` runs on the browser of the player who triggered the action — useful for UI sound effects and other purely-presentational things the server has no analogue for. ```banto cpnt.button({ child: "Buzz in!", class: [], onClick: () => { client.playSound("ding"); var.buzzedPlayers.push(player.sessionId); } }) ``` Two restrictions: - `client.X(...)` may **only** appear inside an action arrow body. - `client.X(...)` may **not** appear inside an iterator body (`.map`, `.forEach`). The current registry has one function — `client.playSound(sound)` — where `sound` is one of the built-in cue names (`"tick"`, `"ding"`, `"whoosh"`, `"chching"`, `"applause"`, `"countdown"`, `"anticipation"`, `"waiting"`) **or** an `audio` asset (`asst.`) to play one of your own uploaded clips. --- ## 13. Comments Single-line `//` comments only — same as JavaScript. Multi-line `/* … */` comments are not allowed. ```banto // This var holds whether the host has hit Start yet. var hasStarted { type: boolean, default: false } ``` --- ## Where to go next - **[Recipes](./recipes.md)** — the patterns you'll keep reaching for: text inputs, kicks, scoring, conditional UI, state transitions. - **[Registry reference](./registry-reference.md)** — every component and function Banto ships with. --- # CLI reference `banto` is the only command you need to install. It scaffolds a project, compiles your `.banto` files, formats them, manages your image/audio assets, and publishes your game to [banto.tv](https://banto.tv). If you've never used it, start with [Getting started](./getting-started.md); this doc is the keep-it-open-while-you-work reference. ## Install ```bash npm install -g @bantohq/cli banto --help ``` You should see the subcommands: `init`, `build`, `fmt`, `auth`, `publish`, and `assets`. You can also install per-project (`npm install --save-dev @bantohq/cli`) and invoke as `npx banto …`. > **Get the editor extension too.** The **Banto DSL** VS Code extension > gives you syntax highlighting, inline diagnostics, and completions as > you type — it's the fastest way to catch mistakes before you build. > See [Getting started → the extension](./getting-started.md#2-install-the-vs-code-extension). ## The workflow at a glance Your game runs on banto.tv, but you can test it without real phones using the **sandbox**. The loop is: 1. **Write** `.banto` files (the editor extension flags errors live). 2. **`banto build`** to typecheck and catch anything the editor missed. 3. **`banto publish`** — publish as a *private* game (`"public": false`) while you iterate. 4. **`banto sandbox`** — open the owner-only sandbox: the host screen plus simulated player windows in one browser, no phones needed. 5. Edit, re-`publish` (it updates in place), reload the sandbox, repeat. Host it live on [banto.tv](https://banto.tv) from a few phones/tabs for a real multi-device round, and flip `"public": true` to list it. ## Subcommands ### `banto init ` Scaffolds a new project at `./`. The directory must not already exist (or must be empty). The starter is the **empty lobby** template: host shows the room code with a kick list, players see a "you're in" message. ```bash banto init my-game ``` Files created: ```text my-game/ ├── globals.banto ├── start.banto ├── styles.banto.css ├── banto.config.json └── .gitignore ``` ### `banto build` Compiles the project to a single `game.json` IR file at the project root. Run it from anywhere inside the project — the CLI walks up until it finds `globals.banto`. ```bash banto build # quiet banto build --verbose # print per-file progress ``` Output on success: ```text Wrote ./game.json (487 bytes, 0 error(s), 0 warning(s)). ``` On a syntax/type error, you get the same diagnostics your editor shows, then a non-zero exit code: ```text start.banto:14:17 error [banto.cstate-write-interleaved]: … Compilation aborted: 1 error(s), 0 warning(s). ``` `game.json` is a build artifact — keep it out of version control (the starter's `.gitignore` already does this). `banto publish` builds for you, so you rarely need to run `build` by hand except to verify a change compiles. ### `banto fmt [path]` Formats `.banto` files in place using the canonical formatter — the same output as *Format Document* in the editor. ```bash banto fmt # format every .banto file under the current dir banto fmt start.banto # format a single file banto fmt --check # report unformatted files, exit 1 if any (CI-friendly) ``` | Option | What it does | |-----------------|--------------| | `-c`, `--check` | Don't rewrite anything — just list files that aren't formatted and exit `1` if there are any. | `path` may be a file or a directory (defaults to `.`). ### `banto auth` Logs you into [banto.tv](https://banto.tv) using a browser-based loopback flow: 1. The CLI starts a tiny local web server. 2. It opens your browser to a banto.tv consent page. 3. You approve, banto.tv redirects to `localhost`, and the CLI captures the token. ```bash banto auth ``` You only need to run this explicitly the first time. `banto publish` and `banto assets` will trigger the same flow automatically if your token is missing or expired. ### `banto publish` Compiles your project and ships it to banto.tv. The settings in `banto.config.json` (title, description, default datasets) become your game's listing. ```bash banto publish ``` Re-running `banto publish` after edits **updates the existing game in place** — same ID, no duplicate listing. The CLI uses `banto.lock.json` (sibling of `banto.config.json`) to remember which remote game your project is bound to. Publish with `"public": false` while you iterate — the game is reachable only by you (and anyone you send a direct link) so you can host and playtest it privately. Set `"public": true` to list it in the public catalog. > Publishing is **admin-gated** on banto.tv by default. If you get a > 403, your account doesn't have publishing permissions. ### `banto sandbox` Opens the **sandbox** for your currently-published game in the browser — an owner-only test harness that runs the real game with the host screen and simulated player windows side by side, so you can play a full round without real phones. ```bash banto sandbox # default layout (host + 2 player windows) banto sandbox -t 3 # a different layout preset ``` - `-t, --template <0-3>` picks a layout preset (how many player windows to tile); defaults to `2`. Use **+ Add player** in the UI to add or drop players live. - The sandbox opens the **published** build, so run `banto publish` first (and again after each change, then reload). - It's **owner-gated** — the browser must be signed in to banto.tv as the project's owner (the account from `banto auth`). - Click the **Inspector** button (top-right) for a live view of every `var.*` and state-param, the datasets, each window's `cstate`, and a **Logs** panel showing your `func.log(...)` output and any runtime errors. ### `banto assets` Manage the custom image/audio assets your game renders with `cpnt.image`, `cpnt.audio`, or `client.playSound`. Drop media files in an `assets/` directory at your project root; reference them from `.banto` as `asst.` (the file's base name, no extension). See [Custom image & audio assets](./language-guide.md#custom-image--audio-assets) for the language side. ```bash banto assets push # upload new/changed files from ./assets banto assets ls # list this project's assets + moderation state banto assets rm # delete an owned asset (by base name or id) banto assets catalog # open the public asset catalog in your browser ``` | Subcommand | What it does | |--------------------|--------------| | `push [dir]` | Uploads new or changed files from `./assets` (or `dir`) to your account, records their ids in `banto.lock.json`, and reconciles moderation status. | | `ls` | Lists each asset with its kind, moderation status, and id. | | `rm ` | Deletes one of your assets from the server and the lock file. | | `catalog` | Opens the shared, admin-approved asset catalog on banto.tv. | Titles come from an `assets` map in `banto.config.json`, keyed by the file's base name: ```json { "assets": { "logo": { "title": "Game logo" }, "victory-fanfare": { "title": "Victory fanfare" } } } ``` > **Moderation.** Uploaded assets start **pending** and are visible > only in your own games until an admin approves them. `asst.` > references still build and render for you while pending — approval > just makes the asset usable by others and listable in the catalog. ## Config files ### `banto.config.json` (per project) How your game appears on banto.tv. Lives next to `globals.banto`. ```json { "title": "Grant's Trivia Tournament", "description": "It's like Trivia, but as a tournament!", "public": false, "default": { "questionSet": "this" }, "new": { "questionSet": [ { "prompt": "Which of these are programming languages?", "options": ["Python", "HTML", "JavaScript", "HTTP"], "correctOptions": [0, 2] } ] }, "assets": { "logo": { "title": "Game logo" } } } ``` | Field | What it does | |---------------|--------------| | `title` | Display name on the banto.tv listing. | | `description` | Short blurb on the listing page. | | `public` | `false` = only you and people with a direct link can find it. `true` = listed in the public catalog. | | `default.*` | For each `data` block your project declares (`questionSet`, `promptSet`, or `dynamic`), the data source pre-selected when a host starts a game. Use a banto.tv data-source ID, or the literal `"this"` to point at the matching `new.*` entry. | | `new.*` | Inline contents of a fresh data source to create on banto.tv as part of publishing. Shape matches the `data` block: questions for `questionSet`, strings for `promptSet`, or a `(string \| number \| (string \| number \| (string \| number)[])[])[]` list for `dynamic`. | | `assets.*` | Titles for your custom assets, keyed by the media file's base name. Used by `banto assets push`. | ### Where credentials live `banto auth` saves your bearer token outside the project, scoped to your OS user: | OS | Path | |-----------------|---------------------------------| | Linux / macOS | `~/.config/banto/` | | Windows | `%APPDATA%\banto\` | The folder holds `credentials.json` — your bearer token. Treat it like a password. You don't normally need to edit it by hand; `banto auth` writes it for you. `banto.lock.json` (next to `banto.config.json`, in your project) is different: it remembers which remote game and assets this project is bound to. Commit it — it's how re-publishing updates in place instead of creating a duplicate. ## Troubleshooting | Symptom | Fix | |---|---| | `banto build` says "no `globals.banto` found in this directory or any parent" | You're not inside a Banto project. `cd` into one or run `banto init `. | | `banto build` reports `unknown-asset` | You referenced `asst.` for a file you haven't pushed yet. Run `banto assets push`, or check the base name matches a file in `assets/`. | | `banto publish` returns 403 | Your account isn't allowed to publish. Publishing is admin-gated by default. | | `banto init` says "target is not empty" | Pick a different name, or move the existing files out. There's no `--force` flag. | | The browser-opened consent page errors out | Re-run `banto auth`. The loopback listener uses a random port and the previous attempt's port may have been blocked. | --- # 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.`. 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.` 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]: }` 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 }); ``` --- # Registry reference A quick catalog of the components and functions Banto ships with. This doc is hand-curated (categories + tightened descriptions) but **drift-checked** against the canonical registries in `packages/core/src/registries/` by `npm run docs:check` — every item in those JSON files must appear here and vice versa. When in doubt, the JSON files are the source of truth. ## Contents - [Components: `cpnt.X`](#components-cpntx) - [Server functions: `func.X`](#server-functions-funcx) - [Client functions: `client.X`](#client-functions-clientx) - [`Object` built-ins](#object-built-ins) --- ## Components: `cpnt.X` Used inside `host`/`plyr`/`cpnt` blocks anywhere a `child` (or nested `element`) is expected. Pass an object whose fields match the component's `params`. Optional fields are marked `?` and may be omitted. ### Layout & screens | Component | Params | What it is | |------------------|---------------------------------------------------------------------------------------------------------|------------| | `HostLobby` | `{ onStart: action, roomCode: string, playerButtons: { name: string, sessionId: string, onClick: action(string) }[] }` | The default host lobby. Purple background, room code, player list with kick buttons. | | `PlayerScreen` | `{ name: string, score: number, roomCode: string, child: element }` | Wrapper for player views — shows the player's name, score, and room code along with the inner `child`. | | `HostScreen` | `{ roomCode: string, onSkip: action, sideBar: { label: string, child: element }[], child: element }` | Wrapper for host views — main area + side bar. Default white background. | | `container` | `{ child?: element, class: style }` | A styleable wrapper. The Banto equivalent of `
`. Omit `child` for a purely decorative element. | | `Icon` | `{ id: string, class?: style }` | A [lucide](https://lucide.dev/icons) icon rendered by name. `id` accepts a kebab-case id (`alarm-clock`) or PascalCase name (`AlarmClock`); an unknown name renders nothing. Style the SVG via `class` (size with `w-8 h-8`, color with `text-*`). | | `conditional` | `{ when: boolean, child: element }[]` | Renders the first entry whose `when` is true. Useful for picking a child based on data. | | `portal` | `{ child: element }` | Renders `child` into a React portal on `document.body`, so it escapes parent layout / overflow / stacking context. Use for overlays (toasts, modals, floating menus). | | `modal` | `{ isOpen: boolean, onClose: action, child: element }` | A full-screen dimmed overlay centering `child` while `isOpen` is true. Clicking the backdrop (outside `child`) fires `onClose`. The child owns its styling; the modal only supplies the centered, dimmed overlay. | | `cursorLayer` | `{ cursors: { id: string, x: number, y: number, color: string }[], onMove: action({ x: number, y: number }) }` | A full-bleed overlay rendering one colored cursor per `cursors` entry and reporting this client's pointer movement via `onMove`. Positions are percentages (0–100) of the layer's box, so cursors stay aligned across differently-sized viewports. Drive shared-cursor experiences by writing each `onMove` into `var.*` keyed by session id. | ### Buttons & inputs | Component | Params | What it is | |-------------------|------------------------------------------------------------------------------------------------------------------------------------------------|------------| | `button` | `{ child: element, onClick: action, class: style, keyboardNav?: boolean }` | A clickable element. `keyboardNav` defaults to `true`; set to `false` to opt out of keyboard focus (renders `tabIndex={-1}` and suppresses focus-on-mousedown). | | `textInput` | `{ value: string, onChange?: action(string), onSubmit?: action(string), hint?: string, maxLength?: number, class?: style }` | Single-line text input. Bind `value` to a `cstate.` so typing stays local. | | `numberInput` | `{ value: number, onChange?: action(number), onSubmit?: action(number), hint?: string, min?: number, max?: number, step?: number, class?: style }` | Single-line numeric input. Empty input delivers `0`. | | `textArea` | `{ value: string, onChange?: action(string), onSubmit?: action(string), hint?: string, maxLength?: number, rows?: number, class?: style }` | Multi-line text input. `onSubmit` fires on Ctrl/Cmd+Enter; Enter alone inserts a newline. | | `select` | `{ value: string, options: { label: string, value: string }[], onChange?: action(string), placeholder?: string, class?: style }` | Dropdown. | | `checkbox` | `{ checked: boolean, label?: string, onChange?: action(boolean), class?: style }` | Single boolean checkbox. | | `checkboxGroup` | `{ values: string[], options: { label: string, value: string }[], onChange?: action(string[]), class?: style }` | Multi-select checkboxes. `onChange` receives the full new selection list. | | `radioGroup` | `{ value: string, options: { label: string, value: string }[], onChange?: action(string), class?: style }` | Single-select radios. | | `slider` | `{ value: number, min: number, max: number, step?: number, onChange?: action(number), onSubmit?: action(number), class?: style }` | Range slider. `onChange` ticks during drag; `onSubmit` fires on release. | | `keyboard` | `{ value: string, onChange?: action(string), onSubmit?: action(string), maxLength?: number, showNumbers?: boolean, showSpace?: boolean }` | Bottom-pinned QWERTY keyboard. A–Z plus ENTER (bottom-left) and backspace (bottom-right); optional 0–9 row when `showNumbers` is true; optional spacebar row when `showSpace` is true (also enables physical space key). Doesn't render the value itself — bind `value` to a `cstate.`. `onChange` fires on every key with the new full string; `onSubmit` fires on ENTER. Caps at ~30% viewport width on desktop, full width on mobile. | ### Drawing | Component | Params | What it is | |-----------|--------|------------| | `Canvas` | `{ mode: "draw" | "view", value: { lines: { points: number[], color: string, width: number }[], bg: string, w: number, h: number }, onChange?: action({ lines: { points: number[], color: string, width: number }[], bg: string, w: number, h: number }), colors?: string[], class?: style }` | A react-konva drawing surface. In `mode: "draw"` it shows a toolbar (color palette, pencil/bucket, stroke-width slider, undo, clear) and fires `onChange` with the full new sketch after every stroke or toolbar action. In `mode: "view"` it renders the saved sketch read-only, scaled to fit. Each line stores Konva-format flat `points: [x0,y0,x1,y1,...]`, a stroke `color`, and a stroke `width`; `bg` is the background fill; `w`/`h` are the original surface dimensions used to scale on view. Bind `value` to a `var.*` slot and write the action's argument back to it in `onChange`. `colors` overrides the toolbar palette (defaults to a 10-color set). | ### 3D | Component | Params | What it is | |-----------|--------|------------| | `Scene3D` | `{ grid: { [key: string]: number }, palette: { color: string, top?: string, opacity?: number, texture?: string, shape?: string }[], size?: { x: number, y: number, z: number }, entities?: { id: string, kind: string, pos: { x: number, y: number, z: number }, color?: string, sprite?: string, label?: string, scale?: number }[], camera?: { mode?: "orbit" | "follow" | "iso" | "fixed", target?: string, distance?: number, pitch?: number, pan?: boolean, resetKey?: number }, controls?: "none" | "tap" | "dpad" | "joystick", hover?: "none" | "block" | "adjacent", onBlockClick?: action({ block: string, adjacent: string, face: string }), onEntityClick?: action(string), onMove?: action("north" | "south" | "east" | "west"), class?: style }` | A WebGL voxel scene (three.js), Y-up and grid-aligned. `grid` is a sparse `"x,y,z"` → palette-index map (absent key = air); `palette` entries give a block's `color`, an optional `top` face tint, `opacity`, an optional procedural pixel-art `texture` derived from the entry's colors — one of `"speckle"` (dirt), `"grass"`, `"planks"` (wood/fence), `"bricks"`, `"checker"`, `"waves"` (water), `"stone"`; no image assets needed, other values render untextured — and an optional `shape`: `"fence"` renders a skinny post with rails that auto-connect to adjacent cells of the same entry (clicks/hovers still resolve to the cell), anything else is a full cube. `entities` are non-block actors (`kind` one of `"box"` — a colored box, `"sprite"` — camera-facing emoji/text, `"label"` — a floating pill; `texture`, `shape`, and `kind` are plain `string` so `func.*` helpers can build these lists) whose position changes animate smoothly; an optional `label` on box/sprite entities renders a name tag. `camera` picks a rig: `orbit` (drag-rotate, default), `follow` (tracks the entity with id `target`), `iso`, `fixed`; `pan: true` (orbit only) adds standard free navigation — right/middle-drag or Shift+drag pans (two-finger drag on touch), the wheel zooms toward the pointer, and the focus is clamped to the grid bounds; bump `resetKey` (a counter cstate) to snap back to the declared framing. `controls` renders built-in touch input: `dpad`/`joystick` fire `onMove` with `"north"`/`"south"`/`"east"`/`"west"` (camera-relative; north is −z, east is +x). `hover` outlines the pointer-targeted cell when `onBlockClick` is wired — `"block"` marks the hovered block (edit/harvest UIs), `"adjacent"` marks the placement cell next to the hovered face (build UIs). `onBlockClick` delivers the clicked cell (`block`), the empty cell adjacent to the clicked face (`adjacent` — where a placed block goes), and the `face` name. Pair `onMove` with `func.gridStep` for collision-checked movement. Keep grids modest (a few thousand filled cells) — the whole map rides each update. | ### Media (custom assets) Render your own uploaded images and audio. `src` takes an `asst.` reference (build-validated to the right kind) or a raw asset id / URL string. See [Custom image & audio assets](./language-guide.md#custom-image--audio-assets) for the upload flow (`banto assets push`). | Component | Params | What it is | |-----------|--------|------------| | `image` | `{ src: image, alt?: string, fit?: "cover" | "contain", class?: style }` | Renders a custom image asset. `fit` maps to CSS `object-fit` (`cover` crops to fill, `contain` letterboxes; default `cover`). `alt` is the accessible label. Size it via `class`. A raw string `src` is resolved at runtime and renders nothing if it can't load. | | `audio` | `{ src: audio, autoplay?: boolean, loop?: boolean, controls?: boolean, class?: style }` | Plays a custom audio asset. Set `controls` to show a native player; omit it for background/ambient audio driven by `autoplay`/`loop`. For a one-shot sound cue fired from an action, use `client.playSound(asst.)` instead. | ### Animation | Component | Params | What it is | |---------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------| | `AnimatedComponent` | `{ child: element, class: style, onStart?: action, onComplete?: action, delay: number, transition: { type?: "spring" | "tween", stiffness?: number, damping?: number, duration?: number }, animate: { scale?: number, rotate?: number, opacity?: number, x?: number, y?: number }, initial: { scale?: number, rotate?: number, opacity?: number, x?: number, y?: number } }` | A framer-motion-style entrance animation wrapper. `initial` is the start state; `animate` is the resting state; `transition` controls timing. | > **Heads up.** Components, params, and slot types evolve. The > registry JSON in `packages/core/src/registries/components.json` is > the authoritative list. If something here looks stale, that file > wins. --- ## Server functions: `func.X` Run on the server. Use as expressions everywhere except where noted (`returns: action` functions can also be statements in an action body; `returns: void` functions are statement-only). ### Time & RNG | Call | Returns | Notes | |---|---|---| | `func.currentTime()` | `number` | Milliseconds since Unix epoch. | | `func.randomInt({min, max})` | `number` | Random int in `[min, max]`. Bounds swap if reversed; non-integer bounds are floored. | | `func.randomFloat({min, max})` | `number` | Random float in `[min, max)`. | | `func.shuffle(arr)` | `any[]` | Non-mutating; returns the array in random order. Works on any element type. | | `func.shuffleStrings(arr)` | `string[]` | Deprecated — prefer `func.shuffle`. | | `func.pickRandom(arr)` | `any` | One random element. Returns `undefined` for an empty list. | | `func.pickUnused({total, used})` | `number` | Random int in `[0, total)` not in `used`. Returns `-1` if all used. | | `func.range({start, end, step?})` | `number[]` | Inclusive of `start`, exclusive of `end`. `step` defaults to 1; negative counts down. | ### Math | Call | Returns | Notes | |---|---|---| | `func.floor(n)` | `number` | Equivalent to `Math.floor`. | | `func.ceil(n)` | `number` | Equivalent to `Math.ceil`. | | `func.round(n)` | `number` | Half-up. Equivalent to `Math.round`. | | `func.abs(n)` | `number` | Absolute value. | | `func.clamp({value, min, max})` | `number` | Constrain to `[min, max]`. Bounds swap if reversed. | | `func.sum(arr)` | `number` | Empty → 0. | | `func.avg(arr)` | `number` | Empty → 0 (not NaN, so it renders cleanly). | | `func.minOf(arr)` | `number` | Empty → 0. | | `func.maxOf(arr)` | `number` | Empty → 0. | | `func.toInt(value)` | `number` | Coerces to int; `-1` on failure. | ### Strings | Call | Returns | Notes | |---|---|---| | `func.isAlpha(s)` | `boolean` | True for non-empty ASCII letter strings (A–Z, a–z). Empty / mixed → false. | | `func.isNumeric(s)` | `boolean` | True for non-empty ASCII digit strings (0–9). No signs, decimals, or whitespace. Pair with `func.toInt` for the value. | ### Collections | Call | Returns | Notes | |---|---|---| | `func.size(obj)` | `number` | Sugar over `Object.keys(obj).length`. | | `func.isEmpty(any)` | `boolean` | True for `null`, `undefined`, `""`, `[]`, `{}`. | | `func.hasKey({obj, key})` | `boolean` | True if `obj` has its own property `key`. | | `func.first(arr)` | `any` | First element or `undefined`. | | `func.last(arr)` | `any` | Last element or `undefined`. | | `func.unique(arr)` | `any[]` | First-occurrence dedup, `===` equality (no deep dedup). | | `func.setHas({items, value})` | `boolean` | Set-backed membership check. Builds a `Set` from `items` then checks `value`; O(1) lookup, cheaper than `items.includes(value)` for large lists. `===` equality. | | `func.equals({a, b})` | `boolean` | Deep structural equality. | | `func.merge({a, b})` | `object` | Shallow `{...a, ...b}`. Non-mutating. | | `func.removeKey({obj, key})` | `object` | Copy of `obj` without `key`. Non-mutating. The cleanest way to drop an entry from a record. | | `func.mapValues({obj, key})` | `object` | Project a property out of every value: `{[k]: obj[k][key]}`. | | `func.groupBy({items, key})` | `object` | Group `items` into a record keyed by `item[key]`. Missing keys grouped under `""`. | | `func.fill({value, n})` | `any[]` | Length-`n` array where every entry is `value`. `n ≤ 0` or non-integer → `[]`. Pad a list to a fixed length: `pd.guesses.concat(func.fill({value: emptyRow, n: 6})).slice(0, 6)`. The same reference is repeated — mutating one entry mutates all. | | `func.sortBy({list, key?, desc?})` | `any[]` | Stable non-mutating sort. With `key`, orders by that property (leaderboards: `func.sortBy({list: Object.values(var.allPlayers), key: "coins", desc: true})`); without, compares elements directly. Numbers numeric, strings lexicographic; incomparable values last. | ### 3D grids (for `cpnt.Scene3D`) | Call | Returns | Notes | |---|---|---| | `func.voxKey({x, y, z})` | `string` | Build a grid cell key: `{x:1,y:0,z:2}` → `"1,0,2"`. Coordinates truncate to integers. | | `func.voxParse(key)` | `{ x, y, z }` | Inverse of `voxKey`. Malformed components parse to 0. | | `func.voxFill({from, to, value, merge?})` | `{ [key]: number }` | Grid map filling the inclusive cuboid between `from` and `to` (either corner order) with palette index `value` — platforms, walls, floors without nested `.map` loops. `merge` entries overlay the fill (they win), so layers compose while keeping the map type. Fill regions over 100k cells contribute nothing. | | `func.gridStep({grid, from, dir, climb?, gravity?})` | `{ x, y, z }` | Collision-checked single-cell step (Y-up; north is −z, east is +x). Blocked destination → returns `from` unchanged. `climb: true` steps up 1-block ledges; `gravity: true` then drops onto the nearest support (or y=0). Wire directly to `Scene3D`'s `onMove`. | | `func.enclosure({grid, from, walls, min, max, y?})` | `{ x, z }[]` | Flood-fill enclosure test on the x/z plane: walks 4-directionally from `from`, blocked by cells whose grid value at layer `y` (default 0) is in `walls`. Returns every open cell of the region when it's fully walled in; `[]` when the start is a wall/out of bounds or the region leaks past `min`..`max`. Non-empty ⇒ enclosed; `.length` is the pen's area. Fenced pens, sealed rooms, territory claims. | ### Game control & UX | Call | Returns | Notes | |---|---|---| | `func.kickPlayer(sessionId)` | `action` | Disconnects the player and removes them from `var.players`. Statement-callable from action bodies. | | `func.notify({to, message, type?})` | `void` | Pop a toast. `to` ∈ `"host" | "all" | `; `type` ∈ `"error" | "success" | "standard"` (default `"standard"`). Fire-and-forget — unknown sessionIds drop silently. | | `func.log({message, level?})` | `void` | Write to the server log. `level` defaults to `"info"`. Handy for debugging your game's server logic. | > **About the canonical list.** `packages/core/src/registries/functions.json` > is the single source of truth. New functions are added there. --- ## Client functions: `client.X` Run on the player's browser. Restrictions: - May only appear inside an **action arrow body** (an `onClick`, `onChange`, `onSubmit`, etc.). - May not appear inside an iterator body (`.map`, `.forEach`, …). - Always returns `void`; can't be used as an expression. | Call | Argument | |---|---| | `client.playSound(sound)` | A built-in cue name — `"tick"`, `"ding"`, `"whoosh"`, `"chching"`, `"applause"`, `"countdown"`, `"anticipation"`, `"waiting"` — **or** an `audio` asset (`asst.`) to play a custom clip. | The compiler optimizes statically-known calls (e.g., a literal cue name or a single `asst.`) so they fire instantly on click without a server round-trip. Calls with non-literal arguments still work — they just dispatch via the server. --- ## `Object` built-ins `Object.X(...)` works the way you expect from JavaScript: | Call | Returns | Notes | |---|---|---| | `Object.keys(obj)` | `string[]` | The object's own keys. | | `Object.values(obj)` | `T[]` | The object's own values. | | `Object.entries(obj)` | `[string, T][]` | Tuples. The standard input to `.map(([key, value]) => ...)`. | | `Object.fromEntries(arr)` | `object` | Inverse of `Object.entries`. | These are the only `Object.*` calls Banto recognizes. Anything else (`Object.assign`, `Object.freeze`, etc.) is a compile error. --- # Gotchas & common compile errors The fastest way to stop fighting the compiler. Each row is a mistake that costs a build round-trip, the diagnostic code you'll see (`banto.`), and the fix. Read this once before writing a game; skim it again the first time a build fails. > Source of truth for the rules is `SYNTAX-AND-USAGE.md`; this page is the > "things people actually trip on" subset. Diagnostic codes come from > `packages/core/src/diagnostic-codes.ts`. ## Project & state structure | Symptom | Code | Fix | |---|---|---| | Build says no start state | `missing-start-banto` | The project **must** have a `start.banto` whose `state` block is named `start`. | | A state renders nothing / errors on entry | `default-host-missing`, `default-plyr-missing` | **Every** state needs one `host default { condition: true, … }` **and** one `plyr default { condition: true, … }`. These are the fallback screens. | | "default must be last" / "default condition must be true" | `default-not-last`, `default-condition-not-true` | The `default` block goes **last** among its `host`/`plyr` siblings, and its `condition` must be literally `true`. Put conditional variants above it. | | `lstn ... next` can't find a state | `state-not-found` | The `next.state` string must match a real state name. States are files: `foo.banto` → `state foo`. | | `next` complains about inputs | `lstn-next-missing-inputs`, `lstn-next-inputs-mismatch`, `lstn-next-extraneous-inputs` | If the target state declares `params`, pass exactly those (names + types) in `next.inputs`. | ## Where blocks are allowed | Symptom | Code | Fix | |---|---|---| | `cpnt` / `data` / `func` / global `var` rejected in a state file | `cpnt-only-in-globals`, `data-only-in-globals`, `func-only-in-globals`, `block-not-in-globals` | Declare custom components, datasets, custom funcs, and global vars in **`globals.banto`**. State files hold `state`, `host`, `plyr`, `lstn`, and state-local `var`/`cstate`. | | `cstate` rejected | `cstate-only-in-states` | `cstate` (client-local draft state) lives inside a **state** block, not globals. | ## Actions, client vs server | Symptom | Code | Fix | |---|---|---| | Action arrow rejected for too many params | `action-arrow-too-many-params` | Action arrows take **at most one** parameter: `arg => { … }`. Components pass a single value (`onChange: (v) => …`). | | `client.X` rejected | `client-func-not-in-action`, `client-func-in-loop` | `client.*` (e.g. `client.playSound`) may appear **only** inside an action arrow body (`onClick`/`onChange`/`onSubmit`), and **never** inside a `.map`/`.forEach`/`.filter` body. | | `cstate` write rejected | `cstate-write-interleaved`, `client-phase-mixed-branch` | The compiler splits a handler into `client prelude → server body → client epilogue` (see the [recipe](./recipes.md#instant-ui-with-the-client-prelude--epilogue)). So: keep `cstate` writes as a contiguous **prefix and/or suffix** with your server writes (`var.*`, `func.notify`, …) grouped in the middle — a server write after an epilogue `cstate` write is `cstate-write-interleaved`. `cstate` writes **may** sit inside `if`/`forEach` branches, but such a branch can't also hold a `var`/server write (`client-phase-mixed-branch`) — split it. | | Nothing happens when a button is clicked | *(no error)* | The **action must sit directly in a component's handler slot** (`onClick: () => …`). An action nested inside a plain-object data param is silently dropped — hoist it to the handler field. | | Text/number input loses characters or won't submit | *(no error)* | Bind `value` to a **`cstate.`**, write `cstate. = arg` in `onChange`, and commit to `var.*` (and clear the cstate) in `onSubmit`. Binding `value` straight to a `var` fights the server round-trip. | ## Iterators & registry functions | Symptom | Code | Fix | |---|---|---| | `.map(...)` callback rejected | `iterator-callback-statement-body` | The value-returning iterators — `.map`, `.filter`, `.find`, `.findIndex`, `.some`, `.every` — need a single **expression** callback (`x => x.score`), not a `{ … }` statement block. `.forEach` is the one that takes a statement body. | | An unlisted method rejected (`.reduce`, `.flatMap`, `.at`, …) | `instance-method-unknown`, `instance-method-wrong-type` | Banto recognizes a fixed method set — see the [full list](../SYNTAX-AND-USAGE.md#the-full-method-list). An unknown name, or a real method on the wrong type (`.push` on a string), is a compile error; rebuild it from the supported methods (e.g. `.reduce` → a `.forEach` accumulating into a `let`/`var`). | | Passing an arrow to `func.mapValues` / `func.groupBy` fails | `call-arg-type-mismatch` | Registry funcs **don't accept arrow callbacks**. `mapValues`/`groupBy` take a `key` string (a property name), not a transform function. | | `break` / `continue` rejected | `break-continue-outside-loop` | Only valid inside a `.forEach` body. | | `Object.assign` / `Object.freeze` / etc. rejected | `unknown-func` | Banto recognizes only `Object.keys`, `Object.values`, `Object.entries`, `Object.fromEntries`. Everything else is a compile error. | | A list of components renders empty for a non-named chain root | *(fixed in compiler)* | `[1,2,3].map(...)` etc. now compile to a `chain` IR node and iterate correctly — you don't need a named intermediate. | ## Custom `func.X` Custom funcs are **pure helpers over their `params`** — think of them as stateless utilities. | Symptom | Code | Fix | |---|---|---| | Func body references `var`/`cstate`/`state`/`data`/`player` | `func-scope-forbidden` | Pass game state **in as an argument** (`func.score({players: var.players})`), don't read it from scope. | | Writing to `params.x` inside a func | `func-param-readonly` | Params are read-only. Build a new value and `return` it. | | Func calls itself | `func-recursion` | Recursion is disallowed. Flatten to a loop / `func.range(...).forEach(...)`. | | Return-type errors | `func-missing-return`, `func-return-in-void`, `func-return-type-mismatch` | If `returnType` is set, every path must `return` an assignable value; if it's unset, don't `return` a value. | ## Custom `cpnt.X` Custom components can read **global** state and forward actions, but they're still read-only and state-scoped data isn't in scope. | Symptom | Code | Fix | |---|---|---| | A `cpnt` body reads a var that's declared inside a state file | `unknown-var` | A `cpnt` lives in globals and can only read **global** vars (declared in globals.banto) + the inherent `roomCode`/`players`/`currentTime`. Pass a state-scoped value in through `params` at the call site. | | A `cpnt` body reads `state.params` | `state-params-wrong-block` | `state.params` isn't visible to a component. Pass the field(s) you need in via `params`. | | A `cpnt` body reads `cstate.X` | `cstate-wrong-block` | `cstate` is per-state client draft state; a component can't reach it. Bind it at the call site and pass the value in. | | Writing to `var`/`cstate` inside a `cpnt` body | *(type/ref error)* | Components are read-only. Do the write in the call-site handler, or forward an `action` param (below) and write there. | | An invoked action param writes `cstate` / calls `client.*` | `cpnt-action-invoke-client-phase` | `params.onSubmit(v)` inlines the handler into a server-side slot, so it can only do server work (`var`/`func.*`). Move any `cstate`/`client.*` work to the call site. | | Invoking (`params.onPick(v)`) an action that was forwarded into the cpnt through a `.map` | `cpnt-action-invoke-not-inlinable` | The handler isn't known at compile time, so it can't be inlined with an argument. Forward it to a slot instead (`onClick: params.onPick`, no call), or invoke it at the call site and pass a plain `action` down. | Reading a global var or `data.*`, invoking an `action` param with an argument (`params.onSubmit(params.draft)`), and forwarding an `action` through a nested `.map` into another custom cpnt all work — see the [language guide](./language-guide.md#8-components-cpnt). ## Type aliases (`type X = …`) Declare reusable named types and enums in `globals.banto`; reference them anywhere a type is expected. Aliases are compile-time only. | Symptom | Code | Fix | |---|---|---| | `type X = …` rejected in a state file | `type-only-in-globals` | Type aliases live in **`globals.banto`** only. Move it there; reference it from any file. | | Two aliases with the same name | `duplicate-type` | Rename one. Alias names must be unique (they don't collide with `var`/`cpnt` names — different namespace). | | A type name doesn't resolve | `unknown-type` | `type: Widget` needs a matching `type Widget = …` in globals. Check the spelling, or use a built-in type. | | An alias refers to itself | `type-alias-cycle` | Recursive types aren't supported (`type Tree = { kids: Tree[] }` and mutual `A`/`B` pairs are both rejected). Flatten the shape, or use `any` for the recursive part. | | Inline `"a" \| "b"` in a `var`/`params` annotation rejected | `string-enum-not-user-declarable` | Enums must be **named**: declare `type Mode = "a" \| "b"` in globals and reference `Mode`. | ## CSS `styles.banto.css` is a **restricted** subset. `css-forbidden-property`, `css-forbidden-function`, `css-forbidden-atrule`, and `css-forbidden-element-selector` mean you used something outside the allowlist — styling has to be done via a `css` block reference (declared in `styles.banto.css`) ## Custom assets (`asst.X`) | Symptom | Code | Fix | |---|---|---| | `asst.` rejected as unknown | `unknown-asset` | The base name must match a file you've pushed. Add it under `assets/`, title it in `banto.config.json`, and run `banto assets push`. | | Image asset in an audio slot (or vice versa) | *(type error)* | `asst.` carries a kind. `cpnt.image` wants an `image`, `cpnt.audio`/`client.playSound` want `audio`. Push the right file and reference it. | ## Runtime (no compile error, but wrong on screen) - **Host overflow is a bug.** The host screen shows on a TV and must never scroll in any direction. Size fonts/padding so the busiest phase (max players, longest text) still fits. - **An empty dataset means you didn't seed one.** When you host a published game, `data.questionSet`/`promptSet`/`dynamic` come from the data source the host picks at game start — which is your `banto.config.json` `default.*` binding. If `default.*` is missing (and there's no `new.*` seed to bind `"this"` to), the dataset is empty. Ship a `new.*` seed or point `default.*` at a real data-source id. - **A custom asset renders nothing.** If a `cpnt.image`/`cpnt.audio` shows nothing, the `asst.` either wasn't pushed (`banto assets push`) or you passed a raw string id/URL that failed to load. `asst.` refs are build-checked; raw strings are not. - **`func.pickRandom` / `func.first` / `func.last` return `undefined` on an empty list.** Branch before use. Number aggregates (`sum`/`avg`/`minOf`/`maxOf`) return `0` on empty instead, so they render cleanly. - **Subtraction glued to a name mis-parses.** Identifiers may contain hyphens (`css.welcome-screen`), so `var.a-var.b` lexes as one name and either errors or reads wrong. Put spaces around the minus: `var.a - var.b`. (Only subtraction — `+ * / %` aren't affected.)