@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
@@ -0,0 +1,246 @@
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 running = false;
26
+ let scheduled = null;
27
+ let previousActions = {};
28
+ let lastDecisionMs = 0;
29
+ let lastError = null;
30
+
31
+ async function ensureSpecs() {
32
+ if (!actionSpec) actionSpec = runtime.getActionSpec();
33
+ if (!observationSpec) observationSpec = runtime.getObservationSpec();
34
+ }
35
+
36
+ async function ensureInitialized(context) {
37
+ if (initialized) return;
38
+ await controller.init?.(context);
39
+ initialized = true;
40
+ }
41
+
42
+ async function resetRuntime(options = {}, { stopPlayback = true } = {}) {
43
+ if (stopPlayback) stop();
44
+ result = runtime.reset(options);
45
+ previousResult = null;
46
+ heldActions = null;
47
+ heldFramesRemaining = 0;
48
+ policyStep = 0;
49
+ runtimeStep = Number(result?.info?.step ?? 0);
50
+ previousActions = {};
51
+ lastError = null;
52
+ await ensureSpecs();
53
+ const context = buildContext({ resetDriverIds: controlledDrivers() });
54
+ await ensureInitialized(context);
55
+ await controller.reset?.(context);
56
+ return result;
57
+ }
58
+
59
+ async function reset(options = {}) {
60
+ return resetRuntime(options, { stopPlayback: true });
61
+ }
62
+
63
+ async function ensureResult() {
64
+ if (!result) await resetRuntime({}, { stopPlayback: false });
65
+ }
66
+
67
+ async function resetDrivers(placements = {}, resultOptions = {}) {
68
+ if (typeof runtime.resetDrivers !== 'function') {
69
+ throw new Error('The supplied Paddock runtime does not support resetDrivers().');
70
+ }
71
+ const resetDriverIds = Object.keys(placements);
72
+ previousResult = result;
73
+ result = runtime.resetDrivers(placements, resultOptions);
74
+ heldActions = null;
75
+ heldFramesRemaining = 0;
76
+ lastError = null;
77
+ resetDriverIds.forEach((driverId) => {
78
+ delete previousActions[driverId];
79
+ });
80
+ await ensureSpecs();
81
+ const context = buildContext({ resetDriverIds });
82
+ await ensureInitialized(context);
83
+ await controller.reset?.(context);
84
+ return result;
85
+ }
86
+
87
+ async function beginDecision() {
88
+ await ensureResult();
89
+ const context = buildContext();
90
+ await ensureInitialized(context);
91
+ const startedAt = now();
92
+ const actions = await controller.decideBatch(context);
93
+ lastDecisionMs = now() - startedAt;
94
+ heldActions = actions && typeof actions === 'object' ? actions : {};
95
+ heldFramesRemaining = repeat;
96
+ policyStep += 1;
97
+ }
98
+
99
+ async function stepFrame() {
100
+ await ensureResult();
101
+ if (!heldActions || heldFramesRemaining <= 0) {
102
+ await beginDecision();
103
+ }
104
+ const actionIndex = repeat - heldFramesRemaining;
105
+ const actions = heldActions;
106
+ const previousActionsForStep = previousActions;
107
+ previousResult = result;
108
+ result = runtime.step(actions);
109
+ heldFramesRemaining -= 1;
110
+ runtimeStep = Number(result?.info?.step ?? runtimeStep + 1);
111
+ previousActions = actions;
112
+ await controller.onStep?.(buildContext({
113
+ actionIndex,
114
+ actions,
115
+ previousActions: previousActionsForStep,
116
+ previousResult,
117
+ }));
118
+ return result;
119
+ }
120
+
121
+ async function step() {
122
+ let latest = result;
123
+ const targetPolicyStep = policyStep + 1;
124
+ while (policyStep < targetPolicyStep || heldFramesRemaining > 0) {
125
+ latest = await stepFrame();
126
+ if (latest?.done) break;
127
+ if (policyStep >= targetPolicyStep && heldFramesRemaining <= 0) break;
128
+ }
129
+ return latest;
130
+ }
131
+
132
+ function start() {
133
+ if (running) return;
134
+ lastError = null;
135
+ running = true;
136
+ const schedule = scheduler ?? defaultScheduler();
137
+ const tick = async () => {
138
+ if (!running) return;
139
+ scheduled = null;
140
+ try {
141
+ await stepFrame();
142
+ } catch (error) {
143
+ lastError = error;
144
+ running = false;
145
+ scheduled = null;
146
+ return;
147
+ }
148
+ if (!running || result?.done) {
149
+ running = false;
150
+ return;
151
+ }
152
+ scheduled = schedule(tick);
153
+ };
154
+ scheduled = schedule(tick);
155
+ }
156
+
157
+ function stop() {
158
+ running = false;
159
+ if (scheduled && typeof scheduled.cancel === 'function') scheduled.cancel();
160
+ else if (typeof scheduled === 'number' && typeof cancelAnimationFrame === 'function') cancelAnimationFrame(scheduled);
161
+ scheduled = null;
162
+ }
163
+
164
+ function controlledDrivers() {
165
+ return actionSpec?.controlledDrivers ?? result?.info?.controlledDrivers ?? [];
166
+ }
167
+
168
+ function buildContext(extra = {}) {
169
+ const drivers = controlledDrivers();
170
+ const observation = result?.observation ?? runtime.getObservation?.() ?? {};
171
+ return {
172
+ runtime,
173
+ mode,
174
+ actionRepeat: repeat,
175
+ controlledDrivers: drivers,
176
+ actionSpec,
177
+ observationSpec,
178
+ result,
179
+ previousResult: extra.previousResult ?? previousResult,
180
+ observation,
181
+ orderedObservations: drivers.map((driverId, index) => ({
182
+ driverId,
183
+ index,
184
+ observation: observation?.[driverId] ?? null,
185
+ vector: observation?.[driverId]?.vector ?? null,
186
+ })),
187
+ metrics: result?.metrics ?? {},
188
+ events: result?.events ?? [],
189
+ info: result?.info ?? null,
190
+ previousActions: extra.previousActions ?? previousActions,
191
+ orderedPreviousActions: drivers.map((driverId) => (extra.previousActions ?? previousActions)?.[driverId] ?? null),
192
+ actions: extra.actions ?? heldActions,
193
+ policyStep,
194
+ runtimeStep,
195
+ heldFramesRemaining,
196
+ actionIndex: extra.actionIndex ?? 0,
197
+ resetDriverIds: extra.resetDriverIds ?? [],
198
+ };
199
+ }
200
+
201
+ return {
202
+ reset,
203
+ resetDrivers,
204
+ step,
205
+ stepFrame,
206
+ start,
207
+ stop,
208
+ get result() {
209
+ return result;
210
+ },
211
+ get actionSpec() {
212
+ return actionSpec;
213
+ },
214
+ get observationSpec() {
215
+ return observationSpec;
216
+ },
217
+ get stats() {
218
+ return {
219
+ policyStep,
220
+ runtimeStep,
221
+ heldFramesRemaining,
222
+ actions: heldActions,
223
+ actionRepeat: repeat,
224
+ lastDecisionMs,
225
+ running,
226
+ lastError,
227
+ };
228
+ },
229
+ };
230
+ }
231
+
232
+ function now() {
233
+ return typeof performance !== 'undefined' && typeof performance.now === 'function'
234
+ ? performance.now()
235
+ : Date.now();
236
+ }
237
+
238
+ function defaultScheduler() {
239
+ if (typeof requestAnimationFrame === 'function') {
240
+ return (callback) => requestAnimationFrame(callback);
241
+ }
242
+ return (callback) => {
243
+ const id = setTimeout(callback, 0);
244
+ return { cancel: () => clearTimeout(id) };
245
+ };
246
+ }
@@ -1,17 +1,101 @@
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 terminated = Boolean(state.terminated || episode.terminated);
49
+ const truncated = Boolean(state.truncated || maxStepTruncated || episode.truncated);
50
+ const endReason = state.endReason ??
51
+ (terminated ? episode.endReason : null) ??
52
+ (maxStepTruncated ? 'max-steps' : null) ??
53
+ (truncated ? episode.endReason : null);
54
+ return [driverId, {
55
+ terminated,
56
+ truncated,
57
+ endReason,
58
+ episodeStep: state.episodeStep,
59
+ episodeId: state.episodeId,
60
+ }];
61
+ }));
62
+ }
63
+
64
+ export function evaluateEpisode(snapshot, options, episodeState, controlledDrivers = options.controlledDrivers) {
10
65
  if (snapshot.raceControl.finished && options.episode.endOnRaceFinish) {
11
66
  return { terminated: true, truncated: false, endReason: 'race-finish' };
12
67
  }
13
- if (episodeState.step >= options.episode.maxSteps) {
68
+ let allTerminated = controlledDrivers.length > 0;
69
+ let allTruncated = controlledDrivers.length > 0;
70
+ let firstTerminatedReason = null;
71
+ for (const driverId of controlledDrivers) {
72
+ const state = ensureDriverEpisodeState(episodeState, driverId);
73
+ if (!state.terminated) allTerminated = false;
74
+ else firstTerminatedReason ??= state.endReason;
75
+ if (!state.truncated && state.episodeStep < options.episode.maxSteps) allTruncated = false;
76
+ if (!allTerminated && !allTruncated) break;
77
+ }
78
+ if (allTerminated) {
79
+ return { terminated: true, truncated: false, endReason: controlledDrivers.length === 1 ? firstTerminatedReason : 'all-drivers-terminated' };
80
+ }
81
+ if (allTruncated) {
14
82
  return { terminated: false, truncated: true, endReason: 'max-steps' };
15
83
  }
16
84
  return { terminated: false, truncated: false, endReason: null };
17
85
  }
86
+
87
+ function createDriverEpisodeState() {
88
+ return {
89
+ episodeStep: 0,
90
+ episodeId: 0,
91
+ terminated: false,
92
+ truncated: false,
93
+ endReason: null,
94
+ };
95
+ }
96
+
97
+ function ensureDriverEpisodeState(episodeState, driverId) {
98
+ if (!episodeState.drivers) episodeState.drivers = new Map();
99
+ if (!episodeState.drivers.has(driverId)) episodeState.drivers.set(driverId, createDriverEpisodeState());
100
+ return episodeState.drivers.get(driverId);
101
+ }
@@ -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) {