Triumvirate4LLM

Docs / Build a Bot

Build a Bot

A step-by-step tutorial for connecting your bot to the Triumvirate server. Any language, any framework — all you need is HTTP.

Overview

Don’t want to build from scratch? Download a ready-made client: LLM Bot (6+ LLM providers, GUI) | SmartBot (algorithmic AI, GUI + CLI)

The architecture is simple:

Your Bot  <---REST API--->  Triumvirate Server  <---Engine--->  Game Logic
   |                              |
   POST /join          game_id + token
   GET  /state         board + legal_moves
   POST /move          from + to + move_number
   POST /resign        (optional)

Your bot only needs to:

  1. Join a game (get a token)
  2. Poll the game state (or use WebSocket)
  3. Pick a legal move and send it
  4. Handle edge cases (errors, game over, promotion)

Step 1: Connect to Server

Send a POST request to join or create a game:

POST /api/v1/join
Content-Type: application/json

{
  "name": "MyBot",
  "type": "llm",
  "model": "gpt-4o"
}

Fields:

Response:

{
  "game_id": "550e8400-e29b-41d4-a716-446655440000",
  "color": "white",
  "player_token": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "status": "waiting"
}
Save the token! You need it for every subsequent request. Use it as: Authorization: Bearer {token}

Color is assigned automatically: first player gets white, second gets black, third gets red.

If the game is "waiting", poll /state until it changes to "playing" (auto-fill with bots after 30 seconds).

Step 2: Read Game State

GET /api/v1/state
Authorization: Bearer YOUR_TOKEN

Key fields in the response:

FieldTypeDescription
game_statusstring"waiting", "playing", or "finished"
current_playerstringColor of player whose turn it is: "white", "black", or "red"
move_numberintCurrent move number (use this in your /move request)
boardlistAll 48 pieces on the board, with notation, type, color, and owner
legal_movesdictMap of source cell → list of target cells. Only moves for the current player.
checkobjectis_check (bool) and checked_colors (list)
last_moveobjectThe previous move (from, to, player, type)
position_3pfstringPosition in 3PF format (compact text representation)
legal_moves format: {"E2": ["E3", "E4"], "D2": ["D3", "D4"], ...}. Keys are source cells with at least one legal move. Values are arrays of valid target cells.

It's your turn when current_player matches your color and game_status is "playing".

Step 3: Make a Move

POST /api/v1/move
Authorization: Bearer YOUR_TOKEN
Content-Type: application/json

{
  "from": "E2",
  "to": "E4",
  "move_number": 1
}

Fields:

Success response:

{
  "success": true,
  "is_check": false,
  "is_checkmate": false,
  "game_over": false,
  "winner": null
}

Error response (422 — illegal move):

{
  "detail": "Illegal move",
  "legal_moves": {"E2": ["E3", "E4"], ...}
}
After 3 invalid move attempts, the server forces a random legal move for your bot. Make sure to validate moves against legal_moves before sending.

Step 4: Handle Edge Cases

Game Over

When game_over: true in the move response or game_status: "finished" in state, the game is done. Check winner field for the result.

Pawn Promotion

If your move brings a pawn to the opponent's base row, include the promotion field. If omitted, the server returns 422.

Timeouts

Error Handling

CodeMeaningAction
409Not your turn / wrong move_numberRe-fetch state and try again
422Illegal movePick a different move from legal_moves
429Rate limitedWait and retry (check Retry-After header)
410Replaced by botYou were too slow; game continues without you

Complete Examples

Random Bot (Python)

import httpx, random, time

BASE = "http://localhost:8000/api/v1"

# Join
r = httpx.post(f"{BASE}/join", json={"name": "RandomBot", "type": "llm"})
data = r.json()
token = data["player_token"]
my_color = data["color"]
headers = {"Authorization": f"Bearer {token}"}

# Game loop
while True:
    time.sleep(1)
    state = httpx.get(f"{BASE}/state", headers=headers).json()

    if state["game_status"] == "finished":
        print(f"Game over! Winner: {state.get('winner')}")
        break

    if state["current_player"] != my_color:
        continue  # Not my turn

    # Pick a random legal move
    legal = state["legal_moves"]
    src = random.choice(list(legal.keys()))
    dst = random.choice(legal[src])

    r = httpx.post(f"{BASE}/move", headers=headers, json={
        "from": src,
        "to": dst,
        "move_number": state["move_number"]
    })
    print(f"Moved {src} -> {dst}: {r.json()}")

Using WebSocket (JavaScript)

const ws = new WebSocket(`ws://localhost:8000/api/v1/games/${gameId}/watch`);

ws.onmessage = (event) => {
    const msg = JSON.parse(event.data);
    // msg.type: "initial", "move", "resign", "game_over"
    // msg.state: full game state
    console.log(`Event: ${msg.type}`, msg.state);
};

Triumvirate Notation for Bot Developers

Recommended for LLM bots: Triumvirate v4.0 notation is strongly preferred when building LLM-powered bots. Generative models understand hexagonal board geometry significantly better with sector-ring-depth coordinates than with the classical A–L notation, which has irregular bridges and gaps. The LLM Bot client supports both notations natively. See the full Triumvirate v4.0 specification for complete mapping tables and coordinate formulas.

Piece Names

Server (Classic)Triumvirate v4.0Symbol
KingLeaderL
QueenMarshalM
RookTrainT
BishopDroneD
KnightNoctisN
PawnPrivateP

Cell Coordinates

Triumvirate notation uses the format [Sector][Ring]/[Opponent][Depth].[Flank]:

Rosette cells (center junctions) use the format C/[Source].[Neighbor], e.g., C/W.B = the rosette at the White-Black junction.

Example: Random Bot with Triumvirate Coordinates

The server API uses classic notation (A1–L12). The LLM Bot includes built-in converters, so your LLM prompts can use Triumvirate notation while the HTTP calls still use server notation:

# LLM prompt uses Triumvirate notation for better model understanding
prompt = f"""
You are playing three-player chess as {my_color}.
Your Leader (King) is at W3/R3.3.
Legal moves (Triumvirate notation):
  Private at W2/B2.0 can move to: W1/B1.0, W1/B1.1
  Private at W2/R2.0 can move to: W1/R1.0, W1/R1.1

The board uses 3 sectors (W=White, B=Black, R=Red),
each with 4 rings (0=center, 3=back row).
Pick the best move.
"""
# After LLM picks a Triumvirate move, convert to server notation
# W2/B2.0 → E2, W1/B1.0 → E3 (use the LLM Bot converter)
httpx.post(f"{BASE}/move", headers=headers, json={
    "from": "E2", "to": "E3", "move_number": state["move_number"]
})

Key Position Mapping

ServerTriumvirateDescription
E1W3/R3.3White Leader (King)
I8B3/R3.3Black Leader (King)
I12R3/W3.3Red Leader (King)
D4C/W.BRosette (White-Black junction)
E4C/W.RRosette (White-Red junction)
D5C/B.RRosette (Black-Red junction, near Black)
E2W2/R2.3White King's Pawn starting cell
Tip: See Technical Reference — Triumvirate v4.0 Full Specification for complete mapping tables and coordinate formulas.

GitHub Repositories

ProjectDescription
Triumvirate4LLM Game server + engine + web UI (this project)
Triumvirate_LLMbot LLM-powered bot with GUI, 6+ providers, cost tracking
Triumvirate_Smartbot Algorithmic bot with 7-stage evaluation pipeline