@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.
- package/CHANGELOG.md +39 -0
- package/INSTALL_AND_UPDATE.md +3 -1
- package/README.md +142 -9
- package/docs/architecture.md +49 -15
- package/docs/concepts.md +20 -3
- package/docs/data_contract.md +185 -27
- package/docs/data_model.md +1 -1
- package/docs/learnings.md +1 -1
- package/docs/rules.md +140 -19
- package/docs/system_specs.md +39 -16
- package/docs/training.md +103 -1
- package/examples/train-basic-policy.mjs +5 -6
- package/examples/trainingData.mjs +133 -0
- package/package.json +5 -2
- package/src/api/PaddockSimulatorController.js +32 -2
- package/src/app/F1SimulatorApp.js +1101 -128
- package/src/app/domBindings.js +10 -1
- package/src/config/defaultOptions.js +4 -4
- package/src/environment/actions.js +25 -2
- package/src/environment/evaluation.js +184 -0
- package/src/environment/index.d.ts +281 -1
- package/src/environment/index.js +13 -0
- package/src/environment/observationOptions.js +12 -0
- package/src/environment/observations.js +189 -17
- package/src/environment/options.js +7 -0
- package/src/environment/recorder.js +28 -0
- package/src/environment/runtime.js +41 -6
- package/src/environment/scenarios.js +220 -0
- package/src/environment/sensors.js +145 -35
- package/src/environment/specs.js +92 -8
- package/src/environment/workerProtocol.js +52 -0
- package/src/index.d.ts +366 -3
- package/src/index.js +21 -0
- package/src/rendering/proceduralTrackAsset.js +336 -23
- package/src/simulation/collisionGeometry.js +214 -0
- package/src/simulation/driverController.js +431 -65
- package/src/simulation/raceSimulation.js +2132 -171
- package/src/simulation/rules/collisionSteward.js +69 -0
- package/src/simulation/rules/penaltyLedger.js +153 -0
- package/src/simulation/rules/pitLaneSpeedingSteward.js +71 -0
- package/src/simulation/rules/tireRequirementSteward.js +29 -0
- package/src/simulation/rules/trackLimitsSteward.js +85 -0
- package/src/simulation/rulesConfig.js +352 -0
- package/src/simulation/trackModel.js +1154 -76
- package/src/simulation/units.js +6 -4
- package/src/simulation/vehicleGeometry.js +247 -0
- package/src/simulation/vehiclePhysics.js +82 -50
- package/src/simulation/wheelSurface.js +338 -0
- package/src/styles.css +300 -42
- package/src/ui/bannerTemplates.js +69 -0
- package/src/ui/componentTemplates.js +36 -96
- package/src/ui/raceControlStatusBanner.js +39 -0
- package/src/ui/shellTemplate.js +1 -1
- package/src/ui/templateUtils.js +23 -0
package/docs/rules.md
CHANGED
|
@@ -7,15 +7,105 @@ This file documents the rules currently implemented by the simulator. If the rac
|
|
|
7
7
|
Default rules are defined in `src/simulation/raceSimulation.js` as `DEFAULT_RULES`:
|
|
8
8
|
|
|
9
9
|
- DRS detection gap: `1` second.
|
|
10
|
-
- Safety car speed: `
|
|
11
|
-
- Safety car lead distance: `
|
|
12
|
-
- Safety car queue gap: `
|
|
10
|
+
- Safety car speed: `166 km/h`.
|
|
11
|
+
- Safety car lead distance: about `55m`.
|
|
12
|
+
- Safety car queue gap: about `22m`.
|
|
13
13
|
- Collision restitution: `0.18`.
|
|
14
14
|
- Standing start enabled by default.
|
|
15
15
|
- Start lights: `5`.
|
|
16
16
|
- Start light interval: `0.72` seconds.
|
|
17
17
|
- Lights-out hold: `0.78` seconds.
|
|
18
18
|
|
|
19
|
+
Race rules are normalized before the simulation starts. Hosts can choose a package ruleset preset with `rules.ruleset` or `rules.profile`:
|
|
20
|
+
|
|
21
|
+
- `paddock`: the simplified package default.
|
|
22
|
+
- `grandPrix2025`: a 2024-2025-era grand-prix-style preset.
|
|
23
|
+
- `fia2025`: an alias for `grandPrix2025` for hosts that prefer that name.
|
|
24
|
+
- `custom`: starts from the package defaults and applies host-provided module options.
|
|
25
|
+
|
|
26
|
+
The preset only chooses defaults. Explicit `rules.modules` values override the preset.
|
|
27
|
+
|
|
28
|
+
## Rule Modules
|
|
29
|
+
|
|
30
|
+
Advanced race behavior is organized under `rules.modules` so hosts can enable, disable, or tune each system independently:
|
|
31
|
+
|
|
32
|
+
```js
|
|
33
|
+
rules: {
|
|
34
|
+
ruleset: 'fia2025',
|
|
35
|
+
modules: {
|
|
36
|
+
pitStops: {
|
|
37
|
+
enabled: true,
|
|
38
|
+
pitLaneSpeedLimitKph: 80,
|
|
39
|
+
defaultStopSeconds: 2.8,
|
|
40
|
+
maxConcurrentPitLaneCars: 3,
|
|
41
|
+
minimumPitLaneGapMeters: 20,
|
|
42
|
+
doubleStacking: false,
|
|
43
|
+
tirePitRequestThresholdPercent: 50,
|
|
44
|
+
tirePitCommitThresholdPercent: 30,
|
|
45
|
+
},
|
|
46
|
+
tireStrategy: {
|
|
47
|
+
enabled: true,
|
|
48
|
+
compounds: ['S', 'M', 'H'],
|
|
49
|
+
mandatoryDistinctDryCompounds: 2,
|
|
50
|
+
},
|
|
51
|
+
penalties: {
|
|
52
|
+
enabled: true,
|
|
53
|
+
stewardStrictness: 0.85,
|
|
54
|
+
trackLimits: { strictness: 0.85, consequences: [{ type: 'time', seconds: 5 }] },
|
|
55
|
+
collision: { strictness: 0.65, consequences: [{ type: 'time', seconds: 5 }] },
|
|
56
|
+
tireRequirement: { strictness: 1, consequences: [{ type: 'time', seconds: 10 }] },
|
|
57
|
+
pitLaneSpeeding: { strictness: 1, speedLimitKph: 80 },
|
|
58
|
+
},
|
|
59
|
+
weather: { enabled: false },
|
|
60
|
+
reliability: { enabled: false },
|
|
61
|
+
fuelLoad: { enabled: true },
|
|
62
|
+
},
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
The current implementation normalizes and exposes all module config, records a penalty ledger, enforces collision penalties, track-limit penalties, tire-requirement penalties, and pit-lane speeding penalties, creates/renders pit-lane geometry for every track, treats pit-lane asphalt, working-lane service areas, and garage boxes as legal drivable surfaces, and runs a first automated pit-stop pass when `pitStops.enabled` is true. Weather effects, reliability failures, and fuel-load performance effects are staged behind the module contract and are not fully simulated yet.
|
|
67
|
+
|
|
68
|
+
## Steward Strictness
|
|
69
|
+
|
|
70
|
+
Penalty subsections use `strictness` from `0` to `1` instead of a boolean:
|
|
71
|
+
|
|
72
|
+
- `1`: enforce close to the configured rule.
|
|
73
|
+
- `0`: do not enforce that subsection.
|
|
74
|
+
- Values between `0` and `1` increase the margin before a rule applies.
|
|
75
|
+
|
|
76
|
+
The simulator clamps invalid strictness values into the `0..1` range. `penalties.stewardStrictness` multiplies each subsection strictness, so hosts can make all stewards more lenient or stricter while still preserving per-rule tuning.
|
|
77
|
+
|
|
78
|
+
Supported penalty subsections:
|
|
79
|
+
|
|
80
|
+
- `trackLimits`
|
|
81
|
+
- `collision`
|
|
82
|
+
- `tireRequirement`
|
|
83
|
+
- `pitLaneSpeeding`
|
|
84
|
+
|
|
85
|
+
Track-limit enforcement uses the white line as the legal edge. The steward checks the four wheel contact patches and records a violation only when all four patches are fully beyond the same side of the white line by more than the strictness-adjusted margin. Touching the line, riding a kerb, or having only some wheels beyond the line is not enough. Kerbs remain a different surface for grip/drag, but they are inside track limits. Warning decisions are emitted as `track-limits` events with `decision: 'warning'`, `violationCount`, and `warningsBeforePenalty`; penalty decisions are also recorded in `snapshot.penalties` and emitted as `penalty` events in the same step. The active-excursion state remains continuous, so staying fully outside for several frames counts as one excursion until at least one wheel returns inside/legal.
|
|
86
|
+
|
|
87
|
+
The built-in driver AI is expected to respect that same white-line rule through normal control inputs. Its racing-line planner keeps a centerline comfort margin for the car footprint, and the controller progressively lifts, applies mild braking when needed, and steers back inward when the car approaches the legal edge. This does not let the car defy physics; it only changes when the default AI chooses to brake, coast, and steer. Kerbs still allow normal recovery speed rather than gravel-style stopping behavior.
|
|
88
|
+
|
|
89
|
+
Pit-lane surfaces are legal road for track-limit purposes. `pit-entry`, `pit-lane`, `pit-exit`, and `pit-box` track states set `inPitLane: true`, so ray sensors, runoff response, and the track-limit steward do not treat normal pit entry, service, or exit as off-track excursions.
|
|
90
|
+
|
|
91
|
+
Penalty decisions are recorded in `snapshot.penalties` and emitted as `penalty` events in the same step. Each entry includes the penalty type, driver id, strictness, status, penalty seconds, pending service conversion seconds, lap, timestamp, and rule-specific context. Multiple time penalties for the same driver are additive: two separate +5s entries produce `penaltySeconds: 10` on that car's snapshot and classification adjustment.
|
|
92
|
+
|
|
93
|
+
Each penalty subsection can define `consequences`. Supported consequences are:
|
|
94
|
+
|
|
95
|
+
- `{ type: 'warning' }`
|
|
96
|
+
- `{ type: 'time', seconds: 5 }`
|
|
97
|
+
- `{ type: 'time', seconds: 10 }`
|
|
98
|
+
- `{ type: 'time', seconds: 20 }`
|
|
99
|
+
- `{ type: 'driveThrough', conversionSeconds: 20 }`
|
|
100
|
+
- `{ type: 'stopGo', seconds: 10, conversionSeconds: 30 }`
|
|
101
|
+
- `{ type: 'positionDrop', positions: 1 }`
|
|
102
|
+
- `{ type: 'gridDrop', positions: 3 }`
|
|
103
|
+
- `{ type: 'disqualification' }`
|
|
104
|
+
|
|
105
|
+
Penalty status is part of the simulation model. Immediate consequences use `applied`; drive-through and stop-go consequences start as `issued`, can become `served` through the controller API or pit-stop service, can be `cancelled`, and convert to `applied` time penalties if still unserved when final classification is built. During a pit stop, eligible penalties are served before tire work starts: applied time penalties add their seconds as a stationary hold, stop-go penalties add their configured `seconds`, and drive-through penalties are marked served by the pit-lane traversal without adding stationary hold time.
|
|
106
|
+
|
|
107
|
+
`penaltySeconds` is derived from applied time consequences for compatibility with timing/UI consumers. Rules without explicit consequences default to their existing time penalty seconds. If a time or stop-go penalty is served in the pit box, those seconds are spent before the tire change and the penalty no longer contributes to final adjusted time. At final classification, remaining time consequences and unserved service conversions are added to the driver's finish time; the classified order is sorted by `finishTime + penaltySeconds`, with raw finish order used only as a tie-breaker. Position-drop consequences then move classified drivers down by the configured number of places, and disqualified drivers are classified after non-disqualified finishers. Grid-drop consequences apply during `pre-start` by moving the driver down the grid.
|
|
108
|
+
|
|
19
109
|
## Race Modes
|
|
20
110
|
|
|
21
111
|
The simulator has three race-control modes:
|
|
@@ -28,9 +118,11 @@ Safety car deployment is ignored during `pre-start`.
|
|
|
28
118
|
|
|
29
119
|
## Standing Start
|
|
30
120
|
|
|
121
|
+
Every race initializes cars into staggered two-column start slots on the normalized start straight. Standing starts use compact grid spacing. Already-released green starts keep wider rolling-start longitudinal gaps, but still alternate left/right slot offsets instead of placing every car on the centerline. `standingStart` controls whether those slots are locked behind start lights or immediately released into green-flag running.
|
|
122
|
+
|
|
31
123
|
When standing start is enabled:
|
|
32
124
|
|
|
33
|
-
-
|
|
125
|
+
- Procedural tracks normalize the start/finish area into an explicit straight so the grid and immediate launch area are not placed on a curved segment.
|
|
34
126
|
- Cars stay locked until the start-light sequence releases them.
|
|
35
127
|
- Start lights increment over time.
|
|
36
128
|
- When lights go out, cars release and race mode becomes `green`.
|
|
@@ -43,27 +135,28 @@ Race order is based on `raceDistance`, descending. Ties fall back to original dr
|
|
|
43
135
|
|
|
44
136
|
During safety car, order is frozen at deployment time. That prevents passing from reshuffling the timing tower while the safety car is active.
|
|
45
137
|
|
|
46
|
-
Timing history is sampled per car and used to
|
|
138
|
+
The simulation builds hidden timing lines around every track at an F1-style mini-sector spacing target of roughly `150m..200m`. Timing history is sampled per car and timing-line crossing timestamps are used to calculate:
|
|
47
139
|
|
|
48
140
|
- Interval to the car ahead as `intervalAheadSeconds` / `gapAheadSeconds`.
|
|
49
|
-
-
|
|
141
|
+
- Direct same-lead-lap gap to the leader as `leaderGapSeconds`.
|
|
142
|
+
- Whole-lap deficits as `intervalAheadLaps` / `gapAheadLaps` and `leaderGapLaps`.
|
|
50
143
|
- DRS detection timing.
|
|
51
144
|
|
|
52
|
-
The timing tower can switch at runtime between `Int` mode, which displays interval to the car ahead, and `Gap` mode, which displays
|
|
145
|
+
Seconds gaps are the difference between when two cars crossed the same timing line. The race engine falls back to timing-history interpolation only before the cars have a shared timing-line sample. The timing tower can switch at runtime between `Int` mode, which displays interval to the car ahead, and `Gap` mode, which displays direct gap to the leader. When the relevant gap is one or more whole laps, the tower shows `+1`, `+2`, and so on instead of a misleading seconds estimate. Timing continues to be calculated during pre-start, safety-car, and post-finish states even when the UI shows state labels such as `Grid`, `SC`, or `FIN`.
|
|
53
146
|
|
|
54
147
|
## Units
|
|
55
148
|
|
|
56
|
-
The race engine uses simulator units internally. `src/simulation/units.js` converts simulator distance and speed to public meter and km/h values. The current speed calibration maps the simulation maximum speed to an F1-like `330 km/h`; rendered car sprite dimensions are a visual scale and are not used as the physical distance scale.
|
|
149
|
+
The race engine uses simulator units internally. `src/simulation/units.js` converts simulator distance and speed to public meter and km/h values. Public timer, lap-time, sector-time, gap-time, penalty-time, and service-countdown values are seconds. Public distance values with a `Meters` suffix are meters. The current speed calibration maps the simulation maximum speed to an F1-like `330 km/h`; rendered car sprite dimensions are a visual scale and are not used as the physical distance scale.
|
|
57
150
|
|
|
58
151
|
## Laps
|
|
59
152
|
|
|
60
153
|
Lap is computed from each car's cumulative race distance over the track length. Total laps are provided by mount options and default to `10`.
|
|
61
154
|
|
|
62
|
-
Each track is automatically divided into three equal sectors. Sector and lap telemetry is recorded when a car's cumulative race distance crosses a sector boundary or start/finish boundary. Timing values are stored in seconds and exposed on each car snapshot as `lapTelemetry`.
|
|
155
|
+
Each track is automatically divided into three equal sectors. Sector and lap telemetry is recorded when a car's cumulative race distance crosses a sector boundary or start/finish boundary. Completed current-lap sector split times before the active sector are exposed as `currentSectors`, while `liveSectors` keeps only the active sector's elapsed time updated before the boundary is crossed and clears any future-sector values. `sectorProgress` is recomputed every snapshot as the live S1/S2/S3 fill state for the current lap, so sector-map UI does not derive progress from completed split state or keep stale future sectors lit. Timing values are stored in seconds and exposed on each car snapshot as `lapTelemetry`.
|
|
63
156
|
|
|
64
|
-
Each car is marked `finished` when it reaches `track.length * totalLaps`.
|
|
157
|
+
Each car is marked `finished` when it reaches `track.length * totalLaps`. It receives a frozen provisional `finishRank`, exposes `raceStatus: 'waved-flag'` / `wavedFlag: true`, and emits a `car-finish` event. The first finisher becomes provisional `winner`, but the race keeps running until every car has crossed the finish distance. While the field is still finishing, timing order keeps already-finished cars in provisional finish order ahead of cars still running, so a waved-through car cannot lose timing-table position just because another car circulates farther before final classification.
|
|
65
158
|
|
|
66
|
-
When all cars have finished, the simulator records `finishedAt` and final `classification`, emits a `race-finish` event, freezes order to the classified result, deploys the safety car, and switches race mode to `safety-car`. Finished cars keep circulating under safety-car behavior instead of hard-stopping. The current implementation does not yet implement
|
|
159
|
+
When all cars have finished, the simulator records `finishedAt` and final `classification`, emits a `race-finish` event, freezes order to the classified result after outstanding penalties and classification consequences, deploys the safety car, and switches race mode to `safety-car`. Finished cars keep circulating under safety-car behavior instead of hard-stopping. The current implementation does not yet implement championship scoring.
|
|
67
160
|
|
|
68
161
|
## DRS
|
|
69
162
|
|
|
@@ -72,7 +165,7 @@ DRS behavior:
|
|
|
72
165
|
- DRS is disabled during safety car.
|
|
73
166
|
- Each track has DRS zones.
|
|
74
167
|
- A car latches into a DRS zone when it crosses that zone start.
|
|
75
|
-
- A car becomes DRS eligible if it was close enough to the car
|
|
168
|
+
- A car becomes DRS eligible if it was close enough to the physically-ahead car at the relevant detection crossing, including lapped traffic.
|
|
76
169
|
- The current detection window is controlled by `drsDetectionSeconds`.
|
|
77
170
|
- When eligible inside the latched zone, `drsActive` becomes true.
|
|
78
171
|
|
|
@@ -93,7 +186,9 @@ Safety car behavior:
|
|
|
93
186
|
- Race order freezes.
|
|
94
187
|
- DRS state is cleared for all cars.
|
|
95
188
|
- Driver aggression is reduced.
|
|
96
|
-
-
|
|
189
|
+
- The safety car tracks a target about `55m` ahead of the leader instead of continuously pulling away once that target is reached.
|
|
190
|
+
- Cars target compact frozen-order queue slots about `22m` apart behind the safety car.
|
|
191
|
+
- Safety-car queueing uses conservative corner speed and centerline correction so the train stays on the resized meter-calibrated track.
|
|
97
192
|
- Timing values continue to be calculated, but gaps in the timing tower display as `SC` for non-leaders.
|
|
98
193
|
|
|
99
194
|
When safety car is cleared:
|
|
@@ -121,9 +216,19 @@ Green-flag behavior uses:
|
|
|
121
216
|
- Patience.
|
|
122
217
|
- Current tire energy.
|
|
123
218
|
- Track curvature.
|
|
219
|
+
- Capped preview curvature for braking decisions.
|
|
220
|
+
- Forward track-heading feed-forward for corner entry.
|
|
124
221
|
- Nearby traffic.
|
|
125
222
|
- Preferred and available lane offsets.
|
|
126
223
|
|
|
224
|
+
The built-in racing controller still drives through normal vehicle inputs only. It chooses a forward racing target, blends steering from the target angle, upcoming track heading, lateral offset, edge recovery pressure, and a corner-apex racing-line offset, then sends clamped steering/throttle/brake values into the vehicle physics integrator. It is expected to use the available track width and kerbs on generated circuits, including sharp generated turns. When heading alignment or steering load shows the car cannot yet carry throttle through a corner, throttle is cut before braking is requested so the controller does not fight itself. It does not snap car position, override heading, or bypass steering-rate/yaw-rate limits.
|
|
225
|
+
|
|
226
|
+
Traffic decisions use real meter-scaled gaps and lateral spacing, not raw simulator-size constants. A trailing car inside the attack window commits toward a visible passing lane instead of sitting on the centerline. A leading car with a close rear threat can defend the threatened side before the rival is fully alongside. Side-by-side overlap still creates spacing pressure so defensive and attacking moves are expressed as target-line choices and normal steering/brake/throttle inputs, not as collision avoidance teleports or artificial position overrides.
|
|
227
|
+
|
|
228
|
+
Rejoin behavior also remains physics-driven. When a car leaves the legal racing surface, the controller enters a short recovery hold and keeps using rejoin controls after the center point first reaches track or kerb until heading and edge margin are stable. That prevents the car from immediately returning to full racing throttle while still pointed across the track. Recovery targets look forward along the racing surface, bias inward from the edge, and request bounded surface-specific speeds for track, kerb, grass, gravel, and barrier states.
|
|
229
|
+
|
|
230
|
+
Tire energy can degrade to 1%. The vehicle physics layer converts tire energy into a nonlinear grip factor, so degradation has a visible performance cost across the full 100% to 1% range while still leaving a damaged car controllable enough to return to the pits.
|
|
231
|
+
|
|
127
232
|
## Vehicle Physics
|
|
128
233
|
|
|
129
234
|
Vehicle physics live in `src/simulation/vehiclePhysics.js`.
|
|
@@ -149,34 +254,50 @@ Track state can classify a car as on:
|
|
|
149
254
|
|
|
150
255
|
- `track`
|
|
151
256
|
- `kerb`
|
|
257
|
+
- `pit-entry`
|
|
258
|
+
- `pit-lane`
|
|
259
|
+
- `pit-exit`
|
|
260
|
+
- `pit-box`
|
|
152
261
|
- `gravel`
|
|
153
262
|
- `grass`
|
|
154
263
|
- `barrier`
|
|
155
264
|
|
|
156
265
|
Surface affects grip, drag, and rolling resistance.
|
|
157
266
|
|
|
267
|
+
Surface physics is wheel-based. `src/simulation/wheelSurface.js` classifies each wheel contact patch against the track model and the car uses the worst current surface for physics: `barrier > gravel > grass > kerb > pit-box > pit-lane > pit-entry/pit-exit > track`. Normal main-track running uses an analytic signed-offset fast path from cached wheel geometry, while pit-lane and pit-connector edge cases use full patch sampling. One wheel on gravel slows the car with gravel behavior even if the other three wheels are on asphalt. In addition, `vehiclePhysics.js` compares left-side and right-side wheel resistance and applies a small capped yaw-rate bias toward the slower side, so asymmetric gravel/grass/kerb contact can tug the car toward the dirty side without becoming a full tire-force simulation. One wheel on kerb reports `kerb` and applies kerb-level grip/drag without causing a track-limit violation by itself. Pit entry, pit lane, pit exit, and pit-box surfaces remain legal drivable surfaces.
|
|
268
|
+
|
|
269
|
+
When pit stops are enabled, each team is assigned one shared service area in the working lane plus two garage boxes. Driver pairs share the service area, and the service area, queue slot, and garage boxes inherit the team color from `driver.team.color` when present, otherwise the lead driver's color. The current automatic stop plan distributes pit calls across available race laps as bounded pit trains, then lets opportunistic cars join the entry only when fewer than `pitStops.maxConcurrentPitLaneCars` are active and the nearest active pit-lane car is at least `pitStops.minimumPitLaneGapMeters` ahead; committed cars enter at the next pit-entry window and can queue in the working lane if their team service area is busy. Built-in AI cars also request stops from tire condition: below `pitStops.tirePitRequestThresholdPercent` they request `pitIntent: 1`, and below `pitStops.tirePitCommitThresholdPercent` they request `pitIntent: 2`. Expert-controlled cars do not receive those tire-threshold automatic calls; their model or host must request the stop with `pitIntent`. Hosts and expert actions can change a pending stop with `pitIntent`: `0` clears the pending call, `1` stays active until a free-enough pit-entry window appears, and `2` commits to entering at the next pit-entry window even if capacity or gap checks would block mode `1`. The same request may include a target compound, either through `setPitIntent(driverId, intent, targetCompound)` or expert action `pitCompound`; invalid compounds are rejected. Pit intent and target compound are locked while the car is entering, queued, servicing, or exiting, and intent resets to `0` after pit exit. Completed stops can be re-armed by later tire condition or host intent, so pit stops are not one-use per race. Safety-car mode does not block pit intent. Closed pit-lane race-control state keeps pending calls on track until `setPitLaneOpen(true)`, while red-flag state freezes cars and closes the effective pit lane until cleared. If the gap is too small for mode `1`, the following car stays on the main track and retries later rather than being forced to stop in the lane. Allowed cars steer through a forward main-track approach into the pit-entry road, slow to the configured limiter speed before the main pit-lane start, follow the main fast lane, pass through the team-colored queue point, hold for any pit-served penalties, hold for the resolved tire-service time, change to the requested compound or default alternate compound, add that compound to `usedTireCompounds`, then steer back to the racing surface through `pit-exit`. While a car is stationary in service, the browser renderer shows a small countdown above it: red `+Ns` for pit-served penalty hold time and yellow `Ns` for normal pit-service time. The queue point is a rolling gate when the active service area is free, so the car continues into service without a full stop. If a team-mate is already servicing or still exiting from the active service area, the arriving car targets the queue point behind that service area, holds a bounded approach speed until the final capture distance, waits without obstructing the main fast lane, then follows a short queue-release route only after the active service area is physically clear; this keeps both the queue stop and the move into service continuous instead of snapping the car between slots or crawling through the last several meters. Cars whose position is controlled by pit entry, queue, service, or exit logic are not laterally displaced by generic collision correction; if another car overlaps one, the movable car is corrected away while the pit-controlled car stays on its authored route or stop point. `pitStops.variability.enabled` optionally adjusts service time from `team.pitCrew.speed`, `team.pitCrew.consistency`, and `team.pitCrew.reliability`; `pitStops.variability.perfect: true` forces `pitStops.defaultStopSeconds` for training. The pit limiter applies only on the straight main pit lane and working lane between the lane start and lane end; the pit-entry connector road and pit-exit connector road are legal pit-lane surfaces but are not speed-limited by `pitStops.pitLaneSpeedLimitKph`, although automatic entry routing caps connector overspeed so worn-tire cars can still reach the limiter and assigned service area.
|
|
270
|
+
|
|
158
271
|
## Contact And Collision Handling
|
|
159
272
|
|
|
160
273
|
Collision handling uses:
|
|
161
274
|
|
|
162
|
-
-
|
|
163
|
-
-
|
|
275
|
+
- Shared vehicle geometry from `src/simulation/vehicleGeometry.js`.
|
|
276
|
+
- A body collision hull for car-vs-car contact.
|
|
277
|
+
- Four wheel/contact-patch shapes for surface and track-limit state, not for car-vs-car contact.
|
|
278
|
+
- Track-progress candidate pruning before narrow-phase checks.
|
|
279
|
+
- SAT overlap checks for body/body contact.
|
|
280
|
+
- Swept broadphase and conservative substep checks so fast cars cannot tunnel through each other between fixed steps.
|
|
164
281
|
- Limited position correction.
|
|
165
282
|
- Contact velocity response.
|
|
166
283
|
- Yaw nudges.
|
|
167
284
|
- Contact cooldown.
|
|
168
285
|
|
|
169
|
-
The
|
|
286
|
+
The collision footprint intentionally matches the visible main car body instead of the transparent sprite rectangle. Empty sprite corners and wheel-only overlap do not count as car-vs-car contact. Longitudinal protection can only assist after swept body geometry confirms a real shape contact; it cannot invent a collision from spacing alone. Contact events expose `firstShapeId`, `secondShapeId`, `contactType`, `depth`, and `timeOfImpact` for debug views and host callbacks.
|
|
287
|
+
|
|
288
|
+
When collision stewarding is enabled, fresh contact is reviewed against impact severity, closing speed, and whether one car clearly hit another from behind. Physical track order is used for rear-contact fault, so lapped traffic is judged by who is actually ahead on the circuit rather than by cumulative race distance. Low-speed/light contact is treated as a racing incident. For meaningful rear contact, the trailing car receives the only penalty and the entry records `aheadDriverId`, `atFaultDriverId`, `impactSpeedKph`, and the configured impact-speed threshold. If rear-contact responsibility is unclear, both involved cars receive shared-fault collision penalties with `sharedFault: true`. Collision penalties are independent from the existing physical contact response.
|
|
289
|
+
|
|
290
|
+
When tire-requirement stewarding is enabled, a finished car is reviewed once against `tireStrategy.mandatoryDistinctDryCompounds`. Cars start with their initial tire in `usedTireCompounds`; completed automatic pit stops add the changed compound, so a strict two-compound rule can be satisfied by the current pit-stop module.
|
|
170
291
|
|
|
171
292
|
## Known Non-Goals For Now
|
|
172
293
|
|
|
173
294
|
These are not currently implemented:
|
|
174
295
|
|
|
175
|
-
-
|
|
176
|
-
-
|
|
296
|
+
- Strategic pit-call timing beyond the current first automatic stop.
|
|
297
|
+
- Double stacking and pit-crew conflicts.
|
|
298
|
+
- Manual tire strategy selection beyond changing to the first different configured compound.
|
|
177
299
|
- Fuel load strategy.
|
|
178
300
|
- Weather.
|
|
179
|
-
- Penalties.
|
|
180
301
|
- Mechanical failures.
|
|
181
302
|
- Race finish ceremony.
|
|
182
303
|
- Multiplayer controls.
|
package/docs/system_specs.md
CHANGED
|
@@ -26,14 +26,18 @@ import {
|
|
|
26
26
|
Headless expert environment import:
|
|
27
27
|
|
|
28
28
|
```js
|
|
29
|
-
import {
|
|
29
|
+
import {
|
|
30
|
+
createPaddockEnvironment,
|
|
31
|
+
createRolloutRecorder,
|
|
32
|
+
runEnvironmentEvaluation,
|
|
33
|
+
} from '@inventure71/paddockjs/environment';
|
|
30
34
|
```
|
|
31
35
|
|
|
32
36
|
The environment subpath is the only public headless training import. It must stay free of DOM, PixiJS, CSS, and browser app dependencies.
|
|
33
|
-
`createProgressReward()` is published from the same subpath as
|
|
37
|
+
`createProgressReward()` is published from the same subpath only as non-canonical demo reward code for examples and smoke tests. It must remain optional and replaceable; environment stepping must continue to work with a custom `reward(context)` callback or no reward callback.
|
|
34
38
|
Ray observations expose track-transition distance and car distance per ray. Track transitions use `kind: 'exit'` when the ray starts on track and reaches the border, `kind: 'entry'` when the ray starts off track and reaches the road again, and `kind: null` with max distance when no transition is visible.
|
|
35
39
|
The package must not own model training, model persistence, model registries, or trained policy behavior. The supported contract is: external code reads observations, returns normalized actions, and advances either the headless environment or browser expert mode.
|
|
36
|
-
The shared expert runtime exposes `getActionSpec()` and `getObservationSpec()` so external code can inspect controlled drivers, action ranges, sensor layout, nearby-car limits, and vector schema before connecting a model.
|
|
40
|
+
The shared expert runtime exposes `getActionSpec()` and `getObservationSpec()` so external code can inspect controlled drivers, action ranges, sensor layout, nearby-car limits, track lookahead fields, and the versioned vector schema before connecting a model. Environment reset may apply deterministic scenario placement presets or explicit placement/traffic layouts; model steps may not mutate position and still use normalized actions only. Neutral rollout recording, deterministic evaluation metrics, and the JSON worker protocol are environment utilities, not training algorithms.
|
|
37
41
|
|
|
38
42
|
All-in-one mount call:
|
|
39
43
|
|
|
@@ -100,6 +104,11 @@ simulator.mountRaceTelemetryDrawer(raceWorkbenchRoot, {
|
|
|
100
104
|
});
|
|
101
105
|
|
|
102
106
|
await simulator.start();
|
|
107
|
+
simulator.setPitIntent('budget', 2, 'H'); // committed automatic pit request with target tire
|
|
108
|
+
const pitIntent = simulator.getPitIntent('budget');
|
|
109
|
+
const targetCompound = simulator.getPitTargetCompound('budget');
|
|
110
|
+
simulator.setPitLaneOpen(false);
|
|
111
|
+
simulator.setRedFlagDeployed(true);
|
|
103
112
|
```
|
|
104
113
|
|
|
105
114
|
Standalone helper functions are also exported for host code that prefers function calls:
|
|
@@ -152,6 +161,13 @@ Returned controller:
|
|
|
152
161
|
restart(nextOptions),
|
|
153
162
|
selectDriver(driverId),
|
|
154
163
|
setSafetyCarDeployed(deployed),
|
|
164
|
+
setRedFlagDeployed(deployed),
|
|
165
|
+
setPitLaneOpen(open),
|
|
166
|
+
setPitIntent(driverId, intent, targetCompound),
|
|
167
|
+
getPitIntent(driverId),
|
|
168
|
+
getPitTargetCompound(driverId),
|
|
169
|
+
servePenalty(penaltyId),
|
|
170
|
+
cancelPenalty(penaltyId),
|
|
155
171
|
callSafetyCar(),
|
|
156
172
|
clearSafetyCar(),
|
|
157
173
|
toggleSafetyCar(),
|
|
@@ -169,24 +185,25 @@ Returned controller:
|
|
|
169
185
|
- The race canvas is required before `start()` because PixiJS needs a canvas host.
|
|
170
186
|
- Timing tower, telemetry, controls, and race-data panels are optional from a runtime safety perspective; omitted panels simply do not render their readouts.
|
|
171
187
|
- Timing tower entries are fixed rows stacked from the top of the timing list. Row vertical position must be based on rank/index, never distributed by available height or total entry count.
|
|
172
|
-
- Timing tower entries display team icons. The tower can switch at runtime between interval-to-car-ahead timing and
|
|
173
|
-
- Snapshots expose calibrated display units through `speedKph`, `distanceMeters`, and `gapMeters`; internal physics remains in simulator units.
|
|
174
|
-
- Snapshots expose automatic three-sector track metadata through `track.sectors` and per-car lap/sector timing through `car.lapTelemetry`, including sector performance classes for overall-best, personal-best, and slower completed sector times.
|
|
188
|
+
- Timing tower entries display team icons. The tower can switch at runtime between interval-to-car-ahead timing and direct gap-to-leader timing. Seconds gaps are calculated from hidden timing-line crossings, and whole-lap deficits display as `+N`.
|
|
189
|
+
- Snapshots expose calibrated display units through `speedKph`, `distanceMeters`, and `gapMeters`; internal physics remains in simulator units. Public timer, lap-time, sector-time, gap-time, penalty-time, and service-countdown values are seconds.
|
|
190
|
+
- Snapshots expose automatic three-sector track metadata through `track.sectors` and per-car lap/sector timing through `car.lapTelemetry`, including completed current-lap sector splits before the active sector, live active-sector elapsed time, per-sector progress, and sector performance classes for overall-best, personal-best, and slower completed sector times. Future-sector timing/progress entries are cleared so banner and sidebar telemetry cannot display stale split values from another lap or state.
|
|
175
191
|
- Entries can include optional `team` metadata. Team color defaults to car color when omitted.
|
|
176
192
|
- Mounted package surfaces show a package-owned red start-light loading overlay until `start()` finishes PixiJS, asset, control, and initial readout initialization.
|
|
177
193
|
- `preset` is a preset-first API. Presets are resolved before explicit host overrides so hosts can use `dashboard`, `timing-overlay`, `compact-race`, or `full-dashboard` as a starting point and still override specific `ui` or `theme` fields.
|
|
178
194
|
- `theme` is the public sizing/color contract. It maps to package CSS variables for `accentColor`, `greenColor`, `yellowColor`, `timingTowerMaxWidth`, and `raceViewMinHeight`.
|
|
179
|
-
- `initialCameraMode` accepts `'overview'`, `'leader'`, `'selected'`,
|
|
180
|
-
- Camera controls can be embedded in the race canvas
|
|
181
|
-
- Telemetry surfaces are detached package components: core scalar readouts, sector graph, broadcast sector banner, lap-time table, and sector-time table. The broadcast sector banner shows the selected car identity, uses the selected car color for its frame/label, and keeps sector performance colors inside the sector bars. It is an explicitly mounted independent surface, not the default telemetry-drawer lower-third. `mountTelemetryPanel()` is
|
|
182
|
-
- `mountRaceTelemetryDrawer()` creates a package-owned race workbench: race canvas, embedded timing tower, lower-third banner, safety-car control, and a right-side telemetry drawer. Pass `{ raceDataTelemetryDetail: true }` when the drawer lower-third should include compact project telemetry detail. The drawer opens smoothly, takes width from the race view instead of overlaying it,
|
|
195
|
+
- `initialCameraMode` accepts `'overview'`, `'leader'`, `'selected'`, `'show-all'`, or `'pit'`; invalid values fall back to `'leader'`. The overview camera frames the active generated track bounds with package-owned padding and pit-lane extent. The pit camera frames the operational `track.pitLane` lane, boxes, service areas, and queue areas instead of the longer entry/exit access roads, zooms out when needed to keep that pit-lane work area inside the active race-view safe area, and its control is hidden/disabled if the active track has no pit lane. Zoom buttons and wheel zoom work in every camera mode, including overview, show-all, and pit, but cannot zoom farther out than the active track frame.
|
|
196
|
+
- Camera controls default to external placement so they do not cover the race view. They can still be embedded in the race canvas by setting `ui.cameraControls: 'embedded'`, externally mounted, or omitted with `false`. Generated camera controls include a `Mute banners` toggle that is off by default and temporarily suppresses project/radio lower-thirds while active.
|
|
197
|
+
- Telemetry surfaces are detached package components: core scalar readouts, sector graph, broadcast sector banner, lap-time table, and sector-time table. The broadcast sector banner shows the selected car identity, uses the selected car color for its frame/label, and keeps sector performance colors inside the sector bars. It is an explicitly mounted independent surface, not the default telemetry-drawer lower-third. `mountTelemetryPanel()` is the stack template around those detached pieces, owns vertical scrolling when constrained, and `ui.telemetryModules` controls which pieces appear in stack/drawer templates.
|
|
198
|
+
- `mountRaceTelemetryDrawer()` creates a package-owned race workbench: external top controls, race canvas, embedded timing tower, lower-third banner, top steward message, safety-car control, and a right-side telemetry drawer. The top row holds camera controls, banner mute, safety car, and telemetry toggle outside the canvas. Pass `{ raceDataTelemetryDetail: true }` when the drawer lower-third should include compact project telemetry detail. The drawer opens smoothly, takes width from the race view instead of overlaying it, is inert/hidden to interaction when closed, and embeds the same `telemetry-stack` component used by standalone telemetry panels.
|
|
183
199
|
- The FPS readout can be shown or hidden with `ui.showFps`.
|
|
184
200
|
- `ui.layoutPreset: 'left-tower-overlay'` is a package-owned preset that creates a left broadcast gutter inside the race view, places the timing tower there at the same width as the default timing-board column, frames the PixiJS camera around the remaining usable race area, and keeps camera controls and start lights out of the tower area. In the combined shell, project and radio lower-thirds stay inside the race window while being allowed to cover the timing sidebar.
|
|
185
|
-
- `ui.raceDataBanners.initial` controls the starting lower-third (`'project'`, `'radio'`, or `'hidden'`), and `ui.raceDataBanners.enabled` controls which lower-third types can appear.
|
|
201
|
+
- `ui.raceDataBanners.initial` controls the starting lower-third (`'project'`, `'radio'`, or `'hidden'`), and `ui.raceDataBanners.enabled` controls which lower-third types can appear. The runtime banner mute state temporarily disables project/radio lower-thirds without changing these configured allow-lists.
|
|
202
|
+
- Project and radio lower-third pills include a package-owned top-right close button that dismisses the current pill early. The next pill appears only through normal driver selection or radio scheduling.
|
|
186
203
|
- `ui.raceDataBannerSize` controls lower-third sizing: `'custom'` keeps the default CSS-variable-driven banner size for host tuning, while `'auto'` uses the race space to the right of the timing board when wide enough and overlaps the timing board only when space is constrained.
|
|
187
204
|
- `ui.raceDataTelemetryDetail` adds compact S1/S2/S3 sector detail to the project lower-third while keeping radio mode unchanged.
|
|
188
205
|
- `ui.timingTowerVerticalFit` controls vertical tower behavior in the combined overlay preset: `'expand-race-view'` grows the race window to fit the tower, while `'scroll'` crops the tower area and scrolls timing rows inside it. The same values are accepted as `mountRaceCanvas()` options when `includeTimingTower` embeds the tower in the race canvas.
|
|
189
|
-
- Hosts may scale the whole mounted simulator through the container. The horizontal proportions inside package-owned presets are not public API and should not be configurable through raw width or ratio options. The camera reads the current canvas dimensions so wider or taller host windows reveal more of the race view without needing host-owned camera math. The timing tower has a package-owned max width because overly wide timing boards degrade readability; standalone hosts can constrain vertical height through the mount container and let the timing entries scroll internally. Mobile and narrow embeds are handled by package CSS: timing boards stack full-width when they no longer work as side gutters, camera controls
|
|
206
|
+
- Hosts may scale the whole mounted simulator through the container. The horizontal proportions inside package-owned presets are not public API and should not be configurable through raw width or ratio options. The camera reads the current canvas dimensions so wider or taller host windows reveal more of the race view without needing host-owned camera math, and the renderer keeps grass coverage beyond the simulated world so deep zoom-out does not show host background. The timing tower has a package-owned max width because overly wide timing boards degrade readability; standalone hosts can constrain vertical height through the mount container and let the timing entries scroll internally. Mobile and narrow embeds are handled by package CSS: timing boards stack full-width when they no longer work as side gutters, camera controls stay in external control rows when mounted, and full-width timing boards do not reserve horizontal camera gutter space.
|
|
190
207
|
- The host does not need to provide simulator assets.
|
|
191
208
|
- The host passes data, not internal DOM.
|
|
192
209
|
- Host driver IDs and entry `driverId` values must be unique. Entries may omit `driverNumber`; provided numbers must be unique.
|
|
@@ -194,16 +211,19 @@ Returned controller:
|
|
|
194
211
|
- `restart(nextOptions)` can change race data and deterministic seeds such as `trackSeed`, but it does not support changing asset URLs. Asset changes require `destroy()` and a fresh mount because PixiJS texture loading is an initialization boundary.
|
|
195
212
|
- `onDriverOpen(driver)` is the navigation boundary.
|
|
196
213
|
- Lifecycle callbacks are optional: `onLoadingChange`, `onReady`, `onError`, `onDriverSelect`, `onRaceEvent`, `onLapChange`, and `onRaceFinish`. Host callback failures are routed to `onError` when possible and must not stop the simulator loop.
|
|
197
|
-
- Race completion is part of the simulation snapshot. Cars receive individual `finished`, `finishTime`, and `classifiedRank` values as they cross the finish distance. The first finisher sets `raceControl.winner`;
|
|
214
|
+
- Race completion is part of the simulation snapshot. Cars receive individual `finished`, `finishTime`, `finishRank`, `status: 'waved-flag'`, `raceStatus: 'waved-flag'`, `wavedFlag`, `penaltySeconds`, `adjustedFinishTime`, and `classifiedRank` values as they cross the finish distance. The first finisher sets a provisional `raceControl.winner`; already-finished cars remain frozen in provisional finish order while the remaining cars complete the distance. Final `raceControl.classification` and `raceControl.finished` are set only after every car finishes. Final classification converts unserved drive-through and stop-go penalties into configured time, sorts by `finishTime + penaltySeconds`, applies position-drop and disqualification consequences, then race control switches to `safety-car`, the final order freezes to the classified result, and the field keeps circulating under safety-car behavior.
|
|
215
|
+
- Race rules support package presets and custom module config. Supported rulesets are `paddock`, `grandPrix2025`, `fia2025`, and `custom`. Advanced modules include pit stops, tire strategy, penalties, weather, reliability, and fuel load. The current simulator normalizes all module config, creates/renders track-owned pit-lane geometry on an explicit start/finish straight, treats pit-lane asphalt, working-lane service areas, and garage boxes as legal drivable surfaces, runs automatic bounded pit-train entry/service/exit through the main fast lane and shared team service areas when pit stops are enabled, lets host/expert pit calls choose a target tire compound, uses the team queue point as a rolling gate when the service area is free, queues a second team car behind the active service area and moves it forward through a queue-release route only after the active service area is physically clear, keeps pit-route approach speed bounded until the final queue/service capture instead of crawling through open route, optionally varies pit-service time from team pit-crew stats with a perfect-training override, supports pit-lane open/closed state plus red flags, requests pit stops from configurable tire-energy thresholds, applies nonlinear tire-grip degradation down to 1%, applies the pit speed limiter only on the main pit lane/working lane, enforces pit-lane speeding on speed-limited pit-lane parts but not on entry/exit connectors, keeps the safety car about `55m` ahead of the leader with compact `22m` frozen-order queue slots, lets the built-in driver AI ride kerbs, attack, defend, and recover through normal steering/throttle/brake decisions with short controller-state commitments for rejoin and attack decisions, and enforces stewarded penalties for collisions, track limits, pit-lane speeding, and tire requirements.
|
|
216
|
+
- Penalty subsections use `strictness` from `0` to `1`, not only boolean enablement. `strictness: 0` means the subsection is not enforced; `strictness: 1` applies the configured rule margin. Steward decisions are exposed as `penalty` events and as top-level `snapshot.penalties`. Penalty entries include normalized consequences and lifecycle status. Immediate consequences apply time, grid, position, or disqualification effects directly; drive-through and stop-go consequences remain issued until served, cancelled, or converted at final classification.
|
|
217
|
+
- The browser UI can opt into top steward messages with `ui.penaltyBanners` and timing-row penalty badges with `ui.timingPenaltyBadges`. Steward messages render track-limit warning events and penalty decisions from the simulation; time-penalty messages put the penalty seconds in a large left chip and use penalty-colored backgrounds, while warnings use warning-colored backgrounds. Timing-row `!` badges are rendered only from `snapshot.penalties`; warning events do not count. UI code must not recalculate steward decisions.
|
|
198
218
|
- The simulator must stay interactive after being installed through `npm install @inventure71/paddockjs`.
|
|
199
219
|
- The package must build correctly through a browser bundler that supports JavaScript modules, CSS imports, and image imports.
|
|
200
220
|
- The simulation should remain deterministic for the same seed, track seed, drivers, entries, and rules.
|
|
201
|
-
- When `trackSeed` is omitted in a browser mount, the simulator creates a fresh procedural circuit for that mount. Explicit `trackSeed` values are deterministic and cached by seed for repeated mounts.
|
|
221
|
+
- When `trackSeed` is omitted in a browser mount, the simulator creates a fresh procedural circuit for that mount. Explicit `trackSeed` values are deterministic and cached by seed for repeated mounts. Procedural generation rejects circuits with excessive local heading jumps or turn accumulation so the built-in physics-driven AI can push the generated layout without requiring pose correction.
|
|
202
222
|
- The renderer should target a paced 60 FPS simulation/render loop.
|
|
203
223
|
- The render loop should pause while the race canvas is offscreen or the document is hidden, then resume without catching up the elapsed hidden time. Layout measurements needed for overlay camera safe areas should be cached between resize/layout invalidations. Runtime DOM updates should skip unchanged text/markup so visible embeds do not rewrite stable readouts every frame.
|
|
204
224
|
- Restart and rerender paths must destroy replaced PixiJS display children while preserving shared loaded textures.
|
|
205
225
|
- Restart supports race/data/seed changes but does not support asset URL changes or browser expert mode changes. Texture loading and ticker ownership are mount-time boundaries; hosts must destroy and mount again to change either boundary.
|
|
206
|
-
- Expert environment code can create a headless `createPaddockEnvironment()` from the `@inventure71/paddockjs/environment` subpath. It requires explicit `controlledDrivers`, accepts normalized actions `{ steering, throttle, brake }`, advances only through `step(actions)`, and returns environment-loop JavaScript results with `observation`, `reward`, `terminated`, `truncated`, `done`, `events`, `state`, and `info`.
|
|
226
|
+
- Expert environment code can create a headless `createPaddockEnvironment()` from the `@inventure71/paddockjs/environment` subpath. It requires explicit `controlledDrivers`, accepts normalized actions `{ steering, throttle, brake }` plus optional `pitIntent` and `pitCompound`, advances only through `step(actions)`, and returns environment-loop JavaScript results with `observation`, `reward`, `terminated`, `truncated`, `done`, `events`, `state`, and `info`. Controlled drivers do not receive tire-threshold automatic pit calls; they request automatic pit service through `pitIntent`, may choose the target tire through `pitCompound`, and observe pit-lane/service/race-control state in `observation[driverId].object.self` and `.race`. `pitIntent: 0` is a no-op clear request and does not fail when pit stops are disabled. `self.onTrack` follows wheel-level legality, so track, kerb, and legal pit-lane/box surfaces are on-track while gravel/grass/barrier are off-track. Environment scenarios may set reset positions with `preset`, `placements`, and relative `traffic`; this is setup data, not a policy action.
|
|
207
227
|
- Expert ray sensors originate from the controlled car center. The default compact set is `[-135, -60, -20, 0, 20, 60, 135, 180]`, giving forward, side, and rear awareness while staying small. Rays detect track edges against the actual track geometry and detect car hits by ray-to-car-footprint intersection.
|
|
208
228
|
- Browser expert mode is opt-in with `expert: { enabled: true, controlledDrivers, frameSkip }`. When enabled, the returned controller exposes `expert.reset()`, `expert.step(actions)`, `expert.getObservation()`, and `expert.getState()`.
|
|
209
229
|
- Browser expert mode wraps the same `RaceSimulation` instance that the visual canvas renders. It must not create a parallel simulation for the same mount.
|
|
@@ -238,7 +258,7 @@ Returned controller:
|
|
|
238
258
|
- `pixi.js` available through the package dependency graph.
|
|
239
259
|
|
|
240
260
|
Raw Node imports of the package root are not a supported runtime check because the browser component entry imports CSS and image assets. The `@inventure71/paddockjs/environment` subpath is the supported browser-free import path for headless JavaScript training.
|
|
241
|
-
The repository starter loop is executable with `node examples/train-basic-policy.mjs`. It is a dependency-free example that trains/evaluates a tiny policy against the environment contract; it is not a packaged Gymnasium bridge or a recommended final RL algorithm.
|
|
261
|
+
The repository starter loop is executable with `node examples/train-basic-policy.mjs`. It imports the public environment subpath and self-contained example data from `examples/trainingData.mjs`. It is a dependency-free example that trains/evaluates a tiny policy against the environment contract; it is not a packaged Gymnasium bridge or a recommended final RL algorithm. The package exposes a JSON-serializable worker protocol wrapper so external processes can bridge to the JavaScript environment without PaddockJS choosing Python, Gymnasium, PettingZoo, model storage, or training infrastructure.
|
|
242
262
|
|
|
243
263
|
## Verification
|
|
244
264
|
|
|
@@ -252,6 +272,9 @@ Expected:
|
|
|
252
272
|
|
|
253
273
|
- All Vitest tests pass.
|
|
254
274
|
- `npm pack --dry-run` succeeds and includes source files plus bundled assets.
|
|
275
|
+
- A packed tarball installs and builds inside a fresh temporary Vite consumer app.
|
|
276
|
+
- The tracked showcase host builds.
|
|
277
|
+
- Chromium browser smoke verifies desktop/mobile canvas rendering, package-panel overflow constraints, public API buttons, and visual policy-runner stepping.
|
|
255
278
|
|
|
256
279
|
Run from a browser host that consumes the published package:
|
|
257
280
|
|
package/docs/training.md
CHANGED
|
@@ -11,6 +11,7 @@ PaddockJS owns:
|
|
|
11
11
|
- normalized actions
|
|
12
12
|
- events
|
|
13
13
|
- optional reward callback hooks
|
|
14
|
+
- race-rule overrides for the existing simulator rules
|
|
14
15
|
- browser expert stepping
|
|
15
16
|
- sensor visualization
|
|
16
17
|
|
|
@@ -21,7 +22,43 @@ Users own:
|
|
|
21
22
|
- model weights
|
|
22
23
|
- model storage
|
|
23
24
|
- model loading
|
|
24
|
-
- reward design
|
|
25
|
+
- reward design
|
|
26
|
+
|
|
27
|
+
## Supported Now
|
|
28
|
+
|
|
29
|
+
The current expert API is a JavaScript environment contract. It supports:
|
|
30
|
+
|
|
31
|
+
- `createPaddockEnvironment()` from `@inventure71/paddockjs/environment`
|
|
32
|
+
- explicit `controlledDrivers`
|
|
33
|
+
- normalized actions: `steering`, `throttle`, `brake`, optional `pitIntent`, and optional `pitCompound`
|
|
34
|
+
- manual stepping with optional `frameSkip`
|
|
35
|
+
- object observations in real units plus a versioned numeric vector and schema, including rays, nearby cars, track lookahead/curvature, pit-lane surface, pit target compound, pit service state, pit-lane open state, and red-flag state
|
|
36
|
+
- full simulator state under `result.state.snapshot`
|
|
37
|
+
- global and per-controlled-driver events
|
|
38
|
+
- optional host-owned `reward(context)` callbacks, or no reward
|
|
39
|
+
- `getActionSpec()` and `getObservationSpec()`
|
|
40
|
+
- scenarios with `participants: 'all'`, `'controlled-only'`, or an explicit driver-id list
|
|
41
|
+
- reset-only scenario placement through `scenario.preset`, `scenario.placements`, and `scenario.traffic`
|
|
42
|
+
- neutral rollout recording through `createRolloutRecorder()`
|
|
43
|
+
- deterministic evaluation metrics through `runEnvironmentEvaluation()` and `createEvaluationTracker()`
|
|
44
|
+
- a JSON-serializable worker protocol wrapper through `createEnvironmentWorkerProtocol()`
|
|
45
|
+
- built-in AI for non-controlled cars
|
|
46
|
+
- browser expert mode through `mountF1Simulator(..., { expert })`
|
|
47
|
+
- opt-in browser ray visualization with `expert.visualizeSensors`
|
|
48
|
+
|
|
49
|
+
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.
|
|
50
|
+
|
|
51
|
+
## Deferred
|
|
52
|
+
|
|
53
|
+
These are not part of the current package API:
|
|
54
|
+
|
|
55
|
+
- Python Gymnasium wrapper
|
|
56
|
+
- static obstacle/ghost-car scenario modes
|
|
57
|
+
- debug mutation APIs for arbitrary simulator state
|
|
58
|
+
- assisted-control modes that blend model output with built-in AI
|
|
59
|
+
- model loading, model storage, model registries, or trained policies
|
|
60
|
+
|
|
61
|
+
Those features need separate design work because they change scenario ownership, reproducibility, and the safety boundary between user experiments and package internals. The worker protocol is intentionally only a message wrapper around the JavaScript environment; it is not a Gymnasium package and does not choose a Python RL stack.
|
|
25
62
|
|
|
26
63
|
## Policy Shape
|
|
27
64
|
|
|
@@ -44,6 +81,12 @@ const policy = {
|
|
|
44
81
|
- `steering`: `-1` full left, `1` full right
|
|
45
82
|
- `throttle`: `0` to `1`
|
|
46
83
|
- `brake`: `0` to `1`
|
|
84
|
+
- `pitIntent`: optional `0`, `1`, or `2`; `0` means no pit request and is accepted as a no-op even when pit stops are disabled, `1` means keep trying until a free-enough pit-entry window appears, and `2` means commit to entering at the next pit-entry window even if pit-lane capacity or gap checks would block mode `1`
|
|
85
|
+
- `pitCompound`: optional tire target such as `'S'`, `'M'`, or `'H'`; it must be one of `rules.modules.tireStrategy.compounds`
|
|
86
|
+
|
|
87
|
+
Controlled drivers do not receive tire-threshold automatic pit calls from the built-in strategy. A model must request a stop with `pitIntent`, and may choose the tire with `pitCompound`; then the simulator owns the pit entry, queue, service, penalty hold, tire change, and pit exit sequence until `pitStopStatus` returns to `completed`. For deterministic training, keep `rules.modules.pitStops.variability.enabled` false or set `rules.modules.pitStops.variability.perfect: true`; when variability is enabled without `perfect`, team `pitCrew` speed, consistency, and reliability affect service time from the same seeded simulation RNG.
|
|
88
|
+
|
|
89
|
+
`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.
|
|
47
90
|
|
|
48
91
|
## Headless Training Loop
|
|
49
92
|
|
|
@@ -55,6 +98,9 @@ const env = createPaddockEnvironment({
|
|
|
55
98
|
entries,
|
|
56
99
|
controlledDrivers: ['budget'],
|
|
57
100
|
frameSkip: 4,
|
|
101
|
+
rules: {
|
|
102
|
+
standingStart: false,
|
|
103
|
+
},
|
|
58
104
|
reward: myReward,
|
|
59
105
|
});
|
|
60
106
|
|
|
@@ -66,6 +112,62 @@ while (!result.done) {
|
|
|
66
112
|
}
|
|
67
113
|
```
|
|
68
114
|
|
|
115
|
+
## Scenario Reset Control
|
|
116
|
+
|
|
117
|
+
Scenario placement belongs to environment reset, not to model control. It is useful for deterministic training or evaluation starts such as cornering, recovery, overtaking packs, and pit entry:
|
|
118
|
+
|
|
119
|
+
```js
|
|
120
|
+
const env = createPaddockEnvironment({
|
|
121
|
+
drivers,
|
|
122
|
+
entries,
|
|
123
|
+
controlledDrivers: ['budget'],
|
|
124
|
+
scenario: {
|
|
125
|
+
preset: 'off-track-recovery',
|
|
126
|
+
placements: {
|
|
127
|
+
budget: {
|
|
128
|
+
distanceMeters: 420,
|
|
129
|
+
offsetMeters: 16,
|
|
130
|
+
speedKph: 65,
|
|
131
|
+
headingErrorRadians: 0.4,
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
traffic: [
|
|
135
|
+
{
|
|
136
|
+
driverId: 'alpha',
|
|
137
|
+
relativeTo: 'budget',
|
|
138
|
+
deltaDistanceMeters: 24,
|
|
139
|
+
offsetMeters: -1.5,
|
|
140
|
+
speedKph: 68,
|
|
141
|
+
},
|
|
142
|
+
],
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Supported fixed presets are `cornering`, `off-track-recovery`, `overtaking-pack`, and `pit-entry`. Explicit `placements` override preset placement for the same driver. `traffic` places cars relative to another placed or existing car. After reset, `step(actions)` still advances controlled cars only through the normal action contract.
|
|
148
|
+
|
|
149
|
+
## Rollouts And Evaluation
|
|
150
|
+
|
|
151
|
+
PaddockJS can export neutral transition data without owning a trainer:
|
|
152
|
+
|
|
153
|
+
```js
|
|
154
|
+
import { createRolloutRecorder } from '@inventure71/paddockjs/environment';
|
|
155
|
+
|
|
156
|
+
const recorder = createRolloutRecorder();
|
|
157
|
+
let result = env.reset();
|
|
158
|
+
const action = { budget: policy.predict(result.observation.budget) };
|
|
159
|
+
const next = env.step(action);
|
|
160
|
+
recorder.recordStep(result, action, next);
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
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.
|
|
164
|
+
|
|
165
|
+
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 do not update a model or compute rewards.
|
|
166
|
+
|
|
167
|
+
## Example Rewards
|
|
168
|
+
|
|
169
|
+
`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.
|
|
170
|
+
|
|
69
171
|
## Visual Playback Loop
|
|
70
172
|
|
|
71
173
|
```js
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { createPaddockEnvironment, createProgressReward } from '
|
|
4
|
-
import {
|
|
5
|
-
import { DEMO_PROJECT_DRIVERS } from '../src/data/demoDrivers.js';
|
|
3
|
+
import { createPaddockEnvironment, createProgressReward } from '@inventure71/paddockjs/environment';
|
|
4
|
+
import { TRAINING_DEMO_DRIVERS, TRAINING_DEMO_ENTRIES } from './trainingData.mjs';
|
|
6
5
|
|
|
7
6
|
const DEFAULT_OPTIONS = Object.freeze({
|
|
8
7
|
generations: 4,
|
|
@@ -38,7 +37,7 @@ function main() {
|
|
|
38
37
|
return;
|
|
39
38
|
}
|
|
40
39
|
|
|
41
|
-
const driverId =
|
|
40
|
+
const driverId = TRAINING_DEMO_DRIVERS[0].id;
|
|
42
41
|
const rng = createSeededRandom(options.seed);
|
|
43
42
|
const reward = createProgressReward();
|
|
44
43
|
const evaluate = (policy, seedOffset = 0) => evaluatePolicy({
|
|
@@ -122,10 +121,10 @@ function evaluatePolicy({ policy, driverId, reward, options, seedOffset = 0 }) {
|
|
|
122
121
|
}
|
|
123
122
|
|
|
124
123
|
function createTrainingEnvironment({ driverId, reward, options, episode, seedOffset }) {
|
|
125
|
-
const drivers = options.traffic ?
|
|
124
|
+
const drivers = options.traffic ? TRAINING_DEMO_DRIVERS.slice(0, 8) : TRAINING_DEMO_DRIVERS.slice(0, 1);
|
|
126
125
|
return createPaddockEnvironment({
|
|
127
126
|
drivers,
|
|
128
|
-
entries:
|
|
127
|
+
entries: TRAINING_DEMO_ENTRIES,
|
|
129
128
|
controlledDrivers: [driverId],
|
|
130
129
|
seed: options.seed + seedOffset + episode,
|
|
131
130
|
trackSeed: options.seed + 2026 + seedOffset + episode,
|