Each of the six built-in strategy bots is now implemented in a different language (Python, Go, Rust, PHP, TypeScript, Java) to demonstrate that the HTTP protocol is truly language-agnostic. Added per-language container templates, resource specs, and forkable starter kit repos for participants. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
46 KiB
AI Code Battle — Implementation Plan
1. Overview
AI Code Battle is a competitive bot programming platform where participants write HTTP servers that control units on a grid world. The game engine orchestrates matches asynchronously, stores replays, and serves a web platform where visitors watch rendered game replays and browse leaderboards. Matches are never live — they are evaluated offline by match workers and presented as completed replays.
The platform ships with several built-in strategy bots, each deployed as its own container, serving as both opponents for new participants and reference implementations for the HTTP protocol.
2. System Architecture
┌─────────────────────────────────────────────────────────────────────┐
│ Web Platform │
│ ┌──────────────┐ ┌──────────────┐ ┌───────────────────────────┐ │
│ │ Leaderboard │ │ Match History │ │ Replay Viewer (Canvas) │ │
│ └──────────────┘ └──────────────┘ └───────────────────────────┘ │
└──────────────────────────────┬──────────────────────────────────────┘
│ HTTPS
┌──────────▼──────────┐
│ API Server │
│ (user reg, bot reg, │
│ leaderboard, replay │
│ metadata, health) │
└──────────┬──────────┘
│
┌────────────────┼────────────────┐
│ │ │
┌────────▼───────┐ ┌─────▼──────┐ ┌───────▼────────┐
│ PostgreSQL │ │ Match │ │ Object Store │
│ (users, bots, │ │ Queue │ │ (S3-compat) │
│ matches, │ │ (Redis) │ │ replay JSON │
│ ratings) │ │ │ │ + map data │
└────────────────┘ └─────┬──────┘ └────────────────┘
│
┌─────────▼─────────┐
│ Match Workers │ ← Rackspace Spot instances
│ (stateless, │
│ interruptible) │
└─────────┬─────────┘
│ HTTP (per-turn requests)
┌───────────────┼───────────────┐
│ │ │
┌────────▼──────┐ ┌─────▼─────┐ ┌───────▼──────┐
│ Participant │ │ Built-in │ │ Participant │
│ Bot A │ │ Strategy │ │ Bot B │
│ (external) │ │ Bots │ │ (external) │
└───────────────┘ │ (containers)│ └──────────────┘
└────────────┘
Component Summary
| Component | Role | Scaling Model |
|---|---|---|
| API Server | REST API for web platform, bot registration, match metadata | Horizontally scaled, always-on |
| Match Worker | Pulls match jobs from queue, executes full game simulation, uploads replay | Stateless pods on Rackspace Spot |
| Tournament Scheduler | Creates match jobs based on matchmaking algorithm | Single process, cron-like |
| Web Frontend | Static SPA — replay viewer, leaderboard, registration | CDN / static hosting |
| Strategy Bots | Built-in HTTP bots (one container each) | Always-on, lightweight |
| PostgreSQL | Users, bots, matches, ratings | Single primary + read replica |
| Redis | Match job queue, rate limiting, caching | Single instance |
| Object Store | Replay JSON files, map definitions | S3-compatible (Minio or provider) |
3. Game Mechanics
3.1 Map & Grid
The game plays on a toroidal grid — a rectangular grid that wraps both horizontally and vertically (no edges, no corners). This eliminates positional advantages from map boundaries.
Tile types:
| Tile | Symbol | Description |
|---|---|---|
| Open | . |
Passable empty tile |
| Wall | # |
Impassable barrier |
| Energy | * |
Collectible resource (respawns) |
| Core | C |
Player spawn point (owned by a player) |
Grid parameters (configurable per match):
| Parameter | Default | Range | Description |
|---|---|---|---|
rows |
60 | 30–120 | Grid height |
cols |
60 | 30–120 | Grid width |
wall_density |
0.15 | 0.05–0.30 | Fraction of tiles that are walls |
energy_nodes |
20 | 8–50 | Number of energy spawn locations |
cores_per_player |
1 | 1–2 | Starting cores per player |
3.2 Units (Bots)
Each player controls bots — mobile units on the grid.
- Bots move one tile per turn in a cardinal direction:
N,E,S,W - Bots that do not receive a move order hold position
- Bots are binary — alive or dead, no hit points
- A bot ordered into a wall tile stays in place (order ignored)
- Two friendly bots ordered to the same tile: both die (self-collision)
- A bot ordered onto a tile occupied by a stationary enemy: both die
Each player starts with one bot spawned at each of their cores.
3.3 Energy & Economy
Energy is the sole resource. It is used to spawn new bots.
Energy nodes:
- Fixed positions on the map that periodically produce collectible energy
- Energy appears on a node every
energy_intervalturns (default: 10) - When energy is present on a node, it is visible to any player who can see the tile
Collection:
- A bot adjacent to (or on) an energy tile collects it if no enemy bot is also adjacent to that energy
- If bots from multiple players are adjacent to the same energy, the energy is destroyed — nobody gets it (contested resources are denied)
- Collection happens after combat resolution each turn
Spawning:
- Cost: 3 energy per bot
- Spawning happens automatically when a player has ≥3 energy and an unoccupied, unrazed core
- One bot spawns per core per turn maximum
- If a player has multiple cores and enough energy, one bot spawns at each eligible core simultaneously
- Spawn priority: core that has been idle longest
3.4 Combat
Combat uses a focus fire algorithm inspired by the aichallenge ants system. This rewards formations and positioning over raw unit count.
Attack radius: squared Euclidean distance ≤ attack_radius2 (default: 5,
meaning ~2.24 tiles — includes cardinal and diagonal neighbors plus one more ring).
Resolution (simultaneous):
for each bot B on the grid:
enemies_of_B = count of enemy bots within attack_radius2 of B
for each enemy E within attack_radius2 of B:
enemies_of_E = count of E's enemies within attack_radius2 of E
if enemies_of_B >= enemies_of_E:
mark B as dead
break (B is already dead, no need to check further)
All deaths are resolved simultaneously — no cascading within a single turn.
Key properties:
- 2v1: the lone bot dies, the pair survives (superior numbers win cleanly)
- 1v1: both die (mutual destruction)
- Tight formations are defensive — a cluster facing scattered enemies takes fewer losses because each bot in the cluster has a lower enemy count
- Multi-player battles create emergent alliances and third-party exploitation
3.5 Fog of War
Each player has limited visibility. Only tiles within vision_radius2
(default: 49, ~7 tiles) of any owned bot are visible.
What players see within their vision:
- All tile types (open, wall, energy, core)
- Enemy bots and their owner IDs
- Dead bots (for one turn after death)
What players do NOT see:
- Anything outside their collective vision radius
- How much energy opponents have
- Total number of opponents (discovered through play)
Walls are sent every turn they are visible (no incremental discovery state — keeps the protocol stateless-friendly for HTTP bots).
3.6 Scoring & Win Conditions
Scoring:
- Each player starts with 1 point per core owned
- Capturing a core (enemy bot moves onto an undefended enemy core): +2 points to capturer, −1 point to owner; core is razed
- Razed cores stop spawning but the player continues with remaining bots
- Energy collected: tracked as a tiebreaker statistic (not added to score)
- Bots eliminated: tracked as a statistic
Win conditions (checked in order):
| Condition | Trigger | Resolution |
|---|---|---|
| Sole Survivor | Only one player has living bots | That player wins; bonus +2 per surviving enemy core |
| Annihilation | All players eliminated simultaneously | Draw |
| Dominance | One player controls ≥80% of all bots for 100 consecutive turns | That player wins |
| Turn Limit | Turn count reaches max_turns (default: 500) |
Highest score wins; ties broken by energy collected, then bots alive |
3.7 Turn Structure
Each turn executes in a strict, deterministic sequence:
1. Send game state to all players (HTTP POST, filtered by fog of war)
2. Await responses (up to 3-second timeout per player, in parallel)
3. Validate all responses against schema
4. Phase: MOVE — execute valid movement orders
5. Phase: COMBAT — resolve focus-fire algorithm, remove dead bots
6. Phase: CAPTURE — enemy bots on undefended cores raze them
7. Phase: COLLECT — uncontested energy adjacent to bots is collected
8. Phase: SPAWN — players with ≥3 energy spawn bots at eligible cores
9. Phase: ENERGY_TICK — energy nodes on their interval produce new energy
10. Phase: ENDGAME — check win conditions
11. Record turn state for replay
All player requests in step 1 are sent concurrently. Responses are collected with the 3-second deadline. The engine does not proceed to step 3 until all responses are in or timed out.
3.8 Map Generation
Maps are generated offline and stored in the map library. They are not generated on-the-fly during matches.
Symmetry requirements:
- 2-player maps: 180° rotational symmetry (point symmetry through center)
- 3-player maps: 120° rotational symmetry
- 4-player maps: 90° rotational symmetry
- 6-player maps: 60° rotational symmetry
Generation algorithm:
- Generate one sector (1/N of the map for N players)
- Place walls using cellular automata (random seed → smooth with neighbor rules)
- Place cores and energy nodes within the sector
- Validate connectivity: BFS from core must reach all energy nodes and the sector boundary
- Mirror/rotate the sector to fill the full map
- Validate full-map connectivity: all cores must be reachable from each other
- Store the map with metadata (player count, dimensions, wall density)
Map library:
- Pre-generated pool of 50+ maps per player count (2, 3, 4, 6)
- Maps are curated — auto-generated then play-tested with strategy bots
- Matchmaking selects the least-recently-used map for each match
4. Communication Protocol
4.1 HTTP Interface
The game engine communicates with bots via HTTP POST requests. Each bot exposes a single endpoint.
Bot endpoint: POST {bot_base_url}/turn
The engine sends the game state as a JSON body. The bot responds with its moves
as a JSON body. No other endpoints are required from the bot (though /health is
recommended for registration validation).
Request flow per turn:
Engine Bot
│ │
│ POST /turn │
│ Headers: auth + metadata │
│ Body: game state JSON │
│─────────────────────────────►│
│ │ (bot computes moves)
│ 200 OK │
│ Body: moves JSON │
│◄─────────────────────────────│
│ │
4.2 Game State Schema (Engine → Bot)
{
"match_id": "m_7f3a9b2c",
"turn": 42,
"config": {
"rows": 60,
"cols": 60,
"max_turns": 500,
"vision_radius2": 49,
"attack_radius2": 5,
"spawn_cost": 3,
"energy_interval": 10
},
"you": {
"id": 0,
"energy": 7,
"score": 3
},
"bots": [
{ "row": 10, "col": 15, "owner": 0 },
{ "row": 12, "col": 15, "owner": 0 },
{ "row": 30, "col": 40, "owner": 1 }
],
"energy": [
{ "row": 20, "col": 25 }
],
"cores": [
{ "row": 5, "col": 5, "owner": 0, "active": true },
{ "row": 55, "col": 55, "owner": 1, "active": true }
],
"walls": [
{ "row": 10, "col": 10 },
{ "row": 10, "col": 11 }
],
"dead": [
{ "row": 15, "col": 20, "owner": 1 }
]
}
Schema rules:
bots,energy,cores,walls,dead— only includes tiles within the player's collective visionownerIDs are consistent within a match but randomized per match (player 0 is always "you")configis identical for all players and does not change between turnswallsare sent every turn they are visible (stateless — bot does not need to track previously seen walls, though smart bots will)deadcontains bots that died on the previous turn (visible for one turn)
4.3 Move Schema (Bot → Engine)
{
"moves": [
{ "row": 10, "col": 15, "direction": "N" },
{ "row": 12, "col": 15, "direction": "E" }
]
}
Validation rules:
movesmust be an array (may be empty — all bots hold position)- Each move must reference a
(row, col)where the player owns a bot directionmust be one of:"N","E","S","W"- Duplicate
(row, col)entries: first valid entry wins, rest ignored - Moves referencing tiles with no owned bot: ignored
- Moves into walls: ignored (bot stays)
- Any response that fails top-level schema validation: entire response discarded, all bots hold
- The engine never parses, evaluates, or interprets any field beyond
moves[].row,moves[].col,moves[].direction
4.4 Authentication (HMAC Shared Secret)
Each registered bot has a shared secret generated at registration time. The secret is known only to the bot owner and the game engine. It authenticates both directions — the bot can verify requests came from the real game engine, and the engine can verify responses came from the real bot.
Engine → Bot (request signing):
Headers sent with every request:
X-ACB-Match-Id: m_7f3a9b2c
X-ACB-Turn: 42
X-ACB-Timestamp: 1711200000
X-ACB-Bot-Id: b_4e8c1d2f
X-ACB-Signature: <hex-encoded HMAC-SHA256>
Signature computation:
signing_string = "{match_id}.{turn}.{timestamp}.{sha256(request_body)}"
signature = HMAC-SHA256(shared_secret, signing_string)
The bot verifies:
- Compute the expected signature from the headers and request body
- Compare with
X-ACB-Signature(constant-time comparison) - Verify
X-ACB-Timestampis within ±30 seconds of current time (prevents replay attacks) - If verification fails: bot should return 401 and ignore the request
Bot → Engine (response signing):
Response headers:
X-ACB-Signature: <hex-encoded HMAC-SHA256>
Signature computation:
signing_string = "{match_id}.{turn}.{sha256(response_body)}"
signature = HMAC-SHA256(shared_secret, signing_string)
The engine verifies the response signature. If invalid, the response is discarded (bots hold position). This prevents man-in-the-middle from injecting moves.
Why HMAC over OAuth/JWT/mTLS:
- Minimal complexity — no token refresh, no certificate management
- Bot developers add a single header computation, not an auth library
- Symmetric: both sides can verify the other with the same secret
- Sufficient for the threat model (prevent impersonation and tampering)
Secret management:
- Secrets are generated as 256-bit random values, hex-encoded (64 characters)
- Displayed once at registration time; bot owner must save it
- Can be rotated via the web platform (old secret invalidated immediately)
- Stored hashed (bcrypt) in the database — the engine uses the hash to verify, so the raw secret is never stored. Correction: HMAC requires the raw secret, so it is stored encrypted (AES-256-GCM) with a master key, not hashed. The master key is held in an environment variable, never in the database.
4.5 Timeout & Error Handling
| Scenario | Behavior |
|---|---|
| Bot responds within 3s | Moves validated and applied normally |
| Bot responds after 3s | Response discarded; bots hold position for that turn |
| Bot returns non-200 status | Treated as timeout; bots hold position |
| Bot returns invalid JSON | Treated as timeout; bots hold position |
| Bot returns valid JSON failing schema | Entire response discarded; bots hold position |
| Bot connection refused | Bots hold position; engine retries next turn |
| Bot connection timeout (TCP) | Engine uses 2s connect timeout within the 3s budget |
| 10 consecutive failures | Bot marked as crashed for this match; bots become inert for remaining turns |
The bot is never killed or disconnected. Even after being marked crashed, the match continues — the crashed bot's units simply hold position every turn until they are destroyed or the match ends.
5. Strategy Bots
Six built-in strategy bots serve as reference implementations and permanent ladder opponents. Each is implemented in a different programming language to demonstrate that the HTTP protocol is truly language-agnostic and to provide starter code for participants across the most popular ecosystems.
Each bot is deployed as its own container running a lightweight HTTP server.
| Bot | Language | Complexity | Expected Rank |
|---|---|---|---|
| RandomBot | Python | Trivial | 6th (floor) |
| GathererBot | Go | Low | 4th–5th |
| RusherBot | Rust | Low | 4th–5th |
| GuardianBot | PHP | Medium | 3rd–4th |
| SwarmBot | TypeScript | Medium | 1st–2nd |
| HunterBot | Java | High | 1st–2nd |
5.1 RandomBot — Python
Language rationale: Python is the most accessible language for newcomers. The random bot doubles as the simplest possible starter template — a participant can fork it and have a working bot in minutes.
Strategy: Makes uniformly random valid moves each turn.
Behavior:
- For each owned bot, pick a random direction (N/E/S/W) or hold (20% chance)
- No pathfinding, no memory, no awareness of enemies
- Serves as the absolute baseline — any reasonable bot should beat this
Value: Ensures new participants have an easy opponent to test against. Rating floor anchor.
Implementation: Flask or bare http.server. ~50 lines of strategy code.
HMAC verification via hmac stdlib module.
5.2 GathererBot — Go
Language rationale: Go is the same language as the game engine and platform services, making this the canonical "how to build a bot" reference. Demonstrates idiomatic Go HTTP server patterns.
Strategy: Maximize energy collection, avoid combat entirely.
Behavior:
- BFS from each owned bot to the nearest visible energy
- Assign each bot to the closest uncontested energy (greedy matching)
- If an enemy bot is within vision, move away from it
- Never voluntarily enters attack range of an enemy
- Spawns bots as fast as energy allows
Value: Tests whether aggressive bots can actually close games or whether passive resource hoarding is dominant (it shouldn't be).
Implementation: net/http stdlib server. Shared game/ package with
grid utilities, BFS, and distance calculations that participants can reuse.
5.3 RusherBot — Rust
Language rationale: Rust participants get maximum compute within the 3-second timeout. This bot demonstrates that Rust's performance advantage matters less than strategy — a dumb fast bot still loses to a smart slow one.
Strategy: Identify and rush the nearest enemy core as fast as possible.
Behavior:
- BFS from each owned bot toward the nearest known enemy core
- If no enemy core is known, spread out to explore (random walk with bias toward unexplored territory)
- Ignores energy except incidentally (walks over it)
- Ignores enemy bots unless they block the path
- Spawns bots immediately and sends all toward the target
Value: Punishes bots that neglect defense. Tests whether the combat system allows pure aggression to dominate (it shouldn't — rusher bots will walk into defensive formations and die).
Implementation: axum or actix-web. Serde for JSON. HMAC via hmac
and sha2 crates. Demonstrates Rust's zero-copy deserialization.
5.4 GuardianBot — PHP
Language rationale: PHP is often overlooked in competitive programming but is widely known and trivially deployable. This demonstrates that even PHP — without async, without frameworks — can compete on equal footing when the interface is HTTP. Lowers the barrier for the large PHP developer community.
Strategy: Defend own core, gather nearby energy, cautious expansion.
Behavior:
- Maintain a perimeter of bots within 5 tiles of each owned core
- Assign excess bots (beyond perimeter needs) to gather energy within 10 tiles of a core
- If enemy bots are spotted approaching, consolidate defenders between the enemy and the core
- Only sends scouts (lone bots) to explore beyond the safe zone
- Very conservative spawning — maintains energy reserve of 6
Value: Tests whether turtling is viable. Should beat rushers but lose to gatherers/swarms in the long game (inferior economy due to limited territory).
Implementation: PHP built-in server (php -S) with a single router
script. hash_hmac() for HMAC. JSON via json_decode/json_encode.
BFS implemented with SplQueue.
5.5 SwarmBot — TypeScript
Language rationale: TypeScript (Node.js) is the most popular language for web developers entering the platform. This bot demonstrates maintaining complex state across turns — the swarm's formation tracking, rally points, and center-of-mass calculation benefit from TypeScript's type system.
Strategy: Keep units in tight formations, advance as a group toward enemies.
Behavior:
- All bots maintain cohesion — no bot moves if it would be >3 tiles from the nearest friendly bot
- The swarm moves as a unit toward the nearest enemy presence
- BFS-based center-of-mass steering: average position of all owned bots is the swarm center; steer toward enemy center of mass
- Energy collection is incidental (pass over it during advance)
- New spawns rally to the swarm before advancing
Value: Exploits the focus combat system — a tight group defeats scattered enemies. But slow expansion means inferior economy. Should dominate combat but can be outscored by gatherers on large maps.
Implementation: Express.js or Fastify. State persisted in-process across
turns (the HTTP server stays alive between requests). HMAC via Node.js
crypto module. Typed interfaces for game state and moves.
5.6 HunterBot — Java
Language rationale: Java is dominant in competitive programming (Battlecode
is Java-only). This is the most sophisticated strategy bot, demonstrating
that Java's verbosity is offset by mature data structures (PriorityQueue,
HashMap) and predictable GC behavior within the timeout window.
Strategy: Target isolated enemy bots for efficient kills.
Behavior:
- Identify enemy bots that are ≥4 tiles from their nearest friendly bot (isolated targets)
- Send pairs of bots to intercept isolated enemies (2v1 wins cleanly)
- If no isolated targets, default to gatherer behavior
- Maintain a map of known enemy positions across turns, predict movement based on last-seen direction and speed
- Avoid engaging formations of 3+ enemy bots
- Opportunistic energy collection when not actively hunting
Value: Sophisticated target selection and prediction. Represents an intermediate-to-advanced-skill bot. Should beat random/gatherer/rusher but struggle against swarm formations.
Implementation: Javalin or com.sun.net.httpserver. javax.crypto.Mac
for HMAC. Maintains a HashMap<Position, EnemyTracker> across turns for
movement prediction. Hungarian algorithm for optimal bot-to-target assignment.
5.7 Container Templates
Each language has its own container structure. All share the same contract:
listen on port 8080, serve POST /turn and GET /health.
Go (GathererBot):
strategy-gatherer/
├── Dockerfile
├── main.go # HTTP server, HMAC verification
├── strategy.go # Gatherer-specific logic
├── game/
│ ├── state.go # Game state types
│ ├── grid.go # Grid utilities (BFS, distance, wrapping)
│ └── moves.go # Move response types
└── go.mod
Python (RandomBot):
strategy-random/
├── Dockerfile
├── main.py # HTTP server, HMAC verification, strategy
├── game.py # Game state types and grid utilities
└── requirements.txt # (minimal — stdlib only for random bot)
Rust (RusherBot):
strategy-rusher/
├── Dockerfile
├── Cargo.toml
└── src/
├── main.rs # HTTP server, HMAC verification
├── strategy.rs # Rusher-specific logic
└── game.rs # Game state types, grid utilities
PHP (GuardianBot):
strategy-guardian/
├── Dockerfile
├── index.php # Router + HMAC verification
├── strategy.php # Guardian-specific logic
├── game.php # Game state types, BFS, grid utilities
└── composer.json # (optional — no external deps needed)
TypeScript (SwarmBot):
strategy-swarm/
├── Dockerfile
├── package.json
├── tsconfig.json
└── src/
├── index.ts # HTTP server, HMAC verification
├── strategy.ts # Swarm-specific logic
└── game.ts # Game state types, grid utilities
Java (HunterBot):
strategy-hunter/
├── Dockerfile
├── pom.xml
└── src/main/java/com/acb/hunter/
├── App.java # HTTP server, HMAC verification
├── Strategy.java # Hunter-specific logic
├── GameState.java # Game state deserialization
└── Grid.java # Grid utilities, BFS, distance
Shared contract (all languages):
- Listen on port 8080
POST /turn— receives game state, runs strategy, returns movesGET /health— returns 200 (used for registration health check)- HMAC signature verification on incoming requests
- HMAC signature on outgoing responses
- Request logging (turn number, compute time, move count)
Container specs:
| Bot | Build Image | Runtime Image | Memory Limit | CPU Limit |
|---|---|---|---|---|
| RandomBot | python:3.13-slim |
python:3.13-slim |
64MB | 0.1 cores |
| GathererBot | golang:1.24-alpine |
alpine:3.21 |
128MB | 0.25 cores |
| RusherBot | rust:1.85-alpine |
alpine:3.21 |
128MB | 0.25 cores |
| GuardianBot | php:8.4-cli-alpine |
php:8.4-cli-alpine |
128MB | 0.25 cores |
| SwarmBot | node:22-alpine |
node:22-alpine |
128MB | 0.25 cores |
| HunterBot | eclipse-temurin:21-alpine |
eclipse-temurin:21-jre-alpine |
256MB | 0.5 cores |
Java gets a higher resource allocation due to JVM overhead. All others are intentionally constrained — strategy bots should be lightweight.
5.8 Starter Kit & SDK Libraries
To lower the barrier for participants writing their own bots, the platform provides starter kits for each supported language. Each starter kit is a minimal, forkable repository containing:
- A working HTTP server with HMAC verification already implemented
- Type definitions for the game state and move schemas
- Grid utility functions (toroidal distance, BFS, neighbor enumeration)
- A stub strategy function that holds all bots in place (participant fills in)
- A Dockerfile that builds and runs the bot
- A README with quickstart instructions
Starter kit languages (matching strategy bots):
| Kit | Repository | Notes |
|---|---|---|
acb-starter-python |
Template repo | Flask-based, ~100 lines total |
acb-starter-go |
Template repo | Shares game/ package with GathererBot |
acb-starter-rust |
Template repo | axum + serde, strongly typed |
acb-starter-php |
Template repo | Zero dependencies, built-in server |
acb-starter-typescript |
Template repo | Fastify, full type definitions |
acb-starter-java |
Template repo | Javalin, Maven-based |
Participants are not limited to these languages. Any language that can serve HTTP and compute HMAC-SHA256 can compete. The starter kits simply eliminate boilerplate for the most common choices.
6. Tournament System
6.1 Matchmaking
Matches are created continuously by the tournament scheduler, a process that runs on a fixed interval (default: every 10 seconds).
Algorithm:
- Select seed bot: the registered bot with the most time since its last match (tiebreak: lowest bot ID)
- Determine match size: based on the seed bot's least-played format (2-player, 3-player, 4-player, or 6-player)
- Select opponents: from the eligible pool, preferring: a. Closest skill rating to seed (Pareto distribution: 80% within 16 ranks) b. Least recently paired with the seed c. Fewest games played in the last 24 hours (keeps game counts even)
- Select map: least recently used map for the chosen player count
- Assign player slots: random
- Create match job: push to Redis queue with match config + bot endpoints
Eligibility:
- Bot must be registered and active (passed health check within last hour)
- Bot must not be in a match currently (one match at a time per bot)
- Bot must not have been marked crashed in its last 3 consecutive matches (cooldown: 30 minutes)
6.2 Rating System
Algorithm: Glicko-2
Glicko-2 is preferred over TrueSkill for this platform because:
- No licensing concerns (TrueSkill is patented by Microsoft)
- Includes a volatility parameter (σ) that adapts to inconsistent performance
- Well-suited to multi-player games via pairwise decomposition
- Established in competitive gaming (chess, Go, online games)
Parameters per bot:
mu(μ): rating estimate (default: 1500)phi(φ): rating deviation / uncertainty (default: 350)sigma(σ): rating volatility (default: 0.06)
Display rating: mu - 2*phi (conservative estimate shown on leaderboard)
Update frequency: after every match. Ratings converge quickly — a new bot reaches a stable rating within ~30 matches.
Multi-player adaptation:
- A 4-player match produces 6 pairwise results (every pair of players)
- Each pairwise result is: win/loss based on relative score, or draw if equal
- Glicko-2 update is applied once per match using all pairwise outcomes
6.3 Continuous Tournament
The tournament runs indefinitely with no seasons or resets (initially).
Match throughput target: enough matches that every active bot plays at least 10 matches per day. With N active bots and M match workers:
- 2-player matches: each match involves 2 bots, takes ~3 minutes (500 turns × 3s max + overhead)
- One worker produces ~20 matches/hour
- 3 workers: ~60 matches/hour, ~1440/day — supports ~288 active bots at 10 games/day
Scaling: add more spot workers to increase throughput.
7. Replay System
7.1 Replay Data Format
Replays are JSON files optimized for compact storage while supporting full client-side reconstruction of every game turn.
{
"version": 1,
"match_id": "m_7f3a9b2c",
"date": "2026-03-23T14:30:00Z",
"players": [
{ "bot_id": "b_4e8c1d2f", "name": "SwarmBot", "owner": "alice" },
{ "bot_id": "b_9a1b3c4d", "name": "HunterBot", "owner": "bob" }
],
"result": {
"winner": 0,
"condition": "turn_limit",
"final_scores": [7, 3],
"final_energy": [12, 4],
"final_bots": [18, 6]
},
"config": {
"rows": 60,
"cols": 60,
"max_turns": 500,
"vision_radius2": 49,
"attack_radius2": 5,
"spawn_cost": 3,
"energy_interval": 10
},
"map": {
"walls": [[10,10], [10,11], [10,12]],
"energy_nodes": [[20,25], [40,35]],
"cores": [
{ "pos": [5,5], "owner": 0 },
{ "pos": [55,55], "owner": 1 }
]
},
"turns": [
{
"moves": {
"0": [{"from":[10,15],"dir":"N"},{"from":[12,15],"dir":"E"}],
"1": [{"from":[50,45],"dir":"S"}]
},
"spawns": [[5,5,0]],
"deaths": [[30,40,1]],
"captures": [],
"energy_collected": {"0": [[20,25]]},
"energy_spawned": [[35,15]],
"scores": [3, 1]
}
]
}
Size estimate: a 500-turn, 4-player match with ~50 bots total produces a replay of ~200–500 KB uncompressed, ~30–80 KB gzipped.
Optimization: for very long matches, the turns array can use delta
encoding — only recording events that changed from the previous turn.
7.2 Storage
- Replays are stored in S3-compatible object storage (Minio self-hosted or provider-managed)
- Path:
replays/{year}/{month}/{match_id}.json.gz - Retention: indefinite for top-100 matches per month; older matches pruned after 90 days
- Map definitions stored separately:
maps/{map_id}.json - The API server returns signed URLs for replay access (no public bucket)
7.3 Browser Replay Viewer
The replay viewer is a client-side TypeScript application rendered on HTML5 Canvas.
Rendering pipeline:
- Fetch replay JSON from object storage (via signed URL from API)
- Parse and index: build per-turn game state by replaying events from turn 0
- Render the current turn to canvas
- User controls advance/rewind the turn index
Visual design:
| Element | Rendering |
|---|---|
| Grid | Subtle grid lines on dark background |
| Walls | Dark gray filled squares |
| Open tiles | Transparent (background shows through) |
| Energy nodes | Small yellow diamond; pulse animation when energy is present |
| Cores | Large player-colored circle with ring; X overlay when razed |
| Bots | Player-colored filled circles; brief trail showing last move direction |
| Dead bots | Fading red X for one turn |
| Fog of war | Dark semi-transparent overlay on tiles outside selected player's vision |
| Combat | Flash effect on tiles where kills occurred |
Controls:
| Control | Function |
|---|---|
| Play / Pause | Toggle automatic playback |
| Speed slider | 1x, 2x, 4x, 8x, 16x (turns per second: 2, 4, 8, 16, 32) |
| Turn scrubber | Drag to any turn; displays turn number |
| Perspective dropdown | "All" (omniscient) or per-player fog of war view |
| Zoom | Scroll to zoom; drag to pan |
| Score overlay | Per-player score, energy, bot count — updates each turn |
| Minimap | Small overview of full grid in corner (for large maps) |
Shareable URLs: https://aicodebattle.com/replay/{match_id} — the
replay viewer is the landing page for any match. No login required to watch.
8. Web Platform
8.1 User Registration
- Email + password or OAuth (GitHub recommended — target audience is developers)
- Email verification required before bot registration
- Profile: username (unique), display name, avatar (from OAuth provider)
8.2 Bot Registration
Registration flow:
- User navigates to "Register Bot" in their dashboard
- Provides:
- Bot name (unique, alphanumeric + hyphens, 3–32 chars)
- Endpoint URL (HTTPS required for competitive play; HTTP allowed for development with a flag)
- Description (optional, shown on leaderboard)
- Platform generates:
bot_id: unique identifier (b_prefix + 8 hex chars)shared_secret: 256-bit random, hex-encoded (64 chars)
- Platform displays the shared secret once — user must copy it
- Platform performs a health check:
GET {endpoint_url}/health- Must return 200 within 5 seconds
- If health check fails, registration is saved but bot is marked inactive
- Platform performs a protocol test: sends a mock turn-0 game state to
POST {endpoint_url}/turnwith valid HMAC- Bot must return a valid (possibly empty) moves response within 3 seconds
- If protocol test fails, bot is marked inactive with an error message
Bot status lifecycle:
PENDING → ACTIVE → INACTIVE (health check failed)
→ SUSPENDED (manual by admin)
→ RETIRED (by owner)
Only ACTIVE bots participate in matchmaking.
Ongoing health checks: the platform pings each active bot's /health
endpoint every 15 minutes. Three consecutive failures → marked INACTIVE.
Bots automatically return to ACTIVE when health checks resume passing.
8.3 Leaderboard
- Default sort: Glicko-2 display rating (mu - 2*phi) descending
- Columns: rank, bot name, owner, rating, games played, win rate, last active
- Filterable by: player count tier (2p, 3p, 4p, 6p), time range
- Updates in near-real-time (WebSocket push or 30-second polling)
- Public — no login required to view
8.4 Match History & Profiles
Bot profile page (/bot/{bot_name}):
- Current rating + rating history chart
- Recent matches (last 50) with links to replay viewer
- Win/loss/draw breakdown
- Performance vs. each opponent
- Bot description, owner, registration date
User profile page (/user/{username}):
- List of owned bots
- Aggregate statistics across all bots
Match page (/match/{match_id}):
- Participants, map, final scores
- Embedded replay viewer (auto-plays)
- Turn-by-turn event log (collapsible)
9. Deployment & Infrastructure
9.1 Container Architecture
| Image | Base | Purpose | Replicas |
|---|---|---|---|
acb-api |
Go binary on Alpine | REST API server | 2 (always-on) |
acb-worker |
Go binary on Alpine | Match execution worker | 3–10 (spot) |
acb-scheduler |
Go binary on Alpine | Tournament matchmaking | 1 (always-on) |
acb-web |
Nginx + static files | Frontend SPA | 1 (or CDN) |
acb-strategy-random |
Python 3.13 slim | RandomBot | 1 |
acb-strategy-gatherer |
Go on Alpine | GathererBot | 1 |
acb-strategy-rusher |
Rust on Alpine | RusherBot | 1 |
acb-strategy-guardian |
PHP 8.4 CLI Alpine | GuardianBot | 1 |
acb-strategy-swarm |
Node 22 Alpine | SwarmBot (TypeScript) | 1 |
acb-strategy-hunter |
Temurin 21 JRE Alpine | HunterBot (Java) | 1 |
9.2 Rackspace Spot Deployment
Match workers are the primary consumers of compute and are perfectly suited for spot instances:
- Stateless: workers pull jobs from a queue, execute, and push results. No persistent local state.
- Interruptible: if a spot instance is reclaimed mid-match, the match job is re-queued after a staleness timeout (10 minutes with no progress update). The match is replayed from scratch on another worker.
- Bursty: match throughput can flex with spot availability. More instances = faster ladder convergence, but no hard deadline.
Instance sizing:
- Match workers: 2 vCPU, 4 GB RAM per instance (each runs one match at a time)
- Strategy bots: can share a single small instance (all 6 use <1GB total; Java's JVM is the biggest consumer at ~256MB)
- API server + scheduler: 2 vCPU, 4 GB RAM, always-on (not spot)
Deployment layout:
Always-on tier (standard instances):
├── acb-api (×2, behind load balancer)
├── acb-scheduler (×1)
├── acb-web (×1 or CDN)
├── acb-strategy-* (×1 each, shared instance)
├── PostgreSQL (managed or self-hosted)
├── Redis (managed or self-hosted)
└── Minio / S3-compatible store
Spot tier (preemptible instances):
├── acb-worker (×3 minimum, scale up as available)
└── (each worker is a standalone container, no coordination needed)
Spot reclaim handling:
- Worker registers a shutdown hook that catches SIGTERM
- On SIGTERM, worker sets the current match status to
interruptedin Redis - Worker exits gracefully (within the 30-second SIGTERM grace period)
- Scheduler's stale-match reaper detects
interruptedor stalein_progressmatches and re-queues them - Another worker picks up the job
9.3 Data Stores
PostgreSQL:
- Tables:
users,bots,matches,match_participants,maps,ratings - Single primary instance; read replica for leaderboard queries
- Connection pooling via PgBouncer
- Backup: daily automated dumps to object storage
Redis:
- Match job queue (Redis Streams or List-based queue)
- Rate limiting (per-bot, per-endpoint)
- Session cache
- Leaderboard cache (sorted sets)
- No persistence required — queue jobs are recoverable from PostgreSQL match
records with
queuedstatus
Object Storage (S3-compatible):
- Replay files (gzipped JSON)
- Map definition files
- Bot submission metadata / logs
- Signed URL generation for replay access (1-hour expiry)
9.4 Networking & Security
External traffic:
- Web platform: HTTPS only, behind reverse proxy (Caddy or nginx)
- Bot endpoints: engine connects outbound to registered URLs
Internal traffic:
- API ↔ PostgreSQL: private network
- API ↔ Redis: private network
- Workers ↔ Redis: private network (workers may be in different regions — use Redis over TLS if cross-region)
- Workers → bot endpoints: public internet (HTTPS required for competitive bots)
- Workers → strategy bots: private network (same infrastructure)
Security boundaries:
- The game engine (workers) never executes bot code — HTTP only
- All bot responses are schema-validated before processing
- HMAC authentication prevents request/response forgery
- Rate limiting on API endpoints (registration, health checks)
- Bot endpoint URLs validated at registration (no internal IPs, no localhost)
- Workers run with no inbound ports — they only make outbound HTTP calls
9.5 Monitoring
| Signal | Tool | Alert Threshold |
|---|---|---|
| Match throughput | Prometheus counter | <10 matches/hour for >30 minutes |
| Worker count | Prometheus gauge | <2 live workers for >15 minutes |
| Bot health check failures | Prometheus counter | >50% of active bots failing |
| API latency (p99) | Prometheus histogram | >500ms |
| Match queue depth | Redis metric | >100 pending matches |
| Replay storage usage | S3 metric | >80% of quota |
| Error rate (5xx) | Access logs | >1% of requests |
10. Implementation Phases
Phase 1: Core Engine (foundation)
Build the game simulation as a standalone Go library with a CLI runner.
Deliverables:
engine/package: grid, bots, energy, combat, fog of war, turn executioncmd/acb-local/CLI: run a match between two local bot processes (stdin/stdout for dev convenience) and output a replay JSON file- Replay JSON writer
- Comprehensive unit tests for combat resolution, fog of war, wrapping, collision, scoring, endgame conditions
- Map generation tool:
cmd/acb-mapgen/
Exit criteria: can run a complete 500-turn match between two bots locally and produce a valid replay file.
Phase 2: HTTP Protocol & Strategy Bots
Deliverables:
- HTTP bot interface in the engine (replaces stdin/stdout for production)
- HMAC signing and verification library (Go, reusable by GathererBot)
- GathererBot (Go) and RandomBot (Python) — validate the protocol works across languages before building the remaining four
- RusherBot (Rust), GuardianBot (PHP), SwarmBot (TypeScript), HunterBot (Java)
- All 6 bots containerized with language-appropriate Dockerfiles
- Starter kit template repos for each language (fork-ready)
- Integration test: engine runs a full match between bots in different languages over HTTP
Exit criteria: can run a complete match between any two strategy bot containers (in different languages) over HTTP, with HMAC authentication, producing a valid replay.
Phase 3: Replay Viewer
Deliverables:
- TypeScript Canvas-based replay viewer
- Play/pause, scrub, speed control
- Fog of war perspective toggle
- Score overlay
- Loads replay JSON from local file or URL
Exit criteria: can open a replay file in a browser and watch a complete match with all visual elements rendering correctly.
Phase 4: Match Orchestration
Deliverables:
- Match worker service (
acb-worker): pulls from Redis queue, runs matches, uploads replays, records results - Tournament scheduler (
acb-scheduler): matchmaking algorithm, creates jobs - PostgreSQL schema and migrations
- Stale match reaper (handles interrupted spot instances)
- Match result → Glicko-2 rating update pipeline
Exit criteria: scheduler creates matches, workers execute them autonomously, ratings update, replays are stored. System recovers from worker interruption.
Phase 5: Web Platform
Deliverables:
- API server (
acb-api): user registration, bot registration, leaderboard, match history, replay URLs - Web frontend (
acb-web): registration, bot management dashboard, leaderboard, match history, embedded replay viewer - Bot health check system (periodic + on-registration)
- Shared secret generation, display, rotation
Exit criteria: a user can register, add a bot, see it appear on the leaderboard after matches are played, and watch replays of its games.
Phase 6: Deployment & Production
Deliverables:
- Container images pushed to registry
- Rackspace Spot deployment for workers
- Always-on deployment for API, scheduler, strategy bots, datastores
- TLS termination, DNS, CDN for static assets
- Monitoring dashboards and alerts
- Backup automation for PostgreSQL and replay storage
Exit criteria: platform is publicly accessible, matches run continuously, strategy bots compete on the ladder, external participants can register and play.