@inventure71/paddockjs 0.3.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 (202) hide show
  1. package/CHANGELOG.md +91 -0
  2. package/INSTALL_AND_UPDATE.md +5 -2
  3. package/README.md +328 -13
  4. package/docs/architecture.md +150 -41
  5. package/docs/concepts.md +22 -3
  6. package/docs/custom_model_controller.md +329 -0
  7. package/docs/data_contract.md +474 -34
  8. package/docs/data_model.md +1 -1
  9. package/docs/index.md +1 -0
  10. package/docs/learnings.md +23 -1
  11. package/docs/rules.md +169 -21
  12. package/docs/system_specs.md +56 -20
  13. package/docs/training.md +369 -25
  14. package/examples/python/README.md +62 -0
  15. package/examples/python/base_policy_server.py +191 -0
  16. package/examples/python/requirements.txt +2 -0
  17. package/examples/train-basic-policy.mjs +5 -6
  18. package/examples/trainingData.mjs +133 -0
  19. package/package.json +15 -4
  20. package/src/api/PaddockSimulatorController.js +36 -2
  21. package/src/app/BrowserExpertAdapter.js +367 -9
  22. package/src/app/F1SimulatorApp.js +615 -940
  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 +14 -1
  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 +24 -4
  48. package/src/environment/actions.js +38 -7
  49. package/src/environment/controllerLoop.js +246 -0
  50. package/src/environment/episode.js +86 -2
  51. package/src/environment/evaluation.js +206 -0
  52. package/src/environment/index.d.ts +638 -6
  53. package/src/environment/index.js +14 -0
  54. package/src/environment/metrics.js +159 -0
  55. package/src/environment/observationOptions.js +33 -0
  56. package/src/environment/observations.js +500 -43
  57. package/src/environment/options.js +41 -2
  58. package/src/environment/recorder.js +28 -0
  59. package/src/environment/runtime.js +260 -24
  60. package/src/environment/scenarios.js +249 -0
  61. package/src/environment/sensors/bodySenses.js +23 -0
  62. package/src/environment/sensors/boundarySenses.js +17 -0
  63. package/src/environment/sensors/carRays.js +75 -0
  64. package/src/environment/sensors/contactSenses.js +32 -0
  65. package/src/environment/sensors/index.js +157 -0
  66. package/src/environment/sensors/indexedRayBands.js +103 -0
  67. package/src/environment/sensors/nearbyCars.js +52 -0
  68. package/src/environment/sensors/opponentRadar.js +65 -0
  69. package/src/environment/sensors/rayConfig.js +110 -0
  70. package/src/environment/sensors/rayDefaults.js +6 -0
  71. package/src/environment/sensors/rayGeometry.js +60 -0
  72. package/src/environment/sensors/rayGuards.js +19 -0
  73. package/src/environment/sensors/sensorTargets.js +60 -0
  74. package/src/environment/sensors/surfaceRays.js +296 -0
  75. package/src/environment/sensors/trackRays.js +261 -0
  76. package/src/environment/sensors.js +1 -257
  77. package/src/environment/specs.js +188 -9
  78. package/src/environment/workerProtocol.js +64 -0
  79. package/src/index.d.ts +533 -7
  80. package/src/index.js +26 -0
  81. package/src/rendering/proceduralTrackAsset.js +20 -510
  82. package/src/rendering/renderSnapshot.js +38 -12
  83. package/src/rendering/track/finishLineRenderer.js +34 -0
  84. package/src/rendering/track/gridRenderer.js +47 -0
  85. package/src/rendering/track/offsetStrokeSafety.js +136 -0
  86. package/src/rendering/track/pitLaneRenderer.js +248 -0
  87. package/src/rendering/track/trackMaterialRenderer.js +326 -0
  88. package/src/rendering/track/trackRenderConstants.js +38 -0
  89. package/src/rendering/track/trackRenderGeometry.js +94 -0
  90. package/src/simulation/collisionGeometry.js +214 -0
  91. package/src/simulation/driver/driverControlConstants.js +28 -0
  92. package/src/simulation/driver/driverController.js +47 -0
  93. package/src/simulation/driver/driverInput.js +28 -0
  94. package/src/simulation/driver/driverMath.js +6 -0
  95. package/src/simulation/driver/driverPersonality.js +25 -0
  96. package/src/simulation/driver/edgeRecovery.js +48 -0
  97. package/src/simulation/driver/racingControls.js +399 -0
  98. package/src/simulation/driver/racingLinePlan.js +166 -0
  99. package/src/simulation/driver/recoveryDynamics.js +38 -0
  100. package/src/simulation/driver/rejoinControls.js +213 -0
  101. package/src/simulation/driver/safetyCarControls.js +35 -0
  102. package/src/simulation/driver/trafficScan.js +51 -0
  103. package/src/simulation/driverController.js +4 -253
  104. package/src/simulation/participants/participantInteractions.js +120 -0
  105. package/src/simulation/pit/pitFlow.js +234 -0
  106. package/src/simulation/pit/pitIntent.js +92 -0
  107. package/src/simulation/pit/pitOccupancy.js +47 -0
  108. package/src/simulation/pit/pitPenaltyService.js +71 -0
  109. package/src/simulation/pit/pitQueue.js +55 -0
  110. package/src/simulation/pit/pitRouteMovement.js +160 -0
  111. package/src/simulation/pit/pitRouting.js +319 -0
  112. package/src/simulation/pit/pitService.js +41 -0
  113. package/src/simulation/pit/pitServiceConstants.js +21 -0
  114. package/src/simulation/pit/pitServiceProfile.js +45 -0
  115. package/src/simulation/pit/pitSnapshots.js +61 -0
  116. package/src/simulation/pit/pitState.js +113 -0
  117. package/src/simulation/pit/pitTireService.js +92 -0
  118. package/src/simulation/race/classification.js +104 -0
  119. package/src/simulation/race/gridPenalties.js +37 -0
  120. package/src/simulation/race/raceControlState.js +38 -0
  121. package/src/simulation/race/raceDistance.js +32 -0
  122. package/src/simulation/race/raceFinish.js +65 -0
  123. package/src/simulation/race/raceLifecycle.js +71 -0
  124. package/src/simulation/race/raceOrder.js +113 -0
  125. package/src/simulation/race/raceProgress.js +124 -0
  126. package/src/simulation/race/raceSetup.js +93 -0
  127. package/src/simulation/race/raceStep.js +80 -0
  128. package/src/simulation/race/redFlag.js +15 -0
  129. package/src/simulation/race/retirements.js +44 -0
  130. package/src/simulation/race/safetyCar.js +86 -0
  131. package/src/simulation/race/stalledDnf.js +86 -0
  132. package/src/simulation/raceSimulation.js +386 -1012
  133. package/src/simulation/replay/replayGhosts.js +128 -0
  134. package/src/simulation/rules/collisionSteward.js +69 -0
  135. package/src/simulation/rules/moduleConfig.js +72 -0
  136. package/src/simulation/rules/penaltyConfig.js +99 -0
  137. package/src/simulation/rules/penaltyEvents.js +18 -0
  138. package/src/simulation/rules/penaltyLedger.js +153 -0
  139. package/src/simulation/rules/penaltyStats.js +26 -0
  140. package/src/simulation/rules/pitLaneSpeedingSteward.js +71 -0
  141. package/src/simulation/rules/ruleConfigMerge.js +41 -0
  142. package/src/simulation/rules/ruleDefaults.js +85 -0
  143. package/src/simulation/rules/rulesReview.js +141 -0
  144. package/src/simulation/rules/rulesetPresets.js +47 -0
  145. package/src/simulation/rules/tireRequirementSteward.js +29 -0
  146. package/src/simulation/rules/trackLimitsSteward.js +85 -0
  147. package/src/simulation/rulesConfig.js +34 -0
  148. package/src/simulation/snapshots/raceSnapshots.js +192 -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/trackQueryIndex.js +945 -0
  171. package/src/simulation/track/trackSectors.js +19 -0
  172. package/src/simulation/track/trackStatePolicy.js +24 -0
  173. package/src/simulation/track/trackValidation.js +98 -0
  174. package/src/simulation/trackModel.js +1 -628
  175. package/src/simulation/units.js +6 -4
  176. package/src/simulation/vehicle/contactResolution.js +116 -0
  177. package/src/simulation/vehicle/mainTrackWheelSurface.js +82 -0
  178. package/src/simulation/vehicle/pitWheelSurface.js +69 -0
  179. package/src/simulation/vehicle/runoffResponse.js +88 -0
  180. package/src/simulation/vehicle/surfacePriority.js +28 -0
  181. package/src/simulation/vehicle/vehicleGeometry.js +247 -0
  182. package/src/simulation/vehicle/vehiclePhysics.js +363 -0
  183. package/src/simulation/vehicle/vehicleSnapshots.js +316 -0
  184. package/src/simulation/vehicle/vehicleState.js +186 -0
  185. package/src/simulation/vehicle/wheelSurface.js +150 -0
  186. package/src/simulation/vehicleGeometry.js +1 -0
  187. package/src/simulation/vehiclePhysics.js +1 -104
  188. package/src/simulation/warmup/runtimeWarmup.js +224 -0
  189. package/src/simulation/wheelSurface.js +1 -0
  190. package/src/styles.css +342 -42
  191. package/src/ui/bannerTemplates.js +69 -0
  192. package/src/ui/cameraControlsTemplate.js +20 -0
  193. package/src/ui/carOverviewTemplate.js +33 -0
  194. package/src/ui/componentTemplates.js +20 -426
  195. package/src/ui/raceCanvasTemplate.js +69 -0
  196. package/src/ui/raceControlStatusBanner.js +39 -0
  197. package/src/ui/raceControlsTemplate.js +36 -0
  198. package/src/ui/shellTemplate.js +6 -5
  199. package/src/ui/telemetryDrawerTemplate.js +55 -0
  200. package/src/ui/telemetryTemplates.js +136 -0
  201. package/src/ui/templateUtils.js +23 -0
  202. package/src/ui/timingTowerTemplate.js +34 -0
@@ -1,9 +1,16 @@
1
1
  import { CHAMPIONSHIP_ENTRY_BLUEPRINTS } from '../data/championship.js';
2
2
  import { normalizeSimulatorDrivers } from '../data/normalizeDrivers.js';
3
+ import { normalizePhysicsMode } from '../simulation/vehicle/vehiclePhysics.js';
4
+ import { normalizeWarmupOptions } from '../simulation/warmup/runtimeWarmup.js';
5
+ import { normalizeObservationOptions } from './observationOptions.js';
6
+ import { resolveScenarioPlacementConfig } from './scenarios.js';
7
+ import { normalizeRayOptions } from './sensors.js';
3
8
 
4
9
  const DEFAULT_SEED = 1971;
5
10
  const DEFAULT_FRAME_SKIP = 1;
6
11
  const DEFAULT_MAX_STEPS = 10000;
12
+ const RESULT_STATE_OUTPUTS = new Set(['full', 'minimal', 'none']);
13
+ const RESET_OBSERVATION_SCOPES = new Set(['all', 'reset']);
7
14
 
8
15
  const DEFAULT_SENSOR_OPTIONS = {
9
16
  rays: {
@@ -35,6 +42,7 @@ export function resolveEnvironmentOptions(options = {}) {
35
42
  ...options,
36
43
  seed,
37
44
  trackSeed,
45
+ trackGeneration: options.trackGeneration ?? {},
38
46
  totalLaps: options.totalLaps,
39
47
  drivers: normalizedDrivers,
40
48
  };
@@ -58,20 +66,41 @@ export function resolveEnvironmentOptions(options = {}) {
58
66
  }
59
67
  });
60
68
 
69
+ const externalRenderer = normalizeExternalRenderer(options.externalRenderer);
70
+
61
71
  return {
62
72
  ...resolved,
73
+ physicsMode: normalizePhysicsMode(options.physicsMode),
74
+ warmup: normalizeWarmupOptions(options.warmup, 'environment'),
63
75
  drivers: participantDrivers,
64
76
  controlledDrivers,
65
77
  frameSkip: normalizePositiveInteger(options.frameSkip, DEFAULT_FRAME_SKIP, 'frameSkip'),
66
78
  actionPolicy: options.actionPolicy === 'report' ? 'report' : 'strict',
67
79
  scenario,
80
+ observation: {
81
+ ...normalizeObservationOptions(options.observation ?? {}),
82
+ },
68
83
  sensors: mergeSensorOptions(options.sensors),
69
84
  sensorsByDriver: options.sensorsByDriver ?? {},
70
85
  episode: {
71
86
  maxSteps: normalizePositiveInteger(options.episode?.maxSteps, DEFAULT_MAX_STEPS, 'episode.maxSteps'),
72
87
  endOnRaceFinish: options.episode?.endOnRaceFinish !== false,
73
88
  },
89
+ result: normalizeResultOptions(options.result),
74
90
  reward: typeof options.reward === 'function' ? options.reward : null,
91
+ externalRenderer,
92
+ };
93
+ }
94
+
95
+ export function normalizeResultOptions(result = {}) {
96
+ const stateOutput = RESULT_STATE_OUTPUTS.has(result.stateOutput) ? result.stateOutput : 'full';
97
+ const resetDriversObservationScope = RESET_OBSERVATION_SCOPES.has(result.resetDriversObservationScope)
98
+ ? result.resetDriversObservationScope
99
+ : 'all';
100
+ return {
101
+ ...result,
102
+ stateOutput,
103
+ resetDriversObservationScope,
75
104
  };
76
105
  }
77
106
 
@@ -118,6 +147,7 @@ function resolveScenario(scenario = {}, controlledDrivers, driverIds) {
118
147
  return {
119
148
  participants,
120
149
  nonControlled: 'ai',
150
+ ...resolveScenarioPlacementConfig(scenario, driverIds),
121
151
  };
122
152
  }
123
153
 
@@ -138,13 +168,22 @@ function normalizePositiveInteger(value, fallback, label) {
138
168
 
139
169
  function mergeSensorOptions(sensors = {}) {
140
170
  return {
141
- rays: {
171
+ rays: normalizeRayOptions({
142
172
  ...DEFAULT_SENSOR_OPTIONS.rays,
143
173
  ...(sensors.rays ?? {}),
144
- },
174
+ }),
145
175
  nearbyCars: {
146
176
  ...DEFAULT_SENSOR_OPTIONS.nearbyCars,
147
177
  ...(sensors.nearbyCars ?? {}),
148
178
  },
149
179
  };
150
180
  }
181
+
182
+ function normalizeExternalRenderer(value) {
183
+ if (value == null) return null;
184
+ if (typeof value === 'function') return { onFrame: value };
185
+ if (typeof value === 'object' && typeof value.onFrame === 'function') {
186
+ return { onFrame: value.onFrame };
187
+ }
188
+ throw new Error('PaddockJS environment externalRenderer must be a function or an object with onFrame(frame).');
189
+ }
@@ -0,0 +1,28 @@
1
+ export function createRolloutRecorder() {
2
+ const transitions = [];
3
+ return {
4
+ recordStep(previousResult, action, nextResult) {
5
+ const transition = createRolloutTransition(previousResult, action, nextResult);
6
+ transitions.push(transition);
7
+ return transition;
8
+ },
9
+ clear() {
10
+ transitions.length = 0;
11
+ },
12
+ toJSON() {
13
+ return transitions.map((entry) => ({ ...entry }));
14
+ },
15
+ };
16
+ }
17
+
18
+ export function createRolloutTransition(previousResult, action, nextResult) {
19
+ return {
20
+ observation: previousResult.observation,
21
+ action,
22
+ reward: nextResult.reward,
23
+ nextObservation: nextResult.observation,
24
+ terminated: nextResult.terminated,
25
+ truncated: nextResult.truncated,
26
+ info: nextResult.info,
27
+ };
28
+ }
@@ -1,21 +1,32 @@
1
1
  import { createRaceSimulation, FIXED_STEP } from '../simulation/raceSimulation.js';
2
- import { resolveActionMap } from './actions.js';
2
+ import { disableWarmupOptions, runWarmupWithGuard } from '../simulation/warmup/runtimeWarmup.js';
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';
17
+ import { applyEnvironmentPlacements, applyEnvironmentScenario, normalizeEnvironmentPlacements } from './scenarios.js';
7
18
  import { buildActionSpec, buildObservationSpec } from './specs.js';
8
19
 
9
20
  export function createPaddockEnvironment(options = {}) {
10
21
  let resolvedOptions = resolveEnvironmentOptions(options);
11
- let sim = createRaceSimulation(resolvedOptions);
22
+ let sim = createSimulationWithEnvironmentScenario(resolvedOptions);
12
23
  return createEnvironmentRuntime({
13
24
  getSimulation: () => sim,
14
25
  setSimulation(nextSim) {
15
26
  sim = nextSim;
16
27
  },
17
28
  createSimulation(nextOptions) {
18
- return createRaceSimulation(nextOptions);
29
+ return createSimulationWithEnvironmentScenario(nextOptions);
19
30
  },
20
31
  getOptions: () => resolvedOptions,
21
32
  setOptions(nextOptions) {
@@ -26,20 +37,52 @@ export function createPaddockEnvironment(options = {}) {
26
37
  });
27
38
  }
28
39
 
40
+ function createSimulationWithEnvironmentScenario(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'));
61
+ applyEnvironmentScenario(sim, options);
62
+ return sim;
63
+ }
64
+
65
+ function shouldUseTrackQueryIndex(options) {
66
+ return options.trackQueryIndex !== false;
67
+ }
68
+
29
69
  export function createEnvironmentRuntime(host) {
30
70
  const episodeState = createEpisodeState();
31
71
 
72
+ initializeControlledPitIntent(host);
73
+ initializeDriverEpisodes(episodeState, host.getOptions().controlledDrivers);
74
+
32
75
  function reset(nextOptions = {}) {
33
- const options = resolveEnvironmentOptions({
34
- ...host.getOptions(),
35
- ...nextOptions,
36
- });
76
+ const options = resolveEnvironmentOptions(mergeEnvironmentResetOptions(host.getOptions(), nextOptions));
37
77
  host.setOptions(options);
38
78
  host.setSimulation(host.createSimulation(options));
79
+ initializeControlledPitIntent(host);
39
80
  episodeState.step = 0;
40
81
  episodeState.previousSnapshot = null;
82
+ initializeDriverEpisodes(episodeState, options.controlledDrivers);
41
83
  const result = buildResult({ host, episodeState, events: [], actionErrors: [] });
42
84
  episodeState.lastResult = result;
85
+ emitExternalRenderFrame(host, result, { source: 'reset' });
43
86
  host.afterReset(result);
44
87
  return result;
45
88
  }
@@ -47,23 +90,39 @@ export function createEnvironmentRuntime(host) {
47
90
  function step(actions = {}) {
48
91
  const options = host.getOptions();
49
92
  const sim = host.getSimulation();
50
- const { controlsByDriver, errors } = resolveActionMap(actions, options.controlledDrivers, {
93
+ const { controlsByDriver, pitIntentByDriver, errors } = resolveActionMap(actions, options.controlledDrivers, {
51
94
  policy: options.actionPolicy,
52
95
  });
53
96
 
97
+ if (options.actionPolicy === 'report') {
98
+ options.controlledDrivers.forEach((driverId) => {
99
+ if (!Object.hasOwn(controlsByDriver, driverId)) sim.clearCarControls?.(driverId);
100
+ });
101
+ }
54
102
  Object.entries(controlsByDriver).forEach(([driverId, controls]) => {
55
103
  sim.setCarControls(driverId, controls);
56
104
  });
105
+ Object.entries(pitIntentByDriver).forEach(([driverId, pitIntent]) => {
106
+ const applied = Boolean(sim.setPitIntent?.(driverId, pitIntent));
107
+ if (!applied && !isNoopPitIntent(pitIntent)) {
108
+ handleActionError(`Pit intent could not be applied for controlled driver: ${driverId}`, options.actionPolicy, errors);
109
+ }
110
+ });
57
111
 
58
- episodeState.previousSnapshot = sim.snapshot();
112
+ episodeState.previousSnapshot = episodeState.lastRewardSnapshot ?? snapshotForResult(sim, options, options.result.stateOutput);
59
113
  const stepEvents = [];
60
114
  for (let index = 0; index < options.frameSkip; index += 1) {
61
115
  sim.step(FIXED_STEP);
62
- stepEvents.push(...collectStepEvents(sim.snapshot().events));
116
+ const events = typeof sim.consumeStepEvents === 'function'
117
+ ? sim.consumeStepEvents()
118
+ : sim.events;
119
+ stepEvents.push(...collectStepEvents(events));
63
120
  }
64
121
  episodeState.step += 1;
122
+ advanceDriverEpisodes(episodeState, options.controlledDrivers);
65
123
  const result = buildResult({ host, episodeState, events: stepEvents, actionErrors: errors, actions });
66
124
  episodeState.lastResult = result;
125
+ emitExternalRenderFrame(host, result, { source: 'step', actions });
67
126
  host.afterStep(result);
68
127
  return result;
69
128
  }
@@ -73,8 +132,49 @@ export function createEnvironmentRuntime(host) {
73
132
  buildResult({ host, episodeState, events: [], actionErrors: [] }).observation;
74
133
  }
75
134
 
76
- function getState() {
77
- 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;
78
178
  }
79
179
 
80
180
  function getActionSpec() {
@@ -88,53 +188,189 @@ export function createEnvironmentRuntime(host) {
88
188
  function destroy() {
89
189
  episodeState.lastResult = null;
90
190
  episodeState.previousSnapshot = null;
191
+ episodeState.lastObservationSnapshot = null;
192
+ episodeState.lastRewardSnapshot = null;
193
+ episodeState.drivers?.clear?.();
194
+ }
195
+
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);
226
+ }
227
+
228
+ function isNoopPitIntent(pitIntent) {
229
+ if (pitIntent && typeof pitIntent === 'object') {
230
+ return Number(pitIntent.intent ?? pitIntent.pitIntent) === 0;
91
231
  }
232
+ return Number(pitIntent) === 0;
233
+ }
92
234
 
93
- return { reset, step, getObservation, getState, getActionSpec, getObservationSpec, destroy };
235
+ function initializeControlledPitIntent(host) {
236
+ const sim = host.getSimulation?.();
237
+ const options = host.getOptions?.();
238
+ options?.controlledDrivers?.forEach?.((driverId) => {
239
+ sim?.setAutomaticPitIntentEnabled?.(driverId, false);
240
+ sim?.setPitIntent?.(driverId, 0);
241
+ const car = sim?.cars?.find?.((item) => item.id === driverId);
242
+ if (car) car.environmentControlled = true;
243
+ });
94
244
  }
95
245
 
96
- 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
+ }) {
97
255
  const options = host.getOptions();
98
- 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);
99
261
  const observation = buildEnvironmentObservation({
100
- snapshot,
262
+ snapshot: observationSnapshot,
101
263
  previousSnapshot: episodeState.previousSnapshot,
102
264
  options,
103
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,
104
290
  });
105
- const episode = evaluateEpisode(snapshot, options, episodeState);
106
- const reward = computeReward({ options, observation, events, snapshot, actions, previousSnapshot: episodeState.previousSnapshot });
291
+ episodeState.lastObservationSnapshot = observationSnapshot;
292
+ episodeState.lastRewardSnapshot = rewardSnapshot;
107
293
  return {
108
294
  observation,
109
295
  reward,
296
+ metrics,
110
297
  terminated: episode.terminated,
111
298
  truncated: episode.truncated,
112
299
  done: episode.terminated || episode.truncated,
113
300
  events,
114
- state: { snapshot },
301
+ state,
115
302
  info: {
116
303
  step: episodeState.step,
117
- elapsedSeconds: snapshot.time,
304
+ elapsedSeconds: observationSnapshot.time,
118
305
  seed: options.seed,
119
306
  trackSeed: options.trackSeed,
120
307
  controlledDrivers: [...options.controlledDrivers],
121
308
  actionErrors,
122
309
  endReason: episode.endReason,
310
+ drivers: driverEpisodeInfo,
123
311
  },
124
312
  };
125
313
  }
126
314
 
127
- 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 }) {
128
336
  if (!options.reward) return null;
129
337
  return Object.fromEntries(options.controlledDrivers.map((driverId) => [
130
338
  driverId,
131
- Number(options.reward({
339
+ normalizeRewardValue(options.reward({
132
340
  driverId,
133
341
  previous: previousSnapshot,
134
342
  current: observation[driverId],
135
343
  action: actions?.[driverId],
136
344
  events: observation[driverId]?.events ?? events,
137
345
  state: { snapshot },
138
- }) ?? 0),
346
+ metrics: metrics?.[driverId] ?? null,
347
+ episode: driverEpisodeInfo?.[driverId] ?? null,
348
+ })),
139
349
  ]));
140
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
+ }