- Go 79.9%
- HTML 19.7%
- Dockerfile 0.4%
CAT — the bot arena
The last one is cuddled by kittens.
CAT is a card-driven race game for bot-programming events: humans write
bots, bots play, everyone watches on the beamer. Four or six players
(-players) move their pieces around a board using cards — no dice, no
drawing mid-round, and the nasty cards (swaps, splits, backward moves)
make it tactical.
The twist of CAT: opposite seats are teams (0&2 vs 1&3 at a 4-player table; 0&3, 1&4 and 2&5 at a 6-player one), but partners may not communicate — and neither may your bots. The only legal signal is the one card you pass your partner each round.
┌────────────┐ TCP :5555, JSON lines ┌────────────┐
│ your bot │ ◄────────────────────────► │ │
└────────────┘ │ cat-server │ HTTP :8080
┌────────────┐ │ │ ◄──────────── beamer
│ any lang │ ◄────────────────────────► │ │ (SSE live board)
└────────────┘ └────────────┘
Quick start
mise install # installs Go (see mise.toml)
go build ./...
# terminal 1: the server (waits for 4 players)
go run ./cmd/cat-server
# terminal 2-5: four bots (the bundled example bot)
go run ./cmd/cat-bot -name alice
go run ./cmd/cat-bot -name bob
go run ./cmd/cat-bot -name carol
go run ./cmd/cat-bot -name dan
# beamer: open http://localhost:8080/
Too few friends around? Fill seats with built-in random bots:
go run ./cmd/cat-server -bots 3 # you provide only 1 bot
Or let the table fill itself when players are slow to arrive: with
-fill-wait 1m the lobby waits — each joining player resets the clock —
and once a minute passes without a new player, built-in bots take the
remaining seats so a game always gets going — even an empty table runs
builtin-only games, so the board never sits idle. Unlike -bots, these
fillers leave after every game: queued players take the freed chairs first,
and only if the table is still short after another wait do the bots return.
A game played only by fillers is just a placeholder — the first player to
connect aborts it, sits down immediately, and the wait starts over.
Six at the table (three teams of two, 96-space track):
go run ./cmd/cat-server -players 6
Writing a bot
Read PROTOCOL.md — it is a newline-delimited JSON protocol
over TCP, usable from any language. The server sends you your hand and a
list of all legal moves; your bot just picks one. Minimal viable bot:
answer exchange_request with any hand card and move_request with any
element of legal.
cmd/cat-bot is a commented Go template; copy it and replace
chooseMove/chooseExchange.
Rules of the event:
- The game is tick-based: you have
-turn-timeout(default 3s) per decision. Too slow / invalid ⇒ the server plays a random legal move for you (flagged orange on the beamer — the audience will notice). - Crashed bots may reconnect with the same name and resume their seat.
- Names are registered on first join: the server issues a secret that your bot must store and use to answer the auth challenge on every later join (see PROTOCOL.md "Authentication & highscores"). This protects your seat and your highscores from impersonation.
- Bots of the same team must not exchange information outside the game.
Server flags
| flag | default | |
|---|---|---|
-tcp |
:5555 |
bot/spectator TCP address |
-http |
:8080 |
visualization address |
-turn-timeout |
3s |
thinking time per move |
-exchange-timeout |
3s |
thinking time for the partner exchange |
-pace |
400ms |
minimum delay between plays, so spectators can follow |
-restart-delay |
10s |
pause before the next game starts |
-players |
4 |
players at the table: 4 (two teams) or 6 (three teams) |
-track-len |
16/player | track spaces: 64 for 4 players, 96 for 6 |
-bots |
0 |
seats filled with built-in random bots |
-fill-wait |
0 |
lobby wait: when no further player joined for this long, the empty seats are filled with built-in random bots for one game (0 = wait forever) |
-reveal-hands |
off |
spectators: web view + spectators see hands, seated bots don't (honor system — anyone can open a spectator connection); all: bots too (debugging; spoils the game) |
-seed |
random | deterministic games for debugging |
-data |
cat-data.json |
player registry + persistent highscores (empty = in-memory) |
-public-tcp |
(empty) | public host:port bots should connect to, shown on the board (empty = hidden) |
Every flag can also be set through the environment: CAT_ plus the flag
name uppercased with dashes as underscores (-turn-timeout →
$CAT_TURN_TIMEOUT, -public-tcp → $CAT_PUBLIC_TCP, ...). A flag given
on the command line takes precedence over its environment variable.
The server runs a continuous series: when a game ends its players are put at the end of the waiting queue in random order and the seats are refilled from the front of the queue, so any number of bots can rotate through the seats; the score across games is shown on the board. Players connecting while a game is running (or while the table is full) spectate and are queued; each name can be logged in only once.
Across games the server keeps two persistent leaderboards — per player and
per team (pair of names) — stored in the -data file, shown on the board
page and served at /api/highscores. Built-in random bots are not tracked.
If a participant loses their secret, delete their entry from the data file
and let them re-register.
Running with Docker
The image is published as codeberg.org/nisrael/cat:latest. All server
options are set through the CAT_* environment variables described above
(don't quote the values — the quotes would become part of them):
services:cat:image:codeberg.org/nisrael/cat:latestrestart:unless-stoppedports:- "5555:5555"# bots/spectators (TCP, JSON lines)- "8080:8080"# board visualization (HTTP)volumes:- ./data:/data# player registry + highscoresenvironment:# host:port bots should connect to, shown on the board- CAT_PUBLIC_TCP=cat.example.org:5555- CAT_PLAYERS=6- CAT_PACE=200msThe image's entrypoint is cat-server, so flags work too: a command:
list (or arguments after the image name with docker run) is passed
straight to the server and overrides the environment. Note that setting
command: replaces the image default -data /data/cat-data.json, so keep
-data in the list if you want persistent highscores.
Repository layout
internal/game— pure rules engine (no I/O), exhaustively tested, including random self-play games (go test ./...)internal/server— TCP protocol, game hub with tick deadlines, HTTP/SSE, embedded visualization (internal/server/web/index.html)cmd/cat-server— the server binarycmd/cat-bot— example bot / template
Rule interpretations
The full rules are in RULES.md, summarized in
PROTOCOL.md — and encoded in the legal move list the
server hands your bot. The running server renders both at /rules and
/protocol. Decisions made where the rules are ambiguous:
- Each piece may be part of a 7-split at most once.
- A 7 that wins the game mid-card may leave the remainder unused.
- A joker may emulate a J even when the J would have no effect (discard).
- Entering the arrival area is optional when passing the start space (both moves are offered).
License
MIT — see LICENSES/MIT.txt. The project is REUSE compliant: every file carries SPDX copyright and license information.
This project was built with the help of Claude Code.