@inventure71/paddockjs 0.3.0 → 2.0.0
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 +91 -0
- package/INSTALL_AND_UPDATE.md +5 -2
- package/README.md +328 -13
- package/docs/architecture.md +150 -41
- package/docs/concepts.md +22 -3
- package/docs/custom_model_controller.md +329 -0
- package/docs/data_contract.md +474 -34
- package/docs/data_model.md +1 -1
- package/docs/index.md +1 -0
- package/docs/learnings.md +23 -1
- package/docs/rules.md +169 -21
- package/docs/system_specs.md +56 -20
- package/docs/training.md +369 -25
- package/examples/python/README.md +62 -0
- package/examples/python/base_policy_server.py +191 -0
- package/examples/python/requirements.txt +2 -0
- package/examples/train-basic-policy.mjs +5 -6
- package/examples/trainingData.mjs +133 -0
- package/package.json +15 -4
- package/src/api/PaddockSimulatorController.js +36 -2
- package/src/app/BrowserExpertAdapter.js +367 -9
- package/src/app/F1SimulatorApp.js +615 -940
- 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 +14 -1
- 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 +24 -4
- package/src/environment/actions.js +38 -7
- package/src/environment/controllerLoop.js +246 -0
- package/src/environment/episode.js +86 -2
- package/src/environment/evaluation.js +206 -0
- package/src/environment/index.d.ts +638 -6
- package/src/environment/index.js +14 -0
- package/src/environment/metrics.js +159 -0
- package/src/environment/observationOptions.js +33 -0
- package/src/environment/observations.js +500 -43
- package/src/environment/options.js +41 -2
- package/src/environment/recorder.js +28 -0
- package/src/environment/runtime.js +260 -24
- package/src/environment/scenarios.js +249 -0
- 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 +65 -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 +60 -0
- package/src/environment/sensors/surfaceRays.js +296 -0
- package/src/environment/sensors/trackRays.js +261 -0
- package/src/environment/sensors.js +1 -257
- package/src/environment/specs.js +188 -9
- package/src/environment/workerProtocol.js +64 -0
- package/src/index.d.ts +533 -7
- package/src/index.js +26 -0
- package/src/rendering/proceduralTrackAsset.js +20 -510
- 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 +214 -0
- 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 -253
- package/src/simulation/participants/participantInteractions.js +120 -0
- package/src/simulation/pit/pitFlow.js +234 -0
- package/src/simulation/pit/pitIntent.js +92 -0
- package/src/simulation/pit/pitOccupancy.js +47 -0
- package/src/simulation/pit/pitPenaltyService.js +71 -0
- package/src/simulation/pit/pitQueue.js +55 -0
- package/src/simulation/pit/pitRouteMovement.js +160 -0
- package/src/simulation/pit/pitRouting.js +319 -0
- package/src/simulation/pit/pitService.js +41 -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 +92 -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 +71 -0
- package/src/simulation/race/raceOrder.js +113 -0
- package/src/simulation/race/raceProgress.js +124 -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 +86 -0
- package/src/simulation/race/stalledDnf.js +86 -0
- package/src/simulation/raceSimulation.js +386 -1012
- package/src/simulation/replay/replayGhosts.js +128 -0
- package/src/simulation/rules/collisionSteward.js +69 -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/penaltyLedger.js +153 -0
- package/src/simulation/rules/penaltyStats.js +26 -0
- package/src/simulation/rules/pitLaneSpeedingSteward.js +71 -0
- package/src/simulation/rules/ruleConfigMerge.js +41 -0
- package/src/simulation/rules/ruleDefaults.js +85 -0
- package/src/simulation/rules/rulesReview.js +141 -0
- package/src/simulation/rules/rulesetPresets.js +47 -0
- package/src/simulation/rules/tireRequirementSteward.js +29 -0
- package/src/simulation/rules/trackLimitsSteward.js +85 -0
- package/src/simulation/rulesConfig.js +34 -0
- package/src/simulation/snapshots/raceSnapshots.js +192 -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/trackQueryIndex.js +945 -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 -628
- package/src/simulation/units.js +6 -4
- package/src/simulation/vehicle/contactResolution.js +116 -0
- package/src/simulation/vehicle/mainTrackWheelSurface.js +82 -0
- package/src/simulation/vehicle/pitWheelSurface.js +69 -0
- package/src/simulation/vehicle/runoffResponse.js +88 -0
- package/src/simulation/vehicle/surfacePriority.js +28 -0
- package/src/simulation/vehicle/vehicleGeometry.js +247 -0
- package/src/simulation/vehicle/vehiclePhysics.js +363 -0
- package/src/simulation/vehicle/vehicleSnapshots.js +316 -0
- package/src/simulation/vehicle/vehicleState.js +186 -0
- package/src/simulation/vehicle/wheelSurface.js +150 -0
- package/src/simulation/vehicleGeometry.js +1 -0
- package/src/simulation/vehiclePhysics.js +1 -104
- package/src/simulation/warmup/runtimeWarmup.js +224 -0
- package/src/simulation/wheelSurface.js +1 -0
- package/src/styles.css +342 -42
- package/src/ui/bannerTemplates.js +69 -0
- package/src/ui/cameraControlsTemplate.js +20 -0
- package/src/ui/carOverviewTemplate.js +33 -0
- package/src/ui/componentTemplates.js +20 -426
- package/src/ui/raceCanvasTemplate.js +69 -0
- package/src/ui/raceControlStatusBanner.js +39 -0
- package/src/ui/raceControlsTemplate.js +36 -0
- package/src/ui/shellTemplate.js +6 -5
- package/src/ui/telemetryDrawerTemplate.js +55 -0
- package/src/ui/telemetryTemplates.js +136 -0
- package/src/ui/templateUtils.js +23 -0
- package/src/ui/timingTowerTemplate.js +34 -0
package/docs/data_contract.md
CHANGED
|
@@ -14,7 +14,13 @@ mountF1Simulator(root, {
|
|
|
14
14
|
onDriverOpen,
|
|
15
15
|
seed,
|
|
16
16
|
trackSeed,
|
|
17
|
+
trackQueryIndex,
|
|
18
|
+
warmup,
|
|
17
19
|
totalLaps,
|
|
20
|
+
physicsMode,
|
|
21
|
+
rules,
|
|
22
|
+
participantInteractions,
|
|
23
|
+
replayGhosts,
|
|
18
24
|
initialCameraMode,
|
|
19
25
|
theme,
|
|
20
26
|
title,
|
|
@@ -24,6 +30,7 @@ mountF1Simulator(root, {
|
|
|
24
30
|
showBackLink,
|
|
25
31
|
ui,
|
|
26
32
|
assets,
|
|
33
|
+
expert,
|
|
27
34
|
onLoadingChange,
|
|
28
35
|
onReady,
|
|
29
36
|
onError,
|
|
@@ -43,12 +50,19 @@ const simulator = createPaddockSimulator({
|
|
|
43
50
|
onDriverOpen,
|
|
44
51
|
seed,
|
|
45
52
|
trackSeed,
|
|
53
|
+
trackQueryIndex,
|
|
54
|
+
warmup,
|
|
46
55
|
totalLaps,
|
|
56
|
+
physicsMode,
|
|
57
|
+
rules,
|
|
58
|
+
participantInteractions,
|
|
59
|
+
replayGhosts,
|
|
47
60
|
initialCameraMode,
|
|
48
61
|
preset,
|
|
49
62
|
theme,
|
|
50
63
|
ui,
|
|
51
64
|
assets,
|
|
65
|
+
expert,
|
|
52
66
|
});
|
|
53
67
|
|
|
54
68
|
simulator.mountRaceCanvas(canvasRoot, {
|
|
@@ -85,7 +99,7 @@ simulator.mountRaceDataPanel(raceDataRoot);
|
|
|
85
99
|
Headless training code imports the browser-free environment subpath:
|
|
86
100
|
|
|
87
101
|
```js
|
|
88
|
-
import { createPaddockEnvironment
|
|
102
|
+
import { createPaddockEnvironment } from '@inventure71/paddockjs/environment';
|
|
89
103
|
|
|
90
104
|
const env = createPaddockEnvironment({
|
|
91
105
|
drivers,
|
|
@@ -95,11 +109,19 @@ const env = createPaddockEnvironment({
|
|
|
95
109
|
trackSeed: 2026,
|
|
96
110
|
totalLaps: 3,
|
|
97
111
|
frameSkip: 2,
|
|
112
|
+
rules: {
|
|
113
|
+
ruleset: 'custom',
|
|
114
|
+
standingStart: false,
|
|
115
|
+
modules: {
|
|
116
|
+
penalties: {
|
|
117
|
+
trackLimits: { strictness: 0.25 },
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
},
|
|
98
121
|
scenario: {
|
|
99
122
|
participants: 'all',
|
|
100
123
|
nonControlled: 'ai',
|
|
101
124
|
},
|
|
102
|
-
reward: createProgressReward(),
|
|
103
125
|
});
|
|
104
126
|
|
|
105
127
|
let result = env.reset();
|
|
@@ -108,18 +130,131 @@ result = env.step({
|
|
|
108
130
|
});
|
|
109
131
|
```
|
|
110
132
|
|
|
111
|
-
`controlledDrivers` is required. It supports one or many externally controlled cars. Non-controlled participants use the built-in driver AI in the
|
|
133
|
+
`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
|
+
`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
|
+
`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.
|
|
137
|
+
`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:
|
|
112
138
|
|
|
113
|
-
|
|
139
|
+
```js
|
|
140
|
+
rules: {
|
|
141
|
+
ruleset: 'fia2025',
|
|
142
|
+
modules: {
|
|
143
|
+
pitStops: {
|
|
144
|
+
enabled: true,
|
|
145
|
+
pitLaneSpeedLimitKph: 80,
|
|
146
|
+
maxConcurrentPitLaneCars: 3,
|
|
147
|
+
minimumPitLaneGapMeters: 20,
|
|
148
|
+
tirePitRequestThresholdPercent: 50,
|
|
149
|
+
tirePitCommitThresholdPercent: 30,
|
|
150
|
+
},
|
|
151
|
+
tireDegradation: {
|
|
152
|
+
enabled: false,
|
|
153
|
+
},
|
|
154
|
+
stalledDnf: {
|
|
155
|
+
enabled: true,
|
|
156
|
+
maxStoppedSeconds: 12,
|
|
157
|
+
speedThresholdKph: 5,
|
|
158
|
+
},
|
|
159
|
+
penalties: {
|
|
160
|
+
trackLimits: { strictness: 0.8 },
|
|
161
|
+
collision: { strictness: 0.5, consequences: [{ type: 'time', seconds: 5 }] },
|
|
162
|
+
tireRequirement: { strictness: 1, consequences: [{ type: 'time', seconds: 10 }] },
|
|
163
|
+
pitLaneSpeeding: { strictness: 1, speedLimitKph: 80 },
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
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.
|
|
170
|
+
|
|
171
|
+
Scenario support:
|
|
114
172
|
|
|
115
173
|
```js
|
|
116
174
|
scenario: {
|
|
117
175
|
participants: 'all' | 'controlled-only' | ['budget', 'alpha'],
|
|
118
176
|
nonControlled: 'ai',
|
|
177
|
+
preset: 'cornering' | 'off-track-recovery' | 'overtaking-pack' | 'pit-entry',
|
|
178
|
+
placements: {
|
|
179
|
+
budget: {
|
|
180
|
+
distanceMeters: 420,
|
|
181
|
+
offsetMeters: 16,
|
|
182
|
+
speedKph: 65,
|
|
183
|
+
headingErrorRadians: 0.4,
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
traffic: [
|
|
187
|
+
{
|
|
188
|
+
driverId: 'alpha',
|
|
189
|
+
relativeTo: 'budget',
|
|
190
|
+
deltaDistanceMeters: 24,
|
|
191
|
+
offsetMeters: -1.5,
|
|
192
|
+
speedKph: 68,
|
|
193
|
+
headingErrorRadians: 0,
|
|
194
|
+
},
|
|
195
|
+
],
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
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
|
+
|
|
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)`.
|
|
202
|
+
|
|
203
|
+
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
|
+
|
|
205
|
+
```js
|
|
206
|
+
participantInteractions: {
|
|
207
|
+
defaultProfile: 'normal',
|
|
208
|
+
drivers: {
|
|
209
|
+
'model-a': { profile: 'normal' },
|
|
210
|
+
'model-b': { profile: 'isolated-training' },
|
|
211
|
+
},
|
|
119
212
|
}
|
|
120
213
|
```
|
|
121
214
|
|
|
122
|
-
|
|
215
|
+
Supported profiles are:
|
|
216
|
+
|
|
217
|
+
- `normal`: collidable, ray-detectable, nearby-detectable, pit-lane-blocking, and race-order-affecting.
|
|
218
|
+
- `isolated-training`: non-colliding, hidden from rays and nearby-car observations, non-blocking in pit-lane occupancy, but still included in race order.
|
|
219
|
+
- `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`.
|
|
220
|
+
- `phantom-race`: non-colliding and non-blocking in pit-lane occupancy, but visible to rays and nearby-car observations and included in race order.
|
|
221
|
+
- `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`.
|
|
222
|
+
|
|
223
|
+
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.
|
|
224
|
+
|
|
225
|
+
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`.
|
|
226
|
+
|
|
227
|
+
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.
|
|
228
|
+
|
|
229
|
+
Replay ghosts are separate trajectory-driven reference entities:
|
|
230
|
+
|
|
231
|
+
```js
|
|
232
|
+
replayGhosts: [
|
|
233
|
+
{
|
|
234
|
+
id: 'best-lap',
|
|
235
|
+
label: 'Best Lap',
|
|
236
|
+
color: '#00ff84',
|
|
237
|
+
opacity: 0.35,
|
|
238
|
+
visible: true,
|
|
239
|
+
trajectory: [
|
|
240
|
+
{
|
|
241
|
+
timeSeconds: 0,
|
|
242
|
+
x: 100,
|
|
243
|
+
y: 200,
|
|
244
|
+
headingRadians: 1.2,
|
|
245
|
+
speedKph: 180,
|
|
246
|
+
progressMeters: 0,
|
|
247
|
+
},
|
|
248
|
+
],
|
|
249
|
+
sensors: {
|
|
250
|
+
detectableByRays: false,
|
|
251
|
+
detectableAsNearby: false,
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
]
|
|
255
|
+
```
|
|
256
|
+
|
|
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.
|
|
123
258
|
|
|
124
259
|
Actions use normalized low-level controls:
|
|
125
260
|
|
|
@@ -133,9 +268,69 @@ Actions use normalized low-level controls:
|
|
|
133
268
|
}
|
|
134
269
|
```
|
|
135
270
|
|
|
136
|
-
|
|
271
|
+
Browser expert mode also exposes an external render hook for authoritative node-frame visualization:
|
|
272
|
+
|
|
273
|
+
```js
|
|
274
|
+
simulator.expert.attachExternalRenderer(source);
|
|
275
|
+
simulator.expert.detachExternalRenderer();
|
|
276
|
+
const state = simulator.expert.getExternalRendererState();
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
`source` contract:
|
|
280
|
+
|
|
281
|
+
```js
|
|
282
|
+
{
|
|
283
|
+
subscribe(onFrame) {
|
|
284
|
+
// call onFrame({ snapshot, observation, meta? })
|
|
285
|
+
return () => { /* unsubscribe */ };
|
|
286
|
+
},
|
|
287
|
+
}
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
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/`.
|
|
291
|
+
|
|
292
|
+
Operationally this is one architecture with two modes:
|
|
293
|
+
|
|
294
|
+
- local expert mode: browser owns stepping
|
|
295
|
+
- external render mode: browser only renders authoritative external frames
|
|
296
|
+
|
|
297
|
+
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.
|
|
298
|
+
|
|
299
|
+
`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.
|
|
300
|
+
|
|
301
|
+
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`.
|
|
302
|
+
|
|
303
|
+
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.
|
|
137
304
|
|
|
138
|
-
The recommended
|
|
305
|
+
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.
|
|
306
|
+
|
|
307
|
+
```js
|
|
308
|
+
import { createPaddockDriverControllerLoop } from '@inventure71/paddockjs';
|
|
309
|
+
|
|
310
|
+
const controller = {
|
|
311
|
+
async init(ctx) {
|
|
312
|
+
this.model = await loadUserModel(ctx.observationSpec);
|
|
313
|
+
},
|
|
314
|
+
async reset(ctx) {
|
|
315
|
+
this.hidden = resetHiddenState(ctx.controlledDrivers.length);
|
|
316
|
+
},
|
|
317
|
+
async decideBatch(ctx) {
|
|
318
|
+
const vectors = ctx.orderedObservations.map((item) => item.vector);
|
|
319
|
+
return this.model.predictBatch(vectors, this.hidden);
|
|
320
|
+
},
|
|
321
|
+
onStep(ctx) {
|
|
322
|
+
recordUserOwnedStats(ctx.metrics, ctx.actions);
|
|
323
|
+
},
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
const loop = createPaddockDriverControllerLoop({
|
|
327
|
+
runtime: env,
|
|
328
|
+
controller,
|
|
329
|
+
actionRepeat: 4,
|
|
330
|
+
});
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
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.
|
|
139
334
|
|
|
140
335
|
The environment exposes specs for external training code:
|
|
141
336
|
|
|
@@ -144,9 +339,53 @@ const actionSpec = env.getActionSpec();
|
|
|
144
339
|
const observationSpec = env.getObservationSpec();
|
|
145
340
|
```
|
|
146
341
|
|
|
147
|
-
`actionSpec` describes controlled drivers
|
|
342
|
+
`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.
|
|
343
|
+
|
|
344
|
+
Observation output can be compacted for training throughput:
|
|
148
345
|
|
|
149
|
-
|
|
346
|
+
```js
|
|
347
|
+
observation: {
|
|
348
|
+
profile: 'physical-driver',
|
|
349
|
+
output: 'vector', // 'full' | 'vector' | 'object'
|
|
350
|
+
includeSchema: false,
|
|
351
|
+
vectorType: 'float32', // 'array' | 'float32'
|
|
352
|
+
}
|
|
353
|
+
```
|
|
354
|
+
|
|
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.
|
|
356
|
+
|
|
357
|
+
Result state output can also be compacted:
|
|
358
|
+
|
|
359
|
+
```js
|
|
360
|
+
result: {
|
|
361
|
+
stateOutput: 'none', // 'full' | 'minimal' | 'none'
|
|
362
|
+
resetDriversObservationScope: 'reset', // 'all' | 'reset'
|
|
363
|
+
}
|
|
364
|
+
```
|
|
365
|
+
|
|
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.
|
|
367
|
+
|
|
368
|
+
The JSON worker protocol exposes the same reset-result controls through `resultOptions`:
|
|
369
|
+
|
|
370
|
+
```js
|
|
371
|
+
protocol.handle({
|
|
372
|
+
id: 'reset-agent-3',
|
|
373
|
+
type: 'resetDrivers',
|
|
374
|
+
placements: {
|
|
375
|
+
'agent-3': { distanceMeters: 1200, offsetMeters: 0, speedKph: 80 },
|
|
376
|
+
},
|
|
377
|
+
resultOptions: {
|
|
378
|
+
observationScope: 'reset',
|
|
379
|
+
stateOutput: 'none',
|
|
380
|
+
},
|
|
381
|
+
});
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
`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.
|
|
385
|
+
|
|
386
|
+
This is still an environment-control message, not an action. A policy cannot submit `resetDrivers` inside `step(actions)`.
|
|
387
|
+
|
|
388
|
+
`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:
|
|
150
389
|
|
|
151
390
|
- forward progress in meters from `state.snapshot.cars[].distanceMeters`
|
|
152
391
|
- on-track speed in `current.object.self.speedKph`
|
|
@@ -169,14 +408,48 @@ const reward = createProgressReward({
|
|
|
169
408
|
});
|
|
170
409
|
```
|
|
171
410
|
|
|
172
|
-
Custom reward functions receive the same context:
|
|
411
|
+
Custom reward functions receive the same context plus neutral environment facts:
|
|
173
412
|
|
|
174
413
|
```js
|
|
175
|
-
reward({ driverId, previous, current, action, events, state }) {
|
|
176
|
-
|
|
414
|
+
reward({ driverId, previous, current, action, events, state, metrics, episode }) {
|
|
415
|
+
if (metrics.destroyed) return -200;
|
|
416
|
+
if (metrics.offTrack) return -12;
|
|
417
|
+
if (episode.terminated) return 0;
|
|
418
|
+
return metrics.legalProgressDeltaMeters;
|
|
177
419
|
}
|
|
178
420
|
```
|
|
179
421
|
|
|
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.
|
|
423
|
+
|
|
424
|
+
Neutral rollout recording is available for external training loops:
|
|
425
|
+
|
|
426
|
+
```js
|
|
427
|
+
import { createRolloutRecorder } from '@inventure71/paddockjs/environment';
|
|
428
|
+
|
|
429
|
+
const recorder = createRolloutRecorder();
|
|
430
|
+
const previous = env.reset();
|
|
431
|
+
const action = { budget: { steering: 0, throttle: 1, brake: 0 } };
|
|
432
|
+
const next = env.step(action);
|
|
433
|
+
recorder.recordStep(previous, action, next);
|
|
434
|
+
```
|
|
435
|
+
|
|
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.
|
|
437
|
+
|
|
438
|
+
Deterministic evaluation helpers run fixed seeds/scenarios and report environment quality metrics:
|
|
439
|
+
|
|
440
|
+
```js
|
|
441
|
+
import { runEnvironmentEvaluation } from '@inventure71/paddockjs/environment';
|
|
442
|
+
|
|
443
|
+
const report = runEnvironmentEvaluation({
|
|
444
|
+
baseOptions: { drivers, entries, controlledDrivers: ['budget'] },
|
|
445
|
+
policy(observation) {
|
|
446
|
+
return myPolicy.predict(observation);
|
|
447
|
+
},
|
|
448
|
+
});
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
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.
|
|
452
|
+
|
|
180
453
|
`step(actions)` returns a JavaScript object designed for environment loops:
|
|
181
454
|
|
|
182
455
|
```js
|
|
@@ -196,10 +469,29 @@ reward({ driverId, previous, current, action, events, state }) {
|
|
|
196
469
|
lapProgressMeters,
|
|
197
470
|
trackOffsetMeters,
|
|
198
471
|
trackHeadingErrorRadians,
|
|
199
|
-
onTrack,
|
|
472
|
+
onTrack, // true for track, kerb, and legal pit-lane/box surfaces
|
|
200
473
|
surface,
|
|
474
|
+
inPitLane,
|
|
475
|
+
pitLanePart,
|
|
476
|
+
pitBoxId,
|
|
201
477
|
tireEnergy,
|
|
478
|
+
pitIntent,
|
|
479
|
+
pitStopStatus,
|
|
480
|
+
pitStopPhase,
|
|
481
|
+
pitStopServiceRemainingSeconds,
|
|
482
|
+
pitStopPenaltyServiceRemainingSeconds,
|
|
483
|
+
pitStopsCompleted,
|
|
202
484
|
},
|
|
485
|
+
trackRelation: {
|
|
486
|
+
lateralOffsetMeters,
|
|
487
|
+
headingErrorRadians,
|
|
488
|
+
legalWidthMeters,
|
|
489
|
+
leftBoundaryMeters,
|
|
490
|
+
rightBoundaryMeters,
|
|
491
|
+
onLegalSurface,
|
|
492
|
+
surface,
|
|
493
|
+
},
|
|
494
|
+
contactPatches,
|
|
203
495
|
race: {
|
|
204
496
|
position,
|
|
205
497
|
totalCars,
|
|
@@ -216,6 +508,23 @@ reward({ driverId, previous, current, action, events, state }) {
|
|
|
216
508
|
},
|
|
217
509
|
},
|
|
218
510
|
reward,
|
|
511
|
+
metrics: {
|
|
512
|
+
budget: {
|
|
513
|
+
progressDeltaMeters,
|
|
514
|
+
legalProgressDeltaMeters,
|
|
515
|
+
offTrack,
|
|
516
|
+
kerb,
|
|
517
|
+
fullyOutsideWhiteLine,
|
|
518
|
+
severeCut,
|
|
519
|
+
destroyed,
|
|
520
|
+
destroyReason,
|
|
521
|
+
under30kph,
|
|
522
|
+
spinOrBackwards,
|
|
523
|
+
completedLap,
|
|
524
|
+
lapTimeSeconds,
|
|
525
|
+
contactCount,
|
|
526
|
+
},
|
|
527
|
+
},
|
|
219
528
|
terminated,
|
|
220
529
|
truncated,
|
|
221
530
|
done,
|
|
@@ -231,11 +540,29 @@ reward({ driverId, previous, current, action, events, state }) {
|
|
|
231
540
|
controlledDrivers,
|
|
232
541
|
actionErrors,
|
|
233
542
|
endReason,
|
|
543
|
+
drivers: {
|
|
544
|
+
budget: {
|
|
545
|
+
terminated,
|
|
546
|
+
truncated,
|
|
547
|
+
endReason,
|
|
548
|
+
episodeStep,
|
|
549
|
+
episodeId,
|
|
550
|
+
},
|
|
551
|
+
},
|
|
234
552
|
},
|
|
235
553
|
}
|
|
236
554
|
```
|
|
237
555
|
|
|
238
|
-
|
|
556
|
+
`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:
|
|
557
|
+
|
|
558
|
+
```txt
|
|
559
|
+
snapshot.cars = physics-driven race participants
|
|
560
|
+
snapshot.replayGhosts = trajectory-driven replay/reference entities
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
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
|
+
|
|
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.
|
|
239
566
|
|
|
240
567
|
Default rays use a compact center-origin set with forward, side, and rear awareness:
|
|
241
568
|
|
|
@@ -250,11 +577,18 @@ Track distances are sampled against the actual track model. When the ray starts
|
|
|
250
577
|
angleDegrees,
|
|
251
578
|
angleRadians,
|
|
252
579
|
lengthMeters,
|
|
580
|
+
roadEdge: {
|
|
581
|
+
hit,
|
|
582
|
+
distanceMeters,
|
|
583
|
+
kind, // 'exit' | 'entry' | null
|
|
584
|
+
},
|
|
253
585
|
track: {
|
|
254
586
|
hit,
|
|
255
587
|
distanceMeters,
|
|
256
588
|
kind, // 'exit' | 'entry' | null
|
|
257
589
|
},
|
|
590
|
+
kerb: { hit, distanceMeters, surface },
|
|
591
|
+
illegalSurface: { hit, distanceMeters, surface },
|
|
258
592
|
car: {
|
|
259
593
|
hit,
|
|
260
594
|
distanceMeters,
|
|
@@ -264,6 +598,26 @@ Track distances are sampled against the actual track model. When the ray starts
|
|
|
264
598
|
}
|
|
265
599
|
```
|
|
266
600
|
|
|
601
|
+
`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:
|
|
602
|
+
|
|
603
|
+
```js
|
|
604
|
+
sensors: {
|
|
605
|
+
rays: {
|
|
606
|
+
layout: 'driver-front-heavy',
|
|
607
|
+
channels: ['roadEdge', 'kerb', 'illegalSurface', 'car'],
|
|
608
|
+
precision: 'driver', // 'driver' | 'debug'
|
|
609
|
+
rays: [
|
|
610
|
+
{ id: 'front', angleDegrees: 0, lengthMeters: 260 },
|
|
611
|
+
{ id: 'right', angleDegrees: 90, lengthMeters: 80 },
|
|
612
|
+
],
|
|
613
|
+
},
|
|
614
|
+
}
|
|
615
|
+
```
|
|
616
|
+
|
|
617
|
+
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
|
+
|
|
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.
|
|
620
|
+
|
|
267
621
|
Nearby-car observations are car-relative:
|
|
268
622
|
|
|
269
623
|
```js
|
|
@@ -275,11 +629,16 @@ Nearby-car observations are car-relative:
|
|
|
275
629
|
relativeSpeedKph,
|
|
276
630
|
relativeHeadingRadians,
|
|
277
631
|
ahead,
|
|
632
|
+
behind,
|
|
278
633
|
sameLap,
|
|
634
|
+
closingRateMetersPerSecond,
|
|
635
|
+
timeToContactSeconds,
|
|
636
|
+
leftOverlap,
|
|
637
|
+
rightOverlap,
|
|
279
638
|
}
|
|
280
639
|
```
|
|
281
640
|
|
|
282
|
-
If no `reward` callback is provided, `result.reward` is `null`. If provided, rewards are returned by controlled driver ID.
|
|
641
|
+
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`.
|
|
283
642
|
|
|
284
643
|
Browser expert mode is opt-in through the browser mount API:
|
|
285
644
|
|
|
@@ -305,7 +664,7 @@ simulator.expert.step({
|
|
|
305
664
|
|
|
306
665
|
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.
|
|
307
666
|
|
|
308
|
-
`expert.visualizeSensors` is a browser-only visual debugging option. When `visualizeSensors: true` or `visualizeSensors: { rays: true }` is set, the simulator draws the controlled
|
|
667
|
+
`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.
|
|
309
668
|
|
|
310
669
|
## Lap Telemetry Snapshot
|
|
311
670
|
|
|
@@ -319,6 +678,8 @@ Each `car.lapTelemetry` snapshot includes current/last/best lap and sector timin
|
|
|
319
678
|
currentSectorElapsed,
|
|
320
679
|
currentSectorProgress,
|
|
321
680
|
currentSectors,
|
|
681
|
+
sectorProgress,
|
|
682
|
+
liveSectors,
|
|
322
683
|
lastLapTime,
|
|
323
684
|
bestLapTime,
|
|
324
685
|
lastSectors,
|
|
@@ -332,7 +693,7 @@ Each `car.lapTelemetry` snapshot includes current/last/best lap and sector timin
|
|
|
332
693
|
}
|
|
333
694
|
```
|
|
334
695
|
|
|
335
|
-
`overall-best` means fastest sector time currently known across the field, `personal-best` means that driver's own fastest non-overall sector, and `slower` marks a completed sector that is slower than the driver's personal best. Missing or
|
|
696
|
+
`currentSectors` contains completed split times for the current lap only, so the active sector remains `null` until its boundary is crossed and sectors after the active sector are cleared. `sectorProgress` is a `0..1` progress array for the live sector-map surface: recorded completed sectors before the active sector are `1`, the active sector is live, and missing or future sectors are `0`. `liveSectors` mirrors recorded completed current-lap sector times before the active sector and fills only the active sector with its live elapsed time. It does not infer earlier sector values purely from the current sector number, and it never displays future-sector values for the current lap, so skipped, missing, or stale split data stays blank instead of creating stale S1/S2 readouts. `overall-best` means fastest sector time currently known across the field, `personal-best` means that driver's own fastest non-overall sector, and `slower` marks a completed sector that is slower than the driver's personal best. Missing or future sector values are `null`.
|
|
336
697
|
|
|
337
698
|
## Required Options
|
|
338
699
|
|
|
@@ -340,9 +701,27 @@ Each `car.lapTelemetry` snapshot includes current/last/best lap and sector timin
|
|
|
340
701
|
|
|
341
702
|
`totalLaps` is optional. Values are normalized to a finite positive integer, with invalid or non-positive input falling back to a one-lap race.
|
|
342
703
|
|
|
343
|
-
`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. Calling `restart({ trackSeed })` rebuilds the simulation on the deterministic circuit for that seed.
|
|
704
|
+
`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.
|
|
705
|
+
|
|
706
|
+
`trackGeneration` is optional and applies only when a procedural `trackSeed` is used. It accepts profile presets plus explicit overrides:
|
|
344
707
|
|
|
345
|
-
|
|
708
|
+
```js
|
|
709
|
+
{
|
|
710
|
+
profile: 'training-short',
|
|
711
|
+
length: { minMeters: 900, maxMeters: 1800 },
|
|
712
|
+
startStraight: { gridMeters: 0, exitMeters: 80, blendMeters: 80 },
|
|
713
|
+
pitLane: { enabled: false },
|
|
714
|
+
shape: { scale: 0.2, cornerDensity: 1.3, variation: 0.22 },
|
|
715
|
+
validation: { minClearanceMultiplier: 1, maxLocalTurnRadians: 1.85 },
|
|
716
|
+
attempts: { primary: 80, fallback: 200 },
|
|
717
|
+
}
|
|
718
|
+
```
|
|
719
|
+
|
|
720
|
+
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.
|
|
721
|
+
|
|
722
|
+
`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.
|
|
723
|
+
|
|
724
|
+
`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.
|
|
346
725
|
|
|
347
726
|
Each driver must have:
|
|
348
727
|
|
|
@@ -424,12 +803,17 @@ Entries are optional. If omitted, defaults are used.
|
|
|
424
803
|
name: 'Ledger Racing',
|
|
425
804
|
color: '#00ff84',
|
|
426
805
|
icon: 'LR',
|
|
806
|
+
pitCrew: {
|
|
807
|
+
speed: 0.72,
|
|
808
|
+
consistency: 0.81,
|
|
809
|
+
reliability: 0.9,
|
|
810
|
+
},
|
|
427
811
|
},
|
|
428
812
|
}
|
|
429
813
|
```
|
|
430
814
|
|
|
431
815
|
Entries match drivers by `driverId`. Entry `driverId` values must be unique. `driverNumber` is optional; if omitted, PaddockJS falls back to stable grid order. When `driverNumber` is provided, values must be unique.
|
|
432
|
-
The car/driver overview primarily renders the existing driver and vehicle rating components from `driver` and `vehicle`. `team` is optional team-level metadata for race identity and
|
|
816
|
+
The car/driver overview primarily renders the existing driver and vehicle rating components from `driver` and `vehicle`. `team` is optional team-level metadata for race identity and pit behavior; `color` defaults to the driver/car color, and `icon` defaults from the team name or timing code. `team.pitCrew` may define `speed`, `consistency`, and `reliability` from `0` to `1`; these values affect optional pit-stop variability when `rules.modules.pitStops.variability.enabled` is true. The timing tower uses the team icon in the car/team column. `driver.customFields`, `vehicle.customFields`, and top-level driver `customFields` are accepted as extra metadata after those defined components.
|
|
433
817
|
|
|
434
818
|
## Rating Rules
|
|
435
819
|
|
|
@@ -470,7 +854,11 @@ Optional lifecycle callbacks:
|
|
|
470
854
|
}
|
|
471
855
|
```
|
|
472
856
|
|
|
473
|
-
`onRaceEvent` receives simulation events such as `contact`, `safety-car`, `green-flag`, `start-lights-out`, and `race-finish`. Host callback errors are caught; if `onError` exists, it receives `{ callback: name }` context for callback failures.
|
|
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.
|
|
858
|
+
|
|
859
|
+
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
|
+
|
|
861
|
+
Penalty status values are `issued`, `served`, `applied`, and `cancelled`. Time, position-drop, grid-drop, and disqualification consequences are immediate `applied` penalties. Drive-through and stop-go consequences are service penalties: they start as `issued`, can be completed with `servePenalty(penaltyId)`, and convert to applied time if unserved when final classification is calculated. Pit stops also serve eligible penalties before tire work starts: applied time penalties add their seconds as a hold, stop-go penalties add their configured service seconds, and drive-through penalties are marked served by the pit-lane traversal without extra stationary hold time.
|
|
474
862
|
|
|
475
863
|
## Asset Overrides
|
|
476
864
|
|
|
@@ -516,7 +904,7 @@ Current UI options:
|
|
|
516
904
|
```js
|
|
517
905
|
ui: {
|
|
518
906
|
layoutPreset: 'standard',
|
|
519
|
-
cameraControls: '
|
|
907
|
+
cameraControls: 'external',
|
|
520
908
|
showFps: true,
|
|
521
909
|
showTimingTower: true,
|
|
522
910
|
showTelemetry: true,
|
|
@@ -535,20 +923,27 @@ ui: {
|
|
|
535
923
|
raceDataBannerSize: 'custom',
|
|
536
924
|
raceDataTelemetryDetail: false,
|
|
537
925
|
timingTowerVerticalFit: 'expand-race-view',
|
|
926
|
+
},
|
|
927
|
+
debug: {
|
|
928
|
+
physicsModeIndicator: false,
|
|
538
929
|
}
|
|
539
930
|
```
|
|
540
931
|
|
|
541
|
-
- `layoutPreset`: `'standard'` or `'left-tower-overlay'`. The overlay preset creates a left broadcast gutter inside the race canvas, places the timing tower in that gutter at the same width as the default timing-board column, and
|
|
542
|
-
- `cameraControls`: `'embedded'`, `'external'`, or `false`. Embedded controls render inside the race canvas. External controls are mounted with `mountCameraControls(root)`. `false` leaves camera controls unrendered, though callers can still drive selection through controller methods.
|
|
932
|
+
- `layoutPreset`: `'standard'` or `'left-tower-overlay'`. The overlay preset creates a left broadcast gutter inside the race canvas, places the timing tower in that gutter at the same width as the default timing-board column, and frames the camera around the remaining race-view area. Camera controls are external by default. In the combined shell, the project/radio lower-third stays inside the race window and can render over the timing sidebar.
|
|
933
|
+
- `cameraControls`: `'embedded'`, `'external'`, or `false`. The default is external so camera controls do not cover the race view. Embedded controls render inside the race canvas only when explicitly requested. External controls are mounted with `mountCameraControls(root)` or included in package-owned workbench templates. The generated controls include mode buttons, zoom buttons, and a `Mute banners` toggle that temporarily disables project/radio lower-thirds while active. A browser-playback speed button is optional for normal camera controls through `ui.simulationSpeedControl: true`; the complete race workbench enables it by default and cycles `1x`, `2x`, `3x`, `4x`, `5x`, `10x`, then back to `1x`. `false` leaves camera controls unrendered, though callers can still drive selection through controller methods.
|
|
543
934
|
- `showFps`: controls whether the race canvas renders the FPS readout.
|
|
544
935
|
- `showRaceDataPanel`: controls whether the precombined shell includes the project/radio lower-third inside the race window.
|
|
545
936
|
- `showTimingTower`, `showTelemetry`: reserved component visibility flags for host layout decisions.
|
|
937
|
+
- `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.
|
|
546
938
|
- `telemetryIncludesOverview`: controls whether the telemetry stack template embeds the car/driver overview. Composable hosts can also pass `mountTelemetryPanel(root, { includeOverview: false })`.
|
|
547
939
|
- `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`.
|
|
548
940
|
- `raceDataBanners.initial`: `'project'`, `'radio'`, or `'hidden'`. This controls which lower-third appears first in the precombined shell.
|
|
549
941
|
- `raceDataBanners.enabled`: array containing `'project'` and/or `'radio'`. Disabled banner types never appear, including after driver selection.
|
|
942
|
+
- Project/radio lower-thirds include a package-owned top-right close button. Clicking it hides the current pill before its scheduled timeout and waits for the next normal driver selection or radio schedule before showing another pill. The camera-control `Mute banners` toggle is off by default and suppresses both project and radio lower-thirds until toggled off again; steward penalty banners are not part of this mute state.
|
|
550
943
|
- `raceDataBannerSize`: `'custom'` preserves the default lower-third geometry and exposes package CSS variables for host tuning. `'auto'` uses the race space to the right of the timing board when there is enough room and falls back to full lower-third overlap when there is not.
|
|
551
|
-
- `raceDataTelemetryDetail`: when `true`, the project lower-third includes compact S1/S2/S3 sector progress and timing readouts. Radio mode keeps the normal quote layout. The separate `mountTelemetrySectorBanner()` component remains available for hosts that explicitly want an independent sector banner.
|
|
944
|
+
- `raceDataTelemetryDetail`: when `true`, the project lower-third includes compact S1/S2/S3 sector progress and timing readouts and stays visible until the user dismisses it, banners are muted, or another banner state replaces it. Radio mode keeps the normal quote layout and schedule. The separate `mountTelemetrySectorBanner()` component remains available for hosts that explicitly want an independent sector banner.
|
|
945
|
+
- `penaltyBanners`: when `true`, the race view shows a top steward message for track-limit warnings and new steward penalty decisions. Time penalties show a large `+10s` style chip in the left block, with the affected car and rule/reason beside it. Warning-only messages use warning colors and remain separate from penalty decisions. It does not replace the project/radio lower-third.
|
|
946
|
+
- `timingPenaltyBadges`: when `true`, timing rows for penalized drivers show a red `!` badge with an accessible penalty label. Warning-only events do not count as penalties and do not show the badge.
|
|
552
947
|
- `timingTowerVerticalFit`: `'expand-race-view'` lets the combined race window grow to contain the timing tower. `'scroll'` keeps the race window height and scrolls the timing list inside the cropped tower. The same values can be passed to `mountRaceCanvas(root, { includeTimingTower: true, timingTowerVerticalFit })` for an embedded composable timing tower.
|
|
553
948
|
|
|
554
949
|
No UI option exists for raw timing-tower width, max width, or horizontal ratio. The timing tower is capped by the package CSS variable `--timing-board-max-width` because very wide timing boards read poorly. Host pages can scale the whole simulator by changing the mount container, but package-owned layout presets keep their internal proportions inside PaddockJS. For standalone timing towers, give the mount root a fixed height when a fixed vertical footprint is needed; the package keeps the frame inside that height and scrolls only the timing entries. Narrow hosts are handled internally: side-gutter timing towers become stacked/full-width, embedded timing towers stop behaving like desktop side overlays, and the camera safe area stops reserving a left gutter when the measured timing board is effectively full-width.
|
|
@@ -587,13 +982,33 @@ After every car completes `totalLaps`, `getSnapshot()` returns:
|
|
|
587
982
|
finishedAt: 123.4,
|
|
588
983
|
winner: { id, code, name, rank, finished },
|
|
589
984
|
classification: [
|
|
590
|
-
{ id, code, timingCode, name, rank, lap, lapsCompleted, distanceMeters, gapMeters, gapSeconds, intervalSeconds, finished, finishTime },
|
|
985
|
+
{ id, code, timingCode, name, rank, lap, lapsCompleted, distanceMeters, gapMeters, gapSeconds, intervalSeconds, gapLaps, intervalLaps, finished, finishTime, dnf, dnfReason, dnfAt, dnfOrder, penaltySeconds, adjustedFinishTime, positionDrop, disqualified },
|
|
591
986
|
],
|
|
592
987
|
},
|
|
593
988
|
}
|
|
594
989
|
```
|
|
595
990
|
|
|
596
|
-
Cars also include `team`, `speedKph`, `distanceMeters`, `gapAheadMeters`, `gapAheadSeconds`, `intervalAheadSeconds`, `leaderGapSeconds`, `finished`, `finishTime`, `classifiedRank`, and `lapTelemetry`. `gapAheadSeconds` and `intervalAheadSeconds` are the interval to the car directly ahead. `leaderGapSeconds` is the
|
|
991
|
+
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.
|
|
992
|
+
|
|
993
|
+
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.
|
|
994
|
+
|
|
995
|
+
Car snapshots expose `surface`, `signedOffset`, `crossTrackError`, `inPitLane`, `pitLanePart`, `pitBoxId`, and `pitLaneCrossTrackError` so hosts can distinguish main-circuit running from pit entry, fast lane, working lane, pit exit, and service-box states without recalculating geometry. `surface` is the worst current wheel/contact-patch surface, not only the center point. Each car also exposes `wheels`, with one entry per contact patch:
|
|
996
|
+
|
|
997
|
+
```js
|
|
998
|
+
{
|
|
999
|
+
id: 'front-left',
|
|
1000
|
+
x,
|
|
1001
|
+
y,
|
|
1002
|
+
signedOffset,
|
|
1003
|
+
crossTrackError,
|
|
1004
|
+
surface,
|
|
1005
|
+
onTrack,
|
|
1006
|
+
inPitLane,
|
|
1007
|
+
fullyOutsideWhiteLine,
|
|
1008
|
+
}
|
|
1009
|
+
```
|
|
1010
|
+
|
|
1011
|
+
Use `wheels` when a host needs exact debug overlays, per-wheel surface labels, or track-limit state. A car is track-limit illegal only when all four wheel contact patches are fully outside the same white line; a mixed state such as one wheel on gravel and one wheel still inside the white line reports the worse physics surface but does not count as a track-limit violation.
|
|
597
1012
|
|
|
598
1013
|
## Track And Lap Telemetry Snapshot
|
|
599
1014
|
|
|
@@ -607,6 +1022,21 @@ Every built track is automatically divided into three equal sectors. `snapshot.t
|
|
|
607
1022
|
]
|
|
608
1023
|
```
|
|
609
1024
|
|
|
1025
|
+
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
|
+
|
|
1027
|
+
Every built track also exposes `snapshot.track.pitLane`. The pit lane is deterministic for the track seed and contains:
|
|
1028
|
+
|
|
1029
|
+
- `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
|
+
- `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.
|
|
1031
|
+
- `mainLane`: straight pit-lane fast-lane start/end points, heading, and length.
|
|
1032
|
+
- `workingLane`: parallel pit box lane start/end points, offset, width, and centerline points.
|
|
1033
|
+
- `exit`: connector points from the pit lane back to the racing surface after the start line, plus the true track `edgePoint`, an overlapping lane-facing `trackConnectPoint` on the track surface, and a procedural `roadCenterline` that is tangent to the straight pit lane at pit-lane end and tangent to the main track at merge.
|
|
1034
|
+
- `teams`: team pit groups with id, name, color, index, the two assigned garage box ids, and one shared service area id.
|
|
1035
|
+
- `serviceAreas`: 10 team service areas, one per team, each with center, queue point, corners, queue corners, team index, and optional `teamId`, `teamName`, and `teamColor`.
|
|
1036
|
+
- `boxes`: 20 unused garage boxes as 10 team pairs, each with center, lane target, corners, team index, box index, and optional `teamId`, `teamName`, and `teamColor`.
|
|
1037
|
+
|
|
1038
|
+
Pit-lane road and box states are legal drivable surfaces. Track state may report `surface: 'pit-entry'`, `'pit-lane'`, `'pit-exit'`, or `'pit-box'` with `inPitLane: true`; `crossTrackError` remains the distance from the main race track for compatibility, while pit-specific offsets are exposed as pit-lane fields on the internal state.
|
|
1039
|
+
|
|
610
1040
|
Each car exposes `lapTelemetry`:
|
|
611
1041
|
|
|
612
1042
|
```js
|
|
@@ -617,6 +1047,8 @@ Each car exposes `lapTelemetry`:
|
|
|
617
1047
|
currentSectorElapsed,
|
|
618
1048
|
currentSectorProgress,
|
|
619
1049
|
currentSectors: [s1Time, s2Time, s3Time],
|
|
1050
|
+
sectorProgress: [s1Progress, s2Progress, s3Progress],
|
|
1051
|
+
liveSectors: [s1LiveTime, s2LiveTime, s3LiveTime],
|
|
620
1052
|
lastLapTime,
|
|
621
1053
|
bestLapTime,
|
|
622
1054
|
lastSectors: [s1Time, s2Time, s3Time],
|
|
@@ -625,7 +1057,7 @@ Each car exposes `lapTelemetry`:
|
|
|
625
1057
|
}
|
|
626
1058
|
```
|
|
627
1059
|
|
|
628
|
-
Times are seconds or `null` when no timing exists yet. `currentSectorProgress` is `0..1` within the active sector.
|
|
1060
|
+
Times are seconds or `null` when no timing exists yet. `currentSectorProgress` is `0..1` within the active sector. `sectorProgress` is the live fill state for the current lap: completed sectors before the active sector are filled only when a real split was recorded, the active sector progresses live, and missing/future sectors stay empty. `currentSectors` contains only completed current-lap split times before the active sector, while `liveSectors` combines those recorded completed splits with the active sector's elapsed time and clears any stale future-sector values.
|
|
629
1061
|
|
|
630
1062
|
## Unit Conversion
|
|
631
1063
|
|
|
@@ -637,7 +1069,7 @@ The simulation keeps its internal physics in simulator units. Public speed and d
|
|
|
637
1069
|
- `simSpeedToMetersPerSecond(simUnitsPerSecond)`
|
|
638
1070
|
- `kphToSimSpeed(kph)`
|
|
639
1071
|
|
|
640
|
-
The current calibrated speed scale maps `VEHICLE_LIMITS.maxSpeed` to an F1-like `330 km/h`. Rendered car sprite size remains a visual scale and is intentionally larger than physical car length for readability.
|
|
1072
|
+
All public timer, lap-time, sector-time, gap-time, penalty-time, and service-countdown values are seconds. Public distance values with a `Meters` suffix are meters. Internal cumulative fields such as `raceDistance` stay in simulator units and must be converted before being presented as physical distance. The current calibrated speed scale maps `VEHICLE_LIMITS.maxSpeed` to an F1-like `330 km/h`. Rendered car sprite size remains a visual scale and is intentionally larger than physical car length for readability.
|
|
641
1073
|
|
|
642
1074
|
## Returned Controller
|
|
643
1075
|
|
|
@@ -651,24 +1083,32 @@ Controller methods:
|
|
|
651
1083
|
- `restart(nextOptions)`: restarts the simulation with merged non-asset options. Use `destroy()` and mount a new simulator to change bundled or host-provided asset URLs.
|
|
652
1084
|
- `selectDriver(driverId)`: selects and focuses a driver.
|
|
653
1085
|
- `setSafetyCarDeployed(deployed)`: toggles safety car state.
|
|
1086
|
+
- `setRedFlagDeployed(deployed)`: freezes or releases the race under red-flag race control.
|
|
1087
|
+
- `setPitLaneOpen(open)`: opens or closes the pit lane for new pending pit entries.
|
|
654
1088
|
- `callSafetyCar()`: deploys the safety car.
|
|
655
1089
|
- `clearSafetyCar()`: releases the safety car.
|
|
656
1090
|
- `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.
|
|
1092
|
+
- `getPitIntent(driverId)`: reads the current pit intent.
|
|
1093
|
+
- `getPitTargetCompound(driverId)`: reads the current pit target tire.
|
|
1094
|
+
- `getSimulationSpeed()`: returns the active browser playback multiplier from the package-owned simulation-speed control, defaulting to `1`.
|
|
1095
|
+
- `servePenalty(penaltyId)`: marks an issued drive-through or stop-go penalty as served.
|
|
1096
|
+
- `cancelPenalty(penaltyId)`: cancels a penalty so it no longer affects service, timing, grid, or classification.
|
|
657
1097
|
- `getSnapshot()`: returns the latest simulation snapshot.
|
|
658
1098
|
|
|
659
1099
|
Composable controllers additionally expose:
|
|
660
1100
|
|
|
661
1101
|
- `mountRaceControls(root)`: renders the top control/header component.
|
|
662
|
-
- `mountCameraControls(root)`: renders package-owned camera mode and
|
|
1102
|
+
- `mountCameraControls(root)`: renders package-owned camera mode, zoom, and project/radio banner mute controls outside the race canvas.
|
|
663
1103
|
- `mountSafetyCarControl(root)`: renders a package-owned safety-car button that binds to the same race-control state as other safety buttons.
|
|
664
|
-
- `mountTimingTower(root)`: renders the timing tower component.
|
|
665
|
-
- `mountRaceCanvas(root, { includeRaceDataPanel, includeTimingTower, includeTelemetrySectorBanner, timingTowerVerticalFit })`: renders the PixiJS canvas host, optional FPS, start lights, and
|
|
666
|
-
- `mountTelemetryPanel(root, { includeOverview })`: renders the package-owned telemetry stack template. The stack is only a composition of detached telemetry surfaces
|
|
1104
|
+
- `mountTimingTower(root)`: renders the timing tower component. The tower includes one hidden race-control status banner slot above the timing rows; `raceControl.mode: 'safety-car'` shows the yellow safety-car status, and `raceControl.mode: 'red-flag'` shows the red red-flag status.
|
|
1105
|
+
- `mountRaceCanvas(root, { includeRaceDataPanel, includeTimingTower, includeTelemetrySectorBanner, timingTowerVerticalFit })`: renders the PixiJS canvas host, optional FPS, start lights, and the top steward message. Camera controls are external by default and render inside the race canvas only when `ui.cameraControls: 'embedded'` is explicitly requested. Pass `includeRaceDataPanel: true` to place the project/radio lower-third inside the race window so it shares race-canvas clipping and layering. Pass `includeTelemetrySectorBanner: true` only when the host intentionally wants the independent sector lower-third in addition to the project/radio banner. Pass `includeTimingTower: true` to place the timing tower inside the race canvas; `timingTowerVerticalFit: 'expand-race-view'` grows the canvas to the tower height, while `'scroll'` keeps the canvas height and scrolls timing rows inside the tower frame. This is required before `start()`.
|
|
1106
|
+
- `mountTelemetryPanel(root, { includeOverview })`: renders the package-owned telemetry stack template. The stack is only a composition of detached telemetry surfaces, owns vertical scrolling when its host is shorter than its contents, and includes the car/driver overview by default unless `includeOverview: false` is passed or `ui.telemetryIncludesOverview` is `false`.
|
|
667
1107
|
- `mountTelemetryCore(root)`: renders selected-car scalar telemetry only.
|
|
668
1108
|
- `mountTelemetrySectors(root)`: renders the live sector progress graph only.
|
|
669
1109
|
- `mountTelemetryLapTimes(root)`: renders current, last, and best lap timing only.
|
|
670
1110
|
- `mountTelemetrySectorTimes(root)`: renders last and best sector timing only.
|
|
671
|
-
- `mountRaceTelemetryDrawer(root, { timingTowerVerticalFit, drawerInitiallyOpen, raceDataTelemetryDetail })`: renders a template that combines race canvas, embedded timing tower, the project/radio lower-third, safety-car control, and a right-side telemetry drawer. 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
|
|
1111
|
+
- `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.
|
|
672
1112
|
- `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.
|
|
673
1113
|
- `mountRaceDataPanel(root)`: renders the project/race-data lower-third as a separate component for hosts that intentionally want it outside the race canvas.
|
|
674
1114
|
- `start()`: initializes PixiJS, binds mounted controls, and starts the simulation loop.
|