@inventure71/paddockjs 1.0.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (203) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/INSTALL_AND_UPDATE.md +4 -3
  3. package/README.md +219 -12
  4. package/docs/architecture.md +126 -45
  5. package/docs/concepts.md +9 -5
  6. package/docs/custom_model_controller.md +329 -0
  7. package/docs/data_contract.md +317 -22
  8. package/docs/data_model.md +1 -1
  9. package/docs/index.md +10 -0
  10. package/docs/learnings.md +22 -0
  11. package/docs/rules.md +71 -14
  12. package/docs/sense_contract.md +59 -0
  13. package/docs/system_specs.md +75 -13
  14. package/docs/training.md +280 -32
  15. package/examples/python/README.md +62 -0
  16. package/examples/python/base_policy_server.py +191 -0
  17. package/examples/python/requirements.txt +2 -0
  18. package/package.json +12 -4
  19. package/src/api/PaddockSimulatorController.js +6 -33
  20. package/src/app/BrowserExpertAdapter.js +76 -9
  21. package/src/app/BrowserExpertExternalRenderer.js +303 -0
  22. package/src/app/F1SimulatorApp.js +291 -1630
  23. package/src/app/banners/bannerFormatters.js +55 -0
  24. package/src/app/banners/raceDataBannerController.js +141 -0
  25. package/src/app/banners/stewardMessageController.js +99 -0
  26. package/src/app/camera/cameraConstants.js +21 -0
  27. package/src/app/camera/cameraController.js +265 -0
  28. package/src/app/camera/cameraGeometry.js +176 -0
  29. package/src/app/domBindings.js +4 -0
  30. package/src/app/readouts/carOverviewRenderer.js +109 -0
  31. package/src/app/readouts/controlStateRenderer.js +37 -0
  32. package/src/app/readouts/raceStatusRenderer.js +133 -0
  33. package/src/app/readouts/readoutFormatters.js +58 -0
  34. package/src/app/readouts/telemetryRenderer.js +113 -0
  35. package/src/app/readouts/timingTowerRenderer.js +177 -0
  36. package/src/app/rendering/appAssets.js +38 -0
  37. package/src/app/rendering/carRenderer.js +286 -0
  38. package/src/app/rendering/displayUtils.js +22 -0
  39. package/src/app/rendering/drsTrailRenderer.js +90 -0
  40. package/src/app/rendering/expertSensorRenderer.js +126 -0
  41. package/src/app/rendering/pitLaneStatusRenderer.js +50 -0
  42. package/src/app/rendering/replayGhostRenderer.js +124 -0
  43. package/src/app/rendering/trackRenderer.js +25 -0
  44. package/src/app/runtime/frameLoop.js +66 -0
  45. package/src/app/runtime/runtimeTiming.js +34 -0
  46. package/src/app/runtime/runtimeVisibility.js +45 -0
  47. package/src/config/defaultOptions.js +20 -0
  48. package/src/config/restartOptions.js +34 -0
  49. package/src/environment/actions.js +13 -5
  50. package/src/environment/controllerLoop.js +261 -0
  51. package/src/environment/episode.js +98 -2
  52. package/src/environment/evaluation.js +54 -32
  53. package/src/environment/index.d.ts +376 -10
  54. package/src/environment/index.js +1 -0
  55. package/src/environment/metrics.js +159 -0
  56. package/src/environment/observationOptions.js +22 -1
  57. package/src/environment/observationVector.js +200 -0
  58. package/src/environment/observations.js +199 -141
  59. package/src/environment/options.js +37 -5
  60. package/src/environment/recorder.js +13 -6
  61. package/src/environment/rewards.js +13 -2
  62. package/src/environment/runtime.js +272 -23
  63. package/src/environment/scenarios.js +57 -9
  64. package/src/environment/sensors/bodySenses.js +23 -0
  65. package/src/environment/sensors/boundarySenses.js +17 -0
  66. package/src/environment/sensors/carRays.js +75 -0
  67. package/src/environment/sensors/contactSenses.js +32 -0
  68. package/src/environment/sensors/index.js +157 -0
  69. package/src/environment/sensors/indexedRayBands.js +103 -0
  70. package/src/environment/sensors/nearbyCars.js +52 -0
  71. package/src/environment/sensors/opponentRadar.js +75 -0
  72. package/src/environment/sensors/rayConfig.js +110 -0
  73. package/src/environment/sensors/rayDefaults.js +6 -0
  74. package/src/environment/sensors/rayGeometry.js +60 -0
  75. package/src/environment/sensors/rayGuards.js +19 -0
  76. package/src/environment/sensors/sensorTargets.js +66 -0
  77. package/src/environment/sensors/surfaceRays.js +360 -0
  78. package/src/environment/sensors/trackRays.js +424 -0
  79. package/src/environment/sensors.js +1 -367
  80. package/src/environment/specs.js +159 -26
  81. package/src/environment/workerProtocol.js +13 -1
  82. package/src/index.d.ts +171 -6
  83. package/src/index.js +6 -32
  84. package/src/rendering/proceduralTrackAsset.js +20 -823
  85. package/src/rendering/renderSnapshot.js +38 -12
  86. package/src/rendering/track/finishLineRenderer.js +34 -0
  87. package/src/rendering/track/gridRenderer.js +47 -0
  88. package/src/rendering/track/offsetStrokeSafety.js +136 -0
  89. package/src/rendering/track/pitLaneRenderer.js +248 -0
  90. package/src/rendering/track/trackMaterialRenderer.js +326 -0
  91. package/src/rendering/track/trackRenderConstants.js +38 -0
  92. package/src/rendering/track/trackRenderGeometry.js +94 -0
  93. package/src/simulation/collisionGeometry.js +1 -1
  94. package/src/simulation/driver/driverControlConstants.js +28 -0
  95. package/src/simulation/driver/driverController.js +47 -0
  96. package/src/simulation/driver/driverInput.js +28 -0
  97. package/src/simulation/driver/driverMath.js +6 -0
  98. package/src/simulation/driver/driverPersonality.js +25 -0
  99. package/src/simulation/driver/edgeRecovery.js +48 -0
  100. package/src/simulation/driver/racingControls.js +399 -0
  101. package/src/simulation/driver/racingLinePlan.js +166 -0
  102. package/src/simulation/driver/recoveryDynamics.js +38 -0
  103. package/src/simulation/driver/rejoinControls.js +213 -0
  104. package/src/simulation/driver/safetyCarControls.js +35 -0
  105. package/src/simulation/driver/trafficScan.js +51 -0
  106. package/src/simulation/driverController.js +4 -619
  107. package/src/simulation/participants/participantInteractions.js +120 -0
  108. package/src/simulation/pit/pitFlow.js +239 -0
  109. package/src/simulation/pit/pitIntent.js +92 -0
  110. package/src/simulation/pit/pitKinematics.js +24 -0
  111. package/src/simulation/pit/pitOccupancy.js +47 -0
  112. package/src/simulation/pit/pitPenaltyService.js +71 -0
  113. package/src/simulation/pit/pitQueue.js +60 -0
  114. package/src/simulation/pit/pitRouteMovement.js +167 -0
  115. package/src/simulation/pit/pitRouting.js +319 -0
  116. package/src/simulation/pit/pitService.js +43 -0
  117. package/src/simulation/pit/pitServiceConstants.js +21 -0
  118. package/src/simulation/pit/pitServiceProfile.js +45 -0
  119. package/src/simulation/pit/pitSnapshots.js +61 -0
  120. package/src/simulation/pit/pitState.js +113 -0
  121. package/src/simulation/pit/pitTireService.js +94 -0
  122. package/src/simulation/race/classification.js +104 -0
  123. package/src/simulation/race/gridPenalties.js +37 -0
  124. package/src/simulation/race/raceControlState.js +38 -0
  125. package/src/simulation/race/raceDistance.js +32 -0
  126. package/src/simulation/race/raceFinish.js +65 -0
  127. package/src/simulation/race/raceLifecycle.js +67 -0
  128. package/src/simulation/race/raceOrder.js +113 -0
  129. package/src/simulation/race/raceProgress.js +129 -0
  130. package/src/simulation/race/raceSetup.js +93 -0
  131. package/src/simulation/race/raceStep.js +80 -0
  132. package/src/simulation/race/redFlag.js +15 -0
  133. package/src/simulation/race/retirements.js +44 -0
  134. package/src/simulation/race/safetyCar.js +92 -0
  135. package/src/simulation/race/stalledDnf.js +77 -0
  136. package/src/simulation/raceSimulation.js +231 -2817
  137. package/src/simulation/replay/replayGhosts.js +128 -0
  138. package/src/simulation/rules/moduleConfig.js +72 -0
  139. package/src/simulation/rules/penaltyConfig.js +99 -0
  140. package/src/simulation/rules/penaltyEvents.js +18 -0
  141. package/src/simulation/rules/penaltyStats.js +26 -0
  142. package/src/simulation/rules/ruleConfigMerge.js +41 -0
  143. package/src/simulation/rules/ruleDefaults.js +85 -0
  144. package/src/simulation/rules/rulesReview.js +144 -0
  145. package/src/simulation/rules/rulesetPresets.js +47 -0
  146. package/src/simulation/rules/trackLimitsSteward.js +1 -1
  147. package/src/simulation/rulesConfig.js +5 -323
  148. package/src/simulation/snapshots/raceSnapshots.js +193 -0
  149. package/src/simulation/timing/drsTiming.js +87 -0
  150. package/src/simulation/timing/gapEstimation.js +38 -0
  151. package/src/simulation/timing/lapTelemetry.js +182 -0
  152. package/src/simulation/timing/raceTiming.js +15 -0
  153. package/src/simulation/timing/sectorPerformance.js +60 -0
  154. package/src/simulation/timing/timingConstants.js +5 -0
  155. package/src/simulation/timing/timingHistory.js +46 -0
  156. package/src/simulation/timing/timingLines.js +79 -0
  157. package/src/simulation/track/centerlineSpline.js +59 -0
  158. package/src/simulation/track/drsZones.js +67 -0
  159. package/src/simulation/track/pitLaneAccess.js +103 -0
  160. package/src/simulation/track/pitLaneLayout.js +395 -0
  161. package/src/simulation/track/pitLaneState.js +160 -0
  162. package/src/simulation/track/proceduralCenterline.js +237 -0
  163. package/src/simulation/track/sampleGeometry.js +49 -0
  164. package/src/simulation/track/spatialQueries.js +154 -0
  165. package/src/simulation/track/startStraight.js +141 -0
  166. package/src/simulation/track/trackConstants.js +109 -0
  167. package/src/simulation/track/trackGenerationOptions.js +283 -0
  168. package/src/simulation/track/trackMath.js +241 -0
  169. package/src/simulation/track/trackModel.js +240 -0
  170. package/src/simulation/track/trackQueryGrid.js +94 -0
  171. package/src/simulation/track/trackQueryIndex.js +659 -0
  172. package/src/simulation/track/trackQueryPitIndex.js +140 -0
  173. package/src/simulation/track/trackQueryScratch.js +46 -0
  174. package/src/simulation/track/trackQueryStats.js +28 -0
  175. package/src/simulation/track/trackSectors.js +19 -0
  176. package/src/simulation/track/trackStatePolicy.js +24 -0
  177. package/src/simulation/track/trackValidation.js +98 -0
  178. package/src/simulation/trackModel.js +1 -1706
  179. package/src/simulation/vehicle/contactResolution.js +181 -0
  180. package/src/simulation/vehicle/mainTrackWheelSurface.js +82 -0
  181. package/src/simulation/vehicle/pitWheelSurface.js +69 -0
  182. package/src/simulation/vehicle/runoffResponse.js +87 -0
  183. package/src/simulation/vehicle/surfacePriority.js +28 -0
  184. package/src/simulation/vehicle/vehicleGeometry.js +247 -0
  185. package/src/simulation/vehicle/vehicleKinematics.js +37 -0
  186. package/src/simulation/vehicle/vehiclePhysics.js +364 -0
  187. package/src/simulation/vehicle/vehicleSnapshots.js +327 -0
  188. package/src/simulation/vehicle/vehicleState.js +220 -0
  189. package/src/simulation/vehicle/wheelSurface.js +152 -0
  190. package/src/simulation/vehicleGeometry.js +1 -247
  191. package/src/simulation/vehiclePhysics.js +1 -136
  192. package/src/simulation/warmup/runtimeWarmup.js +224 -0
  193. package/src/simulation/wheelSurface.js +1 -338
  194. package/src/styles.css +42 -0
  195. package/src/ui/cameraControlsTemplate.js +20 -0
  196. package/src/ui/carOverviewTemplate.js +33 -0
  197. package/src/ui/componentTemplates.js +14 -360
  198. package/src/ui/raceCanvasTemplate.js +69 -0
  199. package/src/ui/raceControlsTemplate.js +36 -0
  200. package/src/ui/shellTemplate.js +5 -4
  201. package/src/ui/telemetryDrawerTemplate.js +55 -0
  202. package/src/ui/telemetryTemplates.js +136 -0
  203. package/src/ui/timingTowerTemplate.js +34 -0
@@ -1,10 +1,20 @@
1
1
  import { createRaceSimulation, FIXED_STEP } from '../simulation/raceSimulation.js';
2
+ import { disableWarmupOptions, runWarmupWithGuard } from '../simulation/warmup/runtimeWarmup.js';
2
3
  import { handleActionError, resolveActionMap } from './actions.js';
3
4
  import { collectStepEvents } from './events.js';
4
- import { createEpisodeState, evaluateEpisode } from './episode.js';
5
+ import {
6
+ advanceDriverEpisodes,
7
+ buildDriverEpisodeInfo,
8
+ createEpisodeState,
9
+ evaluateEpisode,
10
+ initializeDriverEpisodes,
11
+ markDestroyedDriverEpisodes,
12
+ resetDriverEpisodes,
13
+ } from './episode.js';
14
+ import { buildDriverMetrics } from './metrics.js';
5
15
  import { buildEnvironmentObservation } from './observations.js';
6
16
  import { resolveEnvironmentOptions } from './options.js';
7
- import { applyEnvironmentScenario } from './scenarios.js';
17
+ import { applyEnvironmentPlacements, applyEnvironmentScenario, normalizeEnvironmentPlacements } from './scenarios.js';
8
18
  import { buildActionSpec, buildObservationSpec } from './specs.js';
9
19
 
10
20
  export function createPaddockEnvironment(options = {}) {
@@ -28,28 +38,55 @@ export function createPaddockEnvironment(options = {}) {
28
38
  }
29
39
 
30
40
  function createSimulationWithEnvironmentScenario(options) {
31
- const sim = createRaceSimulation(options);
41
+ const simulationOptions = {
42
+ ...options,
43
+ trackQueryIndex: shouldUseTrackQueryIndex(options),
44
+ };
45
+ runWarmupWithGuard({
46
+ options: simulationOptions,
47
+ surface: 'environment',
48
+ execute: ({ warmup }) => {
49
+ const warmupSimulation = createRaceSimulation(disableWarmupOptions(simulationOptions, warmup.surface));
50
+ initializeControlledPitIntentForSimulation(warmupSimulation, options);
51
+ applyEnvironmentScenario(warmupSimulation, options);
52
+ applyControlledRunoffResponse(warmupSimulation, options);
53
+ for (let index = 0; index < warmup.steps; index += 1) warmupSimulation.step(FIXED_STEP);
54
+ const warmupSnapshot = warmupSimulation.snapshotObservation?.() ?? warmupSimulation.snapshot();
55
+ buildEnvironmentObservation({
56
+ snapshot: warmupSnapshot,
57
+ options,
58
+ events: [],
59
+ });
60
+ },
61
+ });
62
+ const sim = createRaceSimulation(disableWarmupOptions(simulationOptions, 'environment'));
63
+ initializeControlledPitIntentForSimulation(sim, options);
32
64
  applyEnvironmentScenario(sim, options);
65
+ applyControlledRunoffResponse(sim, options);
33
66
  return sim;
34
67
  }
35
68
 
69
+ function shouldUseTrackQueryIndex(options) {
70
+ return options.trackQueryIndex !== false;
71
+ }
72
+
36
73
  export function createEnvironmentRuntime(host) {
37
74
  const episodeState = createEpisodeState();
38
75
 
39
76
  initializeControlledPitIntent(host);
77
+ initializeDriverEpisodes(episodeState, host.getOptions().controlledDrivers);
40
78
 
41
79
  function reset(nextOptions = {}) {
42
- const options = resolveEnvironmentOptions({
43
- ...host.getOptions(),
44
- ...nextOptions,
45
- });
80
+ const options = resolveEnvironmentOptions(mergeEnvironmentResetOptions(host.getOptions(), nextOptions));
46
81
  host.setOptions(options);
47
82
  host.setSimulation(host.createSimulation(options));
48
83
  initializeControlledPitIntent(host);
49
84
  episodeState.step = 0;
50
85
  episodeState.previousSnapshot = null;
86
+ initializeDriverEpisodes(episodeState, options.controlledDrivers);
51
87
  const result = buildResult({ host, episodeState, events: [], actionErrors: [] });
52
88
  episodeState.lastResult = result;
89
+ emitExternalRenderFrame(host, result, { source: 'reset' });
53
90
  host.afterReset(result);
54
91
  return result;
55
92
  }
@@ -61,6 +98,11 @@ export function createEnvironmentRuntime(host) {
61
98
  policy: options.actionPolicy,
62
99
  });
63
100
 
101
+ if (options.actionPolicy === 'report') {
102
+ options.controlledDrivers.forEach((driverId) => {
103
+ if (!Object.hasOwn(controlsByDriver, driverId)) sim.clearCarControls?.(driverId);
104
+ });
105
+ }
64
106
  Object.entries(controlsByDriver).forEach(([driverId, controls]) => {
65
107
  sim.setCarControls(driverId, controls);
66
108
  });
@@ -71,7 +113,7 @@ export function createEnvironmentRuntime(host) {
71
113
  }
72
114
  });
73
115
 
74
- episodeState.previousSnapshot = episodeState.lastResult?.state?.snapshot ?? sim.snapshot();
116
+ episodeState.previousSnapshot = episodeState.lastRewardSnapshot ?? snapshotForResult(sim, options, options.result.stateOutput);
75
117
  const stepEvents = [];
76
118
  for (let index = 0; index < options.frameSkip; index += 1) {
77
119
  sim.step(FIXED_STEP);
@@ -81,8 +123,17 @@ export function createEnvironmentRuntime(host) {
81
123
  stepEvents.push(...collectStepEvents(events));
82
124
  }
83
125
  episodeState.step += 1;
84
- const result = buildResult({ host, episodeState, events: stepEvents, actionErrors: errors, actions });
126
+ advanceDriverEpisodes(episodeState, options.controlledDrivers);
127
+ const result = buildResult({
128
+ host,
129
+ episodeState,
130
+ events: stepEvents,
131
+ actionErrors: errors,
132
+ actions,
133
+ rewardEnabled: true,
134
+ });
85
135
  episodeState.lastResult = result;
136
+ emitExternalRenderFrame(host, result, { source: 'step', actions });
86
137
  host.afterStep(result);
87
138
  return result;
88
139
  }
@@ -92,8 +143,50 @@ export function createEnvironmentRuntime(host) {
92
143
  buildResult({ host, episodeState, events: [], actionErrors: [] }).observation;
93
144
  }
94
145
 
95
- function getState() {
96
- return { snapshot: host.getSimulation().snapshot() };
146
+ function getState({ output = 'full' } = {}) {
147
+ const sim = host.getSimulation();
148
+ if (output === 'none') return null;
149
+ if (output === 'minimal') return { snapshot: sim.snapshotObservation?.() ?? sim.snapshot() };
150
+ return { snapshot: sim.snapshot() };
151
+ }
152
+
153
+ function resetDrivers(placements = {}, resultOptions = {}) {
154
+ const options = host.getOptions();
155
+ const sim = host.getSimulation();
156
+ const driverIds = new Set(options.controlledDrivers);
157
+ const normalizedPlacements = normalizeEnvironmentPlacements(placements, driverIds);
158
+ const snapshot = sim.snapshotObservation?.() ?? sim.snapshot();
159
+ const carsById = new Map(snapshot.cars.map((car) => [car.id, car]));
160
+ applyEnvironmentPlacements(sim, snapshot.track, carsById, normalizedPlacements);
161
+ Object.keys(normalizedPlacements).forEach((driverId) => {
162
+ sim.clearCarControls?.(driverId);
163
+ sim.setAutomaticPitIntentEnabled?.(driverId, false);
164
+ const car = sim.cars?.find?.((item) => item.id === driverId);
165
+ resetEnvironmentPitStop(car);
166
+ sim.setPitIntent?.(driverId, 0);
167
+ if (car) sim.applyRunoffResponse?.(car);
168
+ });
169
+ resetDriverEpisodes(episodeState, Object.keys(normalizedPlacements));
170
+ episodeState.previousSnapshot = null;
171
+ const resetDriverIds = Object.keys(normalizedPlacements);
172
+ const observationScope = resultOptions.observationScope ??
173
+ resultOptions.resetDriversObservationScope ??
174
+ options.result.resetDriversObservationScope;
175
+ const result = buildResult({
176
+ host,
177
+ episodeState,
178
+ events: [],
179
+ actionErrors: [],
180
+ controlledDrivers: observationScope === 'reset' ? resetDriverIds : options.controlledDrivers,
181
+ stateOutput: resultOptions.stateOutput,
182
+ });
183
+ episodeState.lastResult = result;
184
+ emitExternalRenderFrame(host, result, {
185
+ source: 'resetDrivers',
186
+ resetDriverIds,
187
+ observationScope,
188
+ });
189
+ return result;
97
190
  }
98
191
 
99
192
  function getActionSpec() {
@@ -107,9 +200,42 @@ export function createEnvironmentRuntime(host) {
107
200
  function destroy() {
108
201
  episodeState.lastResult = null;
109
202
  episodeState.previousSnapshot = null;
203
+ episodeState.lastObservationSnapshot = null;
204
+ episodeState.lastRewardSnapshot = null;
205
+ episodeState.drivers?.clear?.();
110
206
  }
111
207
 
112
- return { reset, step, getObservation, getState, getActionSpec, getObservationSpec, destroy };
208
+ return { reset, step, resetDrivers, getObservation, getState, getActionSpec, getObservationSpec, destroy };
209
+ }
210
+
211
+ function mergeEnvironmentResetOptions(currentOptions, nextOptions = {}) {
212
+ return mergePlainObjects(currentOptions, nextOptions);
213
+ }
214
+
215
+ const REPLACE_OBJECT_PATHS = new Set([
216
+ 'scenario.placements',
217
+ ]);
218
+
219
+ function mergePlainObjects(base, overrides, path = []) {
220
+ if (!isPlainObject(base) || !isPlainObject(overrides)) return overrides;
221
+ const merged = { ...base };
222
+ Object.entries(overrides).forEach(([key, value]) => {
223
+ if (value === undefined) return;
224
+ const nextPath = [...path, key];
225
+ merged[key] = isPlainObject(value) &&
226
+ isPlainObject(base[key]) &&
227
+ !REPLACE_OBJECT_PATHS.has(nextPath.join('.'))
228
+ ? mergePlainObjects(base[key], value, nextPath)
229
+ : value;
230
+ });
231
+ return merged;
232
+ }
233
+
234
+ function isPlainObject(value) {
235
+ return value != null &&
236
+ typeof value === 'object' &&
237
+ !Array.isArray(value) &&
238
+ (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null);
113
239
  }
114
240
 
115
241
  function isNoopPitIntent(pitIntent) {
@@ -122,54 +248,177 @@ function isNoopPitIntent(pitIntent) {
122
248
  function initializeControlledPitIntent(host) {
123
249
  const sim = host.getSimulation?.();
124
250
  const options = host.getOptions?.();
251
+ initializeControlledPitIntentForSimulation(sim, options);
252
+ }
253
+
254
+ function initializeControlledPitIntentForSimulation(sim, options) {
125
255
  options?.controlledDrivers?.forEach?.((driverId) => {
126
256
  sim?.setAutomaticPitIntentEnabled?.(driverId, false);
127
257
  sim?.setPitIntent?.(driverId, 0);
258
+ const car = sim?.cars?.find?.((item) => item.id === driverId);
259
+ if (car) car.environmentControlled = true;
128
260
  });
129
261
  }
130
262
 
131
- function buildResult({ host, episodeState, events, actionErrors, actions = {} }) {
263
+ function resetEnvironmentPitStop(car) {
264
+ const stop = car?.pitStop;
265
+ if (!stop) return;
266
+ stop.status = 'pending';
267
+ stop.phase = null;
268
+ stop.intent = 0;
269
+ stop.queueingForService = false;
270
+ stop.route = null;
271
+ stop.routeProgress = 0;
272
+ stop.routeStartRaceDistance = null;
273
+ stop.routeEndRaceDistance = null;
274
+ stop.serviceRemaining = 0;
275
+ stop.penaltyServiceRemaining = 0;
276
+ stop.penaltyServiceTotal = 0;
277
+ stop.servingPenaltyIds = [];
278
+ stop.serviceProfile = null;
279
+ }
280
+
281
+ function buildResult({
282
+ host,
283
+ episodeState,
284
+ events,
285
+ actionErrors,
286
+ actions = {},
287
+ controlledDrivers = null,
288
+ stateOutput = null,
289
+ rewardEnabled = false,
290
+ }) {
132
291
  const options = host.getOptions();
133
- const snapshot = host.getSimulation().snapshot();
292
+ const sim = host.getSimulation();
293
+ const resultDrivers = controlledDrivers ?? options.controlledDrivers;
294
+ const resolvedStateOutput = stateOutput ?? options.result.stateOutput;
295
+ const observationSnapshot = snapshotForResult(sim, options, resolvedStateOutput);
296
+ markDestroyedDriverEpisodes(episodeState, options.controlledDrivers, observationSnapshot);
134
297
  const observation = buildEnvironmentObservation({
135
- snapshot,
298
+ snapshot: observationSnapshot,
136
299
  previousSnapshot: episodeState.previousSnapshot,
137
300
  options,
138
301
  events,
302
+ controlledDrivers: resultDrivers,
139
303
  });
140
- const episode = evaluateEpisode(snapshot, options, episodeState);
141
- const reward = computeReward({ options, observation, events, snapshot, actions, previousSnapshot: episodeState.previousSnapshot });
304
+ const episode = evaluateEpisode(observationSnapshot, options, episodeState, resultDrivers);
305
+ const metrics = buildDriverMetrics({
306
+ snapshot: observationSnapshot,
307
+ previousSnapshot: episodeState.previousSnapshot,
308
+ options: { ...options, controlledDrivers: resultDrivers },
309
+ events,
310
+ });
311
+ const rewardEpisodeInfo = buildDriverEpisodeInfo(episodeState, {
312
+ ...options,
313
+ controlledDrivers: resultDrivers,
314
+ }, episode);
315
+ const driverEpisode = resultDrivers === options.controlledDrivers
316
+ ? episode
317
+ : evaluateEpisode(observationSnapshot, options, episodeState, options.controlledDrivers);
318
+ const driverEpisodeInfo = buildDriverEpisodeInfo(episodeState, options, driverEpisode);
319
+ const { state, rewardSnapshot } = buildResultState(sim, observationSnapshot, resolvedStateOutput);
320
+ const reward = rewardEnabled
321
+ ? computeReward({
322
+ options: { ...options, controlledDrivers: resultDrivers },
323
+ observation,
324
+ events,
325
+ snapshot: rewardSnapshot,
326
+ actions,
327
+ previousSnapshot: episodeState.previousSnapshot,
328
+ metrics,
329
+ driverEpisodeInfo: rewardEpisodeInfo,
330
+ })
331
+ : null;
332
+ episodeState.lastObservationSnapshot = observationSnapshot;
333
+ episodeState.lastRewardSnapshot = rewardSnapshot;
142
334
  return {
143
335
  observation,
144
336
  reward,
337
+ metrics,
145
338
  terminated: episode.terminated,
146
339
  truncated: episode.truncated,
147
340
  done: episode.terminated || episode.truncated,
148
341
  events,
149
- state: { snapshot },
342
+ state,
150
343
  info: {
151
344
  step: episodeState.step,
152
- elapsedSeconds: snapshot.time,
345
+ elapsedSeconds: observationSnapshot.time,
153
346
  seed: options.seed,
154
347
  trackSeed: options.trackSeed,
155
- controlledDrivers: [...options.controlledDrivers],
348
+ controlledDrivers: [...resultDrivers],
156
349
  actionErrors,
157
350
  endReason: episode.endReason,
351
+ drivers: driverEpisodeInfo,
158
352
  },
159
353
  };
160
354
  }
161
355
 
162
- function computeReward({ options, observation, events, snapshot, actions, previousSnapshot }) {
356
+ function applyControlledRunoffResponse(sim, options) {
357
+ options?.controlledDrivers?.forEach?.((driverId) => {
358
+ const car = sim?.cars?.find?.((item) => item.id === driverId);
359
+ if (car) sim?.applyRunoffResponse?.(car);
360
+ });
361
+ }
362
+
363
+ function buildResultState(sim, observationSnapshot, stateOutput) {
364
+ if (stateOutput === 'none') return { state: null, rewardSnapshot: observationSnapshot };
365
+ if (stateOutput === 'minimal') return { state: { snapshot: observationSnapshot }, rewardSnapshot: observationSnapshot };
366
+ const snapshot = sim.snapshot();
367
+ return { state: { snapshot }, rewardSnapshot: snapshot };
368
+ }
369
+
370
+ function snapshotForResult(sim, options, stateOutput) {
371
+ if (canUseTrainingSnapshot(options, stateOutput)) {
372
+ return sim.snapshotTraining?.() ?? sim.snapshotObservation?.() ?? sim.snapshot();
373
+ }
374
+ return sim.snapshotObservation?.() ?? sim.snapshot();
375
+ }
376
+
377
+ function canUseTrainingSnapshot(options, stateOutput) {
378
+ return stateOutput === 'none' &&
379
+ options.observation?.output === 'vector' &&
380
+ options.observation?.includeSchema === false;
381
+ }
382
+
383
+ function computeReward({ options, observation, events, snapshot, actions, previousSnapshot, metrics, driverEpisodeInfo }) {
163
384
  if (!options.reward) return null;
164
385
  return Object.fromEntries(options.controlledDrivers.map((driverId) => [
165
386
  driverId,
166
- Number(options.reward({
387
+ normalizeRewardValue(options.reward({
167
388
  driverId,
168
389
  previous: previousSnapshot,
169
390
  current: observation[driverId],
170
391
  action: actions?.[driverId],
171
392
  events: observation[driverId]?.events ?? events,
172
393
  state: { snapshot },
173
- }) ?? 0),
394
+ metrics: metrics?.[driverId] ?? null,
395
+ episode: driverEpisodeInfo?.[driverId] ?? null,
396
+ })),
174
397
  ]));
175
398
  }
399
+
400
+ function emitExternalRenderFrame(host, result, meta = {}) {
401
+ const options = host.getOptions?.();
402
+ const onFrame = options?.externalRenderer?.onFrame;
403
+ if (typeof onFrame !== 'function') return;
404
+ try {
405
+ onFrame({
406
+ snapshot: result?.state?.snapshot ??
407
+ host.getSimulation?.()?.snapshotObservation?.() ??
408
+ host.getSimulation?.()?.snapshot?.(),
409
+ observation: result?.observation ?? {},
410
+ meta: {
411
+ ...meta,
412
+ step: result?.info?.step ?? null,
413
+ elapsedSeconds: result?.info?.elapsedSeconds ?? null,
414
+ },
415
+ });
416
+ } catch {
417
+ // External rendering hooks are observer-only and must not break stepping.
418
+ }
419
+ }
420
+
421
+ function normalizeRewardValue(value) {
422
+ const reward = Number(value ?? 0);
423
+ return Number.isFinite(reward) ? reward : 0;
424
+ }
@@ -1,5 +1,5 @@
1
1
  import { normalizeAngle } from '../simulation/simMath.js';
2
- import { offsetTrackPoint, pointAt } from '../simulation/trackModel.js';
2
+ import { offsetTrackPoint, pointAt } from '../simulation/track/trackModel.js';
3
3
  import { kphToSimSpeed, metersToSimUnits, simUnitsToMeters } from '../simulation/units.js';
4
4
 
5
5
  export const ENVIRONMENT_SCENARIO_PRESETS = Object.freeze([
@@ -38,10 +38,7 @@ export function applyEnvironmentScenario(sim, options) {
38
38
  ...(scenario.placements ?? {}),
39
39
  };
40
40
 
41
- Object.entries(placements).forEach(([driverId, placement]) => {
42
- if (!carsById.has(driverId)) return;
43
- applyPlacement(sim, snapshot.track, carsById.get(driverId), driverId, placement);
44
- });
41
+ applyEnvironmentPlacements(sim, snapshot.track, carsById, placements);
45
42
 
46
43
  const nextSnapshot = sim.snapshot();
47
44
  const nextCarsById = new Map(nextSnapshot.cars.map((car) => [car.id, car]));
@@ -49,15 +46,21 @@ export function applyEnvironmentScenario(sim, options) {
49
46
  const reference = nextCarsById.get(trafficPlacement.relativeTo);
50
47
  const car = nextCarsById.get(trafficPlacement.driverId);
51
48
  if (!reference || !car) return;
52
- applyPlacement(sim, nextSnapshot.track, car, trafficPlacement.driverId, {
49
+ applyEnvironmentPlacements(sim, nextSnapshot.track, nextCarsById, {
50
+ [trafficPlacement.driverId]: {
53
51
  distanceMeters: reference.distanceMeters + trafficPlacement.deltaDistanceMeters,
54
52
  offsetMeters: trafficPlacement.offsetMeters,
55
53
  speedKph: trafficPlacement.speedKph ?? reference.speedKph,
56
54
  headingErrorRadians: trafficPlacement.headingErrorRadians ?? 0,
55
+ },
57
56
  });
58
57
  });
59
58
  }
60
59
 
60
+ export function normalizeEnvironmentPlacements(placements = {}, driverIds = new Set()) {
61
+ return normalizePlacementMap(placements, driverIds);
62
+ }
63
+
61
64
  function normalizePlacementMap(value, driverIds) {
62
65
  if (value == null) return {};
63
66
  if (typeof value !== 'object' || Array.isArray(value)) {
@@ -174,13 +177,32 @@ function findHighCurvatureDistance(track) {
174
177
  return best?.distance ?? fallback;
175
178
  }
176
179
 
177
- function applyPlacement(sim, track, currentCar, driverId, placement) {
180
+ export function applyEnvironmentPlacements(sim, track, carsById, placements = {}) {
181
+ const states = {};
182
+ Object.entries(placements).forEach(([driverId, placement]) => {
183
+ const currentCar = carsById.get(driverId);
184
+ if (!currentCar) return;
185
+ states[driverId] = placementToCarState(track, currentCar, placement, {
186
+ physicsMode: sim.physicsMode,
187
+ });
188
+ });
189
+ if (typeof sim.setCarStates === 'function') {
190
+ sim.setCarStates(states);
191
+ return;
192
+ }
193
+ Object.entries(states).forEach(([driverId, partial]) => {
194
+ sim.setCarState(driverId, partial);
195
+ });
196
+ }
197
+
198
+ function placementToCarState(track, currentCar, placement, { physicsMode = 'arcade' } = {}) {
178
199
  const currentDistanceMeters = currentCar.distanceMeters ?? simUnitsToMeters(currentCar.raceDistance ?? currentCar.progress ?? 0);
179
200
  const raceDistance = metersToSimUnits(Math.max(0, placement.distanceMeters ?? currentDistanceMeters));
180
201
  const offset = metersToSimUnits(placement.offsetMeters ?? 0);
181
202
  const base = pointAt(track, raceDistance);
182
203
  const position = offsetTrackPoint(base, offset);
183
204
  const heading = normalizeAngle(base.heading + (placement.headingErrorRadians ?? 0));
205
+ const speed = placement.speedKph == null ? null : kphToSimSpeed(placement.speedKph);
184
206
  const partial = {
185
207
  x: position.x,
186
208
  y: position.y,
@@ -191,9 +213,35 @@ function applyPlacement(sim, track, currentCar, driverId, placement) {
191
213
  desiredOffset: offset,
192
214
  heading,
193
215
  previousHeading: heading,
216
+ destroyed: false,
217
+ destroyReason: null,
218
+ destroyedAt: null,
219
+ outOfRace: false,
220
+ canAttack: true,
221
+ throttle: 0,
222
+ brake: 0,
223
+ longitudinalAcceleration: 0,
224
+ lateralAcceleration: 0,
225
+ lateralG: 0,
226
+ longitudinalG: 0,
227
+ gripUsage: 0,
228
+ slipAngleRadians: 0,
229
+ tractionLimited: false,
230
+ steeringAngle: 0,
231
+ yawRate: 0,
232
+ stabilityState: 'stable',
194
233
  };
195
- if (placement.speedKph != null) partial.speed = kphToSimSpeed(placement.speedKph);
196
- sim.setCarState(driverId, partial);
234
+ if (speed != null) {
235
+ partial.speed = speed;
236
+ if (physicsMode === 'simulator') {
237
+ partial.velocityX = Math.cos(heading) * speed;
238
+ partial.velocityY = Math.sin(heading) * speed;
239
+ } else {
240
+ partial.velocityX = null;
241
+ partial.velocityY = null;
242
+ }
243
+ }
244
+ return partial;
197
245
  }
198
246
 
199
247
  function assertKnownDriver(driverId, driverIds, label) {
@@ -0,0 +1,23 @@
1
+ import { simSpeedToMetersPerSecond } from '../../simulation/units.js';
2
+
3
+ export function buildBodySenses(car) {
4
+ return {
5
+ id: car.id,
6
+ speedKph: car.speedKph,
7
+ speedMetersPerSecond: simSpeedToMetersPerSecond(car.speed ?? 0),
8
+ headingRadians: car.heading,
9
+ yawRateRadiansPerSecond: car.yawRate ?? 0,
10
+ steeringAngleRadians: car.steeringAngle ?? 0,
11
+ throttle: car.throttle ?? 0,
12
+ brake: car.brake ?? 0,
13
+ appliedControls: car.appliedControls ?? null,
14
+ lateralG: car.lateralG ?? 0,
15
+ longitudinalG: car.longitudinalG ?? 0,
16
+ gripUsage: car.gripUsage ?? 0,
17
+ slipAngleRadians: car.slipAngleRadians ?? 0,
18
+ tractionLimited: Boolean(car.tractionLimited),
19
+ stabilityState: car.stabilityState ?? 'stable',
20
+ destroyed: Boolean(car.destroyed),
21
+ destroyReason: car.destroyReason ?? null,
22
+ };
23
+ }
@@ -0,0 +1,17 @@
1
+ import { simUnitsToMeters } from '../../simulation/units.js';
2
+
3
+ export function buildBoundarySenses(car, snapshot, onLegalSurface) {
4
+ const widthMeters = simUnitsToMeters(snapshot.track?.width ?? 0);
5
+ const halfWidthMeters = widthMeters / 2;
6
+ const lateralOffsetMeters = simUnitsToMeters(car.signedOffset ?? 0);
7
+
8
+ return {
9
+ lateralOffsetMeters,
10
+ headingErrorRadians: car.trackHeadingError ?? 0,
11
+ legalWidthMeters: widthMeters,
12
+ leftBoundaryMeters: halfWidthMeters + lateralOffsetMeters,
13
+ rightBoundaryMeters: halfWidthMeters - lateralOffsetMeters,
14
+ onLegalSurface,
15
+ surface: car.surface ?? 'track',
16
+ };
17
+ }
@@ -0,0 +1,75 @@
1
+ import { metersToSimUnits, simUnitsToMeters } from '../../simulation/units.js';
2
+ import { VEHICLE_GEOMETRY } from '../../simulation/vehicle/vehicleGeometry.js';
3
+ import { dot, getCarRayOrigin, getCarRayVector, intersectAxisAlignedBoxRay } from './rayGeometry.js';
4
+ import { rayDetectableTargets } from './sensorTargets.js';
5
+
6
+ export function estimateCarHit(
7
+ car,
8
+ snapshot,
9
+ angleDegrees,
10
+ lengthMeters,
11
+ origin = getCarRayOrigin(car),
12
+ targets = rayDetectableTargets(car, snapshot),
13
+ ) {
14
+ const ray = getCarRayVector(car, angleDegrees);
15
+ const maxDistance = metersToSimUnits(lengthMeters);
16
+ let closest = null;
17
+ targets.forEach((other) => {
18
+ if (!carRayBroadphaseHit(origin, ray, maxDistance, other)) return;
19
+ const hitDistance = intersectCarFootprint(origin, ray, other);
20
+ if (hitDistance == null || hitDistance > maxDistance) return;
21
+ const distanceMeters = simUnitsToMeters(hitDistance);
22
+ if (!closest || hitDistance < closest.distanceSimUnits) {
23
+ closest = {
24
+ hit: true,
25
+ distanceSimUnits: hitDistance,
26
+ distanceMeters,
27
+ driverId: other.id,
28
+ targetId: other.id,
29
+ targetType: other.entityType,
30
+ relativeSpeedKph: other.speedKph - car.speedKph,
31
+ };
32
+ }
33
+ });
34
+ if (!closest) return createCarRayMiss(lengthMeters);
35
+ const { distanceSimUnits, ...publicHit } = closest;
36
+ return publicHit;
37
+ }
38
+
39
+ export function createCarRayMiss(lengthMeters) {
40
+ return {
41
+ hit: false,
42
+ distanceMeters: lengthMeters,
43
+ driverId: null,
44
+ targetId: null,
45
+ targetType: null,
46
+ relativeSpeedKph: 0,
47
+ };
48
+ }
49
+
50
+ function carRayBroadphaseHit(origin, ray, maxDistance, other) {
51
+ const dx = other.x - origin.x;
52
+ const dy = other.y - origin.y;
53
+ const projection = dx * ray.x + dy * ray.y;
54
+ const radius = Math.hypot(VEHICLE_GEOMETRY.bodyLength, VEHICLE_GEOMETRY.bodyWidth) / 2;
55
+ if (projection < -radius || projection > maxDistance + radius) return false;
56
+ const perpendicularSquared = Math.max(0, dx * dx + dy * dy - projection * projection);
57
+ return perpendicularSquared <= radius * radius;
58
+ }
59
+
60
+ function intersectCarFootprint(origin, ray, other) {
61
+ const forward = { x: Math.cos(other.heading), y: Math.sin(other.heading) };
62
+ const right = { x: -Math.sin(other.heading), y: Math.cos(other.heading) };
63
+ const delta = { x: origin.x - other.x, y: origin.y - other.y };
64
+ const localOrigin = {
65
+ x: dot(delta, forward),
66
+ y: dot(delta, right),
67
+ };
68
+ const localRay = {
69
+ x: dot(ray, forward),
70
+ y: dot(ray, right),
71
+ };
72
+ const halfLength = VEHICLE_GEOMETRY.bodyLength / 2;
73
+ const halfWidth = VEHICLE_GEOMETRY.bodyWidth / 2;
74
+ return intersectAxisAlignedBoxRay(localOrigin, localRay, halfLength, halfWidth);
75
+ }
@@ -0,0 +1,32 @@
1
+ import { simUnitsToMeters } from '../../simulation/units.js';
2
+
3
+ const SURFACE_CODES = {
4
+ track: 0,
5
+ kerb: 1,
6
+ 'pit-entry': 2,
7
+ 'pit-lane': 2,
8
+ 'pit-exit': 2,
9
+ 'pit-box': 2,
10
+ grass: 3,
11
+ gravel: 4,
12
+ barrier: 5,
13
+ };
14
+
15
+ export const CONTACT_PATCH_IDS = Object.freeze(['front-left', 'front-right', 'rear-left', 'rear-right']);
16
+
17
+ export function buildContactPatchSenses(car) {
18
+ const wheelsById = new Map((car.wheels ?? []).map((wheel) => [wheel.id, wheel]));
19
+ return CONTACT_PATCH_IDS.map((id) => {
20
+ const wheel = wheelsById.get(id);
21
+ return {
22
+ id,
23
+ present: Boolean(wheel),
24
+ signedOffsetMeters: simUnitsToMeters(wheel?.signedOffset ?? 0),
25
+ crossTrackErrorMeters: simUnitsToMeters(wheel?.crossTrackError ?? 0),
26
+ surface: wheel?.surface ?? 'track',
27
+ surfaceCode: SURFACE_CODES[wheel?.surface ?? 'track'] ?? SURFACE_CODES.barrier,
28
+ onLegalSurface: Boolean(wheel?.onTrack || wheel?.inPitLane),
29
+ inPitLane: Boolean(wheel?.inPitLane),
30
+ };
31
+ });
32
+ }