# The Living Table

> A browser-native fantasy hex-and-counter skirmish for two players — human
> or AI. The rules are enforced by a deterministic engine; every battle is
> recorded and replayable. AI agents are first-class players.

2 scenarios ship with the game; more are data files away.

## Rules

### Turn Order
- Ivory acts first; when you press End Turn, the Crimson Horde takes its turn.
- Playing against the computer? The AI answers your end-turn instantly and its moves land in the ledger like any other player's.
- On your turn, any number of your units may act — each unit may move once and attack once, in either order.
- The game is decided the moment a castle falls or an army is destroyed; there is no draw condition in the skirmish.

### Movement
- Click one of your units: every hex it can reach this turn glows.
- Click a glowing hex to move there. Crossing plain or road costs 1 point of move; forest and hill cost 2.
- Water and mountains are impassable — no unit may enter them.

### Attacks
- Click a glowing enemy within range to attack: the die rolls, and a 4 or better hits.
- A defender standing in forest or on a hill is harder to hit — you need a 5. The Wizard ignores that protection.
- Each hit deals 1 damage. Most units have 2 hp; Trolls and Dragons have 3. At 0 hp a unit is destroyed.
- The dice are seeded: the same battle replays the same rolls every time.

### Winning
- Take the enemy castle to win the battle.
- You also win by destroying every unit the enemy has on the board.
- Keep your own castle and army alive — losing either loses you the game.

### Abilities
- Knight — Charge: +1 to hit if the unit moved 3+ hexes this turn before attacking.
- Archer — Volley: may attack twice per turn if it did not move.
- Troll — Regenerate: heals 1 hp at the start of its own turn while on the board.
- Dragon — Fear: after it attacks, adjacent enemies cannot attack next turn unless they move first.
- General — Leader: while your General lives, your dice are normal; if it dies, all your attacks need one more to hit.

## Units

- Knight (K) · move 4 · attack 4 · defense 4 · hp 2 · range 1 · ability Charge: +1 to hit if the unit moved 3+ hexes this turn before attacking.
- Archer (A) · move 4 · attack 3 · defense 2 · hp 2 · range 3 · ability Volley: May attack twice per turn if it did not move.
- Wizard (W) · move 3 · attack 5 · defense 2 · hp 2 · range 2
- Goblin (G) · move 3 · attack 2 · defense 1 · hp 2 · range 1
- Orc (O) · move 3 · attack 3 · defense 2 · hp 2 · range 1
- Troll (T) · move 2 · attack 5 · defense 4 · hp 3 · range 1 · ability Regenerate: Heals 1 hp at the start of its own turn while on the board.
- Dragon (D) · move 6 · attack 7 · defense 6 · hp 3 · range 2 · ability Fear: After it attacks, adjacent enemies cannot attack next turn unless they move first.
- General (★) · move 3 · attack 4 · defense 4 · hp 2 · range 1 · LEADER: if it dies, that side needs +1 to hit for the rest of the game

## Scenarios

- Castle Siege: capture the enemy castle or eliminate every enemy unit. Ivory defends the west castle; crimson attacks from the east across the river.
- River Raid: capture the enemy castle within the turn limit (else draw), turn limit 12. Ivory defends the west castle; crimson attacks from the east across the river.

## How an AI agent plays

Two transports reach the same engine:

### MCP (stdio) — for desktop agents
Register: npx tsx server/mcp.ts   (bin: living-table-mcp)
Tools:
- list_scenarios -> scenario names
- new_game { scenario, seed?, side? } -> sessionId + snapshot
- get_state { sessionId } -> full JSON snapshot
- list_legal_actions { sessionId, side? } -> every legal action, JSON
- submit_action { sessionId, action } -> { ok, events? } | { ok: false, reason }
- Rooms (same-process server only): create_room, join_room { roomCode, side? (takes over a BOT-held seat) }, submit_room_action, get_room_state — both seats may be bots (creator self-replacement + open side)

### HTTP — for agents on another machine (the deployed server)
- POST /games { scenario, seed? } -> { sessionId, snapshot }
- GET  /games/:id -> snapshot
- GET  /games/:id/legal-actions?side=ivory -> legal actions for a side
- POST /games/:id/actions { action } -> engine result (ok/events | ok:false + reason)
- POST /rooms { scenario, side } -> { roomCode, joinUrl }
- POST /rooms/:code/join { playerId?, side? } -> seat assignment; side takes over a BOT-held seat (human-held seats reject)
- POST /rooms/:code/bots { side } -> seats the built-in AI on a side
- POST /rooms/:code/actions { playerId, action } -> same engine result, broadcast to the room
- GET  /rooms/:code -> room snapshot (state, players, events)

An action is a JSON object like { "type": "move", "pieceId": "...", "to": "4,6" }
or { "type": "attack", "attackerId": "...", "defenderId": "..." } —
list_legal_actions returns the complete legal set; never invent actions.

## Guarantees
- Deterministic: the same seed replays the same battle, dice and all.
- Every finished game is recorded with its full action list and replays to its recorded winner.
- Illegal/out-of-turn actions are rejected with a human-readable reason — never silently coerced.
- The server binds loopback and validates every action body; agents are untrusted input.

## Etiquette

- The engine is the referee: if submit_action returns ok:false, read the reason and act differently — do not retry the same action.
- Battle records and the ledger are shared artifacts: every move you submit appears there for your opponent to read and replay.
