A two-player implementation of the paper-and-pencil game Dots (a.k.a. Points / Krapky) in Rust. This is the surround-and-capture game — not Dots-and-Boxes.
Players take turns placing colored dots on the intersections of a grid. When a dot completes a closed chain of your color that encloses one or more enemy dots, those dots are captured and score a point; the enclosed area becomes unplayable. Chains may connect diagonally, and dots on the board border can never be captured.
▶ Play it live: dots.mlngo.eu
The desktop GUI (dots-gui) — Blue captures a Red dot by surrounding it.
This is a Cargo workspace:
crates/dots-core— the pure game engine: board, rules, capture detection, scoring. No I/O, no UI. All state changes go through a single entry point,Game::apply(player, action), which returns a list ofEvents. This keeps the engine reusable — a future network server could wrap it unchanged.crates/dots-gui— a desktop GUI (built oneframe/egui) for local hot-seat play. Rendering and input only; all logic lives in the engine.crates/dots-server— an online backend (axum+ WebSocket) with a browser client and a SQLite Elo leaderboard. Wraps the same engine unchanged. The client is authored in TypeScript (static/app.ts) and transpiled + minified to JavaScript at build time bybuild.rs—cargo buildneeds no Node toolchain. Node is optional and only used for type-checking (cd crates/dots-server && npm install && npm run typecheck).
# Run the GUI (two players, same machine)
cargo run -p dots-gui
# Run the online server, then open http://127.0.0.1:8080
cargo run -p dots-server
# Run all tests
cargo test- Click an intersection to place a dot for the current player (Red moves first).
- Surround your opponent's dot(s) with a closed loop of your own dots to capture them — a polygon is drawn through your surrounding dots, the enclosed territory is shaded in your color, and captured dots are dimmed.
- The side panel shows whose turn it is and the score, and offers Pass, Resign, and New Game (with width/height sliders).
- The game ends when the board is full, a player resigns, or both players pass in a row. The player with the most captured enemy dots wins.
A live instance is hosted at dots.mlngo.eu. To run your own,
cargo run -p dots-server starts the web backend and serves a single-page client at
http://127.0.0.1:8080. To play:
- Enter your name, pick a board size, and Create game — you get a shareable link (and a 4-character code) to send to a friend.
- Opening the link (or entering the code) takes the second player straight to a name prompt, then the game begins.
- Play in the browser; the board, captures, scores, and turns stay in sync over WebSocket.
- When the game ends, everyone sees the result, the players' Elo rating change (chess.com-style),
and the leaderboard. Stats are stored in a SQLite database (
leaderboard.dbby default).
Spectators: once two players are in, anyone else who opens the link joins as a read-only spectator. Their feed is delayed by 2 moves so a player can't gain an advantage by spectating their own game (e.g. to reveal the opponent's hidden turds).
Bonus mode: when creating a game you can enable "⚡ extra move on capture" — capturing at least one dot lets you move again instead of passing the turn.
Experimental mode (💩 hidden turds): when enabled, on your turn you can arm "Drop a turd" and click a cell to hide a trap instead of placing a dot. Your opponent cannot see your turds (spectators and replays can); you can't step on your own. If your opponent steps on one, it's revealed then removed and they lose 2 points (scores can go negative).
Replays: the most recent 1000 finished games are stored and can be replayed via their link. After a game ends the same link shows a step-through replay (⏮ / ⏭ through every move); the game-over screen also offers Play again, Watch replay, and Leaderboard. Opening a link for a game that has aged out of the store shows "this game is no longer available."
Behavior and limits:
- Chess-style clocks. Each player gets
2 × (board intersections) ÷ 2seconds (i.e. one second per cell — 81s on 9×9, 169s on 13×13, 361s on 19×19). Only the player on the move spends time, and running out of time loses the game (the opponent wins "on time"). Both clocks are shown live. - Disconnecting mid-game forfeits to the opponent (spectators may leave freely).
- At most 100 games run in parallel (games + waiting lobby entries); further
Creates are rejected until a slot frees up. - Inbound messages are size-capped (4 KiB) and rate-limited per connection; floods are dropped.
- A game still waiting for an opponent is reaped after 10 minutes; in-progress games end via the clocks.
Configuration via environment variables:
DOTS_ADDR— listen address (default127.0.0.1:8080).DOTS_DB— SQLite file path (defaultleaderboard.db).
This is a casual party game with no user accounts. It's safe to run, but mind the following before exposing it on a public address:
- Terminate TLS in front of it. The server speaks plain HTTP/WebSocket; run it behind a reverse
proxy (nginx, Caddy, …) that terminates HTTPS. The client automatically upgrades to
wss://when the page is served overhttps://. - The leaderboard is not authoritative. Players are identified only by the name they type, so names can be impersonated and ratings farmed by self-play. Treat standings as fun, not trustworthy.
- Game codes are short (4 characters) and therefore guessable — anyone who finds a code can take an open seat or spectate. A code is not a secret.
- Abuse limits are deliberately light (this is a hobby project): a global cap on parallel games,
a max message size, and a per-connection message rate limit (
crates/dots-server/src/limits.rs). There is no per-IP limiting — so put it behind a proxy/CDN if you need that, and don't expose it to a hostile internet expecting it to fend for itself.
- Capture algorithm (
crates/dots-core/src/capture.rs): your dots act as 8-connected walls; the algorithm flood-fills "free" cells (4-connected) inward from the border, and any region that can't reach the border and contains at least one enemy dot is captured. This correctly handles diagonal walls, multiple simultaneous captures, and the rule that border ("grounded") dots are safe. - Built test-first (TDD): see the
#[cfg(test)]modules indots-coreand the end-to-end test incrates/dots-core/tests/game_flow.rs.
The engine boundary (Action in, Event out, via one apply call) kept the rules untouched while
the desktop GUI and the online server were added on top. Still open:
- An optional traditional "cross" starting position (via
GameConfig). - The full competitive "grounding" end-game declaration.
- Persistent accounts (the leaderboard currently keys on the name typed per game).
Licensed under the MIT License.
