An algorithmic AI bot developed for the 01Founders Rust curriculum. This project features a high-performance bot with a dynamic state-machine strategy and a custom SDL2 visualizer with built-in fairness auditing.
Filler is a competitive game where two bots fight for territory on a grid (the Anfield).
-
The Rule: Each new piece must overlap exactly one cell of your existing territory.
-
The Victory: The player who occupies the most surface area when no more moves are possible wins.
My AI utilizes a Dynamic State Machine that adapts its scoring weights based on the current proximity to the opponent and available mobility.
The bot detects the game phase in state.rs and adjusts its "Brain" in strategy.rs:
-
Expansion Phase: Focuses on perimeter growth to secure a large bounding box.
-
Engagement Phase: Scales a
mobility_factorbased on how many moves the opponent has. If the opponent is more mobile, the bot shifts to Aggressive Blocking. -
Combat Phase: Focuses exclusively on Frontier Control, identifying cells that directly block the opponent's access to external space.
Instead of a simple heatmap, the bot uses Graph Theory to evaluate the map:
- Frontier Analysis: Uses a BFS "Flood Fill" to identify "External Empty Space" reachable from the borders. Only cells touching this "Air" are high-priority targets.
- Trap Recognition: Identifies "Trapped" regions (cells surrounded on 3+ sides) to avoid wasting moves in territory that is already effectively secured.
The visualizer (built with SDL2) serves as both a playback tool and a statistical referee.
-
Interactive Playback
Use Space to pause, Left/Right arrows to step through moves, and Up/Down to adjust playback speed. -
Scientific Audit
Upon match completion, the terminal outputs an Early Game Fairness Analysis.
It calculates the total "cell area" offered to each player up to the first pass to determine whether a win was due to strategy or "luck of the draw". -
Real-time Pass Tracking
Visual markers (STUCK) appear the moment a player is unable to place a piece, clearly highlighting the "bonus turns" taken by the winner. -
Dynamic Scaling
Automatically resizes the UI to fit any Anfield size, from$10 \times 10$ to$100 \times 100$ . -
Zero-Cost Diagnostics: Uses a custom debug_log! macro that yields 0ms overhead during competition.
filler/
├── docker_image/ # Docker setup: game_engine, robots, maps
├── solution/ # AI logic program (Rinbo)
│ ├── src/
│ │ ├── main.rs # Entry point
│ │ ├── game/ # Game modules
│ │ │ ├── mod.rs # Re-exports the modules
│ │ │ ├── anfield.rs # Parsing & grid representation
│ │ │ ├── piece.rs # Piece parsing and shape utils
│ │ │ ├── strategy.rs # Core move decision logic
│ │ │ ├── token.rs # Player token management
│ │ │ └── state.rs # GameState to control the strategy
│ ├── Cargo.toml
│ └── Cargo.lock
│
└── viz/ # visualizer program
├── assets/ # font for text
├── src/
| ├── game/ # Game modules (anfield, piece, etc.)
| ├── parser/ # STDIN stream parsing
| ├── draw/ # SDL2 rendering logic
| ├── analyze/ # Game analysis & fairness logic
│ ├── main.rs # Entry point for visualizer
├── Cargo.toml
└── Cargo.lock
-
Docker (Desktop or CLI)
-
Rust toolchain: Rustup
-
For the Visualizer, install SDL2 development libraries:
- 🐧 Linux (Debian/Ubuntu):
sudo apt install libsdl2-dev libsdl2-ttf-dev - 🍎 macOS:
brew install sdl2 sdl2_ttf - 🪟 Windows: Follow Rust-SDL2 Windows guide or use vcpkg.
- 🐧 Linux (Debian/Ubuntu):
git clone https://github.com/Rinrino/filler.git
cd filler-
Download the official assets: filler.zip
-
Unzip it → you should now have a
docker_image/ folder. -
Prepare binaries (inside
docker_image/):
cd docker_image
# For standard Linux/x86_64 users:
rm -f m1_game_engine m1_robots # Remove Apple Silicon files
mv linux_game_engine game_engine
mv linux_robots robots
chmod +x game_engine robots/*
cd ..Apple M1/M2 users: rename the
m1_*files instead (e.g.mv m1_game_engine game_engine).
FROM rust:1.78-slim-bookworm
COPY ./maps /filler/maps
COPY ./robots /filler/robots
COPY ./game_engine /filler/game_engine
RUN chmod +x /filler/game_engine && chmod +x /filler/robots/\*
WORKDIR /filler
ENTRYPOINT ["/bin/bash"]
docker build -t rinbo ./docker_imagedocker run -v "$(pwd)/solution":/filler/solution -it rinbocd /filler/solution && cargo build --release# Navigate back to the engine directory
cd /filler
# High Performance Mode (Recommended for tournaments)
./game_engine -f maps/map01 -p1 robots/bender -p2 ./solution/target/release/rinbo
# Debug Mode (Generates 'rinbo_debug.log')
RINBO_DEBUG=1 ./game_engine -f maps/map01 -p1 robots/bender -p2 ./solution/target/release/rinbo- To play as Player 1 (@):
./game_engine -f maps/map01 -p1 ./solution/target/release/rinbo -p2 robots/terminator
- To play as Player 2 ($):
./game_engine -f maps/map01 -p1 robots/bender -p2 ./solution/target/release/rinbo
When finished: type exit
Option 1. Live Stream (Recommended) Run this from the project root. This uses your host's local binaries to pipe data into the UI:
./docker_image/game_engine -f ./docker_image/maps/map01 -p1 ./docker_image/robots/terminator -p2 ./solution/target/release/rinbo | cargo run --release --manifest-path viz/Cargo.toml
# 1. Generate the log
./docker_image/game_engine -f ./docker_image/maps/map01 -p1 ./solution/target/release/rinbo -p2 ./docker_image/robots/terminator > output.log
# 2. Open in visualizer
cargo run --release --manifest-path viz/Cargo.toml < output.logDuring development, a significant architectural limitation was identified in the game_engine piece generation.
- Engine Logic: The engine generates pieces within a rectangular bounding box that often includes several rows/columns of "empty" padding (.) at the top or left.
-
Protocol Requirement: The bot must return the
$(X, Y)$ coordinate for the top-left corner of the provided box. -
Validation Rule: The engine strictly requires
$X \ge 0$ and$Y \ge 0$ .
If a piece has "padding" (empty dots) on its top or left side, it becomes mathematically impossible to place the actual shape against the top or left edges of the Anfield.
- Example: To place a shape at Row 0, using a box with 1 row of top-padding, the bot would need to return Y = -1.
- Because the engine rejects negative coordinates, these moves are lost,creating "dead zones" on the map.
✨ My Solution: filler-game-engine
I developed a custom, high-performance remake of the engine in Rust that implements a
trim_and_buildalgorithm. This strips all external padding from pieces before transmission, ensuring 100% of the Anfield grid is playable and fair.
This project is licensed under the MIT License - see the LICENSE file for details.

