A stochastic reinforcement learning research environment where an autonomous snake agent learns to navigate a dynamic lava maze and reach the exit — while avoiding Goodhart's Law.
- Project Overview
- Motivation
- Reinforcement Learning Concepts
- Mathematical Formulation
- Goodhart's Law Discussion
- Environment Design
- Training Pipeline
- Results
- Future Improvements
- Screenshots
Lava Escape RL transforms a classic Snake game into a professional reinforcement learning research environment. The snake is no longer controlled by a human — it is an autonomous learning agent trained via Q-Learning and Deep Q-Networks (DQN).
The agent must:
- 🧭 Navigate a procedurally-generated lava maze
- 🍎 Collect food for energy bonuses
- 🚪 Reach the exit tile to win the episode
- 🔥 Avoid dynamically-spreading lava
- 🔄 Learn to avoid reward hacking (Goodhart's Law)
Snake/
├── src/ # Original Java Snake game (unchanged)
├── python_rl/
│ ├── config.yaml # Central hyperparameter configuration
│ ├── env/
│ │ ├── lava_escape_env.py # Gymnasium-compatible MDP environment
│ │ ├── maze.py # Wilson's algorithm maze generator
│ │ ├── lava.py # Stochastic lava cellular automaton
│ │ └── renderer.py # Pygame visual renderer
│ ├── agents/
│ │ ├── q_learning.py # Tabular Q-learning (3 exploration modes)
│ │ ├── dqn.py # Deep Q-Network (PyTorch)
│ │ └── replay_buffer.py # Experience replay buffer
│ ├── rewards/
│ │ └── __init__.py # NaiveReward + ShapedReward (anti-Goodhart)
│ ├── training/
│ │ ├── train_q.py # Q-learning training loop
│ │ ├── train_dqn.py # DQN training loop
│ │ └── logger.py # CSV episode logger
│ ├── visualization/
│ │ ├── plot_curves.py # Learning curves + Goodhart plots
│ │ └── policy_heatmap.py # Q-value & policy arrow heatmap
│ └── evaluate.py # Evaluation with greedy policy
└── results/
├── models/ # Saved model checkpoints
├── logs/ # CSV training logs
└── plots/ # Generated figures
"When a measure becomes a target, it ceases to be a good measure." — Charles Goodhart, 1975
Most toy RL environments have trivially aligned reward functions — if the agent maximises reward, it automatically solves the task. The real world is different: optimising a proxy metric often leads to unintended behaviour.
This project uses a Snake game to demonstrate this concretely:
| Reward Design | Agent Behaviour | True Task Solved? |
|---|---|---|
| Food count only | Circles food endlessly | ❌ Never exits |
| Survival time only | Actively avoids exit | ❌ Exit = death |
| Shaped reward | Navigates to exit | ✅ Yes |
This makes Lava Escape RL both an RL research tool and a Goodhart's Law demonstration platform.
An agent interacts with an environment in discrete time steps. At each step
- The agent observes state
$s_t \in \mathcal{S}$ - The agent selects action
$a_t \in \mathcal{A}$ according to policy$\pi$ - The environment transitions to
$s_{t+1} \sim P(\cdot \mid s_t, a_t)$ - The agent receives scalar reward
$r_t = R(s_t, a_t)$
Three strategies are implemented and compared:
ε-greedy: $$a_t = \begin{cases} \text{random} & \text{with probability } \varepsilon \ \arg\max_a Q(s_t, a) & \text{with probability } 1 - \varepsilon \end{cases}$$
Decaying ε-greedy:
epsilon_decay in config.yaml).
Softmax (Boltzmann) Exploration:
The environment is formalised as an MDP
| Component | Symbol | Description |
|---|---|---|
| State space | Flattened grid channels: cell type, lava age, BFS distance-to-exit | |
| Action space | ${$UP, DOWN, LEFT, RIGHT$}$, |
|
| Transition |
Stochastic — lava spread makes |
|
| Reward | Shaped multi-objective signal (see §4.3) | |
| Discount |
|
Lava cells spread probabilistically, making the transition function non-deterministic:
This is derived from the independent competitor model: each lava neighbour independently tries to ignite the cell with probability
| Lava state | Transition | Probability |
|---|---|---|
| EMPTY → LAVA | Spread from |
|
| LAVA → COOLED | Cooling | |
| COOLED → LAVA | Reignition | |
| COOLED → EMPTY | Full cooling |
The discounted return from time
The action-value function (Q-function) under policy
The Bellman optimality equation: $$Q^(s, a) = \mathbb{E}!\left[r + \gamma \max_{a'} Q^(s', a') \mid s, a\right]$$
Q-learning estimates
Where:
-
$\alpha \in (0,1]$ — learning rate (step size) -
$\delta_t$ — the TD error: how wrong our current estimate was
DQN parameterises
where the Bellman target uses a frozen target network
The target network
The anti-Goodhart reward is a linear combination of aligned objectives:
$$R(s,a) = w_{\text{exit}} \cdot \mathbb{1}[\text{exit}] \cdot B_{\text{exit}}(\text{coverage}) \
- w_{\text{food}} \cdot \mathbb{1}[\text{food}] \cdot r_f \
- w_{\text{surv}} \cdot r_s \
- w_{\text{expl}} \cdot \mathbb{1}[\text{new cell}] \cdot r_e \
- w_{\text{lava}} \cdot \Phi_{\text{lava}}(s) \
- w_{\text{loop}} \cdot \mathbb{1}[\text{revisit within } W] \
- w_{\text{stag}} \cdot \mathbb{1}[\text{no progress} > T]$$
The exit bonus is scaled by grid coverage:
where
The lava proximity penalty uses a distance-weighted sum:
where
Goodhart's Law states that when a measure becomes a target, it ceases to be a good measure. In RL, this manifests as reward hacking: the agent finds unexpected ways to maximise the proxy reward without achieving the intended goal.
# NaiveReward(mode='food_only')
# Exit gives NO reward → agent ignores the goal
reward = food_reward if ate_food else 0.0The agent learns to circulate around food tiles indefinitely, never attempting to reach the exit. It achieves high reward while completely failing the task.
# NaiveReward(mode='survival_only')
# Exit TERMINATES the episode → agent avoids it!
reward = survival_bonus # per step aliveThe agent learns that reaching the exit kills it (from a reward perspective). It actively avoids the exit and survives as long as possible — maximum reward, zero success.
The shaped reward ensures that the globally optimal policy under the reward function is also the intended behaviour:
-
Exit dominates:
$B_{\text{exit}} = 100 \gg r_f = 5 \gg r_s = 0.1$ — no other strategy can match cumulative exit reward - Food is instrumental: food helps survival but alone can never match exit reward
- Stagnation penalty: breaks food-farming loops
- Loop penalty: prevents circular motion for proximity survival
This design makes reward hacking mathematically suboptimal.
The maze is generated using Wilson's loop-erased random walk, which samples uniformly from all possible spanning trees of the grid graph. This guarantees:
- Every cell is reachable (no isolated pockets)
- No closed loops — the agent must navigate properly
- Unbiased structure — no directional preference
The agent receives a flat vector of dimensionality
| Channel | Content | Range |
|---|---|---|
| 0 | Cell type (normalised) | [0, 1] |
| 1 | Lava age (how long cell has been lava) | [0, 1] |
| 2 | BFS distance to exit (inverted, closer=higher) | [0, 1] |
| +2 scalars | Snake head position (normalised) | [0, 1] |
Discrete:
- Lava spread (probabilistic neighbour model)
- Food respawn locations (random empty cell)
- Episode-to-episode maze variation (different random seeds)
- Lava cool/reignite transitions
cd python_rl
pip install -r requirements.txtpython training/train_q.py --reward shaped --strategy epsilon_greedy --episodes 5000python training/train_q.py --reward naive_food --episodes 5000python training/train_dqn.py --reward shaped --episodes 3000 --device cpupython training/train_q.py --strategy epsilon_greedy
python training/train_q.py --strategy softmaxpython visualization/plot_curves.py --comparepython evaluate.py --agent q --model ../results/models/q_shaped_final.pkl
python evaluate.py --agent dqn --model ../results/models/dqn_shaped_final.ptAll hyperparameters live in python_rl/config.yaml — edit them without touching code.
Training logs are saved as CSV in
results/logs/. Plots are generated inresults/plots/.
| Agent | Reward Mode | Exit Success Rate | Mean Return |
|---|---|---|---|
| Q-Learning | Shaped | ~35–60% | +40 to +80 |
| Q-Learning | Food Only | ~0–2% | high proxy ❌ |
| Q-Learning | Survival Only | ~0% | moderate proxy ❌ |
| DQN | Shaped | ~50–75% | +60 to +110 |
Results vary by seed and episode count. Run evaluate.py after training for exact numbers.
- Prioritised Experience Replay — sample high-TD-error transitions more often
- Dueling DQN — separate value and advantage streams
- PPO / A3C — policy gradient methods for comparison
- Curriculum learning — start with small mazes, gradually increase difficulty
- Multi-agent — competitive/cooperative multi-snake scenarios
- LSTM-DQN — partial observability with memory
- Intrinsic motivation — curiosity-driven exploration (ICM)
- Hyperparameter sweep — automated tuning with Optuna
- Web dashboard — real-time training metrics via Streamlit
Add your screenshots here after running training and evaluation.
[SCREENSHOT_1 — Lava maze with snake, food, and exit tiles]
[SCREENSHOT_2 — Reward vs Episode and exit success rate over training]
[SCREENSHOT_3 — Side-by-side: naive vs shaped reward agent behaviour]
If you use this project in academic work, please cite:
@misc{lava_escape_rl_2026,
title = {Lava Escape RL: Stochastic Reinforcement Learning with Anti-Goodhart Reward Design},
year = {2026},
url = {https://github.com/houssam-05-ctrl/Snake}
}
MIT — see LICENSE for details.