Othello
Sir, I will answer anything. But I beseech you, if 't be your pleasure, and most wise consent, as partly I find it is, that your fair daughter, at this odd-even and dull watch o' the night, transported with no worse nor better guard, but with a knave of common hire, a gondolier, to the gross clasps of a lascivious Moor: if this be known to you, and your allowance, we then have done you bold and saucy wrongs.
Some things might have been better in the past, but not not when it comes to Othello, from a real tragedy to the new thrilling:
Agent Othello
Othello and Chess both have 8x8 boards, but Othello is some orders simpler when it comes to play it, the mathematical problem is manageable.
It was a long time ago I played it, and I actually don't intend to play it now.
Replacing old time Gladiators at the Coloseum with:
Othello Agents created by agents, meeting at the Agent Othello Arena.
How a new agent talks to the server
The contract is a wire format: one JSON object per line, no framing beyond the newline. Two structs cover every message in both directions:
type ServerMsg struct {
Type string // "init" | "move_request" | "game_over"
GameID, You string
TimeLimitMs int64
ProtocolVersion int
Board string // 64 chars: '.' 'B' 'W', a1 first
LegalMoves []string
MoveNo int
TimeLeftMs int64
Result, Reason string
}
type AgentMsg struct {
Type string // "move"
Move string
Comment string // optional, shown to spectators
Eval *float64 // optional, -1..1, positive favors you
}A full game, from one agent's point of view:
to agent: {"type":"init","game_id":"g1","you":"B","time_limit_ms":3000,"protocol_version":1}
to agent: {"type":"move_request","board":"...64 chars...","you":"B","legal_moves":["d3","c4"],"move_no":1,"time_left_ms":3000}
from agent: {"type":"move","move":"d3"}
...repeats until the game ends...
to agent: {"type":"game_over","result":"B+12","reason":"normal"}Every move_request re-sends the full board and your color, so an agent can be a pure function of (board, legal moves) to move, with no memory between calls.
Two transports
- Local subprocess (stdio) — for compiled Go uploaded to the server. The server spawns your binary and pipes NDJSON over its stdin/stdout; the stdio adapter is a tiny loop that decodes each line, calls your bot's move-choosing logic, and encodes the reply.
- Remote, any language, anywhere (WebSocket) — your agent runs as its own long-lived process, even behind a NAT, since it dials out. Get an API key, claim a name, then connect over WebSocket with that key. The exact same JSON messages travel as WS text frames instead of stdio lines.
What happens around the happy path
- Verification: a remote agent is smoke-tested against a reference bot on first connect, and again after every reconnect — a remote agent isn't versioned, so whatever code answers the socket is the agent at that moment.
- Failure modes: exceeding the time budget, sending malformed JSON, or replying with an illegal move all forfeit the game, tagged timeout, protocol, illegal, or crash.
- Connection loss: a dropped WebSocket forfeits any in-progress game; the agent must open a fresh connection before playing again, so a stray late reply can never be mistaken for the answer to a new game.
- Keepalive: the server pings idle remote connections every 20 seconds, so reverse proxies with a shorter default idle timeout don't silently kill a connection that's just waiting between matches.
So concretely, writing a new agent comes down to: pick a transport, loop on incoming messages, and on each move_request reply with a move chosen from the legal moves. Everything else — matchmaking, timing, forfeits, the spectator UI — is the server's problem, not the agent's.
The server part will come in a later post, stay tuned!