From board-game-arena
Implement a board game on Board Game Arena (BGA) Studio, from rules+graphics to a playable game. Covers project setup, new framework (2025+) architecture, PHP state machine, JS client, and the automated deploy→test loop.
How this skill is triggered — by the user, by Claude, or both
Slash command
/board-game-arena:board-game-arenaThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
You are helping implement a board game on Board Game Arena Studio, using the **new BGA framework (2025+)**. The workflow has three phases: **Setup**, **Implementation**, and **Test Loop**.
references/README.mdreferences/bga-animations.mdreferences/bga-autofit.mdreferences/bga-cards.mdreferences/bga-dice.mdreferences/bga-score-sheet.mdreferences/bgg-submission.mdreferences/config-files.mdreferences/counter.mdreferences/debug-helpers.mdreferences/deck.mdreferences/draggable.mdreferences/expandable-section.mdreferences/guidelines.mdreferences/js-framework.mdreferences/makefile-template.mdreferences/notifications.mdreferences/php-code-quality.mdreferences/php-framework.mdreferences/player-table-counter.mdYou are helping implement a board game on Board Game Arena Studio, using the new BGA framework (2025+). The workflow has three phases: Setup, Implementation, and Test Loop.
ssh-keygen -t ed25519 -f ~/.ssh/id_rsa_bgasftp -i ~/.ssh/id_rsa -P 2022 -o IdentitiesOnly=yes [email protected]~/.ssh/id_rsa, etc.). To get deployment config (username, key path), look at Makefiles of existing BGA projects in the workspace, or ask the user.make check and make deploy working in the project directoryimg/ (SVG preferred)When a game is created on BGA Studio, a scaffold is auto-generated server-side. Download it as the reference:
mkdir -p bga_initial_code_template
sftp -i ~/.ssh/id_rsa -P 2022 -o IdentitiesOnly=yes [email protected]
> get -r GAMENAME/* bga_initial_code_template/
Never modify bga_initial_code_template/ — it's the framework reference.
→ Commit now (scaffold + .gitignore).
modules/php/Game.php — main class
modules/php/material.inc.php — constants, tile costs, static data
modules/php/States/ — one .php file per game state
modules/js/Game.js — ES6 client (must export class named exactly `Game`)
stats.json — statistics config
gameoptions.json — game options config
gamepreferences.json — player preferences config
gameinfos.inc.php — game metadata
dbmodel.sql — custom MySQL tables
GAMENAME.css — styles
img/ — assets (SVG, PNG)
Makefile — check + deploy targets
If the repository has no commits yet, initialize it now: git init, create a .gitignore (exclude bga_initial_code_template/), and make the initial commit with the scaffold and project structure.
Create a Makefile with check (PHP lint) and deploy (SCP to BGA Studio) targets. See references/makefile-template.md for the full template.
BGA Studio is SFTP-only (no SSH shell) — rsync does not work. Use scp with explicit file lists.
→ Commit now (Makefile + config files).
For users comfortable with PHP tooling, the skill can layer four quality tools on top of make check:
code-quality + dead-code (auto-apply) — mechanical simplificationsTogether they form a make audit target — a local "PR review" loop, read-only, ~10 s. Tools install once into a shared ~/.bga-tools/ Composer toolbox (no per-project re-install). The audit is not gated on deploy — quality is advisory, never blocks a session push.
Skip this section if you're not comfortable with PHP tooling — the skill works fine without it. If you want it, see references/php-code-quality.md for the full setup (toolbox install, four config files, stubs for the BGA framework, baseline workflow, and an opt-in pre-commit hook).
Before writing game logic, convert the rulebook to Markdown and analyze it systematically. This surfaces ambiguities that a program cannot resolve by "common sense" the way a human player would.
doc/RULES.md — primary text reference; only go back to the PDF for illustrationsdoc/AUTHOR_QUESTIONS.md — questions categorized by status (OPEN/ASSUMED/CLOSED) and type (RULES-MISSING, RULES-AMBIGUOUS, RULES-IMPLICIT, FEEDBACK, etc.)doc/ASSUMPTIONS.md — each assumption gets an ID [Hx] referenced in both questions and codeSee references/rules-clarification.md for the full process, document format, categories, and anti-patterns.
Key rule: don't block all implementation waiting for answers — categorize, assume where reasonable, and work on what's clear. But every assumption must be documented.
→ Commit now (doc/RULES.md + doc/AUTHOR_QUESTIONS.md + doc/ASSUMPTIONS.md).
<?php
declare(strict_types=1);
namespace Bga\Games\GameName; // PascalCase — matches the scaffold
use Bga\GameFramework\Table;
class Game extends Table
{
public const MY_CONSTANT = 1;
public function __construct() {
parent::__construct();
// Minimal — do NOT call initGameStateLabels here
}
protected function initTable(): void {
// Using $this->bga->globals for all state (any type, JSON-serialized)
// initGameStateLabels is legacy and int-only — avoid it
}
public function setupNewGame(array $players, array $options = []): mixed {
// 1. Set player colors
$colors = $this->getGameinfos()['player_colors'];
foreach ($players as $pid => $player) {
$color = array_shift($colors);
$this->DbQuery("UPDATE player SET player_color='$color' WHERE player_id='$pid'");
}
// 2. reloadPlayersBasicInfos BEFORE any INSERT or initStat
$this->reloadPlayersBasicInfos();
// 3. Custom tables
// 4. initStat
return StartState::class;
}
public function getAllDatas(int $currentPlayerId): array {
return [
// data visible to all players
];
}
public function getGameProgression(): int {
return 0; // 0–100
}
}
Critical Game.php rules:
Bga\Games\GameName — PascalCase, matching the scaffold (not lowercase)public const on the Game class, NOT define() in material.inc.php. Reference as self::MY_CONST in Game.php, Game::MY_CONST in States. (See TECHNICAL_NOTES.md for why define() breaks under namespaces.)$this->bga->globals->set/get (any type, JSON-serialized). initGameStateLabels is legacy and int-only — these are two distinct systems, do not mix themmaterial.inc.php is auto-included by the framework — never include() it manuallyself:: on instance methods — PHP 8.4 generates warnings that corrupt JSON output. Use $this-> everywhere. (self::CONST for class constants is fine){$var} not ${var} in strings (PHP 8.4)implode(separator, array) not implode(array, separator) (PHP 8.4)<?php
declare(strict_types=1);
namespace Bga\Games\GameName\States;
use Bga\GameFramework\StateType;
use Bga\GameFramework\States\GameState;
use Bga\GameFramework\States\PossibleAction;
use Bga\GameFramework\Actions\Types\IntArrayParam;
use Bga\GameFramework\Actions\Types\StringParam;
use Bga\GameFramework\UserException;
use Bga\Games\GameName\Game;
class PlayerTurn extends GameState
{
function __construct(protected Game $game) {
// Only id and type — NO name, description, or transitions
parent::__construct($game, id: 20, type: StateType::ACTIVE_PLAYER);
}
public function onEnteringState(array $args): void { }
#[PossibleAction]
public function actDoSomething(string $param): string {
$playerId = $this->game->getCurrentPlayerId();
// validate...
// do work...
// Send notification for every action
$this->game->bga->notify->all('somethingDone', clienttranslate('${player_name} did something'), [
'player_id' => $playerId,
'player_name' => $this->game->getPlayerNameById($playerId),
]);
return NextState::class; // return the next state class, not a string
}
// Action parameter validation (autowired — no action.php needed)
#[PossibleAction]
public function actSelectCards(#[IntArrayParam] array $ids): string { ... }
#[PossibleAction]
public function actMove(#[StringParam(enum: ['up','down','left','right'])] string $dir): string { ... }
}
Critical state rules:
id and type — no name, description, or transitionsreturn ResolveAction::class)ACTIVE_PLAYER and MULTIPLE_ACTIVE_PLAYER statesMULTIPLE_ACTIVE_PLAYER: the framework automatically waits for all players when the action returns a state classsetPlayerNonMultiactive($id, 'transitionName') — transition names don't exist in the new frameworkargGameEnd() or stGameEnd() — they are final. Use a state 98 (computeScores) insteadIn MULTIPLE_ACTIVE_PLAYER actions, the client does not send activePlayerId. Do not add it as a PHP parameter:
// ✗ Wrong — client never sends this parameter
public function actChoose(int $activePlayerId, string $choice): string { ... }
// ✓ Correct
public function actChoose(string $choice): string {
$playerId = $this->game->getCurrentPlayerId(); // framework provides it
...
return NextState::class;
}
// Globals — modern API (any type, JSON-serialized)
$this->bga->globals->set('turn_number', $n); // int, string, array, bool — all work
$n = (int)$this->bga->globals->get('turn_number');
// ⚠ initGameStateLabels is a SEPARATE legacy system — int-only, do not mix with bga->globals
// Notifications (3 params)
$this->bga->notify->all('eventName', clienttranslate('${player_name} did X'), [
'player_name' => $this->getPlayerNameById($pid),
]);
$this->bga->notify->player($pid, 'eventName', clienttranslate('msg'), [...]);
// Stats
$this->bga->tableStats->inc('turns_played', 1);
$this->bga->playerStats->inc('tiles_placed', 1, $pid);
// Scores (write before transitioning to state 99)
$this->bga->playerScore->set($pid, $score);
// DB helpers
$rows = $this->getCollectionFromDb("SELECT * FROM my_table"); // not getObjectListFromDB(sql, true) — broken
$row = $this->getObjectFromDB("SELECT * FROM my_table WHERE id=$id");
When you need a game component, check this table FIRST. Each library has a detailed reference in references/. Read the reference file before implementing — it contains setup code, full API, and pitfalls.
| Need | Library | Reference | Side |
|---|---|---|---|
| Cards/tiles (server logic) | Deck | references/deck.md | PHP |
| Cards/tiles (client display) | BgaCards | references/bga-cards.md | JS |
| Per-player counters (money, resources) | PlayerCounter | references/player-table-counter.md | PHP |
| Game-wide counter (round, phase) | TableCounter | references/player-table-counter.md | PHP |
| Numeric display with animation | Counter | references/counter.md | JS |
| Dice rolling & display | BgaDice | references/bga-dice.md | JS |
| Item collections (hand, market) | Stock | references/stock.md | JS |
| Token placement areas | Zone | references/zone.md | JS |
| Scrollable/infinite board | Scrollmap | references/scrollmap.md | JS |
| Move animations & scoring | BgaAnimations | references/bga-animations.md | JS |
| End-game score sheet | BgaScoreSheet | references/bga-score-sheet.md | JS |
| Auto-size text on cards | BgaAutofit | references/bga-autofit.md | JS |
| Collapsible sections | ExpandableSection | references/expandable-section.md | JS |
| Drag-and-drop (legacy) | Draggable | references/draggable.md | JS |
Workflow: identify the need → find the library in the table → read
references/<file>.md→ implement using the patterns from the reference.
// modules/js/Game.js — ES6, no framework dependencies
export class Game { // MUST be named exactly "Game"
constructor() {
// called once when the game loads
}
setup(gamedatas) {
// 1. Build board/pieces from gamedatas
for (const [pid, player] of Object.entries(gamedatas.players)) {
// render player areas
}
// 2. Connect click handlers
this.bga.gameui.connectClass('my-piece', 'onclick', 'onPieceClick');
// 3. Setup notification handlers (auto-detects notif_* methods)
this.bga.notifications.setupPromiseNotifications();
// 4. Render initial state (onEnteringState is NOT called after setup)
const stateArgs = gamedatas.gamestate?.args;
// Use stateArgs to render the complete initial UI
}
onEnteringState(stateName, args) {
// show/hide UI per state (called on state transitions, NOT after setup)
}
onLeavingState(stateName) { }
onUpdateActionButtons(stateName, args) {
// Add action buttons — REQUIRED for multiactive states
if (stateName === 'playerTurn') {
this.bga.statusBar.addActionButton(_('Pass'), () => {
this.bga.actions.performAction('actPass');
}, { color: 'secondary' });
}
}
// Notification handlers — auto-registered by setupPromiseNotifications()
// Name = 'notif_' + the notification type from PHP notify->all('somethingDone', ...)
async notif_somethingDone(args) {
// args = PHP notification args; values are STRINGS — parseInt() before arithmetic
await this.bga.gameui.slideToObject('token', 'target').play().promise;
}
// Action handler — ONLY from user events, NEVER from notifications/loops/callbacks
onPieceClick(e) {
const id = e.currentTarget.id;
this.bga.actions.performAction('actSelectPiece', { id: parseInt(id, 10) });
}
}
Assets: reference as g_gamethemeurl + 'img/file.svg' or this.bga.images.getImgUrl('file.svg')
For full format details, see references/config-files.md.
gameoptions.json (game-affecting options, shown at table creation):
{
"100": {
"name": "Board size",
"values": {
"1": { "name": "Small" },
"2": { "name": "Standard", "tmdisplay": "Standard board" }
},
"default": 2
}
}
stats.json (displayed at game end):
{
"table": {
"total_rounds": { "id": 10, "name": "Number of rounds", "type": "int" }
},
"player": {
"cards_played": { "id": 10, "name": "Cards played", "type": "int" }
}
}
gamepreferences.json (cosmetic, per player):
{
"100": {
"name": "Colorblind mode",
"values": { "0": { "name": "Disabled" }, "1": { "name": "Enabled" } },
"default": 0,
"needReload": true,
"cssPref": true
}
}
gameinfos.inc.php — essential fields:
$gameinfos = [
'players' => [2, 3, 4],
'suggest_player_number' => 3,
'player_colors' => ['ff0000', '008000', '0000ff', 'ffa500'],
'favorite_colors_support' => true,
'is_beta' => 1, // cannot be 0 until release
'is_coop' => 0,
'complexity' => 2, 'luck' => 2, 'strategy' => 3, 'diplomacy' => 1,
'bgg_id' => 0,
];
Config rules: no comments in JSON, no trailing commas, names are auto-translated (no totranslate() needed). After changes: deploy AND reload via BGA Studio manage page.
For detailed API beyond the patterns above, load the relevant reference file.
| Topic | Reference | When to load |
|---|---|---|
| JS full API (DOM, animations, tooltips, dialogs, panels) | references/js-framework.md | Building complex UI |
| PHP full API (DB, players, states, scoring, undo) | references/php-framework.md | Advanced game logic |
| Notification system (PHP + JS) | references/notifications.md | Custom notification handling |
| Config files (options, prefs, stats, gameinfos) | references/config-files.md | Setting up or modifying config |
| Translations & i18n | references/translations.md | Adding translatable strings |
| BGA Studio Guidelines (layout, a11y, UX) | references/guidelines.md | Polishing UI / preparing for review |
Debug helpers (debug_<name>, premium-gate workaround) | references/debug-helpers.md | Verifying state mid-game, end-game stats panel showing "Go premium" |
| Rules clarification process | references/rules-clarification.md | Analyzing rules, managing author Q&A |
| BGG submission helper | references/bgg-submission.md | Producing the BGG entry that unlocks bgg_id (post-release, optional) |
mcp__claude-in-chrome__tabs_context_mcp() → note tabId
mcp__claude-in-chrome__read_console_messages(tabId, clear: true)
Variables:
GAME_ID — numeric BGA Studio game ID (found in the Studio URL)GAME_NAME — technical game name (e.g., mygame)cd PROJECT_DIR && make deploy
mcp__claude-in-chrome__navigate(tabId, url: "https://studio.boardgamearena.com/lobby?game={GAME_ID}")
Wait 3 seconds.
Click "Play with friends" on the lobby page, then "Express start" on the table page:
// Step 1: On the lobby page (/lobby?game=GAME_ID), click "Play with friends"
const pwf = Array.from(document.querySelectorAll('a')).find(a => a.textContent.includes('Play with friends'));
pwf?.click();
Wait 5 seconds — URL changes to /table?table=N.
// Step 2: On the table page, click "Express start" (overriding confirm dialog)
const origConfirm = window.confirm;
window.confirm = () => true;
const expressBtn = Array.from(document.querySelectorAll('a, button')).find(b => b.textContent.trim() === 'Express start');
expressBtn?.click();
window.confirm = origConfirm;
Wait 5 seconds — BGA creates the hotseat game and redirects to /tableview?table=N.
BGA redirects to /tableview (spectator view) after Express start. Navigate to the player view:
mcp__claude-in-chrome__navigate(tabId, url: "https://studio.boardgamearena.com/1/{GAME_NAME}?table=N")
/1/GAMENAME?table=N = player view (can play, sees hand)/tableview?table=N = spectator view (no game_play_area, cannot play)?testuser=PLAYER_ID to play as the other player in hotseatFrom the game page (not lobby), use gameui.ajaxcall — it attaches the CSRF token automatically, while raw fetch() is rejected:
gameui.ajaxcall('/table/table/quitgame.html', {table: TABLE_ID, neutralized: true, s: 'table_quitgame'}, gameui,
() => console.log('quit ok'),
() => console.log('quit err')
);
On the lobby page use mainsite instead of gameui. See TECHNICAL_NOTES.md for the why.
mcp__claude-in-chrome__read_console_messages(tabId, pattern: "error|Error|fatal")
| Console output | Meaning |
|---|---|
| No messages | Game launched successfully |
JSON_ERROR_SYNTAX + fatal PHP | PHP error — read message after <b>Fatal error</b> |
JSON_ERROR_SYNTAX + warning | PHP warning corrupting JSON — read the warning |
| Any other error string | Look it up in Common Initial Errors below for the fix |
Full cycle: ~30 seconds.
The URL without testuser= shows the admin view: https://studio.boardgamearena.com/1/GAME_NAME?table=N
goToState (Svelte panel — automate via JS):
const input = document.getElementById('debugParamDlg-parameter-state-input');
input.value = '20'; // target state ID
input.dispatchEvent(new Event('input', {bubbles: true}));
document.getElementById('debugParamDlgApply').click();
Full SQL/request logs (best for stack traces):
/1/GAME_NAME/GAME_NAME/logaccess.html?table=N
debug_<name> PHP helpers from the toolbar: any public function debug_* on
Game.php is invokable from the Studio Debug input (right of the table); output
to the chat log via notify->all('log', ...). See references/debug-helpers.md.
End-game stats panel says "Go premium": click "Become premium" on your Studio
account (free in dev), or use a debug_dumpStats helper to read stats live.
After changing stats.json, gameoptions.json, gamepreferences.json, or gameinfos.inc.php:
mcp__claude-in-chrome__navigate(tabId, url: "https://studio.boardgamearena.com/studiogame?game={GAME_NAME}")
Then:
const links = Array.from(document.querySelectorAll('a'));
links.find(a => a.textContent.includes('Reload game informations'))?.click();
links.find(a => a.textContent.includes('Reload statistics'))?.click();
links.find(a => a.textContent.includes('Reload game options'))?.click();
Commit automatically after each completed step — do not wait for the user to ask. Each commit should capture a coherent, working (or at least lint-passing) state.
Commit points (at minimum):
git add -A && git commit -m "feat: description of what was done
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>"
Key rule: smaller, frequent commits are better than large ones. If a step involves multiple independent changes (e.g., implementing two unrelated game states), commit each separately.
After first deploy, BGA Studio may show these warnings — they are non-blocking for development:
| Warning | Fix |
|---|---|
| "This game doesn't have a description" | Fill in the Game Metadata Manager on BGA Studio (UI, not code) |
| "This game doesn't have a valid BGG_ID" | Set 'bgg_id' => 0 in gameinfos.inc.php until a BGG page exists. Then reload game informations. When ready to publish, see references/bgg-submission.md. |
The numeric GAME_ID is not the same as the game's internal ID visible in the page source (game_id). Get it from the "Play" link on the manage page:
// On the studiogame page, get the correct lobby URL:
Array.from(document.querySelectorAll('a')).find(a => a.href.match(/lobby\?game=\d+/))?.href
// → "https://studio.boardgamearena.com/lobby?game=XXXXX" ← use this number
| Error | Fix |
|---|---|
reflexion_time cannot be initialized | Move initGameStateLabels from constructor to initTable() |
Constant X already defined | Migrate to public const on the Game class (see 2.1). For legacy code that must keep define(), guard each one: defined('X') || define('X', …) |
Access level must be public | getAllDatas, getGameProgression must be public |
Invalid id for state class | Delete scaffold state files from server via SFTP |
Unknown statistic id | Reload statistics via BGA Studio manage panel |
This transition (X) is impossible | Return state class from act method instead of calling setPlayerNonMultiactive(id, 'name') |
too many authentication failures | Add -o IdentitiesOnly=yes to SSH/SCP commands |
| Scores not showing at game end | Write scores with $this->bga->playerScore->set($pid, $score) in state 98, not state 99 |
Unknown column 'X' in 'field list' | Check (a) inline -- comments in dbmodel.sql (see "No SQL Comments"); (b) the table was created from an older dbmodel.sql — BGA does not auto-migrate (see "BGA Schema Changes During Development"); (c) the name is reserved (see "Table Name Conflicts"). |
static::DbQuery or self::DbQuery | PHP 8.4 warns on static calls to instance methods, corrupting JSON. Always use $this->DbQuery(...). Same applies to all other DB helpers. |
| Logic bug: graph/query silently missing rows | getCollectionFromDb() uses the first selected column as the PHP array key — duplicate values silently overwrite each other. Always put a unique column (e.g., id, move_number) first: SELECT move_number, from_position, to_position FROM ... not SELECT from_position, to_position FROM .... |
Rule: prefix every custom table with a game-specific tag (e.g., mygame_moves, mygame_board). Never use these reserved names: moves, player, global, stats, gamelog, replaysavepoint, or anything starting with bga_. Collisions cause CREATE TABLE IF NOT EXISTS to silently no-op — see TECHNICAL_NOTES.md for the mechanism.
Critical rule: write zero -- comments inside dbmodel.sql — anywhere in the file. Document the schema in a separate Markdown file (e.g., SCHEMA.md) instead. See TECHNICAL_NOTES.md for why (BGA collapses newlines before sending the file to MySQL, turning any -- into a comment that eats the rest of the file).
Safe pattern — self-documenting column names, no comments:
CREATE TABLE IF NOT EXISTS `mygame_moves` (
`move_number` INT(3) NOT NULL,
`player_id` INT(10) NOT NULL,
`from_position` INT(2) NOT NULL,
`to_position` INT(2) NOT NULL,
PRIMARY KEY (`move_number`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
dbmodel.sql runs once per new game table instance (when setupNewGame is called). CREATE TABLE IF NOT EXISTS will silently skip if a table already exists with a wrong schema.
When the schema is wrong on an existing table:
dbmodel.sql will be appliedmoves → mygame_moves) and redeploy before creating the fresh tablejavascript_tool (DOM interaction) not computer (pixel coordinates)read_console_messages with a pattern filter, not read_page(int)$row['value']bga_rand(min, max) for dice/random — rand() and mt_rand() forbidden in reviewgetCurrentPlayerId() in setupNewGame() or args methods — causes "Not logged" errors. (For zombieTurn(), see Zombie handling below.)activeNextPlayer() / changeActivePlayer() are no-ops inside ACTIVE_PLAYER statesgetObjectListFromDB($sql) returns a simple array — use it instead of getCollectionFromDb when the first selected column may have duplicates (see Common Initial Errors for the keying pitfall)=== not == — hex colors like '4baae2' miscast with ==#[PossibleAction] + typed parameters = no action.php needed$args, $activePlayerId, $currentPlayerId — reserved by frameworkescapeStringForDB($str) — mandatory for any player-supplied string in SQLclienttranslate() for all notification messages — literal text only, no variables insideUserException messages in clienttranslate() — they're displayed to the player. SystemException / VisibleSystemException go to server logs and stay untranslated. See references/translations.md for the full i18n workflow (alpha vs beta, "Display dummy translation" debug button, raw-value-vs-translated-label pattern)PlayerStats::init has no $playerId — signature is init($name, $value, bool $updateTableStat = false). Passing a player ID silently casts to true and crashes setupNewGame. Inits all players in one call — no loop. (inc/set DO take $playerId.)setup() runs before onEnteringState() — render complete initial state in setup using gamedatas.gamestate.argssetupPromiseNotifications() in setup() — auto-detects all notif_* methodsonEnteringState — use onUpdateActionButtons for active-dependent UIperformAction only from user events — NEVER from notifications, loops, callbacks, or state methodsslideToObject() returns dojo animation — must call .play() on the resultbgaAnimationsActive() before animatingparseInt(value, 10) — notification args are strings, += will concatenate instead of addgamedatas across players — no per-player client dataisCurrentPlayerSpectator() || typeof g_replayFrom != 'undefined' || g_archive_modemygame_selected, not selectedattachToNewParent clones the element — destroys original references and dojo.connect handlersgetCurrentPlayerId() in zombieTurn() — use the $active_player_id parameter"zombiePass" transition (exact spelling)window.confirm = () => true)#[PossibleAction]) are effective for testing end-game / specific positions — call via gameui.ajaxcall, remove before commit/1/GAME_NAME/GAME_NAME/logaccess.html?table=N#overall-content gets .gamestate_playerTurn etc. — use for conditional visibility without JSProvides UI/UX resources: 50+ styles, color palettes, font pairings, guidelines, charts for web/mobile across React, Next.js, Vue, Svelte, Tailwind, React Native, Flutter. Aids planning, building, reviewing interfaces.
Fetches up-to-date documentation from Context7 for libraries and frameworks like React, Next.js, Prisma. Use for setup questions, API references, and code examples.
npx claudepluginhub rbellec/claude-code-bga --plugin board-game-arena