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 Godot SDK code. Second, the Quick Reference section below IS the payload you can paste directly into your tool of choice as a system prompt, .cursorrules file, or 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’d rather hand-roll, the API Reference and the FAQ have every signature and gotcha you’ll need.
Setup
Claude (Claude Code, Claude Desktop)
Claude automatically reads any CLAUDE.md files in or above the directory it’s operating in. The SDK ships one at addons/board_sdk/CLAUDE.md — once the addon is installed in your project, Claude picks it up for free with no extra configuration. To prime Claude at the project root as well, copy that file (or its contents) into a CLAUDE.md at your repo root. For brand-new projects, install the bootstrap-board-godot-game Claude Code skill once with cp -R skills/bootstrap-board-godot-game ~/.claude/skills/ — after that, open Claude Code and run /bootstrap-board-godot-game to scaffold a complete Board game from a few prompts.
Cursor
Cursor reads .cursorrules at your project root and injects it into every chat. Copy the entire Quick Reference section below into .cursorrules, commit it to source control, and the whole team gets the same context. Cursor also respects CLAUDE.md-style files if you have AI Composer configured for multi-file context, so the addon’s addons/board_sdk/CLAUDE.md works there too.
GitHub Copilot and others
Copilot doesn’t have a project-level rules file like the others. The next-best thing is a top-of-file comment block in your main script (main.gd or whichever file you spend the most time in) that contains the Quick Reference below — Copilot reads the surrounding file context aggressively and will pick up the conventions from there. 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 — humans can read it too, but the framing assumes the reader is about to write code against the SDK.)
You are writing GDScript against the Board Godot SDK (board-godot-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).
GDScript basics
Boardis a global autoload registered by the addon. Nousing,import, orextendsneeded — just callBoard.input.subscribe()etc. from any script.- Always guard SDK calls with
if not Board.is_on_device: returnat the top of any function that depends on the native plugin. Off-device (editor, desktop), every method either no-ops or returns a typed default ([],{},-1,false). - The SDK is in a single autoload script — no scene-tree wiring, no nodes to instantiate.
- Method names are
snake_case.load_datais named to avoid shadowing GDScript’s built-inload(). There is no direct delete (matching the Unity SDK); detach players withremove_players_from_save/remove_active_profile_from_saveand the OS deletes the save once none remain.
Touch input
Subscribe to Board.input.contacts_received, activate a Piece Set model .tflite, and filter contacts by your game’s known glyph_id values.
func _ready() -> void:
if not Board.is_on_device:
return
Board.initialize("00000000-0000-0000-0000-000000000000")
Board.input.contacts_received.connect(_on_contacts)
Board.input.activate("models/your_piece_set.tflite")
Board.input.subscribe()
func _on_contacts(contacts: Array) -> void:
for c in contacts:
if c.glyph_id == GLYPH_PAWN: # game-side constants, not SDK helpers
_move_pawn(c.contact_id, c.x, c.y)
Contact Dictionary shape (every contact has these keys):
| Key | Type | Notes |
|---|---|---|
contact_id |
int |
Unique per physical contact. Stable across frames. |
x, y |
float |
Display-pixel position. Y-down, origin top-left. |
orientation |
float |
Degrees, screen-CW positive. Only meaningful for Glyphs. |
type_id |
int |
0 finger, 1 glyph. |
phase_id |
int |
1 BEGAN, 2 MOVED, 3 ENDED, 4 CANCELED, 5 STATIONARY. |
glyph_id |
int |
0 for finger; 1+ for Piece type. Maps to game-side names. |
is_touched |
bool |
true while the Piece is being physically held. |
frame_number |
int |
Kernel frame for cross-layer correlation. |
Tip: The per-frame
contacts_receivedpayload is a deliberately untyped Array of raw Dictionaries (the 60 fps hot path). For one-off typed access you can wrap a single entry withBoardContact.from_dict(c)— that gives youBoardContact.Type(FINGER,GLYPH,BLOB),BoardContact.Phase(NONE,BEGAN,MOVED,ENDED,CANCELED,STATIONARY),facing() -> Vector2, andis_piece(). Don’t convert the whole per-frame loop; the rawc.get(...)reads below are the intended fast path, andBoard.input.TYPE_*/PHASE_*int constants (same values) are there for raw-dict reads.
Warning: Filter and dispatch contacts by
glyph_id—glyph_id0is a finger and1+identifies a Piece type. But to track a specific Piece instance across frames, key oncontact_id, because two identical Pieces (e.g. two Pawns) share the sameglyph_id. Usingglyph_idas the per-instance tracking key collapses all instances of one Piece type into a single entry — every game with duplicate Pieces will be broken until this is fixed.
Tracking Pieces across frames and detecting edges
contacts_received is the only touch signal — a per-frame snapshot Array of contact Dictionaries. There are no separate per-contact edge signals. To track a Piece across frames or detect discrete edges (tap-down / tap-up via is_touched, or a Piece being lifted), keep your own previous-frame map keyed by contact_id and diff it each frame:
var _piece_positions: Dictionary[int, Vector2] = {}
var _prev: Dictionary[int, Dictionary] = {} # contact_id -> last frame's contact
func _ready() -> void:
if not Board.is_on_device:
return
Board.input.contacts_received.connect(_on_contacts)
func _on_contacts(contacts: Array) -> void:
var seen := {}
for c in contacts:
seen[c.contact_id] = true
if c.glyph_id != 0: # 0 is a finger; 1+ is a Piece
_piece_positions[c.contact_id] = Vector2(c.x, c.y)
var prior: Dictionary = _prev.get(c.contact_id, {})
# Tap-down / tap-up edges: diff is_touched against the prior frame.
if c.is_touched and not prior.get("is_touched", false):
_on_tap_down(c)
elif not c.is_touched and prior.get("is_touched", false):
_on_tap_up(c)
_prev[c.contact_id] = c
# Cleanup: any contact_id missing this frame has ended.
for id in _prev.keys():
if not seen.has(id):
_piece_positions.erase(id)
_prev.erase(id)
Keeping the _prev map yourself is the supported way to get edge events — the SDK only ever hands you the current-frame snapshot, matching the Unity SDK’s per-frame model.
Players and sessions
var players: Array[BoardPlayer] = Board.session.get_players() # array of BoardPlayer
# Show OS player selector to add a new Player
var rid := Board.session.present_add_player()
if rid >= 0:
await Board.session.player_selector_finished
players = Board.session.get_players()
BoardPlayer fields: player_id (String), session_id (int), display_name (String), type (BoardPlayer.Type, either PROFILE or GUEST), avatar_id (String). Use the is_guest() / is_profile() helpers instead of comparing type by hand. get_active_profile() returns a BoardPlayer or null (check if profile == null), never an empty Dictionary.
The roster is OS-owned: a game cannot silently add, remove, or swap players. The only ways to change who is playing are the OS selector overlays — Board.session.present_add_player() / Board.session.present_replace_player(session_id) — or a wholesale Board.session.reset_players(). Both selector calls take an optional trailing ai_type_indices := PackedInt32Array() filter: an empty array (the default) offers all AI types registered via set_ai_player_types(); a non-empty array restricts the selector’s “Add AI” options to that subset.
# Offer only AI types 0 and 2 in the selector
Board.session.present_add_player(PackedInt32Array([0, 2]))
Also: Board.session.is_session_ready() (the readiness check; not is_ready()), Board.session.are_services_ready(), Board.session.players_changed signal.
Save games
The async await_* helpers wrap the request-id + signal pattern with await:
var blob := PackedByteArray()
blob.resize(64)
var cover_png := PackedByteArray() # optional cover PNG bytes; empty = no cover
var meta := await Board.save.await_create("My Save", blob, played_ms, "1.0.0", cover_png)
if meta == null:
push_warning("save failed")
await_create() returns a BoardSaveMetadata or null (check if meta == null). A BoardSaveMetadata has fields id, description, created_at, updated_at, played_time, file_size, game_version, and player_count, plus an is_valid() helper. The save_created and save_listed signals carry typed BoardSaveMetadata too.
Also: Board.save.await_list() → Array[BoardSaveMetadata], Board.save.await_load(save_id) → PackedByteArray. The load method is load_data, never load. There is no direct delete; use remove_players_from_save / remove_active_profile_from_save (the OS deletes the save once no players remain).
Board.save.get_app_storage_info() → Dictionary is synchronous and returns the app’s storage budget: keys total_storage (int bytes), used_storage (int bytes), remaining_storage (int bytes), and usage_percentage (float, 0.0–1.0). Off-device returns {}. Use it to render a storage meter or pre-flight whether a save will fit before writing it.
Pause menu
Set context once (omit keys you don’t have — do not pass empty strings as defaults), then handle the result signal:
Board.pause.set_context({
"game_name": "My Game",
"offer_save_option": true,
"custom_buttons": [{ "id": "restart", "title": "Restart", "icon": Board.pause.ICON_CIRCULAR_ARROW }],
"audio_tracks": [{ "id": "music", "name": "Music", "value": 75 }],
})
Board.pause.pause_result_received.connect(func(result: BoardPauseResult):
match result.action:
Board.pause.ACTION_RESUME: pass
Board.pause.ACTION_QUIT: Board.application.quit()
Board.pause.ACTION_SAVE_AND_QUIT: _save_then_quit()
Board.pause.ACTION_CUSTOM_BUTTON:
if result.custom_button_id == "restart":
get_tree().reload_current_scene.call_deferred()
)
pause_result_received carries a BoardPauseResult (and poll_result() returns one). Its fields are action (String), custom_button_id (String), and audio_tracks (Array), plus an is_present() helper — check if not result.is_present() to test for “no result yet.” The ACTION_* / ICON_* constants on Board.pause are unchanged, and the set_context() payload is still a caller-supplied Dictionary (its custom_buttons and audio_tracks entries stay Dictionaries too).
Avatars
Board.avatar is a cached, player-scoped loader. Pass a known player’s avatar_id (from Board.session.get_players()) — a BoardPlayer’s avatar_id is a String, so wrap it in int(...). await_load_avatar returns a ready-to-display ImageTexture (already decoded), or null on failure / off-device. Textures are cached after first load, and concurrent loads of the same id coalesce onto one fetch.
var tex := await Board.avatar.await_load_avatar(int(player.avatar_id))
if tex != null:
$TextureRect.texture = tex
Also: Board.avatar.await_default_avatar() → ImageTexture (the default avatar, id 0), and Board.avatar.clear_cache() to drop cached textures.
Common patterns
State-machine with per-Piece handlers
Drive game logic from a small state machine, dispatching on glyph_id to per-Piece handler methods. Keeps the contacts handler readable as the game grows.
enum GameState { LOBBY, PLAYING, GAME_OVER }
var _state := GameState.LOBBY
func _on_contacts(contacts: Array) -> void:
if _state != GameState.PLAYING:
return
for c in contacts:
match c.glyph_id:
GLYPH_PAWN: _handle_pawn(c)
GLYPH_KNIGHT: _handle_knight(c)
_: pass # ignore unknown glyphs
Deferred scene reload on Restart
Reloading the current scene from inside a signal handler crashes Godot in some cases — defer the reload to the next idle frame:
Board.pause.pause_result_received.connect(func(result: BoardPauseResult):
if result.action == Board.pause.ACTION_CUSTOM_BUTTON \
and result.custom_button_id == "restart":
get_tree().reload_current_scene.call_deferred()
)
Mouse fallback for off-device development
When iterating in the editor or on desktop, Board.is_on_device is false and no contacts arrive. Wire a _unhandled_input(InputEvent) handler that fabricates contact Dictionaries from mouse events so you can prototype without redeploying every change:
func _unhandled_input(event: InputEvent) -> void:
if Board.is_on_device:
return # let the real input pipeline drive
if event is InputEventMouseButton and event.pressed:
_on_contacts([{
"contact_id": 1, "x": event.position.x, "y": event.position.y,
"orientation": 0.0, "type_id": Board.input.TYPE_FINGER,
"phase_id": Board.input.PHASE_BEGAN, "glyph_id": 0,
"is_touched": true, "frame_number": 0,
}])
Build and deploy summary
No build scripts are required. Build the APK with Godot’s own Android export, then install it onto the Board over Board Connect.
Build (Godot export)
- In the editor:
Project -> Export Project... -> Android. - Headless / CI:
godot --headless --export-debug "Android" build/your_game.apk.
With gradle_build/use_gradle_build=true in the Android export preset, the export runs the Gradle build, bundles the addon’s board.aar (so lib/arm64-v8a/libboard.so is present for the install gate) and the Piece Set Model into the PCK, and signs with the debug keystore. The model loads from the PCK, so a vanilla export Just Works — there is no asset-staging step.
One-time setup (noCompress)
After installing the Custom Android Build Template (Project -> Install Android Build Template), open android/build/build.gradle and add the following line to the aaptOptions block, right after the ignoreAssetsPattern line:
noCompress "pck", "sparsepck"
Godot’s stock template doesn’t set this. Without it the PCK is deflated inside the APK and cold boot takes ~2 minutes instead of a few seconds. This is a one-time edit per project.
Install (human, web UI)
Open the Board Connect web UI in a browser — the Board shows its address under Settings -> System — and drag the .apk onto it. Then launch the game from the Library.
Install (agent, board-connect CLI)
Agents install over Board Connect with the board-connect CLI (install it from dev.board.fun/connect/install) — no ADB and no scripts. Pair once (the user taps Approve on the device), then install, launch, and inspect. Only pair takes the Board address; every later command resolves the target from the saved default (or -b/--board / BOARD_HOST / discovery), so it needs no host:
board-connect pair <host> # one-time; user taps Approve on the Board
board-connect install <apk> --launch # push the exported .apk and start it
board-connect launch <package> # (re)start the game by package
board-connect logs <package> --follow # stream the game's logs live until Ctrl-C
board-connect screenshot --out shot.png # grab the current frame
Important Notes
- Y-down coordinates, no flips.
(x, y)from contacts is in display pixels with origin top-left. No transforms needed. - Quit cleanly with
Board.application.quit(), notget_tree().quit(). The SDK version notifies BoardOS so the launcher behaves correctly; the raw Godot quit is treated like a crash. is_session_ready()queries readiness. Pair withare_services_ready()if you also need to know whether the OS-level services are connected.load_data, notload.load()is a GDScript built-in; using that name on a class instance produces parse errors. (There is no direct delete to begin with — remove players instead.)- Don’t pass empty strings as defaults.
Board.pause.set_context({"game_id": ""})silently disables the pause button because the native side treats present-but-empty as “explicit empty,” not “use default.” Omit the key entirely instead. - Install gate. The Board’s install gate (Board Connect, port 8843) rejects any APK that doesn’t contain
lib/arm64-v8a/libboard.so(“this APK is not built with Board SDK”). The bundledboard.aarincludes it. This requires BoardOS 1.10.0 or later.
For more depth see the API Reference and the consumer-facing addons/board_sdk/CLAUDE.md, which ships inside every addon copy. The bootstrap-board-godot-game skill in the SDK’s bundled skills/bootstrap-board-godot-game/ directory scaffolds new projects end-to-end. File feedback at hello@board.fun. The SDK is available to download at https://dev.board.fun/.
</content>
</invoke>