Complete, machine-oriented ruleset of BangArena — a browser-based Wild West card game for 4–8 players with hidden roles (the Marshal's role is public), where humans, programmed bots and, experimentally and by invitation, language-model agents play by the same rules. Generated from the game data the server uses; ids in code are the contract.
Act only by choosing exactly one item of legalActions of the current DecisionRequest and echoing its actionId. Never invent commands or arguments.
You see what a human in your seat sees: your own hand and role, every public fact of the table, public events and your private events. Other hands are card backs; hidden roles are null.
The Marshal's role is public from the deal; the other roles are hidden until elimination or the end of the match (on an early end — abandoned / force_end — they stay hidden); you always see your own role. Infer allegiance from public behaviour (who attacks whom), never from assumptions about seat order.
There is no free-text channel. Human table signals (role claims, reactions, marks) are ready-made presets only and are not passed to agents over REST / MCP (you get the PlayerView and legalActions). The word «reactions» in an observation means the Ricochet / Shot answer mechanic, not human emotes.
Every decision has a deadline. A missing or illegal answer is replaced by the server (see §14).
law_clears_table: All living players have team != raiders and team != lone (the impostor is team raiders). Dead aides still win with marshal.
marshal_dead_and_not_lone_only: Marshal is dead AND living count != 1 with that living player team == lone. All raiders win including dead raiders.
sole_survivor: Exactly one living player and that player team == lone.
marshal_killed_by_raider_while_impostor_alive: LEGACY (sim only, W4R_DEFAULTS.impostorWin='raider_kill'): Marshal killed by a raider while the impostor lives → impostor wins alone. Live rule since IMP-2 (11.10.2026): the impostor is team raiders and wins with them.
lones_only_left: Role mode partners only. Every living player is a lone → all lones win (dead lones too).
Role distribution — classic and partners modes
seats
marshal
aide
raider
lone
4
1
0
2
1
5
1
1
2
1
6
1
1
3
1
7
1
2
3
1
8
1
2
3
2
Role distribution — impostor mode, the Double Agent (5+ seats; below 5 the table plays classic)
legacy («Old Classic»): the classic distribution with the original card-game rules — Cover = draw a card, hearts succeed; innate cover stacks with a Cover card; a failed Jail skips the whole turn; the Flask does not heal with two players alive; no High Noon; no glory; marshal +1 maxHp at every table, no lone bonus; char_01 4 maxHp; raider bounty 3 cards.
partners: same distribution as classic; if every living player is a lone, all lone players win (lones_only_left).
impostor (the Double Agent): team raiders, hidden. At the deal the marshal privately learns two seats as aide — the impostor and one random real aide (knownRoles, seat event marshal_briefing); the impostor and every raider privately know each other (seat event team_briefing). +1 maxHp at 8 seats.
marshal: public from the deal, +4 seats +2, 5 seats +0, 6 seats +2, 7 seats +1, 8 seats +1 maxHp, cannot be jailed, takes the first turn.
Phases: setup → per turn draw → play (⇄ reaction) → discard → next living seat … → ended.
turnSeat := marshal; then next living seat in seat order (wraps around)
on turn start:
1. Dynamite check (if in play before you) — spades 2..9: 3 damage, discard it; else pass it to the next living seat
2. Jail check (if jailed) — hearts: escape, normal turn; else draw 2 but skip play (→ discard); Jail is discarded either way
3. draw 2 (automatic; some characters open a pendingChoice instead)
play: any number of cards; play_shot at most once per turn (unless unlimited; from round 8, High Noon, unlimited for all); end_play to finish
reaction: opened by an attack; only pendingReaction.targetSeat (or the head of reactionQueue) acts
discard: if hand > hp, discard exactly (hand − hp) cards
deck empty → discard pile is shuffled into the deck; if both are empty, draw yields no card, Dynamite passes without exploding, Jail frees the seat, and card-based Cover fails. Wagon/Caravan/Bazaar cannot draw their own resolving UID.
ring(a, b) = min steps between a and b around the ring of LIVING seats
distance(a → b) = max(1, ring(a, b) + (b has Horse ? 1 : 0) − (a has Scope ? 1 : 0))
range(a) = weapon.weaponRange, or 1 (Revolver) without a weapon
play_shot legal ⇔ distance(a → b) ≤ range(a) and the Shot limit allows it
play_steal legal ⇔ distance(a → b) ≤ 1; play_duel / play_discard_any / Jail: any living seat
built-in: char_09 always has a Horse, char_11 always has a Scope
Draws 1 card from the deck for every hit taken (even Dynamite) — 2 cards if the hit leaves them at 2 ♥ or less.
—
char_02
Red Suit
Красная Масть
4
In the draw, reveals the 2nd card: if it's red, draws 1 more.
—
char_03
Two-Faced
Двуликая
4
Can play a Shot as a Ricochet and a Ricochet as a Shot.
legal items with args.playedAs ("shot" / "miss") — a Ricochet listed as a Shot and back
char_04
Tollman
Данник
3
When hit by a player, takes a random card from their hand.
—
char_05
Grip
Хват
4
May take the first draw at random from another player's hand.
pending choice → choose_first_draw
char_06
Iron Brow
Железный Лоб
4
Always has Cover, card or no card; a Cover card adds no second check.
—
char_07
Three Eyes
Три Глаза
4
In the draw, looks at the top 3: keeps 2, puts 1 back on top.
pending choice → choose_look_top
char_08
Lucky
Счастливчик
4
On a check — two tries, the better one counts.
—
char_09
Pass Shadow
Тень Перевала
3
Always mounted: a built-in Horse, they count as 2 farther away.
—
char_10
Vulture Jr.
Стервятник-младший
4
May take the first draw from the top of the discard pile. When someone is eliminated, draws 2 cards from the deck.
pending choice → choose_first_draw
char_11
Eagle Eye
Орлиный Глаз
4
Shoots farther: built-in Scope.
—
char_12
Bonesetter
Костоправ
5
Any time: discard 2 cards to heal 1 ♥. Not above max, not from zero.
use_discard_heal items
char_13
Double Barrel
Двойной Ствол
4
Point-blank (distance 1), dodging their Shot takes 2 Ricochets (Cover counts as one). Farther away, one is enough.
targets of their Shot need 2 Ricochets
char_14
Empty Hands
Пустые Руки
4
Hand empty? Draws 1 card right away.
—
char_15
Scavenger
Падальщик
4
When someone is eliminated, takes their hand and cards in play.
—
char_16
Mad Dog
Бешеный
4
No shot limit: play as many Shots per turn as you like. And sees 1 farther: built-in Scope.
play_shot stays legal after the first Shot
maxHp of a seat = character maxHp + role extraMaxHp; the Marshal's bonus depends on the seat count (4 seats +2, 5 seats +0, 6 seats +2, 7 seats +1, 8 seats +1; role_table.json marshalExtraHp). Start hand = maxHp cards; the Marshal also gets extra start cards by seat count (6 seats +2, 7 seats +1, 8 seats -1; role_table.json roleStartCards.marshal, not in legacy). Hidden roles (Deputy, Raider, Loner, Double Agent) get no ♥ and no start-card bonus (PRIV-1): their start ♥ and hand equal the character's ♥, so the public deal reveals no hidden role.
play_shot at you → play_miss (Ricochet) | play_heal_self (only at hp 1 with 3+ living) | pass_reaction (−1 hp). Cover in play triggers an automatic 50/50 coin flip first (no card): success cancels the hit.
play_aoe_shot → each other living seat answers in turn, same options as a Shot.
play_raid → each other living seat: play_raid_discard_shot | pass_reaction (−1 hp).
play_duel → target first, then alternating: play_duel_discard_shot | pass_reaction (that side loses 1 hp, duel ends).
A Jail / Dynamite check flips the top deck card; suit and rank decide. A Cover check is a fair coin flip from the match RNG (event cover_check with coin: true, flips, no card). Public events.
Cover: 50 % → hit cancelled (suits do not matter; Owner 09.10.2026). char_06 innate Cover + a Cover card = one flip (Owner 10.10.2026). Jail: hearts → escape. Dynamite: spades 2–9 → explodes (3 damage).
High Noon (Owner 10.10.2026): from round 8 (rounds count at the Marshal's turns; public event high_noon { round }) there is no Shot quota for anyone, Cover — card or innate — is not checked, and nobody heals (NOON-2, owner 12.10.2026: play_heal_self, play_heal_all and use_discard_heal are not legal).
char_08 (Lucky) gets two tries on any check (two cards for Jail / Dynamite, two flips for Cover) and takes the better.
hp reaches 0 → eliminated: role revealed to all, hand and in-play cards discarded (char_15 Scavenger takes them).
Killer = source of the last damage (in a Duel: the other duellist). Dynamite has no killer.
Glory (not in legacy): the killer gets a star (glory_star, public) and then answers a pending choice glory with choose_glory — range (+1 weapon range), heart (+1 maxHp and heal 1) or sight (every draw: look at the top 3, keep 2; offered once, not to characters that own the draw step). Stars and upgrades are public: players[].glory = { stars, range, heart, sight }.
Bounty: killing a seat that has stars also takes one of its stars (+ an upgrade choice) and draws 1 card per star of the victim (glory_bounty).
Last bullet: the marshal gets a star when his last aide is out.
legacy («Old Classic»): no stars; the killer of a raider draws 3 cards (kill_reward event; 2 in the old non-glory rules).
Penalty: a marshal who kills an aide discards hand, in-play cards and weapon (kill_penalty event).
state version; also observationVersion of the request
public
phase
setup | draw | play | reaction | discard | ended
public
turnSeat, you
whose turn; your seat
public
pendingReaction, reactionQueue
the attack being answered and the queue of answers
public
pendingChoice
a character choice in progress (look-top cards only for the chooser)
redacted
players[i].hp, maxHp, alive, characterId
per seat
public
players[i].roleId
own role, the Marshal, eliminated players, everyone once the match has ended normally (stays hidden on an early end: abandoned / force_end); else null with roleHidden: true
REST: GET /api/ai/rooms/{roomId}/observe?seat=N → DecisionRequest (+ legal_mask alias); POST /api/ai/rooms/{roomId}/decision with the body above. Header x-agent-token: <seat agent token>.
MCP: POST /mcp, tools arena_get_rules, arena_get_observation, arena_list_legal_actions, arena_get_decision_request, arena_submit_decision.
Rules summary as JSON: GET /api/arena/rules.
commandId is the idempotency key: resending the same commandId returns the same result (duplicate).
args is optional; it is merged over the item's argsSchema (use it to choose the cards of discard).
observationVersion (or basedOnVersion) lets the server reject a decision taken on a stale state.
Take the first draw from the deck, a random hand card of a seat (Grip) or the discard top (Vulture Jr.).
choose_look_top
pending choice (Three Eyes)
{ choiceId, putBackUid }
look_top:<choiceId>:<cardUid>
Keep 2 of the top 3; the named card goes back on top.
choose_glory
pending choice (a star of glory: elimination, bounty or the Marshal's last bullet)
{ choiceId, perk: 'range' | 'heart' | 'sight' }
glory:<choiceId>:<perk>
range = +1 weapon range; heart = +1 max hp and heal 1; sight = every draw look at the top 3 and keep 2 (offered once, not to characters that already own the draw step). Default on timeout: heart.
choose_prayer
pending choice (Preacher; staged character, not dealt yet)
{ choiceId, pray: boolean }
prayer:<choiceId>:yes|no
After another seat is eliminated: pray for a 20% return with 1 hp (once per game) or decline and keep the attempt.
use_peek_role
phase play, your turn, no pending choice (Tracker; staged character, not dealt yet)
{ targetSeat }
peek:<seat>
Learn the exact hidden role of a living seat; only you see the result. 1 use at 4–5 seats, 2 at 6–7, 3 at 8.
use_discard_heal
phase play / discard / reaction, any seat's turn, no pending choice (Bonesetter)
{ cardUids: [a, b] }
discard_heal:<a>:<b>
Discard 2 cards to heal 1 (not above max, not from 0).
Decision deadline: 30 s (deadlineAt); a disconnected human seat gets 8 s; an AI seat has a per-seat timeout (default 15 s).
No answer in time → the server answers for the seat (pass / end_play / default discard).
Illegal actionId: policy reask_then_random — the first illegal answer gets HTTP 400 with reask: true (ask the model again), the next one is replaced by a random legal item. Seats may be configured skip or random.
During the pause between turns a decision may be rejected as retryable — fetch a fresh request and answer again.