@inventure71/paddockjs 2.0.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/INSTALL_AND_UPDATE.md +1 -1
  3. package/README.md +30 -5
  4. package/docs/architecture.md +15 -9
  5. package/docs/concepts.md +6 -4
  6. package/docs/data_contract.md +28 -15
  7. package/docs/data_model.md +1 -1
  8. package/docs/index.md +9 -0
  9. package/docs/rules.md +37 -7
  10. package/docs/sense_contract.md +59 -0
  11. package/docs/system_specs.md +56 -7
  12. package/docs/training.md +16 -10
  13. package/package.json +1 -1
  14. package/src/api/PaddockSimulatorController.js +2 -33
  15. package/src/app/BrowserExpertAdapter.js +9 -300
  16. package/src/app/BrowserExpertExternalRenderer.js +303 -0
  17. package/src/app/F1SimulatorApp.js +1 -42
  18. package/src/config/restartOptions.js +34 -0
  19. package/src/environment/controllerLoop.js +17 -2
  20. package/src/environment/episode.js +14 -2
  21. package/src/environment/index.d.ts +16 -2
  22. package/src/environment/observationVector.js +200 -0
  23. package/src/environment/observations.js +58 -285
  24. package/src/environment/recorder.js +13 -6
  25. package/src/environment/rewards.js +13 -2
  26. package/src/environment/runtime.js +62 -14
  27. package/src/environment/scenarios.js +22 -3
  28. package/src/environment/sensors/index.js +3 -3
  29. package/src/environment/sensors/opponentRadar.js +13 -3
  30. package/src/environment/sensors/sensorTargets.js +8 -2
  31. package/src/environment/sensors/surfaceRays.js +90 -26
  32. package/src/environment/sensors/trackRays.js +185 -22
  33. package/src/environment/specs.js +78 -40
  34. package/src/index.d.ts +2 -0
  35. package/src/index.js +1 -32
  36. package/src/simulation/pit/pitFlow.js +5 -0
  37. package/src/simulation/pit/pitKinematics.js +24 -0
  38. package/src/simulation/pit/pitQueue.js +5 -0
  39. package/src/simulation/pit/pitRouteMovement.js +7 -0
  40. package/src/simulation/pit/pitService.js +2 -0
  41. package/src/simulation/pit/pitTireService.js +2 -0
  42. package/src/simulation/race/raceLifecycle.js +2 -6
  43. package/src/simulation/race/raceProgress.js +6 -1
  44. package/src/simulation/race/raceStep.js +1 -1
  45. package/src/simulation/race/redFlag.js +3 -3
  46. package/src/simulation/race/safetyCar.js +10 -4
  47. package/src/simulation/race/stalledDnf.js +8 -17
  48. package/src/simulation/raceSimulation.js +1 -0
  49. package/src/simulation/rules/rulesReview.js +3 -0
  50. package/src/simulation/snapshots/raceSnapshots.js +1 -0
  51. package/src/simulation/track/trackQueryGrid.js +94 -0
  52. package/src/simulation/track/trackQueryIndex.js +16 -302
  53. package/src/simulation/track/trackQueryPitIndex.js +140 -0
  54. package/src/simulation/track/trackQueryScratch.js +46 -0
  55. package/src/simulation/track/trackQueryStats.js +28 -0
  56. package/src/simulation/vehicle/contactResolution.js +67 -2
  57. package/src/simulation/vehicle/runoffResponse.js +11 -12
  58. package/src/simulation/vehicle/vehicleKinematics.js +37 -0
  59. package/src/simulation/vehicle/vehiclePhysics.js +2 -1
  60. package/src/simulation/vehicle/vehicleSnapshots.js +11 -0
  61. package/src/simulation/vehicle/vehicleState.js +36 -2
  62. package/src/simulation/vehicle/wheelSurface.js +3 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.0.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Harden the simulator training environment and model-facing sense contract. This patch keeps vector-only observations aligned with full object observations, keeps indexed ray acceleration equivalent to the legacy ray contract for driver-facing senses, preserves stable terminal-car and pit-route state during training loops, and documents the stricter model-sense boundary.
8
+
3
9
  ## 2.0.0
4
10
 
5
11
  ### Major Changes
@@ -112,7 +112,7 @@ npm run check
112
112
  Install a specific version when the host needs a controlled upgrade:
113
113
 
114
114
  ```bash
115
- npm install @inventure71/paddockjs@2.0.0
115
+ npm install @inventure71/paddockjs@2.0.1
116
116
  npm run check
117
117
  ```
118
118
 
package/README.md CHANGED
@@ -12,7 +12,7 @@ npm install @inventure71/paddockjs
12
12
 
13
13
  ## Documentation
14
14
 
15
- Start with [docs/index.md](docs/index.md) for system specs, rules, concepts, data contracts, and architecture notes. If you already trained a driver model and want to run it in PaddockJS, use the [Custom Model Controller Guide](docs/custom_model_controller.md).
15
+ Start with [docs/index.md](docs/index.md) for the full package map: what to install, which API to use, what data to pass, how rules modules work, how the browser and headless runtimes differ, and which verification commands prove a change. If you are integrating the package into a host site, read [System Specs](docs/system_specs.md), [Data Contract](docs/data_contract.md), and [Rules](docs/rules.md) first. If you already trained a driver model and want to run it in PaddockJS, use the [Custom Model Controller Guide](docs/custom_model_controller.md).
16
16
 
17
17
  ## Package Workflow
18
18
 
@@ -40,9 +40,9 @@ npm run changeset
40
40
 
41
41
  Local development and showcase builds require Node `20.19.0` or newer. CI currently runs the package check on Node 22 and releases on Node 24.
42
42
 
43
- ## 2.0.0 Release Highlights
43
+ ## 2.0 Release Highlights
44
44
 
45
- Version `2.0.0` is the current major release line. It adds the stricter opt-in simulator physics mode, richer procedural track generation, indexed track/ray queries, barrier destruction and stalled-DNF behavior, batch-training participant profiles, replay ghosts, compact/vectorized environment output, policy-server/live-preview Policy Runner paths, Python policy-server examples, the custom model controller guide, the Rules showcase page, and the full release-gated local preview coverage.
45
+ The `2.0.x` release line adds the stricter opt-in simulator physics mode, richer procedural track generation, indexed track/ray queries, barrier destruction and stalled-DNF behavior, batch-training participant profiles, replay ghosts, compact/vectorized environment output, policy-server/live-preview Policy Runner paths, Python policy-server examples, the custom model controller guide, the Rules showcase page, and the full release-gated local preview coverage.
46
46
 
47
47
  The package still does not ship trained model weights, model storage, a Python Gymnasium/PettingZoo package, static obstacles, weather, reliability failures, fuel-load effects, or debug mutation APIs. Those remain host-owned or future scope.
48
48
 
@@ -239,6 +239,8 @@ When multiple drivers are controlled, sensor visualization renders the selected
239
239
 
240
240
  ## API
241
241
 
242
+ The root package exports the browser mounts, composable helper mounts, data/rating helpers, bundled asset and preset constants, procedural-track helper, simulator unit converters, and `createPaddockDriverControllerLoop`. The browser-free training runtime lives under `@inventure71/paddockjs/environment`; see [docs/system_specs.md](docs/system_specs.md) for the full export catalog.
243
+
242
244
  All-in-one mount:
243
245
 
244
246
  ```js
@@ -324,6 +326,23 @@ simulator.mountRaceTelemetryDrawer(document.getElementById('sim-race-workbench')
324
326
  });
325
327
  ```
326
328
 
329
+ When a composable host intends to use pit APIs or automatic pit behavior, include the pit rules in the simulator options before mounting components:
330
+
331
+ ```js
332
+ const simulator = createPaddockSimulator({
333
+ drivers,
334
+ entries,
335
+ rules: {
336
+ ruleset: 'custom',
337
+ modules: {
338
+ pitStops: { enabled: true },
339
+ stalledDnf: { enabled: true },
340
+ tireStrategy: { enabled: true, mandatoryDistinctDryCompounds: 2 },
341
+ },
342
+ },
343
+ });
344
+ ```
345
+
327
346
  `drivers` is the host-owned project/pilot list. `entries` is the optional driver/car/team pairing sheet. The car/driver overview uses the existing driver and vehicle rating components from each entry. Driver and vehicle entries can also include `customFields` as extra label/value metadata. Entries can include `team: { id, name, color, icon, pitCrew }`; the timing tower uses the team icon and defaults team color to the car color when omitted. `team.pitCrew` accepts `speed`, `consistency`, and `reliability` values from `0` to `1` for optional pit-service variability. Assets, including the default car image and generic driver helmet, are bundled by default, so the host website does not need to provide simulator images or textures.
328
347
 
329
348
  The returned object supports:
@@ -439,6 +458,8 @@ Pit-lane asphalt, working-lane service areas, and garage boxes are legal drivabl
439
458
 
440
459
  When `rules.modules.pitStops.enabled` is true, cars automatically form bounded pit trains when lane space is available. They brake to the limiter by the main pit-lane start, drive along the main fast lane, pass through the team queue spot as a rolling gate, roll into the team-colored working-lane service area when it is clear, stop, serve eligible penalties before tire work, show the remaining stationary service time above the car, change to the requested configured tire compound or the default alternate compound, and exit back to the race track.
441
460
 
461
+ `tireStrategy` does not enable automatic pit stops. It controls available compounds, pit-stop target compound choices, and tire-requirement stewarding. Use `rules.modules.pitStops.enabled: true` for automatic pit routing and pit APIs, or choose a preset such as `ruleset: 'fia2025'` / `ruleset: 'grandPrix2025'` that enables both pit stops and the two-compound tire strategy by default.
462
+
442
463
  Team-mates share one service area. Every car enters through the queue spot first, but it only stops there when the active service spot is blocked. A second team car waits in the queue spot until the active service spot is physically clear, including the previous car's first movement out of the box, without blocking the main fast lane.
443
464
 
444
465
  By default, built-in AI cars request an opportunistic `pitIntent: 1` below 50% tire energy and commit `pitIntent: 2` below 30%. Hosts can change those thresholds with `tirePitRequestThresholdPercent` and `tirePitCommitThresholdPercent`. Expert/headless controlled drivers do not receive those automatic tire-threshold calls and must request service with `pitIntent`.
@@ -473,6 +494,10 @@ const simulator = await mountF1Simulator(root, {
473
494
  perfect: false,
474
495
  },
475
496
  },
497
+ tireStrategy: {
498
+ enabled: true,
499
+ mandatoryDistinctDryCompounds: 2,
500
+ },
476
501
  tireDegradation: {
477
502
  enabled: true,
478
503
  },
@@ -498,7 +523,7 @@ const simulator = await mountF1Simulator(root, {
498
523
 
499
524
  For deterministic single-skill training or visual checkpoint comparison, set `rules.modules.tireDegradation.enabled: false` so tyre energy remains fixed while the car still drives through normal steering, throttle, brake, and surface physics. `rules.modules.stalledDnf` defaults to `enabled: false` so base simulator and training environments keep stuck off-track cars live unless a host opts in. Set `enabled: true` to retire cars that are off legal racing or pit surfaces while below `speedThresholdKph` for `maxStoppedSeconds`.
500
525
 
501
- Supported rulesets are `paddock`, `grandPrix2025`, `fia2025`, and `custom`. Presets only choose defaults; explicit module config wins. Weather, reliability, and fuel-load performance effects are reserved future modules and are not active 2.0.0 behavior. Penalty subsections use `strictness` from `0` to `1` instead of plain booleans. Track limits use the white line as the legal edge and require all four wheel contact patches to be fully outside the same side of the line before recording a violation, so normal kerb riding is not punished. Per-car `surface` is resolved from the worst wheel surface, snapshots include `car.wheels` for per-wheel surface and white-line state, and asymmetric left/right wheel resistance adds a small capped yaw tug toward the slower side when only one side is on a worse surface. Collision stewarding is driven by a body collision hull, not transparent sprite bounds or wheel-only overlap, and contact events include shape ids, contact type, depth, and time of impact. It considers impact severity, closing speed, and whether one car clearly hit another from behind; clear rear contact penalizes only the physically trailing car, including lapped traffic cases, while unclear meaningful contact records shared-fault penalties for both cars. Stalled off-track DNFs emit `car-dnf` with `reason: 'stalled-off-track'`, freeze the car, remove it from active control/collision/sensor/pit participation, and classify it with the existing DNF metadata. Pit-lane speeding is enforced on the main fast lane, working lane, service areas, and garage boxes, but not on pit-entry or pit-exit connector roads. Track-limit warnings are emitted as `track-limits` events, while penalty decisions are exposed through `snapshot.penalties` plus `penalty` events. Penalty consequences support warning, time, drive-through, stop-go, position-drop, grid-drop, and disqualification payloads. Time consequences are additive, drive-through and stop-go penalties are service obligations, and unserved service penalties convert to configured time at final classification.
526
+ Supported rulesets are `paddock`, `grandPrix2025`, `fia2025`, and `custom`. Presets only choose defaults; explicit module config wins. Weather, reliability, and fuel-load performance effects are reserved future modules and are not active `2.0.x` behavior. Penalty subsections use `strictness` from `0` to `1` instead of plain booleans. Track limits use the white line as the legal edge and require all four wheel contact patches to be fully outside the same side of the line before recording a violation, so normal kerb riding is not punished. Per-car `surface` is resolved from the worst wheel surface, snapshots include `car.wheels` for per-wheel surface and white-line state, and asymmetric left/right wheel resistance adds a small capped yaw tug toward the slower side when only one side is on a worse surface. Collision stewarding is driven by a body collision hull, not transparent sprite bounds or wheel-only overlap, and contact events include shape ids, contact type, depth, and time of impact. It considers impact severity, closing speed, and whether one car clearly hit another from behind; clear rear contact penalizes only the physically trailing car, including lapped traffic cases, while unclear meaningful contact records shared-fault penalties for both cars. Stalled off-track DNFs emit `car-dnf` with `reason: 'stalled-off-track'`, freeze the car, remove it from active control/collision/sensor/pit participation, and classify it with the existing DNF metadata. Pit-lane speeding is enforced on the main fast lane, working lane, service areas, and garage boxes, but not on pit-entry or pit-exit connector roads. Track-limit warnings are emitted as `track-limits` events, while penalty decisions are exposed through `snapshot.penalties` plus `penalty` events. Penalty consequences support warning, time, drive-through, stop-go, position-drop, grid-drop, and disqualification payloads. Time consequences are additive, drive-through and stop-go penalties are service obligations, and unserved service penalties convert to configured time at final classification.
502
527
 
503
528
  `initialCameraMode` accepts `'overview'`, `'leader'`, `'selected'`, `'show-all'`, or `'pit'`; invalid values fall back to `'leader'`. The overview camera frames the active generated track bounds, including package-owned track padding and pit-lane extent, instead of using a fixed world-center zoom. Camera mode changes ease from the current camera target to the next target after the initial frame, so switching between leader, selected, overview, show-all, and pit views does not snap the world view.
504
529
 
@@ -553,7 +578,7 @@ Mounted package surfaces include a lightweight red start-light loading overlay.
553
578
 
554
579
  The runtime also pauses its render ticker when the race canvas is offscreen or the browser tab is hidden. This keeps pages with multiple PaddockJS embeds responsive without requiring host code to manually start and stop each simulator. At `5x` and `10x` browser playback, noncritical timing/readout DOM refreshes run at a lower cadence while fixed-step simulation and race events keep using authoritative race state.
555
580
 
556
- Lifecycle callbacks are optional and host-owned. PaddockJS emits `onLoadingChange`, `onReady`, `onError`, `onDriverSelect`, `onRaceEvent`, `onLapChange`, and `onRaceFinish`; callback errors are routed to `onError` when provided and do not stop the simulator loop. Race snapshots include per-car interval timing, leader-gap timing, whole-lap gap counts, calibrated `speedKph`, automatic `track.sectors`, hidden `track.timingLines`, per-car `lapTelemetry`, finish state, `raceControl.winner` after the first finisher, and final `raceControl.classification` only after the whole field is finished or DNF. Sector telemetry clears future-sector values so banners and sidebars show completed splits before the active sector plus the active live timer, not stale later-sector entries. Cars that have crossed the line before full race completion expose `raceStatus: 'waved-flag'` / `wavedFlag: true` and stay frozen in provisional finish order; later barrier contact or stalled-off-track detection does not turn that result into DNF. Destroyed/out-of-race cars expose DNF metadata, show `DNF` at the bottom of the timing tower, render faded/gray in the race canvas, and appear after finishers in final classification with no finish time. If a DNF car is restored before final classification, it re-enters live timing and the race waits for it again; once `raceControl.finished` is true, classification stays final. Final classification applies time penalties and unserved service conversions through `adjustedFinishTime`, then applies position-drop and disqualification consequences to finishers before DNF entries. The field then circulates in safety-car mode and the race canvas shows a package-owned winner banner.
581
+ Lifecycle callbacks are optional and host-owned. PaddockJS emits `onLoadingChange`, `onReady`, `onError`, `onDriverSelect`, `onRaceEvent`, `onLapChange`, and `onRaceFinish`; lifecycle callback errors are routed to `onError` when provided and do not stop the simulator loop. `onDriverOpen(driver)` is host-owned navigation code and is not wrapped as a lifecycle callback. Race snapshots include per-car interval timing, leader-gap timing, whole-lap gap counts, calibrated `speedKph`, automatic `track.sectors`, hidden `track.timingLines`, per-car `lapTelemetry`, finish state, `raceControl.winner` after the first finisher, and final `raceControl.classification` only after the whole field is finished or DNF. Sector telemetry clears future-sector values so banners and sidebars show completed splits before the active sector plus the active live timer, not stale later-sector entries. Cars that have crossed the line before full race completion expose `raceStatus: 'waved-flag'` / `wavedFlag: true` and stay frozen in provisional finish order; later barrier contact or stalled-off-track detection does not turn that result into DNF. Destroyed/out-of-race cars expose DNF metadata, show `DNF` at the bottom of the timing tower, render faded/gray in the race canvas, and appear after finishers in final classification with no finish time. If a DNF car is restored before final classification, it re-enters live timing and the race waits for it again; once `raceControl.finished` is true, classification stays final. Final classification applies time penalties and unserved service conversions through `adjustedFinishTime`, then applies position-drop and disqualification consequences to finishers before DNF entries. The field then circulates in safety-car mode and the race canvas shows a package-owned winner banner.
557
582
 
558
583
  ## License
559
584
 
@@ -103,15 +103,15 @@ Responsibilities:
103
103
  `src/environment/` owns the browser-free expert environment:
104
104
 
105
105
  - `index.js` and `index.d.ts` expose the public subpath API.
106
- - `runtime.js` owns environment-loop `reset()`, `step(actions)`, `resetDrivers(placements)`, `getObservation()`, and `getState()` around an injected race-simulation host. It also owns partial reset option merging, preserving omitted nested plain-object groups while replacing arrays and explicit scenario placement maps. It owns per-driver episode bookkeeping for batched environments and formats result state as `full`, `minimal`, or `none` without changing simulator stepping. `resetDrivers()` reapplies normal runoff/barrier classification to the selected cars after placement and before result construction, clears only selected driver episode terminal state, and does not rewind global `info.step`.
106
+ - `runtime.js` owns environment-loop `reset()`, `step(actions)`, `resetDrivers(placements)`, `getObservation()`, and `getState()` around an injected race-simulation host. It also owns partial reset option merging, preserving omitted nested plain-object groups while replacing arrays and explicit scenario placement maps. It owns per-driver episode bookkeeping for batched environments and formats result state as `full`, `minimal`, or `none` without changing simulator stepping. `resetDrivers()` reapplies normal runoff/barrier classification to the selected cars after placement and before result construction, clears only selected driver episode terminal state, and does not rewind global `info.step`. Reset-only observation scope affects the returned observation and metrics maps, while full `info.drivers` remains scoped to each controlled driver's own episode state. Reward callbacks are transition-only and run from `step(actions)`, not reset/read result construction.
107
107
  - `race/stalledDnf.js` owns the optional stalled off-track retirement rule. It consumes current wheel/track surface state after vehicle physics, ignores pre-start/red-flag/finished/pit-controlled states, tracks per-car stopped time below the configured speed threshold, and converts eligible stuck cars into the existing DNF path with `reason: 'stalled-off-track'`. Other modules should consume the resulting `outOfRace`/`dnf` state instead of reimplementing stalled detection.
108
108
  - `options.js` validates `controlledDrivers`, scenario participants, reset placement config, frame skip, action policy, sensor config, and episode limits.
109
109
  - `actions.js` maps normalized public controls onto simulator steering/throttle/brake controls plus pit intent and optional target compound requests. It treats steering, throttle, and brake as required vehicle-control fields so broken model output is reported instead of silently becoming zeroed controls.
110
110
  - `scenarios.js` owns environment reset placement presets, absolute placements, and relative traffic layouts. It applies them through simulator state APIs during environment creation/reset only; it must not create policy-time teleport or assisted-control paths.
111
- - `observations.js`, `sensors.js`, and `events.js` build sensor-style observations, versioned fixed-schema vectors, track lookahead/curvature, ray/nearby-car readings, and global/per-driver events. Observation output formatting is separate from object/vector construction so compact training loops can request vector-only results while `getObservationSpec()` remains the schema source. Vector-only mode uses a direct vector path and can return `Float32Array` buffers without building the full public object observation. When a loop requests vector-only, schema-free, no-state output, `runtime.js` uses the internal `snapshotTraining()` path instead of the heavier public observation snapshot; reward hooks and any returned state still use public snapshot shapes. Environment-owned simulations default to the internal track query index unless `trackQueryIndex: false` is set, because repeated track/surface/ray fallback queries otherwise dominate step cost. `sensors.js` is a compatibility barrel; nearby-car scans, ray defaults, ray config normalization, track rays, surface rays, indexed ray-band intersections, car rays, body senses, contact-patch senses, boundary senses, opponent radar, and ray geometry live under `src/environment/sensors/`. Expensive surface-ray channels are opt-in; driver rays use cached origin state, shared per-observation ray target context, analytic track intersections where the local strip is valid, and indexed ray-boundary intersections across the nearby off-track recovery band. Batch-training rays use the indexed road/surface path for exact road-edge, kerb, and illegal-surface distances while keeping the cheaper target filtering for car rays. Destroyed/out-of-race cars keep the active ray schema but short-circuit to miss-valued rays before creating track ray contexts, which prevents terminal cars in batch training from repeatedly querying far-out geometry. Pit-connector and unusual cases still fall back to sampled checks, but those checks route through the indexed track-query path when the index is active. Ray precision defaults to the sampled model-facing `driver` contract; `debug` precision is diagnostic-only and must not be silently substituted into policy inputs or model-sense visualization.
111
+ - `observations.js`, `observationVector.js`, `sensors.js`, and `events.js` build sensor-style observations, versioned fixed-schema vectors, track lookahead/curvature, ray/nearby-car readings, and global/per-driver events. `observations.js` owns object observation assembly and direct compact-source preparation, while `observationVector.js` is the single owner of model-facing vector order, schema entries, and typed-array formatting for both object-backed and direct vector paths. Observation output formatting is separate from object/vector construction so compact training loops can request vector-only results while `getObservationSpec()` remains the schema source. Vector-only mode uses a direct vector path and can return `Float32Array` buffers without building the full public object observation. When a loop requests vector-only, schema-free, no-state output, `runtime.js` uses the internal `snapshotTraining()` path instead of the heavier public observation snapshot; returned state still uses public snapshot shapes. Environment-owned simulations default to the internal track query index unless `trackQueryIndex: false` is set, because repeated track/surface/ray fallback queries otherwise dominate step cost. `sensors.js` is a compatibility barrel; nearby-car scans, ray defaults, ray config normalization, track rays, surface rays, indexed ray-band intersections, car rays, body senses, contact-patch senses, boundary senses, opponent radar, and ray geometry live under `src/environment/sensors/`. Expensive surface-ray channels are opt-in and appear in the vector schema whenever requested. Replay-ghost sensor type flags are also part of compact ray/nearby-car vectors so vector-only consumers can distinguish real cars from opt-in references. Driver rays use a precision-aware cached origin state and shared per-observation ray target context. Normal driver `precision: 'driver'` road-edge, kerb, and illegal-surface channels keep the sampled values that policies receive with or without the track query index. The `batch-training` participant profile keeps the indexed recovery-ray path as its high-throughput training contract, and other indexed ray-boundary shortcuts are reserved for non-driver/debug precision paths. Destroyed/out-of-race cars keep the active ray schema but short-circuit to miss-valued rays before creating track ray contexts, which prevents terminal cars in batch training from repeatedly querying far-out geometry. Pit-connector and unusual cases still fall back to sampled checks. Ray precision defaults to the sampled model-facing `driver` contract; `debug` precision is diagnostic-only and must not be silently substituted into policy inputs or model-sense visualization.
112
112
  - `metrics.js` owns reward-neutral per-driver facts such as progress delta, legal progress delta, off-track/severe-cut state, destroyed state, low-speed state, spin/backwards/instability state, lap completion, lap time, and contact count. Contact metrics dedupe the identifiers on each normalized physical contact event so one event increments each involved driver once. It must not encode or choose rewards.
113
- - `recorder.js` owns neutral rollout transition export. It records environment-loop data and does not interpret rewards or policies.
114
- - `controllerLoop.js` owns browser/headless controller orchestration around an injected runtime. It may lazily reset a fresh runtime before scheduled playback, but public `reset()` remains an explicit playback stop. Controller contexts report current `actions` separately from `previousActions`, where previous means the last action map actually applied before the current physics step. Scheduled playback catches controller/runtime failures, stops the loop, and exposes the thrown value through loop stats.
113
+ - `recorder.js` owns neutral rollout transition export. It snapshots observations, actions, rewards, next observations, terminal flags, and info at record time and does not interpret rewards or policies.
114
+ - `controllerLoop.js` owns browser/headless controller orchestration around an injected runtime. It may lazily reset a fresh runtime before scheduled playback, but public `reset()` remains an explicit playback stop. Controller contexts report current `actions` separately from `previousActions`, where previous means the last action map actually applied before the current physics step. It maintains an observation cache so reset-only `resetDrivers()` results can stay small without handing null observations to the next batch decision for non-reset controlled drivers. Scheduled playback catches controller/runtime failures, stops the loop, and exposes the thrown value through loop stats.
115
115
  - `evaluation.js` owns deterministic evaluation cases and metrics. It can run a supplied policy against fixed seeds/scenarios, internally requests the minimal snapshot needed when compact no-state base options are supplied, and reuses environment metric semantics for legal surfaces and contacts. It must not update models or define reward objectives.
116
116
  - `workerProtocol.js` owns the JSON-serializable wrapper used by external bridges. It forwards reset/step/spec/state messages to an environment instance, including `getState` output selection, and does not choose Python, Gymnasium, PettingZoo, or storage infrastructure.
117
117
 
@@ -119,7 +119,7 @@ The environment subpath must not import `src/index.js`, package CSS, PixiJS, `F1
119
119
 
120
120
  `src/simulation/rulesConfig.js` is the public rules normalization facade. Defaults live in `src/simulation/rules/ruleDefaults.js`, ruleset presets in `rulesetPresets.js`, merge/value helpers in `ruleConfigMerge.js`, module normalization in `moduleConfig.js`, and penalty consequence normalization in `penaltyConfig.js`. Race modules should be added through those focused files first, then consumed by simulation logic through the normalized `race.rules` object.
121
121
 
122
- `src/simulation/track/trackModel.js` is the canonical track-model facade. Procedural centerline generation, validation, sample rebuilding, start-straight normalization, sectors, DRS zones, pit-lane layout/access/state, spatial query indexing, and spatial queries live in focused `src/simulation/track/*` modules. `src/simulation/trackModel.js` remains a compatibility re-export for tests or older internal imports, but new production code should import the canonical track module. `buildTrackModel()` attaches a non-enumerable internal `track.queryIndex` after samples, DRS, sectors, and pit-lane geometry are finalized, and cached built models freeze their static geometry so consumers cannot mutate shared pit-lane data between builds. Race simulations clone the built track to own mutable timing/pit state and default to attaching a fresh runtime index fork unless `trackQueryIndex: false` is explicitly requested. The index owns typed centerline segment data, an expanded uniform spatial grid for nearest-track recovery, a tight centerline-segment grid for ray and bounds candidate queries, arc-length buckets, surface band thresholds, pit-lane road/box grids, route-grouped pit-road candidate results, and internal query diagnostics for benchmark/debug use. Public callers still use `nearestTrackState()` and the same snapshot shapes; the index is internal infrastructure and must not leak into JSON snapshots or host contracts. Indexed nearest lookup resolves equal-distance centerline segment ties deterministically inside the index; legacy nearest-sample fallback is reserved for missing indexes, invalid positions, or points outside the indexed world bounds. Ray-boundary queries walk the tight segment grid along the ray corridor instead of querying the expanded recovery grid, which keeps compact training-track profiles from producing thousands of false candidate segments per ray. The model explicitly straightens the start/finish window so the starting grid, pit-entry approach area, and immediate post-line exit area are geometrically straight instead of merely choosing the least-curved generated segment. The pit lane is generated next to that start straight for every built track and exposes entry/exit connectors, explicit procedural road centerlines that overlap the track surface, connect on the lane-facing side of the main track, are tangent to the track at merge points and to the main pit lane at pit-lane endpoints, a straight main fast lane sized from the team/box run plus bounded entry/exit buffers, a parallel working lane, one shared service area per team, queue geometry behind each service area, and two garage boxes per team. `src/rendering/proceduralTrackAsset.js` is now only the Pixi container lifecycle wrapper; main-circuit drawing, pit-lane drawing, grid rendering, finish-line rendering, material constants, render geometry, and offset-stroke safety live under `src/rendering/track/`. `F1SimulatorApp` overlays the dynamic pit-lane status light because open/closed/red-flag state belongs to race control, not the static track texture.
122
+ `src/simulation/track/trackModel.js` is the canonical track-model facade. Procedural centerline generation, validation, sample rebuilding, start-straight normalization, sectors, DRS zones, pit-lane layout/access/state, spatial query indexing, and spatial queries live in focused `src/simulation/track/*` modules. `src/simulation/trackModel.js` remains a compatibility re-export for tests or older internal imports, but new production code should import the canonical track module. `buildTrackModel()` attaches a non-enumerable internal `track.queryIndex` after samples, DRS, sectors, and pit-lane geometry are finalized, and cached built models freeze their static geometry so consumers cannot mutate shared pit-lane data between builds. Race simulations clone the built track to own mutable timing/pit state and default to attaching a fresh runtime index fork unless `trackQueryIndex: false` is explicitly requested. `trackQueryIndex.js` owns the public indexed-query orchestration over typed centerline segment data, arc-length buckets, surface band thresholds, and deterministic nearest/ray query selection. Generic uniform grid operations live in `trackQueryGrid.js`, scratch buffer/epoch reuse lives in `trackQueryScratch.js`, query diagnostics live in `trackQueryStats.js`, and pit-lane road/box grid construction plus pit candidate lookups live in `trackQueryPitIndex.js`. Public callers still use `nearestTrackState()` and the same snapshot shapes; the index is internal infrastructure and must not leak into JSON snapshots or host contracts. Indexed nearest lookup resolves equal-distance centerline segment ties deterministically inside the index; legacy nearest-sample fallback is reserved for missing indexes, invalid positions, or points outside the indexed world bounds. Ray-boundary queries walk the tight segment grid along the ray corridor instead of querying the expanded recovery grid, which keeps compact training-track profiles from producing thousands of false candidate segments per ray. The model explicitly straightens the start/finish window so the starting grid, pit-entry approach area, and immediate post-line exit area are geometrically straight instead of merely choosing the least-curved generated segment. The pit lane is generated next to that start straight for every built track and exposes entry/exit connectors, explicit procedural road centerlines that overlap the track surface, connect on the lane-facing side of the main track, are tangent to the track at merge points and to the main pit lane at pit-lane endpoints, a straight main fast lane sized from the team/box run plus bounded entry/exit buffers, a parallel working lane, one shared service area per team, queue geometry behind each service area, and two garage boxes per team. `src/rendering/proceduralTrackAsset.js` is now only the Pixi container lifecycle wrapper; main-circuit drawing, pit-lane drawing, grid rendering, finish-line rendering, material constants, render geometry, and offset-stroke safety live under `src/rendering/track/`. `F1SimulatorApp` overlays the dynamic pit-lane status light because open/closed/red-flag state belongs to race control, not the static track texture.
123
123
 
124
124
  `src/simulation/rules/` owns focused rule calculation helpers. Each stewarded rule should have its own function and, once non-trivial, its own file. `raceSimulation.js` should orchestrate timing/state and call those helpers; rule modules should return events or penalty payloads rather than mutating the whole simulation directly.
125
125
 
@@ -178,11 +178,13 @@ Warmup ownership is split intentionally: `src/simulation/warmup/runtimeWarmup.js
178
178
 
179
179
  When `options.expert.enabled` is true, `F1SimulatorApp` creates a narrow browser expert adapter around its existing `this.sim` race simulation. The adapter uses the same shared environment runtime as the headless API, but it must not create a second race simulation for the visual mount. Expert browser mounts disable automatic ticker-driven simulation advancement; the canvas updates only after `simulator.expert.reset()` or `simulator.expert.step(actions)` renders the new snapshot. External renderer mode may temporarily redraw the shared track asset from authoritative external snapshots; detach restores the local simulation track before local expert stepping resumes. The shared runtime owns controlled-driver pit intent defaults: controlled cars start with `pitIntent` set to `0`, tire-threshold automatic pit calls are disabled for them, and expert actions may request `0`, `1`, or `2` plus optional `pitCompound` through the simulation API instead of steering into the pit lane directly. Expert mode is a mount-time boundary; runtime restart rejects `expert` changes instead of attempting to rewire ticker ownership in place.
180
180
 
181
+ `src/app/BrowserExpertExternalRenderer.js` owns the external-renderer bridge used by browser expert mode: source subscription lifecycle, external-to-local driver-id mapping, external frame observation/event/penalty id rewriting, external track-surface redraws, local track restoration on detach, and external-renderer state reporting. `BrowserExpertAdapter.js` remains the local visual expert runtime adapter and should not grow those external-frame responsibilities again.
182
+
181
183
  Expert sensor visualization belongs to the browser app layer, not the headless environment. The environment result owns the observation contract; `BrowserExpertAdapter` passes that observation into `F1SimulatorApp.renderExpertFrame()`, and `src/app/rendering/expertSensorRenderer.js` draws opt-in sensor rays in a Pixi world layer so they share the same camera transform as the track and cars. Multi-driver expert mounts visualize the selected controlled car by default, with an explicit `drivers: 'all'` opt-in for heavier all-controlled-car overlays. Ray visualization draws every detected model-facing channel as a separate marker rather than choosing only the nearest hit, which keeps road-edge, kerb, illegal-surface, and car detection debuggable. Barrier walls are track geometry and destruction boundaries, not model-facing ray hits; active ray objects, vectors, schemas, and visualizations must not expose a `barrier` ray channel. The Policy Runner senses panel follows the same boundary for all senses: it reads the active observation object/vector and may show separate diagnostics only when those are labeled as diagnostics, not as model input.
182
184
 
183
185
  Track material rendering must use the same surface-band thresholds as simulation. `src/rendering/track/trackMaterialRenderer.js` exposes `getTrackMaterialBands(track)` as the rendering-side band contract derived from `track.width`, `track.kerbWidth`, `track.gravelWidth`, `track.runoffWidth`, and `track.barrierWidth`; gravel, runoff, visible barrier walls, ray surface hits, wheel surfaces, and barrier destruction must not invent independent offsets or hardcoded widths. Barrier contact uses the visible vehicle footprint against the rendered wall's inner face so visual contact, destruction, and the environment's terminal state agree.
184
186
 
185
- Environment sensor target selection belongs to `src/environment/sensors/sensorTargets.js`. It adapts real cars and opt-in replay ghosts into a common sensor target shape while preserving the invariant that replay ghosts stay out of `snapshot.cars` and race systems.
187
+ Environment sensor target selection belongs to `src/environment/sensors/sensorTargets.js`. It adapts real cars and opt-in replay ghosts into a common sensor target shape while preserving the invariant that replay ghosts stay out of `snapshot.cars` and race systems. Radar enrichment keys targets by entity type plus id, so duplicate ids between real cars and replay ghosts do not cross-contaminate velocity-derived nearby-car facts.
186
188
 
187
189
  `F1SimulatorApp.js` should stay a facade/orchestrator. New camera, rendering, readout, banner, or runtime behavior should usually land in the matching `src/app/*/` ownership module rather than expanding the app class.
188
190
 
@@ -245,13 +247,13 @@ Responsibilities:
245
247
 
246
248
  `src/simulation/rules/penaltyStats.js` owns penalty-stat aggregation and lookup helpers.
247
249
 
248
- `src/simulation/vehicle/contactResolution.js` owns physical collision resolution and contact velocity response. Stewarding remains separate from physical response.
250
+ `src/simulation/vehicle/contactResolution.js` owns physical collision resolution and contact velocity response. Stewarding remains separate from physical response. In simulator mode, every collision branch that changes speed also keeps `velocityX/Y` synchronized, including contacts where one car is fixed by pit entry, queue, service, or exit routing.
249
251
 
250
252
  `src/simulation/vehicle/runoffResponse.js` owns non-penalty off-track vehicle correction.
251
253
 
252
254
  `src/simulation/snapshots/raceSnapshots.js` owns public, render, and observation snapshot assembly.
253
255
 
254
- Weather effects, reliability failures, and fuel-load performance effects are not active 2.0.0 behavior. Their future ownership should stay separate from the race orchestrator: weather belongs in a future `src/simulation/weather/` module that returns track/session condition modifiers, reliability belongs in a future `src/simulation/reliability/` module that emits deterministic failure/degradation events, and fuel-load effects belong in a future vehicle-adjacent performance module that adjusts mass/pace through explicit inputs rather than hidden mutation.
256
+ Weather effects, reliability failures, and fuel-load performance effects are not active `2.0.x` behavior. Their future ownership should stay separate from the race orchestrator: weather belongs in a future `src/simulation/weather/` module that returns track/session condition modifiers, reliability belongs in a future `src/simulation/reliability/` module that emits deterministic failure/degradation events, and fuel-load effects belong in a future vehicle-adjacent performance module that adjusts mass/pace through explicit inputs rather than hidden mutation.
255
257
 
256
258
  `snapshot()` is the full public data contract and must remain stable for host callbacks, environment state, and external callers. Internal browser/rendering paths use lean read models such as `snapshotRender()` so the 60 FPS loop does not serialize setup, wheels, penalties, and other non-render fields every frame. Environment and sensor code may use similarly narrow internal views, cache shared per-observation ray origin state, and broadphase car-ray checks before exact footprint intersection, but those must not change the public `RaceSnapshot` shape.
257
259
 
@@ -263,7 +265,9 @@ Weather effects, reliability failures, and fuel-load performance effects are not
263
265
 
264
266
  `src/simulation/vehicle/wheelSurface.js` owns per-wheel track sampling and the effective car surface. Surface priority, analytic main-track classification, and pit-lane/connector classification are split into `surfacePriority.js`, `mainTrackWheelSurface.js`, and `pitWheelSurface.js`. On normal main-track running it uses the cached wheel geometry and the car center's track normal to analytically classify each wheel patch from signed offsets. It falls back to full patch sampling near pit connectors and inside pit-lane states. `vehiclePhysics.js` consumes that effective surface through the car track state, and separately compares left-side and right-side wheel resistance to add a small capped yaw bias toward the slower side.
265
267
 
266
- `src/simulation/vehiclePhysics.js` owns vehicle integration using the already-resolved wheel surfaces and wheel-state resistance imbalance. It exposes the normalized `physicsMode` contract and routes between the compatibility arcade integrator and the opt-in simulator integrator. The simulator path owns 2D velocity integration, yaw response, traction-budget calculation, speed-sensitive steering, steering scrub, aero drag/downforce balance, per-wheel surface averaging, derived slip-angle telemetry, simulator surface coefficients, and telemetry fields; driver AI and environment policies still provide only steering/throttle/brake/pit intent.
268
+ `src/simulation/vehicle/vehiclePhysics.js` owns vehicle integration using the already-resolved wheel surfaces and wheel-state resistance imbalance. It exposes the normalized `physicsMode` contract and routes between the compatibility arcade integrator and the opt-in simulator integrator. The simulator path owns 2D velocity integration, yaw response, traction-budget calculation, speed-sensitive steering, steering scrub, aero drag/downforce balance, per-wheel surface averaging, derived slip-angle telemetry, simulator surface coefficients, and telemetry fields; steering scrub opposes signed forward motion so reverse slides do not gain energy from steering loss. Driver AI and environment policies still provide only steering/throttle/brake/pit intent.
269
+
270
+ `src/simulation/vehicle/vehicleState.js` owns external state mutation from simulator and environment reset APIs. It rejects non-finite numeric partial state and resynchronizes simulator velocity from speed/heading when callers do not provide an explicit velocity vector, so scenario placement and `resetDrivers()` cannot leave stale physics velocity behind.
267
271
 
268
272
  `src/simulation/rules/pitLaneSpeedingSteward.js` owns pit-speed-limit review. `raceSimulation.js` calls it after authoritative race-state recalculation so it reads the same pit-lane part classification as snapshots and track-limit stewarding. The steward only checks speed-limited pit-lane parts (`fast-lane`, `working-lane`, service areas, and garage boxes), leaving pit-entry and pit-exit connector roads legal but not speed-limited.
269
273
 
@@ -316,6 +320,8 @@ Penalty UI is display-only. `src/app/banners/stewardMessageController.js` may sh
316
320
 
317
321
  `src/config/defaultOptions.js` owns preset resolution, telemetry module normalization, the public theme contract, and browser warmup option normalization defaults. Presets are merged before host options; theme fields are applied as package CSS variables by the all-in-one app and composable controller. This keeps sizing/color customization explicit without turning internal layout ratios into public API.
318
322
 
323
+ `src/config/restartOptions.js` owns shared restart-option merging for both `mountF1Simulator().restart()` and `createPaddockSimulator().restart()`. It preserves omitted driver/entry data, merges nested UI banner and asset texture groups, and resets prior UI/theme state when a restart supplies a new preset so the two browser mount surfaces cannot drift.
324
+
319
325
  Race completion is owned by `src/simulation/raceSimulation.js`. The simulation records provisional `finishOrder` / `finishRank` as each car crosses the line and uses that order for snapshots until final classification exists, so app code never has to re-sort finished cars by live distance. Race retirement state is owned by `src/simulation/race/retirements.js`; it defines DNF detection and ordering while vehicle code only marks the physical destruction event. The app layer reads `raceControl.finished`, `winner`, `classification`, and per-car `raceStatus`/DNF fields from snapshots, renders the timing/winner states, and emits lifecycle callbacks. Penalty lifecycle, pit-stop penalty service, service conversion, time adjustment, position drops, grid drops, disqualification effects, and DNF classification placement are applied inside the simulation, not in the UI. UI code may display countdowns from `pitStop.phase`, `serviceRemainingSeconds`, and `penaltyServiceRemainingSeconds`, but it must not infer, serve, or recalculate steward outcomes from lap text, timing rows, or car position.
320
326
 
321
327
  Composable hosts may choose where each package-owned component root is placed, but they still receive package-generated markup through the public mount functions. The controller marks each mounted root as an `f1-sim-component` styling scope so standalone pieces receive the same package variables as the all-in-one shell.
package/docs/concepts.md CHANGED
@@ -55,7 +55,7 @@ The composable controller has a setup phase:
55
55
 
56
56
  After startup, both controller styles expose runtime methods such as `restart()`, `selectDriver()`, `setSafetyCarDeployed()`, `getSnapshot()`, and `destroy()`.
57
57
 
58
- Pit-stop hosts can also call `setPitIntent(driverId, 0 | 1 | 2)`. `0` means no pending pit request, `1` means take the next pit entry only if it is free, and `2` means keep the request active until the automatic pit-stop sequence completes.
58
+ Pit-stop hosts can also call `setPitIntent(driverId, 0 | 1 | 2, targetCompound?)` after enabling `rules.modules.pitStops`. `0` clears a pending request, `1` keeps trying until a free-enough pit-entry window is available, and `2` commits to entering at the next pit-entry window even when opportunistic capacity or gap checks would block mode `1`. `targetCompound` must be one of `rules.modules.tireStrategy.compounds`; `tireStrategy` controls tire choices and stewarding, but does not enable pit stops by itself.
59
59
 
60
60
  ## Driver
61
61
 
@@ -87,7 +87,7 @@ Entry responsibilities:
87
87
 
88
88
  ## Team
89
89
 
90
- A team is entry-level metadata used for race identity and future pit-lane behavior. The timing tower uses the team icon in its team column. Team color defaults to the car color when omitted.
90
+ A team is entry-level metadata used for race identity and current pit-lane behavior. The timing tower uses the team icon in its team column. Team color defaults to the car color when omitted. When pit stops are enabled, team color marks the shared service area, queue point, and garage boxes, and optional `team.pitCrew` stats can affect service-time variability.
91
91
 
92
92
  ## Driver Ratings
93
93
 
@@ -127,7 +127,7 @@ The package supports:
127
127
 
128
128
  Browser mounts that omit `trackSeed` create a fresh procedural track for that mount. Passing `trackSeed` makes the generated circuit deterministic; repeated procedural seeds are cached within the page runtime. Generated circuits start from seeded connected coarse regions, trace the region boundary, then smooth and warp that boundary into the sampled centerline. This lets tracks move inward and outward through concave sections and chicane-like bends instead of degrading into circular or oval fallback layouts. Failed candidates retry with another deterministic region-derived shape.
129
129
 
130
- Every built track also exposes a deterministic `pitLane` near the start/finish straight. The pit lane has an entry before the start line, an exit after it, explicit lane-aligned entry/exit road centerlines, a straight main fast lane, a parallel working lane, 10 shared team service areas, and 20 unused garage boxes arranged as 10 team pairs. Pit-lane asphalt, service areas, and garage boxes are legal drivable surfaces for sensors, runoff handling, and track-limit stewarding. When the pit-stop module is enabled, cars automatically form bounded pit trains when there is enough rolling gap, brake to the limiter by the main lane start, follow the fast lane, pass through their assigned colored team queue spot, roll into the team service area, change tire compound, and return through the exit. Team-mates share one service area; every car passes through the queue spot first, but it only waits there if the active service area is occupied. A second team car waits without blocking the fast lane before following a short queue-release route into the active service area after the previous car has physically cleared it. Tire condition can request a stop automatically: below the configured request threshold the car asks to pit if free, and below the commit threshold it keeps retrying until served. The speed limiter is active on the straight main pit lane/working lane, not on the entry and exit connector roads.
130
+ Tracks whose resolved generation options enable pit lanes expose a deterministic `pitLane` near the start/finish straight. The `race` profile includes pit-lane geometry; training profiles are pitless by default unless the host explicitly enables a pit lane. An enabled pit lane has an entry before the start line, an exit after it, explicit lane-aligned entry/exit road centerlines, a straight main fast lane, a parallel working lane, 10 shared team service areas, and 20 unused garage boxes arranged as 10 team pairs. Pit-lane asphalt, service areas, and garage boxes are legal drivable surfaces for sensors, runoff handling, track-limit stewarding, and stalled-DNF timing. When the pit-stop module is enabled, cars automatically form bounded pit trains when there is enough rolling gap, brake to the limiter by the main lane start, follow the fast lane, pass through their assigned colored team queue spot, roll into the team service area, change tire compound, and return through the exit. Team-mates share one service area; every car passes through the queue spot first, but it only waits there if the active service area is occupied. A second team car waits without blocking the fast lane before following a short queue-release route into the active service area after the previous car has physically cleared it. Tire condition can request a stop automatically: below the configured request threshold the car asks to pit if free, and below the commit threshold it keeps retrying until served. The speed limiter is active on the straight main pit lane/working lane, not on the entry and exit connector roads.
131
131
 
132
132
  ## Progress
133
133
 
@@ -161,7 +161,9 @@ Per-car timing exposes both interval to the car ahead and direct same-lead-lap g
161
161
 
162
162
  A ruleset is a named preset for race-rule defaults. `paddock` is the package default, `grandPrix2025` / `fia2025` are 2024-2025-era grand-prix-style presets, and `custom` is for host-owned behavior.
163
163
 
164
- A rule module is an advanced subsystem under `rules.modules`, such as pit stops, tire strategy, tire degradation, stalled off-track DNF, penalties, weather, reliability, or fuel load. Presets set defaults, but explicit module config wins.
164
+ A rule module is an advanced subsystem under `rules.modules`. Active modules include pit stops, tire strategy, tire degradation, stalled off-track DNF, and penalties. Weather, reliability, and fuel load are reserved placeholders and do not currently change grip, power, failure risk, mass, or pace. Presets set defaults, but explicit module config wins.
165
+
166
+ `pitStops` and `tireStrategy` are intentionally separate. `pitStops.enabled` owns automatic pit routing, pit-lane status, and successful pit intent requests. `tireStrategy.enabled` owns compound lists, tire-requirement stewarding, and pit target choices. A two-compound rule does not make cars pit unless `pitStops` is enabled separately or the selected ruleset preset enables both modules.
165
167
 
166
168
  Stalled off-track DNF is an opt-in retirement rule. When `rules.modules.stalledDnf.enabled` is `true`, it marks a car DNF when the car is off legal racing/pit surfaces and remains below the configured speed threshold for the configured time, while excluding pre-start, red flag, active pit handling, and already-finished cars.
167
169
 
@@ -14,6 +14,7 @@ mountF1Simulator(root, {
14
14
  onDriverOpen,
15
15
  seed,
16
16
  trackSeed,
17
+ trackGeneration,
17
18
  trackQueryIndex,
18
19
  warmup,
19
20
  totalLaps,
@@ -29,6 +30,7 @@ mountF1Simulator(root, {
29
30
  backLinkLabel,
30
31
  showBackLink,
31
32
  ui,
33
+ debug,
32
34
  assets,
33
35
  expert,
34
36
  onLoadingChange,
@@ -50,6 +52,7 @@ const simulator = createPaddockSimulator({
50
52
  onDriverOpen,
51
53
  seed,
52
54
  trackSeed,
55
+ trackGeneration,
53
56
  trackQueryIndex,
54
57
  warmup,
55
58
  totalLaps,
@@ -61,6 +64,7 @@ const simulator = createPaddockSimulator({
61
64
  preset,
62
65
  theme,
63
66
  ui,
67
+ debug,
64
68
  assets,
65
69
  expert,
66
70
  });
@@ -114,6 +118,7 @@ const env = createPaddockEnvironment({
114
118
  standingStart: false,
115
119
  modules: {
116
120
  penalties: {
121
+ enabled: true,
117
122
  trackLimits: { strictness: 0.25 },
118
123
  },
119
124
  },
@@ -133,7 +138,7 @@ result = env.step({
133
138
  `controlledDrivers` is required. It supports one or many externally controlled cars. Non-controlled participants use the built-in driver AI in the stable 1.0 environment API.
134
139
  `externalRenderer` is optional and observer-only. It can be a function or `{ onFrame(frame) }`, and receives `{ snapshot, observation, meta }` on `reset`, `step`, and `resetDrivers`. The hook does not mutate simulation state and hook failures are isolated so stepping continues.
135
140
  `warmup` is optional and enabled by default. It primes a disposable runtime during load/reset creation and caches by configuration fingerprint so repeated identical resets skip warmup. Use `warmup: { enabled, policy: 'config-change' | 'always' | 'never', steps }` or `warmup: false` to disable.
136
- `physicsMode` is optional and accepts `'arcade'` or `'simulator'`. Invalid values fall back to `'arcade'`, which is the default compatibility mode. `'simulator'` keeps the same public action contract but uses stricter 2D velocity/yaw dynamics and exposes additional telemetry fields on snapshots and observations: `lateralG`, `longitudinalG`, `gripUsage`, `slipAngleRadians`, `tractionLimited`, and `stabilityState`. In simulator mode, `slipAngleRadians` is derived from the car heading versus actual velocity direction, and mixed wheel surfaces are averaged for physics while wheel snapshots still report each contact patch.
141
+ `physicsMode` is optional and accepts `'arcade'` or `'simulator'`. Invalid values fall back to `'arcade'`, which is the default compatibility mode. `'simulator'` keeps the same public action contract but uses stricter 2D velocity/yaw dynamics and exposes additional telemetry fields on snapshots and observations: `velocityX`, `velocityY`, `lateralG`, `longitudinalG`, `gripUsage`, `slipAngleRadians`, `tractionLimited`, and `stabilityState`. In simulator mode, `slipAngleRadians` is derived from the car heading versus actual velocity direction, and mixed wheel surfaces are averaged for physics while wheel snapshots still report each contact patch. Terminal and race-control-held simulator cars publish `velocityX: 0` and `velocityY: 0` whenever scalar `speed` is zero, so training loops do not see stale pre-DNF, pre-grid, pre-red-flag, or pre-queue motion. Arcade snapshots publish `velocityX` and `velocityY` as `null`; consumers that need a heading-aligned arcade velocity should derive it from `heading` and `speed`.
137
142
  `rules` is an optional override object for the race rules documented in [rules.md](rules.md). Flat keys such as `standingStart: false` still work for existing behavior. Advanced systems live under `rules.modules` so hosts can choose a preset and then override individual modules:
138
143
 
139
144
  ```js
@@ -148,6 +153,10 @@ rules: {
148
153
  tirePitRequestThresholdPercent: 50,
149
154
  tirePitCommitThresholdPercent: 30,
150
155
  },
156
+ tireStrategy: {
157
+ enabled: true,
158
+ mandatoryDistinctDryCompounds: 2,
159
+ },
151
160
  tireDegradation: {
152
161
  enabled: false,
153
162
  },
@@ -166,7 +175,7 @@ rules: {
166
175
  }
167
176
  ```
168
177
 
169
- Supported rulesets are `paddock`, `grandPrix2025`, `fia2025`, and `custom`. The `fia2025` name is a 2024-2025-era grand-prix-style package preset; explicit module config always wins over preset defaults. `rules.modules.tireDegradation.enabled: false` freezes tyre energy for deterministic training or visual comparison without changing tire compounds or pit rules. `rules.modules.stalledDnf` defaults to `{ enabled: false, maxStoppedSeconds: 12, speedThresholdKph: 5 }`; set `enabled: true` to retire stuck off-track cars with `dnfReason: 'stalled-off-track'`. Pre-start, red flag, legal pit surfaces, active pit handling, and already-finished cars are excluded. Penalty strictness is clamped from `0` to `1`, where `0` disables enforcement for that subsection and `1` uses the configured rule margin. `rules` is not a direct state-mutation API.
178
+ Supported rulesets are `paddock`, `grandPrix2025`, `fia2025`, and `custom`. The `fia2025` name is a 2024-2025-era grand-prix-style package preset; explicit module config always wins over preset defaults. `rules.modules.pitStops.enabled` is the switch for automatic pit routing, pit-lane status, and successful `setPitIntent()` calls. `rules.modules.tireStrategy.enabled` is separate: it controls available compounds, tire-requirement stewarding, and pit target choices, but it does not enable pit stops. Use `ruleset: 'fia2025'` / `ruleset: 'grandPrix2025'` or explicitly enable both modules when a host wants a two-compound race with automatic stops. `rules.modules.tireDegradation.enabled: false` freezes tyre energy for deterministic training or visual comparison without changing tire compounds or pit rules. `rules.modules.stalledDnf` defaults to `{ enabled: false, maxStoppedSeconds: 12, speedThresholdKph: 5 }`; set `enabled: true` to retire stuck off-track cars with `dnfReason: 'stalled-off-track'` only when they are off legal racing or pit surfaces. Pre-start, red flag, legal pit surfaces, active pit handling, and already-finished cars are excluded. Penalty strictness is clamped from `0` to `1`, where `0` disables enforcement for that subsection and `1` uses the configured rule margin. `rules` is not a direct state-mutation API.
170
179
 
171
180
  Scenario support:
172
181
 
@@ -198,7 +207,7 @@ scenario: {
198
207
 
199
208
  Scenario placement is applied only during environment creation/reset through the simulator state API. It does not give policies a state-mutation path during `step(actions)`. Explicit `placements` override preset placement for the same driver, and `traffic` places another participant relative to a placed or existing car. `env.reset(partialOptions)` preserves omitted nested option groups, but an explicitly supplied `scenario.placements` object replaces the old placement map so callers can clear stale reset placements with `{ scenario: { placements: {} } }`. Static obstacles, debug mutation, assisted controls, and Python Gymnasium wrappers are intentionally deferred. Replay ghosts and participant interaction profiles are supported separately below. The supported package boundary today is JavaScript Gym-style control plus a JSON worker protocol, not a Python Gym package and not a scenario editor.
200
209
 
201
- `env.resetDrivers(placements)` applies the same placement shape to selected controlled drivers without recreating the whole environment. It is an episode-boundary API for batched training: selected drivers get a new `episodeId`, `episodeStep: 0`, cleared manual controls, and cleared pit intent. Non-selected controlled drivers keep their current car state and episode counters. Max-step truncation is evaluated from each reported driver's own `episodeStep`, so resetting one truncated driver clears that driver's done state without rewinding the environment `info.step` or changing other drivers' episode state. Reset placement is still validated against the normal track/runoff/barrier model before observations are returned. If a reset placement puts a car into the barrier destruction boundary, the returned result reports that driver as `destroyed` with `endReason: 'destroyed'` and stable miss-valued rays instead of doing an expensive far-out sensor scan. This API is not accepted inside `step(actions)`.
210
+ `env.resetDrivers(placements)` applies the same placement shape to selected controlled drivers without recreating the whole environment. It is an episode-boundary API for batched training: selected drivers get a new `episodeId`, `episodeStep: 0`, cleared manual controls, and cleared pit intent. Non-selected controlled drivers keep their current car state and episode counters. Max-step truncation is evaluated from each reported driver's own `episodeStep`, so resetting one truncated driver clears that driver's done state without rewinding the environment `info.step` or changing other drivers' episode state. If all reported controlled drivers are ended, top-level `done` is true even when their terminal states are mixed, such as one destroyed driver and one max-step-truncated driver. Reset placement is still validated against the normal track/runoff/barrier model before observations are returned. If a reset placement puts a car into the barrier destruction boundary, the returned result reports that driver as `destroyed` with `endReason: 'destroyed'` and stable miss-valued rays instead of doing an expensive far-out sensor scan. With reset-only observation scope, top-level terminal state is evaluated over the reported reset drivers but `info.drivers` still reflects each controlled driver's own state, so a destroyed reset driver does not falsely terminate healthy non-reset drivers. This API is not accepted inside `step(actions)`.
202
211
 
203
212
  Participant interaction profiles are simulator-owned environment setup, not RL logic. They let hosts run multiple real cars in one environment without forcing every car to collide with or appear in every other car's sensors:
204
213
 
@@ -254,7 +263,7 @@ replayGhosts: [
254
263
  ]
255
264
  ```
256
265
 
257
- Replay ghosts appear in `snapshot.replayGhosts`, interpolate by `timeSeconds`, render as translucent browser overlays, and never enter `snapshot.cars`, timing rows, race order, pit logic, collision resolution, or steward penalties. They are hidden from rays and nearby-car observations by default. If `sensors.detectableByRays` or `sensors.detectableAsNearby` is explicitly enabled, observations may report that replay ghost as a sensor target with `targetType` / `entityType: 'replayGhost'`; it still remains a non-physics reference entity.
266
+ Replay ghosts appear in `snapshot.replayGhosts`, interpolate by `timeSeconds`, render as translucent browser overlays, and never enter `snapshot.cars`, timing rows, race order, pit logic, collision resolution, or steward penalties. They are hidden from rays and nearby-car observations by default. If `sensors.detectableByRays` or `sensors.detectableAsNearby` is explicitly enabled, observations may report that replay ghost as a sensor target with `targetType` / `entityType: 'replayGhost'`; it still remains a non-physics reference entity. Sensor target filtering and enrichment treat `{ entityType, id }` as the target identity, so a replay ghost id collision cannot replace a real car's nearby-radar velocity facts and an opt-in same-id reference ghost remains visible to that car's sensors.
258
267
 
259
268
  Actions use normalized low-level controls:
260
269
 
@@ -352,7 +361,9 @@ observation: {
352
361
  }
353
362
  ```
354
363
 
355
- The default remains `output: 'full'`, `includeSchema: true`, and `vectorType: 'array'`, which returns `{ object, vector, schema, events }` for backward compatibility. `output: 'vector'` returns `{ vector, events }` unless schema inclusion is requested. `output: 'object'` returns `{ object, events }` unless schema inclusion is requested. `getObservationSpec()` remains the canonical schema source for compact loops. `vectorType: 'float32'` returns a `Float32Array` for JavaScript consumers that want typed numeric buffers; JSON worker users should keep the default array output unless their bridge explicitly handles typed arrays. Destroyed or out-of-race cars keep the same ray/vector schema and return miss-valued ray channels so model code does not need a separate terminal tensor shape.
364
+ The default remains `output: 'full'`, `includeSchema: true`, and `vectorType: 'array'`, which returns `{ object, vector, schema, events }` for backward compatibility. `output: 'vector'` returns `{ vector, events }` unless schema inclusion is requested. `output: 'object'` returns `{ object, events }` unless schema inclusion is requested. `getObservationSpec()` remains the canonical schema source for compact loops. When `sensorsByDriver` changes ray or nearby-car shape for individual controlled drivers, `observationSpec.perDriver[driverId]` exposes that driver's exact ray, nearby-car, and vector schema shape. Full observations include the actual object ray geometry and Policy Runner object-backed encoding uses that geometry instead of reconstructing angles or lengths from a default layout. `vectorType: 'float32'` returns a `Float32Array` for JavaScript consumers that want typed numeric buffers; JSON worker users should keep the default array output unless their bridge explicitly handles typed arrays. Destroyed or out-of-race cars keep the same ray/vector schema and return miss-valued ray channels so model code does not need a separate terminal tensor shape.
365
+
366
+ The executable model-sense contract lives in `src/__tests__/environmentSenseContract.test.js` and is summarized in [Model Sense Contract](sense_contract.md). It treats the object observation as a policy-facing contract, checks it against simulator snapshot facts through independent unit and geometry formulas, then decodes each vector schema entry back to the object contract. A new vector field is not considered contract-covered until it has an oracle there.
356
367
 
357
368
  Result state output can also be compacted:
358
369
 
@@ -363,7 +374,7 @@ result: {
363
374
  }
364
375
  ```
365
376
 
366
- The default `stateOutput: 'full'` preserves the public `{ state: { snapshot } }` payload. `minimal` returns the lean observation snapshot used by the environment. `none` returns `state: null`, so external loops should rely on `observation`, `metrics`, and `info` instead. `resetDriversObservationScope: 'reset'` makes `resetDrivers()` return observations and metrics only for reset drivers by default; callers can override per call with `env.resetDrivers(placements, { observationScope: 'all' | 'reset', stateOutput })`. Partial `env.reset(options)` calls merge plain nested option groups such as `rules.modules`, `trackGeneration`, `sensors`, `observation`, `result`, and `episode`; arrays and explicit placement maps replace the old value.
377
+ The default `stateOutput: 'full'` preserves the public `{ state: { snapshot } }` payload. `minimal` returns the lean observation snapshot used by the environment. `none` returns `state: null`, so external loops should rely on `observation`, `metrics`, and `info` instead. `resetDriversObservationScope: 'reset'` makes `resetDrivers()` return observations, metrics, and `info.controlledDrivers` only for reset drivers by default; callers can override per call with `env.resetDrivers(placements, { observationScope: 'all' | 'reset', stateOutput })`. `info.drivers` still carries episode state for the configured controlled drivers so active, non-reset drivers keep their episode ids and terminal flags. `createPaddockDriverControllerLoop()` maintains an observation cache across partial resets and merges reset-driver observations into it before the next controller decision. Partial `env.reset(options)` calls merge plain nested option groups such as `rules.modules`, `trackGeneration`, `sensors`, `observation`, `result`, and `episode`; arrays and explicit placement maps replace the old value.
367
378
 
368
379
  The JSON worker protocol exposes the same reset-result controls through `resultOptions`:
369
380
 
@@ -419,7 +430,7 @@ reward({ driverId, previous, current, action, events, state, metrics, episode })
419
430
  }
420
431
  ```
421
432
 
422
- `metrics` is the same neutral per-driver metrics object returned in `result.metrics[driverId]`, and `episode` is the same per-driver runtime state returned in `result.info.drivers[driverId]`. These are package-owned facts, not reward policy. If no reward callback is provided, `result.reward` is `null`. If a callback returns `undefined`, `null`, `NaN`, or an infinite value, that driver's reward is normalized to `0`. PaddockJS does not infer, select, or tune rewards.
433
+ `metrics` is the same neutral per-driver metrics object returned in `result.metrics[driverId]`, and `episode` is the same per-driver runtime state returned in `result.info.drivers[driverId]`. These are package-owned facts, not reward policy. Reward callbacks run only for `step(actions)` transition results; reset/read paths such as `reset()`, `resetDrivers()`, and cold `getObservation()` are reward-neutral and return no transition reward. If no reward callback is provided, `result.reward` is `null`. If a callback returns `undefined`, `null`, `NaN`, or an infinite value, that driver's reward is normalized to `0`. The starter `createProgressReward()` reads speed/legal-surface facts from the reward snapshot and metrics when object observations are omitted, so the same transition keeps the same demo reward in compact vector-only mode. PaddockJS does not infer, select, or tune rewards.
423
434
 
424
435
  Neutral rollout recording is available for external training loops:
425
436
 
@@ -433,7 +444,7 @@ const next = env.step(action);
433
444
  recorder.recordStep(previous, action, next);
434
445
  ```
435
446
 
436
- Each recorded transition has `{ observation, action, reward, nextObservation, terminated, truncated, info }`. This is data export only; it does not train or update a model.
447
+ Each recorded transition has `{ observation, action, reward, nextObservation, terminated, truncated, info }`. Recorded transitions are cloned at record time, including numeric arrays and typed vectors, so later mutation of a policy action object or observation buffer cannot change already-recorded samples. This is data export only; it does not train or update a model.
437
448
 
438
449
  Deterministic evaluation helpers run fixed seeds/scenarios and report environment quality metrics:
439
450
 
@@ -562,7 +573,7 @@ snapshot.replayGhosts = trajectory-driven replay/reference entities
562
573
 
563
574
  Observation objects use physical units such as kph, meters/second, meters, and radians. Optional `vector` values use fixed documented scaling from `schema`; they do not use hidden per-car normalization. Full simulator truth remains available under `state.snapshot`. Internally, the environment avoids rebuilding full snapshots during each `frameSkip` substep and defaults to the non-public track query index unless `trackQueryIndex: false` is set, but the returned `state.snapshot`, reward callback `previous` snapshot, and reward callback `state.snapshot` keep the same public shape.
564
575
 
565
- The environment observation now exposes local physical driver senses separately from full snapshot truth. `self.yawRateRadiansPerSecond` is the car's current yaw rate. `self.appliedControls` reports the normalized controls that actually drove the latest physics step: `steering` in `-1..1`, `steeringRadians` in simulator radians, `throttle` in `0..1`, and `brake` in `0..1`. For controlled cars this mirrors the accepted environment action; in `actionPolicy: 'report'` mode, a missing or invalid vehicle action releases stale manual controls, lets the built-in AI drive that step, and exposes its exact controls for auditing or clean local imitation datasets. `trackRelation` gives immediate local road relationship: lateral offset, heading error, legal width, left/right boundary distance, legal-surface state, and current surface. `contactPatches` exposes the four wheel/contact-patch surface readings as stable public observation data, with `surfaceCode` intended only as a compact vector encoding.
576
+ The environment observation now exposes local physical driver senses separately from full snapshot truth. `self.yawRateRadiansPerSecond` is the car's current yaw rate. `self.appliedControls` reports the normalized controls that actually drove the latest physics step: `steering` in `-1..1`, `steeringRadians` in simulator radians, `throttle` in `0..1`, and `brake` in `0..1`. For controlled cars this mirrors the accepted environment action; in `actionPolicy: 'report'` mode, a missing or invalid vehicle action releases stale manual controls, lets the built-in AI drive that step, and exposes its exact controls for auditing or clean local imitation datasets. `trackRelation` gives immediate local road relationship: lateral offset, heading error, legal width, left/right boundary distance, legal-surface state, and current surface. `contactPatches` exposes the four wheel/contact-patch surface readings as stable public observation data. Compact physical-driver vectors include each patch's `surfaceCode`, `signedOffsetMeters`, and `crossTrackErrorMeters` so vector-only training loops do not lose a contact-patch sense that exists in object observations.
566
577
 
567
578
  Default rays use a compact center-origin set with forward, side, and rear awareness:
568
579
 
@@ -616,9 +627,9 @@ sensors: {
616
627
 
617
628
  Ray precision defaults to `driver`. Driver precision is the active model-facing sensor contract and uses the normal sampled ray step without extra refinement. `precision: 'debug'` is available for clearly labeled diagnostics with additional edge refinement, but debug precision must not be displayed as model senses unless the policy is also running with that exact sensor config.
618
629
 
619
- Surface channels are computed only when requested. `kerb` is legal racing surface. `illegalSurface` reports the first grass or gravel surface crossed by the ray. The same model-facing ray contract is accelerated for both normal on-track driving and nearby off-track recovery starts using indexed ray-boundary intersections. Ambiguous pit-connector and unusual geometry cases may use indexed sampled fallback internally, but the returned ray object shape is unchanged. Barrier walls are not exposed as a model-facing ray target; active ray objects, vectors, schemas, and visualizations must not expose a `barrier` ray channel. Barrier contact is enforced as a shared destruction boundary in both physics modes at the rendered wall's inner face and reported through events, snapshots, metrics, and per-driver episode state.
630
+ Surface channels are computed only when requested. `kerb` is legal racing surface. `illegalSurface` reports the first grass or gravel surface crossed by the ray. Requested surface-ray channels are part of the active object observation and compact vector schema for both default and physical-driver profiles. For normal driver interactions, `precision: 'driver'` road-edge, kerb, and illegal-surface channels keep the sampled driver contract whether the track query index is enabled or disabled. The `batch-training` participant profile keeps its indexed recovery-ray path as its own high-throughput training contract. Other indexed ray-boundary shortcuts are reserved for non-driver/debug precision paths where sub-sample diagnostics are explicitly requested. Requested surface channels preserve an origin hit at `distanceMeters: 0` even if another requested channel has no visible boundary. Ambiguous pit-connector and unusual geometry cases may use sampled fallback internally, but the returned ray object shape is unchanged. Barrier walls are not exposed as a model-facing ray target; active ray objects, vectors, schemas, and visualizations must not expose a `barrier` ray channel. Barrier contact is enforced as a shared destruction boundary in both physics modes at the rendered wall's inner face and reported through events, snapshots, metrics, and per-driver episode state.
620
631
 
621
- Nearby-car observations are car-relative:
632
+ Nearby-car observations are car-relative. When simulator velocity fields are present, `closingRateMetersPerSecond` and `timeToContactSeconds` use the actual 2D velocity vector instead of assuming the car is moving in its heading direction, so slip and recovery states remain physically accurate:
622
633
 
623
634
  ```js
624
635
  {
@@ -639,6 +650,7 @@ Nearby-car observations are car-relative:
639
650
  ```
640
651
 
641
652
  If no `reward` callback is provided, `result.reward` is `null`. If provided, rewards are returned by controlled driver ID. The callback receives `driverId`, `previous`, `current`, `action`, `events`, `state.snapshot`, `metrics`, and `episode`.
653
+ Reward callbacks run only for `step(actions)` transition results. Reset and read paths return `reward: null`, so episode setup, `resetDrivers()`, and `getObservation()` cannot create fake replay rewards or trigger reward side effects.
642
654
 
643
655
  Browser expert mode is opt-in through the browser mount API:
644
656
 
@@ -838,7 +850,7 @@ onDriverOpen(driver) {
838
850
  }
839
851
  ```
840
852
 
841
- The callback receives the normalized driver object. If the host wants modals, routing, analytics, or external tabs, it should implement that inside this callback.
853
+ The callback receives the normalized driver object. If the host wants modals, routing, analytics, or external tabs, it should implement that inside this callback. `onDriverOpen` is host-owned navigation code and should handle its own failures; unlike lifecycle callbacks, it is not routed through `onError`.
842
854
 
843
855
  Optional lifecycle callbacks:
844
856
 
@@ -854,7 +866,7 @@ Optional lifecycle callbacks:
854
866
  }
855
867
  ```
856
868
 
857
- `onRaceEvent` receives simulation events such as `contact`, `penalty`, `track-limits`, `pit-lane-speeding`, `car-dnf`, `safety-car`, `green-flag`, `start-lights-out`, and `race-finish`. `car-dnf` currently reports stalled off-track retirements with `reason: 'stalled-off-track'`. `contact` events include metadata from the production body collision solver: `firstShapeId`, `secondShapeId`, `contactType`, `depth`, and `timeOfImpact`. Host callback errors are caught; if `onError` exists, it receives `{ callback: name }` context for callback failures.
869
+ `onRaceEvent` receives simulation events such as `contact`, `penalty`, `track-limits`, `pit-lane-speeding`, `car-dnf`, `safety-car`, `green-flag`, `start-lights-out`, and `race-finish`. `car-dnf` currently reports stalled off-track retirements with `reason: 'stalled-off-track'`. `contact` events include metadata from the production body collision solver: `firstShapeId`, `secondShapeId`, `contactType`, `depth`, and `timeOfImpact`. Lifecycle callback errors are caught; if `onError` exists, it receives `{ callback: name }` context for callback failures.
858
870
 
859
871
  Race snapshots include a top-level `penalties` array. Each penalty entry includes `id`, `type`, `driverId`, `strictness`, `status`, `penaltySeconds`, `pendingPenaltySeconds`, `serviceType`, `serviceRequired`, `serviceServedAt`, `appliedAt`, `cancelledAt`, `unserved`, `positionDrop`, `gridDrop`, `disqualified`, `consequences`, `lap`, `at`, and rule-specific context such as `otherCarId`, `aheadDriverId`, `atFaultDriverId`, `sharedFault`, and `impactSpeedKph` for collision penalties or `speedKph`, `speedLimitKph`, `excessKph`, and `pitLanePart` for pit-lane speeding penalties. Clear rear contact has one at-fault driver; unclear meaningful contact records one shared-fault penalty per involved driver. Multiple time penalties for the same driver are summed into the car snapshot's `penaltySeconds` and adjusted finish/classification time.
860
872
 
@@ -1024,7 +1036,7 @@ Every built track is automatically divided into three equal sectors. `snapshot.t
1024
1036
 
1025
1037
  Every built track also exposes hidden `snapshot.track.timingLines`. Timing lines are spaced from the track length at an F1-style mini-sector target of roughly `150m..200m`; they are simulation metadata for gap calculation and are not rendered by default.
1026
1038
 
1027
- Every built track also exposes `snapshot.track.pitLane`. The pit lane is deterministic for the track seed and contains:
1039
+ Tracks whose resolved generation options enable pit lanes expose `snapshot.track.pitLane`. The `race` profile includes this geometry; training profiles are pitless by default unless explicitly overridden. When present, the pit lane is deterministic for the track seed and contains:
1028
1040
 
1029
1041
  - `entry`: track distance before the start line, the true track `edgePoint`, an overlapping lane-facing `trackConnectPoint` on the track surface, connector points from the racing surface to the pit lane, and a procedural `roadCenterline` that is tangent to the main track at entry and tangent to the straight pit lane at the pit-lane start.
1030
1042
  - `layout`: the model-owned pit sizing data, including the box-run length, total main-lane length, entry/exit distances relative to start/finish, and entry/exit buffers. The main lane is sized from the configured team/box count instead of using a fixed oversized straight.
@@ -1088,7 +1100,7 @@ Controller methods:
1088
1100
  - `callSafetyCar()`: deploys the safety car.
1089
1101
  - `clearSafetyCar()`: releases the safety car.
1090
1102
  - `toggleSafetyCar()`: switches safety car deployment based on the current snapshot.
1091
- - `setPitIntent(driverId, intent, targetCompound?)`: requests, clears, or updates a pending automatic pit stop and optional target tire.
1103
+ - `setPitIntent(driverId, intent, targetCompound?)`: requests, clears, or updates a pending automatic pit stop and optional target tire. This requires `rules.modules.pitStops.enabled: true` and track pit-lane geometry; `tireStrategy` alone only affects compound choices.
1092
1104
  - `getPitIntent(driverId)`: reads the current pit intent.
1093
1105
  - `getPitTargetCompound(driverId)`: reads the current pit target tire.
1094
1106
  - `getSimulationSpeed()`: returns the active browser playback multiplier from the package-owned simulation-speed control, defaulting to `1`.
@@ -1111,6 +1123,7 @@ Composable controllers additionally expose:
1111
1123
  - `mountRaceTelemetryDrawer(root, { timingTowerVerticalFit, drawerInitiallyOpen, raceDataTelemetryDetail })`: renders a template that combines an external top control row, race canvas, embedded timing tower, the project/radio lower-third, top steward message, safety-car control, and a right-side telemetry drawer. The control row contains camera controls, the `1x..10x` simulation-speed toggle, banner mute, the safety-car button, and the telemetry toggle so those controls do not cover the race view. Pass `raceDataTelemetryDetail: true` when this template should put compact S1/S2/S3 detail in the project lower-third instead of mounting a second sector popup. The drawer embeds the same package-owned telemetry stack used by `mountTelemetryPanel()` and takes layout space from the race window when opened.
1112
1124
  - `mountCarDriverOverview(root)`: renders the package-owned car/driver overview as a separate component with a Car/Driver toggle, center visual, and linked stat cells from the existing driver/vehicle rating components.
1113
1125
  - `mountRaceDataPanel(root)`: renders the project/race-data lower-third as a separate component for hosts that intentionally want it outside the race canvas.
1126
+ - `querySelector(selector)` / `querySelectorAll(selector)`: search across the mounted package-owned composable roots. These exist for integration tests and advanced host glue; ordinary hosts should prefer explicit controller methods and mounted component roots.
1114
1127
  - `start()`: initializes PixiJS, binds mounted controls, and starts the simulation loop.
1115
1128
 
1116
1129
  Mount component roots before calling `start()`. If a component is not mounted, the runtime skips that UI surface instead of requiring hidden placeholder DOM. Mounted surfaces render a package-owned loading overlay immediately; `start()` removes those overlays after PixiJS, assets, controls, and initial readouts have initialized.
@@ -141,7 +141,7 @@ constructorArgs: {
141
141
 
142
142
  It also copies the converted driver fields and vehicle fields onto the object used by `createRaceSimulation()`.
143
143
 
144
- Team metadata is copied onto the same normalized driver object. `team.color` defaults to the driver/car color when omitted, and `team.icon` defaults from the team name or timing code. Teams are metadata-only for now, but they are intentionally modeled separately from driver and vehicle data so future pit boxes, pit colors, team strategy, and team-owned fields do not have to be forced into car or driver objects.
144
+ Team metadata is copied onto the same normalized driver object. `team.color` defaults to the driver/car color when omitted, and `team.icon` defaults from the team name or timing code. Teams are intentionally modeled separately from driver and vehicle data because current pit-lane service areas, queue colors, garage box identity, optional pit-crew stats, and future team-owned fields should not be forced into car or driver objects.
145
145
 
146
146
  ## Unit Conversion
147
147
 
package/docs/index.md CHANGED
@@ -9,9 +9,18 @@ Use these docs as the package source of truth:
9
9
  - [Concepts](concepts.md): vocabulary used by the simulator.
10
10
  - [Data Contract](data_contract.md): shape of host-provided drivers, car pairings, ratings, assets, and callbacks.
11
11
  - [Bring Your Own Model](training.md): environment contract, policy convention, and visual playback loop.
12
+ - [Model Sense Contract](sense_contract.md): executable proof boundary for policy-facing observation senses.
12
13
  - [Custom Model Controller Guide](custom_model_controller.md): how to wrap a trained model as a batched driver controller for browser playback or headless loops.
13
14
  - [Architecture](architecture.md): module ownership and data/control flow.
14
15
  - [Data Model Notes](data_model.md): lower-level simulator data model notes kept with package docs.
15
16
  - [Learnings](learnings.md): implementation lessons and layout verification rules learned from simulator integration issues.
16
17
 
18
+ Common paths:
19
+
20
+ - Installing PaddockJS into a browser host: read [System Specs](system_specs.md), then [Data Contract](data_contract.md), then [INSTALL_AND_UPDATE.md](../INSTALL_AND_UPDATE.md).
21
+ - Building a package-owned composable layout: read the public API and returned-controller sections in [System Specs](system_specs.md), then the sizing and UI options in [Data Contract](data_contract.md).
22
+ - Enabling pits, penalties, tire requirements, safety car, or stalled off-track DNF: read [Rules](rules.md) first. Pit stops require `rules.modules.pitStops.enabled`; `tireStrategy` alone only controls compounds, tire-requirement stewarding, and pit target choices.
23
+ - Connecting a trained model: read [Bring Your Own Model](training.md), then [Custom Model Controller Guide](custom_model_controller.md). PaddockJS owns observations, actions, specs, scenarios, and browser playback; the host owns the model, reward, storage, and training framework.
24
+ - Changing internals: read [Architecture](architecture.md) before editing source so new code lands in the feature-owned module instead of a facade or compatibility barrel.
25
+
17
26
  When simulator behavior changes, update the relevant doc in this folder in the same change.