Minimal CLI-only Rust project: a 2D Gridworld + tabular Q-learning trainer with reproducible metrics and deterministic GIF demos.
Research prototype focused on evaluation and reproducibility.
- I measure success rate and steps-to-goal.
- I can modify reward shaping and verify impact on learning curves.
- I provide reproducible runs via fixed seeds and saved configs.
Before training (random policy)

After training (learned policy)

- Clean Rust 2024 CLI architecture (small modules, clear types, no engine dependencies).
- A minimal RL loop (environment, reward, terminal conditions, Q-table).
- Reproducibility (seeded RNG, persisted config, deterministic rendering).
- Practical outputs:
metrics.csvfor analysisqtable.binfor policy reuse- GIF rollouts for visual verification
- Grid size:
width × height(default 24×24, configurable via CLI) - Goal zone: rightmost
goal_zone_widthcolumns (default 4) - Start state: random position in the left half (x ∈ [0 .. width/2))
- Actions: Up, Down, Left, Right, Stay
- Episode ends when:
- agent enters goal zone ⇒ success
max_stepsreached ⇒ failure
- Optional obstacles (OFF by default):
- random walls with fixed seed (
--world-seed) - layout is re-sampled until at least one reachable start exists
- goal zone is kept wall-free for stability
- random walls with fixed seed (
+1.0when entering goal zone (terminal)-0.01per step (time penalty)- Optional shaping (OFF by default):
+0.02if x increases (closer to goal)-0.02if x decreases
- State:
(x, y)mapped tostate_id = y * width + x - Q-table:
Vec<f32>of lengthnum_states * num_actions - Update:
Q[s,a] = Q[s,a] + alpha * (target - Q[s,a])target = rif terminal elser + gamma * max_a' Q[s',a']
- Exploration: epsilon-greedy with linear decay
- start
1.0→ end0.05 - decays over first
70%of episodes
- start
cargo run --release -- train \
--episodes 50000 \
--max-steps 96 \
--seed 1 \
--out runs/latestOutputs in runs/latest/:
metrics.csv- per-episode metricsqtable.bin- learned Q-table (bincode)config.json- full run configuration for reproducibility
mkdir -p assets
cargo run --release -- demo \
--random \
--seed 2 \
--gif assets/demo_before.gifcargo run --release -- demo \
--policy runs/latest/qtable.bin \
--seed 2 \
--gif assets/demo_after.gifNotes:
demo --policyauto-loadsruns/latest/config.jsonif present next toqtable.bin.- GIF rendering is deterministic given seeds + config.
Training writes runs/<name>/metrics.csv with columns:
episode- episode index (1-based)success-1if agent reached the goal zone, else0steps- steps taken until terminal ormax_stepsepsilon- exploration rate used in that episode
The trainer logs progress every --log-every episodes:
- rolling success rate over the last
--windowepisodes - avg steps to success in that window (if any)
- current epsilon
Example log line:
ep 500/5000 success_rate(last 200)= 84.50% avg_steps_to_success= 31.2 eps=0.336
This repo is designed so you can reproduce results later:
- Training determinism:
--seedcontrols exploration + episode starts- saved
config.jsonrecords environment and learning parameters
- Obstacle determinism:
--world-seedcontrols wall layout sampling
- Demo determinism:
demo --policy runs/latest/qtable.binreuses saved config automatically
Help:
cargo run --release -- --help
cargo run --release -- train --help
cargo run --release -- demo --helpUseful flags:
- Environment:
--width,--height,--goal-zone-width,--max-steps--shaping--obstacles,--wall-density,--world-seed
- Training:
--episodes,--alpha,--gamma--log-every,--window
- Rendering:
--cell-px,--gif-delay-cs
Run formatting + lint + tests:
cargo fmt
cargo clippy --workspace --all-targets --all-features -- -D warnings
cargo test --workspace --all-targets --all-featuresCI runs the same checks on every push/PR.
Cargo.toml
src/
main.rs # CLI entry (train/demo)
env.rs # gridworld dynamics + optional obstacles
qlearn.rs # Q-table + trainer
render.rs # deterministic GIF rendering
metrics.rs # CSV writer
types.rs # config + enums
assets/ # committed demo gifs for README
runs/ # local outputs (gitignored)
.github/
workflows/ci.yml
MIT - see LICENSE.