Boombots is a two-player strategy game about moving robot stacks and setting off chain reactions on an 8×8 board. The project includes a JSON WebSocket server, a full-screen terminal client, a responsive React client, shared Rust game rules, and an experimental search engine.
The included Nix shell provides Rust, Node.js, pnpm, and the required development tools:
nix developWithout Nix, install a Rust toolchain with edition 2024 support, Node.js 22.12 or newer, and pnpm 10.
Start the server:
cargo run -p boombots-server --releaseThen connect two players with either client. For the terminal UI:
cargo run -p boombots-cli -- --name Ada
cargo run -p boombots-cli -- --name GraceTo watch a bot-vs-bot match instead, connect one terminal client and press b in the lobby. The server creates both bots and performs their searches; the terminal only displays the resulting game updates.
For the browser UI:
cd client
pnpm install --frozen-lockfile
pnpm devOpen the displayed development URL, normally http://localhost:5173, in two browser tabs and choose a different name in each.
No container runtime is required. The server and both clients run directly on the host.
The terminal client connects to ws://127.0.0.1:8008 by default. Use flags or the equivalent environment variables to change its identity or endpoint:
cargo run -p boombots-cli -- --name Ada --url ws://game.example:8008
BOOMBOTS_NAME=Ada BOOMBOTS_URL=ws://game.example:8008 cargo run -p boombots-cliLobby controls:
↑/↓orj/k: select a player or challengeTabor←/→: switch between players and challengesEnterorSpace: challenge the selected player or accept an incoming challengeb: start and spectate a server-run bot-vs-bot matchqorEsc: quit
Game controls:
- Arrow keys or
h/j/k/l: move the board cursor Space: select a source and target; pressing it again on the source cycles the stack count+/-: change the number of bots to moveb: boom the selected stackEnter: submit the actionc: clear the current actionqorEsc: quit
While spectating bots, the move controls are disabled. The side currently thinking, its remaining time, and both bots' latest evaluations are shown beside the board. Evaluations use White's perspective: +1.00 means White is ahead by roughly one bot, while -1.00 means Black is ahead by roughly one bot. A proven mate is shown as a signed distance in moves by the winning side: #3 means White can force a win in three moves and #-8 means Black can force one in eight. Press Esc to stop spectating and return to the lobby, or q to disconnect.
The connection form derives ws://<browser-host>:8008, using wss:// on HTTPS pages. Set VITE_WS_URL at build or development time when the WebSocket server is elsewhere:
VITE_WS_URL=wss://game.example/ws pnpm devThe UI only enables stacks belonging to the player whose turn it is. It validates movement direction, distance, target occupancy, and bot count before sending an action; the server remains authoritative.
The server binds to 127.0.0.1:8008 by default. To accept remote connections, provide an address explicitly:
cargo run -p boombots-server --release -- --addr 0.0.0.0:8008
BOOMBOTS_ADDR=0.0.0.0:8008 cargo run -p boombots-server --releaseSet RUST_LOG to change logging verbosity, for example RUST_LOG=debug.
Every connection is a WebSocket carrying UTF-8 JSON text. The first message must be a handshake, and messages are limited to 64 KiB. Messages use a camel-case type discriminator and named fields:
{"type":"handshake","name":"Ada"}{"type":"sendChallenge","targetId":2}{"type":"gameAction","action":{"source":{"x":0,"y":1},"target":{"x":0,"y":2},"count":1}}A terminal client starts a bot match with:
{"type":"startBotMatch"}The server sends the initial game, then announces each search before it starts:
{"type":"botThinking","gameId":1,"team":"white","budgetMs":5000}Bot searches run in server-owned blocking tasks, one turn at a time. Each side receives its own five-second deadline; a result is only applied if the game revision and side to move still match. Leaving or disconnecting removes the unattended match, so a completed late search cannot change it. Bot matches are declared drawn after 200 moves if neither side has won.
The live bots use iterative-deepening alpha-beta search with a bounded transposition table. A color-symmetric four-ply boom quiescence search continues through winning, drawing, and profitable explosions instead of evaluating an unstable position immediately before the blast, and every leaf detects unavoidable opposing boom threats. Early iterative passes may also extend one small-stack attack toward a large enemy component and one full-stack consolidation; a contact line then follows the opponent's genuine evasions. Immediate wins, profitable booms, attacks, and meaningful stacks are ordered ahead of quiet fallback moves. Material remains worth 100 centibots per bot, with a bounded positional blast term for quieter positions. The search encodes proven terminal distances. Black can also use the 2v2, 3v2, and 3v3 WDL tablebases to replace heuristic leaf scores when those material classes are reached; White searches without tablebase access. At a tablebase root, Black's bounded exact-distance traversal also reports mate distance when it resolves within the move deadline.
Every bot move updates that side's slot in the botEvaluations object while preserving the other side's last result. Each evaluation contains a White-relative whiteScore and an optional signed mateIn distance in moves by the winning side. For example, {"white":{"whiteScore":100,"mateIn":null},"black":{"whiteScore":-999985,"mateIn":-8}} preserves White's approximate +1.00 evaluation and says Black most recently proved a win in eight moves. The score and mate sign remain White-relative even when Black is the bot reporting them.
Server messages use the same shape:
{"type":"lobby","users":[{"id":1,"name":"Ada"}],"challenges":[]}The authoritative Rust protocol types are in core/src/net.rs. Invalid messages and illegal actions produce a JSON error message without taking down the connection or server.
White moves first, then turns alternate. On a turn, a player may either move bots or boom one of their stacks.
- A stack of height
Nmay move between 1 andNsquares horizontally or vertically. - A player may move between 1 and
Nbots from that stack; move distance and bot count are independent. - Bots may move onto an empty square or a friendly stack, where the stacks combine. They may not move onto an opponent's stack.
- Booming removes the selected stack and every stack connected to it through adjacent squares, including diagonals. This can trigger a large chain reaction.
- A player wins when the opponent has no bots. If a boom removes both sides' final bots, the game is a draw.
| Path | Purpose |
|---|---|
core/ |
Shared rules and serializable client/server protocol |
server/ |
Tokio JSON WebSocket server and integration tests |
tablebase/ |
2v2, 3v2, and 3v3 retrograde generators and probing library |
cli/ |
Ratatui terminal client |
client/ |
Vite, React, and TypeScript web client |
engine/ |
Separate bitboard search-engine workspace and benchmarks |
The primary Rust workspace contains core, server, and cli. The optimized engine remains a separate workspace because it models the board independently and has its own benchmark dependencies.
Run the primary Rust checks from the repository root:
cargo fmt --all -- --check
cargo clippy --workspace --all-targets -- -D warnings
cargo test --workspaceGenerate and verify the endgame table, or run generation and compression as separate steps:
cargo run -p boombots-tablebase --release -- build
cargo run -p boombots-tablebase --release -- build-3v2
cargo run -p boombots-tablebase --release -- build-3v3
cargo run -p boombots-tablebase --release -- stats
cargo run -p boombots-tablebase --release -- generate
cargo run -p boombots-tablebase --release -- compress \
.data/tablebase/2v2.raw .data/tablebase/2v2.bbtbThese commands write to .data/tablebase by default; an explicit output path may be supplied to
any generation command. The table format and probing API are documented in
tablebase/README.md.
Run the engine checks separately:
cargo fmt --manifest-path engine/Cargo.toml --all -- --check
cargo clippy --manifest-path engine/Cargo.toml --workspace --all-targets -- -D warnings
cargo test --manifest-path engine/Cargo.toml --workspaceRun the web checks from client/:
pnpm install --frozen-lockfile
pnpm test
pnpm build
pnpm audit --prodRun the live-bot benchmark in release mode:
cargo run -p boombots-server --release --example bot_benchmarkIt reports tactical action selection, selective attack/stack extensions, the known nine-ply explosion horizon, fixed-depth and five-second search throughput, and evaluation-quality fixtures. Timing is hardware-sensitive, but the tactical results are deterministic. On the development machine, the initial and optimized alpha-beta search measured:
| Benchmark | Initial | Optimized |
|---|---|---|
| Tactical fixtures | 2/3 | 3/3 |
| Evaluation fixtures | 2/3 | 3/3 |
| Fixed depth-7 nodes | 16,699,341 | 13,500,011 |
| Fixed depth-7 time | 3.779 s | 3.318 s |
| Fixed depth-7 nodes/second | 4.42 M | 4.07 M |
The optimized search reduces depth-7 work by about 19% and wall time by about 12% while retaining the same five-second completed depth. Its fixed 64-square board preserves the wire-format array while avoiding per-node heap allocation; generated actions also use one allocation, and already-generated legal moves use a validation-free internal application path.
Quiescence deliberately spends more work at unstable leaves. Before it, Black's depth-5 search chose the losing advance in the benchmarked nine-ply rush; avoiding that move required a 156,649,300-node depth-6 search taking about 35 seconds. With quiescence, depth 5 finds the same escape within the live budget:
| Tactical frontier | Before quiescence | With quiescence |
|---|---|---|
| Nine-ply rush at depth 5 | Losing advance, +40 | Escape, +740 |
| Rush depth-5 nodes | 5,070,655 | 6,467,239 |
| Rush depth-5 time | 1.33 s | 2.12 s |
| Fixed opening depth-7 nodes | 13,500,011 | 36,239,209 |
| Five-second completed opening depth | 7 | 6 |
The bounded search admits one canonical boom per connected component, so equivalent explosion sources do not multiply the frontier. Movement extensions are limited to early iterative passes, one attack and one consolidation per path, and are stored separately in transposition-table horizons. Starting the server with --release is important for these search depths; a debug build remains useful for development but searches substantially fewer nodes in the same five seconds.
The server integration suite opens temporary localhost sockets to exercise a complete two-player handshake, challenge, game, malformed-message, and action flow over real JSON WebSockets. It also starts a production-budget bot match and verifies that the server produces a move after announcing the five-second turn without receiving a client action.
- A browser cannot connect to
0.0.0.0; uselocalhost, a real hostname, or an explicitVITE_WS_URL. - HTTPS pages require a
wss://endpoint. Put the server behind a TLS-terminating reverse proxy for deployment. - Player names are trimmed, limited to 32 characters, and must be unique while connected.
- If port 8008 is occupied, start the server with another
--addrvalue and point the client at the matching URL.