@inventure71/paddockjs 2.0.0 → 3.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 +37 -1
- package/INSTALL_AND_UPDATE.md +10 -1
- package/README.md +92 -17
- package/docs/architecture.md +26 -16
- package/docs/component_inventory.md +72 -0
- package/docs/concepts.md +7 -5
- package/docs/custom_model_controller.md +2 -2
- package/docs/data_contract.md +121 -33
- package/docs/data_model.md +1 -1
- package/docs/index.md +10 -0
- package/docs/learnings.md +6 -6
- package/docs/rules.md +39 -9
- package/docs/sense_contract.md +59 -0
- package/docs/system_specs.md +76 -20
- package/docs/training.md +27 -20
- package/package.json +1 -1
- package/src/api/PaddockSimulatorController.js +75 -36
- package/src/app/BrowserExpertAdapter.js +9 -300
- package/src/app/BrowserExpertExternalRenderer.js +303 -0
- package/src/app/F1SimulatorApp.js +190 -68
- package/src/app/banners/raceDataBannerController.js +71 -36
- package/src/app/banners/stewardMessageController.js +5 -4
- package/src/app/camera/cameraConstants.js +2 -0
- package/src/app/camera/cameraController.js +86 -29
- package/src/app/camera/cameraGeometry.js +12 -1
- package/src/app/domBindings.js +89 -9
- package/src/app/layoutScheduler.js +27 -0
- package/src/app/layoutSupport.js +165 -0
- package/src/app/raceOverlayClearanceSupport.js +158 -0
- package/src/app/readouts/carOverviewRenderer.js +16 -8
- package/src/app/readouts/controlStateRenderer.js +1 -1
- package/src/app/readouts/raceStatusRenderer.js +30 -21
- package/src/app/readouts/readoutFormatters.js +2 -0
- package/src/app/readouts/telemetryRenderer.js +17 -8
- package/src/app/readouts/timingTowerRenderer.js +62 -10
- package/src/app/runtime/runtimeVisibility.js +66 -5
- package/src/config/cssValues.js +16 -0
- package/src/config/defaultOptions.js +51 -38
- package/src/config/restartOptions.js +34 -0
- package/src/config/themeOptions.js +825 -0
- package/src/config/timingGapMode.js +13 -0
- package/src/config/urlOptions.js +25 -0
- package/src/data/championship.js +14 -9
- package/src/environment/controllerLoop.js +34 -5
- package/src/environment/episode.js +14 -2
- package/src/environment/index.d.ts +17 -4
- package/src/environment/observationVector.js +200 -0
- package/src/environment/observations.js +58 -285
- package/src/environment/options.js +1 -1
- package/src/environment/recorder.js +13 -6
- package/src/environment/rewards.js +13 -2
- package/src/environment/runtime.js +65 -25
- 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 +84 -12
- package/src/environment/sensors/trackRays.js +173 -15
- package/src/environment/specs.js +78 -40
- package/src/index.d.ts +112 -9
- package/src/index.js +26 -33
- package/src/simulation/driver/driverController.js +1 -1
- package/src/simulation/driver/edgeRecovery.js +1 -1
- package/src/simulation/driver/racingControls.js +2 -2
- package/src/simulation/driver/rejoinControls.js +2 -2
- 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/pitRouting.js +2 -4
- 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/raceSetup.js +2 -6
- package/src/simulation/race/raceStep.js +2 -2
- 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/timing/lapTelemetry.js +28 -9
- package/src/simulation/track/pitLaneLayout.js +7 -3
- package/src/simulation/track/spatialQueries.js +5 -28
- package/src/simulation/track/trackQueryGrid.js +94 -0
- package/src/simulation/track/trackQueryIndex.js +193 -434
- 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/trackStatePolicy.js +0 -1
- 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 +5 -4
- package/src/simulation/vehicle/vehicleSnapshots.js +22 -0
- package/src/simulation/vehicle/vehicleState.js +36 -2
- package/src/simulation/vehicle/wheelSurface.js +3 -1
- package/src/simulation/warmup/runtimeWarmup.js +0 -1
- package/src/styles.css +1185 -136
- package/src/ui/bannerTemplates.js +38 -28
- package/src/ui/cameraControlsTemplate.js +16 -5
- package/src/ui/carOverviewTemplate.js +12 -5
- package/src/ui/raceCanvasTemplate.js +31 -8
- package/src/ui/raceControlsTemplate.js +27 -10
- package/src/ui/shellTemplate.js +3 -1
- package/src/ui/telemetryDrawerTemplate.js +23 -10
- package/src/ui/telemetryTemplates.js +58 -32
- package/src/ui/templateUtils.js +48 -0
- package/src/ui/timingTowerTemplate.js +29 -12
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export const TIMING_GAP_MODES = new Set(['interval', 'leader']);
|
|
2
|
+
|
|
3
|
+
export function normalizeTimingGapMode(value) {
|
|
4
|
+
return value === 'leader' ? 'leader' : 'interval';
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function getTimingGapModeLabel(mode) {
|
|
8
|
+
return normalizeTimingGapMode(mode) === 'leader' ? 'Gap' : 'Int';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function getNextTimingGapMode(mode) {
|
|
12
|
+
return normalizeTimingGapMode(mode) === 'leader' ? 'interval' : 'leader';
|
|
13
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
const SAFE_ABSOLUTE_URL_PROTOCOLS = new Set(['http:', 'https:']);
|
|
2
|
+
|
|
3
|
+
export function normalizePublicUrlOption(value, fallback) {
|
|
4
|
+
const fallbackValue = typeof fallback === 'string' && fallback.trim() !== ''
|
|
5
|
+
? fallback.trim()
|
|
6
|
+
: '';
|
|
7
|
+
if (typeof value !== 'string') return fallbackValue;
|
|
8
|
+
|
|
9
|
+
const candidate = value.trim();
|
|
10
|
+
if (candidate === '') return fallbackValue;
|
|
11
|
+
if (/[\u0000-\u001F\u007F]/.test(candidate)) return fallbackValue;
|
|
12
|
+
if (candidate.includes('\\')) return fallbackValue;
|
|
13
|
+
if (candidate.startsWith('#')) return candidate;
|
|
14
|
+
if (candidate.startsWith('//')) return fallbackValue;
|
|
15
|
+
|
|
16
|
+
const schemeMatch = /^[A-Za-z][A-Za-z0-9+.-]*:/.exec(candidate);
|
|
17
|
+
if (!schemeMatch) return candidate;
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
const url = new URL(candidate);
|
|
21
|
+
return SAFE_ABSOLUTE_URL_PROTOCOLS.has(url.protocol) ? candidate : fallbackValue;
|
|
22
|
+
} catch {
|
|
23
|
+
return fallbackValue;
|
|
24
|
+
}
|
|
25
|
+
}
|
package/src/data/championship.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { DriverData } from './driverData.js';
|
|
2
2
|
import { DEMO_PROJECT_DRIVERS } from './demoDrivers.js';
|
|
3
3
|
import { VehicleData } from './vehicleData.js';
|
|
4
|
+
import { DEFAULT_DARK_THEME_TOKENS, normalizeCssColorToken } from '../config/themeOptions.js';
|
|
4
5
|
|
|
5
6
|
export { DriverData, DRIVER_STAT_DEFINITIONS } from './driverData.js';
|
|
6
7
|
export { VehicleData, VEHICLE_STAT_DEFINITIONS } from './vehicleData.js';
|
|
@@ -187,7 +188,7 @@ function normalizeTeam(team, driver, timingCode) {
|
|
|
187
188
|
...source,
|
|
188
189
|
id,
|
|
189
190
|
name,
|
|
190
|
-
color: source.color ?? driver.color,
|
|
191
|
+
color: normalizeCssColorToken(source.color ?? driver.color, driver.color),
|
|
191
192
|
icon: source.icon ?? normalizeTeamIcon(source.name ?? source.id ?? timingCode ?? driver.name),
|
|
192
193
|
};
|
|
193
194
|
}
|
|
@@ -201,22 +202,26 @@ export function buildChampionshipDriverGrid(drivers = DEMO_PROJECT_DRIVERS, entr
|
|
|
201
202
|
const usedTimingCodes = new Set();
|
|
202
203
|
|
|
203
204
|
return drivers.map((driver, index) => {
|
|
205
|
+
const safeDriver = {
|
|
206
|
+
...driver,
|
|
207
|
+
color: normalizeCssColorToken(driver.color, DEFAULT_DARK_THEME_TOKENS.primary),
|
|
208
|
+
};
|
|
204
209
|
const entry = {
|
|
205
|
-
...(blueprintByDriverId.get(
|
|
206
|
-
...(entryByDriverId.get(
|
|
210
|
+
...(blueprintByDriverId.get(safeDriver.id) ?? {}),
|
|
211
|
+
...(entryByDriverId.get(safeDriver.id) ?? {}),
|
|
207
212
|
};
|
|
208
|
-
const timingCode = pickUniqueTimingCode(entry.timingName ??
|
|
209
|
-
const driverNumber = entry.driverNumber ??
|
|
210
|
-
const driverArgs = buildDriverConstructorArgs({ driverId:
|
|
211
|
-
const vehicleArgs = buildVehicleConstructorArgs({ driverId:
|
|
213
|
+
const timingCode = pickUniqueTimingCode(entry.timingName ?? safeDriver.timingName ?? safeDriver.name ?? safeDriver.code, usedTimingCodes);
|
|
214
|
+
const driverNumber = entry.driverNumber ?? safeDriver.driverNumber ?? index + 1;
|
|
215
|
+
const driverArgs = buildDriverConstructorArgs({ driverId: safeDriver.id, ...entry });
|
|
216
|
+
const vehicleArgs = buildVehicleConstructorArgs({ driverId: safeDriver.id, ...entry });
|
|
212
217
|
|
|
213
218
|
return {
|
|
214
|
-
...
|
|
219
|
+
...safeDriver,
|
|
215
220
|
code: timingCode,
|
|
216
221
|
timingCode,
|
|
217
222
|
raceName: timingCode,
|
|
218
223
|
driverNumber,
|
|
219
|
-
team: normalizeTeam(entry.team,
|
|
224
|
+
team: normalizeTeam(entry.team, safeDriver, timingCode),
|
|
220
225
|
pace: driverArgs.pace,
|
|
221
226
|
racecraft: driverArgs.racecraft,
|
|
222
227
|
consistency: driverArgs.consistency,
|
|
@@ -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;
|
|
@@ -156,8 +168,7 @@ export function createPaddockDriverControllerLoop({
|
|
|
156
168
|
|
|
157
169
|
function stop() {
|
|
158
170
|
running = false;
|
|
159
|
-
|
|
160
|
-
else if (typeof scheduled === 'number' && typeof cancelAnimationFrame === 'function') cancelAnimationFrame(scheduled);
|
|
171
|
+
cancelScheduled(scheduled);
|
|
161
172
|
scheduled = null;
|
|
162
173
|
}
|
|
163
174
|
|
|
@@ -167,7 +178,10 @@ export function createPaddockDriverControllerLoop({
|
|
|
167
178
|
|
|
168
179
|
function buildContext(extra = {}) {
|
|
169
180
|
const drivers = controlledDrivers();
|
|
170
|
-
const observation =
|
|
181
|
+
const observation = {
|
|
182
|
+
...observationCache,
|
|
183
|
+
...(result?.observation ?? runtime.getObservation?.() ?? {}),
|
|
184
|
+
};
|
|
171
185
|
return {
|
|
172
186
|
runtime,
|
|
173
187
|
mode,
|
|
@@ -235,9 +249,24 @@ function now() {
|
|
|
235
249
|
: Date.now();
|
|
236
250
|
}
|
|
237
251
|
|
|
252
|
+
function cancelScheduled(handle) {
|
|
253
|
+
if (handle == null) return;
|
|
254
|
+
if (typeof handle.cancel === 'function') {
|
|
255
|
+
handle.cancel();
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
if (typeof handle === 'number') {
|
|
259
|
+
if (typeof cancelAnimationFrame === 'function') cancelAnimationFrame(handle);
|
|
260
|
+
if (typeof clearTimeout === 'function') clearTimeout(handle);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
238
264
|
function defaultScheduler() {
|
|
239
265
|
if (typeof requestAnimationFrame === 'function') {
|
|
240
|
-
return (callback) =>
|
|
266
|
+
return (callback) => {
|
|
267
|
+
const id = requestAnimationFrame(callback);
|
|
268
|
+
return { cancel: () => cancelAnimationFrame(id) };
|
|
269
|
+
};
|
|
241
270
|
}
|
|
242
271
|
return (callback) => {
|
|
243
272
|
const id = setTimeout(callback, 0);
|
|
@@ -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
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export type TireCompound = 'S' | 'M' | 'H';
|
|
2
|
-
export type PaddockPhysicsMode = 'arcade' | '
|
|
2
|
+
export type PaddockPhysicsMode = 'arcade' | 'advanced';
|
|
3
3
|
export type PaddockProceduralTrackProfile = 'race' | 'training-short' | 'training-medium' | 'training-technical';
|
|
4
4
|
|
|
5
5
|
export interface PaddockProceduralTrackOptions {
|
|
@@ -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;
|
|
@@ -470,7 +478,6 @@ export interface PaddockEnvironmentOptions {
|
|
|
470
478
|
seed?: number;
|
|
471
479
|
trackSeed?: number;
|
|
472
480
|
trackGeneration?: PaddockProceduralTrackOptions;
|
|
473
|
-
trackQueryIndex?: boolean;
|
|
474
481
|
totalLaps?: number;
|
|
475
482
|
frameSkip?: number;
|
|
476
483
|
physicsMode?: PaddockPhysicsMode;
|
|
@@ -590,12 +597,18 @@ export interface PaddockActionSpec {
|
|
|
590
597
|
}
|
|
591
598
|
|
|
592
599
|
export interface PaddockObservationSpec {
|
|
593
|
-
version:
|
|
600
|
+
version: 3 | 6;
|
|
594
601
|
controlledDrivers: string[];
|
|
595
602
|
object: Record<string, unknown>;
|
|
596
603
|
vector: {
|
|
597
604
|
schema: PaddockObservationSchemaEntry[];
|
|
598
605
|
};
|
|
606
|
+
perDriver: Record<string, {
|
|
607
|
+
object: Record<string, unknown>;
|
|
608
|
+
vector: {
|
|
609
|
+
schema: PaddockObservationSchemaEntry[];
|
|
610
|
+
};
|
|
611
|
+
}>;
|
|
599
612
|
}
|
|
600
613
|
|
|
601
614
|
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
|
+
}
|