@inventure71/paddockjs 2.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 +6 -0
- package/INSTALL_AND_UPDATE.md +1 -1
- package/README.md +30 -5
- package/docs/architecture.md +15 -9
- package/docs/concepts.md +6 -4
- package/docs/data_contract.md +28 -15
- package/docs/data_model.md +1 -1
- package/docs/index.md +9 -0
- package/docs/rules.md +37 -7
- package/docs/sense_contract.md +59 -0
- package/docs/system_specs.md +56 -7
- package/docs/training.md +16 -10
- package/package.json +1 -1
- package/src/api/PaddockSimulatorController.js +2 -33
- package/src/app/BrowserExpertAdapter.js +9 -300
- package/src/app/BrowserExpertExternalRenderer.js +303 -0
- package/src/app/F1SimulatorApp.js +1 -42
- package/src/config/restartOptions.js +34 -0
- package/src/environment/controllerLoop.js +17 -2
- package/src/environment/episode.js +14 -2
- package/src/environment/index.d.ts +16 -2
- package/src/environment/observationVector.js +200 -0
- package/src/environment/observations.js +58 -285
- package/src/environment/recorder.js +13 -6
- package/src/environment/rewards.js +13 -2
- package/src/environment/runtime.js +62 -14
- package/src/environment/scenarios.js +22 -3
- package/src/environment/sensors/index.js +3 -3
- package/src/environment/sensors/opponentRadar.js +13 -3
- package/src/environment/sensors/sensorTargets.js +8 -2
- package/src/environment/sensors/surfaceRays.js +90 -26
- package/src/environment/sensors/trackRays.js +185 -22
- package/src/environment/specs.js +78 -40
- package/src/index.d.ts +2 -0
- package/src/index.js +1 -32
- package/src/simulation/pit/pitFlow.js +5 -0
- package/src/simulation/pit/pitKinematics.js +24 -0
- package/src/simulation/pit/pitQueue.js +5 -0
- package/src/simulation/pit/pitRouteMovement.js +7 -0
- package/src/simulation/pit/pitService.js +2 -0
- package/src/simulation/pit/pitTireService.js +2 -0
- package/src/simulation/race/raceLifecycle.js +2 -6
- package/src/simulation/race/raceProgress.js +6 -1
- package/src/simulation/race/raceStep.js +1 -1
- package/src/simulation/race/redFlag.js +3 -3
- package/src/simulation/race/safetyCar.js +10 -4
- package/src/simulation/race/stalledDnf.js +8 -17
- package/src/simulation/raceSimulation.js +1 -0
- package/src/simulation/rules/rulesReview.js +3 -0
- package/src/simulation/snapshots/raceSnapshots.js +1 -0
- package/src/simulation/track/trackQueryGrid.js +94 -0
- package/src/simulation/track/trackQueryIndex.js +16 -302
- 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/vehicle/contactResolution.js +67 -2
- package/src/simulation/vehicle/runoffResponse.js +11 -12
- package/src/simulation/vehicle/vehicleKinematics.js +37 -0
- package/src/simulation/vehicle/vehiclePhysics.js +2 -1
- package/src/simulation/vehicle/vehicleSnapshots.js +11 -0
- package/src/simulation/vehicle/vehicleState.js +36 -2
- package/src/simulation/vehicle/wheelSurface.js +3 -1
|
@@ -16,17 +16,12 @@ import { PitLaneStatusRenderer } from './rendering/pitLaneStatusRenderer.js';
|
|
|
16
16
|
import { ReplayGhostRenderer } from './rendering/replayGhostRenderer.js';
|
|
17
17
|
import { renderTrackSurface } from './rendering/trackRenderer.js';
|
|
18
18
|
import {
|
|
19
|
-
formatLapGap,
|
|
20
|
-
formatRaceGap,
|
|
21
|
-
getPenaltyByDriver,
|
|
22
19
|
getTimingOrderKey,
|
|
23
20
|
getTimingPenaltyKey,
|
|
24
|
-
isWavedFlagCar,
|
|
25
21
|
renderTimingTower,
|
|
26
22
|
syncTimingGapModeControls,
|
|
27
23
|
} from './readouts/timingTowerRenderer.js';
|
|
28
|
-
import {
|
|
29
|
-
import { getOverviewFields, renderCarDriverOverview } from './readouts/carOverviewRenderer.js';
|
|
24
|
+
import { renderCarDriverOverview } from './readouts/carOverviewRenderer.js';
|
|
30
25
|
import { renderLapTelemetry, renderTelemetryReadouts } from './readouts/telemetryRenderer.js';
|
|
31
26
|
import {
|
|
32
27
|
renderRaceFinish,
|
|
@@ -327,18 +322,6 @@ export class F1SimulatorApp {
|
|
|
327
322
|
});
|
|
328
323
|
}
|
|
329
324
|
|
|
330
|
-
createRawCarGeometryGraphic(driver) {
|
|
331
|
-
return this.carRenderer.createRawCarGeometryGraphic(driver);
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
createServiceCountdownLabel() {
|
|
335
|
-
return this.carRenderer.createServiceCountdownLabel();
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
applyServiceCountdownTone(label, tone) {
|
|
339
|
-
this.carRenderer.applyServiceCountdownTone(label, tone);
|
|
340
|
-
}
|
|
341
|
-
|
|
342
325
|
bindControls() {
|
|
343
326
|
const eventOptions = { signal: this.abortController.signal };
|
|
344
327
|
|
|
@@ -838,32 +821,12 @@ export class F1SimulatorApp {
|
|
|
838
821
|
return getTimingOrderKey(cars, { timingGapMode: this.timingGapMode });
|
|
839
822
|
}
|
|
840
823
|
|
|
841
|
-
isWavedFlagCar(car) {
|
|
842
|
-
return isWavedFlagCar(car);
|
|
843
|
-
}
|
|
844
|
-
|
|
845
|
-
getPenaltyByDriver(penalties = []) {
|
|
846
|
-
return getPenaltyByDriver(penalties);
|
|
847
|
-
}
|
|
848
|
-
|
|
849
824
|
getTimingPenaltyKey(penalties = []) {
|
|
850
825
|
return getTimingPenaltyKey(penalties, {
|
|
851
826
|
timingPenaltyBadgesEnabled: this.timingPenaltyBadgesEnabled,
|
|
852
827
|
});
|
|
853
828
|
}
|
|
854
829
|
|
|
855
|
-
formatTimingGap(car) {
|
|
856
|
-
return formatRaceGap(car, this.timingGapMode);
|
|
857
|
-
}
|
|
858
|
-
|
|
859
|
-
formatLapGap(laps) {
|
|
860
|
-
return formatLapGap(laps);
|
|
861
|
-
}
|
|
862
|
-
|
|
863
|
-
formatTelemetryGap(car, mode) {
|
|
864
|
-
return formatTelemetryGap(car, mode);
|
|
865
|
-
}
|
|
866
|
-
|
|
867
830
|
syncTimingGapModeControls() {
|
|
868
831
|
syncTimingGapModeControls({
|
|
869
832
|
readouts: this.readouts,
|
|
@@ -886,10 +849,6 @@ export class F1SimulatorApp {
|
|
|
886
849
|
renderLapTelemetry(this.readouts, telemetry);
|
|
887
850
|
}
|
|
888
851
|
|
|
889
|
-
getOverviewFields(driver, mode) {
|
|
890
|
-
return getOverviewFields(driver, mode);
|
|
891
|
-
}
|
|
892
|
-
|
|
893
852
|
renderCarDriverOverview(car) {
|
|
894
853
|
const previousKey = this.lastOverviewRenderKey;
|
|
895
854
|
this.lastOverviewRenderKey = renderCarDriverOverview({
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export function mergeRestartOptions(previousOptions, nextOptions = {}) {
|
|
2
|
+
const resetFromPreset = Object.hasOwn(nextOptions, 'preset');
|
|
3
|
+
const previousUi = resetFromPreset ? {} : (previousOptions.ui ?? {});
|
|
4
|
+
const previousTheme = resetFromPreset ? {} : (previousOptions.theme ?? {});
|
|
5
|
+
const previousAssets = previousOptions.assets ?? {};
|
|
6
|
+
const nextAssets = nextOptions.assets ?? {};
|
|
7
|
+
|
|
8
|
+
return {
|
|
9
|
+
...previousOptions,
|
|
10
|
+
...nextOptions,
|
|
11
|
+
ui: {
|
|
12
|
+
...previousUi,
|
|
13
|
+
...(nextOptions.ui ?? {}),
|
|
14
|
+
raceDataBanners: {
|
|
15
|
+
...(previousUi.raceDataBanners ?? {}),
|
|
16
|
+
...(nextOptions.ui?.raceDataBanners ?? {}),
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
theme: {
|
|
20
|
+
...previousTheme,
|
|
21
|
+
...(nextOptions.theme ?? {}),
|
|
22
|
+
},
|
|
23
|
+
assets: {
|
|
24
|
+
...previousAssets,
|
|
25
|
+
...nextAssets,
|
|
26
|
+
trackTextures: {
|
|
27
|
+
...(previousAssets.trackTextures ?? {}),
|
|
28
|
+
...(nextAssets.trackTextures ?? {}),
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
drivers: nextOptions.drivers ?? previousOptions.drivers,
|
|
32
|
+
entries: nextOptions.entries ?? previousOptions.entries,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
@@ -22,6 +22,7 @@ export function createPaddockDriverControllerLoop({
|
|
|
22
22
|
let heldFramesRemaining = 0;
|
|
23
23
|
let policyStep = 0;
|
|
24
24
|
let runtimeStep = 0;
|
|
25
|
+
let observationCache = {};
|
|
25
26
|
let running = false;
|
|
26
27
|
let scheduled = null;
|
|
27
28
|
let previousActions = {};
|
|
@@ -33,6 +34,11 @@ export function createPaddockDriverControllerLoop({
|
|
|
33
34
|
if (!observationSpec) observationSpec = runtime.getObservationSpec();
|
|
34
35
|
}
|
|
35
36
|
|
|
37
|
+
async function refreshSpecs() {
|
|
38
|
+
actionSpec = runtime.getActionSpec();
|
|
39
|
+
observationSpec = runtime.getObservationSpec();
|
|
40
|
+
}
|
|
41
|
+
|
|
36
42
|
async function ensureInitialized(context) {
|
|
37
43
|
if (initialized) return;
|
|
38
44
|
await controller.init?.(context);
|
|
@@ -47,9 +53,10 @@ export function createPaddockDriverControllerLoop({
|
|
|
47
53
|
heldFramesRemaining = 0;
|
|
48
54
|
policyStep = 0;
|
|
49
55
|
runtimeStep = Number(result?.info?.step ?? 0);
|
|
56
|
+
observationCache = { ...(result?.observation ?? {}) };
|
|
50
57
|
previousActions = {};
|
|
51
58
|
lastError = null;
|
|
52
|
-
await
|
|
59
|
+
await refreshSpecs();
|
|
53
60
|
const context = buildContext({ resetDriverIds: controlledDrivers() });
|
|
54
61
|
await ensureInitialized(context);
|
|
55
62
|
await controller.reset?.(context);
|
|
@@ -71,6 +78,10 @@ export function createPaddockDriverControllerLoop({
|
|
|
71
78
|
const resetDriverIds = Object.keys(placements);
|
|
72
79
|
previousResult = result;
|
|
73
80
|
result = runtime.resetDrivers(placements, resultOptions);
|
|
81
|
+
observationCache = {
|
|
82
|
+
...observationCache,
|
|
83
|
+
...(result?.observation ?? {}),
|
|
84
|
+
};
|
|
74
85
|
heldActions = null;
|
|
75
86
|
heldFramesRemaining = 0;
|
|
76
87
|
lastError = null;
|
|
@@ -106,6 +117,7 @@ export function createPaddockDriverControllerLoop({
|
|
|
106
117
|
const previousActionsForStep = previousActions;
|
|
107
118
|
previousResult = result;
|
|
108
119
|
result = runtime.step(actions);
|
|
120
|
+
observationCache = { ...(result?.observation ?? {}) };
|
|
109
121
|
heldFramesRemaining -= 1;
|
|
110
122
|
runtimeStep = Number(result?.info?.step ?? runtimeStep + 1);
|
|
111
123
|
previousActions = actions;
|
|
@@ -167,7 +179,10 @@ export function createPaddockDriverControllerLoop({
|
|
|
167
179
|
|
|
168
180
|
function buildContext(extra = {}) {
|
|
169
181
|
const drivers = controlledDrivers();
|
|
170
|
-
const observation =
|
|
182
|
+
const observation = {
|
|
183
|
+
...observationCache,
|
|
184
|
+
...(result?.observation ?? runtime.getObservation?.() ?? {}),
|
|
185
|
+
};
|
|
171
186
|
return {
|
|
172
187
|
runtime,
|
|
173
188
|
mode,
|
|
@@ -45,8 +45,9 @@ export function buildDriverEpisodeInfo(episodeState, options, episode) {
|
|
|
45
45
|
return Object.fromEntries(options.controlledDrivers.map((driverId) => {
|
|
46
46
|
const state = ensureDriverEpisodeState(episodeState, driverId);
|
|
47
47
|
const maxStepTruncated = state.episodeStep >= options.episode.maxSteps;
|
|
48
|
+
const aggregateTruncated = episode.truncated && episode.endReason !== 'all-drivers-ended';
|
|
48
49
|
const terminated = Boolean(state.terminated || episode.terminated);
|
|
49
|
-
const truncated = Boolean(state.truncated || maxStepTruncated ||
|
|
50
|
+
const truncated = Boolean(state.truncated || maxStepTruncated || aggregateTruncated);
|
|
50
51
|
const endReason = state.endReason ??
|
|
51
52
|
(terminated ? episode.endReason : null) ??
|
|
52
53
|
(maxStepTruncated ? 'max-steps' : null) ??
|
|
@@ -67,13 +68,21 @@ export function evaluateEpisode(snapshot, options, episodeState, controlledDrive
|
|
|
67
68
|
}
|
|
68
69
|
let allTerminated = controlledDrivers.length > 0;
|
|
69
70
|
let allTruncated = controlledDrivers.length > 0;
|
|
71
|
+
let allEnded = controlledDrivers.length > 0;
|
|
72
|
+
let anyTruncated = false;
|
|
70
73
|
let firstTerminatedReason = null;
|
|
71
74
|
for (const driverId of controlledDrivers) {
|
|
72
75
|
const state = ensureDriverEpisodeState(episodeState, driverId);
|
|
76
|
+
if (!state.terminated && state.episodeStep >= options.episode.maxSteps) {
|
|
77
|
+
state.truncated = true;
|
|
78
|
+
state.endReason ??= 'max-steps';
|
|
79
|
+
}
|
|
73
80
|
if (!state.terminated) allTerminated = false;
|
|
74
81
|
else firstTerminatedReason ??= state.endReason;
|
|
75
82
|
if (!state.truncated && state.episodeStep < options.episode.maxSteps) allTruncated = false;
|
|
76
|
-
if (
|
|
83
|
+
if (state.truncated || state.episodeStep >= options.episode.maxSteps) anyTruncated = true;
|
|
84
|
+
if (!state.terminated && !state.truncated && state.episodeStep < options.episode.maxSteps) allEnded = false;
|
|
85
|
+
if (!allTerminated && !allTruncated && !allEnded) break;
|
|
77
86
|
}
|
|
78
87
|
if (allTerminated) {
|
|
79
88
|
return { terminated: true, truncated: false, endReason: controlledDrivers.length === 1 ? firstTerminatedReason : 'all-drivers-terminated' };
|
|
@@ -81,6 +90,9 @@ export function evaluateEpisode(snapshot, options, episodeState, controlledDrive
|
|
|
81
90
|
if (allTruncated) {
|
|
82
91
|
return { terminated: false, truncated: true, endReason: 'max-steps' };
|
|
83
92
|
}
|
|
93
|
+
if (allEnded) {
|
|
94
|
+
return { terminated: false, truncated: anyTruncated, endReason: 'all-drivers-ended' };
|
|
95
|
+
}
|
|
84
96
|
return { terminated: false, truncated: false, endReason: null };
|
|
85
97
|
}
|
|
86
98
|
|
|
@@ -364,6 +364,13 @@ export interface PaddockTrackLookaheadObservation {
|
|
|
364
364
|
headingDeltaRadians: number;
|
|
365
365
|
}
|
|
366
366
|
|
|
367
|
+
export interface PaddockAppliedControlsObservation {
|
|
368
|
+
steering: number;
|
|
369
|
+
steeringRadians: number;
|
|
370
|
+
throttle: number;
|
|
371
|
+
brake: number;
|
|
372
|
+
}
|
|
373
|
+
|
|
367
374
|
export interface PaddockDriverObservationObject {
|
|
368
375
|
profile: string;
|
|
369
376
|
self: {
|
|
@@ -375,6 +382,7 @@ export interface PaddockDriverObservationObject {
|
|
|
375
382
|
steeringAngleRadians: number;
|
|
376
383
|
throttle: number;
|
|
377
384
|
brake: number;
|
|
385
|
+
appliedControls: PaddockAppliedControlsObservation | null;
|
|
378
386
|
lap: number;
|
|
379
387
|
completedLaps: number;
|
|
380
388
|
lapProgressMeters: number;
|
|
@@ -413,7 +421,7 @@ export interface PaddockDriverObservationObject {
|
|
|
413
421
|
};
|
|
414
422
|
contactPatches: PaddockContactPatchObservation[];
|
|
415
423
|
race: {
|
|
416
|
-
position: number;
|
|
424
|
+
position: number | null;
|
|
417
425
|
totalCars: number;
|
|
418
426
|
raceMode: string;
|
|
419
427
|
pitLaneOpen: boolean;
|
|
@@ -590,12 +598,18 @@ export interface PaddockActionSpec {
|
|
|
590
598
|
}
|
|
591
599
|
|
|
592
600
|
export interface PaddockObservationSpec {
|
|
593
|
-
version:
|
|
601
|
+
version: 3 | 6;
|
|
594
602
|
controlledDrivers: string[];
|
|
595
603
|
object: Record<string, unknown>;
|
|
596
604
|
vector: {
|
|
597
605
|
schema: PaddockObservationSchemaEntry[];
|
|
598
606
|
};
|
|
607
|
+
perDriver: Record<string, {
|
|
608
|
+
object: Record<string, unknown>;
|
|
609
|
+
vector: {
|
|
610
|
+
schema: PaddockObservationSchemaEntry[];
|
|
611
|
+
};
|
|
612
|
+
}>;
|
|
599
613
|
}
|
|
600
614
|
|
|
601
615
|
export interface PaddockRolloutTransition {
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
const MODEL_RAY_SURFACE_CHANNELS = Object.freeze(['kerb', 'illegalSurface']);
|
|
2
|
+
|
|
3
|
+
export function buildObservationVector(source, sensors, { includeSchema = true, vectorType = 'array' } = {}) {
|
|
4
|
+
const includePhysicalDriverSenses = source.profile === 'physical-driver';
|
|
5
|
+
const schema = includeSchema ? [
|
|
6
|
+
{ name: 'self.speedKph', unit: 'kph', scale: 'fixed:400' },
|
|
7
|
+
{ name: 'self.speedMetersPerSecond', unit: 'm/s', scale: 'fixed:120' },
|
|
8
|
+
{ name: 'self.steeringAngleRadians', unit: 'rad', scale: 'fixed:pi' },
|
|
9
|
+
{ name: 'self.throttle', scale: '0..1' },
|
|
10
|
+
{ name: 'self.brake', scale: '0..1' },
|
|
11
|
+
{ name: 'self.lateralG', scale: 'fixed:8g' },
|
|
12
|
+
{ name: 'self.longitudinalG', scale: 'fixed:6g' },
|
|
13
|
+
{ name: 'self.gripUsage', scale: '0..2' },
|
|
14
|
+
{ name: 'self.slipAngleRadians', unit: 'rad', scale: 'fixed:pi' },
|
|
15
|
+
{ name: 'self.tractionLimited', scale: 'boolean' },
|
|
16
|
+
{ name: 'self.lapProgressRatio', scale: '0..1' },
|
|
17
|
+
{ name: 'self.trackOffsetMeters', unit: 'm', scale: 'fixed:meters' },
|
|
18
|
+
{ name: 'self.trackHeadingErrorRadians', unit: 'rad', scale: 'fixed:pi' },
|
|
19
|
+
{ name: 'self.onTrack', scale: 'boolean' },
|
|
20
|
+
{ name: 'self.inPitLane', scale: 'boolean' },
|
|
21
|
+
{ name: 'self.tireEnergy', scale: '0..100' },
|
|
22
|
+
{ name: 'self.pitIntent', scale: '0..2' },
|
|
23
|
+
{ name: 'self.pitStopActive', scale: 'boolean' },
|
|
24
|
+
{ name: 'race.positionNormalized', scale: '0..1' },
|
|
25
|
+
{ name: 'race.raceModeGreen', scale: 'boolean' },
|
|
26
|
+
{ name: 'race.raceModeSafetyCar', scale: 'boolean' },
|
|
27
|
+
{ name: 'race.redFlag', scale: 'boolean' },
|
|
28
|
+
{ name: 'race.pitLaneOpen', scale: 'boolean' },
|
|
29
|
+
{ name: 'track.curvature', scale: 'track-curvature' },
|
|
30
|
+
] : null;
|
|
31
|
+
const vector = [
|
|
32
|
+
source.self.speedKph / 400,
|
|
33
|
+
source.self.speedMetersPerSecond / 120,
|
|
34
|
+
source.self.steeringAngleRadians / Math.PI,
|
|
35
|
+
source.self.throttle,
|
|
36
|
+
source.self.brake,
|
|
37
|
+
source.self.lateralG / 8,
|
|
38
|
+
source.self.longitudinalG / 6,
|
|
39
|
+
source.self.gripUsage / 2,
|
|
40
|
+
source.self.slipAngleRadians / Math.PI,
|
|
41
|
+
source.self.tractionLimited ? 1 : 0,
|
|
42
|
+
normalizeLapProgress(source),
|
|
43
|
+
source.self.trackOffsetMeters,
|
|
44
|
+
source.self.trackHeadingErrorRadians / Math.PI,
|
|
45
|
+
source.self.onTrack ? 1 : 0,
|
|
46
|
+
source.self.inPitLane ? 1 : 0,
|
|
47
|
+
(source.self.tireEnergy ?? 0) / 100,
|
|
48
|
+
(source.self.pitIntent ?? 0) / 2,
|
|
49
|
+
source.self.pitStopStatus && source.self.pitStopStatus !== 'pending' && source.self.pitStopStatus !== 'completed' ? 1 : 0,
|
|
50
|
+
normalizeRacePosition(source),
|
|
51
|
+
source.race.raceMode === 'green' ? 1 : 0,
|
|
52
|
+
source.race.raceMode === 'safety-car' ? 1 : 0,
|
|
53
|
+
source.race.redFlag ? 1 : 0,
|
|
54
|
+
source.race.pitLaneOpen ? 1 : 0,
|
|
55
|
+
source.track.curvature ?? 0,
|
|
56
|
+
];
|
|
57
|
+
if (includePhysicalDriverSenses) {
|
|
58
|
+
pushSchema(schema,
|
|
59
|
+
{ name: 'self.yawRateRadiansPerSecond', unit: 'rad/s', scale: 'fixed:pi' },
|
|
60
|
+
{ name: 'trackRelation.leftBoundaryMeters', unit: 'm', scale: 'fixed:meters' },
|
|
61
|
+
{ name: 'trackRelation.rightBoundaryMeters', unit: 'm', scale: 'fixed:meters' },
|
|
62
|
+
{ name: 'trackRelation.legalWidthMeters', unit: 'm', scale: 'fixed:meters' },
|
|
63
|
+
);
|
|
64
|
+
vector.push(
|
|
65
|
+
source.self.yawRateRadiansPerSecond / Math.PI,
|
|
66
|
+
source.trackRelation.leftBoundaryMeters,
|
|
67
|
+
source.trackRelation.rightBoundaryMeters,
|
|
68
|
+
source.trackRelation.legalWidthMeters,
|
|
69
|
+
);
|
|
70
|
+
source.contactPatches.forEach((patch, index) => {
|
|
71
|
+
pushSchema(schema,
|
|
72
|
+
{ name: `contactPatches[${index}].present`, scale: 'boolean' },
|
|
73
|
+
{ name: `contactPatches[${index}].surfaceCode`, scale: 'surface-code' },
|
|
74
|
+
{ name: `contactPatches[${index}].onLegalSurface`, scale: 'boolean' },
|
|
75
|
+
{ name: `contactPatches[${index}].signedOffsetMeters`, unit: 'm', scale: 'fixed:meters' },
|
|
76
|
+
{ name: `contactPatches[${index}].crossTrackErrorMeters`, unit: 'm', scale: 'fixed:meters' },
|
|
77
|
+
);
|
|
78
|
+
vector.push(
|
|
79
|
+
patch.present ? 1 : 0,
|
|
80
|
+
patch.surfaceCode / 5,
|
|
81
|
+
patch.onLegalSurface ? 1 : 0,
|
|
82
|
+
patch.signedOffsetMeters,
|
|
83
|
+
patch.crossTrackErrorMeters,
|
|
84
|
+
);
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
source.track.lookahead.forEach((sample, index) => {
|
|
88
|
+
pushSchema(schema,
|
|
89
|
+
{ name: `track.lookahead[${index}].curvature`, scale: 'track-curvature' },
|
|
90
|
+
{ name: `track.lookahead[${index}].headingDeltaRadians`, unit: 'rad', scale: 'fixed:pi' },
|
|
91
|
+
);
|
|
92
|
+
vector.push(sample.curvature ?? 0, (sample.headingDeltaRadians ?? 0) / Math.PI);
|
|
93
|
+
});
|
|
94
|
+
source.rays.forEach((ray, index) => {
|
|
95
|
+
pushSchema(schema,
|
|
96
|
+
{ name: `rays[${index}].track.distanceRatio`, scale: '0..1' },
|
|
97
|
+
{ name: `rays[${index}].track.hit`, scale: 'boolean' },
|
|
98
|
+
{ name: `rays[${index}].track.kindExit`, scale: 'boolean' },
|
|
99
|
+
{ name: `rays[${index}].track.kindEntry`, scale: 'boolean' },
|
|
100
|
+
{ name: `rays[${index}].car.distanceRatio`, scale: '0..1' },
|
|
101
|
+
{ name: `rays[${index}].car.hit`, scale: 'boolean' },
|
|
102
|
+
{ name: `rays[${index}].car.relativeSpeedKph`, unit: 'kph', scale: 'fixed:200' },
|
|
103
|
+
{ name: `rays[${index}].car.targetTypeReplayGhost`, scale: 'boolean' },
|
|
104
|
+
);
|
|
105
|
+
vector.push(
|
|
106
|
+
ratio(ray.track.distanceMeters, ray.lengthMeters),
|
|
107
|
+
ray.track.hit ? 1 : 0,
|
|
108
|
+
ray.track.kind === 'exit' ? 1 : 0,
|
|
109
|
+
ray.track.kind === 'entry' ? 1 : 0,
|
|
110
|
+
ratio(ray.car.distanceMeters, ray.lengthMeters),
|
|
111
|
+
ray.car.hit ? 1 : 0,
|
|
112
|
+
ray.car.relativeSpeedKph / 200,
|
|
113
|
+
ray.car.targetType === 'replayGhost' ? 1 : 0,
|
|
114
|
+
);
|
|
115
|
+
MODEL_RAY_SURFACE_CHANNELS.forEach((channel) => {
|
|
116
|
+
if (sensors.rays.channels?.includes?.(channel)) {
|
|
117
|
+
pushSchema(schema,
|
|
118
|
+
{ name: `rays[${index}].${channel}.distanceRatio`, scale: '0..1' },
|
|
119
|
+
{ name: `rays[${index}].${channel}.hit`, scale: 'boolean' },
|
|
120
|
+
);
|
|
121
|
+
vector.push(
|
|
122
|
+
ratio(ray[channel]?.distanceMeters ?? ray.lengthMeters, ray.lengthMeters),
|
|
123
|
+
ray[channel]?.hit ? 1 : 0,
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
const nearbyLimit = sensors.nearbyCars.enabled ? (sensors.nearbyCars.maxCars ?? source.nearbyCars.length) : 0;
|
|
129
|
+
const nearbyRadius = sensors.nearbyCars.radiusMeters ?? 150;
|
|
130
|
+
for (let index = 0; index < nearbyLimit; index += 1) {
|
|
131
|
+
const nearby = source.nearbyCars[index] ?? null;
|
|
132
|
+
pushSchema(schema,
|
|
133
|
+
{ name: `nearbyCars[${index}].present`, scale: 'boolean' },
|
|
134
|
+
{ name: `nearbyCars[${index}].relativeForwardRatio`, scale: '-1..1' },
|
|
135
|
+
{ name: `nearbyCars[${index}].relativeRightRatio`, scale: '-1..1' },
|
|
136
|
+
{ name: `nearbyCars[${index}].relativeDistanceRatio`, scale: '0..1' },
|
|
137
|
+
{ name: `nearbyCars[${index}].relativeSpeedKph`, unit: 'kph', scale: 'fixed:200' },
|
|
138
|
+
{ name: `nearbyCars[${index}].relativeHeadingRadians`, unit: 'rad', scale: 'fixed:pi' },
|
|
139
|
+
{ name: `nearbyCars[${index}].ahead`, scale: 'boolean' },
|
|
140
|
+
{ name: `nearbyCars[${index}].sameLap`, scale: 'boolean' },
|
|
141
|
+
{ name: `nearbyCars[${index}].entityTypeReplayGhost`, scale: 'boolean' },
|
|
142
|
+
);
|
|
143
|
+
vector.push(
|
|
144
|
+
nearby ? 1 : 0,
|
|
145
|
+
clampRatio((nearby?.relativeForwardMeters ?? 0) / nearbyRadius),
|
|
146
|
+
clampRatio((nearby?.relativeRightMeters ?? 0) / nearbyRadius),
|
|
147
|
+
ratio(nearby?.relativeDistanceMeters ?? nearbyRadius, nearbyRadius),
|
|
148
|
+
(nearby?.relativeSpeedKph ?? 0) / 200,
|
|
149
|
+
(nearby?.relativeHeadingRadians ?? 0) / Math.PI,
|
|
150
|
+
nearby?.ahead ? 1 : 0,
|
|
151
|
+
nearby?.sameLap ? 1 : 0,
|
|
152
|
+
nearby?.entityType === 'replayGhost' ? 1 : 0,
|
|
153
|
+
);
|
|
154
|
+
if (includePhysicalDriverSenses) {
|
|
155
|
+
pushSchema(schema,
|
|
156
|
+
{ name: `nearbyCars[${index}].behind`, scale: 'boolean' },
|
|
157
|
+
{ name: `nearbyCars[${index}].closingRateMetersPerSecond`, unit: 'm/s', scale: 'fixed:100' },
|
|
158
|
+
{ name: `nearbyCars[${index}].timeToContactSeconds`, unit: 's', scale: 'fixed:10' },
|
|
159
|
+
{ name: `nearbyCars[${index}].leftOverlap`, scale: 'boolean' },
|
|
160
|
+
{ name: `nearbyCars[${index}].rightOverlap`, scale: 'boolean' },
|
|
161
|
+
);
|
|
162
|
+
vector.push(
|
|
163
|
+
nearby?.behind ? 1 : 0,
|
|
164
|
+
(nearby?.closingRateMetersPerSecond ?? 0) / 100,
|
|
165
|
+
ratio(nearby?.timeToContactSeconds ?? 10, 10),
|
|
166
|
+
nearby?.leftOverlap ? 1 : 0,
|
|
167
|
+
nearby?.rightOverlap ? 1 : 0,
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return { vector: finalizeVector(vector, vectorType), schema: schema ?? [] };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function finalizeVector(vector, vectorType = 'array') {
|
|
175
|
+
return vectorType === 'float32' ? Float32Array.from(vector) : vector;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function pushSchema(schema, ...entries) {
|
|
179
|
+
if (schema) schema.push(...entries);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function normalizeLapProgress(source) {
|
|
183
|
+
return ratio(source.self.lapProgressMeters, source.track.lengthMeters || 1);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function normalizeRacePosition(source) {
|
|
187
|
+
const totalCars = Number(source.race.totalCars);
|
|
188
|
+
const position = Number(source.race.position);
|
|
189
|
+
if (!Number.isFinite(totalCars) || totalCars <= 1 || !Number.isFinite(position)) return 0;
|
|
190
|
+
return ratio(position - 1, totalCars - 1);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function ratio(value, max) {
|
|
194
|
+
const finite = Number.isFinite(value) ? value : max;
|
|
195
|
+
return Math.max(0, Math.min(1, finite / Math.max(1e-9, max)));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function clampRatio(value) {
|
|
199
|
+
return Math.max(-1, Math.min(1, Number.isFinite(value) ? value : 0));
|
|
200
|
+
}
|