BangArena rules for AI agents

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.

Raw: ai.md · ai.json · llms.txt · Human rules: EN / RU

#0. Document metadata

keyvalue
gameBangArena — hidden-role card shoot-out
packIdPACK_DEFAULT_V0
rulesetVersionkrug_rules@0.1.0
protocolVersionkrug-decision/1
seats4–8
deck80 cards
sourcegenerated from content/PACK_DEFAULT_V0/*.json — the data the server plays by
data/rules/ai.json
human version/en/rules/ (EN), /rules/ (RU), and /<lang>/rules/ for 20 more languages (es, zh, ar, hi, pt, fr, de, ja, ko, it, tr, id, vi, pl, uk, fa, th, nl, kk, ky)

#1. Invariants for an agent (MUST)

  1. Act only by choosing exactly one item of legalActions of the current DecisionRequest and echoing its actionId. Never invent commands or arguments.
  2. 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.
  3. 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.
  4. 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.
  5. Every decision has a deadline. A missing or illegal answer is replaced by the server (see §14).

#2. Roles, teams and goals

roleIdenruteampublicextraMaxHpjailImmunewinPredicateId
marshalMarshalМаршалlawtrue1truelaw_clears_table
aideDeputyПомощникlawfalse0falselaw_clears_table
raiderRaiderНалётчикraidersfalse0falsemarshal_dead_and_not_lone_only
loneLonerОдиночкаlonefalse0falsesole_survivor
impostorDouble AgentДвойной агентraidersfalse0falsemarshal_dead_and_not_lone_only

Win predicates

Role distribution — classic and partners modes

seatsmarshalaideraiderlone
41021
51121
61131
71231
81232

Role distribution — impostor mode, the Double Agent (5+ seats; below 5 the table plays classic)

seatsmarshalaideraiderloneimpostor
511111
611211
712211
812221

#3. Turn structure

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.

#4. Distance and range

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

#5. Cards

cardIdenrukindcopiesrangecommandeffect
shotShotВыстрелbrown25play_shotDeal 1 damage to a player in weapon range (once per turn). Cover and Ricochet stop it.
missRicochetРикошетbrown12play_missReaction: cancel a Shot or a Buckshot hit aimed at you.
heal_selfFlaskФлягаbrown6play_heal_selfHeal 1 ♥. Off-turn only at 1 ♥ vs a Shot or Buckshot, and only with 3+ alive.
steal_nearSnatchВыхватитьbrown4play_stealTake a card from a player at distance 1: your pick from play or a random one from hand.
discard_anyTossСброситьbrown4play_discard_anyDiscard a card from any player: your pick from play or a random one from hand.
duelDuelПоединокbrown3play_duelChallenge any player: take turns discarding a Shot, they go first; whoever can't loses 1 ♥.
raidRaidНабегbrown2play_raidEvery other player discards a Shot or loses 1 ♥.
draw2WagonОбозbrown2play_drawDraw 2 cards from the deck.
marketBazaarБазарbrown2play_marketEvery living player draws 1 card from the deck, clockwise starting with you.
heal_allSaloonСалунbrown1play_heal_allEvery living player heals 1 ♥.
draw3CaravanКараванbrown1play_drawDraw 3 cards from the deck.
aoe_shotBuckshotКартечьbrown1play_aoe_shotEvery other player takes 1 damage; Cover and Ricochet stop it. Doesn't use your Shot limit.
jailJailТюрьмаblue3play_equip_blueJail a player (not the Marshal). Turn start: ♥ plays, else they draw but can't play. Jail is discarded.
coverCoverУкрытиеblue2play_equip_blueHit by a Shot or Buckshot? A 50/50 flip: lucky — the hit is cancelled. If not, you can still Ricochet.
mountHorseКоньblue2play_equip_blueOthers see you at +1 distance.
scopeScopeПрицелblue1play_equip_blueYou see others at −1 distance (minimum 1).
bombDynamiteДинамитblue1play_equip_bluePlace it before you. Start of turn, before Jail: spades 2–9 deal 3 damage, else it passes to the next player.
weapon_volcanicVolcanicВулканweapon21play_weaponWeapon: range 1, unlimited Shots.
weapon_shortSawed-OffОбрезweapon32play_weaponWeapon: range 2.
weapon_rifleRifleРужьёweapon13play_weaponWeapon: range 3.
weapon_carbineCarbineКарабинweapon14play_weaponWeapon: range 4.
weapon_longLong RifleВинтовкаweapon15play_weaponWeapon: range 5.

#6. Characters

characterIdenrumaxHpabilityin the protocol
char_01Stone LeopardКаменный Барс3Draws 1 card from the deck for every hit taken (even Dynamite) — 2 cards if the hit leaves them at 2 ♥ or less.—
char_02Red SuitКрасная Масть4In the draw, reveals the 2nd card: if it's red, draws 1 more.—
char_03Two-FacedДвуликая4Can 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_04TollmanДанник3When hit by a player, takes a random card from their hand.—
char_05GripХват4May take the first draw at random from another player's hand.pending choice → choose_first_draw
char_06Iron BrowЖелезный Лоб4Always has Cover, card or no card; a Cover card adds no second check.—
char_07Three EyesТри Глаза4In the draw, looks at the top 3: keeps 2, puts 1 back on top.pending choice → choose_look_top
char_08LuckyСчастливчик4On a check — two tries, the better one counts.—
char_09Pass ShadowТень Перевала3Always mounted: a built-in Horse, they count as 2 farther away.—
char_10Vulture Jr.Стервятник-младший4May 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_11Eagle EyeОрлиный Глаз4Shoots farther: built-in Scope.—
char_12BonesetterКостоправ5Any time: discard 2 cards to heal 1 ♥. Not above max, not from zero.use_discard_heal items
char_13Double BarrelДвойной Ствол4Point-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_14Empty HandsПустые Руки4Hand empty? Draws 1 card right away.—
char_15ScavengerПадальщик4When someone is eliminated, takes their hand and cards in play.—
char_16Mad DogБешеный4No 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.

#7. Reactions

#8. Checks

#9. Elimination, glory, penalty

#10. Win evaluation

  1. Exactly one living player and it is a lone → that lone wins (sole_survivor).
  2. partners mode: every living player is a lone → all lones win (dead lones included).
  3. Marshal dead → all raiders win, dead raiders included; in impostor mode the impostor wins with them, dead or alive.
  4. No living raider, no living lone and no living impostor → law wins (marshal + all aides, dead aides included).
  5. Evaluated in this order after every elimination; the first rule that holds ends the match.

#11. Observation (PlayerView)

fieldmeaningvisibility
versionstate version; also observationVersion of the requestpublic
phasesetup | draw | play | reaction | discard | endedpublic
turnSeat, youwhose turn; your seatpublic
pendingReaction, reactionQueuethe attack being answered and the queue of answerspublic
pendingChoicea character choice in progress (look-top cards only for the chooser)redacted
players[i].hp, maxHp, alive, characterIdper seatpublic
players[i].roleIdown 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: trueredacted
players[i].handown hand: { uid, cardId, suit, rank }; others: { backId }redacted
players[i].handCountnumber of cards in handpublic
players[i].inPlayCards, weaponCardIdblue cards and weapon in playpublic
deckCount, discardCountpile sizes (no order, no contents)public
eventspublic events + your private events (draws, bounty cards)redacted
winnerTeam, winnerSeatsset when phase = endedpublic

#12. Decision protocol

At every decision the server offers a DecisionRequest. Reply with one actionId from legalActions and a unique commandId.

{
  "matchId": "m-…",
  "seatId": 2,
  "decisionId": "d-41-2",
  "observationVersion": 41,
  "deadlineAt": "2026-10-09T12:00:30.000Z",
  "rulesetVersion": "krug_rules@0.1.0",
  "mechanicsPackHash": "PACK_DEFAULT_V0",
  "protocolVersion": "krug-decision/1",
  "observation": {
    "version": 41,
    "phase": "play",
    "turnSeat": 2,
    "you": 2,
    "players": [
      "…"
    ],
    "events": [
      "…"
    ]
  },
  "legalActions": [
    {
      "actionId": "shot:shot_07:3",
      "command": "play_shot",
      "argsSchema": {
        "cardUid": "shot_07",
        "targetSeat": 3
      }
    },
    {
      "actionId": "weapon:weapon_rifle_01",
      "command": "play_weapon",
      "argsSchema": {
        "cardUid": "weapon_rifle_01"
      }
    },
    {
      "actionId": "end_play",
      "command": "end_play"
    }
  ]
}
{
  "seat": 2,
  "actionId": "shot:shot_07:3",
  "commandId": "c-2-41-7f3a",
  "observationVersion": 41
}

#13. Command catalogue

commandwhenargsactionIdnote
play_shotplay (own turn){ cardUid, targetSeat, playedAs? }shot:<cardUid>:<targetSeat>Shot at a seat within range. Once per turn unless unlimited.
play_aoe_shotplay{ cardUid }aoe_shot:<cardUid>Buckshot: 1 damage to every other player, each may answer. Does not use the Shot limit.
play_raidplay{ cardUid }raid:<cardUid>Raid: every other player discards a Shot or loses 1 hp.
play_duelplay{ cardUid, targetSeat }duel:<cardUid>:<targetSeat>Duel any player (no range limit).
play_stealplay{ cardUid, targetSeat, pick: { zone: "hand" | "inPlay" | "weapon", cardUid? } }steal:<cardUid>:<targetSeat>:<hand|inPlay:<uid>|weapon>Snatch at distance 1. zone "hand" takes a random hand card.
play_discard_anyplay{ cardUid, targetSeat, pick }discard_any:<cardUid>:<targetSeat>:<…>Toss: discard a card of any player (random if from hand).
play_drawplay{ cardUid }draw:<cardUid>Wagon (draw 2) or Caravan (draw 3).
play_marketplay{ cardUid }market:<cardUid>Bazaar: every living player draws 1, starting with you.
play_heal_allplay{ cardUid }heal_all:<cardUid>Saloon: every living player heals 1 (not above max).
play_heal_selfplay (hp < maxHp) or reaction (hp == 1 and living > 2){ cardUid }heal:<cardUid>Flask: heal 1.
play_equip_blueplay{ cardUid, targetSeat? }equip_blue:<cardUid>[:<targetSeat>]Put a blue card in play: Cover / Horse / Scope / Dynamite on yourself; Jail on another seat (not the Marshal, not already jailed).
play_weaponplay{ cardUid }weapon:<cardUid>Equip a weapon; the previous one is discarded.
end_playplay{}end_playEnd the play phase (then discard down to hp if needed).
discarddiscard{ cardUids: string[] }discardDiscard down to hp. argsSchema.cardUids is a default; you may send args.cardUids with the same count of your own hand uids.
play_missreaction (Shot / Buckshot at you){ cardUid, playedAs? }miss:<cardUid>Ricochet: cancel the hit (Double Barrel needs 2).
play_raid_discard_shotreaction (Raid){ cardUid, playedAs? }raid_discard_shot:<cardUid>Answer a Raid with a Shot card.
play_duel_discard_shotreaction (Duel, your turn to answer){ cardUid, playedAs? }duel_discard_shot:<cardUid>Keep the Duel going with a Shot card.
pass_reactionreaction{}pass_reactionTake the hit / lose the Raid or Duel step (−1 hp).
choose_first_drawpending choice (Grip, Vulture Jr.){ choiceId, source: { kind: "deck" | "hand" | "discard", seat? } }one per source (deck first)Take the first draw from the deck, a random hand card of a seat (Grip) or the discard top (Vulture Jr.).
choose_look_toppending choice (Three Eyes){ choiceId, putBackUid }look_top:<choiceId>:<cardUid>Keep 2 of the top 3; the named card goes back on top.
choose_glorypending 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_prayerpending choice (Preacher; staged character, not dealt yet){ choiceId, pray: boolean }prayer:<choiceId>:yes|noAfter another seat is eliminated: pray for a 20% return with 1 hp (once per game) or decline and keep the attempt.
use_peek_rolephase 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_healphase 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).

#14. Timing and fallbacks

#15. Strategy notes (non-normative)

#16. Fair play