@inventure71/paddockjs 1.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.
- package/CHANGELOG.md +58 -0
- package/INSTALL_AND_UPDATE.md +4 -3
- package/README.md +219 -12
- package/docs/architecture.md +126 -45
- package/docs/concepts.md +9 -5
- package/docs/custom_model_controller.md +329 -0
- package/docs/data_contract.md +317 -22
- package/docs/data_model.md +1 -1
- package/docs/index.md +10 -0
- package/docs/learnings.md +22 -0
- package/docs/rules.md +71 -14
- package/docs/sense_contract.md +59 -0
- package/docs/system_specs.md +75 -13
- package/docs/training.md +280 -32
- package/examples/python/README.md +62 -0
- package/examples/python/base_policy_server.py +191 -0
- package/examples/python/requirements.txt +2 -0
- package/package.json +12 -4
- package/src/api/PaddockSimulatorController.js +6 -33
- package/src/app/BrowserExpertAdapter.js +76 -9
- package/src/app/BrowserExpertExternalRenderer.js +303 -0
- package/src/app/F1SimulatorApp.js +291 -1630
- package/src/app/banners/bannerFormatters.js +55 -0
- package/src/app/banners/raceDataBannerController.js +141 -0
- package/src/app/banners/stewardMessageController.js +99 -0
- package/src/app/camera/cameraConstants.js +21 -0
- package/src/app/camera/cameraController.js +265 -0
- package/src/app/camera/cameraGeometry.js +176 -0
- package/src/app/domBindings.js +4 -0
- package/src/app/readouts/carOverviewRenderer.js +109 -0
- package/src/app/readouts/controlStateRenderer.js +37 -0
- package/src/app/readouts/raceStatusRenderer.js +133 -0
- package/src/app/readouts/readoutFormatters.js +58 -0
- package/src/app/readouts/telemetryRenderer.js +113 -0
- package/src/app/readouts/timingTowerRenderer.js +177 -0
- package/src/app/rendering/appAssets.js +38 -0
- package/src/app/rendering/carRenderer.js +286 -0
- package/src/app/rendering/displayUtils.js +22 -0
- package/src/app/rendering/drsTrailRenderer.js +90 -0
- package/src/app/rendering/expertSensorRenderer.js +126 -0
- package/src/app/rendering/pitLaneStatusRenderer.js +50 -0
- package/src/app/rendering/replayGhostRenderer.js +124 -0
- package/src/app/rendering/trackRenderer.js +25 -0
- package/src/app/runtime/frameLoop.js +66 -0
- package/src/app/runtime/runtimeTiming.js +34 -0
- package/src/app/runtime/runtimeVisibility.js +45 -0
- package/src/config/defaultOptions.js +20 -0
- package/src/config/restartOptions.js +34 -0
- package/src/environment/actions.js +13 -5
- package/src/environment/controllerLoop.js +261 -0
- package/src/environment/episode.js +98 -2
- package/src/environment/evaluation.js +54 -32
- package/src/environment/index.d.ts +376 -10
- package/src/environment/index.js +1 -0
- package/src/environment/metrics.js +159 -0
- package/src/environment/observationOptions.js +22 -1
- package/src/environment/observationVector.js +200 -0
- package/src/environment/observations.js +199 -141
- package/src/environment/options.js +37 -5
- package/src/environment/recorder.js +13 -6
- package/src/environment/rewards.js +13 -2
- package/src/environment/runtime.js +272 -23
- package/src/environment/scenarios.js +57 -9
- package/src/environment/sensors/bodySenses.js +23 -0
- package/src/environment/sensors/boundarySenses.js +17 -0
- package/src/environment/sensors/carRays.js +75 -0
- package/src/environment/sensors/contactSenses.js +32 -0
- package/src/environment/sensors/index.js +157 -0
- package/src/environment/sensors/indexedRayBands.js +103 -0
- package/src/environment/sensors/nearbyCars.js +52 -0
- package/src/environment/sensors/opponentRadar.js +75 -0
- package/src/environment/sensors/rayConfig.js +110 -0
- package/src/environment/sensors/rayDefaults.js +6 -0
- package/src/environment/sensors/rayGeometry.js +60 -0
- package/src/environment/sensors/rayGuards.js +19 -0
- package/src/environment/sensors/sensorTargets.js +66 -0
- package/src/environment/sensors/surfaceRays.js +360 -0
- package/src/environment/sensors/trackRays.js +424 -0
- package/src/environment/sensors.js +1 -367
- package/src/environment/specs.js +159 -26
- package/src/environment/workerProtocol.js +13 -1
- package/src/index.d.ts +171 -6
- package/src/index.js +6 -32
- package/src/rendering/proceduralTrackAsset.js +20 -823
- package/src/rendering/renderSnapshot.js +38 -12
- package/src/rendering/track/finishLineRenderer.js +34 -0
- package/src/rendering/track/gridRenderer.js +47 -0
- package/src/rendering/track/offsetStrokeSafety.js +136 -0
- package/src/rendering/track/pitLaneRenderer.js +248 -0
- package/src/rendering/track/trackMaterialRenderer.js +326 -0
- package/src/rendering/track/trackRenderConstants.js +38 -0
- package/src/rendering/track/trackRenderGeometry.js +94 -0
- package/src/simulation/collisionGeometry.js +1 -1
- package/src/simulation/driver/driverControlConstants.js +28 -0
- package/src/simulation/driver/driverController.js +47 -0
- package/src/simulation/driver/driverInput.js +28 -0
- package/src/simulation/driver/driverMath.js +6 -0
- package/src/simulation/driver/driverPersonality.js +25 -0
- package/src/simulation/driver/edgeRecovery.js +48 -0
- package/src/simulation/driver/racingControls.js +399 -0
- package/src/simulation/driver/racingLinePlan.js +166 -0
- package/src/simulation/driver/recoveryDynamics.js +38 -0
- package/src/simulation/driver/rejoinControls.js +213 -0
- package/src/simulation/driver/safetyCarControls.js +35 -0
- package/src/simulation/driver/trafficScan.js +51 -0
- package/src/simulation/driverController.js +4 -619
- package/src/simulation/participants/participantInteractions.js +120 -0
- package/src/simulation/pit/pitFlow.js +239 -0
- package/src/simulation/pit/pitIntent.js +92 -0
- package/src/simulation/pit/pitKinematics.js +24 -0
- package/src/simulation/pit/pitOccupancy.js +47 -0
- package/src/simulation/pit/pitPenaltyService.js +71 -0
- package/src/simulation/pit/pitQueue.js +60 -0
- package/src/simulation/pit/pitRouteMovement.js +167 -0
- package/src/simulation/pit/pitRouting.js +319 -0
- package/src/simulation/pit/pitService.js +43 -0
- package/src/simulation/pit/pitServiceConstants.js +21 -0
- package/src/simulation/pit/pitServiceProfile.js +45 -0
- package/src/simulation/pit/pitSnapshots.js +61 -0
- package/src/simulation/pit/pitState.js +113 -0
- package/src/simulation/pit/pitTireService.js +94 -0
- package/src/simulation/race/classification.js +104 -0
- package/src/simulation/race/gridPenalties.js +37 -0
- package/src/simulation/race/raceControlState.js +38 -0
- package/src/simulation/race/raceDistance.js +32 -0
- package/src/simulation/race/raceFinish.js +65 -0
- package/src/simulation/race/raceLifecycle.js +67 -0
- package/src/simulation/race/raceOrder.js +113 -0
- package/src/simulation/race/raceProgress.js +129 -0
- package/src/simulation/race/raceSetup.js +93 -0
- package/src/simulation/race/raceStep.js +80 -0
- package/src/simulation/race/redFlag.js +15 -0
- package/src/simulation/race/retirements.js +44 -0
- package/src/simulation/race/safetyCar.js +92 -0
- package/src/simulation/race/stalledDnf.js +77 -0
- package/src/simulation/raceSimulation.js +231 -2817
- package/src/simulation/replay/replayGhosts.js +128 -0
- package/src/simulation/rules/moduleConfig.js +72 -0
- package/src/simulation/rules/penaltyConfig.js +99 -0
- package/src/simulation/rules/penaltyEvents.js +18 -0
- package/src/simulation/rules/penaltyStats.js +26 -0
- package/src/simulation/rules/ruleConfigMerge.js +41 -0
- package/src/simulation/rules/ruleDefaults.js +85 -0
- package/src/simulation/rules/rulesReview.js +144 -0
- package/src/simulation/rules/rulesetPresets.js +47 -0
- package/src/simulation/rules/trackLimitsSteward.js +1 -1
- package/src/simulation/rulesConfig.js +5 -323
- package/src/simulation/snapshots/raceSnapshots.js +193 -0
- package/src/simulation/timing/drsTiming.js +87 -0
- package/src/simulation/timing/gapEstimation.js +38 -0
- package/src/simulation/timing/lapTelemetry.js +182 -0
- package/src/simulation/timing/raceTiming.js +15 -0
- package/src/simulation/timing/sectorPerformance.js +60 -0
- package/src/simulation/timing/timingConstants.js +5 -0
- package/src/simulation/timing/timingHistory.js +46 -0
- package/src/simulation/timing/timingLines.js +79 -0
- package/src/simulation/track/centerlineSpline.js +59 -0
- package/src/simulation/track/drsZones.js +67 -0
- package/src/simulation/track/pitLaneAccess.js +103 -0
- package/src/simulation/track/pitLaneLayout.js +395 -0
- package/src/simulation/track/pitLaneState.js +160 -0
- package/src/simulation/track/proceduralCenterline.js +237 -0
- package/src/simulation/track/sampleGeometry.js +49 -0
- package/src/simulation/track/spatialQueries.js +154 -0
- package/src/simulation/track/startStraight.js +141 -0
- package/src/simulation/track/trackConstants.js +109 -0
- package/src/simulation/track/trackGenerationOptions.js +283 -0
- package/src/simulation/track/trackMath.js +241 -0
- package/src/simulation/track/trackModel.js +240 -0
- package/src/simulation/track/trackQueryGrid.js +94 -0
- package/src/simulation/track/trackQueryIndex.js +659 -0
- package/src/simulation/track/trackQueryPitIndex.js +140 -0
- package/src/simulation/track/trackQueryScratch.js +46 -0
- package/src/simulation/track/trackQueryStats.js +28 -0
- package/src/simulation/track/trackSectors.js +19 -0
- package/src/simulation/track/trackStatePolicy.js +24 -0
- package/src/simulation/track/trackValidation.js +98 -0
- package/src/simulation/trackModel.js +1 -1706
- package/src/simulation/vehicle/contactResolution.js +181 -0
- package/src/simulation/vehicle/mainTrackWheelSurface.js +82 -0
- package/src/simulation/vehicle/pitWheelSurface.js +69 -0
- package/src/simulation/vehicle/runoffResponse.js +87 -0
- package/src/simulation/vehicle/surfacePriority.js +28 -0
- package/src/simulation/vehicle/vehicleGeometry.js +247 -0
- package/src/simulation/vehicle/vehicleKinematics.js +37 -0
- package/src/simulation/vehicle/vehiclePhysics.js +364 -0
- package/src/simulation/vehicle/vehicleSnapshots.js +327 -0
- package/src/simulation/vehicle/vehicleState.js +220 -0
- package/src/simulation/vehicle/wheelSurface.js +152 -0
- package/src/simulation/vehicleGeometry.js +1 -247
- package/src/simulation/vehiclePhysics.js +1 -136
- package/src/simulation/warmup/runtimeWarmup.js +224 -0
- package/src/simulation/wheelSurface.js +1 -338
- package/src/styles.css +42 -0
- package/src/ui/cameraControlsTemplate.js +20 -0
- package/src/ui/carOverviewTemplate.js +33 -0
- package/src/ui/componentTemplates.js +14 -360
- package/src/ui/raceCanvasTemplate.js +69 -0
- package/src/ui/raceControlsTemplate.js +36 -0
- package/src/ui/shellTemplate.js +5 -4
- package/src/ui/telemetryDrawerTemplate.js +55 -0
- package/src/ui/telemetryTemplates.js +136 -0
- package/src/ui/timingTowerTemplate.js +34 -0
package/docs/data_contract.md
CHANGED
|
@@ -14,8 +14,14 @@ mountF1Simulator(root, {
|
|
|
14
14
|
onDriverOpen,
|
|
15
15
|
seed,
|
|
16
16
|
trackSeed,
|
|
17
|
+
trackGeneration,
|
|
18
|
+
trackQueryIndex,
|
|
19
|
+
warmup,
|
|
17
20
|
totalLaps,
|
|
21
|
+
physicsMode,
|
|
18
22
|
rules,
|
|
23
|
+
participantInteractions,
|
|
24
|
+
replayGhosts,
|
|
19
25
|
initialCameraMode,
|
|
20
26
|
theme,
|
|
21
27
|
title,
|
|
@@ -24,6 +30,7 @@ mountF1Simulator(root, {
|
|
|
24
30
|
backLinkLabel,
|
|
25
31
|
showBackLink,
|
|
26
32
|
ui,
|
|
33
|
+
debug,
|
|
27
34
|
assets,
|
|
28
35
|
expert,
|
|
29
36
|
onLoadingChange,
|
|
@@ -45,12 +52,19 @@ const simulator = createPaddockSimulator({
|
|
|
45
52
|
onDriverOpen,
|
|
46
53
|
seed,
|
|
47
54
|
trackSeed,
|
|
55
|
+
trackGeneration,
|
|
56
|
+
trackQueryIndex,
|
|
57
|
+
warmup,
|
|
48
58
|
totalLaps,
|
|
59
|
+
physicsMode,
|
|
49
60
|
rules,
|
|
61
|
+
participantInteractions,
|
|
62
|
+
replayGhosts,
|
|
50
63
|
initialCameraMode,
|
|
51
64
|
preset,
|
|
52
65
|
theme,
|
|
53
66
|
ui,
|
|
67
|
+
debug,
|
|
54
68
|
assets,
|
|
55
69
|
expert,
|
|
56
70
|
});
|
|
@@ -104,6 +118,7 @@ const env = createPaddockEnvironment({
|
|
|
104
118
|
standingStart: false,
|
|
105
119
|
modules: {
|
|
106
120
|
penalties: {
|
|
121
|
+
enabled: true,
|
|
107
122
|
trackLimits: { strictness: 0.25 },
|
|
108
123
|
},
|
|
109
124
|
},
|
|
@@ -121,6 +136,9 @@ result = env.step({
|
|
|
121
136
|
```
|
|
122
137
|
|
|
123
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.
|
|
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.
|
|
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.
|
|
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`.
|
|
124
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:
|
|
125
143
|
|
|
126
144
|
```js
|
|
@@ -135,6 +153,18 @@ rules: {
|
|
|
135
153
|
tirePitRequestThresholdPercent: 50,
|
|
136
154
|
tirePitCommitThresholdPercent: 30,
|
|
137
155
|
},
|
|
156
|
+
tireStrategy: {
|
|
157
|
+
enabled: true,
|
|
158
|
+
mandatoryDistinctDryCompounds: 2,
|
|
159
|
+
},
|
|
160
|
+
tireDegradation: {
|
|
161
|
+
enabled: false,
|
|
162
|
+
},
|
|
163
|
+
stalledDnf: {
|
|
164
|
+
enabled: true,
|
|
165
|
+
maxStoppedSeconds: 12,
|
|
166
|
+
speedThresholdKph: 5,
|
|
167
|
+
},
|
|
138
168
|
penalties: {
|
|
139
169
|
trackLimits: { strictness: 0.8 },
|
|
140
170
|
collision: { strictness: 0.5, consequences: [{ type: 'time', seconds: 5 }] },
|
|
@@ -145,7 +175,7 @@ rules: {
|
|
|
145
175
|
}
|
|
146
176
|
```
|
|
147
177
|
|
|
148
|
-
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. 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.
|
|
149
179
|
|
|
150
180
|
Scenario support:
|
|
151
181
|
|
|
@@ -175,7 +205,65 @@ scenario: {
|
|
|
175
205
|
}
|
|
176
206
|
```
|
|
177
207
|
|
|
178
|
-
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.
|
|
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.
|
|
209
|
+
|
|
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)`.
|
|
211
|
+
|
|
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:
|
|
213
|
+
|
|
214
|
+
```js
|
|
215
|
+
participantInteractions: {
|
|
216
|
+
defaultProfile: 'normal',
|
|
217
|
+
drivers: {
|
|
218
|
+
'model-a': { profile: 'normal' },
|
|
219
|
+
'model-b': { profile: 'isolated-training' },
|
|
220
|
+
},
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Supported profiles are:
|
|
225
|
+
|
|
226
|
+
- `normal`: collidable, ray-detectable, nearby-detectable, pit-lane-blocking, and race-order-affecting.
|
|
227
|
+
- `isolated-training`: non-colliding, hidden from rays and nearby-car observations, non-blocking in pit-lane occupancy, but still included in race order.
|
|
228
|
+
- `batch-training`: non-colliding, hidden from rays and nearby-car observations, non-blocking in pit-lane occupancy, and excluded from race order/classification while still being a rendered physics-driven car in `snapshot.cars`.
|
|
229
|
+
- `phantom-race`: non-colliding and non-blocking in pit-lane occupancy, but visible to rays and nearby-car observations and included in race order.
|
|
230
|
+
- `time-trial-overlay`: non-colliding, sensor-hidden, non-blocking, and excluded from race order/classification while still being a physics-driven car in `snapshot.cars`.
|
|
231
|
+
|
|
232
|
+
Every profile flag can be overridden per driver. This does not add teleport, assisted steering, or trainer behavior: cars still advance through the normal vehicle physics and public action/state APIs.
|
|
233
|
+
|
|
234
|
+
For multi-car learner batches, `batch-training` is the default no-collision profile. It hides each learner from other cars' ray sensors and `nearbyCars` observations, excludes learner cars from race order, and still keeps them visible as normal physics cars in snapshots/rendering. Use `isolated-training` when a non-colliding car should still affect order. If a non-colliding car should remain sensor-visible, use `phantom-race` or explicitly override `detectableByRays` / `detectableAsNearby`.
|
|
235
|
+
|
|
236
|
+
Browser render snapshots include each car's resolved `interaction` object. The default browser renderer uses that to draw a blue outline marker around non-colliding physics participants while keeping them solid and clickable. That visual marker does not change collision, sensor, timing, pit, or control behavior.
|
|
237
|
+
|
|
238
|
+
Replay ghosts are separate trajectory-driven reference entities:
|
|
239
|
+
|
|
240
|
+
```js
|
|
241
|
+
replayGhosts: [
|
|
242
|
+
{
|
|
243
|
+
id: 'best-lap',
|
|
244
|
+
label: 'Best Lap',
|
|
245
|
+
color: '#00ff84',
|
|
246
|
+
opacity: 0.35,
|
|
247
|
+
visible: true,
|
|
248
|
+
trajectory: [
|
|
249
|
+
{
|
|
250
|
+
timeSeconds: 0,
|
|
251
|
+
x: 100,
|
|
252
|
+
y: 200,
|
|
253
|
+
headingRadians: 1.2,
|
|
254
|
+
speedKph: 180,
|
|
255
|
+
progressMeters: 0,
|
|
256
|
+
},
|
|
257
|
+
],
|
|
258
|
+
sensors: {
|
|
259
|
+
detectableByRays: false,
|
|
260
|
+
detectableAsNearby: false,
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
]
|
|
264
|
+
```
|
|
265
|
+
|
|
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.
|
|
179
267
|
|
|
180
268
|
Actions use normalized low-level controls:
|
|
181
269
|
|
|
@@ -189,13 +277,69 @@ Actions use normalized low-level controls:
|
|
|
189
277
|
}
|
|
190
278
|
```
|
|
191
279
|
|
|
192
|
-
|
|
280
|
+
Browser expert mode also exposes an external render hook for authoritative node-frame visualization:
|
|
281
|
+
|
|
282
|
+
```js
|
|
283
|
+
simulator.expert.attachExternalRenderer(source);
|
|
284
|
+
simulator.expert.detachExternalRenderer();
|
|
285
|
+
const state = simulator.expert.getExternalRendererState();
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
`source` contract:
|
|
289
|
+
|
|
290
|
+
```js
|
|
291
|
+
{
|
|
292
|
+
subscribe(onFrame) {
|
|
293
|
+
// call onFrame({ snapshot, observation, meta? })
|
|
294
|
+
return () => { /* unsubscribe */ };
|
|
295
|
+
},
|
|
296
|
+
}
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
While attached, expert runtime is strict render-only and rejects local `step()`, `resetDrivers()`, and `reset()`. Detaching restores the current local simulation track surface before hosts resume local expert stepping, even if the last external frame used a different track. Package core stays transport-agnostic: WebSocket connection/discovery belongs to host integration code, not to `src/`.
|
|
300
|
+
|
|
301
|
+
Operationally this is one architecture with two modes:
|
|
302
|
+
|
|
303
|
+
- local expert mode: browser owns stepping
|
|
304
|
+
- external render mode: browser only renders authoritative external frames
|
|
305
|
+
|
|
306
|
+
Policy Runner maps this directly through supported controller modes: `Distilled policy` and `Policy server` for browser-owned stepping, and `Live preview stream` for external render-only frames.
|
|
307
|
+
|
|
308
|
+
`steering` is an absolute steering-wheel target: `-1` points at the maximum left steering limit, `0` points at center, `1` points at the maximum right steering limit, and intermediate values are percentages of that limit. The vehicle physics moves the steering angle toward that target through the configured steering-rate limit, so centering is physical rather than an instantaneous snap. `throttle` and `brake` are clamped from `0` to `1`. `steering`, `throttle`, and `brake` are required on every controlled-driver action; missing or non-finite values fail action validation instead of defaulting to zero.
|
|
193
309
|
|
|
194
310
|
Actions may also include `pitIntent`. `0` clears a pending pit request and is accepted as a no-op even when pit stops are disabled, so fixed-shape policies can always send the full action object. `1` keeps trying until the next free-enough pit-entry window, and `2` commits to entering at the next pit-entry window even when pit-lane capacity or gap checks would block an opportunistic stop. Expert-controlled drivers start with `pitIntent: 0`, and tire-threshold automatic pit calls are disabled for those drivers so models do not manually steer into pit-lane geometry or get surprise pit calls from the built-in strategy. If pit stops are disabled, if the car has no pit assignment, or if the car is already entering, queued, servicing, or exiting, the environment rejects non-zero pit requests through the configured `actionPolicy`.
|
|
195
311
|
|
|
196
312
|
Pit actions may also include `pitCompound` or `pitTargetCompound`, for example `{ pitIntent: 2, pitCompound: 'H' }`. The value must be one of `rules.modules.tireStrategy.compounds`. If omitted, the simulator keeps the existing pending target or picks the first configured compound different from the current tire. The model never steers down pit-lane geometry directly; once a request is accepted, the simulator owns pit entry, queueing, penalty hold, tire service, and pit exit.
|
|
197
313
|
|
|
198
|
-
The recommended
|
|
314
|
+
The recommended runtime convention is a user-owned driver controller with `decideBatch(context) -> { [driverId]: { steering, throttle, brake, pitIntent? } }`. Controllers may load any model format or call any inference backend. PaddockJS only provides the simulator runtime context, stable controlled-driver ordering, cached specs, compact observations, and normalized action validation.
|
|
315
|
+
|
|
316
|
+
```js
|
|
317
|
+
import { createPaddockDriverControllerLoop } from '@inventure71/paddockjs';
|
|
318
|
+
|
|
319
|
+
const controller = {
|
|
320
|
+
async init(ctx) {
|
|
321
|
+
this.model = await loadUserModel(ctx.observationSpec);
|
|
322
|
+
},
|
|
323
|
+
async reset(ctx) {
|
|
324
|
+
this.hidden = resetHiddenState(ctx.controlledDrivers.length);
|
|
325
|
+
},
|
|
326
|
+
async decideBatch(ctx) {
|
|
327
|
+
const vectors = ctx.orderedObservations.map((item) => item.vector);
|
|
328
|
+
return this.model.predictBatch(vectors, this.hidden);
|
|
329
|
+
},
|
|
330
|
+
onStep(ctx) {
|
|
331
|
+
recordUserOwnedStats(ctx.metrics, ctx.actions);
|
|
332
|
+
},
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
const loop = createPaddockDriverControllerLoop({
|
|
336
|
+
runtime: env,
|
|
337
|
+
controller,
|
|
338
|
+
actionRepeat: 4,
|
|
339
|
+
});
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
The older one-car `policy.predict(driverObservation)` style remains easy to wrap inside a controller, but new browser playback and training-style loops should use `decideBatch()` so one inference call can cover every controlled car.
|
|
199
343
|
|
|
200
344
|
The environment exposes specs for external training code:
|
|
201
345
|
|
|
@@ -204,7 +348,53 @@ const actionSpec = env.getActionSpec();
|
|
|
204
348
|
const observationSpec = env.getObservationSpec();
|
|
205
349
|
```
|
|
206
350
|
|
|
207
|
-
`actionSpec` describes controlled drivers, normalized action ranges, and the optional pit intent values. `observationSpec` describes object observation fields, ray layout, nearby-car limits, track lookahead fields, and the versioned vector schema. `observation.lookaheadMeters` is sanitized to a finite numeric array; invalid or empty values fall back to the default `[20, 50, 100, 150]`.
|
|
351
|
+
`actionSpec` describes controlled drivers, normalized action ranges, and the optional pit intent values. `observationSpec` describes object observation fields, ray layout, nearby-car limits, track lookahead fields, and the versioned vector schema. `observation.lookaheadMeters` is sanitized to a finite numeric array; invalid or empty values fall back to the default `[20, 50, 100, 150]`. The opt-in `observation.profile: 'physical-driver'` profile defaults lookahead to `[]` so policies can use local driver-like senses without receiving privileged future track curvature. Realistic training runs should pair this profile with `physicsMode: 'simulator'`; that keeps the model's observed yaw, grip, contact-patch, kerb, and runoff behavior aligned with the physics it is learning to control.
|
|
352
|
+
|
|
353
|
+
Observation output can be compacted for training throughput:
|
|
354
|
+
|
|
355
|
+
```js
|
|
356
|
+
observation: {
|
|
357
|
+
profile: 'physical-driver',
|
|
358
|
+
output: 'vector', // 'full' | 'vector' | 'object'
|
|
359
|
+
includeSchema: false,
|
|
360
|
+
vectorType: 'float32', // 'array' | 'float32'
|
|
361
|
+
}
|
|
362
|
+
```
|
|
363
|
+
|
|
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.
|
|
367
|
+
|
|
368
|
+
Result state output can also be compacted:
|
|
369
|
+
|
|
370
|
+
```js
|
|
371
|
+
result: {
|
|
372
|
+
stateOutput: 'none', // 'full' | 'minimal' | 'none'
|
|
373
|
+
resetDriversObservationScope: 'reset', // 'all' | 'reset'
|
|
374
|
+
}
|
|
375
|
+
```
|
|
376
|
+
|
|
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.
|
|
378
|
+
|
|
379
|
+
The JSON worker protocol exposes the same reset-result controls through `resultOptions`:
|
|
380
|
+
|
|
381
|
+
```js
|
|
382
|
+
protocol.handle({
|
|
383
|
+
id: 'reset-agent-3',
|
|
384
|
+
type: 'resetDrivers',
|
|
385
|
+
placements: {
|
|
386
|
+
'agent-3': { distanceMeters: 1200, offsetMeters: 0, speedKph: 80 },
|
|
387
|
+
},
|
|
388
|
+
resultOptions: {
|
|
389
|
+
observationScope: 'reset',
|
|
390
|
+
stateOutput: 'none',
|
|
391
|
+
},
|
|
392
|
+
});
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
`getState` accepts `stateOptions: { output: 'full' | 'minimal' | 'none' }`. For compatibility with reset-style messages, `resultOptions.stateOutput` is also mapped to the same state output selector on `getState` messages.
|
|
396
|
+
|
|
397
|
+
This is still an environment-control message, not an action. A policy cannot submit `resetDrivers` inside `step(actions)`.
|
|
208
398
|
|
|
209
399
|
`createProgressReward()` is non-canonical demo reward code for examples and quick smoke tests, not the official reward. It returns a callback compatible with `reward(context)` and combines:
|
|
210
400
|
|
|
@@ -229,15 +419,18 @@ const reward = createProgressReward({
|
|
|
229
419
|
});
|
|
230
420
|
```
|
|
231
421
|
|
|
232
|
-
Custom reward functions receive the same context:
|
|
422
|
+
Custom reward functions receive the same context plus neutral environment facts:
|
|
233
423
|
|
|
234
424
|
```js
|
|
235
|
-
reward({ driverId, previous, current, action, events, state }) {
|
|
236
|
-
|
|
425
|
+
reward({ driverId, previous, current, action, events, state, metrics, episode }) {
|
|
426
|
+
if (metrics.destroyed) return -200;
|
|
427
|
+
if (metrics.offTrack) return -12;
|
|
428
|
+
if (episode.terminated) return 0;
|
|
429
|
+
return metrics.legalProgressDeltaMeters;
|
|
237
430
|
}
|
|
238
431
|
```
|
|
239
432
|
|
|
240
|
-
If no reward callback is provided, `result.reward` is `null`. 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.
|
|
241
434
|
|
|
242
435
|
Neutral rollout recording is available for external training loops:
|
|
243
436
|
|
|
@@ -251,7 +444,7 @@ const next = env.step(action);
|
|
|
251
444
|
recorder.recordStep(previous, action, next);
|
|
252
445
|
```
|
|
253
446
|
|
|
254
|
-
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.
|
|
255
448
|
|
|
256
449
|
Deterministic evaluation helpers run fixed seeds/scenarios and report environment quality metrics:
|
|
257
450
|
|
|
@@ -266,7 +459,7 @@ const report = runEnvironmentEvaluation({
|
|
|
266
459
|
});
|
|
267
460
|
```
|
|
268
461
|
|
|
269
|
-
Evaluation reports include distance, lap progress, off-track step count, contact count, recovery success, pass count, and first lap time when available. These metrics are not rewards.
|
|
462
|
+
Evaluation reports include distance, lap progress, off-track step count, contact count, recovery success, pass count, and first lap time when available. They follow the same wheel-level legal-surface and contact-event definitions as environment metrics. `runEnvironmentEvaluation()` can accept compact base options with `result.stateOutput: 'none'`; it internally requests the minimal snapshot needed for evaluation bookkeeping. Direct `createEvaluationTracker(result)` usage requires `state.snapshot`, so use `stateOutput: 'minimal'` or `full` when wiring the tracker manually. These metrics are not rewards.
|
|
270
463
|
|
|
271
464
|
`step(actions)` returns a JavaScript object designed for environment loops:
|
|
272
465
|
|
|
@@ -300,6 +493,16 @@ Evaluation reports include distance, lap progress, off-track step count, contact
|
|
|
300
493
|
pitStopPenaltyServiceRemainingSeconds,
|
|
301
494
|
pitStopsCompleted,
|
|
302
495
|
},
|
|
496
|
+
trackRelation: {
|
|
497
|
+
lateralOffsetMeters,
|
|
498
|
+
headingErrorRadians,
|
|
499
|
+
legalWidthMeters,
|
|
500
|
+
leftBoundaryMeters,
|
|
501
|
+
rightBoundaryMeters,
|
|
502
|
+
onLegalSurface,
|
|
503
|
+
surface,
|
|
504
|
+
},
|
|
505
|
+
contactPatches,
|
|
303
506
|
race: {
|
|
304
507
|
position,
|
|
305
508
|
totalCars,
|
|
@@ -316,6 +519,23 @@ Evaluation reports include distance, lap progress, off-track step count, contact
|
|
|
316
519
|
},
|
|
317
520
|
},
|
|
318
521
|
reward,
|
|
522
|
+
metrics: {
|
|
523
|
+
budget: {
|
|
524
|
+
progressDeltaMeters,
|
|
525
|
+
legalProgressDeltaMeters,
|
|
526
|
+
offTrack,
|
|
527
|
+
kerb,
|
|
528
|
+
fullyOutsideWhiteLine,
|
|
529
|
+
severeCut,
|
|
530
|
+
destroyed,
|
|
531
|
+
destroyReason,
|
|
532
|
+
under30kph,
|
|
533
|
+
spinOrBackwards,
|
|
534
|
+
completedLap,
|
|
535
|
+
lapTimeSeconds,
|
|
536
|
+
contactCount,
|
|
537
|
+
},
|
|
538
|
+
},
|
|
319
539
|
terminated,
|
|
320
540
|
truncated,
|
|
321
541
|
done,
|
|
@@ -331,11 +551,29 @@ Evaluation reports include distance, lap progress, off-track step count, contact
|
|
|
331
551
|
controlledDrivers,
|
|
332
552
|
actionErrors,
|
|
333
553
|
endReason,
|
|
554
|
+
drivers: {
|
|
555
|
+
budget: {
|
|
556
|
+
terminated,
|
|
557
|
+
truncated,
|
|
558
|
+
endReason,
|
|
559
|
+
episodeStep,
|
|
560
|
+
episodeId,
|
|
561
|
+
},
|
|
562
|
+
},
|
|
334
563
|
},
|
|
335
564
|
}
|
|
336
565
|
```
|
|
337
566
|
|
|
338
|
-
|
|
567
|
+
`state.snapshot.cars` contains physics-driven participants only. Each car includes `interaction` with the resolved profile flags. `state.snapshot.replayGhosts` contains trajectory-driven replay/reference entities only. This invariant is shared by browser and headless snapshots:
|
|
568
|
+
|
|
569
|
+
```txt
|
|
570
|
+
snapshot.cars = physics-driven race participants
|
|
571
|
+
snapshot.replayGhosts = trajectory-driven replay/reference entities
|
|
572
|
+
```
|
|
573
|
+
|
|
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.
|
|
575
|
+
|
|
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.
|
|
339
577
|
|
|
340
578
|
Default rays use a compact center-origin set with forward, side, and rear awareness:
|
|
341
579
|
|
|
@@ -350,11 +588,18 @@ Track distances are sampled against the actual track model. When the ray starts
|
|
|
350
588
|
angleDegrees,
|
|
351
589
|
angleRadians,
|
|
352
590
|
lengthMeters,
|
|
591
|
+
roadEdge: {
|
|
592
|
+
hit,
|
|
593
|
+
distanceMeters,
|
|
594
|
+
kind, // 'exit' | 'entry' | null
|
|
595
|
+
},
|
|
353
596
|
track: {
|
|
354
597
|
hit,
|
|
355
598
|
distanceMeters,
|
|
356
599
|
kind, // 'exit' | 'entry' | null
|
|
357
600
|
},
|
|
601
|
+
kerb: { hit, distanceMeters, surface },
|
|
602
|
+
illegalSurface: { hit, distanceMeters, surface },
|
|
358
603
|
car: {
|
|
359
604
|
hit,
|
|
360
605
|
distanceMeters,
|
|
@@ -364,7 +609,27 @@ Track distances are sampled against the actual track model. When the ray starts
|
|
|
364
609
|
}
|
|
365
610
|
```
|
|
366
611
|
|
|
367
|
-
|
|
612
|
+
`track` is kept as the compatibility alias for `roadEdge`. New ray configs may use a per-ray layout and opt into additional channels only when needed:
|
|
613
|
+
|
|
614
|
+
```js
|
|
615
|
+
sensors: {
|
|
616
|
+
rays: {
|
|
617
|
+
layout: 'driver-front-heavy',
|
|
618
|
+
channels: ['roadEdge', 'kerb', 'illegalSurface', 'car'],
|
|
619
|
+
precision: 'driver', // 'driver' | 'debug'
|
|
620
|
+
rays: [
|
|
621
|
+
{ id: 'front', angleDegrees: 0, lengthMeters: 260 },
|
|
622
|
+
{ id: 'right', angleDegrees: 90, lengthMeters: 80 },
|
|
623
|
+
],
|
|
624
|
+
},
|
|
625
|
+
}
|
|
626
|
+
```
|
|
627
|
+
|
|
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.
|
|
629
|
+
|
|
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.
|
|
631
|
+
|
|
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:
|
|
368
633
|
|
|
369
634
|
```js
|
|
370
635
|
{
|
|
@@ -375,11 +640,17 @@ Nearby-car observations are car-relative:
|
|
|
375
640
|
relativeSpeedKph,
|
|
376
641
|
relativeHeadingRadians,
|
|
377
642
|
ahead,
|
|
643
|
+
behind,
|
|
378
644
|
sameLap,
|
|
645
|
+
closingRateMetersPerSecond,
|
|
646
|
+
timeToContactSeconds,
|
|
647
|
+
leftOverlap,
|
|
648
|
+
rightOverlap,
|
|
379
649
|
}
|
|
380
650
|
```
|
|
381
651
|
|
|
382
|
-
If no `reward` callback is provided, `result.reward` is `null`. If provided, rewards are returned by controlled driver ID.
|
|
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.
|
|
383
654
|
|
|
384
655
|
Browser expert mode is opt-in through the browser mount API:
|
|
385
656
|
|
|
@@ -405,7 +676,7 @@ simulator.expert.step({
|
|
|
405
676
|
|
|
406
677
|
When browser expert mode is enabled, automatic ticker simulation advancement is disabled. The visual canvas advances only when host code calls `simulator.expert.reset()` or `simulator.expert.step(actions)`. Browser expert mode is a mount-time option; `restart(nextOptions)` rejects `expert` changes. Destroy and mount a new simulator to switch between built-in ticker control and expert stepping.
|
|
407
678
|
|
|
408
|
-
`expert.visualizeSensors` is a browser-only visual debugging option. When `visualizeSensors: true` or `visualizeSensors: { rays: true }` is set, the simulator draws the controlled
|
|
679
|
+
`expert.visualizeSensors` is a browser-only visual debugging option. When `visualizeSensors: true` or `visualizeSensors: { rays: true }` is set, the simulator draws the selected controlled driver's ray sensors in the Pixi world layer from the car's current position. Ray lengths and hit markers are based on the same observation data returned from the expert environment result. Every detected ray channel gets its own colored marker so road-edge, kerb, illegal surface, and car hits can be debugged independently. Barrier walls are rendered as track geometry, not model-facing ray hits. In multi-driver expert runs, use `visualizeSensors: { rays: true, drivers: 'all' }` only when every controlled car's overlay is intentionally needed; hosts may also pass `drivers: ['driver-a']` to pin the overlay to a fixed controlled subset.
|
|
409
680
|
|
|
410
681
|
## Lap Telemetry Snapshot
|
|
411
682
|
|
|
@@ -442,7 +713,25 @@ Each `car.lapTelemetry` snapshot includes current/last/best lap and sector timin
|
|
|
442
713
|
|
|
443
714
|
`totalLaps` is optional. Values are normalized to a finite positive integer, with invalid or non-positive input falling back to a one-lap race.
|
|
444
715
|
|
|
445
|
-
`trackSeed` is optional. If omitted, each mounted browser simulator creates a fresh procedural circuit. Passing a `trackSeed` makes the track deterministic; repeated procedural seeds are cached so multiple mounts can reuse the same generated track definition
|
|
716
|
+
`trackSeed` is optional. If omitted, each mounted browser simulator creates a fresh procedural circuit. Passing a `trackSeed` makes the track deterministic; repeated procedural seeds are cached so multiple mounts can reuse the same immutable generated track definition when the resolved generation options also match. Callers should treat `createProceduralTrack(seed, options)` results as read-only. Calling `restart({ trackSeed })` rebuilds the simulation on the deterministic circuit for that seed.
|
|
717
|
+
|
|
718
|
+
`trackGeneration` is optional and applies only when a procedural `trackSeed` is used. It accepts profile presets plus explicit overrides:
|
|
719
|
+
|
|
720
|
+
```js
|
|
721
|
+
{
|
|
722
|
+
profile: 'training-short',
|
|
723
|
+
length: { minMeters: 900, maxMeters: 1800 },
|
|
724
|
+
startStraight: { gridMeters: 0, exitMeters: 80, blendMeters: 80 },
|
|
725
|
+
pitLane: { enabled: false },
|
|
726
|
+
shape: { scale: 0.2, cornerDensity: 1.3, variation: 0.22 },
|
|
727
|
+
validation: { minClearanceMultiplier: 1, maxLocalTurnRadians: 1.85 },
|
|
728
|
+
attempts: { primary: 80, fallback: 200 },
|
|
729
|
+
}
|
|
730
|
+
```
|
|
731
|
+
|
|
732
|
+
Profiles are presets, not separate generators. `race` preserves the default full-length circuit with pit lane; `training-short`, `training-medium`, and `training-technical` generate smaller pitless circuits for training or demos. Resolution is `race` defaults, selected profile defaults, then explicit overrides. Procedural tracks are generated by tracing, smoothing, and warping package-owned seeded region masks, then validating the resulting centerline instead of falling back to pure ovals. Invalid candidates are rejected for length, world bounds, non-adjacent clearance, turn sharpness, self-intersections, and weak shape variation. The start/finish area is normalized into an explicit straight window so the starting grid, pit-entry approach, and immediate pit-exit merge area are straight even when the rest of the circuit is generated from curved controls.
|
|
733
|
+
|
|
734
|
+
`trackQueryIndex` controls indexed track/surface query acceleration. Browser/expert simulator mounts, headless environments, and direct race-simulation construction default this option to `true`, so visual race simulation, expert controls, Policy Runner playback, headless training/evaluation runs, and model-sense visualization use the indexed path by default. Set `trackQueryIndex: false` to force legacy non-indexed queries for comparison or debugging. It does not change public snapshot or observation shapes.
|
|
446
735
|
|
|
447
736
|
`initialCameraMode` is optional and accepts `'overview'`, `'leader'`, `'selected'`, `'show-all'`, or `'pit'`. Invalid values fall back to `'leader'`. The `overview` camera frames the generated track bounds with package-owned padding and pit-lane extent. The `pit` camera frames the active track's `pitLane` geometry, zooms out when needed to keep the full pit lane inside the active race-view safe area, and falls back to `leader` when no pit lane is available. Camera zoom controls and wheel zoom apply to every mode, with zoom-out bounded by the active track frame.
|
|
448
737
|
|
|
@@ -561,7 +850,7 @@ onDriverOpen(driver) {
|
|
|
561
850
|
}
|
|
562
851
|
```
|
|
563
852
|
|
|
564
|
-
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`.
|
|
565
854
|
|
|
566
855
|
Optional lifecycle callbacks:
|
|
567
856
|
|
|
@@ -577,7 +866,7 @@ Optional lifecycle callbacks:
|
|
|
577
866
|
}
|
|
578
867
|
```
|
|
579
868
|
|
|
580
|
-
`onRaceEvent` receives simulation events such as `contact`, `penalty`, `track-limits`, `pit-lane-speeding`, `safety-car`, `green-flag`, `start-lights-out`, and `race-finish`. `contact` events include metadata from the production body collision solver: `firstShapeId`, `secondShapeId`, `contactType`, `depth`, and `timeOfImpact`.
|
|
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.
|
|
581
870
|
|
|
582
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.
|
|
583
872
|
|
|
@@ -646,6 +935,9 @@ ui: {
|
|
|
646
935
|
raceDataBannerSize: 'custom',
|
|
647
936
|
raceDataTelemetryDetail: false,
|
|
648
937
|
timingTowerVerticalFit: 'expand-race-view',
|
|
938
|
+
},
|
|
939
|
+
debug: {
|
|
940
|
+
physicsModeIndicator: false,
|
|
649
941
|
}
|
|
650
942
|
```
|
|
651
943
|
|
|
@@ -654,6 +946,7 @@ ui: {
|
|
|
654
946
|
- `showFps`: controls whether the race canvas renders the FPS readout.
|
|
655
947
|
- `showRaceDataPanel`: controls whether the precombined shell includes the project/radio lower-third inside the race window.
|
|
656
948
|
- `showTimingTower`, `showTelemetry`: reserved component visibility flags for host layout decisions.
|
|
949
|
+
- `debug.physicsModeIndicator`: when `true`, renders a small top-left square in the race canvas. Blue means `physicsMode: 'arcade'`; red means `physicsMode: 'simulator'`. It defaults to `false` and is intended only for debug/development use.
|
|
657
950
|
- `telemetryIncludesOverview`: controls whether the telemetry stack template embeds the car/driver overview. Composable hosts can also pass `mountTelemetryPanel(root, { includeOverview: false })`.
|
|
658
951
|
- `telemetryModules`: controls optional telemetry surfaces inside stack/drawer templates. The default object enables `core` scalar readouts, `sectors` progress bars, `lapTimes`, and `sectorTimes`. It can also be `false` to disable all telemetry modules, or an array such as `['sectors', 'lapTimes']` to render only named modules. These modules are also individually mountable with `mountTelemetryCore`, `mountTelemetrySectors`, `mountTelemetryLapTimes`, and `mountTelemetrySectorTimes`.
|
|
659
952
|
- `raceDataBanners.initial`: `'project'`, `'radio'`, or `'hidden'`. This controls which lower-third appears first in the precombined shell.
|
|
@@ -701,13 +994,13 @@ After every car completes `totalLaps`, `getSnapshot()` returns:
|
|
|
701
994
|
finishedAt: 123.4,
|
|
702
995
|
winner: { id, code, name, rank, finished },
|
|
703
996
|
classification: [
|
|
704
|
-
{ id, code, timingCode, name, rank, lap, lapsCompleted, distanceMeters, gapMeters, gapSeconds, intervalSeconds, gapLaps, intervalLaps, finished, finishTime, penaltySeconds, adjustedFinishTime, positionDrop, disqualified },
|
|
997
|
+
{ id, code, timingCode, name, rank, lap, lapsCompleted, distanceMeters, gapMeters, gapSeconds, intervalSeconds, gapLaps, intervalLaps, finished, finishTime, dnf, dnfReason, dnfAt, dnfOrder, penaltySeconds, adjustedFinishTime, positionDrop, disqualified },
|
|
705
998
|
],
|
|
706
999
|
},
|
|
707
1000
|
}
|
|
708
1001
|
```
|
|
709
1002
|
|
|
710
|
-
Cars also include `team`, `speedKph`, `distanceMeters`, `gapAheadMeters`, `gapAheadSeconds`, `intervalAheadSeconds`, `leaderGapSeconds`, `gapAheadLaps`, `intervalAheadLaps`, `leaderGapLaps`, `finished`, `finishTime`, `finishRank`, `status`, `raceStatus`, `wavedFlag`, `penaltySeconds`, `adjustedFinishTime`, `classifiedRank`, and `lapTelemetry`. A car that has crossed the finish before the whole race is complete exposes `status: 'waved-flag'`, `raceStatus: 'waved-flag'`, and `wavedFlag: true`; the timing order keeps those cars frozen by provisional `finishRank` until final classification is available. Classification entries also expose `gapLaps`, `intervalLaps`, `positionDrop`,
|
|
1003
|
+
Cars also include `team`, `speedKph`, `distanceMeters`, `gapAheadMeters`, `gapAheadSeconds`, `intervalAheadSeconds`, `leaderGapSeconds`, `gapAheadLaps`, `intervalAheadLaps`, `leaderGapLaps`, `finished`, `finishTime`, `finishRank`, `status`, `raceStatus`, `wavedFlag`, `destroyed`, `destroyReason`, `destroyedAt`, `dnf`, `dnfReason`, `dnfAt`, `dnfOrder`, `penaltySeconds`, `adjustedFinishTime`, `classifiedRank`, and `lapTelemetry`. A car that has crossed the finish before the whole race is complete exposes `status: 'waved-flag'`, `raceStatus: 'waved-flag'`, and `wavedFlag: true`; the timing order keeps those cars frozen by provisional `finishRank` until final classification is available, and later barrier contact or stalled-off-track detection does not convert that result into DNF. A destroyed/out-of-race car exposes `dnf: true`, keeps compatibility `raceStatus: 'destroyed'`, shows `DNF` in the timing tower, renders faded/gray in the race canvas, and is ordered below running cars by `dnfOrder`. Stalled off-track DNFs use `dnfReason: 'stalled-off-track'` and `destroyed: false`; barrier DNFs keep `destroyed: true`. Classification entries also expose `gapLaps`, `intervalLaps`, `positionDrop`, `disqualified`, and DNF metadata. `gapAheadSeconds` and `intervalAheadSeconds` are the interval to the car directly ahead when both cars are on the same lead-lap cycle; `gapAheadLaps` and `intervalAheadLaps` are the whole-lap deficit to that car when it is at least one lap ahead. `leaderGapSeconds` is the same-lead-lap gap to P1. `leaderGapLaps` is the whole-lap deficit to P1. Seconds gaps are measured from the timestamp difference when both cars crossed the same hidden timing line, with fallback interpolation only when no shared timing line exists yet. The timing tower prefers lap labels such as `+1` over seconds when the relevant lap gap is positive, shows `WAVED` for a finished car before full race classification, and shows `DNF` for retired cars. The first car to finish sets a provisional `raceControl.winner` and receives a `car-finish` event, but the race keeps running until every race participant has finished or is DNF. A DNF car restored before final classification re-enters live timing and must finish; after final classification, resurrection does not reopen the race. Final `raceControl.classification` sorts finishers by `finishTime + penaltySeconds` after converting unserved drive-through and stop-go penalties, applies position-drop and disqualification consequences, then appends DNF entries after finishers.
|
|
711
1004
|
|
|
712
1005
|
When `rules.modules.pitStops.enabled` is true, cars also expose `pitIntent`, `pitStop`, and `usedTireCompounds`. `pitIntent` and `pitStop.intent` use `0` for no request, `1` for an opportunistic request that stays active until a free-enough entry window appears, and `2` for a committed request that enters at the next pit-entry window even if pit capacity or gap checks would block mode `1`. `pitStop.status` is one of `pending`, `entering`, `queued`, `servicing`, `exiting`, or `completed`; `pitStop.phase` may be `entry`, `queue`, `queue-release`, `penalty`, `service`, `exit`, or `null`. It also includes the assigned shared service `boxId`/`boxIndex`, the driver's `garageBoxId`/`garageBoxIndex`, optional team id/color, planned pit-call race distance, physical pit-entry race distance, service seconds remaining, `penaltyServiceRemainingSeconds`, `penaltyServiceTotalSeconds`, `servingPenaltyIds`, target tire, optional `serviceProfile`, queue flag, and completed stop count. Host APIs and expert actions can choose the target tire compound with `setPitIntent(driverId, intent, targetCompound)` or `pitCompound`; invalid compounds are rejected. Automatic pit calls can share the same entry lap, but a pending mode `1` car only joins when the active pit-lane population is below `maxConcurrentPitLaneCars` and the nearest active pit-lane car is at least `minimumPitLaneGapMeters` ahead; mode `2` bypasses that opportunistic-capacity gate and commits to the automatic pit-entry route. Team-mates share one service area; every car enters through the working-lane queue point first. If the service area is free, the queue point behaves as a rolling gate and the car immediately follows a short `queue-release` route into service without exposing `queued`; if the area is occupied, the car exposes `status: queued` until servicing, queue-release, and just-exiting cars have physically cleared the active spot. Completed stops can be re-armed by later tire condition or host intent, so `stopsCompleted` can increase beyond one during longer races. `pitLaneSpeedLimitKph` applies only while the automatic route is inside the straight main pit lane/working lane; entry and exit connector roads remain legal pit surfaces but are not limiter zones. The automatic route brakes before the main lane start, travels along the main fast lane, and crosses into the team service area only near the assigned stop. If `phase` is `penalty`, the car is stationary before tire service and the browser renderer shows a red `+Ns` countdown above the car; if `phase` is `service`, it shows a yellow `Ns` countdown for the normal pit-service time. Optional pit-stop variability records the resolved `serviceProfile`; `pitStops.variability.perfect: true` forces the configured default service time for deterministic training. `usedTireCompounds` starts with the initial tire and receives the tire selected by completed automatic pit stops.
|
|
713
1006
|
|
|
@@ -743,7 +1036,7 @@ Every built track is automatically divided into three equal sectors. `snapshot.t
|
|
|
743
1036
|
|
|
744
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.
|
|
745
1038
|
|
|
746
|
-
|
|
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:
|
|
747
1040
|
|
|
748
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.
|
|
749
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.
|
|
@@ -807,9 +1100,10 @@ Controller methods:
|
|
|
807
1100
|
- `callSafetyCar()`: deploys the safety car.
|
|
808
1101
|
- `clearSafetyCar()`: releases the safety car.
|
|
809
1102
|
- `toggleSafetyCar()`: switches safety car deployment based on the current snapshot.
|
|
810
|
-
- `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.
|
|
811
1104
|
- `getPitIntent(driverId)`: reads the current pit intent.
|
|
812
1105
|
- `getPitTargetCompound(driverId)`: reads the current pit target tire.
|
|
1106
|
+
- `getSimulationSpeed()`: returns the active browser playback multiplier from the package-owned simulation-speed control, defaulting to `1`.
|
|
813
1107
|
- `servePenalty(penaltyId)`: marks an issued drive-through or stop-go penalty as served.
|
|
814
1108
|
- `cancelPenalty(penaltyId)`: cancels a penalty so it no longer affects service, timing, grid, or classification.
|
|
815
1109
|
- `getSnapshot()`: returns the latest simulation snapshot.
|
|
@@ -829,6 +1123,7 @@ Composable controllers additionally expose:
|
|
|
829
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.
|
|
830
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.
|
|
831
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.
|
|
832
1127
|
- `start()`: initializes PixiJS, binds mounted controls, and starts the simulation loop.
|
|
833
1128
|
|
|
834
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.
|
package/docs/data_model.md
CHANGED
|
@@ -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
|
|
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
|
|