# AI Assistant

This page does two things. First, it tells you how to set up Claude, Cursor, GitHub Copilot, or any other AI coding assistant so it can write correct, idiomatic Board Web SDK code. Second, the **Quick Reference** section below IS the payload you can paste directly into your tool of choice as a system prompt, a rules file, or a top-of-file comment block. Copy-paste-and-go.

> **Note**: Everything here is optional. The SDK works the same with or without AI assistance. If you would rather hand-roll, the [API Reference](https://docs.dev.board.fun/web/reference/api) and the [FAQ](https://docs.dev.board.fun/faq) have every signature and gotcha you need.

## Setup

### Claude

Claude reads any `CLAUDE.md` files in or above the directory it is working in. Drop the **Quick Reference** below into a `CLAUDE.md` at your project root and Claude will follow the conventions for free.

### Cursor

Cursor reads a rules file at your project root and injects it into every chat. Copy the entire **Quick Reference** section below into that file, commit it to source control, and the whole team gets the same context.

### GitHub Copilot and others

Copilot does not have a project-level rules file. The next-best thing is a top-of-file comment block in your main module that contains the **Quick Reference** below; Copilot reads surrounding file context aggressively and picks up the conventions. For ChatGPT, Aider, or any tool with a system-prompt slot, paste the **Quick Reference** in directly.

* * *

## Quick Reference

_(This is the section to copy into your AI tool. Everything below this line is written as a self-contained briefing for an AI assistant.)_

You are writing TypeScript against the **Board Web SDK** (`@board.fun/web-sdk`). Board is a 23.8” 1080p landscape touch console; its touch sensor detects both fingers and physical **Pieces** (game tokens with conductive **Glyph** patterns on their bases). The SDK is ESM-only and runs your web app inside Board’s built-in browser.

### Basics

- Import the single frozen `Board` object: `import { Board, BoardContactType } from "@board.fun/web-sdk";`. Every feature hangs off it across six domains: `Board.input`, `Board.session`, `Board.save`, `Board.avatar`, `Board.pause`, `Board.application`.
- **Always gate device calls** on `Board.isOnDevice`. In a desktop browser it is `false` and service-backed calls do NOT no-op: sync calls (`Board.session.*`, `Board.pause.*`, `Board.application.*`, the `save` getters) **throw**, and async calls (`Board.save.*`, `Board.avatar.*`, `Board.session.present*`) **reject** because the native bridge is absent. The one exception is `Board.input.getContacts()`, which safely returns `[]`. Gate sync calls behind an `if (Board.isOnDevice)` check and wrap async calls in `try/catch` so the same build stays runnable without hardware.
- **Do not gate features on `Board.sdkVersion`.** It is informational only. There is no bridge or OS version exposed to games; new OS capabilities degrade gracefully via in-bridge capability checks. The runtime gate is `Board.session.areServicesReady()`.
- Two async styles: **Promises** for request/response calls, and **callbacks/subscriptions** for the touch stream and pause results.

### Touch input

Subscribe to `Board.input`; each callback receives a full per-frame snapshot of contacts. Filter Pieces by `glyphId`, not by the contact type.

```typescript
function onContacts(contacts: BoardContact[]) {
  for (const c of contacts) {
    if (c.type === BoardContactType.Glyph) {
      handlePiece(c.glyphId, c.x, c.y, c.orientation);
    }
  }
}

if (Board.isOnDevice) {
  Board.input.subscribe(onContacts);
}
```

Each contact: `contactId`, `type` (`BoardContactType.Finger` / `Glyph` / `Blob`), `glyphId`, `x`, `y` (device pixels, origin top-left, Y down), `orientation`, `phase`. There are no discrete down/up events; keep your own previous-frame map keyed by `contactId` and diff it for edges. All contacts — finger and Piece — come from the on-device detector, which only runs when the bundle ships a Piece Set Model (recorded at pack time): a bundle with no model gets no input frames at all.

### Players and sessions

The roster is **OS-owned**; the game never silently adds or removes players. Read with `getPlayers()` / `getPlayerCount()` / `getActiveProfile()`. Change it through the OS selector (`presentAddPlayer(aiTypeIndices?)` / `presentReplacePlayer(sessionId, aiTypeIndices?)`, both resolve `Promise<boolean>`, `true` = added/replaced, `false` = dismissed) or `resetPlayers()`. Declare AI types with `setAIPlayerTypes([{ name, description }])

```typescript
const added = await Board.session.presentAddPlayer();
if (added) refreshRoster();
```

### Save games

Create, load, list, and update, all Promise-based: `create`, `load`, `list`, `update`. There is no direct delete. A game removes its own involvement with `removePlayersFromSave` (the current game’s players) or `removeActiveProfileFromSave` (only the active profile); the system deletes the save once no players remain. Plus `loadCoverImage`, `getAppStorageInfo()`, and `getUniquePlayers(saves)`.

```typescript
const meta = await Board.save.create("Turn 12", payload /* Uint8Array */, playedTimeMs, gameVersion);
const saves = await Board.save.list();
await Board.save.removePlayersFromSave(meta.id);
// or, to remove only the active profile:
await Board.save.removeActiveProfileFromSave(meta.id);
```

Save metadata: `id`, `description`, `createdAt`, `updatedAt`, `playedTime`, `fileSize`, `gameVersion`, `playerCount`, `players[]` (each `playerId`, `name`, `avatarId`, `type`, `aiTypeIndex`), `hasCoverImage`, `payloadChecksum`, `coverImageChecksum`.

### Avatars

`Board.avatar.loadPNG(avatarId)` resolves to a cached PNG data URI; `getDefault()` is avatar 0; `forPlayer(player)` is a shortcut. Assign the data URI straight to an `<img>` element’s `src`.

### Pause overlay

The OS owns the menu button and UI; the game supplies context and reads results. `setContext(context)`, `updateContext(partial)`, `clearContext()`. Subscribe with `onResult(callback)` (preferred); a legacy `pollResult()` exists.

```typescript
Board.pause.setContext({
  offerSaveOption: true, // adds the system "Save & Quit" option
  customButtons: [{ id: "restart", title: "Restart", icon: "circulararrow" }],
});
Board.pause.onResult((result) => {
  if (result.action === "quit" || result.action === "save_and_quit") Board.application.quit();
  if (result.action === "custom_button" && result.customButtonId === "restart") restartGame();
});
```

### Application lifecycle

`Board.application.quit()` closes the web app and returns to the launcher. `showProfileSwitcher()` / `hideProfileSwitcher()` drive the OS profile switcher.

### Build and deploy

For a new project, scaffold with `npm create @board.fun/game my-game` (add `-- --template showcase` for a reference app exercising every SDK domain) — it pre-wires the base path, pack scripts, and model placeholder. Otherwise: build a static app with relative paths (`base: "./"` in Vite). Pack with `web-pack dist --package-id fun.board.mygame --name "My Game"`, which writes a flat `<appId>.webapp.zip` and mints a random-UUID `appId` persisted to `board.config.json` (commit that file so saves survive rebuilds). Every bundle ships a Piece Set Model (required — finger input needs it too): download it out of band from the developer portal, keep it in your build output (e.g. `public/model.tflite`) so `web-pack` picks it up, or pass `--model <path>`; the tooling never vendors or downloads it at runtime. Pair once with `board-connect pair <ip>` (tap **Approve** on the device), which saves that Board as the default so later commands need no address. Then deploy with `board-connect install <appId>.webapp.zip --launch`, watch `board-connect logs <appId> --follow`, and confirm device support with `board-connect capabilities` (MP.1.9.x family or newer). If the first install reports the host unavailable, foreground the Board Browser once and retry.

* * *

## Important notes

- **Gate on `Board.isOnDevice`**, not on `Board.sdkVersion`. The runtime capability gate is `Board.session.areServicesReady()`.
- **Y-down coordinates, no flip.** Contacts are device pixels, origin top-left.
- **Identify Pieces by `glyphId`**, not by the contact type.
- **There is no direct save delete.** A game removes its own players with `removePlayersFromSave` or `removeActiveProfileFromSave`. The system deletes the save automatically once no players remain associated with it. A game cannot directly delete a shared save, which protects other players’ progress. This matches the Unity SDK.
- **Commit `board.config.json`** so the `appId` (and therefore saved games) survives rebuilds.

* * *

For more depth see the [API Reference](https://docs.dev.board.fun/web/reference/api) and the [Guides](https://docs.dev.board.fun/guides/). File feedback at [hello@board.fun](mailto:hello@board.fun).
