@inventure71/paddockjs 0.3.0 → 1.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.
Files changed (54) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/INSTALL_AND_UPDATE.md +3 -1
  3. package/README.md +142 -9
  4. package/docs/architecture.md +49 -15
  5. package/docs/concepts.md +20 -3
  6. package/docs/data_contract.md +185 -27
  7. package/docs/data_model.md +1 -1
  8. package/docs/learnings.md +1 -1
  9. package/docs/rules.md +140 -19
  10. package/docs/system_specs.md +39 -16
  11. package/docs/training.md +103 -1
  12. package/examples/train-basic-policy.mjs +5 -6
  13. package/examples/trainingData.mjs +133 -0
  14. package/package.json +5 -2
  15. package/src/api/PaddockSimulatorController.js +32 -2
  16. package/src/app/F1SimulatorApp.js +1101 -128
  17. package/src/app/domBindings.js +10 -1
  18. package/src/config/defaultOptions.js +4 -4
  19. package/src/environment/actions.js +25 -2
  20. package/src/environment/evaluation.js +184 -0
  21. package/src/environment/index.d.ts +281 -1
  22. package/src/environment/index.js +13 -0
  23. package/src/environment/observationOptions.js +12 -0
  24. package/src/environment/observations.js +189 -17
  25. package/src/environment/options.js +7 -0
  26. package/src/environment/recorder.js +28 -0
  27. package/src/environment/runtime.js +41 -6
  28. package/src/environment/scenarios.js +220 -0
  29. package/src/environment/sensors.js +145 -35
  30. package/src/environment/specs.js +92 -8
  31. package/src/environment/workerProtocol.js +52 -0
  32. package/src/index.d.ts +366 -3
  33. package/src/index.js +21 -0
  34. package/src/rendering/proceduralTrackAsset.js +336 -23
  35. package/src/simulation/collisionGeometry.js +214 -0
  36. package/src/simulation/driverController.js +431 -65
  37. package/src/simulation/raceSimulation.js +2132 -171
  38. package/src/simulation/rules/collisionSteward.js +69 -0
  39. package/src/simulation/rules/penaltyLedger.js +153 -0
  40. package/src/simulation/rules/pitLaneSpeedingSteward.js +71 -0
  41. package/src/simulation/rules/tireRequirementSteward.js +29 -0
  42. package/src/simulation/rules/trackLimitsSteward.js +85 -0
  43. package/src/simulation/rulesConfig.js +352 -0
  44. package/src/simulation/trackModel.js +1154 -76
  45. package/src/simulation/units.js +6 -4
  46. package/src/simulation/vehicleGeometry.js +247 -0
  47. package/src/simulation/vehiclePhysics.js +82 -50
  48. package/src/simulation/wheelSurface.js +338 -0
  49. package/src/styles.css +300 -42
  50. package/src/ui/bannerTemplates.js +69 -0
  51. package/src/ui/componentTemplates.js +36 -96
  52. package/src/ui/raceControlStatusBanner.js +39 -0
  53. package/src/ui/shellTemplate.js +1 -1
  54. package/src/ui/templateUtils.js +23 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,44 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - Promote PaddockJS to the first stable release for reusable browser embeds and headless JavaScript training/control workflows. The package now exposes the production simulator through `mountF1Simulator()`, composable mount helpers through `createPaddockSimulator()`, and a browser-free environment API through `@inventure71/paddockjs/environment`.
8
+ - Stabilize the 1.0 race-simulation feature set around pit-lane and pit-stop behavior, stewarded penalties, more realistic vehicle geometry and wheel-level surface handling, improved timing/DRS data, and lower-cost simulation/runtime paths.
9
+
10
+ ### Race Simulation
11
+
12
+ - Add deterministic procedural tracks with pit-lane geometry, service areas, team queue slots, garage boxes, legal pit-lane surfaces, pit entry/exit routes, pit-lane open/closed state, and red-flag gating.
13
+ - Add automatic and host-controlled pit-stop intent, target tire compounds, service timing, penalty service before tire work, team queue behavior, pit-service countdown display, and optional pit-crew service variability.
14
+ - Add penalty ledger support for time, drive-through, stop-go, position-drop, grid-drop, and disqualification consequences, including service/cancel/final-classification conversion behavior.
15
+ - Enforce track limits from all four wheel contact patches, not car center position, and resolve each car's physics surface from the worst wheel surface.
16
+ - Add pit-lane speeding steward enforcement on speed-limited pit-lane parts while keeping pit-entry and pit-exit connector roads legal but not speed-limited.
17
+ - Use body-hull car collision geometry instead of transparent sprite bounds, with swept checks to prevent endpoint tunneling and contact metadata for callbacks/debugging.
18
+ - Fix lapped-traffic collision fault assignment so rear contact is judged by physical track order rather than cumulative race distance.
19
+ - Fix DRS detection so physically-ahead lapped traffic can be the DRS reference car.
20
+ - Improve race timing with fixed timing-line crossing history, interval and leader-gap modes, lap-gap labels for lapped cars, sector telemetry, provisional waved-flag status, and final classification after the full field finishes.
21
+
22
+ ### Performance And Runtime
23
+
24
+ - Cache per-car geometry and wheel-surface state for repeated collision, surface, snapshot, and debug consumers.
25
+ - Reduce collision candidate work with circular-track distance-window pruning before swept AABB/SAT checks.
26
+ - Reduce headless/runtime cost by avoiding repeated full snapshot serialization during frame skips and by adding lean render/observation snapshot paths.
27
+ - Reduce observation cost by reusing ray-origin track state and broadphasing ray/car tests before exact footprint intersection.
28
+ - Reduce browser work by throttling noncritical DOM/timing refreshes at high playback speeds, reusing supplied snapshots for camera-control availability, caching stable pit-lane status graphics, pruning DRS trail histories in place, and skipping repeated text/markup writes.
29
+
30
+ ### Public API And Docs
31
+
32
+ - Add and document the expert/headless environment API, action and observation specs, starter progress reward helper, rollout recorder/evaluation helpers, and worker-protocol wrapper.
33
+ - Add public controller methods for safety car, red flag, pit-lane open/closed state, pit intent/compound control, and penalty serve/cancel controls.
34
+ - Add the `ui.simulationSpeedControl` TypeScript option and document the browser playback speed control.
35
+ - Expand local preview and browser smoke coverage for templates, composable mounts, API controls, behavior, stewarding, collision lab, expert environment, and policy runner pages.
36
+ - Update package docs for 1.0 behavior, data contracts, architecture boundaries, rules, training/control usage, and installation/update workflow.
37
+
38
+ ### Known Future Scope
39
+
40
+ - Weather effects, reliability failures, and fuel-load performance effects remain intentionally out of 1.0.0 and are tracked as future work.
41
+
3
42
  ## 0.3.0
4
43
 
5
44
  ### Minor Changes
@@ -122,7 +122,9 @@ After updating, smoke-test the page that mounts the simulator. Browser behavior
122
122
 
123
123
  The package repo owns its release process:
124
124
 
125
- - `npm run check` runs runtime tests, public type verification, dry-pack verification, and the tracked showcase build.
125
+ - `npm run check` runs runtime tests, public type verification, dry-pack verification, packed-consumer install/build verification, the tracked showcase build, and Chromium browser smoke tests.
126
+ - `npm run consumer:smoke` packs the package, installs the tarball into a fresh temporary Vite app, and builds that app through public package imports.
127
+ - `npm run browser:smoke` builds `local-preview`, starts a local preview server, and checks desktop/mobile canvas rendering, overflow constraints, API buttons, and visual policy-runner stepping in Chromium.
126
128
  - `npm run changeset` records the next version bump and changelog note.
127
129
  - `npm run version-packages` applies pending Changesets locally.
128
130
  - `.github/workflows/ci.yml` verifies the package on push and pull request.
package/README.md CHANGED
@@ -28,12 +28,14 @@ Useful commands:
28
28
 
29
29
  ```bash
30
30
  npm run check
31
+ npm run consumer:smoke
32
+ npm run browser:smoke
31
33
  npm run showcase:dev
32
34
  npm run showcase:build
33
35
  npm run changeset
34
36
  ```
35
37
 
36
- `npm run check` verifies the runtime tests, public declaration file, dry package contents, and showcase build.
38
+ `npm run check` verifies runtime tests, public declarations, dry package contents, packed-package consumption in a fresh Vite app, the showcase build, and real Chromium smoke tests against the showcase pages.
37
39
 
38
40
  Local development and showcase builds require Node `20.19.0` or newer. CI currently runs the package check on Node 22 and releases on Node 24.
39
41
 
@@ -42,7 +44,7 @@ Local development and showcase builds require Node `20.19.0` or newer. CI curren
42
44
  Headless training code imports the environment subpath:
43
45
 
44
46
  ```js
45
- import { createPaddockEnvironment, createProgressReward } from '@inventure71/paddockjs/environment';
47
+ import { createPaddockEnvironment } from '@inventure71/paddockjs/environment';
46
48
  ```
47
49
 
48
50
  The package root remains the browser component API. The environment subpath is intentionally browser-free and does not import DOM, PixiJS, or package CSS.
@@ -54,7 +56,6 @@ const env = createPaddockEnvironment({
54
56
  entries,
55
57
  controlledDrivers: ['budget'],
56
58
  frameSkip: 2,
57
- reward: createProgressReward(),
58
59
  });
59
60
 
60
61
  let result = env.reset();
@@ -63,13 +64,15 @@ result = env.step({
63
64
  });
64
65
  ```
65
66
 
66
- The repository includes a dependency-free starter training loop that uses the same environment contract:
67
+ The environment can run with no reward, as above, or with a host-supplied `reward(context)` callback. The repository also includes dependency-free examples that use the same environment contract:
67
68
 
68
69
  ```bash
69
70
  node examples/train-basic-policy.mjs --generations=4 --candidates=5 --episodes=1 --steps=240
70
71
  ```
71
72
 
72
- Each ray reports track-transition distance and car distance. A track hit uses `kind: 'exit'` when the ray leaves the road and `kind: 'entry'` when an off-track ray points back to the road.
73
+ The starter script imports the public `@inventure71/paddockjs/environment` subpath and uses self-contained example data from `examples/trainingData.mjs`, so it does not depend on private package source modules for demo drivers. `createProgressReward()` remains available as example/demo reward code only; it is not the official reward and not part of the environment objective.
74
+
75
+ Each ray reports track-transition distance and car distance. A track hit uses `kind: 'exit'` when the ray leaves the road and `kind: 'entry'` when an off-track ray points back to the road. `observation.object.self.onTrack` follows the simulator's wheel-level legality rules, so track, kerb, and legal pit-lane/box surfaces are on-track for reward and observation purposes. `pitIntent: 0` is always accepted as the no-op clear value, including environments where pit stops are disabled.
73
76
 
74
77
  External training code can inspect the environment contract without guessing field ranges:
75
78
 
@@ -78,6 +81,24 @@ const actionSpec = env.getActionSpec();
78
81
  const observationSpec = env.getObservationSpec();
79
82
  ```
80
83
 
84
+ The environment also exposes reset-only scenario placement, neutral rollout recording, deterministic evaluation metrics, and a JSON-serializable worker protocol for external bridges:
85
+
86
+ ```js
87
+ const env = createPaddockEnvironment({
88
+ drivers,
89
+ entries,
90
+ controlledDrivers: ['budget'],
91
+ scenario: {
92
+ preset: 'off-track-recovery',
93
+ placements: {
94
+ budget: { distanceMeters: 420, offsetMeters: 16, speedKph: 65 },
95
+ },
96
+ },
97
+ });
98
+ ```
99
+
100
+ Scenario placement is an environment reset feature, not a policy assist. During `step(actions)`, controlled cars still move only through normalized steering, throttle, brake, and pit intent.
101
+
81
102
  Browser expert mode is opt-in through the normal mount API. When enabled, the visual simulator advances only when host code calls `simulator.expert.step(actions)`. Expert mode is a mount-time boundary; changing `expert` through `restart(nextOptions)` is rejected so ticker ownership cannot silently change under a mounted simulator.
82
103
  Set `expert.visualizeSensors` to draw expert sensor rays inside the actual race canvas for visual debugging:
83
104
 
@@ -183,7 +204,7 @@ simulator.mountRaceTelemetryDrawer(document.getElementById('sim-race-workbench')
183
204
  });
184
205
  ```
185
206
 
186
- `drivers` is the host-owned project/pilot list. `entries` is the optional driver/car/team pairing sheet. The car/driver overview uses the existing driver and vehicle rating components from each entry. Driver and vehicle entries can also include `customFields` as extra label/value metadata. Entries can include `team: { id, name, color, icon }`; the timing tower uses the team icon and defaults team color to the car color when omitted. Assets, including the default car image and generic driver helmet, are bundled by default, so the host website does not need to provide simulator images or textures.
207
+ `drivers` is the host-owned project/pilot list. `entries` is the optional driver/car/team pairing sheet. The car/driver overview uses the existing driver and vehicle rating components from each entry. Driver and vehicle entries can also include `customFields` as extra label/value metadata. Entries can include `team: { id, name, color, icon, pitCrew }`; the timing tower uses the team icon and defaults team color to the car color when omitted. `team.pitCrew` accepts `speed`, `consistency`, and `reliability` values from `0` to `1` for optional pit-service variability. Assets, including the default car image and generic driver helmet, are bundled by default, so the host website does not need to provide simulator images or textures.
187
208
 
188
209
  The returned object supports:
189
210
 
@@ -191,9 +212,16 @@ The returned object supports:
191
212
  - `restart(nextOptions)` for non-asset, non-expert race/data/seed changes
192
213
  - `selectDriver(driverId)`
193
214
  - `setSafetyCarDeployed(deployed)`
215
+ - `setRedFlagDeployed(deployed)`
216
+ - `setPitLaneOpen(open)`
194
217
  - `callSafetyCar()`
195
218
  - `clearSafetyCar()`
196
219
  - `toggleSafetyCar()`
220
+ - `setPitIntent(driverId, intent, targetCompound?)`
221
+ - `getPitIntent(driverId)`
222
+ - `getPitTargetCompound(driverId)`
223
+ - `servePenalty(penaltyId)`
224
+ - `cancelPenalty(penaltyId)`
197
225
  - `getSnapshot()`
198
226
  - `expert` when explicitly enabled, otherwise `null`
199
227
 
@@ -226,7 +254,112 @@ ui: {
226
254
 
227
255
  If `trackSeed` is omitted, each mounted browser simulator creates a fresh procedural circuit. Passing `trackSeed` makes the track deterministic so multiple embeds can share the same generated circuit; repeated procedural seeds are cached within the page runtime. `restart({ trackSeed })` rebuilds the race on the deterministic circuit for the new seed. Asset URL changes are not restartable; destroy and mount a new simulator when changing assets.
228
256
 
229
- `initialCameraMode` accepts `'overview'`, `'leader'`, `'selected'`, or `'show-all'`; invalid values fall back to `'leader'`. `layoutPreset: 'left-tower-overlay'` creates a left broadcast gutter inside the race view, frames the camera around the remaining race area, and places the timing tower in that gutter without covering camera controls. The project/radio lower-third stays inside the race window and can intentionally render over the timing sidebar instead of shrinking around it. Composable hosts should pass `{ includeRaceDataPanel: true }` to `mountRaceCanvas()` when they want that lower-third clipped and layered by the race window; `mountRaceDataPanel()` remains available for hosts that intentionally want the banner as a standalone surface. `raceDataTelemetryDetail: true` makes the project lower-third include a compact S1/S2/S3 sector strip; radio mode stays the same and the standalone `mountTelemetrySectorBanner()` surface remains available only when a host explicitly mounts it. `includeTimingTower: true` embeds the timing tower directly inside the race canvas and reserves camera space from the measured tower gutter when the tower is a side overlay. In narrow mobile hosts, package CSS stacks the embedded timing tower full-width and the camera stops reserving a fake left gutter. `mountRaceTelemetryDrawer()` is a higher-level template that mounts the race canvas, embedded timing tower, lower-third, safety-car control, and a right telemetry drawer together; pass `{ raceDataTelemetryDetail: true }` when the drawer lower-third should include compact sector detail. Opening the drawer reduces the race area instead of overlaying it, and closing it removes the drawer from interaction. `raceDataBanners.initial` selects the starting banner state (`'project'`, `'radio'`, or `'hidden'`), while `raceDataBanners.enabled` chooses which banner types may appear. `raceDataBannerSize: 'auto'` uses the race space to the right of the timing board when it is wide enough and falls back to the full lower-third overlap when it is not; `'custom'` preserves the default CSS-variable-driven lower-third size for hosts that want to tune their own banner geometry. `timingTowerVerticalFit: 'expand-race-view'` lets the race window grow tall enough for the tower; `'scroll'` keeps the race window height and scrolls the timing list inside the cropped tower. The same fit values can be passed to `mountRaceCanvas()` when `includeTimingTower` is enabled. Standalone timing towers are capped by `--timing-board-max-width` and fill their mount root height; placing the root in a fixed-height container makes only the timing entries scroll. Timing entries always stack from the top as fixed rows, so P1/P2 occupy the same vertical positions whether the race has 2 cars or 20. The timing tower includes an `Int`/`Gap` broadcast switch: `Int` shows the interval to the car ahead, while `Gap` shows total gap to the leader. `cameraControls: 'external'` moves view controls out of the canvas so hosts can mount them with `mountCameraControls()`. `showFps: false` hides the FPS readout. `telemetryIncludesOverview: false` keeps the car/driver overview out of the telemetry stack so hosts can mount `mountCarDriverOverview()` separately. `telemetryModules` controls optional package-owned telemetry surfaces: `core`, `sectors`, `lapTimes`, and `sectorTimes`; those same surfaces are also available as fully detached mount methods.
257
+ Every generated track includes:
258
+
259
+ - rendered pit lane beside the start/finish straight
260
+ - lane-aligned procedural entry and exit roads
261
+ - main fast lane sized from the team/box layout
262
+ - parallel working lane
263
+ - 10 shared team service areas
264
+ - 20 unused garage boxes arranged as 10 team pairs
265
+
266
+ Pit-lane asphalt, working-lane service areas, and garage boxes are legal drivable surfaces for sensors, runoff handling, and track-limit stewarding. Tire energy degrades down to 1% and affects grip nonlinearly, so badly worn tires are slower and harder to rotate without making the car instantly undrivable.
267
+
268
+ When `rules.modules.pitStops.enabled` is true, cars automatically form bounded pit trains when lane space is available. They brake to the limiter by the main pit-lane start, drive along the main fast lane, pass through the team queue spot as a rolling gate, roll into the team-colored working-lane service area when it is clear, stop, serve eligible penalties before tire work, show the remaining stationary service time above the car, change to the requested configured tire compound or the default alternate compound, and exit back to the race track.
269
+
270
+ Team-mates share one service area. Every car enters through the queue spot first, but it only stops there when the active service spot is blocked. A second team car waits in the queue spot until the active service spot is physically clear, including the previous car's first movement out of the box, without blocking the main fast lane.
271
+
272
+ By default, built-in AI cars request an opportunistic `pitIntent: 1` below 50% tire energy and commit `pitIntent: 2` below 30%. Hosts can change those thresholds with `tirePitRequestThresholdPercent` and `tirePitCommitThresholdPercent`. Expert/headless controlled drivers do not receive those automatic tire-threshold calls and must request service with `pitIntent`.
273
+
274
+ Hosts can override the pit call with `setPitIntent(driverId, 0 | 1 | 2, 'H')` or expert action `{ pitIntent: 2, pitCompound: 'H' }`:
275
+
276
+ - `0` means no request
277
+ - `1` means keep trying until a free pit-entry window is available
278
+ - `2` means commit to entering at the next pit-entry window even when pit-lane capacity or gap checks would block an opportunistic stop
279
+
280
+ Completed stops can be re-armed later by tire condition or by `setPitIntent`, so pit stops are not one-use per race. Optional `pitStops.variability` uses `team.pitCrew` speed, consistency, and reliability to adjust service time and slow-stop chance; `pitStops.variability.perfect: true` forces deterministic default service time for training.
281
+
282
+ `setPitLaneOpen(false)` keeps new pending pit calls on track until reopened and the canvas shows a small red/green/yellow light near pit entry. `setRedFlagDeployed(true)` freezes race movement, closes the effective pit lane, and emits red-flag race-control state until cleared. The pit speed limiter applies on the straight main pit lane/working lane, not on the entry and exit connector roads, but automatic pit-entry routing still keeps connector speeds bounded so cars can reach the limiter safely.
283
+
284
+ Race behavior is configurable through `rules`. Flat existing options such as `standingStart: false` remain supported, and advanced behavior is grouped under module config:
285
+
286
+ ```js
287
+ const simulator = await mountF1Simulator(root, {
288
+ drivers,
289
+ rules: {
290
+ ruleset: 'fia2025',
291
+ modules: {
292
+ pitStops: {
293
+ enabled: true,
294
+ pitLaneSpeedLimitKph: 80,
295
+ maxConcurrentPitLaneCars: 3,
296
+ minimumPitLaneGapMeters: 20,
297
+ tirePitRequestThresholdPercent: 50,
298
+ tirePitCommitThresholdPercent: 30,
299
+ variability: {
300
+ enabled: true,
301
+ perfect: false,
302
+ },
303
+ },
304
+ penalties: {
305
+ trackLimits: { strictness: 0.8 },
306
+ collision: { strictness: 0.5, consequences: [{ type: 'time', seconds: 5 }] },
307
+ tireRequirement: { strictness: 1, consequences: [{ type: 'time', seconds: 10 }] },
308
+ pitLaneSpeeding: { strictness: 1, speedLimitKph: 80 },
309
+ },
310
+ },
311
+ },
312
+ ui: {
313
+ penaltyBanners: true,
314
+ timingPenaltyBadges: true,
315
+ },
316
+ });
317
+ ```
318
+
319
+ Supported rulesets are `paddock`, `grandPrix2025`, `fia2025`, and `custom`. Presets only choose defaults; explicit module config wins. Penalty subsections use `strictness` from `0` to `1` instead of plain booleans. Track limits use the white line as the legal edge and require all four wheel contact patches to be fully outside the same side of the line before recording a violation, so normal kerb riding is not punished. Per-car `surface` is resolved from the worst wheel surface, snapshots include `car.wheels` for per-wheel surface and white-line state, and asymmetric left/right wheel resistance adds a small capped yaw tug toward the slower side when only one side is on a worse surface. Collision stewarding is driven by a body collision hull, not transparent sprite bounds or wheel-only overlap, and contact events include shape ids, contact type, depth, and time of impact. It considers impact severity, closing speed, and whether one car clearly hit another from behind; clear rear contact penalizes only the physically trailing car, including lapped traffic cases, while unclear meaningful contact records shared-fault penalties for both cars. Pit-lane speeding is enforced on the main fast lane, working lane, service areas, and garage boxes, but not on pit-entry or pit-exit connector roads. Track-limit warnings are emitted as `track-limits` events, while penalty decisions are exposed through `snapshot.penalties` plus `penalty` events. Penalty consequences support warning, time, drive-through, stop-go, position-drop, grid-drop, and disqualification payloads. Time consequences are additive, drive-through and stop-go penalties are service obligations, and unserved service penalties convert to configured time at final classification.
320
+
321
+ `initialCameraMode` accepts `'overview'`, `'leader'`, `'selected'`, `'show-all'`, or `'pit'`; invalid values fall back to `'leader'`. The overview camera frames the active generated track bounds, including package-owned track padding and pit-lane extent, instead of using a fixed world-center zoom. Camera mode changes ease from the current camera target to the next target after the initial frame, so switching between leader, selected, overview, show-all, and pit views does not snap the world view.
322
+
323
+ The pit camera targets the generated operational pit-lane geometry, including the lane, boxes, service areas, and queue areas, while excluding the longer entry/exit access roads. It zooms out as needed to fit that work area inside the active race-view safe area, and its control is hidden when the active track has no pit lane.
324
+
325
+ Zoom buttons and wheel zoom apply to every camera mode. Zoom-out is bounded by the active generated track frame so the camera cannot pull far beyond the circuit. The renderer keeps grass coverage larger than the simulated world, and the transparent canvas host uses the same grass color as a fallback around the framed circuit.
326
+
327
+ `layoutPreset: 'left-tower-overlay'` creates a left broadcast gutter inside the race view, frames the camera around the remaining race area, and places the timing tower in that gutter without covering camera controls. The project/radio lower-third stays inside the race window and can intentionally render over the timing sidebar instead of shrinking around it.
328
+
329
+ Composable hosts should pass `{ includeRaceDataPanel: true }` to `mountRaceCanvas()` when they want that lower-third clipped and layered by the race window. `mountRaceDataPanel()` remains available for hosts that intentionally want the banner as a standalone surface.
330
+
331
+ `raceDataTelemetryDetail: true` makes the project lower-third include a compact S1/S2/S3 sector strip with live sector elapsed time and per-sector progress. That telemetry project lower-third stays visible until dismissed, muted, or replaced, while radio mode keeps its normal schedule. The standalone `mountTelemetrySectorBanner()` surface remains available only when a host explicitly mounts it.
332
+
333
+ `includeTimingTower: true` embeds the timing tower directly inside the race canvas and reserves camera space from the measured tower gutter when the tower is a side overlay. In narrow mobile hosts, package CSS stacks the embedded timing tower full-width and the camera stops reserving a fake left gutter.
334
+
335
+ `mountRaceTelemetryDrawer()` is a higher-level template that mounts an external top control row, race canvas, embedded timing tower, lower-third, top steward message, safety-car control, and a right telemetry drawer together. Pass `{ raceDataTelemetryDetail: true }` when the drawer lower-third should include compact sector detail.
336
+
337
+ The drawer top row contains camera controls, a `1x` simulation-speed button that cycles through `2x`, `3x`, `4x`, `5x`, `10x`, and back to `1x`, a `Mute banners` toggle, safety car, and the telemetry toggle so those controls do not cover the race. For other camera-control placements the speed button is hidden by default and can be enabled with `ui.simulationSpeedControl: true`.
338
+
339
+ `Mute banners` is off by default and temporarily suppresses project/radio lower-thirds while it is pressed; steward penalty banners are separate and remain controlled by `penaltyBanners`. Opening the drawer reduces the race area instead of overlaying it, and closing it removes the drawer from interaction. The drawer uses the same package-owned `telemetry-stack` component as `mountTelemetryPanel()`, so constrained host heights scroll telemetry vertically instead of letting the sidebar escape its frame.
340
+
341
+ Banner and timing layout options:
342
+
343
+ - `raceDataBanners.initial` selects the starting banner state: `'project'`, `'radio'`, or `'hidden'`.
344
+ - `raceDataBanners.enabled` chooses which banner types may appear.
345
+ - `raceDataBannerSize: 'auto'` uses the race space to the right of the timing board when it is wide enough and falls back to the full lower-third overlap when it is not.
346
+ - `raceDataBannerSize: 'custom'` preserves the default CSS-variable-driven lower-third size for hosts that want to tune their own banner geometry.
347
+ - `timingTowerVerticalFit: 'expand-race-view'` lets the race window grow tall enough for the tower.
348
+ - `timingTowerVerticalFit: 'scroll'` keeps the race window height and scrolls the timing list inside the cropped tower.
349
+
350
+ The same timing fit values can be passed to `mountRaceCanvas()` when `includeTimingTower` is enabled. Standalone timing towers are capped by `--timing-board-max-width` and fill their mount root height; placing the root in a fixed-height container makes only the timing entries scroll. Timing entries always stack from the top as fixed rows, so P1/P2 occupy the same vertical positions whether the race has 2 cars or 20.
351
+
352
+ The timing tower includes an `Int`/`Gap` broadcast switch: `Int` shows the interval to the car ahead, while `Gap` shows total gap to the leader. Seconds gaps are measured from hidden fixed timing-line crossings on the track. If a car is one or more whole laps behind in the selected mode, the tower shows labels such as `+1` or `+2` instead of a seconds estimate.
353
+
354
+ Other UI switches:
355
+
356
+ - `cameraControls` defaults to `'external'`; set it to `'embedded'` only when controls should intentionally appear inside the canvas, or `false` to omit them.
357
+ - `penaltyBanners: true` shows warnings and penalties in the top steward message instead of the project/radio lower-third.
358
+ - `showFps: false` hides the FPS readout.
359
+ - `telemetryIncludesOverview: false` keeps the car/driver overview out of the telemetry stack so hosts can mount `mountCarDriverOverview()` separately.
360
+ - `telemetryModules` controls optional package-owned telemetry surfaces: `core`, `sectors`, `lapTimes`, and `sectorTimes`.
361
+
362
+ Project/radio lower-thirds include a package-owned top-right close button that hides the current pill before its scheduled timeout. Time penalties put a large `+10s` style chip in the left block of the steward message and the affected car/rule beside it. Warning messages use warning colors and do not create timing-row `!` badges. The telemetry surfaces are also available as fully detached mount methods.
230
363
 
231
364
  ### Composable overlay layout contract
232
365
 
@@ -236,9 +369,9 @@ For the all-in-one simulator, call `mountF1Simulator()` and let PaddockJS genera
236
369
 
237
370
  Mounted package surfaces include a lightweight red start-light loading overlay. The runtime removes each overlay after PixiJS, assets, controls, and the initial readouts are ready.
238
371
 
239
- The runtime also pauses its render ticker when the race canvas is offscreen or the browser tab is hidden. This keeps pages with multiple PaddockJS embeds responsive without requiring host code to manually start and stop each simulator.
372
+ The runtime also pauses its render ticker when the race canvas is offscreen or the browser tab is hidden. This keeps pages with multiple PaddockJS embeds responsive without requiring host code to manually start and stop each simulator. At `5x` and `10x` browser playback, noncritical timing/readout DOM refreshes run at a lower cadence while fixed-step simulation and race events keep using authoritative race state.
240
373
 
241
- Lifecycle callbacks are optional and host-owned. PaddockJS emits `onLoadingChange`, `onReady`, `onError`, `onDriverSelect`, `onRaceEvent`, `onLapChange`, and `onRaceFinish`; callback errors are routed to `onError` when provided and do not stop the simulator loop. Race snapshots include per-car interval timing, leader-gap timing, calibrated `speedKph`, automatic `track.sectors`, per-car `lapTelemetry`, finish state, `raceControl.winner` after the first finisher, and final `raceControl.classification` only after the whole field finishes. The field then circulates in safety-car mode and the race canvas shows a package-owned winner banner.
374
+ Lifecycle callbacks are optional and host-owned. PaddockJS emits `onLoadingChange`, `onReady`, `onError`, `onDriverSelect`, `onRaceEvent`, `onLapChange`, and `onRaceFinish`; callback errors are routed to `onError` when provided and do not stop the simulator loop. Race snapshots include per-car interval timing, leader-gap timing, whole-lap gap counts, calibrated `speedKph`, automatic `track.sectors`, hidden `track.timingLines`, per-car `lapTelemetry`, finish state, `raceControl.winner` after the first finisher, and final `raceControl.classification` only after the whole field finishes. Sector telemetry clears future-sector values so banners and sidebars show completed splits before the active sector plus the active live timer, not stale later-sector entries. Cars that have crossed the line before full race completion expose `raceStatus: 'waved-flag'` / `wavedFlag: true` and stay frozen in provisional finish order. Final classification applies time penalties and unserved service conversions through `adjustedFinishTime`, then applies position-drop and disqualification consequences. The field then circulates in safety-car mode and the race canvas shows a package-owned winner banner.
242
375
 
243
376
  ## License
244
377
 
@@ -68,6 +68,7 @@ training script
68
68
  -> createPaddockEnvironment(options)
69
69
  -> resolveEnvironmentOptions(options)
70
70
  -> createRaceSimulation(...)
71
+ -> applyEnvironmentScenario(...) for reset-only placements
71
72
  -> env.reset()
72
73
  -> env.step(actions)
73
74
  -> resolveActionMap(actions)
@@ -76,6 +77,8 @@ training script
76
77
  -> optional reward(context)
77
78
  ```
78
79
 
80
+ The environment runtime consumes per-step events directly from the simulation during `frameSkip` and builds the full public snapshot once for the returned result. This keeps `reset()` / `step()` semantics unchanged while avoiding repeated full car, wheel, penalty, and track serialization inside one environment action.
81
+
79
82
  Browser expert mode reuses the same environment runtime around the visual app's existing `RaceSimulation` instance. When `expert.enabled` is true, the visual ticker does not advance simulation time automatically; `simulator.expert.reset()` and `simulator.expert.step(actions)` are the only simulation-advance entry points.
80
83
 
81
84
  ## Public Entry
@@ -99,12 +102,22 @@ Responsibilities:
99
102
 
100
103
  - `index.js` and `index.d.ts` expose the public subpath API.
101
104
  - `runtime.js` owns environment-loop `reset()`, `step(actions)`, `getObservation()`, and `getState()` around an injected race-simulation host.
102
- - `options.js` validates `controlledDrivers`, scenario participants, frame skip, action policy, sensor config, and episode limits.
103
- - `actions.js` maps normalized public controls onto simulator steering/throttle/brake controls.
104
- - `observations.js`, `sensors.js`, and `events.js` build sensor-style observations, fixed-schema vectors, ray/nearby-car readings, and global/per-driver events.
105
+ - `options.js` validates `controlledDrivers`, scenario participants, reset placement config, frame skip, action policy, sensor config, and episode limits.
106
+ - `actions.js` maps normalized public controls onto simulator steering/throttle/brake controls plus pit intent and optional target compound requests.
107
+ - `scenarios.js` owns environment reset placement presets, absolute placements, and relative traffic layouts. It applies them through simulator state APIs during environment creation/reset only; it must not create policy-time teleport or assisted-control paths.
108
+ - `observations.js`, `sensors.js`, and `events.js` build sensor-style observations, versioned fixed-schema vectors, track lookahead/curvature, ray/nearby-car readings, and global/per-driver events.
109
+ - `recorder.js` owns neutral rollout transition export. It records environment-loop data and does not interpret rewards or policies.
110
+ - `evaluation.js` owns deterministic evaluation cases and metrics. It can run a supplied policy against fixed seeds/scenarios, but it must not update models or define reward objectives.
111
+ - `workerProtocol.js` owns the JSON-serializable wrapper used by external bridges. It forwards reset/step/spec/state messages to an environment instance and does not choose Python, Gymnasium, PettingZoo, or storage infrastructure.
105
112
 
106
113
  The environment subpath must not import `src/index.js`, package CSS, PixiJS, `F1SimulatorApp`, or DOM-specific code.
107
114
 
115
+ `src/simulation/rulesConfig.js` owns ruleset normalization and modular race-rule defaults. It keeps old flat rule keys compatible while adding preset/module config for pit stops, tire strategy, penalties, weather, reliability, and fuel load. Race modules should be added there first, then consumed by simulation logic through the normalized `race.rules` object.
116
+
117
+ `src/simulation/trackModel.js` owns procedural track geometry, including start/finish normalization, sectors, DRS zones, and the deterministic pit-lane model. The model explicitly straightens the start/finish window so the starting grid, pit-entry approach area, and immediate post-line exit area are geometrically straight instead of merely choosing the least-curved generated segment. The pit lane is generated next to that start straight for every built track and exposes entry/exit connectors, explicit procedural road centerlines that overlap the track surface, connect on the lane-facing side of the main track, are tangent to the track at merge points and to the main pit lane at pit-lane endpoints, a straight main fast lane sized from the team/box run plus bounded entry/exit buffers, a parallel working lane, one shared service area per team, queue geometry behind each service area, and two garage boxes per team. The model also stores pit-lane world bounds so `nearestTrackState()` can skip pit road and service-box geometry checks for normal main-track points outside the pit area. `nearestTrackState()` classifies `pit-entry`, `pit-lane`, `pit-exit`, and `pit-box` as legal drivable states while preserving main-track `crossTrackError` for existing consumers. `src/rendering/proceduralTrackAsset.js` renders the same model with team-colored service areas, queue slots, and paired garage boxes, but draws main-circuit asphalt, kerbs, and borders above pit-lane asphalt so visual crossings are owned by the race track; it also renders an oversized grass background beyond the simulated world bounds, while the transparent race canvas host uses the same grass color as a fallback so deep zoom-out views do not expose host-page background. `F1SimulatorApp` overlays the dynamic pit-lane status light because open/closed/red-flag state belongs to race control, not the static track texture. `src/simulation/raceSimulation.js` owns the current pit-stop state machine: team/service-area assignment, bounded pit-train scheduling, active pit-lane capacity/gap checks, pit-lane open/closed and red-flag gating, same-team staged service queueing, target-compound selection, pit-crew service-time variability, forward approach-route generation, route segment speed-limiter metadata, fast-lane routing, steering-based route following, snap-limited queue capture, physical service-area occupancy checks, movement-based queue-to-service release, service timing, tire change, and exit routing all consume `track.pitLane` instead of recreating a separate visual-only path.
118
+
119
+ `src/simulation/rules/` owns focused rule calculation helpers. Each stewarded rule should have its own function and, once non-trivial, its own file. `raceSimulation.js` should orchestrate timing/state and call those helpers; rule modules should return events or penalty payloads rather than mutating the whole simulation directly.
120
+
108
121
  `src/api/PaddockSimulatorController.js` owns composable mounting.
109
122
 
110
123
  Responsibilities:
@@ -137,7 +150,7 @@ Responsibilities:
137
150
  - Restart behavior for race data, seed, and track changes. Asset URL changes and browser expert mode changes are intentionally outside restart because texture loading and ticker ownership are part of initialization.
138
151
  - Lifecycle cleanup, including partial-init failure cleanup and destruction of replaced PixiJS display children without destroying shared textures.
139
152
 
140
- When `options.expert.enabled` is true, `F1SimulatorApp` creates a narrow browser expert adapter around its existing `this.sim` race simulation. The adapter uses the same shared environment runtime as the headless API, but it must not create a second race simulation for the visual mount. Expert browser mounts disable automatic ticker-driven simulation advancement; the canvas updates only after `simulator.expert.reset()` or `simulator.expert.step(actions)` renders the new snapshot. Expert mode is a mount-time boundary; runtime restart rejects `expert` changes instead of attempting to rewire ticker ownership in place.
153
+ When `options.expert.enabled` is true, `F1SimulatorApp` creates a narrow browser expert adapter around its existing `this.sim` race simulation. The adapter uses the same shared environment runtime as the headless API, but it must not create a second race simulation for the visual mount. Expert browser mounts disable automatic ticker-driven simulation advancement; the canvas updates only after `simulator.expert.reset()` or `simulator.expert.step(actions)` renders the new snapshot. The shared runtime owns controlled-driver pit intent defaults: controlled cars start with `pitIntent` set to `0`, tire-threshold automatic pit calls are disabled for them, and expert actions may request `0`, `1`, or `2` plus optional `pitCompound` through the simulation API instead of steering into the pit lane directly. Expert mode is a mount-time boundary; runtime restart rejects `expert` changes instead of attempting to rewire ticker ownership in place.
141
154
 
142
155
  Expert sensor visualization belongs to the browser app layer, not the headless environment. The environment result owns the observation contract; `BrowserExpertAdapter` passes that observation into `F1SimulatorApp.renderExpertFrame()`, and `F1SimulatorApp` draws opt-in sensor rays in a Pixi world layer so they share the same camera transform as the track and cars.
143
156
 
@@ -152,6 +165,7 @@ This file is still large. When changing it substantially, prefer extracting cohe
152
165
  Responsibilities:
153
166
 
154
167
  - Race-control mode.
168
+ - Normalized ruleset/module state received from `rulesConfig.js`.
155
169
  - Start sequence.
156
170
  - Safety car.
157
171
  - Car creation.
@@ -160,20 +174,31 @@ Responsibilities:
160
174
  - Per-car lap and sector telemetry.
161
175
  - Timing history.
162
176
  - DRS eligibility.
177
+ - Steward penalty ledger orchestration using focused rule helpers.
163
178
  - Collision response.
164
179
  - Snapshot creation.
165
180
 
166
- `src/simulation/driverController.js` owns AI control decisions.
181
+ `snapshot()` is the full public data contract and must remain stable for host callbacks, environment state, and external callers. Internal browser/rendering paths use lean read models such as `snapshotRender()` so the 60 FPS loop does not serialize setup, wheels, penalties, and other non-render fields every frame. Environment and sensor code may use similarly narrow internal views, cache shared per-observation ray origin state, and broadphase car-ray checks before exact footprint intersection, but those must not change the public `RaceSnapshot` shape.
182
+
183
+ `src/simulation/driverController.js` owns AI control decisions. It does not move cars directly; it samples the current track/race context, plans green-flag, rejoin, and safety-car targets, and returns steering/throttle/brake controls for `vehiclePhysics.js` to integrate. Green-flag steering combines target-point pursuit, capped upcoming-heading feed-forward, lateral correction, edge-pressure recovery, and a corner-apex offset so default cars can use the track width and carry speed through generated corners without bypassing steering-rate, grip, or yaw-rate limits. Its traffic planner keeps attack, defense, side-overlap, and lane-change thresholds in calibrated meter units so changing rendered scale or simulator-unit density does not silently weaken racecraft decisions. Safety-car queueing is also meter-calibrated: cars pursue compact frozen-order slots, add centerline correction, and cap corner speed so resized tracks do not create runaway safety-car gaps or off-track trains. Attack commitment and rejoin recovery are stored as deterministic per-car controller state; they affect only the next steering/throttle/brake request, not car pose or race order. The controller also treats braking and throttle as mutually exclusive during high steering or edge-recovery demand so it does not request power while trying to save a sharp corner.
184
+
185
+ `src/simulation/vehicleGeometry.js` owns deterministic car footprint math. It exposes the rendered-scale body hull, four wheel/contact-patch oriented rectangles, current/previous pose generation, AABB helpers, and a per-car `geometryState` cache keyed by current and previous pose. Collision detection, wheel-surface sampling, snapshots, and debug previews consume this module instead of each recreating car dimensions.
186
+
187
+ `src/simulation/collisionGeometry.js` owns shape-confirmed vehicle contact. It first prunes pairs by nearby track progress with a circular-track window, then uses swept body AABBs, SAT body/body overlap checks, and conservative continuous checks between previous and current poses. Wheel shapes are intentionally excluded from car-vs-car collision for cost and stability; wheel geometry is still used by the surface and track-limit pipeline. `raceSimulation.js` only orchestrates response/cooldown/steward review after this module returns contact metadata, and collision stewarding derives rear-contact fault from physical track order so lapped cars are handled consistently.
167
188
 
168
- `src/simulation/vehiclePhysics.js` owns vehicle integration and surface physics.
189
+ `src/simulation/wheelSurface.js` owns per-wheel track sampling and the effective car surface. On normal main-track running it uses the cached wheel geometry and the car center's track normal to analytically classify each wheel patch from signed offsets. Callers that already computed the center track state pass it in so wheel-surface classification does not repeat the same nearest-track scan. It falls back to full patch sampling near pit connectors and inside pit-lane states. It derives whether the full car is outside the white line and returns the worst wheel surface for physics. `vehiclePhysics.js` consumes that effective surface through the car track state, and separately compares left-side and right-side wheel resistance to add a small capped yaw bias toward the slower side. It does not classify surfaces from the car center.
169
190
 
170
- `src/simulation/trackModel.js` owns track construction, procedural track generation, automatic three-sector metadata, DRS zones, and nearest-track queries. Browser mounts generate a fresh procedural seed when `trackSeed` is omitted; explicit procedural track seeds are cached so repeated mounts with the same seed do not regenerate or rebuild the same model.
191
+ `src/simulation/vehiclePhysics.js` owns vehicle integration using the already-resolved effective surface and wheel-state resistance imbalance.
192
+
193
+ `src/simulation/rules/pitLaneSpeedingSteward.js` owns pit-speed-limit review. `raceSimulation.js` calls it after authoritative race-state recalculation so it reads the same pit-lane part classification as snapshots and track-limit stewarding. The steward only checks speed-limited pit-lane parts (`fast-lane`, `working-lane`, service areas, and garage boxes), leaving pit-entry and pit-exit connector roads legal but not speed-limited.
194
+
195
+ `src/simulation/trackModel.js` owns track construction, procedural track generation, automatic three-sector metadata, DRS zones, and nearest-track queries. Browser mounts generate a fresh procedural seed when `trackSeed` is omitted; explicit procedural track seeds are cached so repeated mounts with the same seed do not regenerate or rebuild the same model. Procedural generation starts from package-owned spline-like templates with deterministic jitter, validates shape, clearance, local turn accumulation, and per-sample heading jumps, and falls back to another template-derived control set instead of an oval control ring when attempts fail.
171
196
 
172
197
  `src/simulation/units.js` owns conversion between simulator units and public meter/km/h display values. Physics stays in simulator units; snapshots expose calibrated display fields such as `speedKph`, `distanceMeters`, and `gapMeters`.
173
198
 
174
- `src/rendering/renderSnapshot.js` owns interpolation for rendering.
199
+ `src/rendering/renderSnapshot.js` owns interpolation for rendering. It accepts either the full public snapshot or the lean render snapshot; render-only callers should prefer the lean snapshot.
175
200
 
176
- The app runtime pauses its PixiJS ticker when the race canvas is outside the viewport or the document is hidden, then resets the frame clock before resuming. This prevents host pages with several simulator embeds from running every race while only one is visible, and avoids a large simulation catch-up step when the canvas re-enters view. Camera safe-area layout measurements are cached and invalidated by resize observation so the 60 FPS render path does not force repeated DOM geometry reads. Runtime readouts avoid redundant DOM writes for unchanged text, timing markup, finish classification, and static selected-car overview data. Long project-radio delays are also treated as stale schedule state instead of replaying every missed lower-third transition.
201
+ The app runtime pauses its PixiJS ticker when the race canvas is outside the viewport or the document is hidden, then resets the frame clock before resuming. This prevents host pages with several simulator embeds from running every race while only one is visible, and avoids a large simulation catch-up step when the canvas re-enters view. Camera safe-area layout measurements are cached and invalidated by resize observation so the 60 FPS render path does not force repeated DOM geometry reads. Camera framing derives a cached active-track bounds box from generated samples plus pit-lane extent; overview uses that frame directly, and every camera uses it as the zoom-out floor. The first camera frame is applied immediately, then later camera mode changes interpolate target position and scale so leader/selected/overview/show-all/pit switches do not jump the world view. Runtime readouts avoid redundant DOM writes for unchanged text, timing markup, finish classification, and static selected-car overview data, and `updateDom()` reuses its supplied full snapshot for camera-control availability instead of taking another public snapshot. Static pit-lane status graphics are redrawn only when their status/geometry key changes, and DRS trail histories are pruned in place. Startup marks the initial DOM refresh time so the first ticker frame does not immediately redo the same full readout update. At `5x` and `10x` browser playback, noncritical DOM/timing refreshes are throttled more aggressively while fixed-step simulation, event emission, and rendering continue from authoritative simulation state. Long project-radio delays are also treated as stale schedule state instead of replaying every missed lower-third transition.
177
202
 
178
203
  ## Data Layer
179
204
 
@@ -189,29 +214,38 @@ The app runtime pauses its PixiJS ticker when the race canvas is outside the vie
189
214
 
190
215
  ## UI Shell
191
216
 
192
- `src/ui/componentTemplates.js` owns the generated markup for individual UI surfaces:
217
+ `src/ui/componentTemplates.js` owns the generated markup and composition for non-banner UI surfaces:
193
218
 
194
219
  - Race controls.
195
220
  - Camera controls.
196
221
  - Safety-car control.
197
222
  - Timing tower.
198
223
  - Race canvas.
199
- - Race-data panel, including the optional project telemetry detail variant.
200
224
  - Telemetry stack template.
201
225
  - Detached telemetry core, sector graph, lap-time table, and sector-time table.
202
- - Broadcast sector banner.
203
226
  - Race telemetry drawer template.
204
227
  - Car/driver overview panel.
205
228
 
206
- `src/ui/shellTemplate.js` composes those component templates into the default all-in-one simulator DOM and package-owned layout presets such as `left-tower-overlay`. Telemetry graph/table surfaces are independent package-owned components controlled by `ui.telemetryModules` when used through stack/drawer templates; the app renders them from `car.lapTelemetry` without requiring host-owned DOM. The telemetry stack can embed the car/driver overview, but the overview is also a separately mountable package-owned component. The broadcast sector banner is a separate lower-third-style sector graph that can be mounted standalone or embedded inside the race canvas only when a host explicitly asks for that independent popup; `F1SimulatorApp` binds the selected car name/code/color to it through the same readout path as the other telemetry surfaces. The race telemetry drawer template composes a race canvas with embedded timing tower, project/radio lower-third, safety-car control, and detached telemetry components in a right drawer. Its open/close state is owned by `F1SimulatorApp`; the safety-car control remains available while telemetry is open. The drawer reserves final race-view space with a stable margin while the sidebar itself animates with a compositor transform, avoiding grid-template reflow during the slide. The drawer race area inherits the workbench height so host embeds do not leave unused black space below the simulation. The left-tower overlay preset is responsible for internal component placement and package-owned proportions; the project/radio lower-third remains owned by the race canvas so it can either use the race space beside the timing tower in `auto` sizing mode or overlap the tower when space is constrained. Composable hosts can also ask the race-canvas template to embed the timing tower directly with `includeTimingTower`, using the same expand-vs-scroll vertical fit contract. `F1SimulatorApp` measures the resulting timing-tower gutter when framing the PixiJS camera, whether the tower comes from the prebuilt shell or from an embedded composable race canvas. On narrow hosts, CSS stacks timing boards full-width and the camera safe-area measurement treats those boards as stacked content instead of side gutters. Hosts should not provide the internal simulator markup or tune preset internals with raw sizing options.
229
+ `src/ui/bannerTemplates.js` owns package banner markup:
230
+
231
+ - Top steward message for warnings and penalties.
232
+ - Shared race-data lower-third used by project and radio modes.
233
+ - Optional project telemetry detail inside the race-data lower-third.
234
+ - Standalone broadcast sector banner.
235
+
236
+ `src/ui/templateUtils.js` owns shared template helpers such as escaping and loading-state markup. Banner templates and non-banner component templates should share helpers through this module instead of importing from each other.
237
+
238
+ `src/ui/shellTemplate.js` composes those component templates into the default all-in-one simulator DOM and package-owned layout presets such as `left-tower-overlay`. Telemetry graph/table surfaces are independent package-owned components controlled by `ui.telemetryModules` when used through stack/drawer templates; the app renders them from `car.lapTelemetry` without requiring host-owned DOM. The telemetry stack can embed the car/driver overview, owns vertical scrolling when the host constrains its height, and is also embedded by the hide/show race telemetry drawer. The overview is also a separately mountable component. The project and radio lower-thirds share the same race-data panel markup; runtime state only swaps content and mode classes. The optional project telemetry detail lives inside that shared lower-third, while the broadcast sector banner remains a separate lower-third-style sector graph that can be mounted standalone or embedded inside the race canvas only when a host explicitly asks for that independent popup. `F1SimulatorApp` binds the selected car name/code/color to it through the same readout path as the other telemetry surfaces. The race telemetry drawer template composes an external top control row, race canvas with embedded timing tower, project/radio lower-third, top steward message, safety-car control, and the telemetry stack in a right drawer. Its open/close state is owned by `F1SimulatorApp`; the safety-car control, project/radio banner mute toggle, and telemetry toggle stay in the top row while telemetry is open. The drawer reserves final race-view space with a stable margin while the sidebar itself animates with a compositor transform, avoiding grid-template reflow during the slide. The drawer race area uses the configured workbench height so host embeds do not leave unused black space below the simulation. The left-tower overlay preset is responsible for internal component placement and package-owned proportions; the project/radio lower-third remains owned by the race canvas so it can either use the race space beside the timing tower in `auto` sizing mode or overlap the tower when space is constrained. Composable hosts can also ask the race-canvas template to embed the timing tower directly with `includeTimingTower`, using the same expand-vs-scroll vertical fit contract. `F1SimulatorApp` measures the resulting timing-tower gutter when framing the PixiJS camera, whether the tower comes from the prebuilt shell or from an embedded composable race canvas. It also owns camera mode availability, wheel/button zoom for every camera mode, temporary project/radio banner muting, hiding the pit camera control when the active track has no pit-lane geometry, and fitting the pit camera from operational `track.pitLane` lane, box, service, and queue bounds instead of host-provided coordinates or long access-road endpoints. On narrow hosts, CSS stacks timing boards full-width and the camera safe-area measurement treats those boards as stacked content instead of side gutters. Hosts should not provide the internal simulator markup or tune preset internals with raw sizing options.
239
+
240
+ Penalty UI is display-only. `F1SimulatorApp` may show track-limit warning events and `snapshot.penalties` in the top steward message, and may show timing-row penalty badges when the host enables those UI options, but the UI must not infer, modify, or recalculate steward decisions.
207
241
 
208
242
  `src/config/defaultOptions.js` owns preset resolution, telemetry module normalization, and the public theme contract. Presets are merged before host options; theme fields are applied as package CSS variables by the all-in-one app and composable controller. This keeps sizing/color customization explicit without turning internal layout ratios into public API.
209
243
 
210
- Race completion is owned by `src/simulation/raceSimulation.js`. The app layer reads `raceControl.finished`, `winner`, and `classification` from snapshots, renders the winner banner, and emits lifecycle callbacks. UI code must not infer finish state from lap text or timing rows.
244
+ Race completion is owned by `src/simulation/raceSimulation.js`. The simulation records provisional `finishOrder` / `finishRank` as each car crosses the line and uses that order for snapshots until final classification exists, so app code never has to re-sort finished cars by live distance. The app layer reads `raceControl.finished`, `winner`, `classification`, and per-car `raceStatus` from snapshots, renders the timing/winner states, and emits lifecycle callbacks. Penalty lifecycle, pit-stop penalty service, service conversion, time adjustment, position drops, grid drops, and disqualification effects are applied inside the simulation, not in the UI. UI code may display countdowns from `pitStop.phase`, `serviceRemainingSeconds`, and `penaltyServiceRemainingSeconds`, but it must not infer, serve, or recalculate steward outcomes from lap text, timing rows, or car position.
211
245
 
212
246
  Composable hosts may choose where each package-owned component root is placed, but they still receive package-generated markup through the public mount functions. The controller marks each mounted root as an `f1-sim-component` styling scope so standalone pieces receive the same package variables as the all-in-one shell.
213
247
 
214
- `src/styles.css` styles the generated shell, imports package fonts, caps timing-tower width for readability, and owns the fixed-height timing-list scroll behavior. Timing rows are fixed grid rows stacked from the top, so rank positions do not stretch or redistribute when the number of entries changes. The timing tower owns its runtime interval-vs-leader-gap switch, while `F1SimulatorApp` reads both timing values from the race snapshot. Component templates include lightweight package-owned loading overlays; `F1SimulatorApp` removes them after startup initialization has completed.
248
+ `src/styles.css` styles the generated shell, imports package fonts, caps timing-tower width for readability, and owns the fixed-height timing-list scroll behavior. Timing rows are fixed grid rows stacked from the top, so rank positions do not stretch or redistribute when the number of entries changes. The timing tower owns its runtime interval-vs-leader-gap switch, while `F1SimulatorApp` reads both seconds and whole-lap gap values from the race snapshot and formats positive lap deficits as `+N` before seconds. Fixed hidden timing-line creation, crossing timestamps, and seconds-gap calculation stay inside `src/simulation/raceSimulation.js`. Sector-map surfaces read `lapTelemetry.sectorProgress` for bar fill and `lapTelemetry.liveSectors` for live active-sector time; the simulation owns those values so UI components do not infer active split state from DOM state. Future sectors are cleared by the simulation and ignored defensively by the renderer, so banner/sidebar sector nodes cannot keep stale timing text or yellow/purple/green performance classes after the active sector changes. Component templates include lightweight package-owned loading overlays; `F1SimulatorApp` removes them after startup initialization has completed.
215
249
 
216
250
  The host page should only provide:
217
251
 
package/docs/concepts.md CHANGED
@@ -55,6 +55,8 @@ The composable controller has a setup phase:
55
55
 
56
56
  After startup, both controller styles expose runtime methods such as `restart()`, `selectDriver()`, `setSafetyCarDeployed()`, `getSnapshot()`, and `destroy()`.
57
57
 
58
+ Pit-stop hosts can also call `setPitIntent(driverId, 0 | 1 | 2)`. `0` means no pending pit request, `1` means take the next pit entry only if it is free, and `2` means keep the request active until the automatic pit-stop sequence completes.
59
+
58
60
  ## Driver
59
61
 
60
62
  A driver is the host-facing entity shown as a race entry. In the portfolio use case, each driver maps to a project.
@@ -116,14 +118,16 @@ Vehicle ratings are `0-100` values converted into physical setup values:
116
118
 
117
119
  ## Track
118
120
 
119
- The track is a closed sampled centerline with width, kerbs, runoff, DRS zones, and surface classification.
121
+ The track is a closed sampled centerline with width, kerbs, runoff, DRS zones, pit-lane geometry, and surface classification.
120
122
 
121
123
  The package supports:
122
124
 
123
125
  - A default named track for low-level simulation callers that provide neither a track nor a track seed.
124
126
  - Procedural browser tracks from generated or explicit `trackSeed` values.
125
127
 
126
- Browser mounts that omit `trackSeed` create a fresh procedural track for that mount. Passing `trackSeed` makes the generated circuit deterministic; repeated procedural seeds are cached within the page runtime.
128
+ Browser mounts that omit `trackSeed` create a fresh procedural track for that mount. Passing `trackSeed` makes the generated circuit deterministic; repeated procedural seeds are cached within the page runtime. Generated circuits use validated template-based spline controls rather than a pure oval fallback, so failed candidates retry into another shaped layout instead of degrading into a circular track.
129
+
130
+ Every built track also exposes a deterministic `pitLane` near the start/finish straight. The pit lane has an entry before the start line, an exit after it, explicit lane-aligned entry/exit road centerlines, a straight main fast lane, a parallel working lane, 10 shared team service areas, and 20 unused garage boxes arranged as 10 team pairs. Pit-lane asphalt, service areas, and garage boxes are legal drivable surfaces for sensors, runoff handling, and track-limit stewarding. When the pit-stop module is enabled, cars automatically form bounded pit trains when there is enough rolling gap, brake to the limiter by the main lane start, follow the fast lane, pass through their assigned colored team queue spot, roll into the team service area, change tire compound, and return through the exit. Team-mates share one service area; every car passes through the queue spot first, but it only waits there if the active service area is occupied. A second team car waits without blocking the fast lane before following a short queue-release route into the active service area after the previous car has physically cleared it. Tire condition can request a stop automatically: below the configured request threshold the car asks to pit if free, and below the commit threshold it keeps retrying until served. The speed limiter is active on the straight main pit lane/working lane, not on the entry and exit connector roads.
127
131
 
128
132
  ## Progress
129
133
 
@@ -148,9 +152,20 @@ Snapshots include:
148
152
  - Safety car state.
149
153
  - Current rules.
150
154
  - Events from the last step.
155
+ - Penalty ledger.
151
156
  - Ordered cars with telemetry and setup data.
152
157
 
153
- Per-car timing exposes both interval to the car ahead and cumulative gap to the leader. Per-car `lapTelemetry` exposes current lap, current sector, current/last/best lap times, current/last/best sector times, sector progress, and sector performance status. Sector performance status marks completed sector times as `overall-best`, `personal-best`, or `slower`, which drives the purple/green/yellow timing colors in sector graphs and tables. Per-car speed and distance display fields are calibrated through the simulator unit conversion helpers instead of treating rendered world units as meters.
158
+ Per-car timing exposes both interval to the car ahead and direct same-lead-lap gap to the leader. Gap seconds are calculated from hidden fixed timing-line crossings around the track, matching the idea of mini-sector timing points instead of measuring a live distance delta and guessing from speed. Per-car `lapTelemetry` exposes current lap, current sector, current/last/best lap times, completed current-lap sector times, live sector elapsed times, per-sector progress, and sector performance status. Sector performance status marks completed sector times as `overall-best`, `personal-best`, or `slower`, which drives the purple/green/yellow timing colors in sector graphs and tables. Per-car speed and distance display fields are calibrated through the simulator unit conversion helpers instead of treating rendered world units as meters, and public timing values are seconds.
159
+
160
+ ## Rulesets And Modules
161
+
162
+ A ruleset is a named preset for race-rule defaults. `paddock` is the package default, `grandPrix2025` / `fia2025` are 2024-2025-era grand-prix-style presets, and `custom` is for host-owned behavior.
163
+
164
+ A rule module is an advanced subsystem under `rules.modules`, such as pit stops, tire strategy, penalties, weather, reliability, or fuel load. Presets set defaults, but explicit module config wins.
165
+
166
+ Penalty strictness is a stewarding value from `0` to `1`. `0` means the penalty subsection is not enforced. `1` means the subsection applies close to its configured rule margin.
167
+
168
+ A penalty consequence is the result attached to a steward decision. Supported consequences are warning, time, drive-through, stop-go, position-drop, grid-drop, and disqualification payloads. `penaltySeconds` is the sum of applied time consequences for timing, UI consumers, and final classification ordering; unserved drive-through and stop-go penalties convert into applied time when the final classification is calculated.
154
169
 
155
170
  ## Render Snapshot
156
171
 
@@ -162,6 +177,8 @@ The render snapshot interpolates moving entities between physics ticks for smoot
162
177
 
163
178
  The race data panel is the lower-third UI shown over the track.
164
179
 
180
+ The camera controls own race-view modes, zoom, and a temporary `Mute banners` toggle for project/radio lower-thirds.
181
+
165
182
  It has two modes:
166
183
 
167
184
  - Selected project mode.