ai-code-battle/docs/plan/plan.md
jedarden d7cf4625e2 Strategy bots: one per language with starter kits
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>
2026-03-23 21:32:22 -04:00

1180 lines
46 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 | 30120 | Grid height |
| `cols` | 60 | 30120 | Grid width |
| `wall_density` | 0.15 | 0.050.30 | Fraction of tiles that are walls |
| `energy_nodes` | 20 | 850 | Number of energy spawn locations |
| `cores_per_player` | 1 | 12 | 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_interval` turns (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:**
1. Generate one **sector** (1/N of the map for N players)
2. Place walls using cellular automata (random seed → smooth with neighbor rules)
3. Place cores and energy nodes within the sector
4. Validate connectivity: BFS from core must reach all energy nodes and the
sector boundary
5. Mirror/rotate the sector to fill the full map
6. Validate full-map connectivity: all cores must be reachable from each other
7. 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)
```json
{
"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 vision
- `owner` IDs are consistent within a match but randomized per match (player 0
is always "you")
- `config` is identical for all players and does not change between turns
- `walls` are sent every turn they are visible (stateless — bot does not need to
track previously seen walls, though smart bots will)
- `dead` contains bots that died on the previous turn (visible for one turn)
### 4.3 Move Schema (Bot → Engine)
```json
{
"moves": [
{ "row": 10, "col": 15, "direction": "N" },
{ "row": 12, "col": 15, "direction": "E" }
]
}
```
**Validation rules:**
- `moves` must be an array (may be empty — all bots hold position)
- Each move must reference a `(row, col)` where the player owns a bot
- `direction` must 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:
1. Compute the expected signature from the headers and request body
2. Compare with `X-ACB-Signature` (constant-time comparison)
3. Verify `X-ACB-Timestamp` is within ±30 seconds of current time (prevents
replay attacks)
4. 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 | 4th5th |
| RusherBot | Rust | Low | 4th5th |
| GuardianBot | PHP | Medium | 3rd4th |
| SwarmBot | TypeScript | Medium | 1st2nd |
| HunterBot | Java | High | 1st2nd |
### 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 moves
- `GET /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:**
1. **Select seed bot**: the registered bot with the most time since its last
match (tiebreak: lowest bot ID)
2. **Determine match size**: based on the seed bot's least-played format
(2-player, 3-player, 4-player, or 6-player)
3. **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)
4. **Select map**: least recently used map for the chosen player count
5. **Assign player slots**: random
6. **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.
```json
{
"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 ~200500 KB uncompressed, ~3080 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:**
1. Fetch replay JSON from object storage (via signed URL from API)
2. Parse and index: build per-turn game state by replaying events from turn 0
3. Render the current turn to canvas
4. 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:**
1. User navigates to "Register Bot" in their dashboard
2. Provides:
- **Bot name** (unique, alphanumeric + hyphens, 332 chars)
- **Endpoint URL** (HTTPS required for competitive play; HTTP allowed for
development with a flag)
- **Description** (optional, shown on leaderboard)
3. Platform generates:
- `bot_id`: unique identifier (`b_` prefix + 8 hex chars)
- `shared_secret`: 256-bit random, hex-encoded (64 chars)
4. Platform displays the shared secret **once** — user must copy it
5. 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**
6. Platform performs a **protocol test**: sends a mock turn-0 game state to
`POST {endpoint_url}/turn` with 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 | 310 (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:**
1. Worker registers a shutdown hook that catches SIGTERM
2. On SIGTERM, worker sets the current match status to `interrupted` in Redis
3. Worker exits gracefully (within the 30-second SIGTERM grace period)
4. Scheduler's stale-match reaper detects `interrupted` or stale `in_progress`
matches and re-queues them
5. 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 `queued` status
**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 execution
- `cmd/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.