A 2D simulation where procedurally-defined creatures — rigid bones connected by motorized joints — learn to walk, crawl, and hop via reinforcement learning (PPO from scratch in PyTorch). An outer evolutionary loop mutates creature morphology across generations, using RL-trained fitness as the selection signal. A Pygame GUI editor lets you visually create and edit creature genomes.
# Create a virtual environment (recommended)
python -m venv .venv
source .venv/bin/activate # Linux/Mac
# .venv\Scripts\activate # Windows
# Install PyTorch with CUDA (adjust cu121 to your CUDA version)
pip install torch --index-url https://download.pytorch.org/whl/cu121
# Install all other dependencies
pip install -r requirements.txtpython -c "import torch; print('CUDA:', torch.cuda.is_available())"python main.py editor
python main.py editor --genome creature/presets/quadruped.jsonpython main.py train --genome creature/presets/biped.json --timesteps 1000000
python main.py train --genome creature/presets/biped.json --timesteps 200000 --renderpython main.py replay \
--checkpoint checkpoints/biped_v1/best.pt \
--genome creature/presets/biped.json# Full run
python main.py evolve --config config/default.yaml
# Quick test (4 creatures, 3 generations, 50k steps each)
python main.py evolve --pop-size 4 --generations 3 --per-timesteps 50000tensorboard --logdir logs/evolute/
├── main.py # CLI dispatcher
├── requirements.txt
├── config/
│ └── default.yaml # All hyperparameters
├── creature/
│ ├── morphology.py # Genome, Bone, Joint dataclasses
│ ├── builder.py # Pymunk physics builder + Creature class
│ └── presets/
│ ├── biped.json
│ ├── quadruped.json
│ ├── worm.json
│ └── tripod.json
├── environment/
│ ├── creature_env.py # Gym-like environment
│ └── renderer.py # Pygame renderer
├── editor/
│ └── creature_editor.py # Interactive GUI editor
├── rl/
│ ├── networks.py # ActorCritic neural network
│ ├── buffer.py # Rollout buffer with GAE
│ ├── normalizer.py # Welford running normalizer
│ ├── ppo.py # PPO trainer
│ └── train.py # Training CLI
├── evolution/
│ ├── genome_ops.py # Mutation & crossover operators
│ ├── population.py # Population management
│ └── evolve.py # Evolution main loop
├── scripts/
│ ├── milestone1_test.py # Physics build test (no rendering)
│ └── milestone2_test.py # Renderer + physics sanity test
├── checkpoints/ # Created at runtime
└── logs/ # TensorBoard logs
Creatures are defined as human-editable JSON files. See any file in creature/presets/ for examples.
Key fields:
bones: Rigid body segments. One root bone ("parent": null), all others reference a parent by ID.joints: Connect pairs of bones.is_motorized: truejoints become RL action dimensions.attach_point: Offset (in metres) from the parent bone's local origin where this bone attaches.angle_limit_deg:[min, max]rotation range for the joint.
| Action | Input |
|---|---|
| Select bone/joint | Left-click |
| Start Add Bone | Click "Add Bone" button, then click a parent bone and drag |
| Edit properties | Click bone/joint → use sliders in left panel |
| Delete bone | Select bone → "Delete Bone" button → confirm with Y |
| Mirror limb | Select bone → "Mirror" button |
| Test Drive | "Test Drive" button — toggles live physics |
| Save genome | "Save" button → type filename |
| Load genome | "Load" button → type filename (from creature/saved/) |
| Pan camera | Left/Right arrow keys |
Observation (flat np.float32 vector, size dynamic per genome):
- Per bone (6 values): relative position to torso (x, y), relative angle, velocity (vx, vy), angular velocity
- Global (4 values): torso height, torso angle, torso velocity (vx, vy)
- Per motorized joint (1 value): current angle normalized to limit range
- Per foot/leaf bone (1 value): ground contact flag
Action: One continuous value in [-1, 1] per motorized joint (after tanh squashing).
reward = (
forward_velocity_x # move right
- 0.001 * sum(action²) # energy penalty
- 0.5 * abs(torso_angle) # stay upright
+ 0.05 # alive bonus
)
if fallen: reward -= 1.0; done = True| Parameter | Default |
|---|---|
learning_rate |
3e-4 |
gamma |
0.99 |
gae_lambda |
0.95 |
clip_eps |
0.2 |
n_steps |
2048 |
n_epochs |
10 |
minibatch_size |
256 |
entropy_coef |
0.001 |
- Genome files are loaded with Python's stdlib
json(noeval/pickle). - All genome data is validated by
validate_genome()before use. - Editor file save/load sanitizes filenames with
os.path.basename()and enforces a hardcoded safe directory. - PyTorch checkpoints use
weights_only=Trueto prevent arbitrary code execution.
- GPU training: CUDA is used automatically if available; set
--device cputo force CPU. - Quick iteration: Use
--per-timesteps 50000for fast evolution test runs. - Parallel evolution (stretch goal): The per-genome training loops are independent and can be parallelised with
multiprocessing.Pool— each worker uses its own pymunk space, and GPU tensors are moved back to CPU for inter-process communication. - TensorBoard: Monitor training progress with
tensorboard --logdir logs/.