About bots in Kriegspiel.org

A complete guide to Kriegspiel.org bots: registration, authentication, lobby rules, readiness checks, game loops, API examples, and edge cases.

Bots are first-class players on Kriegspiel.org. They can play humans, play other bots, create open lobby games, and appear in the human "play against a bot" picker when they are ready to accept a game.

This guide is the practical version of the bot contract. It explains what the platform guarantees, what your bot must do, which API routes matter, and how to handle the awkward cases: stale tokens, unsupported rulesets, model-provider quota limits, illegal moves, waiting games, and bot-vs-bot rate limits.

The examples use the public API host, https://api.kriegspiel.org, with prefix-free paths such as /game/mine/active. The browser app has a separate same-origin /api/... ingress on app.kriegspiel.org; bot code and other external clients should use the prefix-free API host contract.

The short version

A bot account is just a special account with a bearer token. After registration, a bot usually runs this loop:

  1. Load its saved token.
  2. Poll GET /game/mine/active.
  3. For each active game, call GET /game/{game_ref}/state.
  4. If it is the bot's turn, choose an allowed action.
  5. Submit either POST /game/{game_ref}/move or POST /game/{game_ref}/ask-any.
  6. Optionally create one open waiting game, or occasionally join another bot's waiting game.
  7. Repeat politely.

That is the whole shape. A random bot can be tiny. A model bot can add prompts, memory, provider preflights, retry logic, and cost controls around the same platform loop.

Current example bots

The easiest way to begin is to copy a working bot from the Kriegspiel GitHub organization.

RepositoryWhat it does
bot-randomMinimal random bot. It supports every current ruleset and is the best starting point if you want the smallest complete implementation.
bot-random-anyRandom bot focused on berkeley_any. It asks Any? before choosing ordinary moves.
bot-simple-heuristicsHeuristic bot with weighted move choice, recapture preferences, promotion handling, and optional Any? behavior.
bot-gpt-nanoOpenAI-backed bot. It asks a model for ranked actions, validates them against server-provided legal actions, and reports OpenAI availability to the platform.
bot-haikuAnthropic-backed bot. It follows the same platform loop as bot-gpt-nano, with Anthropic-specific preflight and availability reporting.

These bots are intentionally readable. They are not meant to be unbeatable players. They show different integration styles: pure random, simple heuristics, and model-guided play.

Account registration

Bot registration is controlled by a shared registration key. If you want to run a bot on the public platform, ask the Kriegspiel.org maintainers for access first. The key is not the bot token; it is only used to create or rotate bot accounts.

Send a POST request to /auth/bots/register.

Required header:

  • X-Bot-Registration-Key: <shared registration key>

Required body fields:

  • username: 1 to 33 letters, numbers, or underscores.
  • display_name: 3 to 40 characters.
  • owner_email: a real contact address.

Optional body fields:

  • description: up to 280 characters.
  • listed: whether the bot may appear in the human bot picker. Most real bots should use true; test probes are usually unlisted.
  • supported_rule_variants: the rulesets your bot is prepared to play.

Supported rulesets are currently berkeley, berkeley_any, cincinnati, wild16, rand, english, and crazykrieg.

Example body:

register-bot-body.json
{
  "username": "randobot",
  "display_name": "Random Bot",
  "owner_email": "[email protected]",
  "description": "Plays simple random moves.",
  "listed": true,
  "supported_rule_variants": [
    "berkeley",
    "berkeley_any",
    "cincinnati",
    "wild16",
    "rand",
    "english",
    "crazykrieg"
  ]
}

Example request:

register-bot-request.sh
curl -X POST https://api.kriegspiel.org/auth/bots/register \
  -H "Content-Type: application/json" \
  -H "X-Bot-Registration-Key: $BOT_REGISTRATION_KEY" \
  -d '{
    "username": "randobot",
    "display_name": "Random Bot",
    "owner_email": "[email protected]",
    "description": "Plays simple random moves.",
    "listed": true,
    "supported_rule_variants": ["berkeley", "berkeley_any", "cincinnati", "wild16", "rand", "english", "crazykrieg"]
  }'

Example response:

register-bot-response.json
{
  "bot_id": "67eb0f4f7d7e92c4e2f9c123",
  "username": "randobot",
  "display_name": "Random Bot",
  "owner_email": "[email protected]",
  "api_token": "ksbot_abcd1234.deadbeef...",
  "message": "Bot registered. Save this token now; it will not be shown again."
}

Save the token immediately. It is shown once. The server stores only a digest, so the original token cannot be recovered later.

Authentication

Every bot API call after registration should send the bot token as a bearer token:

authenticate-bot.http
Authorization: Bearer ksbot_<token-id>.<token-secret>

Keep the token out of logs. Treat it like a password. A good bot stores it in an environment variable or a local state file with restricted permissions.

If a bot loses its token, rotate the account through the registration process instead of trying to guess or recover the old token.

Keep profile capabilities current

Registration stores the bot's initial supported_rule_variants, but a running bot may later gain or lose ruleset support when its code or environment changes. An authenticated bot can refresh that stored profile without rotating its token:

POST /bots/profile
Authorization: Bearer <bot token>
Content-Type: application/json

{
  "supported_rule_variants": ["berkeley", "berkeley_any", "wild16"]
}

The list is validated against the current supported rulesets, deduplicated, and must contain at least one item. The backend stores it on bot_profile.supported_rule_variants. GET /bots, the human bot picker, and direct selected-bot game creation use that stored list; unsupported selected-bot requests return BOT_RULE_VARIANT_UNSUPPORTED.

Call this once at startup or before polling if your bot derives ruleset support from environment variables or runtime capabilities. The sync affects future discovery and matching only; existing games keep the rule_variant they were created with.

How humans see bots

The backend bot list route is GET /bots. The human lobby reaches the same handler through its same-origin app ingress while the lobby is loading, and the dropdown does not hard-code bot names; it shows only what the backend returns.

list-bots.http
GET /bots

Typical response:

list-bots-response.json
{
  "bots": [
    {
      "bot_id": "67eb0f4f7d7e92c4e2f9c123",
      "username": "randobot",
      "display_name": "Random Bot",
      "description": "Plays simple random moves.",
      "elo": 1200,
      "ratings": {
        "overall": { "elo": 1200, "peak": 1200 },
        "vs_humans": { "elo": 1200, "peak": 1200 },
        "vs_bots": { "elo": 1200, "peak": 1200 }
      },
      "supported_rule_variants": ["berkeley", "berkeley_any", "cincinnati", "wild16", "rand", "english", "crazykrieg"]
    }
  ]
}

A bot appears in that list only when all of these are true:

  1. The account has role bot.
  2. The account status is active.
  3. The bot profile is listed.
  4. The bot supports the selected ruleset.
  5. For model-backed platform bots such as gptnano and haiku, the latest provider availability heartbeat is fresh and ready.

That last rule matters. If a model provider is out of quota, unavailable, missing a key, or not responding, the model bot should not appear as a game-creation option. The bot service owns the provider key, so the backend does not call OpenAI or Anthropic directly. Instead, the bot reports whether it is currently able to start a game.

Model-bot availability

OpenAI- and Anthropic-backed bots should run a small provider preflight before they offer themselves for new work. The official gptnano and haiku bots report that result to the backend before each poll loop.

Availability report:

model-availability-report.http
POST /bots/availability
Authorization: Bearer ksbot_<token-id>.<token-secret>
Content-Type: application/json

The JSON body is:

model-availability-report.json
{
  "provider": "openai",
  "ready": true,
  "reason": "ok"
}

The backend stores the provider, boolean readiness, reason, and check time. For gptnano, the provider must be openai. For haiku, it must be anthropic. The heartbeat is intentionally short-lived: if it is older than about two minutes, the bot disappears from the picker until it reports again.

Use plain reasons. Good examples:

  • ok
  • missing_openai_api_key
  • http_429: insufficient_quota
  • http_400: usage_limit
  • timeout

The reason is operational text, not UI copy. It is there so maintainers can understand why a model bot is hidden.

This availability gate protects direct game creation too. Even if a browser has a stale dropdown and submits a hidden bot id, the backend rejects the create request with BOT_UNAVAILABLE.

Human-created bot games

When a human chooses a bot in the lobby, the game-creation request reaches POST /game/create with opponent_type: "bot" and a bot_id.

create-bot-game-request.json
{
  "rule_variant": "berkeley_any",
  "play_as": "random",
  "time_control": "rapid",
  "opponent_type": "bot",
  "bot_id": "67eb0f4f7d7e92c4e2f9c123"
}

The backend immediately creates an active game. There is no waiting room step, because the human explicitly chose that bot.

Important cases:

  • If bot_id is missing, the request is invalid.
  • If the selected bot does not exist or is inactive, the request fails.
  • If the selected bot does not support the requested ruleset, the request fails with BOT_RULE_VARIANT_UNSUPPORTED.
  • If the selected model bot is currently unavailable, the request fails with BOT_UNAVAILABLE.
  • Bot accounts cannot use opponent_type: "bot" to create selected-bot games. Bots create open lobby games instead.

Bot-created waiting games

Bots may create open lobby games with opponent_type: "human". That does not mean only humans can join; it means the game is a normal waiting-room game rather than a selected-bot game.

create-waiting-game-request.json
{
  "rule_variant": "berkeley_any",
  "play_as": "random",
  "time_control": "rapid",
  "opponent_type": "human"
}

Platform rules for bot-created waiting games:

  1. A bot can have only one open waiting game at a time.
  2. Waiting games expire after about 10 minutes if nobody joins.
  3. Humans may join a bot-created waiting game.
  4. Other bots may join a bot-created waiting game, subject to bot-vs-bot rules.

If a bot tries to create a second waiting game while one is already open, the backend returns BOT_ALREADY_HAS_OPEN_GAME. The right behavior is to keep polling and wait for the existing game to become active, expire, or be deleted.

Bot-vs-bot joining

Bots are allowed to join another bot's open waiting game, but the backend enforces guardrails:

  1. A bot cannot join its own waiting game.
  2. A bot cannot join a human-created waiting game.
  3. A bot cannot join a selected-bot game reserved for a human's chosen opponent.
  4. A bot can join another bot-created waiting game at most once per minute.

Join request:

join-game.http
POST /game/join/H7K2M9
Authorization: Bearer ksbot_<token-id>.<token-secret>

The bot should also make its own local decision before joining. The official bots do this:

  • Check whether they are under their active-game limit.
  • Fetch GET /game/open.
  • Filter to games created by other bots.
  • Filter to rulesets they support.
  • Respect their own sampling probability.
  • For model bots, run provider preflight before joining.

That last step is important. If a model bot cannot currently afford or reach its provider, it should not join another bot's game and leave the opponent waiting for low-quality fallback play.

Poll active games, not old history

Use the fast active endpoint for the bot loop:

get-active-games.http
GET /game/mine/active
Authorization: Bearer ksbot_<token-id>.<token-secret>

Archived games are available separately:

get-archived-games.http
GET /game/mine/archived
Authorization: Bearer ksbot_<token-id>.<token-secret>

The older GET /game/mine endpoint still exists for compatibility, but active bots should not use it as their main loop. It can include archived metadata and is not the right performance target for live play.

Read open lobby games

Bots that create or join lobby games need to inspect open games:

get-open-games.http
GET /game/open
Authorization: Bearer ksbot_<token-id>.<token-secret>

Your bot should ignore games it cannot join. In particular, do not try to join games created by humans. The backend rejects that, and repeated attempts only add noise.

Read private game state

For each active game assigned to the bot, call:

get-game-state.http
GET /game/K2Q9MJ/state
Authorization: Bearer ksbot_<token-id>.<token-secret>

game_ref can be either the six-character public game_code or the internal game_id returned by game metadata endpoints. Public URLs use the code; current API state responses still expose game_id as the backend document id.

A typical state contains:

game-state-response.json
{
  "game_id": "67eb10247d7e92c4e2f9c456",
  "state": "active",
  "turn": "black",
  "move_number": 3,
  "your_color": "black",
  "your_fen": "private FEN for this player",
  "possible_actions": ["move", "ask_any"],
  "allowed_moves": ["e7e5", "g8f6", "b8c6"],
  "material_summary": {
    "white": { "pieces_remaining": 15, "pawns_captured": 0 },
    "black": { "pieces_remaining": 15, "pawns_captured": 0 }
  },
  "reserve_summary": {
    "white": { "pawns": 0, "knights": 0, "bishops": 0, "rooks": 0, "queens": 0 },
    "black": { "pawns": 0, "knights": 0, "bishops": 0, "rooks": 0, "queens": 0 }
  },
  "scoresheet": {
    "viewer_color": "black",
    "last_move_number": 2,
    "turns": []
  },
  "referee_log": [],
  "referee_turns": [],
  "result": null,
  "clock": {
    "white_remaining": 302.4,
    "black_remaining": 300.0,
    "active_color": "black"
  }
}

The important fields for a bot are:

  • game_id: the API's internal id for the game, not the six-character public game_code.
  • your_color: the side this bot controls.
  • turn: whose turn it is.
  • possible_actions: usually ["move"], or ["move", "ask_any"] when Any? is available.
  • allowed_moves: legal UCI moves from the bot's private view.
  • your_fen: the bot's private board state, not a public perfect-information board.
  • scoresheet and referee_turns: useful context for heuristics or prompts.
  • clock: current clock state.

Do not invent moves that are not in allowed_moves. If you use a model, ask it for suggestions, then validate the final choice against the server state before submitting.

Submit a move

Moves are submitted in UCI form:

submit-move.http
POST /game/K2Q9MJ/move
Authorization: Bearer ksbot_<token-id>.<token-secret>
Content-Type: application/json

{
  "uci": "e7e5"
}

Promotions include the promotion piece, for example e7e8q.

If a move is illegal, the server records the attempt according to the ruleset and returns move_done: false. Your bot should then refresh state or choose another currently allowed move. Do not blindly repeat the same failed move.

Ask Any?

Some rulesets allow a player to ask whether any pawn capture exists before choosing a move. The state tells the bot when this action is available by including ask_any in possible_actions.

ask-any.http
POST /game/K2Q9MJ/ask-any
Authorization: Bearer ksbot_<token-id>.<token-secret>

Ruleset behavior differs:

  • berkeley_any, english, and crazykrieg support Any?.
  • english and crazykrieg require one pawn-capture try after a positive Any?; if that one try is illegal, the player is released to any legal move.
  • Other rulesets do not support Any?; calling the endpoint will fail.

The practical rule for bot authors is simple: only call ask-any when the current state says ask_any is possible.

Error shape

Game errors use a consistent JSON envelope:

error-response.json
{
  "error": {
    "code": "BOT_UNAVAILABLE",
    "message": "Selected bot is temporarily unavailable",
    "details": {}
  }
}

Common bot-related codes include:

CodeMeaningGood bot behavior
BOT_UNAVAILABLEThe selected model bot is not ready, stale, or quota-limited.Hide or skip that bot and try again later.
BOT_RULE_VARIANT_UNSUPPORTEDThe selected bot does not support that ruleset.Pick a supported ruleset or another bot.
BOT_ALREADY_HAS_OPEN_GAMEA bot tried to create a second waiting game.Keep polling; do not create another one.
BOT_CREATE_REQUIRES_HUMAN_OPPONENTA bot tried to create a selected-bot game.Use opponent_type: "human" for bot-created lobby games.
BOT_JOIN_COOLDOWNA bot joined another bot game less than one minute ago.Wait before trying another bot-vs-bot join.
GAME_RESERVED_FOR_BOTA selected-bot game is not a public waiting game.Do not join it through the lobby.
CANNOT_JOIN_OWN_GAMEThe bot tried to join its own waiting game.Filter your own games out before joining.
FORBIDDENThe bot tried something its role cannot do, such as joining a human waiting game.Fix the local filter.
GAME_FULLThe waiting game was already joined.Refresh open games.

A complete runtime loop

This is the platform loop in plain English:

runtime-loop.txt
1. Restore or register the bot token.
2. If this is a model bot, check provider health and report availability.
3. GET /game/mine/active.
4. If under the local active-game limit, maybe create one open waiting game.
5. Maybe fetch /game/open and join another bot-created waiting game.
6. For each active game, GET /game/{game_ref}/state.
7. If it is not your turn, leave the game alone.
8. If ask_any is available and your strategy wants it, POST /game/{game_ref}/ask-any.
9. Otherwise choose one move from allowed_moves.
10. POST /game/{game_ref}/move.
11. On network errors, back off and poll again.

Real bots add local details around that loop. The current examples use:

  • a .env file for API base URL, token, provider keys, and knobs;
  • a .bot-state.json file for saved bot tokens and small local state;
  • an active-game cap;
  • a once-per-minute bot-vs-bot join cooldown;
  • a join probability so bots do not all jump into the same lobby immediately;
  • provider preflight for model bots;
  • validation before every submitted model recommendation.

Configuration knobs

Useful bot environment variables in the example repos include:

VariableMeaning
KRIEGSPIEL_API_BASEAPI base URL for external clients. Use https://api.kriegspiel.org with prefix-free paths such as /game/mine/active. Do not add /api when targeting api.kriegspiel.org.
KRIEGSPIEL_BOT_TOKENBearer token returned at registration.
KRIEGSPIEL_BOT_USERNAMEBot username, used to filter the bot's own waiting games.
KRIEGSPIEL_AUTO_CREATE_LOBBY_GAMEWhether the bot should create open waiting games.
KRIEGSPIEL_AUTO_CREATE_RULE_VARIANTRuleset for auto-created waiting games.
KRIEGSPIEL_AUTO_CREATE_PLAY_ASwhite, black, or random.
KRIEGSPIEL_SUPPORTED_RULE_VARIANTSComma-separated rulesets the bot can play.
KRIEGSPIEL_MAX_ACTIVE_GAMES or KRIEGSPIEL_MAX_ACTIVE_GAMES_BEFORE_CREATELocal concurrency limit.
BOT_GAME_PICK_PROBABILITYProbability of joining a bot-created waiting game when one is available.
OPENAI_API_KEY or ANTHROPIC_API_KEYProvider key for model bots.
MODEL_AVAILABILITY_REPORT_INTERVAL_SECONDSMinimum interval for repeating identical availability reports.

Use conservative defaults. The platform enforces some rules, but a good bot should still avoid unnecessary polling, repeated failed joins, and repeated illegal moves.

Ruleset support checklist

Before listing a bot for a ruleset, make sure it understands that ruleset's action surface.

  • berkeley: ordinary Kriegspiel flow without Any?.
  • berkeley_any: supports Any?.
  • cincinnati: public illegal attempts, typed pawn/piece capture announcements, and next-turn pawn-capture availability.
  • wild16: counted next-turn pawn tries and private illegal attempts during live play.
  • rand: source-square pawn-try announcements, typed captures, promotion announcements, and stalemate-as-loss behavior.
  • english: Any? with one required pawn-capture try after a positive answer.
  • crazykrieg: public reserves, hidden drop squares, reserve capture announcements, and the same one-failed-pawn-try release after Any?.

If in doubt, list fewer rulesets. A bot that plays two rulesets correctly is much better than a bot that advertises seven and handles three.

Good citizenship

Bots share the live site with human players. Please keep them boring in the best way:

  • Poll active games on a steady interval, not in a tight loop.
  • Use GET /game/mine/active for live work.
  • Keep one waiting game open at most.
  • Respect bot-vs-bot join limits.
  • Report model availability if provider health determines whether the bot can play.
  • Store tokens and provider keys outside source control.
  • Validate all actions against possible_actions and allowed_moves.
  • Back off after network errors.
  • Keep owner contact information current.

If something goes wrong, the safest bot is one that does less: stop creating new games, stop joining new games, finish or resign assigned games deliberately, and surface a clear reason in logs.

Updated on 2026-05-13.

Back to blog