@inventure71/paddockjs 1.0.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (190) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/INSTALL_AND_UPDATE.md +4 -3
  3. package/README.md +194 -12
  4. package/docs/architecture.md +119 -44
  5. package/docs/concepts.md +4 -2
  6. package/docs/custom_model_controller.md +329 -0
  7. package/docs/data_contract.md +299 -17
  8. package/docs/index.md +1 -0
  9. package/docs/learnings.md +22 -0
  10. package/docs/rules.md +37 -10
  11. package/docs/system_specs.md +23 -10
  12. package/docs/training.md +272 -30
  13. package/examples/python/README.md +62 -0
  14. package/examples/python/base_policy_server.py +191 -0
  15. package/examples/python/requirements.txt +2 -0
  16. package/package.json +12 -4
  17. package/src/api/PaddockSimulatorController.js +4 -0
  18. package/src/app/BrowserExpertAdapter.js +367 -9
  19. package/src/app/F1SimulatorApp.js +305 -1603
  20. package/src/app/banners/bannerFormatters.js +55 -0
  21. package/src/app/banners/raceDataBannerController.js +141 -0
  22. package/src/app/banners/stewardMessageController.js +99 -0
  23. package/src/app/camera/cameraConstants.js +21 -0
  24. package/src/app/camera/cameraController.js +265 -0
  25. package/src/app/camera/cameraGeometry.js +176 -0
  26. package/src/app/domBindings.js +4 -0
  27. package/src/app/readouts/carOverviewRenderer.js +109 -0
  28. package/src/app/readouts/controlStateRenderer.js +37 -0
  29. package/src/app/readouts/raceStatusRenderer.js +133 -0
  30. package/src/app/readouts/readoutFormatters.js +58 -0
  31. package/src/app/readouts/telemetryRenderer.js +113 -0
  32. package/src/app/readouts/timingTowerRenderer.js +177 -0
  33. package/src/app/rendering/appAssets.js +38 -0
  34. package/src/app/rendering/carRenderer.js +286 -0
  35. package/src/app/rendering/displayUtils.js +22 -0
  36. package/src/app/rendering/drsTrailRenderer.js +90 -0
  37. package/src/app/rendering/expertSensorRenderer.js +126 -0
  38. package/src/app/rendering/pitLaneStatusRenderer.js +50 -0
  39. package/src/app/rendering/replayGhostRenderer.js +124 -0
  40. package/src/app/rendering/trackRenderer.js +25 -0
  41. package/src/app/runtime/frameLoop.js +66 -0
  42. package/src/app/runtime/runtimeTiming.js +34 -0
  43. package/src/app/runtime/runtimeVisibility.js +45 -0
  44. package/src/config/defaultOptions.js +20 -0
  45. package/src/environment/actions.js +13 -5
  46. package/src/environment/controllerLoop.js +246 -0
  47. package/src/environment/episode.js +86 -2
  48. package/src/environment/evaluation.js +54 -32
  49. package/src/environment/index.d.ts +361 -9
  50. package/src/environment/index.js +1 -0
  51. package/src/environment/metrics.js +159 -0
  52. package/src/environment/observationOptions.js +22 -1
  53. package/src/environment/observations.js +325 -40
  54. package/src/environment/options.js +37 -5
  55. package/src/environment/runtime.js +222 -21
  56. package/src/environment/scenarios.js +37 -8
  57. package/src/environment/sensors/bodySenses.js +23 -0
  58. package/src/environment/sensors/boundarySenses.js +17 -0
  59. package/src/environment/sensors/carRays.js +75 -0
  60. package/src/environment/sensors/contactSenses.js +32 -0
  61. package/src/environment/sensors/index.js +157 -0
  62. package/src/environment/sensors/indexedRayBands.js +103 -0
  63. package/src/environment/sensors/nearbyCars.js +52 -0
  64. package/src/environment/sensors/opponentRadar.js +65 -0
  65. package/src/environment/sensors/rayConfig.js +110 -0
  66. package/src/environment/sensors/rayDefaults.js +6 -0
  67. package/src/environment/sensors/rayGeometry.js +60 -0
  68. package/src/environment/sensors/rayGuards.js +19 -0
  69. package/src/environment/sensors/sensorTargets.js +60 -0
  70. package/src/environment/sensors/surfaceRays.js +296 -0
  71. package/src/environment/sensors/trackRays.js +261 -0
  72. package/src/environment/sensors.js +1 -367
  73. package/src/environment/specs.js +100 -5
  74. package/src/environment/workerProtocol.js +13 -1
  75. package/src/index.d.ts +169 -6
  76. package/src/index.js +5 -0
  77. package/src/rendering/proceduralTrackAsset.js +20 -823
  78. package/src/rendering/renderSnapshot.js +38 -12
  79. package/src/rendering/track/finishLineRenderer.js +34 -0
  80. package/src/rendering/track/gridRenderer.js +47 -0
  81. package/src/rendering/track/offsetStrokeSafety.js +136 -0
  82. package/src/rendering/track/pitLaneRenderer.js +248 -0
  83. package/src/rendering/track/trackMaterialRenderer.js +326 -0
  84. package/src/rendering/track/trackRenderConstants.js +38 -0
  85. package/src/rendering/track/trackRenderGeometry.js +94 -0
  86. package/src/simulation/collisionGeometry.js +1 -1
  87. package/src/simulation/driver/driverControlConstants.js +28 -0
  88. package/src/simulation/driver/driverController.js +47 -0
  89. package/src/simulation/driver/driverInput.js +28 -0
  90. package/src/simulation/driver/driverMath.js +6 -0
  91. package/src/simulation/driver/driverPersonality.js +25 -0
  92. package/src/simulation/driver/edgeRecovery.js +48 -0
  93. package/src/simulation/driver/racingControls.js +399 -0
  94. package/src/simulation/driver/racingLinePlan.js +166 -0
  95. package/src/simulation/driver/recoveryDynamics.js +38 -0
  96. package/src/simulation/driver/rejoinControls.js +213 -0
  97. package/src/simulation/driver/safetyCarControls.js +35 -0
  98. package/src/simulation/driver/trafficScan.js +51 -0
  99. package/src/simulation/driverController.js +4 -619
  100. package/src/simulation/participants/participantInteractions.js +120 -0
  101. package/src/simulation/pit/pitFlow.js +234 -0
  102. package/src/simulation/pit/pitIntent.js +92 -0
  103. package/src/simulation/pit/pitOccupancy.js +47 -0
  104. package/src/simulation/pit/pitPenaltyService.js +71 -0
  105. package/src/simulation/pit/pitQueue.js +55 -0
  106. package/src/simulation/pit/pitRouteMovement.js +160 -0
  107. package/src/simulation/pit/pitRouting.js +319 -0
  108. package/src/simulation/pit/pitService.js +41 -0
  109. package/src/simulation/pit/pitServiceConstants.js +21 -0
  110. package/src/simulation/pit/pitServiceProfile.js +45 -0
  111. package/src/simulation/pit/pitSnapshots.js +61 -0
  112. package/src/simulation/pit/pitState.js +113 -0
  113. package/src/simulation/pit/pitTireService.js +92 -0
  114. package/src/simulation/race/classification.js +104 -0
  115. package/src/simulation/race/gridPenalties.js +37 -0
  116. package/src/simulation/race/raceControlState.js +38 -0
  117. package/src/simulation/race/raceDistance.js +32 -0
  118. package/src/simulation/race/raceFinish.js +65 -0
  119. package/src/simulation/race/raceLifecycle.js +71 -0
  120. package/src/simulation/race/raceOrder.js +113 -0
  121. package/src/simulation/race/raceProgress.js +124 -0
  122. package/src/simulation/race/raceSetup.js +93 -0
  123. package/src/simulation/race/raceStep.js +80 -0
  124. package/src/simulation/race/redFlag.js +15 -0
  125. package/src/simulation/race/retirements.js +44 -0
  126. package/src/simulation/race/safetyCar.js +86 -0
  127. package/src/simulation/race/stalledDnf.js +86 -0
  128. package/src/simulation/raceSimulation.js +230 -2817
  129. package/src/simulation/replay/replayGhosts.js +128 -0
  130. package/src/simulation/rules/moduleConfig.js +72 -0
  131. package/src/simulation/rules/penaltyConfig.js +99 -0
  132. package/src/simulation/rules/penaltyEvents.js +18 -0
  133. package/src/simulation/rules/penaltyStats.js +26 -0
  134. package/src/simulation/rules/ruleConfigMerge.js +41 -0
  135. package/src/simulation/rules/ruleDefaults.js +85 -0
  136. package/src/simulation/rules/rulesReview.js +141 -0
  137. package/src/simulation/rules/rulesetPresets.js +47 -0
  138. package/src/simulation/rules/trackLimitsSteward.js +1 -1
  139. package/src/simulation/rulesConfig.js +5 -323
  140. package/src/simulation/snapshots/raceSnapshots.js +192 -0
  141. package/src/simulation/timing/drsTiming.js +87 -0
  142. package/src/simulation/timing/gapEstimation.js +38 -0
  143. package/src/simulation/timing/lapTelemetry.js +182 -0
  144. package/src/simulation/timing/raceTiming.js +15 -0
  145. package/src/simulation/timing/sectorPerformance.js +60 -0
  146. package/src/simulation/timing/timingConstants.js +5 -0
  147. package/src/simulation/timing/timingHistory.js +46 -0
  148. package/src/simulation/timing/timingLines.js +79 -0
  149. package/src/simulation/track/centerlineSpline.js +59 -0
  150. package/src/simulation/track/drsZones.js +67 -0
  151. package/src/simulation/track/pitLaneAccess.js +103 -0
  152. package/src/simulation/track/pitLaneLayout.js +395 -0
  153. package/src/simulation/track/pitLaneState.js +160 -0
  154. package/src/simulation/track/proceduralCenterline.js +237 -0
  155. package/src/simulation/track/sampleGeometry.js +49 -0
  156. package/src/simulation/track/spatialQueries.js +154 -0
  157. package/src/simulation/track/startStraight.js +141 -0
  158. package/src/simulation/track/trackConstants.js +109 -0
  159. package/src/simulation/track/trackGenerationOptions.js +283 -0
  160. package/src/simulation/track/trackMath.js +241 -0
  161. package/src/simulation/track/trackModel.js +240 -0
  162. package/src/simulation/track/trackQueryIndex.js +945 -0
  163. package/src/simulation/track/trackSectors.js +19 -0
  164. package/src/simulation/track/trackStatePolicy.js +24 -0
  165. package/src/simulation/track/trackValidation.js +98 -0
  166. package/src/simulation/trackModel.js +1 -1706
  167. package/src/simulation/vehicle/contactResolution.js +116 -0
  168. package/src/simulation/vehicle/mainTrackWheelSurface.js +82 -0
  169. package/src/simulation/vehicle/pitWheelSurface.js +69 -0
  170. package/src/simulation/vehicle/runoffResponse.js +88 -0
  171. package/src/simulation/vehicle/surfacePriority.js +28 -0
  172. package/src/simulation/vehicle/vehicleGeometry.js +247 -0
  173. package/src/simulation/vehicle/vehiclePhysics.js +363 -0
  174. package/src/simulation/vehicle/vehicleSnapshots.js +316 -0
  175. package/src/simulation/vehicle/vehicleState.js +186 -0
  176. package/src/simulation/vehicle/wheelSurface.js +150 -0
  177. package/src/simulation/vehicleGeometry.js +1 -247
  178. package/src/simulation/vehiclePhysics.js +1 -136
  179. package/src/simulation/warmup/runtimeWarmup.js +224 -0
  180. package/src/simulation/wheelSurface.js +1 -338
  181. package/src/styles.css +42 -0
  182. package/src/ui/cameraControlsTemplate.js +20 -0
  183. package/src/ui/carOverviewTemplate.js +33 -0
  184. package/src/ui/componentTemplates.js +14 -360
  185. package/src/ui/raceCanvasTemplate.js +69 -0
  186. package/src/ui/raceControlsTemplate.js +36 -0
  187. package/src/ui/shellTemplate.js +5 -4
  188. package/src/ui/telemetryDrawerTemplate.js +55 -0
  189. package/src/ui/telemetryTemplates.js +136 -0
  190. 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,51 @@ 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
+ applyEnvironmentScenario(warmupSimulation, options);
51
+ for (let index = 0; index < warmup.steps; index += 1) warmupSimulation.step(FIXED_STEP);
52
+ const warmupSnapshot = warmupSimulation.snapshotObservation?.() ?? warmupSimulation.snapshot();
53
+ buildEnvironmentObservation({
54
+ snapshot: warmupSnapshot,
55
+ options,
56
+ events: [],
57
+ });
58
+ },
59
+ });
60
+ const sim = createRaceSimulation(disableWarmupOptions(simulationOptions, 'environment'));
32
61
  applyEnvironmentScenario(sim, options);
33
62
  return sim;
34
63
  }
35
64
 
65
+ function shouldUseTrackQueryIndex(options) {
66
+ return options.trackQueryIndex !== false;
67
+ }
68
+
36
69
  export function createEnvironmentRuntime(host) {
37
70
  const episodeState = createEpisodeState();
38
71
 
39
72
  initializeControlledPitIntent(host);
73
+ initializeDriverEpisodes(episodeState, host.getOptions().controlledDrivers);
40
74
 
41
75
  function reset(nextOptions = {}) {
42
- const options = resolveEnvironmentOptions({
43
- ...host.getOptions(),
44
- ...nextOptions,
45
- });
76
+ const options = resolveEnvironmentOptions(mergeEnvironmentResetOptions(host.getOptions(), nextOptions));
46
77
  host.setOptions(options);
47
78
  host.setSimulation(host.createSimulation(options));
48
79
  initializeControlledPitIntent(host);
49
80
  episodeState.step = 0;
50
81
  episodeState.previousSnapshot = null;
82
+ initializeDriverEpisodes(episodeState, options.controlledDrivers);
51
83
  const result = buildResult({ host, episodeState, events: [], actionErrors: [] });
52
84
  episodeState.lastResult = result;
85
+ emitExternalRenderFrame(host, result, { source: 'reset' });
53
86
  host.afterReset(result);
54
87
  return result;
55
88
  }
@@ -61,6 +94,11 @@ export function createEnvironmentRuntime(host) {
61
94
  policy: options.actionPolicy,
62
95
  });
63
96
 
97
+ if (options.actionPolicy === 'report') {
98
+ options.controlledDrivers.forEach((driverId) => {
99
+ if (!Object.hasOwn(controlsByDriver, driverId)) sim.clearCarControls?.(driverId);
100
+ });
101
+ }
64
102
  Object.entries(controlsByDriver).forEach(([driverId, controls]) => {
65
103
  sim.setCarControls(driverId, controls);
66
104
  });
@@ -71,7 +109,7 @@ export function createEnvironmentRuntime(host) {
71
109
  }
72
110
  });
73
111
 
74
- episodeState.previousSnapshot = episodeState.lastResult?.state?.snapshot ?? sim.snapshot();
112
+ episodeState.previousSnapshot = episodeState.lastRewardSnapshot ?? snapshotForResult(sim, options, options.result.stateOutput);
75
113
  const stepEvents = [];
76
114
  for (let index = 0; index < options.frameSkip; index += 1) {
77
115
  sim.step(FIXED_STEP);
@@ -81,8 +119,10 @@ export function createEnvironmentRuntime(host) {
81
119
  stepEvents.push(...collectStepEvents(events));
82
120
  }
83
121
  episodeState.step += 1;
122
+ advanceDriverEpisodes(episodeState, options.controlledDrivers);
84
123
  const result = buildResult({ host, episodeState, events: stepEvents, actionErrors: errors, actions });
85
124
  episodeState.lastResult = result;
125
+ emitExternalRenderFrame(host, result, { source: 'step', actions });
86
126
  host.afterStep(result);
87
127
  return result;
88
128
  }
@@ -92,8 +132,49 @@ export function createEnvironmentRuntime(host) {
92
132
  buildResult({ host, episodeState, events: [], actionErrors: [] }).observation;
93
133
  }
94
134
 
95
- function getState() {
96
- return { snapshot: host.getSimulation().snapshot() };
135
+ function getState({ output = 'full' } = {}) {
136
+ const sim = host.getSimulation();
137
+ if (output === 'none') return null;
138
+ if (output === 'minimal') return { snapshot: sim.snapshotObservation?.() ?? sim.snapshot() };
139
+ return { snapshot: sim.snapshot() };
140
+ }
141
+
142
+ function resetDrivers(placements = {}, resultOptions = {}) {
143
+ const options = host.getOptions();
144
+ const sim = host.getSimulation();
145
+ const driverIds = new Set(options.controlledDrivers);
146
+ const normalizedPlacements = normalizeEnvironmentPlacements(placements, driverIds);
147
+ const snapshot = sim.snapshotObservation?.() ?? sim.snapshot();
148
+ const carsById = new Map(snapshot.cars.map((car) => [car.id, car]));
149
+ applyEnvironmentPlacements(sim, snapshot.track, carsById, normalizedPlacements);
150
+ Object.keys(normalizedPlacements).forEach((driverId) => {
151
+ sim.clearCarControls?.(driverId);
152
+ sim.setAutomaticPitIntentEnabled?.(driverId, false);
153
+ sim.setPitIntent?.(driverId, 0);
154
+ const car = sim.cars?.find?.((item) => item.id === driverId);
155
+ if (car) sim.applyRunoffResponse?.(car);
156
+ });
157
+ resetDriverEpisodes(episodeState, Object.keys(normalizedPlacements));
158
+ episodeState.previousSnapshot = null;
159
+ const resetDriverIds = Object.keys(normalizedPlacements);
160
+ const observationScope = resultOptions.observationScope ??
161
+ resultOptions.resetDriversObservationScope ??
162
+ options.result.resetDriversObservationScope;
163
+ const result = buildResult({
164
+ host,
165
+ episodeState,
166
+ events: [],
167
+ actionErrors: [],
168
+ controlledDrivers: observationScope === 'reset' ? resetDriverIds : options.controlledDrivers,
169
+ stateOutput: resultOptions.stateOutput,
170
+ });
171
+ episodeState.lastResult = result;
172
+ emitExternalRenderFrame(host, result, {
173
+ source: 'resetDrivers',
174
+ resetDriverIds,
175
+ observationScope,
176
+ });
177
+ return result;
97
178
  }
98
179
 
99
180
  function getActionSpec() {
@@ -107,9 +188,41 @@ export function createEnvironmentRuntime(host) {
107
188
  function destroy() {
108
189
  episodeState.lastResult = null;
109
190
  episodeState.previousSnapshot = null;
191
+ episodeState.lastObservationSnapshot = null;
192
+ episodeState.lastRewardSnapshot = null;
193
+ episodeState.drivers?.clear?.();
110
194
  }
111
195
 
112
- return { reset, step, getObservation, getState, getActionSpec, getObservationSpec, destroy };
196
+ return { reset, step, resetDrivers, getObservation, getState, getActionSpec, getObservationSpec, destroy };
197
+ }
198
+
199
+ function mergeEnvironmentResetOptions(currentOptions, nextOptions = {}) {
200
+ return mergePlainObjects(currentOptions, nextOptions);
201
+ }
202
+
203
+ const REPLACE_OBJECT_PATHS = new Set([
204
+ 'scenario.placements',
205
+ ]);
206
+
207
+ function mergePlainObjects(base, overrides, path = []) {
208
+ if (!isPlainObject(base) || !isPlainObject(overrides)) return overrides;
209
+ const merged = { ...base };
210
+ Object.entries(overrides).forEach(([key, value]) => {
211
+ const nextPath = [...path, key];
212
+ merged[key] = isPlainObject(value) &&
213
+ isPlainObject(base[key]) &&
214
+ !REPLACE_OBJECT_PATHS.has(nextPath.join('.'))
215
+ ? mergePlainObjects(base[key], value, nextPath)
216
+ : value;
217
+ });
218
+ return merged;
219
+ }
220
+
221
+ function isPlainObject(value) {
222
+ return value != null &&
223
+ typeof value === 'object' &&
224
+ !Array.isArray(value) &&
225
+ (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null);
113
226
  }
114
227
 
115
228
  function isNoopPitIntent(pitIntent) {
@@ -125,51 +238,139 @@ function initializeControlledPitIntent(host) {
125
238
  options?.controlledDrivers?.forEach?.((driverId) => {
126
239
  sim?.setAutomaticPitIntentEnabled?.(driverId, false);
127
240
  sim?.setPitIntent?.(driverId, 0);
241
+ const car = sim?.cars?.find?.((item) => item.id === driverId);
242
+ if (car) car.environmentControlled = true;
128
243
  });
129
244
  }
130
245
 
131
- function buildResult({ host, episodeState, events, actionErrors, actions = {} }) {
246
+ function buildResult({
247
+ host,
248
+ episodeState,
249
+ events,
250
+ actionErrors,
251
+ actions = {},
252
+ controlledDrivers = null,
253
+ stateOutput = null,
254
+ }) {
132
255
  const options = host.getOptions();
133
- const snapshot = host.getSimulation().snapshot();
256
+ const sim = host.getSimulation();
257
+ const resultDrivers = controlledDrivers ?? options.controlledDrivers;
258
+ const resolvedStateOutput = stateOutput ?? options.result.stateOutput;
259
+ const observationSnapshot = snapshotForResult(sim, options, resolvedStateOutput);
260
+ markDestroyedDriverEpisodes(episodeState, options.controlledDrivers, observationSnapshot);
134
261
  const observation = buildEnvironmentObservation({
135
- snapshot,
262
+ snapshot: observationSnapshot,
136
263
  previousSnapshot: episodeState.previousSnapshot,
137
264
  options,
138
265
  events,
266
+ controlledDrivers: resultDrivers,
267
+ });
268
+ const episode = evaluateEpisode(observationSnapshot, options, episodeState, resultDrivers);
269
+ const metrics = buildDriverMetrics({
270
+ snapshot: observationSnapshot,
271
+ previousSnapshot: episodeState.previousSnapshot,
272
+ options: { ...options, controlledDrivers: resultDrivers },
273
+ events,
274
+ });
275
+ const rewardEpisodeInfo = buildDriverEpisodeInfo(episodeState, {
276
+ ...options,
277
+ controlledDrivers: resultDrivers,
278
+ }, episode);
279
+ const driverEpisodeInfo = buildDriverEpisodeInfo(episodeState, options, episode);
280
+ const { state, rewardSnapshot } = buildResultState(sim, observationSnapshot, resolvedStateOutput);
281
+ const reward = computeReward({
282
+ options: { ...options, controlledDrivers: resultDrivers },
283
+ observation,
284
+ events,
285
+ snapshot: rewardSnapshot,
286
+ actions,
287
+ previousSnapshot: episodeState.previousSnapshot,
288
+ metrics,
289
+ driverEpisodeInfo: rewardEpisodeInfo,
139
290
  });
140
- const episode = evaluateEpisode(snapshot, options, episodeState);
141
- const reward = computeReward({ options, observation, events, snapshot, actions, previousSnapshot: episodeState.previousSnapshot });
291
+ episodeState.lastObservationSnapshot = observationSnapshot;
292
+ episodeState.lastRewardSnapshot = rewardSnapshot;
142
293
  return {
143
294
  observation,
144
295
  reward,
296
+ metrics,
145
297
  terminated: episode.terminated,
146
298
  truncated: episode.truncated,
147
299
  done: episode.terminated || episode.truncated,
148
300
  events,
149
- state: { snapshot },
301
+ state,
150
302
  info: {
151
303
  step: episodeState.step,
152
- elapsedSeconds: snapshot.time,
304
+ elapsedSeconds: observationSnapshot.time,
153
305
  seed: options.seed,
154
306
  trackSeed: options.trackSeed,
155
307
  controlledDrivers: [...options.controlledDrivers],
156
308
  actionErrors,
157
309
  endReason: episode.endReason,
310
+ drivers: driverEpisodeInfo,
158
311
  },
159
312
  };
160
313
  }
161
314
 
162
- function computeReward({ options, observation, events, snapshot, actions, previousSnapshot }) {
315
+ function buildResultState(sim, observationSnapshot, stateOutput) {
316
+ if (stateOutput === 'none') return { state: null, rewardSnapshot: observationSnapshot };
317
+ if (stateOutput === 'minimal') return { state: { snapshot: observationSnapshot }, rewardSnapshot: observationSnapshot };
318
+ const snapshot = sim.snapshot();
319
+ return { state: { snapshot }, rewardSnapshot: snapshot };
320
+ }
321
+
322
+ function snapshotForResult(sim, options, stateOutput) {
323
+ if (canUseTrainingSnapshot(options, stateOutput)) {
324
+ return sim.snapshotTraining?.() ?? sim.snapshotObservation?.() ?? sim.snapshot();
325
+ }
326
+ return sim.snapshotObservation?.() ?? sim.snapshot();
327
+ }
328
+
329
+ function canUseTrainingSnapshot(options, stateOutput) {
330
+ return stateOutput === 'none' &&
331
+ options.observation?.output === 'vector' &&
332
+ options.observation?.includeSchema === false;
333
+ }
334
+
335
+ function computeReward({ options, observation, events, snapshot, actions, previousSnapshot, metrics, driverEpisodeInfo }) {
163
336
  if (!options.reward) return null;
164
337
  return Object.fromEntries(options.controlledDrivers.map((driverId) => [
165
338
  driverId,
166
- Number(options.reward({
339
+ normalizeRewardValue(options.reward({
167
340
  driverId,
168
341
  previous: previousSnapshot,
169
342
  current: observation[driverId],
170
343
  action: actions?.[driverId],
171
344
  events: observation[driverId]?.events ?? events,
172
345
  state: { snapshot },
173
- }) ?? 0),
346
+ metrics: metrics?.[driverId] ?? null,
347
+ episode: driverEpisodeInfo?.[driverId] ?? null,
348
+ })),
174
349
  ]));
175
350
  }
351
+
352
+ function emitExternalRenderFrame(host, result, meta = {}) {
353
+ const options = host.getOptions?.();
354
+ const onFrame = options?.externalRenderer?.onFrame;
355
+ if (typeof onFrame !== 'function') return;
356
+ try {
357
+ onFrame({
358
+ snapshot: result?.state?.snapshot ??
359
+ host.getSimulation?.()?.snapshotObservation?.() ??
360
+ host.getSimulation?.()?.snapshot?.(),
361
+ observation: result?.observation ?? {},
362
+ meta: {
363
+ ...meta,
364
+ step: result?.info?.step ?? null,
365
+ elapsedSeconds: result?.info?.elapsedSeconds ?? null,
366
+ },
367
+ });
368
+ } catch {
369
+ // External rendering hooks are observer-only and must not break stepping.
370
+ }
371
+ }
372
+
373
+ function normalizeRewardValue(value) {
374
+ const reward = Number(value ?? 0);
375
+ return Number.isFinite(reward) ? reward : 0;
376
+ }
@@ -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,7 +177,23 @@ 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
+ });
187
+ if (typeof sim.setCarStates === 'function') {
188
+ sim.setCarStates(states);
189
+ return;
190
+ }
191
+ Object.entries(states).forEach(([driverId, partial]) => {
192
+ sim.setCarState(driverId, partial);
193
+ });
194
+ }
195
+
196
+ function placementToCarState(track, currentCar, placement) {
178
197
  const currentDistanceMeters = currentCar.distanceMeters ?? simUnitsToMeters(currentCar.raceDistance ?? currentCar.progress ?? 0);
179
198
  const raceDistance = metersToSimUnits(Math.max(0, placement.distanceMeters ?? currentDistanceMeters));
180
199
  const offset = metersToSimUnits(placement.offsetMeters ?? 0);
@@ -191,9 +210,19 @@ function applyPlacement(sim, track, currentCar, driverId, placement) {
191
210
  desiredOffset: offset,
192
211
  heading,
193
212
  previousHeading: heading,
213
+ destroyed: false,
214
+ destroyReason: null,
215
+ destroyedAt: null,
216
+ outOfRace: false,
217
+ canAttack: true,
218
+ throttle: 0,
219
+ brake: 0,
220
+ steeringAngle: 0,
221
+ yawRate: 0,
222
+ stabilityState: 'stable',
194
223
  };
195
224
  if (placement.speedKph != null) partial.speed = kphToSimSpeed(placement.speedKph);
196
- sim.setCarState(driverId, partial);
225
+ return partial;
197
226
  }
198
227
 
199
228
  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
+ }