# Road Traffic Simulator — CLAUDE.md

## Project Overview

A pure-vanilla browser-based 2D road traffic simulator. Cars navigate a procedurally-generated road network, obey traffic signals, change lanes, and follow realistic spacing using the Intelligent Driver Model (IDM). Opportunistic lane changes are governed by the MOBIL model. No build tools, no dependencies, no package manager — the entire app is plain HTML5/CSS3/JavaScript loaded via `<script>` tags.

**To run:** Open `index.html` directly in a browser, or serve the folder with any static server (e.g., `npx serve .`, VS Code Live Server). No build step is needed.

---

## Tech Stack

| Concern | Technology |
|---|---|
| Rendering | HTML5 Canvas 2D |
| Language | Vanilla ES6 classes, `'use strict'`, no modules |
| Styling | CSS3 (glassmorphism control panel) |
| Persistence | `localStorage` (map + car count) |
| Tests | None |

All classes are loaded into the global scope in a strict dependency order defined in `index.html`.

---

## File Structure

```
index.html                   Entry point — script load order, control panel HTML
css/
  style.css                  Full-screen canvas layout, control panel overlay
js/
  Settings.js                Global config constants (colors, fps, grid size, MOBIL params, etc.)
  App.js                     initApp() — boots the app, binds all UI controls
  geom/
    Point.js                 Immutable 2D vector with math operations
    Segment.js               Line segment between two Points; parametric sampling
    Rect.js                  Axis-aligned rectangle; sector/side queries
    Curve.js                 Cubic Bézier curve via De Casteljau; arc-length cache
  model/
    Pool.js                  Generic keyed object registry (repository pattern)
    LanePosition.js          A car's registered position within a lane (acquire/release)
    Lane.js                  One directional lane; tracks contained cars; MOBIL gap queries
    Road.js                  Directed connection between two Intersections; owns Lanes
    Intersection.js          Graph node; owns ControlSignals; connects Roads
    ControlSignals.js        4-phase traffic signal logic per intersection
    Trajectory.js            Car path state machine — current/next/temp LanePositions
    Car.js                   Vehicle physics, IDM acceleration, MOBIL lane-change decisions
    World.js                 Simulation engine — Pools, map generation, main tick
  visualizer/
    Graphics.js              Thin Canvas 2D wrapper (primitives only)
    Zoomer.js                Pan/zoom state; world↔screen coordinate transforms
    Visualizer.js            Render loop, draws all scene elements each frame
CLAUDE.md                    This file — project reference for developers and AI assistants
IDM.md                       Deep-dive documentation on the Intelligent Driver Model
mobil.md                     Deep-dive documentation on the MOBIL lane-change model
lane-changing.md             Full documentation of the lane-changing system
documentation.html           Styled in-browser documentation index with links to all markdown files
```

---

## Architecture

```
Browser
  └─ index.html
       ├─ Settings.js          ← shared global constants (including MOBIL parameters)
       ├─ Geometry layer       ← Point, Segment, Rect, Curve (pure math)
       ├─ Model layer          ← World, Intersection, Road, Lane, Car, Trajectory...
       ├─ Visualizer layer     ← Visualizer, Graphics, Zoomer
       └─ App.js               ← initApp() wires UI → World + Visualizer
```

**Data flow each frame:**

```
requestAnimationFrame
  → Visualizer.draw(time)
      → world.onTick(timeFactor × delta / 1000)   [delta in seconds]
          → ControlSignals.onTick() per intersection
          → Car.move() per car
              → IDM acceleration
              → Turn-lane repositioning (left/right turns)
              → MOBIL opportunistic lane change (if enabled + cooldown expired)
              → Trajectory.moveForward()
          → World.refreshCars()  (add/remove to meet carsNumber target)
      → draw grid → intersections → roads → signals → cars (with turn signal stripes)
```

**Script load order matters** — each layer depends on the previous. Do not reorder `<script>` tags in `index.html`.

---

## Settings (`js/Settings.js`)

All global constants live in the `Settings` object. Changing these affects the whole simulation.

| Key | Default | Description |
|---|---|---|
| `Settings.fps` | `30` | Target render frame rate |
| `Settings.gridSize` | `14` | World-units per intersection side |
| `Settings.lanesPerRoad` | `4` | Number of lanes per road in each direction |
| `Settings.defaultTimeFactor` | `5` | Initial simulation speed multiplier |
| `Settings.greenReactionMax` | `5.0` | Upper bound [s] of the uniform random delay before a stopped car responds to a green signal |
| `Settings.lightsFlipInterval` | `20` | Signal phase duration in simulation seconds (slider range 2–60 s) |
| `Settings.rightTurnOnRed` | `true` | Master switch for right-turn-on-red behaviour |
| `Settings.rightTurnOnRedMaxSpeed` | `0.5` | m/s — car must be at or below this speed to qualify for RTOR |
| `Settings.rightTurnOnRedMinGap` | `14` | m — minimum clear distance required at target lane entry for RTOR |
| `Settings.colors.turnSignal` | `'#FFA500'` | Amber colour for the car turn-signal stripe |
| `Settings.colors.*` | various | All other rendering colours |
| `Settings.mobil.enabled` | `true` | Master switch for MOBIL lane changing (toggled by UI button) |
| `Settings.mobil.politeness` | `0.3` | How much MOBIL weighs impact on other drivers (0=selfish, 1=altruistic) |
| `Settings.mobil.safeDecel` | `4` | Max braking [m/s²] allowed to impose on the new follower after a merge (also used by yield gate at intersections) |
| `Settings.mobil.threshold` | `0.1` | Minimum net acceleration gain [m/s²] required to trigger a MOBIL change |
| `Settings.mobil.keepRightBias` | `0.1` | Bonus [m/s²] added to rightward MOBIL moves; subtracted from leftward moves |
| `Settings.mobil.anticipatoryBias` | `0.15` | Bonus [m/s²] added when MOBIL moves the car toward the lateral side it will need two intersections ahead |
| `Settings.mobil.postIntersectionCooldown` | `2.0` | Sim-seconds MOBIL is suppressed after a car exits an intersection crossing; does not affect same-road repositioning moves |
| `Settings.idm.timeHeadwayMin/Max` | `1.0/2.5` | Per-car `timeHeadway` range [s] for standard cars (sampled uniformly at construction) |
| `Settings.idm.fastTimeHeadwayMin/Max` | `0.8/1.5` | Per-car `timeHeadway` range [s] for fast (black) cars |
| `Settings.idm.accelMin/Max` | `0.7/1.4` | Per-car `maxAcceleration` range [m/s²] for standard cars |
| `Settings.idm.fastAccelMin/Max` | `1.5/3.0` | Per-car `maxAcceleration` range [m/s²] for fast cars |

**Signal timing note:** `lightsFlipInterval` is the approximate phase duration in **simulation seconds**. Each intersection applies a ±12.5% random variation: actual phase = `(0.875 + 0.25 × random) × lightsFlipInterval` seconds. At the default of 20 s, phases last 17.5–22.5 simulation seconds, or 3.5–4.5 real seconds at `timeFactor = 5`. The UI slider (range 2–60) displays the value in seconds.

---

## UI Controls → Code Mapping

| Control | Property mutated |
|---|---|
| Car count slider | `world.carsNumber` |
| Sim speed slider (0.1×–10×) | `visualizer.timeFactor` |
| Pause/Resume | `visualizer.running` (toggle) |
| Debug | `visualizer.debug` (toggle) |
| MOBIL ON/OFF | `Settings.mobil.enabled` (toggle) |
| Zoom In/Out | `visualizer.zoomer.zoom(1.4)` |
| Pan D-pad | `visualizer.zoomer.moveCenter(80px)` |
| Lights interval slider | `Settings.lightsFlipInterval` |
| Map X/Y + Generate | `world.generateMap(-xSpan, xSpan, -ySpan, ySpan)` |
| Edit Map / Done Editing | `visualizer.editorMode`, `visualizer.zoomer.editMode` (toggle); auto-pauses on enter, auto-resumes on exit |
| Documentation link (panel footer) | Opens `documentation.html` in a new tab |

`App.js:refreshDisplay()` polls `world` and `visualizer` every 250 ms to update panel readouts.

**Starting defaults (on fresh load with no localStorage save):**
- Map X span: 4, Y span: 2 → `generateMap(-4, 4, -2, 2)`
- Car count: 100
- Cars slider max: 700 (dynamically computed as `300 + (xSpan−2)×200 + (ySpan−2)×200`)

---

## Coordinate Systems

| Space | Origin | Unit |
|---|---|---|
| **World** | Model origin | Meters (m) |
| **Screen** | Canvas top-left | Pixels |

`Zoomer` bridges them. `Zoomer.transform(ctx)` applies `ctx.translate` + `ctx.scale` once per frame; all subsequent draw calls use world coordinates. `Zoomer.screenToWorld(p)` converts mouse/pointer positions for interaction.

`defaultZoom = 4` in `Zoomer` constructor — pixels per world unit at scale 1.

---

## Key Algorithms

### Intelligent Driver Model (IDM) — `Car.getAcceleration()`

Full documentation: [IDM.md](IDM.md)

```
a = maxAccel × (1 − freeRoadCoeff − busyRoadCoeff − anticipationCoeff − intersectionCoeff)

freeRoadCoeff        = (v / vMax)^4
safeDistance         = s0 + v × timeHeadway + v × Δv / (2 × √(a × b))
busyRoadCoeff        = (safeDistance / max(gap, 0.1))^2
anticipationCoeff    = 0.5 × (safe2 / gap2)²   [half-weight term for car two ahead; 0 when no car2]
safeIntersectionDist = 1 + v × timeHeadway + v² / (2b)
intersectionCoeff    = (safeIntersectionDist / distToStopLine)^2
```

Car parameters: `maxSpeed` 30 or 60 m/s, `width = 1.2 m`, `length = 3–5 m` (random), `s0 = 2 m`, `maxDeceleration = 3 m/s²`. `timeHeadway` and `maxAcceleration` are sampled per-car from type-specific ranges (see `Settings.idm.*`) so the fleet contains tailgaters, average drivers, and cautious drivers naturally.

`Car.getAccelerationGiven(speed, gap, leaderSpeed)` is a second variant that accepts explicit inputs and omits the `intersectionCoeff`. It is used by MOBIL to evaluate hypothetical lane scenarios without reading live simulation state.

### MOBIL Lane-Change Model — `Car._evalMobil()`

Full documentation: [mobil.md](mobil.md)

MOBIL (Minimizing Overall Braking Induced by Lane Changes) decides whether an opportunistic lane change is worthwhile. A change is approved only when:

1. **Safety:** the new follower in the target lane will not be forced to brake harder than `Settings.mobil.safeDecel`.
2. **Incentive:** the weighted net acceleration gain across the subject, the new follower, and the old follower exceeds `Settings.mobil.threshold`.

```
netGain = ourGain + politeness × (newFollowerGain + oldFollowerGain)  >  threshold
```

Two optional biases stack on top of `threshold`:
- **Keep-right:** `+keepRightBias` when moving right, `−keepRightBias` when moving left — encourages cars to drift back to the right after overtaking.
- **Anticipatory:** `+anticipatoryBias` when moving toward the lateral side needed for the second intersection ahead (set via `Car._peekSecondAheadBias()`).

MOBIL is blocked when the car has a left or right turn queued (`needsTurnLane = true`), when already in a lane change, or when `laneChangeCooldown > 0`. The cooldown is set to 4 simulation seconds after every successful MOBIL change to prevent oscillation.

MOBIL can be toggled globally via `Settings.mobil.enabled` (the **MOBIL: ON/OFF** button in the panel).

### Turn-Lane Repositioning — `Car.move()`

Before MOBIL is evaluated, cars check whether they need to reach a specific lane for their queued turn:

- Left turn → leftmost lane (`leftmostAdjacent`)
- Right turn → rightmost lane (`rightmostAdjacent`)
- Forward/U-turn → any lane (MOBIL runs freely)

`needsTurnLane` is set `true` for any left or right turn — even if the car is already in the correct lane. This blocks MOBIL from moving the car out of position while it is holding a turn lane near an intersection.

### Intersection Lane-Order Preservation — `Car.pickNextLane()`

When a car picks its destination lane at the next intersection, the lane index is chosen to preserve lateral position — preventing cars side-by-side on the approach from having crossing Bézier paths inside the intersection.

- **Forward:** destination lane index = current lane index, clamped to destination road's lane count.
- **Left turn:** offset from the left edge is preserved (leftmost-source → leftmost-destination).
- **Right turn:** offset from the right edge is preserved (rightmost-source → rightmost-destination).

This means two cars side-by-side on the approach always arrive side-by-side on the exit, regardless of lane count differences between the two roads.

### Green-Light Reaction Delay — `Car.move()` + `Trajectory.canEnterIntersection()`

When a traffic signal transitions from red to green, each stopped car samples a uniform random delay in `[0, Settings.greenReactionMax]` seconds before it begins moving. Until the delay expires, `canEnterIntersection()` returns `false`, keeping the stop-line braking force active even though the signal is green.

The transition is detected in `Car.move()` by comparing `trajectory._signalPermitsEntry()` against the previous tick's value. The `speed < 2 m/s` guard ensures only stopped (or nearly stopped) cars get a delay — cars already rolling through on green are unaffected. Cars deeper in the queue are unaffected by IDM, which naturally delays them behind the car in front.

`Trajectory._signalPermitsEntry()` is the pure signal check (signal table + RTOR logic) without the reaction delay. `canEnterIntersection()` wraps it with the `greenReactionRemaining <= 0` gate.

### Right-Turn-on-Red — `Trajectory.canEnterIntersection()`

When `Settings.rightTurnOnRed` is true, a car waiting at a red light for a right turn may proceed early if two conditions are both met:

1. **Stopped:** `car.speed < Settings.rightTurnOnRedMaxSpeed` (default 0.5 m/s).
2. **Target lane clear:** the nearest registered car in the destination lane has its rear bumper more than `Settings.rightTurnOnRedMinGap` (default 14 m) from the lane entry point, checked via `Lane.getNextAt(0)`.

Note: cars that are mid-crossing the intersection on a Bézier curve are not yet registered in the destination lane and are therefore invisible to this check. The RTOR car enters slowly and IDM handles the gap once any crossing car registers.

### Anticipatory Positioning — `Car._peekSecondAheadBias()` + `Car._evalMobil()`

When a car picks its next road via `pickNextLane()`, it also calls `_peekSecondAheadBias(nextRoad)` to look one road further — sampling a random outgoing road from the far intersection and checking whether it would require a left, right, or forward turn. The result (`'left'`, `'right'`, or `null`) is stored in `Car.turnBias`.

`_evalMobil()` applies `+Settings.mobil.anticipatoryBias` to the net gain whenever a MOBIL move is toward the `turnBias` side. This allows a car approaching a straight-ahead intersection to pre-position for a left turn coming two intersections out, stacking with the keep-right bias (which it overcomes when genuine anticipation is active).

`turnBias` is refreshed every time `pickNextLane()` is called (at the end of each road segment) and cleared to `null` when no second-ahead peek is possible (dead ends, no valid road).

### Yielding and Merge Priority — `Trajectory._nextAcquireIsSafe()`

When a crossing car is near the end of its Bézier curve, `next.acquire()` registers it in the destination lane, which causes destination-lane cars to see it as a new leader and apply IDM braking. At high `timeFactor` or dense traffic this can produce emergency braking.

Before each `next.acquire()` call in `moveForward()`, `_nextAcquireIsSafe()` checks the nearest follower in the destination lane. It computes:

```
gap   = next.position − car.length/2 − follower.position − follower.car.length/2
accel = follower.getAccelerationGiven(follower.speed, gap, car.speed)
safe  = accel ≥ −Settings.mobil.safeDecel
```

If the check fails, `next.acquire()` is delayed by one tick. An override forces acquisition when the remaining curve distance is ≤ `car.length`, preventing a crossing from stalling indefinitely if the destination lane never clears.

### Visual Turn Indicators — `Visualizer.drawCar()`

Cars display a blinking amber side stripe when they have a turn intention. The stripe blinks at ~1 Hz using `Math.floor(currentFrameTime / 500) % 2`. Colour is `Settings.colors.turnSignal` (`#FFA500`). The stripe is drawn at 1/3 of the car's width, flush to the left or right edge depending on turn direction.

Turn direction is inferred from `Car._updateTurnSignal()`, which reads the active `nextLane` or, when mid-crossing, reads directly from the trajectory slots.

### De Casteljau Cubic Bézier — `Curve.js`

Used for smooth lane-change and intersection-crossing paths. `Curve(A, B, O, Q)` takes two endpoints and two control points; `getPoint(a)` evaluates position at normalized parameter `a ∈ [0, 1]` via three nested linear interpolations. Arc length is computed once (10-sample approximation) and cached.

### 4-Phase Traffic Signals — `ControlSignals.js`

Each intersection cycles through 4 phases. The state table (`states` getter) is a 4×4 array of `L/F/R` strings — one row per phase, one column per approach side (0=top, 1=right, 2=bottom, 3=left):

```
Phase 0: ['L',  '',   'L',  '']    ← left turns from N and S
Phase 1: ['FR', '',   'FR', '']    ← forward/right from N and S
Phase 2: ['',   'L',  '',   'L']   ← left turns from E and W
Phase 3: ['',   'FR', '',   'FR']  ← forward/right from E and W
```

2-road intersections (T-junctions, ramp ends) skip the table and use all-green (`['LFR', 'LFR', 'LFR', 'LFR']`). Each intersection randomizes its `flipMultiplier` and `phaseOffset` so signals are not synchronized across the map.

Phase duration formula: `(0.875 + 0.25 × flipMultiplier) × lightsFlipInterval` — giving ±12.5% variation around the global setting.

### Random Grid Map Generation — `World.generateMap()`

Default span: `(-4, 4, -2, 2)` grid units; default starting car population: 100.

1. Place intersections at ~80% of grid positions within the requested span.
2. Connect adjacent vertical pairs with probability 90%; same for horizontal pairs.
3. Call `_addBeltway()` to add a clockwise outer loop: NW, NE, SE, SW corner nodes + a south-side ramp connecting to the nearest inner intersection. Roads on the south beltway segment are split by the ramp into two directed segments.

### Sector-Based Lane Geometry — `Rect.getSectorId()` + `Road.update()`

`Rect.getSectorId(point)` returns which of the 4 sides a road attaches to (0=top, 1=right, 2=bottom, 3=left). `Road.update()` uses these sector IDs to subdivide each intersection's connecting side into `Settings.lanesPerRoad` equal-width lanes using `Segment.split()`, then wires adjacency references (`leftAdjacent`, `rightAdjacent`, `leftmostAdjacent`, `rightmostAdjacent`).

The lane count is set directly from `Settings.lanesPerRoad` (default 4). The previous formula `max(2, floor(sideLength / gridSize))` always yielded 2 and has been replaced. To change the lane count globally, update only `Settings.lanesPerRoad` — all downstream logic (turn rules, MOBIL adjacency, rendering dividers) adapts automatically.

Turn direction between two roads is computed as `(targetSideId − sourceSideId − 1 + 8) % 4` → 0=left, 1=forward, 2=right, 3=U-turn.

---

## Car Lifecycle & Trajectory State Machine

Full documentation: [lane-changing.md](lane-changing.md)

Each `Car` owns a `Trajectory` that manages three `LanePosition` slots:

| Slot | Role |
|---|---|
| `current` | Active lane, always registered in the lane's car registry |
| `next` | Destination lane during any lane change or intersection crossing |
| `temp` | Bézier curve lane driving animation while crossing |

During a lane change or crossing: `temp` becomes the active rendering lane, `current` is released when the car is `2 × length` into the curve, `next` is acquired near the end. On completion, `next` is promoted to `current`.

**`distanceToStopLine`** returns `Infinity` (suppressing intersection braking) in two cases:
1. `canEnterIntersection()` is true — signal is green for the car's intended turn, or RTOR conditions are met.
2. The car is mid-crossing — `next._lane.road !== current._lane.road`.

Adjacent same-road lane changes (MOBIL, turn-lane repositioning) do **not** suppress stop-line braking — the car must still respect the signal while changing lanes on the same road.

**`laneChangeCooldown`** — each `Car` carries a countdown in simulation seconds. After any MOBIL-triggered lane change, this is set to 4.0 s. After an intersection crossing completes, it is set to `max(current, Settings.mobil.postIntersectionCooldown)` — preventing MOBIL from firing right at the mouth of an intersection. Same-road repositioning moves do not touch this counter. MOBIL cannot fire again until it reaches zero.

**`turnBias`** — `'left'`, `'right'`, or `null`. Set by `Car.pickNextLane()` via `_peekSecondAheadBias()` each time a new route is chosen. Read by `_evalMobil()` to apply the anticipatory positioning bonus.

**`turnSignal`** — `'left'`, `'right'`, or `null`. Updated each tick by `Car._updateTurnSignal()` and read by `Visualizer` for amber stripe rendering.

### MOBIL Gap Queries — `Lane.getNextAt()` / `Lane.getPrevAt()`

Added to `Lane.js` specifically for MOBIL. These return the nearest registered `LanePosition` strictly ahead of or behind a raw absolute position, without requiring the querying car to be registered in that lane. Used by `_evalMobil()` to inspect the target lane's leader and follower, and also by `canEnterIntersection()` for the right-turn-on-red entry gap check.

---

## Persistence

`World.save()` serializes intersections, roads (by ID), and `carsNumber` to `localStorage['world']`. `World.load()` reconstructs the graph, resolving road `source`/`target` from intersection IDs. Cars are not persisted — they are re-spawned after load. Generating a new map immediately overwrites the saved data; there is no undo.

---

## Map Editor

The **Map Editor** lets users draw custom road networks from scratch. Toggle it with the **✎ Edit Map** button in the panel.

**Entering edit mode:**
- Simulation auto-pauses (`visualizer.running = false`) so no cars are moving while editing.
- The render loop stays alive (`_editorMode` keeps the rAF loop running) so hover overlays update each frame.
- Canvas cursor changes to crosshair; hint text appears in the panel.

**Interactions while in edit mode:**
- **Click empty space** — places a new intersection snapped to the `Settings.gridSize` grid.
- **Click an intersection** — selects it as the road source (highlighted gold).
- **Click a second intersection** — creates a bidirectional road pair between the two; clears selection.
- **Click the same intersection again** — deselects it.
- **Right-click an intersection** — deletes it and all connected roads in both directions.
- **Esc** — clears the current selection without exiting edit mode.
- **Wheel zoom and D-pad pan** still work normally inside edit mode.

**Exiting edit mode:**
- Click **✓ Done Editing** (the same button toggled).
- Simulation automatically resumes (`visualizer.running = true`) and the Pause button resets to "⏸ Pause".
- Editor state is reset to idle.

**Implementation details:**
- `World.removeRoad(road)` unregisters a road from the pool and from both endpoints' adjacency lists.
- `World.removeIntersection(isc)` snapshots `isc.roads` and `isc.inRoads` before removal (they mutate as each `removeRoad` runs), then removes the intersection from the pool.
- `Visualizer._editor` is the phase state machine: `phase` is `'idle'` or `'selected'`; `selectedIntersection`, `hoveredIntersection`, and `hoveredCell` track cursor state.
- `Visualizer._snapToGrid(screenPoint)` converts a screen point to the enclosing `gridSize × gridSize` world-space `Rect`.
- `Visualizer._getIntersectionAt(screenPoint)` hit-tests all intersections using `rect.containsPoint()`.
- `Visualizer._drawEditorOverlay()` renders (all in world-space after `zoomer.transform()`):
  - **Ghost intersection** — 35% alpha fill at the snapped cursor cell when hovering empty space.
  - **Hover ring** — light blue (`#aaddff`) border on the intersection under the cursor.
  - **Selection ring** — gold (`#f0b040`) border on the selected road-source intersection.
- `Zoomer.editMode = true` suppresses left-drag pan so clicks are not confused with pan gestures.

---

## Debug Mode

Toggle with the **Debug** button (`visualizer.debug = true`). This renders:
- Intersection center text: `flipInterval phaseOffset` for each signal
- Car IDs at car positions
- Red Bézier curve overlay for any car currently mid-crossing an intersection

---

## Known Quirks & Gotchas

- **No collision detection.** Safety gaps are enforced purely by IDM acceleration. At very high `timeFactor` values, cars can visually overlap.
- **`lightsFlipInterval` is in simulation seconds.** The slider displays a "s" suffix. Each intersection applies ±12.5% random variation so signals are not all synchronized.
- **Bimodal car speeds.** 90% of cars get `maxSpeed = 30 m/s`; 10% get `60 m/s` (rendered black). No intermediate tiers.
- **Car width is 1.2 m.** With `Settings.lanesPerRoad = 4` and `gridSize = 14`, each lane is 1.75 m wide — a 1.46× margin. If `lanesPerRoad` is reduced to 2, consider increasing `width` back toward 1.7 for better visual proportion.
- **Beltway ramp is always south.** `_addBeltway()` hard-codes the on/off ramp to the nearest inner intersection on the south edge.
- **`Pool.sample()` is O(n).** Iterates all objects each call. Fine at current scale.
- **Car death is deferred one step.** `car.alive = false` is set inside `Car.move()`; the pool removal and `trajectory.release()` happen in the same `World.onTick()` iteration.
- **`distanceToStopLine = Infinity` suppresses stop-line braking only during intersection crossings** — when `next._lane.road !== current._lane.road`. Adjacent same-road lane changes preserve stop-line braking. This was a bug that was intentionally fixed.
- **MOBIL is blocked for left/right turning cars** even when they are already in the correct turn lane, to prevent MOBIL from moving them out of position near an intersection.
- **MOBIL cooldown is not direction-aware.** After any MOBIL change, the car cannot use MOBIL in either direction for 4 simulation seconds.
- **Multi-lane turn repositioning on 4-lane roads.** A car in the rightmost lane (index 0) with a left turn must cross 3 lanes to reach the leftmost (index 3). Each hop requires `isChangingLanes = false` and a non-cooldown tick. On short road segments at high `timeFactor` the car may not complete all hops before reaching the stop line.
- **RTOR does not see mid-crossing cars.** Cars on a Bézier intersection-crossing path are not registered in the destination lane until near the end of their curve. A right-turn-on-red car checks only the registered cars in the target lane, so a crossing car heading for the same lane is invisible to the gap check until it acquires. IDM handles the merge once registration occurs.
- **Global scope via `<script>` tags.** Every class is a global. Load order in `index.html` is the dependency graph — changing it will break the app.
- **Map editor auto-pauses and auto-resumes simulation.** Entering edit mode forces `visualizer.running = false`; clicking Done Editing sets it back to `true`. If you want the simulation to stay paused after editing, click Pause before clicking Done Editing.

---

## How to Extend

**New car behavior:** Modify `Car.getAcceleration()` or `Car.move()`. For a new speed tier, add a branch to the `maxSpeed` assignment in the `Car` constructor and update `color` accordingly.

**Change lane count:** Set `Settings.lanesPerRoad` to any integer ≥ 1. All turn rules, MOBIL adjacency, and rendering adapt automatically. Adjust `Car.width` proportionally — at `gridSize = 14`, comfortable lane width per direction is `(gridSize / 2) / lanesPerRoad` world units.

**MOBIL tuning:** Adjust `Settings.mobil.politeness`, `threshold`, and `safeDecel`. See [mobil.md](mobil.md) for the full parameter guide and tuning advice.

**Right-turn-on-red tuning:** Set `Settings.rightTurnOnRed = false` to disable entirely. Raise `rightTurnOnRedMaxSpeed` to allow rolling turns; raise `rightTurnOnRedMinGap` for more conservative merges.

**New signal phase:** Extend the 4-row `ControlSignals.states` table. Update any callers that assume exactly 4 phases.

**New road/lane type:** Extend `Road.update()` to generate different lane counts or geometries. The sector system (`Rect.getSectorId`) assumes orthogonal grid connections.

**New geometry primitive:** Follow the immutable pattern in `Point.js` — return new objects from all operations, expose computed properties as getters.

**New rendering feature:** Add a method to `Visualizer.js`; call it from `draw()` in the render pass order. Use `Graphics.js` wrappers rather than `this.ctx` directly where possible.

**New UI control:** Add a button or slider to `index.html`, wire it in `App.js` (follow the existing `btnDebug` / `btnMobil` pattern), and target a `Settings.*` property or a `visualizer.*` / `world.*` property directly.

---

## Future Enhancement Ideas

This section collects all potential improvements and additions identified during development, organised by theme. Ideas marked ✓ have already been implemented. The deeper documentation files ([IDM.md](IDM.md), [mobil.md](mobil.md), [lane-changing.md](lane-changing.md)) contain full implementation sketches for many of these.

---

### Driver Behaviour & Car Physics

**✓ Per-car IDM parameter variation** *(implemented)*
`timeHeadway` and `maxAcceleration` are now sampled per-car from type-specific ranges in `Settings.idm.*` at construction. Standard cars: `timeHeadway` 1.0–2.5 s, `maxAcceleration` 0.7–1.4 m/s². Fast (black) cars: `timeHeadway` 0.8–1.5 s, `maxAcceleration` 1.5–3.0 m/s². This produces tailgaters, average drivers, and cautious drivers naturally within the existing 90/10 population split. Full sketch in [IDM.md](IDM.md).

**Reaction time delay**
Real drivers react to what they observed ~0.8 s ago, not the present state. Implementing this as a small ring buffer of `(gap, leaderSpeed)` observations, evaluated on stale data, would cause stop-and-go jams to amplify and propagate backward more realistically — a hallmark of real motorway congestion that IDM currently only partially reproduces. Full sketch in [IDM.md](IDM.md).

**✓ Multi-anticipation (look two cars ahead)** *(implemented)*
`Car.getAcceleration()` now includes a half-weight `anticipationCoeff` term for the car two positions ahead. When the immediate leader has a slow car behind it, this car begins braking gently earlier rather than waiting for the leader to brake hard, dampening stop-and-go oscillation. Full sketch in [IDM.md](IDM.md).

**Heavy vehicle size class (trucks / buses)**
A third population class with `length = 10–15 m` and `maxSpeed = 20 m/s` would model heavy vehicles. These naturally create "rolling roadblocks" that cause platoon bunching. Combined with MOBIL, other cars would correctly identify the lane with the truck as less desirable and switch away. Full sketch in [IDM.md](IDM.md).

**Graduated deceleration cap**
The current model imposes no upper limit on braking magnitude — IDM can compute `−100 m/s²` in an extreme close-approach. Capping at a realistic maximum (e.g. `−12 m/s²`, tyres-locked emergency stop) would make very close encounters produce visible partial overlaps or "bump" events rather than silent instant recovery, and would allow a collision state to be modelled explicitly. Full sketch in [IDM.md](IDM.md).

---

### MOBIL & Lane-Change Decisions

**✓ MOBIL opportunistic lane changes** *(implemented)*
Cars evaluate adjacent lanes for speed gain using the MOBIL safety + incentive criteria on every tick. See [mobil.md](mobil.md).

**✓ Visual turn indicators (amber blinking stripe)** *(implemented)*
A blinking amber side stripe is rendered when a car has a left or right turn intention. See the Visual Turn Indicators section above.

**✓ Intersection lane-order preservation** *(implemented)*
`Car.pickNextLane()` now maps destination lanes by index rather than random selection, ensuring cars side-by-side on approach always arrive side-by-side on exit with no crossing Bézier paths. See the Intersection Lane-Order Preservation section above.

**✓ Keep-right obligation** *(implemented)*
`_evalMobil()` now adds `+Settings.mobil.keepRightBias` to moves toward the right lane and subtracts it from leftward moves. Cars naturally drift back to the right after overtaking. Tunable as `Settings.mobil.keepRightBias` (default 0.1 m/s²; set to 0 to disable). Full sketch in [mobil.md](mobil.md).

**Per-car MOBIL aggressiveness**
`Settings.mobil.politeness` is currently a global constant. Making it a per-car property, sampled at construction (e.g., 0.1–0.8), would produce realistic individual variation — some drivers change lanes aggressively for small gains, others only when it is clearly beneficial to everyone. Fast (black) cars would naturally receive lower politeness values. Full sketch in [mobil.md](mobil.md).

**Look-ahead horizon for turn-lane suppression**
Currently MOBIL is blocked for the entire road segment whenever a left or right turn is queued. A finer approach would only suppress MOBIL within ~50 m of the stop line, allowing cars to use MOBIL freely on long road segments even when carrying a turn intention. Full sketch in [mobil.md](mobil.md).

**Asymmetric lane-change cooldown**
Replace the single `laneChangeCooldown` with separate left/right timers. A car that just moved left (correctly, for good reason) could still move right if a sudden opportunity arose, without the cooldown blocking it. Full sketch in [mobil.md](mobil.md).

**✓ Safety gap check before turn-lane repositioning** *(implemented)*
`Car._isTurnRepositionSafe(targetLane)` now gates all turn-lane repositioning moves. It checks that the gap to the nearest car behind in the target lane meets the IDM desired following distance at that car's speed. If the gap is too small the move is silently aborted and retried next tick, preventing hard braking at high `timeFactor`. Full sketch in [lane-changing.md](lane-changing.md).

**Multi-step lane repositioning**
A car needing to cross two or more lanes (e.g., from the rightmost to the leftmost on a 4-lane road) currently relies on re-evaluating each tick after each single-lane step. Queuing a `pendingLaneTarget` and automatically initiating each next step in `_finishChangingLanes()` would make multi-lane repositioning faster and more reliable on short road segments. Full sketch in [lane-changing.md](lane-changing.md).

**✓ Anticipatory positioning (look two intersections ahead)** *(implemented)*
`Car.pickNextLane()` now calls `_peekSecondAheadBias()` to sample a road from the far intersection and store the expected turn direction as `Car.turnBias`. `_evalMobil()` applies `+Settings.mobil.anticipatoryBias` when a MOBIL move is toward that side, causing cars to pre-position laterally before they even reach the next intersection. Full sketch in [lane-changing.md](lane-changing.md).

---

### Intersections & Network

**✓ Right-turn-on-red (RTOR)** *(implemented)*
Right-turning cars stopped at a red light may proceed when the target lane entry is clear. Controlled by `Settings.rightTurnOnRed`, `Settings.rightTurnOnRedMaxSpeed`, and `Settings.rightTurnOnRedMinGap`. See the Right-Turn-on-Red section above.

**✓ Yielding and merge priority** *(implemented)*
`Trajectory._nextAcquireIsSafe()` now gates the `next.acquire()` call in `moveForward()`. Before registering in the destination lane, it checks whether doing so would force the nearest follower to brake harder than `Settings.mobil.safeDecel`. If unsafe, acquisition is delayed one tick. An override forces it within the final `car.length` of the curve to prevent indefinite stalls. Full sketch in [lane-changing.md](lane-changing.md).

**Ramp merge model (cooperative beltway merging)**
The beltway's south-side ramp currently merges onto the beltway through a standard intersection crossing. A cooperative ramp merge model — where beltway vehicles running MOBIL move away from the merge point when a ramp car is approaching — would increase ramp throughput and model real motorway weave sections. Full sketch in [mobil.md](mobil.md) and [lane-changing.md](lane-changing.md).

**Mandatory lane change (freeway weaving / urgency)**
MOBIL is purely opportunistic — it only changes lanes when there is a net benefit. Adding a mandatory component that boosts the incentive gain proportionally to urgency (distance remaining before a required exit) would model freeway weaving scenarios where a car *must* cross lanes regardless of conditions. Full sketch in [mobil.md](mobil.md).

---

### Simulation Controls & UI

**IDM parameter calibration panel**
Expose `s0`, `timeHeadway`, `maxAcceleration`, and `maxDeceleration` as real-time sliders in the control panel. This would allow interactive exploration of different traffic regimes — tailgating culture (`T = 0.5 s`), cautious motorway driving (`T = 3 s`), aggressive acceleration (`a = 3 m/s²`) — and let users observe how IDM parameters affect jam formation and lane-change frequency. Full sketch in [IDM.md](IDM.md).

**MOBIL parameter calibration panel**
Similarly, expose `politeness`, `threshold`, and `safeDecel` as sliders. Combined with the MOBIL ON/OFF toggle already present, this would make the MOBIL model directly observable and tunable without editing source code.

**Statistics and charts overlay**
A time-series chart of average speed, car count, and (optionally) throughput at a specific road segment would make emergent phenomena like capacity drop, jam formation, and recovery directly visible. Could be drawn on a small canvas overlay in the panel, updated by `refreshDisplay()`.

**✓ Map save/load UI** *(implemented)*
`btnSave`, `btnLoad`, and `btnClear` buttons are now uncommented and visible in the control panel. They persist the current map (intersections, roads, car count) to `localStorage['world']` via `World.save()` / `World.load()` / `World.clear()`.

**✓ Custom map editor** *(implemented)*
Users can draw their own road networks from scratch using the Map Editor panel section. Clicking **✎ Edit Map** enters editor mode: intersections are placed by clicking empty canvas space (snapped to the grid), bidirectional road pairs are created by clicking two intersections in sequence, and intersections (with all connected roads) are deleted by right-clicking. Clicking **✓ Done Editing** exits the editor and automatically resumes the simulation. See the Map Editor section above for full implementation details.

---

## Documentation Files

| File | Contents |
|---|---|
| [CLAUDE.md](CLAUDE.md) | This file — project overview, architecture, algorithms, quirks |
| [IDM.md](IDM.md) | Full IDM theory, formula derivation, implementation details, limitations, future enhancements |
| [mobil.md](mobil.md) | Full MOBIL theory, implementation walkthrough, bugs fixed, limitations, future enhancements |
| [lane-changing.md](lane-changing.md) | Complete documentation of the lane-changing state machine, Bézier geometry, and signal integration |
| [documentation.html](documentation.html) | Styled in-browser documentation index — brief summaries of and links to all markdown files; accessible from the panel footer in the simulator |
