Spaces:
Sleeping
Sleeping
File size: 5,350 Bytes
de9fc8c | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 | # Game
Pygame-based curriculum car racer used as the simulation backend for RL training.
## Running
```bash
# From project root
uv run python main.py # start at track 1
uv run python main.py 5 # start at track 5
# As a module
uv run python -m game.curriculum_game 5
```
## Controls
| Key | Action |
|-----|--------|
| Arrow keys | Drive (up=throttle, down=brake, left/right=steer) |
| N / P | Next / previous track |
| 1 β 9 | Jump to track number |
| R | Restart (counts as an attempt) |
| ESC | Quit |
## Tracks
16 tracks across 4 difficulty tiers. Each level narrows the road, tightens turns, or increases speed cap.
| Tier | Levels | Shape | Description |
|------|--------|-------|-------------|
| A β Easy | 1β4 | Full ellipses | Wide to narrow ovals, superspeedway |
| B β Medium-Easy | 5β8 | Rounded rectangles | Stadium oval, tight rectangle |
| C β Medium-Hard | 9β12 | Two-arc layouts | Hairpin, chicane, double-hairpin, asymmetric |
| D β Hard | 13β16 | Polygon circuits | L-shape, T-notch, complex circuit, master challenge |
## Game Rules
- Complete **one full lap** without touching the white fence border.
- Touching the fence **or** pressing R = restart from start, attempt counter +1.
- Cross the start/finish line cleanly to finish. A summary screen shows stats.
## HUD
A single top bar shows: track name Β· speed Β· attempt count Β· lap time Β· total time Β· distance Β· max speed. Timer starts on first key press, not on load.
## File Structure
```
game/
oval_racer.py Original single-oval game. Exports draw_headlights, draw_car,
SCREEN_W, SCREEN_H used by curriculum_game and env/.
tracks.py 16 TrackDef objects. Each knows its waypoints, road width,
start position/angle, speed cap, and on-track mask.
curriculum_game.py Main playable game. RaceState drives the lap logic,
reset-on-crash, finish detection, and HUD rendering.
rl_splits.py CarEnv (gym-style wrapper), CurriculumSampler, Evaluator,
and TRAIN / VAL / TEST splits for RL training.
test_tracks.py Headless test: builds all 16 tracks and simulates 150 steps
each. Run with: uv run python -m game.test_tracks
```
## Physics Constants
| Constant | Value | Effect |
|----------|-------|--------|
| `ACCEL` | 0.13 | Throttle acceleration per frame |
| `BRAKE_DECEL` | 0.22 | Braking deceleration per frame |
| `FRICTION` | 0.038 | Passive speed decay per frame |
| `STEER_DEG` | 2.7 | Degrees rotated per steer step |
| `max_speed` | 3.0β4.5 | Per-track speed cap (px/frame) |
Speed is in px/frame. Multiply by FPS (60) to get px/s shown in HUD.
## Finish Line Detection
Two-phase gate crossing to handle fast cars reliably:
1. **Arm** β wait until `gate_side > 50 px` ahead (car is clearly past the gate going forward).
2. **Trigger** β detect `prev_side < 0` and `curr_side >= 0` with `speed > 0.3`.
This prevents the car from triggering on spawn or when reversing back over the line.
## Track Metadata (used by reward)
Each `TrackDef` computes three values at construction time:
| Field | Formula | Purpose |
|-------|---------|---------|
| `optimal_dist` | Waypoint polygon perimeter (px) | Theoretical shortest lap path |
| `par_time_steps` | `optimal_dist / (max_speed Γ 0.7)` | Expected lap frames at 70% speed |
| `complexity` | `(115 / width) Γ (max_speed / 3.0)` | Difficulty multiplier (1.0 β 3.45) |
## RL Interface (`rl_splits.py`)
`CarEnv` exposes a gym-style API:
```python
from game.rl_splits import make_env, TRAIN
env = make_env(TRAIN[0])
obs = env.reset() # [x/W, y/H, sin, cos, speed/max, on_track, gate_side]
obs, reward, done, info = env.step([accel, steer])
# info keys: lap, on_track, step, crashes, lap_dist, out_of_bounds
```
### Reward Function
Rewards are **not** scaled by complexity β all values are fixed and comparable
across every track. Complexity only scales the curriculum `threshold`.
| Term | Trigger | Value | Purpose |
|------|---------|-------|---------|
| Forward pulse | Every step | `+speed/max_speed Γ 0.01` | Prevent stalling |
| Off-track | Every step off road | `β0.5` | Stay on road |
| Crash event | onβoff transition | `β5.0` | Penalise each boundary hit |
| Lap completion | Gate crossed cleanly | `+50 Γ time_ratio Γ dist_ratio` | Fast + efficient path |
| Out of bounds | Terminal | `β100` | Don't leave screen |
**Lap completion breakdown:**
```
time_ratio = clamp(par_time_steps / actual_lap_steps, 0.5, 2.0)
dist_ratio = clamp(optimal_dist / actual_lap_dist, 0.5, 1.0)
```
- `dist_ratio` capped at **1.0** β no bonus for paths shorter than the centreline
(any such path involves off-track corner cutting). `lap_dist` is only
accumulated while `on_track=True`, closing the corner-cutting exploit.
- Best lap: `50 Γ 2.0 Γ 1.0 = 100`
- Worst completed lap: `50 Γ 0.5 Γ 0.5 = 12.5`
**Curriculum threshold scales with complexity, rewards do not:**
```
effective_threshold = base_threshold Γ track.complexity
```
| Track | C | Effective threshold (base=30) |
|-------|---|-------------------------------|
| 1 β Wide Oval | 1.00 | 30 |
| 8 β Small Oval | 2.03 | 61 |
| 14 β T-Notch | 2.66 | 80 |
| 16 β Master Challenge | 3.45 | 104 |
|