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