@inventure71/paddockjs 2.0.0 → 3.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 +37 -1
- package/INSTALL_AND_UPDATE.md +10 -1
- package/README.md +92 -17
- package/docs/architecture.md +26 -16
- package/docs/component_inventory.md +72 -0
- package/docs/concepts.md +7 -5
- package/docs/custom_model_controller.md +2 -2
- package/docs/data_contract.md +121 -33
- package/docs/data_model.md +1 -1
- package/docs/index.md +10 -0
- package/docs/learnings.md +6 -6
- package/docs/rules.md +39 -9
- package/docs/sense_contract.md +59 -0
- package/docs/system_specs.md +76 -20
- package/docs/training.md +27 -20
- package/package.json +1 -1
- package/src/api/PaddockSimulatorController.js +75 -36
- package/src/app/BrowserExpertAdapter.js +9 -300
- package/src/app/BrowserExpertExternalRenderer.js +303 -0
- package/src/app/F1SimulatorApp.js +190 -68
- package/src/app/banners/raceDataBannerController.js +71 -36
- package/src/app/banners/stewardMessageController.js +5 -4
- package/src/app/camera/cameraConstants.js +2 -0
- package/src/app/camera/cameraController.js +86 -29
- package/src/app/camera/cameraGeometry.js +12 -1
- package/src/app/domBindings.js +89 -9
- package/src/app/layoutScheduler.js +27 -0
- package/src/app/layoutSupport.js +165 -0
- package/src/app/raceOverlayClearanceSupport.js +158 -0
- package/src/app/readouts/carOverviewRenderer.js +16 -8
- package/src/app/readouts/controlStateRenderer.js +1 -1
- package/src/app/readouts/raceStatusRenderer.js +30 -21
- package/src/app/readouts/readoutFormatters.js +2 -0
- package/src/app/readouts/telemetryRenderer.js +17 -8
- package/src/app/readouts/timingTowerRenderer.js +62 -10
- package/src/app/runtime/runtimeVisibility.js +66 -5
- package/src/config/cssValues.js +16 -0
- package/src/config/defaultOptions.js +51 -38
- package/src/config/restartOptions.js +34 -0
- package/src/config/themeOptions.js +825 -0
- package/src/config/timingGapMode.js +13 -0
- package/src/config/urlOptions.js +25 -0
- package/src/data/championship.js +14 -9
- package/src/environment/controllerLoop.js +34 -5
- package/src/environment/episode.js +14 -2
- package/src/environment/index.d.ts +17 -4
- package/src/environment/observationVector.js +200 -0
- package/src/environment/observations.js +58 -285
- package/src/environment/options.js +1 -1
- package/src/environment/recorder.js +13 -6
- package/src/environment/rewards.js +13 -2
- package/src/environment/runtime.js +65 -25
- package/src/environment/scenarios.js +22 -3
- package/src/environment/sensors/index.js +3 -3
- package/src/environment/sensors/opponentRadar.js +13 -3
- package/src/environment/sensors/sensorTargets.js +8 -2
- package/src/environment/sensors/surfaceRays.js +84 -12
- package/src/environment/sensors/trackRays.js +173 -15
- package/src/environment/specs.js +78 -40
- package/src/index.d.ts +112 -9
- package/src/index.js +26 -33
- package/src/simulation/driver/driverController.js +1 -1
- package/src/simulation/driver/edgeRecovery.js +1 -1
- package/src/simulation/driver/racingControls.js +2 -2
- package/src/simulation/driver/rejoinControls.js +2 -2
- package/src/simulation/pit/pitFlow.js +5 -0
- package/src/simulation/pit/pitKinematics.js +24 -0
- package/src/simulation/pit/pitQueue.js +5 -0
- package/src/simulation/pit/pitRouteMovement.js +7 -0
- package/src/simulation/pit/pitRouting.js +2 -4
- package/src/simulation/pit/pitService.js +2 -0
- package/src/simulation/pit/pitTireService.js +2 -0
- package/src/simulation/race/raceLifecycle.js +2 -6
- package/src/simulation/race/raceProgress.js +6 -1
- package/src/simulation/race/raceSetup.js +2 -6
- package/src/simulation/race/raceStep.js +2 -2
- package/src/simulation/race/redFlag.js +3 -3
- package/src/simulation/race/safetyCar.js +10 -4
- package/src/simulation/race/stalledDnf.js +8 -17
- package/src/simulation/raceSimulation.js +1 -0
- package/src/simulation/rules/rulesReview.js +3 -0
- package/src/simulation/snapshots/raceSnapshots.js +1 -0
- package/src/simulation/timing/lapTelemetry.js +28 -9
- package/src/simulation/track/pitLaneLayout.js +7 -3
- package/src/simulation/track/spatialQueries.js +5 -28
- package/src/simulation/track/trackQueryGrid.js +94 -0
- package/src/simulation/track/trackQueryIndex.js +193 -434
- 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/trackStatePolicy.js +0 -1
- package/src/simulation/vehicle/contactResolution.js +67 -2
- package/src/simulation/vehicle/runoffResponse.js +11 -12
- package/src/simulation/vehicle/vehicleKinematics.js +37 -0
- package/src/simulation/vehicle/vehiclePhysics.js +5 -4
- package/src/simulation/vehicle/vehicleSnapshots.js +22 -0
- package/src/simulation/vehicle/vehicleState.js +36 -2
- package/src/simulation/vehicle/wheelSurface.js +3 -1
- package/src/simulation/warmup/runtimeWarmup.js +0 -1
- package/src/styles.css +1185 -136
- package/src/ui/bannerTemplates.js +38 -28
- package/src/ui/cameraControlsTemplate.js +16 -5
- package/src/ui/carOverviewTemplate.js +12 -5
- package/src/ui/raceCanvasTemplate.js +31 -8
- package/src/ui/raceControlsTemplate.js +27 -10
- package/src/ui/shellTemplate.js +3 -1
- package/src/ui/telemetryDrawerTemplate.js +23 -10
- package/src/ui/telemetryTemplates.js +58 -32
- package/src/ui/templateUtils.js +48 -0
- package/src/ui/timingTowerTemplate.js +29 -12
package/docs/training.md
CHANGED
|
@@ -46,6 +46,7 @@ The current expert API is a JavaScript environment contract. It supports:
|
|
|
46
46
|
- reset-only scenario placement through `scenario.preset`, `scenario.placements`, `scenario.traffic`, and episode-boundary `resetDrivers(placements)`
|
|
47
47
|
- multi-car interaction profiles through `participantInteractions`, including no-collision training cars that are hidden from other cars' ray and nearby-car sensors by default
|
|
48
48
|
- per-driver episode state and neutral step metrics for batched training loops
|
|
49
|
+
- `episode.maxSteps` as a controlled-driver episode safety horizon; the default is high for browser/expert usability, so training and evaluation loops should set an explicit smaller value
|
|
49
50
|
- neutral rollout recording through `createRolloutRecorder()`
|
|
50
51
|
- deterministic evaluation metrics through `runEnvironmentEvaluation()` and `createEvaluationTracker()`
|
|
51
52
|
- a JSON-serializable worker protocol wrapper through `createEnvironmentWorkerProtocol()`
|
|
@@ -56,7 +57,7 @@ The current expert API is a JavaScript environment contract. It supports:
|
|
|
56
57
|
|
|
57
58
|
The environment also accepts `rules` as a narrow override of the existing race rules, such as `standingStart: false` for training loops. Rule overrides change simulator behavior; keep them fixed when comparing policy runs.
|
|
58
59
|
|
|
59
|
-
`reward(context)` is a user-owned formula over package-owned facts. The context includes the same neutral `metrics` and per-driver `episode` state returned by each step result, so training code does not need to re-derive legality, destruction, or episode termination from raw snapshots:
|
|
60
|
+
`reward(context)` is a user-owned formula over package-owned facts. The context includes the same neutral `metrics` and per-driver `episode` state returned by each step result, so training code does not need to re-derive legality, destruction, or episode termination from raw snapshots. Reward callbacks run only for `step(actions)` transition results; reset/read calls are reward-neutral:
|
|
60
61
|
|
|
61
62
|
```js
|
|
62
63
|
const env = createPaddockEnvironment({
|
|
@@ -144,7 +145,7 @@ Controlled drivers do not receive tire-threshold automatic pit calls from the bu
|
|
|
144
145
|
|
|
145
146
|
`observation.object.self.onTrack` follows the simulator's wheel-level legality rules: track, kerb, and legal pit-lane/box surfaces are on-track for reward/observation purposes, while gravel, grass, and barrier surfaces are off-track. Invalid `observation.lookaheadMeters` values fall back to `[20, 50, 100, 150]` so fixed observation schemas stay usable.
|
|
146
147
|
|
|
147
|
-
Use `observation.profile: 'physical-driver'` when training a policy that should rely on local driver-like senses instead of privileged future track data. This profile keeps the normal action contract, exposes yaw rate, local left/right boundary distance, four contact-patch surface readings, richer opponent radar fields, and surface-aware ray channels in the vector schema. Unless `lookaheadMeters` is explicitly provided, the physical-driver profile uses no track lookahead samples.
|
|
148
|
+
Use `observation.profile: 'physical-driver'` when training a policy that should rely on local driver-like senses instead of privileged future track data. This profile keeps the normal action contract, exposes yaw rate, local left/right boundary distance, four contact-patch surface readings, richer opponent radar fields, and surface-aware ray channels in the vector schema. Compact vectors include contact-patch surface code, signed offset, and cross-track error so vector-only loops keep the same contact-patch facts as object observations. Unless `lookaheadMeters` is explicitly provided, the physical-driver profile uses no track lookahead samples.
|
|
148
149
|
|
|
149
150
|
`observation.object.self.appliedControls` is diagnostic/reference data for the
|
|
150
151
|
latest physics step. For controlled cars it mirrors the normalized action after
|
|
@@ -155,15 +156,15 @@ built-in driver applied. This is intended for audits or clean local imitation
|
|
|
155
156
|
datasets; it does not blend built-in AI with a model during normal controlled
|
|
156
157
|
stepping.
|
|
157
158
|
|
|
158
|
-
For model training, prefer `physicsMode: '
|
|
159
|
+
For model training, prefer `physicsMode: 'advanced'` so the policy learns against the same grip, yaw-rate, contact-patch, kerb, and runoff behavior exposed by those senses. Arcade physics can still be useful as a debugging baseline, but it should not be treated as the main training target for realistic driver policies.
|
|
159
160
|
|
|
160
|
-
Surface-aware rays are opt-in because they are more expensive than the default road-edge/car channels. The environment normalizes both the
|
|
161
|
+
Surface-aware rays are opt-in because they are more expensive than the default road-edge/car channels. The environment normalizes both the compact ray shape and the newer per-ray layout:
|
|
161
162
|
|
|
162
163
|
```js
|
|
163
164
|
const env = createPaddockEnvironment({
|
|
164
165
|
drivers,
|
|
165
166
|
controlledDrivers: ['budget'],
|
|
166
|
-
physicsMode: '
|
|
167
|
+
physicsMode: 'advanced',
|
|
167
168
|
observation: { profile: 'physical-driver' },
|
|
168
169
|
sensors: {
|
|
169
170
|
rays: {
|
|
@@ -183,9 +184,13 @@ Only requested ray channels are computed. The default compact config still compu
|
|
|
183
184
|
|
|
184
185
|
`precision: 'driver'` is the default and the recommended model-facing ray contract. It returns the normal sampled driver-sensor distances without extra refinement. `precision: 'debug'` exists only for clearly labeled diagnostics with additional edge refinement. If the Policy Runner or expert visualization is showing model senses, it must render the active observation values exactly and must not replace them with debug-precision readings.
|
|
185
186
|
|
|
186
|
-
|
|
187
|
+
When a full observation includes both `object` and `vector`, Policy Runner model inputs that need ray geometry use the observation object's actual `angleDegrees` and `lengthMeters`. Compact vector schemas intentionally carry ray channel values, not out-of-band ray geometry, so consumers with per-driver sensor overrides must read the active object observation or the exact per-driver spec instead of reconstructing layout from a default preset.
|
|
187
188
|
|
|
188
|
-
|
|
189
|
+
The model-facing sense proof boundary is documented in [Model Sense Contract](sense_contract.md). The executable contract test compares object observations against independent simulator-snapshot oracles, decodes vector entries by schema name, and checks compact vector-only output against full output for the same deterministic state. New model-facing sense fields must extend that oracle instead of relying only on snapshot or shape tests.
|
|
190
|
+
|
|
191
|
+
Track-position, wheel-surface, pit-lane, and ray queries use the internal startup-built track query index. Browser/expert mounts, headless environments, direct race-simulation construction, compact vector-only training loops, and `batch-training` ray runs all use that canonical indexed path. This does not change the observation shape. Ray `precision: 'driver'` uses indexed projection plus driver-precision validation for road-edge, kerb, and illegal-surface distances, preserving zero-distance origin surface hits and avoiding debug-only refinement in policy inputs. Debug precision remains available only for explicitly labeled diagnostics.
|
|
192
|
+
|
|
193
|
+
`resetDrivers()` is part of episode control, not policy action. After reset placement, the environment immediately reclassifies the selected cars against the same runoff/barrier rules used during stepping before building observations. Recovery starts inside the legal recovery band remain normal physical starts. Placements that are already inside the barrier destruction boundary return a terminal per-driver episode state and miss-valued rays, so a batch loop can assign its own negative reward and reset that driver without paying for far-out ray geometry. In multi-driver batches, top-level `done` is true once every reported controlled driver is terminal, including mixed states where one driver is terminated and another is max-step truncated; per-driver `info.drivers[driverId]` keeps the distinct reason.
|
|
189
194
|
|
|
190
195
|
## Headless Training Loop
|
|
191
196
|
|
|
@@ -263,7 +268,7 @@ In the browser renderer, no-collision participants still look like solid cars, b
|
|
|
263
268
|
|
|
264
269
|
Sensor hiding is per target car. If `model-b` is `batch-training`, then `model-a`'s rays and `nearbyCars` ignore `model-b`. If every training car should be invisible to every other training car, set `participantInteractions.defaultProfile: 'batch-training'`.
|
|
265
270
|
|
|
266
|
-
Replay ghosts are also sensor-hidden by default. If a replay reference should be visible to policy sensors for comparison experiments, opt it in through its own `sensors.detectableByRays` or `sensors.detectableAsNearby` flags. Sensor results mark those targets as replay ghosts; they still do not collide, rank, pit, or receive controls.
|
|
271
|
+
Replay ghosts are also sensor-hidden by default. If a replay reference should be visible to policy sensors for comparison experiments, opt it in through its own `sensors.detectableByRays` or `sensors.detectableAsNearby` flags. Sensor results mark those targets as replay ghosts; they still do not collide, rank, pit, or receive controls. Nearby-car radar enrichment keys targets by both entity type and id, so an opt-in ghost with the same id as a real car cannot overwrite the real car's velocity-derived radar facts.
|
|
267
272
|
|
|
268
273
|
If you need a non-colliding car that still appears in sensors, use `phantom-race` instead. If you need a one-off exception, override the flag explicitly:
|
|
269
274
|
|
|
@@ -281,7 +286,7 @@ participantInteractions: {
|
|
|
281
286
|
|
|
282
287
|
Use that override deliberately. The default no-collision training profile is sensor-hidden because parallel training rollouts usually should not alter each other's observation tensors.
|
|
283
288
|
|
|
284
|
-
Compact vector mode is intended for high-throughput loops. `env.getObservationSpec()` remains the canonical schema source, so external code can request `output: 'vector'` and `includeSchema: false` without serializing object observations and schema data on every step. JavaScript training loops can also request `vectorType: 'float32'` for typed numeric buffers. Keep the default array output for JSON-only bridges unless the bridge explicitly packs typed arrays.
|
|
289
|
+
Compact vector mode is intended for high-throughput loops. `env.getObservationSpec()` remains the canonical schema source, so external code can request `output: 'vector'` and `includeSchema: false` without serializing object observations and schema data on every step. If `sensorsByDriver` changes ray or nearby-car shape for specific drivers, read `observationSpec.perDriver[driverId].vector.schema` for that driver's exact compact schema. JavaScript training loops can also request `vectorType: 'float32'` for typed numeric buffers. Keep the default array output for JSON-only bridges unless the bridge explicitly packs typed arrays.
|
|
285
290
|
|
|
286
291
|
Use `result.stateOutput` to avoid returning more state than the loop needs:
|
|
287
292
|
|
|
@@ -298,16 +303,18 @@ const env = createPaddockEnvironment({
|
|
|
298
303
|
});
|
|
299
304
|
```
|
|
300
305
|
|
|
301
|
-
`stateOutput: 'minimal'` returns the lean public observation snapshot. `stateOutput: 'none'` returns `state: null`. The default is still `full` for existing callers. When `stateOutput: 'none'`, `observation.output: 'vector'`, and `includeSchema: false`, the environment uses an internal compact training snapshot before building observations and metrics. That optimization is not exposed as policy state
|
|
306
|
+
`stateOutput: 'minimal'` returns the lean public observation snapshot. `stateOutput: 'none'` returns `state: null`. The default is still `full` for existing callers. `resetDriversObservationScope: 'reset'` also scopes `info.controlledDrivers` to the drivers present in that reset result's `observation` and `metrics`; use `info.drivers` if you need all configured drivers' episode bookkeeping. When `stateOutput: 'none'`, `observation.output: 'vector'`, and `includeSchema: false`, the environment uses an internal compact training snapshot before building observations and metrics. That optimization is not exposed as policy state. Reward callbacks still receive the documented reward context on `step(actions)`, and reset/read calls remain reward-neutral.
|
|
307
|
+
|
|
308
|
+
Advanced-mode velocity senses are authoritative only when the simulator owns a real world velocity. Arcade snapshots intentionally expose `velocityX/Y` as `null`. When advanced-mode cars are terminal or held by race control, grid logic, or pit queue logic, the environment freezes scalar speed and `velocityX/Y` together at zero instead of carrying old motion into observations or recorded transitions.
|
|
302
309
|
|
|
303
|
-
On the local 20-car batch-training benchmark used for
|
|
310
|
+
On the local 20-car batch-training benchmark used for the original index work (`physicsMode: 'advanced'`, `frameSkip: 4`, `physical-driver`, `driver-front-heavy` rays), the measured environment action cost after compact output and indexed track queries was approximately:
|
|
304
311
|
|
|
305
312
|
```txt
|
|
306
|
-
nearest track states 1.52x faster than
|
|
307
|
-
pit lane states 2.02x faster than
|
|
308
|
-
sampled high-curvature off-track rays 14.81x faster than
|
|
309
|
-
wheel surface near pit connector 1.12x faster than
|
|
310
|
-
20-car batch recovery environment 1.03x faster than
|
|
313
|
+
nearest track states 1.52x faster than the pre-indexed path
|
|
314
|
+
pit lane states 2.02x faster than the pre-indexed path
|
|
315
|
+
sampled high-curvature off-track rays 14.81x faster than the pre-indexed path
|
|
316
|
+
wheel surface near pit connector 1.12x faster than the pre-indexed path
|
|
317
|
+
20-car batch recovery environment 1.03x faster than the pre-indexed path
|
|
311
318
|
```
|
|
312
319
|
|
|
313
320
|
These numbers are hardware- and track-dependent, but the relative result is the useful guidance: compact state/observation output reduces serialization/allocation overhead, and indexed track queries prevent off-track recovery rays from becoming the dominant cost center.
|
|
@@ -362,7 +369,7 @@ env.resetDrivers({
|
|
|
362
369
|
});
|
|
363
370
|
```
|
|
364
371
|
|
|
365
|
-
`resetDrivers()` is an episode-boundary API. It increments that driver's `episodeId`, resets its `episodeStep`, clears manual controls and pit intent, and places the car through the same simulator state-reset path used by scenarios. It does not rewind global `info.step`; max-step truncation is evaluated from the selected driver's own `episodeStep`, so one truncated driver can be reset while the rest of the batch keeps its current episode state. It is not available through `step(actions)` and is not a movement shortcut during an episode. Passing `{ observationScope: 'reset', stateOutput: 'none' }` keeps reset responses small for batched runners.
|
|
372
|
+
`resetDrivers()` is an episode-boundary API. It increments that driver's `episodeId`, resets its `episodeStep`, clears manual controls and pit intent, and places the car through the same simulator state-reset path used by scenarios. It does not rewind global `info.step`; max-step truncation is evaluated from the selected driver's own `episodeStep`, so one truncated driver can be reset while the rest of the batch keeps its current episode state. It is not available through `step(actions)` and is not a movement shortcut during an episode. Passing `{ observationScope: 'reset', stateOutput: 'none' }` keeps reset responses small for batched runners. Even with reset-only observation scope, `info.drivers` remains per-driver: a reset driver's terminal placement does not mark non-reset drivers as terminated or truncated. The controller loop keeps its last full observation cache and merges reset-only observations into it before the next decision, so batched controllers still receive an observation for every controlled driver after a partial reset.
|
|
366
373
|
|
|
367
374
|
The worker protocol mirrors this with a `resetDrivers` message:
|
|
368
375
|
|
|
@@ -396,15 +403,15 @@ const next = env.step(action);
|
|
|
396
403
|
recorder.recordStep(result, action, next);
|
|
397
404
|
```
|
|
398
405
|
|
|
399
|
-
Each transition has `{ observation, action, reward, nextObservation, terminated, truncated, info }`. If the environment has no reward callback, `reward` is still `null`; the recorder does not invent one.
|
|
406
|
+
Each transition has `{ observation, action, reward, nextObservation, terminated, truncated, info }`. If the environment has no reward callback, `reward` is still `null`; the recorder does not invent one. Recorded transitions snapshot arrays, typed vectors, actions, observations, rewards, and info at record time, so later policy-loop mutations do not rewrite previous rollout rows.
|
|
400
407
|
|
|
401
|
-
Environment step results also include reward-neutral `metrics[driverId]` facts such as progress delta, legal progress delta, kerb use, illegal/off-track state, severe cuts, destruction state, under-30-kph state, spin/backwards/instability state, lap completion, lap time, and contact count. `spinOrBackwards` is true for large heading-error backwards cases and for active simulator instability labels such as `spin-risk`, `understeer`, `oversteer`, and `destroyed`. `contactCount` counts each normalized physical contact event once per involved driver even when the event carries
|
|
408
|
+
Environment step results also include reward-neutral `metrics[driverId]` facts such as progress delta, legal progress delta, kerb use, illegal/off-track state, severe cuts, destruction state, under-30-kph state, spin/backwards/instability state, lap completion, lap time, and contact count. `spinOrBackwards` is true for large heading-error backwards cases and for active simulator instability labels such as `spin-risk`, `understeer`, `oversteer`, and `destroyed`. `contactCount` counts each normalized physical contact event once per involved driver even when the event carries older `carId`/`otherCarId` fields and normalized `driverIds`. These metrics are facts for logging, evaluation, and user-defined reward functions; PaddockJS does not turn them into a built-in objective. If a car touches the rendered barrier wall's inner face in either physics mode, that driver receives `metrics[driverId].destroyed: true` and `info.drivers[driverId].endReason: 'destroyed'`; external training code should assign any super-negative crash reward itself and then call `resetDrivers()` for the next episode. If opt-in `rules.modules.stalledDnf.enabled: true` retires a controlled car for being stuck off legal surfaces, the episode terminates with `endReason: 'stalled-off-track'` while `metrics[driverId].destroyed` stays `false`.
|
|
402
409
|
|
|
403
410
|
Deterministic evaluation helpers report simulator quality metrics such as distance, off-track steps, contacts, recovery success, pass count, and first lap time when available. They use the same wheel-level legal-surface and contact-event semantics 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)` calls require a result with `state.snapshot`, so use `stateOutput: 'minimal'` or `full` for manual tracker wiring. Evaluation helpers do not update a model or compute rewards.
|
|
404
411
|
|
|
405
412
|
## Example Rewards
|
|
406
413
|
|
|
407
|
-
`createProgressReward()` is exported for examples and quick smoke tests, but it is non-canonical demo code. It is not the official reward function and should not be treated as the recommended objective for every user. Real training code should pass a domain-specific `reward(context)` or omit rewards entirely when collecting observations or imitation data.
|
|
414
|
+
`createProgressReward()` is exported for examples and quick smoke tests, but it is non-canonical demo code. It is not the official reward function and should not be treated as the recommended objective for every user. It derives speed and legal-surface facts from the reward snapshot and metrics when object observations are omitted, so compact vector-only runs do not change the demo reward for the same transition. Real training code should pass a domain-specific `reward(context)` or omit rewards entirely when collecting observations or imitation data.
|
|
408
415
|
|
|
409
416
|
## Visual Playback Loop
|
|
410
417
|
|
package/package.json
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { F1SimulatorApp } from '../app/F1SimulatorApp.js';
|
|
2
|
+
import { installLayoutSupport } from '../app/layoutSupport.js';
|
|
2
3
|
import {
|
|
3
4
|
createRaceDataPanelMarkup,
|
|
4
5
|
createTelemetrySectorBannerMarkup,
|
|
@@ -18,6 +19,9 @@ import {
|
|
|
18
19
|
createTimingTowerMarkup,
|
|
19
20
|
} from '../ui/componentTemplates.js';
|
|
20
21
|
import { applyPaddockThemeCssVariables, resolveF1SimulatorOptions } from '../config/defaultOptions.js';
|
|
22
|
+
import { formatCssUrl } from '../config/cssValues.js';
|
|
23
|
+
import { mergeRestartOptions } from '../config/restartOptions.js';
|
|
24
|
+
import { getNextTimingGapMode, normalizeTimingGapMode } from '../config/timingGapMode.js';
|
|
21
25
|
|
|
22
26
|
function assertMountTarget(root, label) {
|
|
23
27
|
if (!root || typeof root !== 'object' || !('innerHTML' in root)) {
|
|
@@ -27,42 +31,10 @@ function assertMountTarget(root, label) {
|
|
|
27
31
|
|
|
28
32
|
function setPackageCssVariables(root, assets, theme) {
|
|
29
33
|
root.classList?.add?.('f1-sim-component');
|
|
30
|
-
root.style?.setProperty?.('--broadcast-panel-surface',
|
|
34
|
+
root.style?.setProperty?.('--broadcast-panel-surface', formatCssUrl(assets.broadcastPanel));
|
|
31
35
|
applyPaddockThemeCssVariables(root, theme);
|
|
32
36
|
}
|
|
33
37
|
|
|
34
|
-
function mergeResolvedOptions(previousOptions, nextOptions) {
|
|
35
|
-
const resetFromPreset = Object.hasOwn(nextOptions, 'preset');
|
|
36
|
-
const previousUi = resetFromPreset ? {} : previousOptions.ui;
|
|
37
|
-
const previousTheme = resetFromPreset ? {} : previousOptions.theme;
|
|
38
|
-
return {
|
|
39
|
-
...previousOptions,
|
|
40
|
-
...nextOptions,
|
|
41
|
-
ui: {
|
|
42
|
-
...previousUi,
|
|
43
|
-
...(nextOptions.ui ?? {}),
|
|
44
|
-
raceDataBanners: {
|
|
45
|
-
...(previousUi.raceDataBanners ?? {}),
|
|
46
|
-
...(nextOptions.ui?.raceDataBanners ?? {}),
|
|
47
|
-
},
|
|
48
|
-
},
|
|
49
|
-
theme: {
|
|
50
|
-
...previousTheme,
|
|
51
|
-
...(nextOptions.theme ?? {}),
|
|
52
|
-
},
|
|
53
|
-
assets: {
|
|
54
|
-
...previousOptions.assets,
|
|
55
|
-
...(nextOptions.assets ?? {}),
|
|
56
|
-
trackTextures: {
|
|
57
|
-
...previousOptions.assets.trackTextures,
|
|
58
|
-
...(nextOptions.assets?.trackTextures ?? {}),
|
|
59
|
-
},
|
|
60
|
-
},
|
|
61
|
-
drivers: nextOptions.drivers ?? previousOptions.drivers,
|
|
62
|
-
entries: nextOptions.entries ?? previousOptions.entries,
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
|
|
66
38
|
function createCompositeRoot(getRoots, getOptions) {
|
|
67
39
|
return {
|
|
68
40
|
style: {
|
|
@@ -80,6 +52,12 @@ function createCompositeRoot(getRoots, getOptions) {
|
|
|
80
52
|
querySelectorAll(selector) {
|
|
81
53
|
return getRoots().flatMap((root) => [...(root.querySelectorAll?.(selector) ?? [])]);
|
|
82
54
|
},
|
|
55
|
+
setAttribute(name, value) {
|
|
56
|
+
getRoots().forEach((root) => root.setAttribute?.(name, value));
|
|
57
|
+
},
|
|
58
|
+
removeAttribute(name) {
|
|
59
|
+
getRoots().forEach((root) => root.removeAttribute?.(name));
|
|
60
|
+
},
|
|
83
61
|
applyCssVariables() {
|
|
84
62
|
const options = getOptions();
|
|
85
63
|
getRoots().forEach((root) => setPackageCssVariables(root, options.assets, options.theme));
|
|
@@ -91,6 +69,7 @@ export class PaddockSimulatorController {
|
|
|
91
69
|
constructor(options = {}) {
|
|
92
70
|
this.options = resolveF1SimulatorOptions(options);
|
|
93
71
|
this.roots = new Map();
|
|
72
|
+
this.layoutSupportCleanups = new Map();
|
|
94
73
|
this.app = null;
|
|
95
74
|
this.compositeRoot = createCompositeRoot(() => [...this.roots.values()], () => this.options);
|
|
96
75
|
}
|
|
@@ -102,6 +81,8 @@ export class PaddockSimulatorController {
|
|
|
102
81
|
}
|
|
103
82
|
root.innerHTML = markup;
|
|
104
83
|
setPackageCssVariables(root, this.options.assets, this.options.theme);
|
|
84
|
+
this.layoutSupportCleanups.get(key)?.();
|
|
85
|
+
this.layoutSupportCleanups.set(key, installLayoutSupport(root));
|
|
105
86
|
this.roots.set(key, root);
|
|
106
87
|
return root;
|
|
107
88
|
}
|
|
@@ -127,6 +108,7 @@ export class PaddockSimulatorController {
|
|
|
127
108
|
includeTimingTower = false,
|
|
128
109
|
includeTelemetrySectorBanner = false,
|
|
129
110
|
timingTowerVerticalFit,
|
|
111
|
+
responsiveNarrowLayout,
|
|
130
112
|
} = {}) {
|
|
131
113
|
return this.mountComponent(root, 'race-canvas', createRaceCanvasMarkup({
|
|
132
114
|
...this.options,
|
|
@@ -134,6 +116,7 @@ export class PaddockSimulatorController {
|
|
|
134
116
|
includeTimingTower,
|
|
135
117
|
includeTelemetrySectorBanner,
|
|
136
118
|
timingTowerVerticalFit,
|
|
119
|
+
responsiveNarrowLayout,
|
|
137
120
|
}));
|
|
138
121
|
}
|
|
139
122
|
|
|
@@ -170,7 +153,10 @@ export class PaddockSimulatorController {
|
|
|
170
153
|
}
|
|
171
154
|
|
|
172
155
|
mountRaceDataPanel(root) {
|
|
173
|
-
return this.mountComponent(root, 'race-data-panel', createRaceDataPanelMarkup(
|
|
156
|
+
return this.mountComponent(root, 'race-data-panel', createRaceDataPanelMarkup({
|
|
157
|
+
...this.options,
|
|
158
|
+
standalone: true,
|
|
159
|
+
}));
|
|
174
160
|
}
|
|
175
161
|
|
|
176
162
|
querySelector(selector) {
|
|
@@ -196,15 +182,35 @@ export class PaddockSimulatorController {
|
|
|
196
182
|
destroy() {
|
|
197
183
|
this.app?.destroy();
|
|
198
184
|
this.app = null;
|
|
185
|
+
this.layoutSupportCleanups.forEach((cleanup) => cleanup());
|
|
186
|
+
this.layoutSupportCleanups.clear();
|
|
199
187
|
this.roots.forEach((root) => {
|
|
200
188
|
root.innerHTML = '';
|
|
201
189
|
});
|
|
202
190
|
this.roots.clear();
|
|
203
191
|
}
|
|
204
192
|
|
|
193
|
+
syncOptionsFromRunningApp() {
|
|
194
|
+
const timingGapMode = this.app?.getTimingGapMode?.();
|
|
195
|
+
if (!timingGapMode) return this.options;
|
|
196
|
+
this.options = {
|
|
197
|
+
...this.options,
|
|
198
|
+
ui: {
|
|
199
|
+
...this.options.ui,
|
|
200
|
+
timingGapMode: normalizeTimingGapMode(timingGapMode),
|
|
201
|
+
},
|
|
202
|
+
};
|
|
203
|
+
return this.options;
|
|
204
|
+
}
|
|
205
|
+
|
|
205
206
|
restart(nextOptions = {}) {
|
|
206
|
-
const
|
|
207
|
-
|
|
207
|
+
const currentOptions = this.app ? this.syncOptionsFromRunningApp() : this.options;
|
|
208
|
+
const nextResolvedOptions = resolveF1SimulatorOptions(mergeRestartOptions(currentOptions, nextOptions));
|
|
209
|
+
if (this.app) {
|
|
210
|
+
this.app.restart(nextResolvedOptions);
|
|
211
|
+
this.options = nextResolvedOptions;
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
208
214
|
this.options = nextResolvedOptions;
|
|
209
215
|
this.compositeRoot.applyCssVariables();
|
|
210
216
|
}
|
|
@@ -254,6 +260,39 @@ export class PaddockSimulatorController {
|
|
|
254
260
|
return this.app?.simulationSpeed ?? 1;
|
|
255
261
|
}
|
|
256
262
|
|
|
263
|
+
setTimingGapMode(mode) {
|
|
264
|
+
const timingGapMode = this.app
|
|
265
|
+
? this.app.setTimingGapMode(mode)
|
|
266
|
+
: normalizeTimingGapMode(mode);
|
|
267
|
+
this.options = {
|
|
268
|
+
...this.options,
|
|
269
|
+
ui: {
|
|
270
|
+
...this.options.ui,
|
|
271
|
+
timingGapMode,
|
|
272
|
+
},
|
|
273
|
+
};
|
|
274
|
+
return this.options.ui.timingGapMode;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
getTimingGapMode() {
|
|
278
|
+
return this.app?.getTimingGapMode?.() ?? normalizeTimingGapMode(this.options.ui?.timingGapMode);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
toggleTimingGapMode() {
|
|
282
|
+
if (this.app) {
|
|
283
|
+
const timingGapMode = this.app.toggleTimingGapMode();
|
|
284
|
+
this.options = {
|
|
285
|
+
...this.options,
|
|
286
|
+
ui: {
|
|
287
|
+
...this.options.ui,
|
|
288
|
+
timingGapMode,
|
|
289
|
+
},
|
|
290
|
+
};
|
|
291
|
+
return timingGapMode;
|
|
292
|
+
}
|
|
293
|
+
return this.setTimingGapMode(getNextTimingGapMode(this.options.ui?.timingGapMode));
|
|
294
|
+
}
|
|
295
|
+
|
|
257
296
|
servePenalty(penaltyId) {
|
|
258
297
|
return this.app?.servePenalty(penaltyId) ?? null;
|
|
259
298
|
}
|