@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
@@ -0,0 +1,261 @@
1
+ export function createPaddockDriverControllerLoop({
2
+ runtime,
3
+ controller,
4
+ actionRepeat = 4,
5
+ mode = 'manual',
6
+ scheduler = null,
7
+ } = {}) {
8
+ if (!runtime || typeof runtime.step !== 'function' || typeof runtime.reset !== 'function') {
9
+ throw new Error('createPaddockDriverControllerLoop requires a runtime with reset() and step(actions).');
10
+ }
11
+ if (!controller || typeof controller.decideBatch !== 'function') {
12
+ throw new Error('createPaddockDriverControllerLoop requires a controller with decideBatch(context).');
13
+ }
14
+
15
+ const repeat = Math.max(1, Math.floor(Number(actionRepeat) || 1));
16
+ let actionSpec = null;
17
+ let observationSpec = null;
18
+ let initialized = false;
19
+ let result = null;
20
+ let previousResult = null;
21
+ let heldActions = null;
22
+ let heldFramesRemaining = 0;
23
+ let policyStep = 0;
24
+ let runtimeStep = 0;
25
+ let observationCache = {};
26
+ let running = false;
27
+ let scheduled = null;
28
+ let previousActions = {};
29
+ let lastDecisionMs = 0;
30
+ let lastError = null;
31
+
32
+ async function ensureSpecs() {
33
+ if (!actionSpec) actionSpec = runtime.getActionSpec();
34
+ if (!observationSpec) observationSpec = runtime.getObservationSpec();
35
+ }
36
+
37
+ async function refreshSpecs() {
38
+ actionSpec = runtime.getActionSpec();
39
+ observationSpec = runtime.getObservationSpec();
40
+ }
41
+
42
+ async function ensureInitialized(context) {
43
+ if (initialized) return;
44
+ await controller.init?.(context);
45
+ initialized = true;
46
+ }
47
+
48
+ async function resetRuntime(options = {}, { stopPlayback = true } = {}) {
49
+ if (stopPlayback) stop();
50
+ result = runtime.reset(options);
51
+ previousResult = null;
52
+ heldActions = null;
53
+ heldFramesRemaining = 0;
54
+ policyStep = 0;
55
+ runtimeStep = Number(result?.info?.step ?? 0);
56
+ observationCache = { ...(result?.observation ?? {}) };
57
+ previousActions = {};
58
+ lastError = null;
59
+ await refreshSpecs();
60
+ const context = buildContext({ resetDriverIds: controlledDrivers() });
61
+ await ensureInitialized(context);
62
+ await controller.reset?.(context);
63
+ return result;
64
+ }
65
+
66
+ async function reset(options = {}) {
67
+ return resetRuntime(options, { stopPlayback: true });
68
+ }
69
+
70
+ async function ensureResult() {
71
+ if (!result) await resetRuntime({}, { stopPlayback: false });
72
+ }
73
+
74
+ async function resetDrivers(placements = {}, resultOptions = {}) {
75
+ if (typeof runtime.resetDrivers !== 'function') {
76
+ throw new Error('The supplied Paddock runtime does not support resetDrivers().');
77
+ }
78
+ const resetDriverIds = Object.keys(placements);
79
+ previousResult = result;
80
+ result = runtime.resetDrivers(placements, resultOptions);
81
+ observationCache = {
82
+ ...observationCache,
83
+ ...(result?.observation ?? {}),
84
+ };
85
+ heldActions = null;
86
+ heldFramesRemaining = 0;
87
+ lastError = null;
88
+ resetDriverIds.forEach((driverId) => {
89
+ delete previousActions[driverId];
90
+ });
91
+ await ensureSpecs();
92
+ const context = buildContext({ resetDriverIds });
93
+ await ensureInitialized(context);
94
+ await controller.reset?.(context);
95
+ return result;
96
+ }
97
+
98
+ async function beginDecision() {
99
+ await ensureResult();
100
+ const context = buildContext();
101
+ await ensureInitialized(context);
102
+ const startedAt = now();
103
+ const actions = await controller.decideBatch(context);
104
+ lastDecisionMs = now() - startedAt;
105
+ heldActions = actions && typeof actions === 'object' ? actions : {};
106
+ heldFramesRemaining = repeat;
107
+ policyStep += 1;
108
+ }
109
+
110
+ async function stepFrame() {
111
+ await ensureResult();
112
+ if (!heldActions || heldFramesRemaining <= 0) {
113
+ await beginDecision();
114
+ }
115
+ const actionIndex = repeat - heldFramesRemaining;
116
+ const actions = heldActions;
117
+ const previousActionsForStep = previousActions;
118
+ previousResult = result;
119
+ result = runtime.step(actions);
120
+ observationCache = { ...(result?.observation ?? {}) };
121
+ heldFramesRemaining -= 1;
122
+ runtimeStep = Number(result?.info?.step ?? runtimeStep + 1);
123
+ previousActions = actions;
124
+ await controller.onStep?.(buildContext({
125
+ actionIndex,
126
+ actions,
127
+ previousActions: previousActionsForStep,
128
+ previousResult,
129
+ }));
130
+ return result;
131
+ }
132
+
133
+ async function step() {
134
+ let latest = result;
135
+ const targetPolicyStep = policyStep + 1;
136
+ while (policyStep < targetPolicyStep || heldFramesRemaining > 0) {
137
+ latest = await stepFrame();
138
+ if (latest?.done) break;
139
+ if (policyStep >= targetPolicyStep && heldFramesRemaining <= 0) break;
140
+ }
141
+ return latest;
142
+ }
143
+
144
+ function start() {
145
+ if (running) return;
146
+ lastError = null;
147
+ running = true;
148
+ const schedule = scheduler ?? defaultScheduler();
149
+ const tick = async () => {
150
+ if (!running) return;
151
+ scheduled = null;
152
+ try {
153
+ await stepFrame();
154
+ } catch (error) {
155
+ lastError = error;
156
+ running = false;
157
+ scheduled = null;
158
+ return;
159
+ }
160
+ if (!running || result?.done) {
161
+ running = false;
162
+ return;
163
+ }
164
+ scheduled = schedule(tick);
165
+ };
166
+ scheduled = schedule(tick);
167
+ }
168
+
169
+ function stop() {
170
+ running = false;
171
+ if (scheduled && typeof scheduled.cancel === 'function') scheduled.cancel();
172
+ else if (typeof scheduled === 'number' && typeof cancelAnimationFrame === 'function') cancelAnimationFrame(scheduled);
173
+ scheduled = null;
174
+ }
175
+
176
+ function controlledDrivers() {
177
+ return actionSpec?.controlledDrivers ?? result?.info?.controlledDrivers ?? [];
178
+ }
179
+
180
+ function buildContext(extra = {}) {
181
+ const drivers = controlledDrivers();
182
+ const observation = {
183
+ ...observationCache,
184
+ ...(result?.observation ?? runtime.getObservation?.() ?? {}),
185
+ };
186
+ return {
187
+ runtime,
188
+ mode,
189
+ actionRepeat: repeat,
190
+ controlledDrivers: drivers,
191
+ actionSpec,
192
+ observationSpec,
193
+ result,
194
+ previousResult: extra.previousResult ?? previousResult,
195
+ observation,
196
+ orderedObservations: drivers.map((driverId, index) => ({
197
+ driverId,
198
+ index,
199
+ observation: observation?.[driverId] ?? null,
200
+ vector: observation?.[driverId]?.vector ?? null,
201
+ })),
202
+ metrics: result?.metrics ?? {},
203
+ events: result?.events ?? [],
204
+ info: result?.info ?? null,
205
+ previousActions: extra.previousActions ?? previousActions,
206
+ orderedPreviousActions: drivers.map((driverId) => (extra.previousActions ?? previousActions)?.[driverId] ?? null),
207
+ actions: extra.actions ?? heldActions,
208
+ policyStep,
209
+ runtimeStep,
210
+ heldFramesRemaining,
211
+ actionIndex: extra.actionIndex ?? 0,
212
+ resetDriverIds: extra.resetDriverIds ?? [],
213
+ };
214
+ }
215
+
216
+ return {
217
+ reset,
218
+ resetDrivers,
219
+ step,
220
+ stepFrame,
221
+ start,
222
+ stop,
223
+ get result() {
224
+ return result;
225
+ },
226
+ get actionSpec() {
227
+ return actionSpec;
228
+ },
229
+ get observationSpec() {
230
+ return observationSpec;
231
+ },
232
+ get stats() {
233
+ return {
234
+ policyStep,
235
+ runtimeStep,
236
+ heldFramesRemaining,
237
+ actions: heldActions,
238
+ actionRepeat: repeat,
239
+ lastDecisionMs,
240
+ running,
241
+ lastError,
242
+ };
243
+ },
244
+ };
245
+ }
246
+
247
+ function now() {
248
+ return typeof performance !== 'undefined' && typeof performance.now === 'function'
249
+ ? performance.now()
250
+ : Date.now();
251
+ }
252
+
253
+ function defaultScheduler() {
254
+ if (typeof requestAnimationFrame === 'function') {
255
+ return (callback) => requestAnimationFrame(callback);
256
+ }
257
+ return (callback) => {
258
+ const id = setTimeout(callback, 0);
259
+ return { cancel: () => clearTimeout(id) };
260
+ };
261
+ }
@@ -1,17 +1,113 @@
1
1
  export function createEpisodeState() {
2
2
  return {
3
3
  step: 0,
4
+ drivers: new Map(),
4
5
  previousSnapshot: null,
5
6
  lastResult: null,
6
7
  };
7
8
  }
8
9
 
9
- export function evaluateEpisode(snapshot, options, episodeState) {
10
+ export function initializeDriverEpisodes(episodeState, driverIds = []) {
11
+ episodeState.drivers = new Map(driverIds.map((driverId) => [driverId, createDriverEpisodeState()]));
12
+ }
13
+
14
+ export function advanceDriverEpisodes(episodeState, driverIds = []) {
15
+ driverIds.forEach((driverId) => {
16
+ const state = ensureDriverEpisodeState(episodeState, driverId);
17
+ if (!state.terminated && !state.truncated) state.episodeStep += 1;
18
+ });
19
+ }
20
+
21
+ export function resetDriverEpisodes(episodeState, driverIds = []) {
22
+ driverIds.forEach((driverId) => {
23
+ const state = ensureDriverEpisodeState(episodeState, driverId);
24
+ state.episodeId += 1;
25
+ state.episodeStep = 0;
26
+ state.terminated = false;
27
+ state.truncated = false;
28
+ state.endReason = null;
29
+ });
30
+ }
31
+
32
+ export function markDestroyedDriverEpisodes(episodeState, driverIds = [], snapshot = null) {
33
+ const carsById = new Map((snapshot?.cars ?? []).map((car) => [car.id, car]));
34
+ driverIds.forEach((driverId) => {
35
+ const car = carsById.get(driverId);
36
+ if (!car?.destroyed && !car?.dnf && !car?.outOfRace) return;
37
+ const state = ensureDriverEpisodeState(episodeState, driverId);
38
+ state.terminated = true;
39
+ state.truncated = false;
40
+ state.endReason = car.destroyed ? 'destroyed' : (car.dnfReason ?? car.destroyReason ?? 'destroyed');
41
+ });
42
+ }
43
+
44
+ export function buildDriverEpisodeInfo(episodeState, options, episode) {
45
+ return Object.fromEntries(options.controlledDrivers.map((driverId) => {
46
+ const state = ensureDriverEpisodeState(episodeState, driverId);
47
+ const maxStepTruncated = state.episodeStep >= options.episode.maxSteps;
48
+ const aggregateTruncated = episode.truncated && episode.endReason !== 'all-drivers-ended';
49
+ const terminated = Boolean(state.terminated || episode.terminated);
50
+ const truncated = Boolean(state.truncated || maxStepTruncated || aggregateTruncated);
51
+ const endReason = state.endReason ??
52
+ (terminated ? episode.endReason : null) ??
53
+ (maxStepTruncated ? 'max-steps' : null) ??
54
+ (truncated ? episode.endReason : null);
55
+ return [driverId, {
56
+ terminated,
57
+ truncated,
58
+ endReason,
59
+ episodeStep: state.episodeStep,
60
+ episodeId: state.episodeId,
61
+ }];
62
+ }));
63
+ }
64
+
65
+ export function evaluateEpisode(snapshot, options, episodeState, controlledDrivers = options.controlledDrivers) {
10
66
  if (snapshot.raceControl.finished && options.episode.endOnRaceFinish) {
11
67
  return { terminated: true, truncated: false, endReason: 'race-finish' };
12
68
  }
13
- if (episodeState.step >= options.episode.maxSteps) {
69
+ let allTerminated = controlledDrivers.length > 0;
70
+ let allTruncated = controlledDrivers.length > 0;
71
+ let allEnded = controlledDrivers.length > 0;
72
+ let anyTruncated = false;
73
+ let firstTerminatedReason = null;
74
+ for (const driverId of controlledDrivers) {
75
+ const state = ensureDriverEpisodeState(episodeState, driverId);
76
+ if (!state.terminated && state.episodeStep >= options.episode.maxSteps) {
77
+ state.truncated = true;
78
+ state.endReason ??= 'max-steps';
79
+ }
80
+ if (!state.terminated) allTerminated = false;
81
+ else firstTerminatedReason ??= state.endReason;
82
+ if (!state.truncated && state.episodeStep < options.episode.maxSteps) allTruncated = false;
83
+ if (state.truncated || state.episodeStep >= options.episode.maxSteps) anyTruncated = true;
84
+ if (!state.terminated && !state.truncated && state.episodeStep < options.episode.maxSteps) allEnded = false;
85
+ if (!allTerminated && !allTruncated && !allEnded) break;
86
+ }
87
+ if (allTerminated) {
88
+ return { terminated: true, truncated: false, endReason: controlledDrivers.length === 1 ? firstTerminatedReason : 'all-drivers-terminated' };
89
+ }
90
+ if (allTruncated) {
14
91
  return { terminated: false, truncated: true, endReason: 'max-steps' };
15
92
  }
93
+ if (allEnded) {
94
+ return { terminated: false, truncated: anyTruncated, endReason: 'all-drivers-ended' };
95
+ }
16
96
  return { terminated: false, truncated: false, endReason: null };
17
97
  }
98
+
99
+ function createDriverEpisodeState() {
100
+ return {
101
+ episodeStep: 0,
102
+ episodeId: 0,
103
+ terminated: false,
104
+ truncated: false,
105
+ endReason: null,
106
+ };
107
+ }
108
+
109
+ function ensureDriverEpisodeState(episodeState, driverId) {
110
+ if (!episodeState.drivers) episodeState.drivers = new Map();
111
+ if (!episodeState.drivers.has(driverId)) episodeState.drivers.set(driverId, createDriverEpisodeState());
112
+ return episodeState.drivers.get(driverId);
113
+ }
@@ -1,4 +1,5 @@
1
1
  import { simUnitsToMeters } from '../simulation/units.js';
2
+ import { isEnvironmentCarOffTrack, isEnvironmentContactEvent } from './metrics.js';
2
3
  import { createPaddockEnvironment } from './runtime.js';
3
4
 
4
5
  export const DEFAULT_EVALUATION_CASES = Object.freeze([
@@ -41,7 +42,7 @@ export function runEnvironmentEvaluation({
41
42
  if (!baseOptions || typeof baseOptions !== 'object') {
42
43
  throw new Error('PaddockJS environment evaluation requires baseOptions.');
43
44
  }
44
- if (!policy) {
45
+ if (!isEvaluationPolicy(policy)) {
45
46
  throw new Error('PaddockJS environment evaluation requires a policy function or object with predict().');
46
47
  }
47
48
 
@@ -55,19 +56,28 @@ export function runEnvironmentEvaluation({
55
56
  };
56
57
  }
57
58
 
59
+ function isEvaluationPolicy(policy) {
60
+ return typeof policy === 'function' ||
61
+ (policy != null && typeof policy.predict === 'function');
62
+ }
63
+
58
64
  export function createEvaluationTracker(initialResult) {
65
+ if (!initialResult?.state?.snapshot) {
66
+ throw new Error('PaddockJS environment evaluation tracker requires result.state.snapshot; use result.stateOutput "minimal" or "full".');
67
+ }
59
68
  const initialCars = carsById(initialResult.state.snapshot);
60
69
  const metrics = Object.fromEntries(initialResult.info.controlledDrivers.map((driverId) => {
61
70
  const car = initialCars.get(driverId);
71
+ const offTrack = isEnvironmentCarOffTrack(car);
62
72
  return [driverId, {
63
73
  distanceMeters: 0,
64
74
  lapProgressMeters: lapProgressMetersFromSnapshotCar(car),
65
- offTrackSteps: car && car.surface !== 'track' ? 1 : 0,
75
+ offTrackSteps: offTrack ? 1 : 0,
66
76
  contactCount: 0,
67
77
  recoverySuccess: false,
68
78
  passCount: 0,
69
79
  lapTimeSeconds: null,
70
- startedOffTrack: Boolean(car && car.surface !== 'track'),
80
+ startedOffTrack: offTrack,
71
81
  startPosition: car?.rank ?? null,
72
82
  bestPosition: car?.rank ?? null,
73
83
  startDistanceMeters: cumulativeDistanceMetersFromSnapshotCar(car),
@@ -83,8 +93,9 @@ export function createEvaluationTracker(initialResult) {
83
93
  if (!car || !entry) return;
84
94
  entry.distanceMeters = Math.max(0, cumulativeDistanceMetersFromSnapshotCar(car) - entry.startDistanceMeters);
85
95
  entry.lapProgressMeters = lapProgressMetersFromSnapshotCar(car);
86
- if (car.surface !== 'track') entry.offTrackSteps += 1;
87
- if (entry.startedOffTrack && car.surface === 'track') entry.recoverySuccess = true;
96
+ const offTrack = isEnvironmentCarOffTrack(car);
97
+ if (offTrack) entry.offTrackSteps += 1;
98
+ if (entry.startedOffTrack && !offTrack) entry.recoverySuccess = true;
88
99
  if (Number.isFinite(car.rank)) {
89
100
  entry.bestPosition = entry.bestPosition == null ? car.rank : Math.min(entry.bestPosition, car.rank);
90
101
  entry.passCount = entry.startPosition == null ? 0 : Math.max(0, entry.startPosition - entry.bestPosition);
@@ -95,7 +106,7 @@ export function createEvaluationTracker(initialResult) {
95
106
  }
96
107
  });
97
108
  result.events.forEach((event) => {
98
- if (event.type !== 'collision') return;
109
+ if (!isEnvironmentContactEvent(event)) return;
99
110
  Object.keys(metrics).forEach((driverId) => {
100
111
  if (event.driverId === driverId || event.carId === driverId || event.otherCarId === driverId || event.driverIds?.includes?.(driverId)) {
101
112
  metrics[driverId].contactCount += 1;
@@ -135,6 +146,10 @@ function lapProgressMetersFromSnapshotCar(car) {
135
146
  function runEvaluationCase({ baseOptions, policy, evaluationCase, createEnvironment }) {
136
147
  const env = createEnvironment({
137
148
  ...baseOptions,
149
+ result: {
150
+ ...(baseOptions.result ?? {}),
151
+ stateOutput: normalizeEvaluationStateOutput(baseOptions.result?.stateOutput),
152
+ },
138
153
  seed: evaluationCase.seed ?? baseOptions.seed,
139
154
  trackSeed: evaluationCase.trackSeed ?? baseOptions.trackSeed,
140
155
  scenario: {
@@ -142,34 +157,41 @@ function runEvaluationCase({ baseOptions, policy, evaluationCase, createEnvironm
142
157
  ...(evaluationCase.scenario ?? {}),
143
158
  },
144
159
  });
145
- let result = env.reset();
146
- const tracker = createEvaluationTracker(result);
147
- const maxSteps = Math.max(1, Math.floor(evaluationCase.maxSteps ?? baseOptions.episode?.maxSteps ?? 1000));
148
- let steps = 0;
149
- while (!result.done && steps < maxSteps) {
150
- const actions = Object.fromEntries(result.info.controlledDrivers.map((driverId) => [
151
- driverId,
152
- predictAction(policy, result.observation[driverId], {
160
+ try {
161
+ let result = env.reset();
162
+ const tracker = createEvaluationTracker(result);
163
+ const maxSteps = Math.max(1, Math.floor(evaluationCase.maxSteps ?? baseOptions.episode?.maxSteps ?? 1000));
164
+ let steps = 0;
165
+ while (!result.done && steps < maxSteps) {
166
+ const actions = Object.fromEntries(result.info.controlledDrivers.map((driverId) => [
153
167
  driverId,
154
- caseName: evaluationCase.name,
155
- step: steps,
156
- result,
157
- }),
158
- ]));
159
- result = env.step(actions);
160
- tracker.update(result);
161
- steps += 1;
168
+ predictAction(policy, result.observation[driverId], {
169
+ driverId,
170
+ caseName: evaluationCase.name,
171
+ step: steps,
172
+ result,
173
+ }),
174
+ ]));
175
+ result = env.step(actions);
176
+ tracker.update(result);
177
+ steps += 1;
178
+ }
179
+ return {
180
+ name: evaluationCase.name,
181
+ seed: evaluationCase.seed ?? baseOptions.seed,
182
+ trackSeed: evaluationCase.trackSeed ?? baseOptions.trackSeed,
183
+ steps,
184
+ done: result.done,
185
+ endReason: result.info.endReason,
186
+ metrics: tracker.finish(),
187
+ };
188
+ } finally {
189
+ env.destroy?.();
162
190
  }
163
- env.destroy?.();
164
- return {
165
- name: evaluationCase.name,
166
- seed: evaluationCase.seed ?? baseOptions.seed,
167
- trackSeed: evaluationCase.trackSeed ?? baseOptions.trackSeed,
168
- steps,
169
- done: result.done,
170
- endReason: result.info.endReason,
171
- metrics: tracker.finish(),
172
- };
191
+ }
192
+
193
+ function normalizeEvaluationStateOutput(stateOutput) {
194
+ return stateOutput === 'none' ? 'minimal' : stateOutput;
173
195
  }
174
196
 
175
197
  function predictAction(policy, observation, context) {