Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

34 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Boombots

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.

Quick start

The included Nix shell provides Rust, Node.js, pnpm, and the required development tools:

nix develop

Without 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 --release

Then connect two players with either client. For the terminal UI:

cargo run -p boombots-cli -- --name Ada
cargo run -p boombots-cli -- --name Grace

To 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 dev

Open 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.

Clients

Terminal UI

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-cli

Lobby controls:

  • / or j/k: select a player or challenge
  • Tab or /: switch between players and challenges
  • Enter or Space: challenge the selected player or accept an incoming challenge
  • b: start and spectate a server-run bot-vs-bot match
  • q or Esc: 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 move
  • b: boom the selected stack
  • Enter: submit the action
  • c: clear the current action
  • q or Esc: 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.

Web UI

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 dev

The 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.

Server

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 --release

Set 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.

Rules

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 N may move between 1 and N squares horizontally or vertically.
  • A player may move between 1 and N bots 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.

Project layout

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.

Development

Run the primary Rust checks from the repository root:

cargo fmt --all -- --check
cargo clippy --workspace --all-targets -- -D warnings
cargo test --workspace

Generate 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.bbtb

These 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 --workspace

Run the web checks from client/:

pnpm install --frozen-lockfile
pnpm test
pnpm build
pnpm audit --prod

Run the live-bot benchmark in release mode:

cargo run -p boombots-server --release --example bot_benchmark

It 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.

Troubleshooting

  • A browser cannot connect to 0.0.0.0; use localhost, a real hostname, or an explicit VITE_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 --addr value and point the client at the matching URL.

About

A game server, client and engine for a basic board game called boombots

Topics

Resources

Stars

5 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages