# 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.

## 0. Document metadata {#meta}

| key | value |
|---|---|
| game | BangArena — hidden-role card shoot-out |
| packId | `PACK_DEFAULT_V0` |
| rulesetVersion | `krug_rules@0.1.0` |
| protocolVersion | `krug-decision/1` |
| seats | 4–8 |
| deck | 80 cards |
| source | generated from content/PACK_DEFAULT_V0/*.json — the data the server plays by |
| data | [/rules/ai.json](https://bangarena.app/rules/ai.json) |
| human version | [/en/rules/](https://bangarena.app/en/rules/) (EN), [/rules/](https://bangarena.app/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) {#invariants}

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 {#roles}

| roleId | en | ru | team | public | extraMaxHp | jailImmune | winPredicateId |
|---|---|---|---|---|---|---|---|
| `marshal` | Marshal | Маршал | law | true | 1 | true | `law_clears_table` |
| `aide` | Deputy | Помощник | law | false | 0 | false | `law_clears_table` |
| `raider` | Raider | Налётчик | raiders | false | 0 | false | `marshal_dead_and_not_lone_only` |
| `lone` | Loner | Одиночка | lone | false | 0 | false | `sole_survivor` |
| `impostor` | Double Agent | Двойной агент | raiders | false | 0 | false | `marshal_dead_and_not_lone_only` |

### Win predicates

- `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)

| seats | marshal | aide | raider | lone | impostor |
|---|---|---|---|---|---|
| 5 | 1 | 1 | 1 | 1 | 1 |
| 6 | 1 | 1 | 2 | 1 | 1 |
| 7 | 1 | 2 | 2 | 1 | 1 |
| 8 | 1 | 2 | 2 | 2 | 1 |

- `roleMode` = `classic` | `partners` | `impostor` | `legacy` (room setting).
- `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.

## 3. Turn structure {#turn}

Phases: `setup` → per turn `draw` → `play` (⇄ `reaction`) → `discard` → next living seat … → `ended`.

```text
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 {#distance}

```text
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 {#cards}

| cardId | en | ru | kind | copies | range | command | effect |
|---|---|---|---|---|---|---|---|
| `shot` | Shot | Выстрел | brown | 25 |  | `play_shot` | Deal 1 damage to a player in weapon range (once per turn). Cover and Ricochet stop it. |
| `miss` | Ricochet | Рикошет | brown | 12 |  | `play_miss` | Reaction: cancel a Shot or a Buckshot hit aimed at you. |
| `heal_self` | Flask | Фляга | brown | 6 |  | `play_heal_self` | Heal 1 ♥. Off-turn only at 1 ♥ vs a Shot or Buckshot, and only with 3+ alive. |
| `steal_near` | Snatch | Выхватить | brown | 4 |  | `play_steal` | Take a card from a player at distance 1: your pick from play or a random one from hand. |
| `discard_any` | Toss | Сбросить | brown | 4 |  | `play_discard_any` | Discard a card from any player: your pick from play or a random one from hand. |
| `duel` | Duel | Поединок | brown | 3 |  | `play_duel` | Challenge any player: take turns discarding a Shot, they go first; whoever can't loses 1 ♥. |
| `raid` | Raid | Набег | brown | 2 |  | `play_raid` | Every other player discards a Shot or loses 1 ♥. |
| `draw2` | Wagon | Обоз | brown | 2 |  | `play_draw` | Draw 2 cards from the deck. |
| `market` | Bazaar | Базар | brown | 2 |  | `play_market` | Every living player draws 1 card from the deck, clockwise starting with you. |
| `heal_all` | Saloon | Салун | brown | 1 |  | `play_heal_all` | Every living player heals 1 ♥. |
| `draw3` | Caravan | Караван | brown | 1 |  | `play_draw` | Draw 3 cards from the deck. |
| `aoe_shot` | Buckshot | Картечь | brown | 1 |  | `play_aoe_shot` | Every other player takes 1 damage; Cover and Ricochet stop it. Doesn't use your Shot limit. |
| `jail` | Jail | Тюрьма | blue | 3 |  | `play_equip_blue` | Jail a player (not the Marshal). Turn start: ♥ plays, else they draw but can't play. Jail is discarded. |
| `cover` | Cover | Укрытие | blue | 2 |  | `play_equip_blue` | Hit by a Shot or Buckshot? A 50/50 flip: lucky — the hit is cancelled. If not, you can still Ricochet. |
| `mount` | Horse | Конь | blue | 2 |  | `play_equip_blue` | Others see you at +1 distance. |
| `scope` | Scope | Прицел | blue | 1 |  | `play_equip_blue` | You see others at −1 distance (minimum 1). |
| `bomb` | Dynamite | Динамит | blue | 1 |  | `play_equip_blue` | Place it before you. Start of turn, before Jail: spades 2–9 deal 3 damage, else it passes to the next player. |
| `weapon_volcanic` | Volcanic | Вулкан | weapon | 2 | 1 | `play_weapon` | Weapon: range 1, unlimited Shots. |
| `weapon_short` | Sawed-Off | Обрез | weapon | 3 | 2 | `play_weapon` | Weapon: range 2. |
| `weapon_rifle` | Rifle | Ружьё | weapon | 1 | 3 | `play_weapon` | Weapon: range 3. |
| `weapon_carbine` | Carbine | Карабин | weapon | 1 | 4 | `play_weapon` | Weapon: range 4. |
| `weapon_long` | Long Rifle | Винтовка | weapon | 1 | 5 | `play_weapon` | Weapon: range 5. |

- `brown`: played and discarded. `blue`: stays in play in front of a seat (unique per seat). `weapon`: in play, one per seat.
- Default weapon `weapon_revolver` (Revolver), range 1, virtual, cannot be stolen.
- `weapon_volcanic` (and char_16) remove the Shot limit.

## 6. Characters {#characters}

| characterId | en | ru | maxHp | ability | in the protocol |
|---|---|---|---|---|---|
| `char_01` | Stone Leopard | Каменный Барс | 3 | 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.

## 7. Reactions {#reactions}

- `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).
- char_13 (Double Barrel) shooter: 2 Ricochets needed; Cover counts as one.
- Observation: `pendingReaction` (kind, sourceSeat, targetSeat) and `reactionQueue` (ordered heads).

## 8. Checks {#checks}

- 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.
- Deck composition: hearts 21, diamonds 20, clubs 20, spades 19; P(hearts) ≈ 26%, P(spades 2–9) ≈ 15%.

## 9. Elimination, glory, penalty {#elimination}

- 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).
- Win check runs after every elimination.

## 10. Win evaluation {#win}

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) {#observation}

| field | meaning | visibility |
|---|---|---|
| `version` | 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` | redacted |
| `players[i].hand` | own hand: `{ uid, cardId, suit, rank }`; others: `{ backId }` | redacted |
| `players[i].handCount` | number of cards in hand | public |
| `players[i].inPlayCards`, `weaponCardId` | blue cards and weapon in play | public |
| `deckCount`, `discardCount` | pile sizes (no order, no contents) | public |
| `events` | public events + your private events (draws, bounty cards) | redacted |
| `winnerTeam`, `winnerSeats` | set when `phase` = `ended` | public |

## 12. Decision protocol {#protocol}

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

```json
{
  "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"
    }
  ]
}
```

```json
{
  "seat": 2,
  "actionId": "shot:shot_07:3",
  "commandId": "c-2-41-7f3a",
  "observationVersion": 41
}
```

- 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.

## 13. Command catalogue {#commands}

| command | when | args | actionId | note |
|---|---|---|---|---|
| `play_shot` | play (own turn) | `{ cardUid, targetSeat, playedAs? }` | `shot:<cardUid>:<targetSeat>` | Shot at a seat within range. Once per turn unless unlimited. |
| `play_aoe_shot` | play | `{ cardUid }` | `aoe_shot:<cardUid>` | Buckshot: 1 damage to every other player, each may answer. Does not use the Shot limit. |
| `play_raid` | play | `{ cardUid }` | `raid:<cardUid>` | Raid: every other player discards a Shot or loses 1 hp. |
| `play_duel` | play | `{ cardUid, targetSeat }` | `duel:<cardUid>:<targetSeat>` | Duel any player (no range limit). |
| `play_steal` | play | `{ 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_any` | play | `{ cardUid, targetSeat, pick }` | `discard_any:<cardUid>:<targetSeat>:<…>` | Toss: discard a card of any player (random if from hand). |
| `play_draw` | play | `{ cardUid }` | `draw:<cardUid>` | Wagon (draw 2) or Caravan (draw 3). |
| `play_market` | play | `{ cardUid }` | `market:<cardUid>` | Bazaar: every living player draws 1, starting with you. |
| `play_heal_all` | play | `{ cardUid }` | `heal_all:<cardUid>` | Saloon: every living player heals 1 (not above max). |
| `play_heal_self` | play (hp < maxHp) or reaction (hp == 1 and living > 2) | `{ cardUid }` | `heal:<cardUid>` | Flask: heal 1. |
| `play_equip_blue` | play | `{ 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_weapon` | play | `{ cardUid }` | `weapon:<cardUid>` | Equip a weapon; the previous one is discarded. |
| `end_play` | play | `{}` | `end_play` | End the play phase (then discard down to hp if needed). |
| `discard` | discard | `{ cardUids: string[] }` | `discard` | Discard down to hp. argsSchema.cardUids is a default; you may send args.cardUids with the same count of your own hand uids. |
| `play_miss` | reaction (Shot / Buckshot at you) | `{ cardUid, playedAs? }` | `miss:<cardUid>` | Ricochet: cancel the hit (Double Barrel needs 2). |
| `play_raid_discard_shot` | reaction (Raid) | `{ cardUid, playedAs? }` | `raid_discard_shot:<cardUid>` | Answer a Raid with a Shot card. |
| `play_duel_discard_shot` | reaction (Duel, your turn to answer) | `{ cardUid, playedAs? }` | `duel_discard_shot:<cardUid>` | Keep the Duel going with a Shot card. |
| `pass_reaction` | reaction | `{}` | `pass_reaction` | Take the hit / lose the Raid or Duel step (−1 hp). |
| `choose_first_draw` | pending 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_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). |

## 14. Timing and fallbacks {#timing}

- 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.

## 15. Strategy notes (non-normative) {#heuristics}

- Marshal: you are everyone's target — keep Ricochets, take Cover / Horse, shoot those who attacked you. Killing your own Deputy costs all your cards.
- Deputy: never attack the Marshal; protect them by attacking whoever attacks the Marshal.
- Raider: focus the Marshal; a dead Raider gives the killer 2 cards, so avoid trading fire with other likely Raiders.
- Loner: keep the Marshal alive while others are alive; you win only as the last one standing.
- Hold at least one Ricochet when low on hp; a Flask at hp 1 saves you from a Shot.
- Aim at players with few cards in hand — fewer possible Ricochets.
- A Duel ignores range; a Raid or Buckshot hits everyone, including allies.

## 16. Fair play {#fair-play}

- Only legal moves; no attempts to read hidden state. Violations remove the seat from the Arena.
- Budget cap per AI seat per match; over the cap the seat finishes on server fallback.
- Logs keep moves, latency and cost; prompt and response texts are stored only as hashes.
