@inventure71/paddockjs 3.0.0 → 4.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 +146 -1
- package/INSTALL_AND_UPDATE.md +15 -4
- package/README.md +84 -562
- package/docs/architecture.md +28 -18
- package/docs/composable-layouts.md +77 -0
- package/docs/custom_model_controller.md +4 -4
- package/docs/data-helpers.md +50 -0
- package/docs/data_contract.md +9 -7
- package/docs/getting-started.md +75 -0
- package/docs/headless-environment.md +68 -0
- package/docs/index.md +17 -4
- package/docs/rules.md +6 -2
- package/docs/system_specs.md +49 -6
- package/docs/theming.md +64 -0
- package/docs/training.md +20 -14
- package/docs/troubleshooting.md +76 -0
- package/docs/upgrading-hosts-to-3.md +42 -0
- package/examples/python/README.md +6 -0
- package/examples/python/base_policy_server.py +19 -0
- package/package.json +10 -3
- package/src/api/PaddockSimulatorController.js +78 -22
- package/src/api/runtimeTheme.js +59 -0
- package/src/app/F1SimulatorApp.js +24 -3
- package/src/app/readouts/timingTowerRenderer.js +203 -42
- package/src/app/runtime/frameLoop.js +5 -1
- package/src/config/defaultOptions.js +8 -1
- package/src/config/themeOptions.js +5 -0
- package/src/data/index.d.ts +212 -0
- package/src/data/index.js +14 -0
- package/src/environment/episode.js +11 -1
- package/src/environment/metrics.js +59 -16
- package/src/environment/observationVector.js +50 -33
- package/src/environment/observations.js +89 -20
- package/src/environment/runtime.js +41 -0
- package/src/environment/sensors/carRays.js +48 -37
- package/src/environment/sensors/index.js +501 -97
- package/src/environment/sensors/indexedRayBands.js +134 -46
- package/src/environment/sensors/rayBandTrace.js +1171 -0
- package/src/environment/sensors/rayChannels.js +108 -0
- package/src/environment/sensors/rayConfig.js +71 -25
- package/src/environment/sensors/rayGeometry.js +23 -17
- package/src/environment/sensors/sensorTargets.js +67 -1
- package/src/environment/sensors/surfaceRays.js +130 -89
- package/src/environment/sensors/trackRays.js +115 -1
- package/src/index.d.ts +43 -0
- package/src/index.js +27 -2
- package/src/simulation/collisionGeometry.js +260 -96
- package/src/simulation/pit/pitFlow.js +21 -9
- package/src/simulation/pit/pitQueue.js +5 -3
- package/src/simulation/pit/pitRouteMovement.js +9 -4
- package/src/simulation/pit/pitRouting.js +77 -42
- package/src/simulation/pit/pitState.js +1 -0
- package/src/simulation/pit/pitTireService.js +7 -3
- package/src/simulation/race/raceFinish.js +25 -8
- package/src/simulation/race/raceOrder.js +189 -32
- package/src/simulation/race/raceProgress.js +129 -37
- package/src/simulation/race/raceSetup.js +2 -0
- package/src/simulation/race/raceStep.js +25 -13
- package/src/simulation/raceSimulation.js +20 -17
- package/src/simulation/rules/collisionSteward.js +40 -23
- package/src/simulation/rules/rulesReview.js +77 -56
- package/src/simulation/snapshots/raceSnapshots.js +37 -0
- package/src/simulation/timing/drsTiming.js +143 -30
- package/src/simulation/timing/gapEstimation.js +25 -3
- package/src/simulation/timing/lapTelemetry.js +115 -36
- package/src/simulation/timing/raceTiming.js +5 -1
- package/src/simulation/timing/sectorPerformance.js +104 -26
- package/src/simulation/timing/timingHistory.js +229 -14
- package/src/simulation/timing/timingLines.js +65 -7
- package/src/simulation/track/pitLaneLayout.js +23 -0
- package/src/simulation/track/pitLaneState.js +218 -19
- package/src/simulation/track/spatialQueries.js +67 -11
- package/src/simulation/track/trackMath.js +91 -24
- package/src/simulation/track/trackModel.js +2 -0
- package/src/simulation/track/trackQueryIndex.js +1334 -100
- package/src/simulation/track/trackQueryPitIndex.js +138 -14
- package/src/simulation/track/trackQueryScratch.js +7 -0
- package/src/simulation/track/trackQueryStats.js +25 -0
- package/src/simulation/track/trackSnapshotJson.js +139 -0
- package/src/simulation/track/trackStatePolicy.js +249 -1
- package/src/simulation/vehicle/contactResolution.js +96 -59
- package/src/simulation/vehicle/mainTrackWheelSurface.js +73 -34
- package/src/simulation/vehicle/pitWheelSurface.js +81 -28
- package/src/simulation/vehicle/runoffResponse.js +54 -29
- package/src/simulation/vehicle/vehicleGeometry.js +245 -51
- package/src/simulation/vehicle/vehiclePhysics.js +42 -27
- package/src/simulation/vehicle/vehicleSnapshots.js +23 -0
- package/src/simulation/vehicle/vehicleState.js +6 -0
- package/src/simulation/vehicle/wheelSurface.js +807 -92
- package/src/styles.css +497 -497
|
@@ -48,7 +48,11 @@ export function runFrameLoopTick(app) {
|
|
|
48
48
|
const shouldUpdateDom = now - app.lastDomUpdateTime >= domUpdateIntervalForSpeed(app.simulationSpeed);
|
|
49
49
|
const fullSnapshot = stepEvents.length > 0 || shouldUpdateDom ? app.sim.snapshot() : null;
|
|
50
50
|
if (stepEvents.length > 0) app.emitRaceEvents(stepEvents, fullSnapshot);
|
|
51
|
-
const renderSource = fullSnapshot ??
|
|
51
|
+
const renderSource = fullSnapshot ??
|
|
52
|
+
app.sim.snapshotRenderInto?.(app.renderSourceSnapshotBuffer ?? {}) ??
|
|
53
|
+
app.sim.snapshotRender?.() ??
|
|
54
|
+
app.sim.snapshot();
|
|
55
|
+
if (!fullSnapshot && app.sim.snapshotRenderInto) app.renderSourceSnapshotBuffer = renderSource;
|
|
52
56
|
const renderSnapshot = interpolateRenderSnapshotInto(
|
|
53
57
|
app.renderSnapshotBuffer ?? {},
|
|
54
58
|
renderSource,
|
|
@@ -119,7 +119,14 @@ export const PADDOCK_SIMULATOR_PRESETS = {
|
|
|
119
119
|
},
|
|
120
120
|
};
|
|
121
121
|
|
|
122
|
-
export {
|
|
122
|
+
export {
|
|
123
|
+
DEFAULT_PADDOCK_THEME,
|
|
124
|
+
PADDOCK_THEME_CSS_VARIABLES,
|
|
125
|
+
PADDOCK_THEME_TOKEN_KEYS,
|
|
126
|
+
applyPaddockTheme,
|
|
127
|
+
applyPaddockThemeCssVariables,
|
|
128
|
+
resolvePaddockTheme,
|
|
129
|
+
} from './themeOptions.js';
|
|
123
130
|
|
|
124
131
|
const SUPPORTED_CAMERA_MODES = new Set(['overview', 'leader', 'selected', 'driver', 'show-all', 'pit']);
|
|
125
132
|
|
|
@@ -34,6 +34,7 @@ export const THEME_SIZE_TOKEN_KEYS = [
|
|
|
34
34
|
];
|
|
35
35
|
|
|
36
36
|
const THEME_TOKEN_KEYS = [...THEME_COLOR_TOKEN_KEYS, ...THEME_SIZE_TOKEN_KEYS];
|
|
37
|
+
export const PADDOCK_THEME_TOKEN_KEYS = THEME_TOKEN_KEYS;
|
|
37
38
|
const THEME_TOKEN_SET = new Set(THEME_TOKEN_KEYS);
|
|
38
39
|
const COLOR_TOKEN_SET = new Set(THEME_COLOR_TOKEN_KEYS);
|
|
39
40
|
const SIZE_TOKEN_SET = new Set(THEME_SIZE_TOKEN_KEYS);
|
|
@@ -248,6 +249,7 @@ export const DEFAULT_PADDOCK_THEME_INPUT = {
|
|
|
248
249
|
mode: 'dark',
|
|
249
250
|
use: 'default',
|
|
250
251
|
};
|
|
252
|
+
export const DEFAULT_PADDOCK_THEME = DEFAULT_PADDOCK_THEME_INPUT;
|
|
251
253
|
|
|
252
254
|
const DEFAULT_THEME_PACKAGE = {
|
|
253
255
|
tokens: {
|
|
@@ -346,6 +348,9 @@ export function applyPaddockThemeCssVariables(root, theme = DEFAULT_PADDOCK_THEM
|
|
|
346
348
|
});
|
|
347
349
|
}
|
|
348
350
|
|
|
351
|
+
export const resolvePaddockTheme = normalizePaddockTheme;
|
|
352
|
+
export const applyPaddockTheme = applyPaddockThemeCssVariables;
|
|
353
|
+
|
|
349
354
|
export function normalizeCssColorToken(value, fallback = null) {
|
|
350
355
|
if (typeof value !== 'string') return fallback;
|
|
351
356
|
const candidate = value.trim();
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
export type TireCompound = 'S' | 'M' | 'H';
|
|
2
|
+
export type PaddockProceduralTrackProfile = 'race' | 'training-short' | 'training-medium' | 'training-technical';
|
|
3
|
+
|
|
4
|
+
export interface PaddockProceduralTrackOptions {
|
|
5
|
+
profile?: PaddockProceduralTrackProfile;
|
|
6
|
+
minLengthMeters?: number;
|
|
7
|
+
maxLengthMeters?: number;
|
|
8
|
+
startStraightMeters?: number;
|
|
9
|
+
includePitLane?: boolean;
|
|
10
|
+
length?: {
|
|
11
|
+
minMeters?: number;
|
|
12
|
+
maxMeters?: number;
|
|
13
|
+
};
|
|
14
|
+
startStraight?: {
|
|
15
|
+
gridMeters?: number;
|
|
16
|
+
exitMeters?: number;
|
|
17
|
+
blendMeters?: number;
|
|
18
|
+
lockExtraMeters?: number;
|
|
19
|
+
};
|
|
20
|
+
pitLane?: {
|
|
21
|
+
enabled?: boolean;
|
|
22
|
+
};
|
|
23
|
+
shape?: {
|
|
24
|
+
scale?: number;
|
|
25
|
+
cornerDensity?: number;
|
|
26
|
+
variation?: number;
|
|
27
|
+
};
|
|
28
|
+
validation?: {
|
|
29
|
+
minClearanceMultiplier?: number;
|
|
30
|
+
minShapeVariation?: number;
|
|
31
|
+
minNonAdjacentArcMeters?: number;
|
|
32
|
+
maxLocalTurnRadians?: number;
|
|
33
|
+
maxSampleHeadingDeltaRadians?: number;
|
|
34
|
+
};
|
|
35
|
+
attempts?: {
|
|
36
|
+
primary?: number;
|
|
37
|
+
fallback?: number;
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface CustomField {
|
|
42
|
+
label: string;
|
|
43
|
+
value: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type CustomFieldInput = CustomField[] | Record<string, string>;
|
|
47
|
+
export type PaddockThemeSelector = 'default' | 'active' | 'selectedTeam' | 'team' | `team:${string}` | string;
|
|
48
|
+
|
|
49
|
+
export interface PaddockPitCrewStats {
|
|
50
|
+
speed?: number;
|
|
51
|
+
consistency?: number;
|
|
52
|
+
reliability?: number;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface TeamData {
|
|
56
|
+
id?: string;
|
|
57
|
+
name?: string;
|
|
58
|
+
color?: string;
|
|
59
|
+
icon?: string;
|
|
60
|
+
theme?: PaddockThemeSelector;
|
|
61
|
+
pitCrew?: PaddockPitCrewStats;
|
|
62
|
+
pitCrewStats?: PaddockPitCrewStats;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface SimulatorDriver {
|
|
66
|
+
id: string;
|
|
67
|
+
name: string;
|
|
68
|
+
color: string;
|
|
69
|
+
link?: string;
|
|
70
|
+
icon?: string;
|
|
71
|
+
code?: string;
|
|
72
|
+
timingName?: string;
|
|
73
|
+
tire?: TireCompound;
|
|
74
|
+
raceData?: string[];
|
|
75
|
+
customFields?: CustomFieldInput;
|
|
76
|
+
team?: TeamData | null;
|
|
77
|
+
driverNumber?: number;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface DriverRatings {
|
|
81
|
+
pace: number;
|
|
82
|
+
racecraft: number;
|
|
83
|
+
aggression: number;
|
|
84
|
+
riskTolerance: number;
|
|
85
|
+
patience: number;
|
|
86
|
+
consistency: number;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface VehicleRatings {
|
|
90
|
+
power: number;
|
|
91
|
+
braking: number;
|
|
92
|
+
aero: number;
|
|
93
|
+
dragEfficiency: number;
|
|
94
|
+
mechanicalGrip: number;
|
|
95
|
+
weightControl: number;
|
|
96
|
+
tireCare: number;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface DriverBlueprint extends Partial<DriverRatings> {
|
|
100
|
+
customFields?: CustomFieldInput;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export interface VehicleBlueprint extends Partial<VehicleRatings> {
|
|
104
|
+
id?: string | null;
|
|
105
|
+
name?: string | null;
|
|
106
|
+
customFields?: CustomFieldInput;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export interface DriverConstructorArgs {
|
|
110
|
+
ratings: DriverRatings;
|
|
111
|
+
customFields: CustomField[];
|
|
112
|
+
pace: number;
|
|
113
|
+
racecraft: number;
|
|
114
|
+
consistency: number;
|
|
115
|
+
personality: {
|
|
116
|
+
aggression: number;
|
|
117
|
+
riskTolerance: number;
|
|
118
|
+
patience: number;
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export interface VehicleConstructorArgs {
|
|
123
|
+
id: string | null;
|
|
124
|
+
name: string | null;
|
|
125
|
+
ratings: VehicleRatings;
|
|
126
|
+
customFields: CustomField[];
|
|
127
|
+
powerNewtons: number;
|
|
128
|
+
brakeNewtons: number;
|
|
129
|
+
downforceCoefficient: number;
|
|
130
|
+
dragCoefficient: number;
|
|
131
|
+
tireGrip: number;
|
|
132
|
+
mass: number;
|
|
133
|
+
tireCare: number;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export class DriverData {
|
|
137
|
+
constructor(input?: DriverBlueprint);
|
|
138
|
+
readonly pace: number;
|
|
139
|
+
readonly racecraft: number;
|
|
140
|
+
readonly aggression: number;
|
|
141
|
+
readonly riskTolerance: number;
|
|
142
|
+
readonly patience: number;
|
|
143
|
+
readonly consistency: number;
|
|
144
|
+
readonly customFields: CustomField[];
|
|
145
|
+
ratings(): DriverRatings;
|
|
146
|
+
toConstructorArgs(): DriverConstructorArgs;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export class VehicleData {
|
|
150
|
+
constructor(input?: VehicleBlueprint);
|
|
151
|
+
readonly id: string | null;
|
|
152
|
+
readonly name: string | null;
|
|
153
|
+
readonly power: number;
|
|
154
|
+
readonly braking: number;
|
|
155
|
+
readonly aero: number;
|
|
156
|
+
readonly dragEfficiency: number;
|
|
157
|
+
readonly mechanicalGrip: number;
|
|
158
|
+
readonly weightControl: number;
|
|
159
|
+
readonly tireCare: number;
|
|
160
|
+
readonly customFields: CustomField[];
|
|
161
|
+
ratings(): VehicleRatings;
|
|
162
|
+
toConstructorArgs(): VehicleConstructorArgs;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export interface ChampionshipEntryBlueprint {
|
|
166
|
+
driverId: string;
|
|
167
|
+
driverNumber?: number;
|
|
168
|
+
timingName?: string;
|
|
169
|
+
driver?: DriverData | DriverBlueprint;
|
|
170
|
+
vehicle?: VehicleData | VehicleBlueprint;
|
|
171
|
+
team?: TeamData;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export interface NormalizedSimulatorDriver extends SimulatorDriver {
|
|
175
|
+
code: string;
|
|
176
|
+
timingCode: string;
|
|
177
|
+
raceName: string;
|
|
178
|
+
driverNumber: number;
|
|
179
|
+
team: TeamData | null;
|
|
180
|
+
pace: number;
|
|
181
|
+
racecraft: number;
|
|
182
|
+
consistency: number;
|
|
183
|
+
personality: DriverConstructorArgs['personality'];
|
|
184
|
+
vehicle: VehicleConstructorArgs;
|
|
185
|
+
constructorArgs: {
|
|
186
|
+
driver: DriverConstructorArgs;
|
|
187
|
+
vehicle: VehicleConstructorArgs;
|
|
188
|
+
};
|
|
189
|
+
championship: {
|
|
190
|
+
id: string;
|
|
191
|
+
name: string;
|
|
192
|
+
season: number;
|
|
193
|
+
entryIndex: number;
|
|
194
|
+
vehicleId: string | null;
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export const CHAMPIONSHIP_ENTRY_BLUEPRINTS: ChampionshipEntryBlueprint[];
|
|
199
|
+
export const DEMO_PROJECT_DRIVERS: SimulatorDriver[];
|
|
200
|
+
|
|
201
|
+
export function buildChampionshipDriverGrid(
|
|
202
|
+
drivers?: SimulatorDriver[],
|
|
203
|
+
entries?: ChampionshipEntryBlueprint[],
|
|
204
|
+
): NormalizedSimulatorDriver[];
|
|
205
|
+
export function formatDriverNumber(driverNumber: number | string | null | undefined): string;
|
|
206
|
+
export function normalizeSimulatorDrivers(
|
|
207
|
+
drivers: SimulatorDriver[],
|
|
208
|
+
options?: { entries?: ChampionshipEntryBlueprint[]; caller?: string },
|
|
209
|
+
): NormalizedSimulatorDriver[];
|
|
210
|
+
export function createProceduralTrack(seed?: number | string, options?: PaddockProceduralTrackOptions): unknown;
|
|
211
|
+
export function kphToSimSpeed(kph: number): number;
|
|
212
|
+
export function simSpeedToKph(simSpeed: number): number;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export {
|
|
2
|
+
CHAMPIONSHIP_ENTRY_BLUEPRINTS,
|
|
3
|
+
DriverData,
|
|
4
|
+
VehicleData,
|
|
5
|
+
buildChampionshipDriverGrid,
|
|
6
|
+
formatDriverNumber,
|
|
7
|
+
} from './championship.js';
|
|
8
|
+
export { DEMO_PROJECT_DRIVERS } from './demoDrivers.js';
|
|
9
|
+
export { normalizeSimulatorDrivers } from './normalizeDrivers.js';
|
|
10
|
+
export { createProceduralTrack } from '../simulation/track/trackModel.js';
|
|
11
|
+
export {
|
|
12
|
+
kphToSimSpeed,
|
|
13
|
+
simSpeedToKph,
|
|
14
|
+
} from '../simulation/units.js';
|
|
@@ -2,6 +2,9 @@ export function createEpisodeState() {
|
|
|
2
2
|
return {
|
|
3
3
|
step: 0,
|
|
4
4
|
drivers: new Map(),
|
|
5
|
+
observationScratch: {},
|
|
6
|
+
metricScratch: {},
|
|
7
|
+
destroyedCarsById: new Map(),
|
|
5
8
|
previousSnapshot: null,
|
|
6
9
|
lastResult: null,
|
|
7
10
|
};
|
|
@@ -30,7 +33,14 @@ export function resetDriverEpisodes(episodeState, driverIds = []) {
|
|
|
30
33
|
}
|
|
31
34
|
|
|
32
35
|
export function markDestroyedDriverEpisodes(episodeState, driverIds = [], snapshot = null) {
|
|
33
|
-
const carsById = new Map(
|
|
36
|
+
const carsById = episodeState.destroyedCarsById ?? new Map();
|
|
37
|
+
episodeState.destroyedCarsById = carsById;
|
|
38
|
+
carsById.clear();
|
|
39
|
+
const cars = snapshot?.cars ?? [];
|
|
40
|
+
for (let index = 0; index < cars.length; index += 1) {
|
|
41
|
+
const car = cars[index];
|
|
42
|
+
carsById.set(car.id, car);
|
|
43
|
+
}
|
|
34
44
|
driverIds.forEach((driverId) => {
|
|
35
45
|
const car = carsById.get(driverId);
|
|
36
46
|
if (!car?.destroyed && !car?.dnf && !car?.outOfRace) return;
|
|
@@ -5,6 +5,21 @@ import { simUnitsToMeters } from '../simulation/units.js';
|
|
|
5
5
|
const LEGAL_SURFACES = new Set(['track', 'kerb', 'pit-entry', 'pit-lane', 'pit-exit', 'pit-box']);
|
|
6
6
|
const ILLEGAL_SURFACES = new Set(['grass', 'gravel', 'runoff', 'barrier']);
|
|
7
7
|
const UNSTABLE_STATES = new Set(['spun', 'backwards', 'spin-risk', 'understeer', 'oversteer', 'destroyed']);
|
|
8
|
+
export const ENVIRONMENT_METRIC_FIELDS = Object.freeze([
|
|
9
|
+
'progressDeltaMeters',
|
|
10
|
+
'legalProgressDeltaMeters',
|
|
11
|
+
'offTrack',
|
|
12
|
+
'kerb',
|
|
13
|
+
'fullyOutsideWhiteLine',
|
|
14
|
+
'severeCut',
|
|
15
|
+
'destroyed',
|
|
16
|
+
'destroyReason',
|
|
17
|
+
'under30kph',
|
|
18
|
+
'spinOrBackwards',
|
|
19
|
+
'completedLap',
|
|
20
|
+
'lapTimeSeconds',
|
|
21
|
+
'contactCount',
|
|
22
|
+
]);
|
|
8
23
|
|
|
9
24
|
export function isEnvironmentCarOffTrack(car) {
|
|
10
25
|
if (!car) return false;
|
|
@@ -15,19 +30,32 @@ export function isEnvironmentContactEvent(event) {
|
|
|
15
30
|
return ['collision', 'contact', 'car-contact'].includes(event?.type);
|
|
16
31
|
}
|
|
17
32
|
|
|
18
|
-
export function buildDriverMetrics({ snapshot, previousSnapshot = null, options, events = [] }) {
|
|
19
|
-
const previousById =
|
|
20
|
-
const currentById =
|
|
21
|
-
const contactCounts = contactCountsByDriver(events);
|
|
33
|
+
export function buildDriverMetrics({ snapshot, previousSnapshot = null, options, events = [], scratch = null }) {
|
|
34
|
+
const previousById = carsByIdForMetrics(previousSnapshot, scratch, 'previousCarsById');
|
|
35
|
+
const currentById = carsByIdForMetrics(snapshot, scratch, 'currentCarsById');
|
|
36
|
+
const contactCounts = contactCountsByDriver(events, scratch);
|
|
22
37
|
const metrics = {};
|
|
23
38
|
options.controlledDrivers.forEach((driverId) => {
|
|
24
39
|
const car = currentById.get(driverId);
|
|
25
40
|
const previous = previousById.get(driverId);
|
|
26
|
-
metrics[driverId] = car ? buildMetricForDriver(car, previous, snapshot, contactCounts
|
|
41
|
+
metrics[driverId] = car ? buildMetricForDriver(car, previous, snapshot, contactCounts?.get(driverId) ?? 0) : emptyMetrics();
|
|
27
42
|
});
|
|
28
43
|
return metrics;
|
|
29
44
|
}
|
|
30
45
|
|
|
46
|
+
function carsByIdForMetrics(snapshot, scratch, key) {
|
|
47
|
+
if (!scratch) return new Map((snapshot?.cars ?? []).map((car) => [car.id, car]));
|
|
48
|
+
const carsById = scratch[key] ?? new Map();
|
|
49
|
+
scratch[key] = carsById;
|
|
50
|
+
carsById.clear();
|
|
51
|
+
const cars = snapshot?.cars ?? [];
|
|
52
|
+
for (let index = 0; index < cars.length; index += 1) {
|
|
53
|
+
const car = cars[index];
|
|
54
|
+
carsById.set(car.id, car);
|
|
55
|
+
}
|
|
56
|
+
return carsById;
|
|
57
|
+
}
|
|
58
|
+
|
|
31
59
|
function buildMetricForDriver(car, previous, snapshot, contactCount) {
|
|
32
60
|
const progressDeltaMeters = progressDelta(car, previous);
|
|
33
61
|
const wheelState = classifyWheels(car);
|
|
@@ -140,20 +168,35 @@ function lastLapTimeSeconds(car) {
|
|
|
140
168
|
return car.lapTelemetry?.lastLapTime ?? car.lapTelemetry?.bestLapTime ?? null;
|
|
141
169
|
}
|
|
142
170
|
|
|
143
|
-
function contactCountsByDriver(events) {
|
|
144
|
-
|
|
145
|
-
|
|
171
|
+
function contactCountsByDriver(events, scratch = null) {
|
|
172
|
+
if (!events.length) return null;
|
|
173
|
+
const counts = scratch ? (scratch.contactCounts ?? new Map()) : new Map();
|
|
174
|
+
const eventDriverIds = scratch ? (scratch.eventDriverIds ?? []) : [];
|
|
175
|
+
if (scratch) {
|
|
176
|
+
scratch.contactCounts = counts;
|
|
177
|
+
scratch.eventDriverIds = eventDriverIds;
|
|
178
|
+
}
|
|
179
|
+
counts.clear();
|
|
146
180
|
events.forEach((event) => {
|
|
147
181
|
if (!isEnvironmentContactEvent(event)) return;
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
182
|
+
eventDriverIds.length = 0;
|
|
183
|
+
pushEventDriverId(eventDriverIds, event.driverId);
|
|
184
|
+
pushEventDriverId(eventDriverIds, event.carId);
|
|
185
|
+
pushEventDriverId(eventDriverIds, event.otherCarId);
|
|
186
|
+
const extraDriverIds = event.driverIds ?? [];
|
|
187
|
+
for (let index = 0; index < extraDriverIds.length; index += 1) {
|
|
188
|
+
pushEventDriverId(eventDriverIds, extraDriverIds[index]);
|
|
189
|
+
}
|
|
190
|
+
for (let index = 0; index < eventDriverIds.length; index += 1) {
|
|
191
|
+
const driverId = eventDriverIds[index];
|
|
155
192
|
counts.set(driverId, (counts.get(driverId) ?? 0) + 1);
|
|
156
|
-
}
|
|
193
|
+
}
|
|
157
194
|
});
|
|
195
|
+
eventDriverIds.length = 0;
|
|
158
196
|
return counts;
|
|
159
197
|
}
|
|
198
|
+
|
|
199
|
+
function pushEventDriverId(target, driverId) {
|
|
200
|
+
if (!driverId || target.includes(driverId)) return;
|
|
201
|
+
target.push(driverId);
|
|
202
|
+
}
|
|
@@ -91,40 +91,36 @@ export function buildObservationVector(source, sensors, { includeSchema = true,
|
|
|
91
91
|
);
|
|
92
92
|
vector.push(sample.curvature ?? 0, (sample.headingDeltaRadians ?? 0) / Math.PI);
|
|
93
93
|
});
|
|
94
|
-
source.
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
}
|
|
94
|
+
if (typeof source.appendRayVectorValues === 'function') {
|
|
95
|
+
source.appendRayVectorValues(vector);
|
|
96
|
+
} else if (Array.isArray(source.rayVectorValues)) {
|
|
97
|
+
source.rayVectorValues.forEach((values, index) => {
|
|
98
|
+
pushRaySchema(schema, index, sensors);
|
|
99
|
+
vector.push(...values);
|
|
126
100
|
});
|
|
127
|
-
}
|
|
101
|
+
} else {
|
|
102
|
+
source.rays.forEach((ray, index) => {
|
|
103
|
+
pushRaySchema(schema, index, sensors);
|
|
104
|
+
vector.push(
|
|
105
|
+
ratio(ray.track.distanceMeters, ray.lengthMeters),
|
|
106
|
+
ray.track.hit ? 1 : 0,
|
|
107
|
+
ray.track.kind === 'exit' ? 1 : 0,
|
|
108
|
+
ray.track.kind === 'entry' ? 1 : 0,
|
|
109
|
+
ratio(ray.car.distanceMeters, ray.lengthMeters),
|
|
110
|
+
ray.car.hit ? 1 : 0,
|
|
111
|
+
ray.car.relativeSpeedKph / 200,
|
|
112
|
+
ray.car.targetType === 'replayGhost' ? 1 : 0,
|
|
113
|
+
);
|
|
114
|
+
MODEL_RAY_SURFACE_CHANNELS.forEach((channel) => {
|
|
115
|
+
if (sensors.rays.channels?.includes?.(channel)) {
|
|
116
|
+
vector.push(
|
|
117
|
+
ratio(ray[channel]?.distanceMeters ?? ray.lengthMeters, ray.lengthMeters),
|
|
118
|
+
ray[channel]?.hit ? 1 : 0,
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
}
|
|
128
124
|
const nearbyLimit = sensors.nearbyCars.enabled ? (sensors.nearbyCars.maxCars ?? source.nearbyCars.length) : 0;
|
|
129
125
|
const nearbyRadius = sensors.nearbyCars.radiusMeters ?? 150;
|
|
130
126
|
for (let index = 0; index < nearbyLimit; index += 1) {
|
|
@@ -179,6 +175,27 @@ function pushSchema(schema, ...entries) {
|
|
|
179
175
|
if (schema) schema.push(...entries);
|
|
180
176
|
}
|
|
181
177
|
|
|
178
|
+
function pushRaySchema(schema, index, sensors) {
|
|
179
|
+
pushSchema(schema,
|
|
180
|
+
{ name: `rays[${index}].track.distanceRatio`, scale: '0..1' },
|
|
181
|
+
{ name: `rays[${index}].track.hit`, scale: 'boolean' },
|
|
182
|
+
{ name: `rays[${index}].track.kindExit`, scale: 'boolean' },
|
|
183
|
+
{ name: `rays[${index}].track.kindEntry`, scale: 'boolean' },
|
|
184
|
+
{ name: `rays[${index}].car.distanceRatio`, scale: '0..1' },
|
|
185
|
+
{ name: `rays[${index}].car.hit`, scale: 'boolean' },
|
|
186
|
+
{ name: `rays[${index}].car.relativeSpeedKph`, unit: 'kph', scale: 'fixed:200' },
|
|
187
|
+
{ name: `rays[${index}].car.targetTypeReplayGhost`, scale: 'boolean' },
|
|
188
|
+
);
|
|
189
|
+
MODEL_RAY_SURFACE_CHANNELS.forEach((channel) => {
|
|
190
|
+
if (sensors.rays.channels?.includes?.(channel)) {
|
|
191
|
+
pushSchema(schema,
|
|
192
|
+
{ name: `rays[${index}].${channel}.distanceRatio`, scale: '0..1' },
|
|
193
|
+
{ name: `rays[${index}].${channel}.hit`, scale: 'boolean' },
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
182
199
|
function normalizeLapProgress(source) {
|
|
183
200
|
return ratio(source.self.lapProgressMeters, source.track.lengthMeters || 1);
|
|
184
201
|
}
|
|
@@ -6,18 +6,37 @@ import { buildBodySenses } from './sensors/bodySenses.js';
|
|
|
6
6
|
import { buildBoundarySenses } from './sensors/boundarySenses.js';
|
|
7
7
|
import { buildContactPatchSenses } from './sensors/contactSenses.js';
|
|
8
8
|
import { enrichOpponentRadar } from './sensors/opponentRadar.js';
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
buildNearbyCars,
|
|
11
|
+
appendRaySensorVectorValues,
|
|
12
|
+
buildRaySensors,
|
|
13
|
+
createRayBatchContext,
|
|
14
|
+
normalizeRayOptions,
|
|
15
|
+
} from './sensors.js';
|
|
10
16
|
import { buildObservationVector } from './observationVector.js';
|
|
11
17
|
|
|
12
18
|
const EMPTY_EVENTS = Object.freeze([]);
|
|
13
19
|
|
|
14
|
-
export function buildEnvironmentObservation({
|
|
15
|
-
|
|
16
|
-
|
|
20
|
+
export function buildEnvironmentObservation({
|
|
21
|
+
snapshot,
|
|
22
|
+
options,
|
|
23
|
+
events = [],
|
|
24
|
+
controlledDrivers = options.controlledDrivers,
|
|
25
|
+
scratch = null,
|
|
26
|
+
}) {
|
|
27
|
+
const carsById = carsByIdForObservation(snapshot, scratch);
|
|
28
|
+
const eventsByDriver = groupEventsByDriver(events, controlledDrivers, scratch);
|
|
17
29
|
const defaultSensors = defaultSensorOptions(options);
|
|
18
30
|
const hasSensorOverrides = Object.keys(options.sensorsByDriver ?? {}).length > 0;
|
|
19
31
|
let rayBatchContext = null;
|
|
20
|
-
|
|
32
|
+
let rayScratchBatchContext = null;
|
|
33
|
+
const getRayBatchContext = ({ scratch: useScratch = false } = {}) => {
|
|
34
|
+
if (useScratch) {
|
|
35
|
+
rayScratchBatchContext ??= createRayBatchContext(snapshot, {
|
|
36
|
+
scratch: rayBatchContextScratchForObservation(scratch),
|
|
37
|
+
});
|
|
38
|
+
return rayScratchBatchContext;
|
|
39
|
+
}
|
|
21
40
|
rayBatchContext ??= createRayBatchContext(snapshot);
|
|
22
41
|
return rayBatchContext;
|
|
23
42
|
};
|
|
@@ -66,24 +85,65 @@ function formatObservation(observation, options = {}) {
|
|
|
66
85
|
return formatted;
|
|
67
86
|
}
|
|
68
87
|
|
|
69
|
-
function
|
|
88
|
+
function carsByIdForObservation(snapshot, scratch) {
|
|
89
|
+
if (!scratch) return new Map(snapshot.cars.map((entry) => [entry.id, entry]));
|
|
90
|
+
const carsById = scratch.carsById ?? new Map();
|
|
91
|
+
scratch.carsById = carsById;
|
|
92
|
+
carsById.clear();
|
|
93
|
+
for (let index = 0; index < snapshot.cars.length; index += 1) {
|
|
94
|
+
const entry = snapshot.cars[index];
|
|
95
|
+
carsById.set(entry.id, entry);
|
|
96
|
+
}
|
|
97
|
+
return carsById;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function groupEventsByDriver(events, controlledDrivers, scratch = null) {
|
|
70
101
|
if (!events.length) return null;
|
|
71
|
-
const byDriver = new Map(
|
|
72
|
-
const controlledSet = new Set(
|
|
102
|
+
const byDriver = scratch ? (scratch.eventsByDriver ?? new Map()) : new Map();
|
|
103
|
+
const controlledSet = scratch ? (scratch.controlledEventDrivers ?? new Set()) : new Set();
|
|
104
|
+
const eventDriverIds = scratch ? (scratch.eventDriverIds ?? []) : [];
|
|
105
|
+
if (scratch) {
|
|
106
|
+
scratch.eventsByDriver = byDriver;
|
|
107
|
+
scratch.controlledEventDrivers = controlledSet;
|
|
108
|
+
scratch.eventDriverIds = eventDriverIds;
|
|
109
|
+
}
|
|
110
|
+
byDriver.clear();
|
|
111
|
+
controlledSet.clear();
|
|
112
|
+
for (let index = 0; index < controlledDrivers.length; index += 1) {
|
|
113
|
+
const driverId = controlledDrivers[index];
|
|
114
|
+
byDriver.set(driverId, []);
|
|
115
|
+
controlledSet.add(driverId);
|
|
116
|
+
}
|
|
73
117
|
events.forEach((event) => {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
118
|
+
eventDriverIds.length = 0;
|
|
119
|
+
pushEventDriverId(eventDriverIds, event.driverId);
|
|
120
|
+
pushEventDriverId(eventDriverIds, event.carId);
|
|
121
|
+
pushEventDriverId(eventDriverIds, event.otherCarId);
|
|
122
|
+
const extraDriverIds = event.driverIds ?? [];
|
|
123
|
+
for (let index = 0; index < extraDriverIds.length; index += 1) {
|
|
124
|
+
pushEventDriverId(eventDriverIds, extraDriverIds[index]);
|
|
125
|
+
}
|
|
126
|
+
for (let index = 0; index < eventDriverIds.length; index += 1) {
|
|
127
|
+
const driverId = eventDriverIds[index];
|
|
81
128
|
if (controlledSet.has(driverId)) byDriver.get(driverId)?.push(event);
|
|
82
|
-
}
|
|
129
|
+
}
|
|
83
130
|
});
|
|
131
|
+
eventDriverIds.length = 0;
|
|
84
132
|
return byDriver;
|
|
85
133
|
}
|
|
86
134
|
|
|
135
|
+
function pushEventDriverId(target, driverId) {
|
|
136
|
+
if (!driverId || target.includes(driverId)) return;
|
|
137
|
+
target.push(driverId);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function rayBatchContextScratchForObservation(scratch) {
|
|
141
|
+
if (!scratch) return true;
|
|
142
|
+
const rayScratch = scratch.rayBatchContextScratch ?? {};
|
|
143
|
+
scratch.rayBatchContextScratch = rayScratch;
|
|
144
|
+
return rayScratch;
|
|
145
|
+
}
|
|
146
|
+
|
|
87
147
|
function buildDriverObservationObject(car, snapshot, options, events, sensors, getRayBatchContext = null) {
|
|
88
148
|
const sensorCar = withEnvironmentControlFlag(car, options);
|
|
89
149
|
const onTrack = isCarLegallyOnTrack(car);
|
|
@@ -186,7 +246,15 @@ function buildDriverVectorDirect(car, snapshot, options, events, sensors, getRay
|
|
|
186
246
|
headingErrorRadians: trackHeadingError,
|
|
187
247
|
};
|
|
188
248
|
const contactPatches = includePhysicalDriverSenses ? buildContactPatchSenses(car) : [];
|
|
189
|
-
const
|
|
249
|
+
const appendRayVectorValues = sensors.rays.enabled
|
|
250
|
+
? (vector) => appendRaySensorVectorValues(
|
|
251
|
+
vector,
|
|
252
|
+
sensorCar,
|
|
253
|
+
snapshot,
|
|
254
|
+
sensors.rays,
|
|
255
|
+
rayBatchContextForSensors(sensors, getRayBatchContext, { scratch: true }),
|
|
256
|
+
)
|
|
257
|
+
: null;
|
|
190
258
|
const nearbyCars = sensors.nearbyCars.enabled
|
|
191
259
|
? enrichOpponentRadar(car, buildNearbyCars(car, snapshot, sensors.nearbyCars), snapshot)
|
|
192
260
|
: [];
|
|
@@ -219,7 +287,8 @@ function buildDriverVectorDirect(car, snapshot, options, events, sensors, getRay
|
|
|
219
287
|
curvature: trackCurvature,
|
|
220
288
|
lookahead: buildTrackLookahead(car, snapshot, options),
|
|
221
289
|
},
|
|
222
|
-
rays,
|
|
290
|
+
rays: [],
|
|
291
|
+
appendRayVectorValues,
|
|
223
292
|
nearbyCars,
|
|
224
293
|
};
|
|
225
294
|
return buildObservationVector(source, sensors, {
|
|
@@ -228,9 +297,9 @@ function buildDriverVectorDirect(car, snapshot, options, events, sensors, getRay
|
|
|
228
297
|
});
|
|
229
298
|
}
|
|
230
299
|
|
|
231
|
-
function rayBatchContextForSensors(sensors, getRayBatchContext) {
|
|
300
|
+
function rayBatchContextForSensors(sensors, getRayBatchContext, { scratch = false } = {}) {
|
|
232
301
|
if (!getRayBatchContext || !sensors.rays?.channels?.includes('car')) return null;
|
|
233
|
-
return getRayBatchContext();
|
|
302
|
+
return getRayBatchContext({ scratch });
|
|
234
303
|
}
|
|
235
304
|
|
|
236
305
|
export function defaultSensorOptions(options) {
|