@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.
- package/CHANGELOG.md +58 -0
- package/INSTALL_AND_UPDATE.md +4 -3
- package/README.md +219 -12
- package/docs/architecture.md +126 -45
- package/docs/concepts.md +9 -5
- package/docs/custom_model_controller.md +329 -0
- package/docs/data_contract.md +317 -22
- package/docs/data_model.md +1 -1
- package/docs/index.md +10 -0
- package/docs/learnings.md +22 -0
- package/docs/rules.md +71 -14
- package/docs/sense_contract.md +59 -0
- package/docs/system_specs.md +75 -13
- package/docs/training.md +280 -32
- 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 +6 -33
- package/src/app/BrowserExpertAdapter.js +76 -9
- package/src/app/BrowserExpertExternalRenderer.js +303 -0
- package/src/app/F1SimulatorApp.js +291 -1630
- 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/config/restartOptions.js +34 -0
- package/src/environment/actions.js +13 -5
- package/src/environment/controllerLoop.js +261 -0
- package/src/environment/episode.js +98 -2
- package/src/environment/evaluation.js +54 -32
- package/src/environment/index.d.ts +376 -10
- package/src/environment/index.js +1 -0
- package/src/environment/metrics.js +159 -0
- package/src/environment/observationOptions.js +22 -1
- package/src/environment/observationVector.js +200 -0
- package/src/environment/observations.js +199 -141
- package/src/environment/options.js +37 -5
- package/src/environment/recorder.js +13 -6
- package/src/environment/rewards.js +13 -2
- package/src/environment/runtime.js +272 -23
- package/src/environment/scenarios.js +57 -9
- 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 +75 -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 +66 -0
- package/src/environment/sensors/surfaceRays.js +360 -0
- package/src/environment/sensors/trackRays.js +424 -0
- package/src/environment/sensors.js +1 -367
- package/src/environment/specs.js +159 -26
- package/src/environment/workerProtocol.js +13 -1
- package/src/index.d.ts +171 -6
- package/src/index.js +6 -32
- 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 +239 -0
- package/src/simulation/pit/pitIntent.js +92 -0
- package/src/simulation/pit/pitKinematics.js +24 -0
- package/src/simulation/pit/pitOccupancy.js +47 -0
- package/src/simulation/pit/pitPenaltyService.js +71 -0
- package/src/simulation/pit/pitQueue.js +60 -0
- package/src/simulation/pit/pitRouteMovement.js +167 -0
- package/src/simulation/pit/pitRouting.js +319 -0
- package/src/simulation/pit/pitService.js +43 -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 +94 -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 +67 -0
- package/src/simulation/race/raceOrder.js +113 -0
- package/src/simulation/race/raceProgress.js +129 -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 +92 -0
- package/src/simulation/race/stalledDnf.js +77 -0
- package/src/simulation/raceSimulation.js +231 -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 +144 -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 +193 -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/trackQueryGrid.js +94 -0
- package/src/simulation/track/trackQueryIndex.js +659 -0
- package/src/simulation/track/trackQueryPitIndex.js +140 -0
- package/src/simulation/track/trackQueryScratch.js +46 -0
- package/src/simulation/track/trackQueryStats.js +28 -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 +181 -0
- package/src/simulation/vehicle/mainTrackWheelSurface.js +82 -0
- package/src/simulation/vehicle/pitWheelSurface.js +69 -0
- package/src/simulation/vehicle/runoffResponse.js +87 -0
- package/src/simulation/vehicle/surfacePriority.js +28 -0
- package/src/simulation/vehicle/vehicleGeometry.js +247 -0
- package/src/simulation/vehicle/vehicleKinematics.js +37 -0
- package/src/simulation/vehicle/vehiclePhysics.js +364 -0
- package/src/simulation/vehicle/vehicleSnapshots.js +327 -0
- package/src/simulation/vehicle/vehicleState.js +220 -0
- package/src/simulation/vehicle/wheelSurface.js +152 -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,55 @@ 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
|
+
initializeControlledPitIntentForSimulation(warmupSimulation, options);
|
|
51
|
+
applyEnvironmentScenario(warmupSimulation, options);
|
|
52
|
+
applyControlledRunoffResponse(warmupSimulation, options);
|
|
53
|
+
for (let index = 0; index < warmup.steps; index += 1) warmupSimulation.step(FIXED_STEP);
|
|
54
|
+
const warmupSnapshot = warmupSimulation.snapshotObservation?.() ?? warmupSimulation.snapshot();
|
|
55
|
+
buildEnvironmentObservation({
|
|
56
|
+
snapshot: warmupSnapshot,
|
|
57
|
+
options,
|
|
58
|
+
events: [],
|
|
59
|
+
});
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
const sim = createRaceSimulation(disableWarmupOptions(simulationOptions, 'environment'));
|
|
63
|
+
initializeControlledPitIntentForSimulation(sim, options);
|
|
32
64
|
applyEnvironmentScenario(sim, options);
|
|
65
|
+
applyControlledRunoffResponse(sim, options);
|
|
33
66
|
return sim;
|
|
34
67
|
}
|
|
35
68
|
|
|
69
|
+
function shouldUseTrackQueryIndex(options) {
|
|
70
|
+
return options.trackQueryIndex !== false;
|
|
71
|
+
}
|
|
72
|
+
|
|
36
73
|
export function createEnvironmentRuntime(host) {
|
|
37
74
|
const episodeState = createEpisodeState();
|
|
38
75
|
|
|
39
76
|
initializeControlledPitIntent(host);
|
|
77
|
+
initializeDriverEpisodes(episodeState, host.getOptions().controlledDrivers);
|
|
40
78
|
|
|
41
79
|
function reset(nextOptions = {}) {
|
|
42
|
-
const options = resolveEnvironmentOptions(
|
|
43
|
-
...host.getOptions(),
|
|
44
|
-
...nextOptions,
|
|
45
|
-
});
|
|
80
|
+
const options = resolveEnvironmentOptions(mergeEnvironmentResetOptions(host.getOptions(), nextOptions));
|
|
46
81
|
host.setOptions(options);
|
|
47
82
|
host.setSimulation(host.createSimulation(options));
|
|
48
83
|
initializeControlledPitIntent(host);
|
|
49
84
|
episodeState.step = 0;
|
|
50
85
|
episodeState.previousSnapshot = null;
|
|
86
|
+
initializeDriverEpisodes(episodeState, options.controlledDrivers);
|
|
51
87
|
const result = buildResult({ host, episodeState, events: [], actionErrors: [] });
|
|
52
88
|
episodeState.lastResult = result;
|
|
89
|
+
emitExternalRenderFrame(host, result, { source: 'reset' });
|
|
53
90
|
host.afterReset(result);
|
|
54
91
|
return result;
|
|
55
92
|
}
|
|
@@ -61,6 +98,11 @@ export function createEnvironmentRuntime(host) {
|
|
|
61
98
|
policy: options.actionPolicy,
|
|
62
99
|
});
|
|
63
100
|
|
|
101
|
+
if (options.actionPolicy === 'report') {
|
|
102
|
+
options.controlledDrivers.forEach((driverId) => {
|
|
103
|
+
if (!Object.hasOwn(controlsByDriver, driverId)) sim.clearCarControls?.(driverId);
|
|
104
|
+
});
|
|
105
|
+
}
|
|
64
106
|
Object.entries(controlsByDriver).forEach(([driverId, controls]) => {
|
|
65
107
|
sim.setCarControls(driverId, controls);
|
|
66
108
|
});
|
|
@@ -71,7 +113,7 @@ export function createEnvironmentRuntime(host) {
|
|
|
71
113
|
}
|
|
72
114
|
});
|
|
73
115
|
|
|
74
|
-
episodeState.previousSnapshot = episodeState.
|
|
116
|
+
episodeState.previousSnapshot = episodeState.lastRewardSnapshot ?? snapshotForResult(sim, options, options.result.stateOutput);
|
|
75
117
|
const stepEvents = [];
|
|
76
118
|
for (let index = 0; index < options.frameSkip; index += 1) {
|
|
77
119
|
sim.step(FIXED_STEP);
|
|
@@ -81,8 +123,17 @@ export function createEnvironmentRuntime(host) {
|
|
|
81
123
|
stepEvents.push(...collectStepEvents(events));
|
|
82
124
|
}
|
|
83
125
|
episodeState.step += 1;
|
|
84
|
-
|
|
126
|
+
advanceDriverEpisodes(episodeState, options.controlledDrivers);
|
|
127
|
+
const result = buildResult({
|
|
128
|
+
host,
|
|
129
|
+
episodeState,
|
|
130
|
+
events: stepEvents,
|
|
131
|
+
actionErrors: errors,
|
|
132
|
+
actions,
|
|
133
|
+
rewardEnabled: true,
|
|
134
|
+
});
|
|
85
135
|
episodeState.lastResult = result;
|
|
136
|
+
emitExternalRenderFrame(host, result, { source: 'step', actions });
|
|
86
137
|
host.afterStep(result);
|
|
87
138
|
return result;
|
|
88
139
|
}
|
|
@@ -92,8 +143,50 @@ export function createEnvironmentRuntime(host) {
|
|
|
92
143
|
buildResult({ host, episodeState, events: [], actionErrors: [] }).observation;
|
|
93
144
|
}
|
|
94
145
|
|
|
95
|
-
function getState() {
|
|
96
|
-
|
|
146
|
+
function getState({ output = 'full' } = {}) {
|
|
147
|
+
const sim = host.getSimulation();
|
|
148
|
+
if (output === 'none') return null;
|
|
149
|
+
if (output === 'minimal') return { snapshot: sim.snapshotObservation?.() ?? sim.snapshot() };
|
|
150
|
+
return { snapshot: sim.snapshot() };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function resetDrivers(placements = {}, resultOptions = {}) {
|
|
154
|
+
const options = host.getOptions();
|
|
155
|
+
const sim = host.getSimulation();
|
|
156
|
+
const driverIds = new Set(options.controlledDrivers);
|
|
157
|
+
const normalizedPlacements = normalizeEnvironmentPlacements(placements, driverIds);
|
|
158
|
+
const snapshot = sim.snapshotObservation?.() ?? sim.snapshot();
|
|
159
|
+
const carsById = new Map(snapshot.cars.map((car) => [car.id, car]));
|
|
160
|
+
applyEnvironmentPlacements(sim, snapshot.track, carsById, normalizedPlacements);
|
|
161
|
+
Object.keys(normalizedPlacements).forEach((driverId) => {
|
|
162
|
+
sim.clearCarControls?.(driverId);
|
|
163
|
+
sim.setAutomaticPitIntentEnabled?.(driverId, false);
|
|
164
|
+
const car = sim.cars?.find?.((item) => item.id === driverId);
|
|
165
|
+
resetEnvironmentPitStop(car);
|
|
166
|
+
sim.setPitIntent?.(driverId, 0);
|
|
167
|
+
if (car) sim.applyRunoffResponse?.(car);
|
|
168
|
+
});
|
|
169
|
+
resetDriverEpisodes(episodeState, Object.keys(normalizedPlacements));
|
|
170
|
+
episodeState.previousSnapshot = null;
|
|
171
|
+
const resetDriverIds = Object.keys(normalizedPlacements);
|
|
172
|
+
const observationScope = resultOptions.observationScope ??
|
|
173
|
+
resultOptions.resetDriversObservationScope ??
|
|
174
|
+
options.result.resetDriversObservationScope;
|
|
175
|
+
const result = buildResult({
|
|
176
|
+
host,
|
|
177
|
+
episodeState,
|
|
178
|
+
events: [],
|
|
179
|
+
actionErrors: [],
|
|
180
|
+
controlledDrivers: observationScope === 'reset' ? resetDriverIds : options.controlledDrivers,
|
|
181
|
+
stateOutput: resultOptions.stateOutput,
|
|
182
|
+
});
|
|
183
|
+
episodeState.lastResult = result;
|
|
184
|
+
emitExternalRenderFrame(host, result, {
|
|
185
|
+
source: 'resetDrivers',
|
|
186
|
+
resetDriverIds,
|
|
187
|
+
observationScope,
|
|
188
|
+
});
|
|
189
|
+
return result;
|
|
97
190
|
}
|
|
98
191
|
|
|
99
192
|
function getActionSpec() {
|
|
@@ -107,9 +200,42 @@ export function createEnvironmentRuntime(host) {
|
|
|
107
200
|
function destroy() {
|
|
108
201
|
episodeState.lastResult = null;
|
|
109
202
|
episodeState.previousSnapshot = null;
|
|
203
|
+
episodeState.lastObservationSnapshot = null;
|
|
204
|
+
episodeState.lastRewardSnapshot = null;
|
|
205
|
+
episodeState.drivers?.clear?.();
|
|
110
206
|
}
|
|
111
207
|
|
|
112
|
-
return { reset, step, getObservation, getState, getActionSpec, getObservationSpec, destroy };
|
|
208
|
+
return { reset, step, resetDrivers, getObservation, getState, getActionSpec, getObservationSpec, destroy };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function mergeEnvironmentResetOptions(currentOptions, nextOptions = {}) {
|
|
212
|
+
return mergePlainObjects(currentOptions, nextOptions);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const REPLACE_OBJECT_PATHS = new Set([
|
|
216
|
+
'scenario.placements',
|
|
217
|
+
]);
|
|
218
|
+
|
|
219
|
+
function mergePlainObjects(base, overrides, path = []) {
|
|
220
|
+
if (!isPlainObject(base) || !isPlainObject(overrides)) return overrides;
|
|
221
|
+
const merged = { ...base };
|
|
222
|
+
Object.entries(overrides).forEach(([key, value]) => {
|
|
223
|
+
if (value === undefined) return;
|
|
224
|
+
const nextPath = [...path, key];
|
|
225
|
+
merged[key] = isPlainObject(value) &&
|
|
226
|
+
isPlainObject(base[key]) &&
|
|
227
|
+
!REPLACE_OBJECT_PATHS.has(nextPath.join('.'))
|
|
228
|
+
? mergePlainObjects(base[key], value, nextPath)
|
|
229
|
+
: value;
|
|
230
|
+
});
|
|
231
|
+
return merged;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function isPlainObject(value) {
|
|
235
|
+
return value != null &&
|
|
236
|
+
typeof value === 'object' &&
|
|
237
|
+
!Array.isArray(value) &&
|
|
238
|
+
(Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null);
|
|
113
239
|
}
|
|
114
240
|
|
|
115
241
|
function isNoopPitIntent(pitIntent) {
|
|
@@ -122,54 +248,177 @@ function isNoopPitIntent(pitIntent) {
|
|
|
122
248
|
function initializeControlledPitIntent(host) {
|
|
123
249
|
const sim = host.getSimulation?.();
|
|
124
250
|
const options = host.getOptions?.();
|
|
251
|
+
initializeControlledPitIntentForSimulation(sim, options);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function initializeControlledPitIntentForSimulation(sim, options) {
|
|
125
255
|
options?.controlledDrivers?.forEach?.((driverId) => {
|
|
126
256
|
sim?.setAutomaticPitIntentEnabled?.(driverId, false);
|
|
127
257
|
sim?.setPitIntent?.(driverId, 0);
|
|
258
|
+
const car = sim?.cars?.find?.((item) => item.id === driverId);
|
|
259
|
+
if (car) car.environmentControlled = true;
|
|
128
260
|
});
|
|
129
261
|
}
|
|
130
262
|
|
|
131
|
-
function
|
|
263
|
+
function resetEnvironmentPitStop(car) {
|
|
264
|
+
const stop = car?.pitStop;
|
|
265
|
+
if (!stop) return;
|
|
266
|
+
stop.status = 'pending';
|
|
267
|
+
stop.phase = null;
|
|
268
|
+
stop.intent = 0;
|
|
269
|
+
stop.queueingForService = false;
|
|
270
|
+
stop.route = null;
|
|
271
|
+
stop.routeProgress = 0;
|
|
272
|
+
stop.routeStartRaceDistance = null;
|
|
273
|
+
stop.routeEndRaceDistance = null;
|
|
274
|
+
stop.serviceRemaining = 0;
|
|
275
|
+
stop.penaltyServiceRemaining = 0;
|
|
276
|
+
stop.penaltyServiceTotal = 0;
|
|
277
|
+
stop.servingPenaltyIds = [];
|
|
278
|
+
stop.serviceProfile = null;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function buildResult({
|
|
282
|
+
host,
|
|
283
|
+
episodeState,
|
|
284
|
+
events,
|
|
285
|
+
actionErrors,
|
|
286
|
+
actions = {},
|
|
287
|
+
controlledDrivers = null,
|
|
288
|
+
stateOutput = null,
|
|
289
|
+
rewardEnabled = false,
|
|
290
|
+
}) {
|
|
132
291
|
const options = host.getOptions();
|
|
133
|
-
const
|
|
292
|
+
const sim = host.getSimulation();
|
|
293
|
+
const resultDrivers = controlledDrivers ?? options.controlledDrivers;
|
|
294
|
+
const resolvedStateOutput = stateOutput ?? options.result.stateOutput;
|
|
295
|
+
const observationSnapshot = snapshotForResult(sim, options, resolvedStateOutput);
|
|
296
|
+
markDestroyedDriverEpisodes(episodeState, options.controlledDrivers, observationSnapshot);
|
|
134
297
|
const observation = buildEnvironmentObservation({
|
|
135
|
-
snapshot,
|
|
298
|
+
snapshot: observationSnapshot,
|
|
136
299
|
previousSnapshot: episodeState.previousSnapshot,
|
|
137
300
|
options,
|
|
138
301
|
events,
|
|
302
|
+
controlledDrivers: resultDrivers,
|
|
139
303
|
});
|
|
140
|
-
const episode = evaluateEpisode(
|
|
141
|
-
const
|
|
304
|
+
const episode = evaluateEpisode(observationSnapshot, options, episodeState, resultDrivers);
|
|
305
|
+
const metrics = buildDriverMetrics({
|
|
306
|
+
snapshot: observationSnapshot,
|
|
307
|
+
previousSnapshot: episodeState.previousSnapshot,
|
|
308
|
+
options: { ...options, controlledDrivers: resultDrivers },
|
|
309
|
+
events,
|
|
310
|
+
});
|
|
311
|
+
const rewardEpisodeInfo = buildDriverEpisodeInfo(episodeState, {
|
|
312
|
+
...options,
|
|
313
|
+
controlledDrivers: resultDrivers,
|
|
314
|
+
}, episode);
|
|
315
|
+
const driverEpisode = resultDrivers === options.controlledDrivers
|
|
316
|
+
? episode
|
|
317
|
+
: evaluateEpisode(observationSnapshot, options, episodeState, options.controlledDrivers);
|
|
318
|
+
const driverEpisodeInfo = buildDriverEpisodeInfo(episodeState, options, driverEpisode);
|
|
319
|
+
const { state, rewardSnapshot } = buildResultState(sim, observationSnapshot, resolvedStateOutput);
|
|
320
|
+
const reward = rewardEnabled
|
|
321
|
+
? computeReward({
|
|
322
|
+
options: { ...options, controlledDrivers: resultDrivers },
|
|
323
|
+
observation,
|
|
324
|
+
events,
|
|
325
|
+
snapshot: rewardSnapshot,
|
|
326
|
+
actions,
|
|
327
|
+
previousSnapshot: episodeState.previousSnapshot,
|
|
328
|
+
metrics,
|
|
329
|
+
driverEpisodeInfo: rewardEpisodeInfo,
|
|
330
|
+
})
|
|
331
|
+
: null;
|
|
332
|
+
episodeState.lastObservationSnapshot = observationSnapshot;
|
|
333
|
+
episodeState.lastRewardSnapshot = rewardSnapshot;
|
|
142
334
|
return {
|
|
143
335
|
observation,
|
|
144
336
|
reward,
|
|
337
|
+
metrics,
|
|
145
338
|
terminated: episode.terminated,
|
|
146
339
|
truncated: episode.truncated,
|
|
147
340
|
done: episode.terminated || episode.truncated,
|
|
148
341
|
events,
|
|
149
|
-
state
|
|
342
|
+
state,
|
|
150
343
|
info: {
|
|
151
344
|
step: episodeState.step,
|
|
152
|
-
elapsedSeconds:
|
|
345
|
+
elapsedSeconds: observationSnapshot.time,
|
|
153
346
|
seed: options.seed,
|
|
154
347
|
trackSeed: options.trackSeed,
|
|
155
|
-
controlledDrivers: [...
|
|
348
|
+
controlledDrivers: [...resultDrivers],
|
|
156
349
|
actionErrors,
|
|
157
350
|
endReason: episode.endReason,
|
|
351
|
+
drivers: driverEpisodeInfo,
|
|
158
352
|
},
|
|
159
353
|
};
|
|
160
354
|
}
|
|
161
355
|
|
|
162
|
-
function
|
|
356
|
+
function applyControlledRunoffResponse(sim, options) {
|
|
357
|
+
options?.controlledDrivers?.forEach?.((driverId) => {
|
|
358
|
+
const car = sim?.cars?.find?.((item) => item.id === driverId);
|
|
359
|
+
if (car) sim?.applyRunoffResponse?.(car);
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function buildResultState(sim, observationSnapshot, stateOutput) {
|
|
364
|
+
if (stateOutput === 'none') return { state: null, rewardSnapshot: observationSnapshot };
|
|
365
|
+
if (stateOutput === 'minimal') return { state: { snapshot: observationSnapshot }, rewardSnapshot: observationSnapshot };
|
|
366
|
+
const snapshot = sim.snapshot();
|
|
367
|
+
return { state: { snapshot }, rewardSnapshot: snapshot };
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function snapshotForResult(sim, options, stateOutput) {
|
|
371
|
+
if (canUseTrainingSnapshot(options, stateOutput)) {
|
|
372
|
+
return sim.snapshotTraining?.() ?? sim.snapshotObservation?.() ?? sim.snapshot();
|
|
373
|
+
}
|
|
374
|
+
return sim.snapshotObservation?.() ?? sim.snapshot();
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function canUseTrainingSnapshot(options, stateOutput) {
|
|
378
|
+
return stateOutput === 'none' &&
|
|
379
|
+
options.observation?.output === 'vector' &&
|
|
380
|
+
options.observation?.includeSchema === false;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function computeReward({ options, observation, events, snapshot, actions, previousSnapshot, metrics, driverEpisodeInfo }) {
|
|
163
384
|
if (!options.reward) return null;
|
|
164
385
|
return Object.fromEntries(options.controlledDrivers.map((driverId) => [
|
|
165
386
|
driverId,
|
|
166
|
-
|
|
387
|
+
normalizeRewardValue(options.reward({
|
|
167
388
|
driverId,
|
|
168
389
|
previous: previousSnapshot,
|
|
169
390
|
current: observation[driverId],
|
|
170
391
|
action: actions?.[driverId],
|
|
171
392
|
events: observation[driverId]?.events ?? events,
|
|
172
393
|
state: { snapshot },
|
|
173
|
-
|
|
394
|
+
metrics: metrics?.[driverId] ?? null,
|
|
395
|
+
episode: driverEpisodeInfo?.[driverId] ?? null,
|
|
396
|
+
})),
|
|
174
397
|
]));
|
|
175
398
|
}
|
|
399
|
+
|
|
400
|
+
function emitExternalRenderFrame(host, result, meta = {}) {
|
|
401
|
+
const options = host.getOptions?.();
|
|
402
|
+
const onFrame = options?.externalRenderer?.onFrame;
|
|
403
|
+
if (typeof onFrame !== 'function') return;
|
|
404
|
+
try {
|
|
405
|
+
onFrame({
|
|
406
|
+
snapshot: result?.state?.snapshot ??
|
|
407
|
+
host.getSimulation?.()?.snapshotObservation?.() ??
|
|
408
|
+
host.getSimulation?.()?.snapshot?.(),
|
|
409
|
+
observation: result?.observation ?? {},
|
|
410
|
+
meta: {
|
|
411
|
+
...meta,
|
|
412
|
+
step: result?.info?.step ?? null,
|
|
413
|
+
elapsedSeconds: result?.info?.elapsedSeconds ?? null,
|
|
414
|
+
},
|
|
415
|
+
});
|
|
416
|
+
} catch {
|
|
417
|
+
// External rendering hooks are observer-only and must not break stepping.
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function normalizeRewardValue(value) {
|
|
422
|
+
const reward = Number(value ?? 0);
|
|
423
|
+
return Number.isFinite(reward) ? reward : 0;
|
|
424
|
+
}
|
|
@@ -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,13 +177,32 @@ 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
|
+
physicsMode: sim.physicsMode,
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
if (typeof sim.setCarStates === 'function') {
|
|
190
|
+
sim.setCarStates(states);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
Object.entries(states).forEach(([driverId, partial]) => {
|
|
194
|
+
sim.setCarState(driverId, partial);
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function placementToCarState(track, currentCar, placement, { physicsMode = 'arcade' } = {}) {
|
|
178
199
|
const currentDistanceMeters = currentCar.distanceMeters ?? simUnitsToMeters(currentCar.raceDistance ?? currentCar.progress ?? 0);
|
|
179
200
|
const raceDistance = metersToSimUnits(Math.max(0, placement.distanceMeters ?? currentDistanceMeters));
|
|
180
201
|
const offset = metersToSimUnits(placement.offsetMeters ?? 0);
|
|
181
202
|
const base = pointAt(track, raceDistance);
|
|
182
203
|
const position = offsetTrackPoint(base, offset);
|
|
183
204
|
const heading = normalizeAngle(base.heading + (placement.headingErrorRadians ?? 0));
|
|
205
|
+
const speed = placement.speedKph == null ? null : kphToSimSpeed(placement.speedKph);
|
|
184
206
|
const partial = {
|
|
185
207
|
x: position.x,
|
|
186
208
|
y: position.y,
|
|
@@ -191,9 +213,35 @@ function applyPlacement(sim, track, currentCar, driverId, placement) {
|
|
|
191
213
|
desiredOffset: offset,
|
|
192
214
|
heading,
|
|
193
215
|
previousHeading: heading,
|
|
216
|
+
destroyed: false,
|
|
217
|
+
destroyReason: null,
|
|
218
|
+
destroyedAt: null,
|
|
219
|
+
outOfRace: false,
|
|
220
|
+
canAttack: true,
|
|
221
|
+
throttle: 0,
|
|
222
|
+
brake: 0,
|
|
223
|
+
longitudinalAcceleration: 0,
|
|
224
|
+
lateralAcceleration: 0,
|
|
225
|
+
lateralG: 0,
|
|
226
|
+
longitudinalG: 0,
|
|
227
|
+
gripUsage: 0,
|
|
228
|
+
slipAngleRadians: 0,
|
|
229
|
+
tractionLimited: false,
|
|
230
|
+
steeringAngle: 0,
|
|
231
|
+
yawRate: 0,
|
|
232
|
+
stabilityState: 'stable',
|
|
194
233
|
};
|
|
195
|
-
if (
|
|
196
|
-
|
|
234
|
+
if (speed != null) {
|
|
235
|
+
partial.speed = speed;
|
|
236
|
+
if (physicsMode === 'simulator') {
|
|
237
|
+
partial.velocityX = Math.cos(heading) * speed;
|
|
238
|
+
partial.velocityY = Math.sin(heading) * speed;
|
|
239
|
+
} else {
|
|
240
|
+
partial.velocityX = null;
|
|
241
|
+
partial.velocityY = null;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return partial;
|
|
197
245
|
}
|
|
198
246
|
|
|
199
247
|
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
|
+
}
|