@inventure71/paddockjs 0.3.0 → 1.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 +39 -0
- package/INSTALL_AND_UPDATE.md +3 -1
- package/README.md +142 -9
- package/docs/architecture.md +49 -15
- package/docs/concepts.md +20 -3
- package/docs/data_contract.md +185 -27
- package/docs/data_model.md +1 -1
- package/docs/learnings.md +1 -1
- package/docs/rules.md +140 -19
- package/docs/system_specs.md +39 -16
- package/docs/training.md +103 -1
- package/examples/train-basic-policy.mjs +5 -6
- package/examples/trainingData.mjs +133 -0
- package/package.json +5 -2
- package/src/api/PaddockSimulatorController.js +32 -2
- package/src/app/F1SimulatorApp.js +1101 -128
- package/src/app/domBindings.js +10 -1
- package/src/config/defaultOptions.js +4 -4
- package/src/environment/actions.js +25 -2
- package/src/environment/evaluation.js +184 -0
- package/src/environment/index.d.ts +281 -1
- package/src/environment/index.js +13 -0
- package/src/environment/observationOptions.js +12 -0
- package/src/environment/observations.js +189 -17
- package/src/environment/options.js +7 -0
- package/src/environment/recorder.js +28 -0
- package/src/environment/runtime.js +41 -6
- package/src/environment/scenarios.js +220 -0
- package/src/environment/sensors.js +145 -35
- package/src/environment/specs.js +92 -8
- package/src/environment/workerProtocol.js +52 -0
- package/src/index.d.ts +366 -3
- package/src/index.js +21 -0
- package/src/rendering/proceduralTrackAsset.js +336 -23
- package/src/simulation/collisionGeometry.js +214 -0
- package/src/simulation/driverController.js +431 -65
- package/src/simulation/raceSimulation.js +2132 -171
- package/src/simulation/rules/collisionSteward.js +69 -0
- package/src/simulation/rules/penaltyLedger.js +153 -0
- package/src/simulation/rules/pitLaneSpeedingSteward.js +71 -0
- package/src/simulation/rules/tireRequirementSteward.js +29 -0
- package/src/simulation/rules/trackLimitsSteward.js +85 -0
- package/src/simulation/rulesConfig.js +352 -0
- package/src/simulation/trackModel.js +1154 -76
- package/src/simulation/units.js +6 -4
- package/src/simulation/vehicleGeometry.js +247 -0
- package/src/simulation/vehiclePhysics.js +82 -50
- package/src/simulation/wheelSurface.js +338 -0
- package/src/styles.css +300 -42
- package/src/ui/bannerTemplates.js +69 -0
- package/src/ui/componentTemplates.js +36 -96
- package/src/ui/raceControlStatusBanner.js +39 -0
- package/src/ui/shellTemplate.js +1 -1
- package/src/ui/templateUtils.js +23 -0
package/src/app/domBindings.js
CHANGED
|
@@ -6,7 +6,9 @@ export function querySimulatorDom(root) {
|
|
|
6
6
|
startLightsLabel: root.querySelector('[data-start-lights-label]'),
|
|
7
7
|
towerLap: root.querySelector('[data-tower-lap-readout]'),
|
|
8
8
|
towerTotalLaps: root.querySelector('[data-tower-total-laps]'),
|
|
9
|
-
|
|
9
|
+
towerRaceControlBanner: root.querySelector('[data-tower-race-control-banner]'),
|
|
10
|
+
towerRaceControlKicker: root.querySelector('[data-tower-race-control-kicker]'),
|
|
11
|
+
towerRaceControlTitle: root.querySelector('[data-tower-race-control-title]'),
|
|
10
12
|
timingGapLabel: root.querySelector('[data-timing-gap-label]'),
|
|
11
13
|
lap: root.querySelector('[data-lap-readout]'),
|
|
12
14
|
drs: root.querySelector('[data-drs-readout]'),
|
|
@@ -45,12 +47,17 @@ export function querySimulatorDom(root) {
|
|
|
45
47
|
telemetryDrawerWorkbench: root.querySelector('[data-race-telemetry-drawer]'),
|
|
46
48
|
telemetryDrawer: root.querySelector('[data-telemetry-drawer]'),
|
|
47
49
|
telemetryDrawerToggle: root.querySelector('[data-telemetry-drawer-toggle]'),
|
|
50
|
+
stewardMessage: root.querySelector('[data-steward-message]'),
|
|
51
|
+
stewardMessageKicker: root.querySelector('[data-steward-message-kicker]'),
|
|
52
|
+
stewardMessageTitle: root.querySelector('[data-steward-message-title]'),
|
|
53
|
+
stewardMessageDetail: root.querySelector('[data-steward-message-detail]'),
|
|
48
54
|
raceDataPanel: root.querySelector('[data-race-data-panel]'),
|
|
49
55
|
raceDataKicker: root.querySelector('[data-race-data-kicker]'),
|
|
50
56
|
raceDataTitle: root.querySelector('[data-race-data-title]'),
|
|
51
57
|
raceDataNumber: root.querySelector('[data-race-data-number]'),
|
|
52
58
|
raceDataSubtitle: root.querySelector('[data-race-data-subtitle]'),
|
|
53
59
|
raceDataOpen: root.querySelector('[data-race-data-open]'),
|
|
60
|
+
raceDataDismiss: root.querySelector('[data-race-data-dismiss]'),
|
|
54
61
|
finishPanel: root.querySelector('[data-race-finish-panel]'),
|
|
55
62
|
finishWinner: root.querySelector('[data-race-finish-winner]'),
|
|
56
63
|
finishClassification: root.querySelector('[data-race-finish-classification]'),
|
|
@@ -65,6 +72,8 @@ export function querySimulatorDom(root) {
|
|
|
65
72
|
timingGapModeButtons: root.querySelectorAll('[data-timing-gap-mode]'),
|
|
66
73
|
cameraButtons: root.querySelectorAll('[data-camera-mode]'),
|
|
67
74
|
overviewModeButtons: root.querySelectorAll('[data-overview-mode]'),
|
|
75
|
+
bannerMuteButtons: root.querySelectorAll('[data-race-data-banners-muted]'),
|
|
76
|
+
simulationSpeedButtons: root.querySelectorAll('[data-simulation-speed]'),
|
|
68
77
|
zoomInButton: root.querySelector('[data-zoom-in]'),
|
|
69
78
|
zoomOutButton: root.querySelector('[data-zoom-out]'),
|
|
70
79
|
readouts,
|
|
@@ -13,7 +13,7 @@ export const DEFAULT_F1_SIMULATOR_OPTIONS = {
|
|
|
13
13
|
showBackLink: true,
|
|
14
14
|
ui: {
|
|
15
15
|
layoutPreset: 'standard',
|
|
16
|
-
cameraControls: '
|
|
16
|
+
cameraControls: 'external',
|
|
17
17
|
showFps: true,
|
|
18
18
|
showTimingTower: true,
|
|
19
19
|
showTelemetry: true,
|
|
@@ -46,7 +46,7 @@ export const PADDOCK_SIMULATOR_PRESETS = {
|
|
|
46
46
|
dashboard: {
|
|
47
47
|
ui: {
|
|
48
48
|
layoutPreset: 'standard',
|
|
49
|
-
cameraControls: '
|
|
49
|
+
cameraControls: 'external',
|
|
50
50
|
showFps: true,
|
|
51
51
|
showTimingTower: true,
|
|
52
52
|
showTelemetry: true,
|
|
@@ -74,7 +74,7 @@ export const PADDOCK_SIMULATOR_PRESETS = {
|
|
|
74
74
|
'compact-race': {
|
|
75
75
|
ui: {
|
|
76
76
|
layoutPreset: 'standard',
|
|
77
|
-
cameraControls: '
|
|
77
|
+
cameraControls: 'external',
|
|
78
78
|
showFps: false,
|
|
79
79
|
showTimingTower: false,
|
|
80
80
|
showTelemetry: false,
|
|
@@ -110,7 +110,7 @@ export const PADDOCK_THEME_CSS_VARIABLES = {
|
|
|
110
110
|
raceViewMinHeight: '--paddock-race-view-min-height',
|
|
111
111
|
};
|
|
112
112
|
|
|
113
|
-
const SUPPORTED_CAMERA_MODES = new Set(['overview', 'leader', 'selected', 'show-all']);
|
|
113
|
+
const SUPPORTED_CAMERA_MODES = new Set(['overview', 'leader', 'selected', 'show-all', 'pit']);
|
|
114
114
|
|
|
115
115
|
export function resolveF1SimulatorOptions(options = {}) {
|
|
116
116
|
const presetName = Object.hasOwn(PADDOCK_SIMULATOR_PRESETS, options.preset)
|
|
@@ -4,6 +4,7 @@ import { VEHICLE_LIMITS } from '../simulation/vehiclePhysics.js';
|
|
|
4
4
|
export function resolveActionMap(actions = {}, controlledDrivers = [], { policy = 'strict' } = {}) {
|
|
5
5
|
const errors = [];
|
|
6
6
|
const controlsByDriver = {};
|
|
7
|
+
const pitIntentByDriver = {};
|
|
7
8
|
|
|
8
9
|
controlledDrivers.forEach((driverId) => {
|
|
9
10
|
const action = actions?.[driverId];
|
|
@@ -13,9 +14,11 @@ export function resolveActionMap(actions = {}, controlledDrivers = [], { policy
|
|
|
13
14
|
}
|
|
14
15
|
const normalized = normalizeAction(action, driverId, policy, errors);
|
|
15
16
|
if (normalized) controlsByDriver[driverId] = normalized;
|
|
17
|
+
const pitIntent = normalizePitIntentAction(action, driverId, policy, errors);
|
|
18
|
+
if (pitIntent != null) pitIntentByDriver[driverId] = pitIntent;
|
|
16
19
|
});
|
|
17
20
|
|
|
18
|
-
return { controlsByDriver, errors };
|
|
21
|
+
return { controlsByDriver, pitIntentByDriver, errors };
|
|
19
22
|
}
|
|
20
23
|
|
|
21
24
|
export function normalizeAction(action, driverId = 'unknown', policy = 'strict', errors = []) {
|
|
@@ -39,7 +42,27 @@ function finiteActionValue(value, message, policy, errors) {
|
|
|
39
42
|
return null;
|
|
40
43
|
}
|
|
41
44
|
|
|
42
|
-
function
|
|
45
|
+
function normalizePitIntentAction(action, driverId, policy, errors) {
|
|
46
|
+
if (!Object.hasOwn(action, 'pitIntent')) return null;
|
|
47
|
+
const number = Number(action.pitIntent);
|
|
48
|
+
if (Number.isInteger(number) && number >= 0 && number <= 2) {
|
|
49
|
+
const pitCompound = normalizePitCompoundAction(action, driverId, policy, errors);
|
|
50
|
+
if (pitCompound === false) return null;
|
|
51
|
+
return pitCompound == null ? number : { intent: number, targetCompound: pitCompound };
|
|
52
|
+
}
|
|
53
|
+
handleActionError(`Invalid pitIntent action for controlled driver: ${driverId}`, policy, errors);
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function normalizePitCompoundAction(action, driverId, policy, errors) {
|
|
58
|
+
if (!Object.hasOwn(action, 'pitCompound') && !Object.hasOwn(action, 'pitTargetCompound')) return null;
|
|
59
|
+
const value = action.pitCompound ?? action.pitTargetCompound;
|
|
60
|
+
if (typeof value === 'string' && value.trim()) return value.trim();
|
|
61
|
+
handleActionError(`Invalid pitCompound action for controlled driver: ${driverId}`, policy, errors);
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function handleActionError(message, policy, errors) {
|
|
43
66
|
if (policy === 'strict') throw new Error(message);
|
|
44
67
|
errors.push(message);
|
|
45
68
|
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { simUnitsToMeters } from '../simulation/units.js';
|
|
2
|
+
import { createPaddockEnvironment } from './runtime.js';
|
|
3
|
+
|
|
4
|
+
export const DEFAULT_EVALUATION_CASES = Object.freeze([
|
|
5
|
+
{
|
|
6
|
+
name: 'cornering',
|
|
7
|
+
seed: 7101,
|
|
8
|
+
trackSeed: 2601,
|
|
9
|
+
maxSteps: 420,
|
|
10
|
+
scenario: { preset: 'cornering' },
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
name: 'off-track-recovery',
|
|
14
|
+
seed: 7102,
|
|
15
|
+
trackSeed: 2602,
|
|
16
|
+
maxSteps: 360,
|
|
17
|
+
scenario: { preset: 'off-track-recovery' },
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
name: 'overtaking-pack',
|
|
21
|
+
seed: 7103,
|
|
22
|
+
trackSeed: 2603,
|
|
23
|
+
maxSteps: 600,
|
|
24
|
+
scenario: { preset: 'overtaking-pack' },
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
name: 'pit-entry',
|
|
28
|
+
seed: 7104,
|
|
29
|
+
trackSeed: 2604,
|
|
30
|
+
maxSteps: 480,
|
|
31
|
+
scenario: { preset: 'pit-entry' },
|
|
32
|
+
},
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
export function runEnvironmentEvaluation({
|
|
36
|
+
baseOptions,
|
|
37
|
+
policy,
|
|
38
|
+
cases = DEFAULT_EVALUATION_CASES,
|
|
39
|
+
createEnvironment = createPaddockEnvironment,
|
|
40
|
+
} = {}) {
|
|
41
|
+
if (!baseOptions || typeof baseOptions !== 'object') {
|
|
42
|
+
throw new Error('PaddockJS environment evaluation requires baseOptions.');
|
|
43
|
+
}
|
|
44
|
+
if (!policy) {
|
|
45
|
+
throw new Error('PaddockJS environment evaluation requires a policy function or object with predict().');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
cases: cases.map((evaluationCase) => runEvaluationCase({
|
|
50
|
+
baseOptions,
|
|
51
|
+
policy,
|
|
52
|
+
evaluationCase,
|
|
53
|
+
createEnvironment,
|
|
54
|
+
})),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function createEvaluationTracker(initialResult) {
|
|
59
|
+
const initialCars = carsById(initialResult.state.snapshot);
|
|
60
|
+
const metrics = Object.fromEntries(initialResult.info.controlledDrivers.map((driverId) => {
|
|
61
|
+
const car = initialCars.get(driverId);
|
|
62
|
+
return [driverId, {
|
|
63
|
+
distanceMeters: 0,
|
|
64
|
+
lapProgressMeters: lapProgressMetersFromSnapshotCar(car),
|
|
65
|
+
offTrackSteps: car && car.surface !== 'track' ? 1 : 0,
|
|
66
|
+
contactCount: 0,
|
|
67
|
+
recoverySuccess: false,
|
|
68
|
+
passCount: 0,
|
|
69
|
+
lapTimeSeconds: null,
|
|
70
|
+
startedOffTrack: Boolean(car && car.surface !== 'track'),
|
|
71
|
+
startPosition: car?.rank ?? null,
|
|
72
|
+
bestPosition: car?.rank ?? null,
|
|
73
|
+
startDistanceMeters: cumulativeDistanceMetersFromSnapshotCar(car),
|
|
74
|
+
}];
|
|
75
|
+
}));
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
update(result) {
|
|
79
|
+
const currentCars = carsById(result.state.snapshot);
|
|
80
|
+
result.info.controlledDrivers.forEach((driverId) => {
|
|
81
|
+
const car = currentCars.get(driverId);
|
|
82
|
+
const entry = metrics[driverId];
|
|
83
|
+
if (!car || !entry) return;
|
|
84
|
+
entry.distanceMeters = Math.max(0, cumulativeDistanceMetersFromSnapshotCar(car) - entry.startDistanceMeters);
|
|
85
|
+
entry.lapProgressMeters = lapProgressMetersFromSnapshotCar(car);
|
|
86
|
+
if (car.surface !== 'track') entry.offTrackSteps += 1;
|
|
87
|
+
if (entry.startedOffTrack && car.surface === 'track') entry.recoverySuccess = true;
|
|
88
|
+
if (Number.isFinite(car.rank)) {
|
|
89
|
+
entry.bestPosition = entry.bestPosition == null ? car.rank : Math.min(entry.bestPosition, car.rank);
|
|
90
|
+
entry.passCount = entry.startPosition == null ? 0 : Math.max(0, entry.startPosition - entry.bestPosition);
|
|
91
|
+
}
|
|
92
|
+
const completedLaps = car.lapTelemetry?.completedLaps ?? 0;
|
|
93
|
+
if (completedLaps > 0 && entry.lapTimeSeconds == null) {
|
|
94
|
+
entry.lapTimeSeconds = result.info.elapsedSeconds;
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
result.events.forEach((event) => {
|
|
98
|
+
if (event.type !== 'collision') return;
|
|
99
|
+
Object.keys(metrics).forEach((driverId) => {
|
|
100
|
+
if (event.driverId === driverId || event.carId === driverId || event.otherCarId === driverId || event.driverIds?.includes?.(driverId)) {
|
|
101
|
+
metrics[driverId].contactCount += 1;
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
},
|
|
106
|
+
finish() {
|
|
107
|
+
return Object.fromEntries(Object.entries(metrics).map(([driverId, entry]) => [
|
|
108
|
+
driverId,
|
|
109
|
+
{
|
|
110
|
+
distanceMeters: entry.distanceMeters,
|
|
111
|
+
lapProgressMeters: entry.lapProgressMeters,
|
|
112
|
+
offTrackSteps: entry.offTrackSteps,
|
|
113
|
+
contactCount: entry.contactCount,
|
|
114
|
+
recoverySuccess: entry.recoverySuccess,
|
|
115
|
+
passCount: entry.passCount,
|
|
116
|
+
lapTimeSeconds: entry.lapTimeSeconds,
|
|
117
|
+
},
|
|
118
|
+
]));
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function cumulativeDistanceMetersFromSnapshotCar(car) {
|
|
124
|
+
const distance = Number(car?.distanceMeters ?? car?.raceDistanceMeters);
|
|
125
|
+
if (Number.isFinite(distance)) return distance;
|
|
126
|
+
return simUnitsToMeters(car?.raceDistance ?? 0);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function lapProgressMetersFromSnapshotCar(car) {
|
|
130
|
+
const progressMeters = Number(car?.lapProgressMeters ?? car?.progressMeters);
|
|
131
|
+
if (Number.isFinite(progressMeters)) return progressMeters;
|
|
132
|
+
return simUnitsToMeters(car?.progress ?? 0);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function runEvaluationCase({ baseOptions, policy, evaluationCase, createEnvironment }) {
|
|
136
|
+
const env = createEnvironment({
|
|
137
|
+
...baseOptions,
|
|
138
|
+
seed: evaluationCase.seed ?? baseOptions.seed,
|
|
139
|
+
trackSeed: evaluationCase.trackSeed ?? baseOptions.trackSeed,
|
|
140
|
+
scenario: {
|
|
141
|
+
...(baseOptions.scenario ?? {}),
|
|
142
|
+
...(evaluationCase.scenario ?? {}),
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
let result = env.reset();
|
|
146
|
+
const tracker = createEvaluationTracker(result);
|
|
147
|
+
const maxSteps = Math.max(1, Math.floor(evaluationCase.maxSteps ?? baseOptions.episode?.maxSteps ?? 1000));
|
|
148
|
+
let steps = 0;
|
|
149
|
+
while (!result.done && steps < maxSteps) {
|
|
150
|
+
const actions = Object.fromEntries(result.info.controlledDrivers.map((driverId) => [
|
|
151
|
+
driverId,
|
|
152
|
+
predictAction(policy, result.observation[driverId], {
|
|
153
|
+
driverId,
|
|
154
|
+
caseName: evaluationCase.name,
|
|
155
|
+
step: steps,
|
|
156
|
+
result,
|
|
157
|
+
}),
|
|
158
|
+
]));
|
|
159
|
+
result = env.step(actions);
|
|
160
|
+
tracker.update(result);
|
|
161
|
+
steps += 1;
|
|
162
|
+
}
|
|
163
|
+
env.destroy?.();
|
|
164
|
+
return {
|
|
165
|
+
name: evaluationCase.name,
|
|
166
|
+
seed: evaluationCase.seed ?? baseOptions.seed,
|
|
167
|
+
trackSeed: evaluationCase.trackSeed ?? baseOptions.trackSeed,
|
|
168
|
+
steps,
|
|
169
|
+
done: result.done,
|
|
170
|
+
endReason: result.info.endReason,
|
|
171
|
+
metrics: tracker.finish(),
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function predictAction(policy, observation, context) {
|
|
176
|
+
const action = typeof policy === 'function'
|
|
177
|
+
? policy(observation, context)
|
|
178
|
+
: policy.predict(observation, context);
|
|
179
|
+
return action ?? { steering: 0, throttle: 0, brake: 1 };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function carsById(snapshot) {
|
|
183
|
+
return new Map(snapshot.cars.map((car) => [car.id, car]));
|
|
184
|
+
}
|
|
@@ -1,10 +1,20 @@
|
|
|
1
1
|
export type TireCompound = 'S' | 'M' | 'H';
|
|
2
|
+
export type PaddockPitIntent = 0 | 1 | 2;
|
|
3
|
+
export type PaddockScenarioPreset = 'cornering' | 'off-track-recovery' | 'overtaking-pack' | 'pit-entry';
|
|
2
4
|
|
|
3
5
|
export interface TeamData {
|
|
4
6
|
id?: string;
|
|
5
7
|
name?: string;
|
|
6
8
|
color?: string;
|
|
7
9
|
icon?: string;
|
|
10
|
+
pitCrew?: PaddockPitCrewStats;
|
|
11
|
+
pitCrewStats?: PaddockPitCrewStats;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface PaddockPitCrewStats {
|
|
15
|
+
speed?: number;
|
|
16
|
+
consistency?: number;
|
|
17
|
+
reliability?: number;
|
|
8
18
|
}
|
|
9
19
|
|
|
10
20
|
export interface SimulatorDriver {
|
|
@@ -37,6 +47,49 @@ export interface RaceEvent {
|
|
|
37
47
|
[key: string]: unknown;
|
|
38
48
|
}
|
|
39
49
|
|
|
50
|
+
export type PaddockRulesetName = 'paddock' | 'custom' | 'grandPrix2025' | 'fia2025';
|
|
51
|
+
|
|
52
|
+
export interface PaddockPenaltyEntry {
|
|
53
|
+
id: string;
|
|
54
|
+
type: string;
|
|
55
|
+
at: number;
|
|
56
|
+
lap: number;
|
|
57
|
+
driverId: string;
|
|
58
|
+
strictness: number;
|
|
59
|
+
status: PaddockPenaltyStatus;
|
|
60
|
+
penaltySeconds: number;
|
|
61
|
+
pendingPenaltySeconds?: number;
|
|
62
|
+
consequences: PaddockPenaltyConsequence[];
|
|
63
|
+
serviceType?: 'driveThrough' | 'stopGo' | null;
|
|
64
|
+
serviceRequired?: boolean;
|
|
65
|
+
serviceServedAt?: number | null;
|
|
66
|
+
appliedAt?: number;
|
|
67
|
+
cancelledAt?: number;
|
|
68
|
+
unserved?: boolean;
|
|
69
|
+
positionDrop?: number;
|
|
70
|
+
gridDrop?: number;
|
|
71
|
+
disqualified?: boolean;
|
|
72
|
+
reason?: string;
|
|
73
|
+
otherCarId?: string;
|
|
74
|
+
aheadDriverId?: string;
|
|
75
|
+
atFaultDriverId?: string;
|
|
76
|
+
sharedFault?: boolean;
|
|
77
|
+
impactSpeedKph?: number;
|
|
78
|
+
impactSpeedThresholdKph?: number;
|
|
79
|
+
[key: string]: unknown;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export type PaddockPenaltyConsequence =
|
|
83
|
+
| { type: 'warning' }
|
|
84
|
+
| { type: 'time'; seconds: number }
|
|
85
|
+
| { type: 'driveThrough'; conversionSeconds?: number }
|
|
86
|
+
| { type: 'stopGo'; seconds?: number; conversionSeconds?: number }
|
|
87
|
+
| { type: 'positionDrop'; positions: number }
|
|
88
|
+
| { type: 'gridDrop'; positions: number }
|
|
89
|
+
| { type: 'disqualification' };
|
|
90
|
+
|
|
91
|
+
export type PaddockPenaltyStatus = 'issued' | 'served' | 'applied' | 'cancelled';
|
|
92
|
+
|
|
40
93
|
export interface RaceSnapshot {
|
|
41
94
|
time: number;
|
|
42
95
|
totalLaps: number;
|
|
@@ -48,6 +101,7 @@ export interface RaceSnapshot {
|
|
|
48
101
|
track: Record<string, unknown>;
|
|
49
102
|
cars: Array<Record<string, unknown>>;
|
|
50
103
|
events: RaceEvent[];
|
|
104
|
+
penalties: PaddockPenaltyEntry[];
|
|
51
105
|
[key: string]: unknown;
|
|
52
106
|
}
|
|
53
107
|
|
|
@@ -55,10 +109,88 @@ export interface PaddockAction {
|
|
|
55
109
|
steering: number;
|
|
56
110
|
throttle: number;
|
|
57
111
|
brake: number;
|
|
112
|
+
pitIntent?: PaddockPitIntent;
|
|
113
|
+
pitCompound?: TireCompound | string;
|
|
114
|
+
pitTargetCompound?: TireCompound | string;
|
|
58
115
|
}
|
|
59
116
|
|
|
60
117
|
export type PaddockActionMap = Record<string, PaddockAction>;
|
|
61
118
|
|
|
119
|
+
export interface PaddockRaceRules {
|
|
120
|
+
ruleset?: PaddockRulesetName;
|
|
121
|
+
profile?: PaddockRulesetName;
|
|
122
|
+
modules?: {
|
|
123
|
+
pitStops?: {
|
|
124
|
+
enabled?: boolean;
|
|
125
|
+
pitLaneSpeedLimitKph?: number;
|
|
126
|
+
defaultStopSeconds?: number;
|
|
127
|
+
variability?: {
|
|
128
|
+
enabled?: boolean;
|
|
129
|
+
perfect?: boolean;
|
|
130
|
+
speedImpactSeconds?: number;
|
|
131
|
+
consistencyJitterSeconds?: number;
|
|
132
|
+
issueChance?: number;
|
|
133
|
+
issueMaxDelaySeconds?: number;
|
|
134
|
+
};
|
|
135
|
+
doubleStacking?: boolean;
|
|
136
|
+
maxConcurrentPitLaneCars?: number;
|
|
137
|
+
minimumPitLaneGapMeters?: number;
|
|
138
|
+
tirePitRequestThresholdPercent?: number;
|
|
139
|
+
tirePitCommitThresholdPercent?: number;
|
|
140
|
+
};
|
|
141
|
+
tireStrategy?: {
|
|
142
|
+
enabled?: boolean;
|
|
143
|
+
compounds?: TireCompound[];
|
|
144
|
+
mandatoryDistinctDryCompounds?: number | null;
|
|
145
|
+
};
|
|
146
|
+
penalties?: {
|
|
147
|
+
enabled?: boolean;
|
|
148
|
+
stewardStrictness?: number;
|
|
149
|
+
trackLimits?: {
|
|
150
|
+
strictness?: number;
|
|
151
|
+
warningsBeforePenalty?: number;
|
|
152
|
+
relaxedMarginMeters?: number;
|
|
153
|
+
timePenaltySeconds?: number;
|
|
154
|
+
consequences?: PaddockPenaltyConsequence[];
|
|
155
|
+
};
|
|
156
|
+
collision?: {
|
|
157
|
+
strictness?: number;
|
|
158
|
+
timePenaltySeconds?: number;
|
|
159
|
+
consequences?: PaddockPenaltyConsequence[];
|
|
160
|
+
minimumSeverity?: number;
|
|
161
|
+
relaxedSeverityMargin?: number;
|
|
162
|
+
minimumImpactSpeedKph?: number;
|
|
163
|
+
relaxedImpactSpeedKph?: number;
|
|
164
|
+
};
|
|
165
|
+
tireRequirement?: {
|
|
166
|
+
strictness?: number;
|
|
167
|
+
timePenaltySeconds?: number;
|
|
168
|
+
consequences?: PaddockPenaltyConsequence[];
|
|
169
|
+
};
|
|
170
|
+
pitLaneSpeeding?: {
|
|
171
|
+
strictness?: number;
|
|
172
|
+
speedLimitKph?: number;
|
|
173
|
+
marginKph?: number;
|
|
174
|
+
relaxedMarginKph?: number;
|
|
175
|
+
timePenaltySeconds?: number;
|
|
176
|
+
consequences?: PaddockPenaltyConsequence[];
|
|
177
|
+
};
|
|
178
|
+
};
|
|
179
|
+
weather?: { enabled?: boolean };
|
|
180
|
+
reliability?: { enabled?: boolean };
|
|
181
|
+
fuelLoad?: { enabled?: boolean };
|
|
182
|
+
};
|
|
183
|
+
drsDetectionSeconds?: number;
|
|
184
|
+
safetyCarSpeed?: number;
|
|
185
|
+
safetyCarLeadDistance?: number;
|
|
186
|
+
safetyCarGap?: number;
|
|
187
|
+
collisionRestitution?: number;
|
|
188
|
+
standingStart?: boolean;
|
|
189
|
+
startLightCount?: number;
|
|
190
|
+
startLightInterval?: number;
|
|
191
|
+
startLightsOutHold?: number;
|
|
192
|
+
}
|
|
193
|
+
|
|
62
194
|
export interface PaddockSensorRayResult {
|
|
63
195
|
angleDegrees: number;
|
|
64
196
|
angleRadians: number;
|
|
@@ -87,6 +219,12 @@ export interface PaddockNearbyCarObservation {
|
|
|
87
219
|
sameLap: boolean;
|
|
88
220
|
}
|
|
89
221
|
|
|
222
|
+
export interface PaddockTrackLookaheadObservation {
|
|
223
|
+
distanceMeters: number;
|
|
224
|
+
curvature: number;
|
|
225
|
+
headingDeltaRadians: number;
|
|
226
|
+
}
|
|
227
|
+
|
|
90
228
|
export interface PaddockDriverObservationObject {
|
|
91
229
|
self: {
|
|
92
230
|
id: string;
|
|
@@ -103,14 +241,32 @@ export interface PaddockDriverObservationObject {
|
|
|
103
241
|
trackHeadingErrorRadians: number;
|
|
104
242
|
onTrack: boolean;
|
|
105
243
|
surface: string;
|
|
244
|
+
inPitLane: boolean;
|
|
245
|
+
pitLanePart: 'entry' | 'fast-lane' | 'working-lane' | 'exit' | 'service-box' | 'garage-box' | null;
|
|
246
|
+
pitBoxId: string | null;
|
|
106
247
|
tireEnergy: number | null;
|
|
248
|
+
pitIntent: PaddockPitIntent;
|
|
249
|
+
pitTargetCompound: TireCompound | string | null;
|
|
250
|
+
pitStopStatus: 'pending' | 'entering' | 'queued' | 'servicing' | 'exiting' | 'completed' | null;
|
|
251
|
+
pitStopPhase: 'entry' | 'queue' | 'queue-release' | 'penalty' | 'service' | 'exit' | null;
|
|
252
|
+
pitStopServiceRemainingSeconds: number | null;
|
|
253
|
+
pitStopPenaltyServiceRemainingSeconds: number | null;
|
|
254
|
+
pitStopsCompleted: number;
|
|
107
255
|
};
|
|
108
256
|
race: {
|
|
109
257
|
position: number;
|
|
110
258
|
totalCars: number;
|
|
111
259
|
raceMode: string;
|
|
260
|
+
pitLaneOpen: boolean;
|
|
261
|
+
redFlag: boolean;
|
|
112
262
|
totalLaps: number;
|
|
113
263
|
};
|
|
264
|
+
track: {
|
|
265
|
+
lengthMeters: number;
|
|
266
|
+
widthMeters: number;
|
|
267
|
+
curvature: number;
|
|
268
|
+
lookahead: PaddockTrackLookaheadObservation[];
|
|
269
|
+
};
|
|
114
270
|
rays: PaddockSensorRayResult[];
|
|
115
271
|
nearbyCars: PaddockNearbyCarObservation[];
|
|
116
272
|
events: RaceEvent[];
|
|
@@ -137,10 +293,14 @@ export interface PaddockEnvironmentOptions {
|
|
|
137
293
|
trackSeed?: number;
|
|
138
294
|
totalLaps?: number;
|
|
139
295
|
frameSkip?: number;
|
|
296
|
+
rules?: PaddockRaceRules;
|
|
140
297
|
actionPolicy?: 'strict' | 'report';
|
|
141
298
|
scenario?: {
|
|
142
299
|
participants?: 'all' | 'controlled-only' | string[];
|
|
143
300
|
nonControlled?: 'ai';
|
|
301
|
+
preset?: PaddockScenarioPreset;
|
|
302
|
+
placements?: Record<string, PaddockScenarioPlacement>;
|
|
303
|
+
traffic?: PaddockScenarioTrafficPlacement[];
|
|
144
304
|
};
|
|
145
305
|
sensors?: {
|
|
146
306
|
rays?: {
|
|
@@ -157,6 +317,9 @@ export interface PaddockEnvironmentOptions {
|
|
|
157
317
|
};
|
|
158
318
|
};
|
|
159
319
|
sensorsByDriver?: Record<string, PaddockEnvironmentOptions['sensors']>;
|
|
320
|
+
observation?: {
|
|
321
|
+
lookaheadMeters?: number[];
|
|
322
|
+
};
|
|
160
323
|
episode?: {
|
|
161
324
|
maxSteps?: number;
|
|
162
325
|
endOnRaceFinish?: boolean;
|
|
@@ -164,6 +327,23 @@ export interface PaddockEnvironmentOptions {
|
|
|
164
327
|
reward?: (payload: PaddockRewardContext) => number;
|
|
165
328
|
}
|
|
166
329
|
|
|
330
|
+
export interface PaddockScenarioPlacement {
|
|
331
|
+
distanceMeters?: number;
|
|
332
|
+
startDistanceMeters?: number;
|
|
333
|
+
offsetMeters?: number;
|
|
334
|
+
speedKph?: number;
|
|
335
|
+
headingErrorRadians?: number;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
export interface PaddockScenarioTrafficPlacement {
|
|
339
|
+
driverId: string;
|
|
340
|
+
relativeTo: string;
|
|
341
|
+
deltaDistanceMeters?: number;
|
|
342
|
+
offsetMeters?: number;
|
|
343
|
+
speedKph?: number;
|
|
344
|
+
headingErrorRadians?: number;
|
|
345
|
+
}
|
|
346
|
+
|
|
167
347
|
export interface PaddockRewardContext {
|
|
168
348
|
driverId: string;
|
|
169
349
|
previous: RaceSnapshot | null;
|
|
@@ -195,12 +375,14 @@ export interface PaddockActionSpec {
|
|
|
195
375
|
steering: { min: -1; max: 1; unit: 'normalized' };
|
|
196
376
|
throttle: { min: 0; max: 1; unit: 'normalized' };
|
|
197
377
|
brake: { min: 0; max: 1; unit: 'normalized' };
|
|
378
|
+
pitIntent: { values: [0, 1, 2]; unit: 'request'; optional: true };
|
|
379
|
+
pitCompound: { values: string[]; unit: 'compound'; optional: true };
|
|
198
380
|
};
|
|
199
381
|
};
|
|
200
382
|
}
|
|
201
383
|
|
|
202
384
|
export interface PaddockObservationSpec {
|
|
203
|
-
version:
|
|
385
|
+
version: 2;
|
|
204
386
|
controlledDrivers: string[];
|
|
205
387
|
object: Record<string, unknown>;
|
|
206
388
|
vector: {
|
|
@@ -208,6 +390,81 @@ export interface PaddockObservationSpec {
|
|
|
208
390
|
};
|
|
209
391
|
}
|
|
210
392
|
|
|
393
|
+
export interface PaddockRolloutTransition {
|
|
394
|
+
observation: Record<string, PaddockDriverObservation>;
|
|
395
|
+
action: PaddockActionMap;
|
|
396
|
+
reward: null | Record<string, number>;
|
|
397
|
+
nextObservation: Record<string, PaddockDriverObservation>;
|
|
398
|
+
terminated: boolean;
|
|
399
|
+
truncated: boolean;
|
|
400
|
+
info: PaddockEnvironmentResult['info'];
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
export interface PaddockRolloutRecorder {
|
|
404
|
+
recordStep(
|
|
405
|
+
previousResult: PaddockEnvironmentResult,
|
|
406
|
+
action: PaddockActionMap,
|
|
407
|
+
nextResult: PaddockEnvironmentResult
|
|
408
|
+
): PaddockRolloutTransition;
|
|
409
|
+
clear(): void;
|
|
410
|
+
toJSON(): PaddockRolloutTransition[];
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
export interface PaddockEvaluationCase {
|
|
414
|
+
name: string;
|
|
415
|
+
seed?: number;
|
|
416
|
+
trackSeed?: number;
|
|
417
|
+
maxSteps?: number;
|
|
418
|
+
scenario?: PaddockEnvironmentOptions['scenario'];
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
export interface PaddockEvaluationDriverMetrics {
|
|
422
|
+
distanceMeters: number;
|
|
423
|
+
lapProgressMeters: number;
|
|
424
|
+
offTrackSteps: number;
|
|
425
|
+
contactCount: number;
|
|
426
|
+
recoverySuccess: boolean;
|
|
427
|
+
passCount: number;
|
|
428
|
+
lapTimeSeconds: number | null;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
export interface PaddockEvaluationCaseReport {
|
|
432
|
+
name: string;
|
|
433
|
+
seed?: number;
|
|
434
|
+
trackSeed?: number;
|
|
435
|
+
steps: number;
|
|
436
|
+
done: boolean;
|
|
437
|
+
endReason: string | null;
|
|
438
|
+
metrics: Record<string, PaddockEvaluationDriverMetrics>;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
export interface PaddockEvaluationReport {
|
|
442
|
+
cases: PaddockEvaluationCaseReport[];
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
export type PaddockPolicyLike =
|
|
446
|
+
| ((observation: PaddockDriverObservation, context: Record<string, unknown>) => PaddockAction)
|
|
447
|
+
| { predict(observation: PaddockDriverObservation, context: Record<string, unknown>): PaddockAction };
|
|
448
|
+
|
|
449
|
+
export interface PaddockEnvironmentWorkerMessage {
|
|
450
|
+
id?: string | number | null;
|
|
451
|
+
type: 'reset' | 'step' | 'getActionSpec' | 'getObservationSpec' | 'getObservation' | 'getState' | 'destroy' | string;
|
|
452
|
+
options?: Partial<PaddockEnvironmentOptions>;
|
|
453
|
+
actions?: PaddockActionMap;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
export interface PaddockEnvironmentWorkerResponse {
|
|
457
|
+
id: string | number | null;
|
|
458
|
+
ok: boolean;
|
|
459
|
+
type: string;
|
|
460
|
+
result?: unknown;
|
|
461
|
+
error?: string;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
export interface PaddockEnvironmentWorkerProtocol {
|
|
465
|
+
handle(message: PaddockEnvironmentWorkerMessage): PaddockEnvironmentWorkerResponse;
|
|
466
|
+
}
|
|
467
|
+
|
|
211
468
|
export interface PaddockEnvironmentResult {
|
|
212
469
|
observation: Record<string, PaddockDriverObservation>;
|
|
213
470
|
reward: null | Record<string, number>;
|
|
@@ -239,3 +496,26 @@ export interface PaddockEnvironment {
|
|
|
239
496
|
|
|
240
497
|
export function createPaddockEnvironment(options: PaddockEnvironmentOptions): PaddockEnvironment;
|
|
241
498
|
export function createProgressReward(options?: PaddockProgressRewardOptions): (context: PaddockRewardContext) => number;
|
|
499
|
+
export function createRolloutRecorder(): PaddockRolloutRecorder;
|
|
500
|
+
export function createRolloutTransition(
|
|
501
|
+
previousResult: PaddockEnvironmentResult,
|
|
502
|
+
action: PaddockActionMap,
|
|
503
|
+
nextResult: PaddockEnvironmentResult
|
|
504
|
+
): PaddockRolloutTransition;
|
|
505
|
+
export const ENVIRONMENT_SCENARIO_PRESETS: readonly PaddockScenarioPreset[];
|
|
506
|
+
export const DEFAULT_EVALUATION_CASES: readonly PaddockEvaluationCase[];
|
|
507
|
+
export function createEvaluationTracker(initialResult: PaddockEnvironmentResult): {
|
|
508
|
+
update(result: PaddockEnvironmentResult): void;
|
|
509
|
+
finish(): Record<string, PaddockEvaluationDriverMetrics>;
|
|
510
|
+
};
|
|
511
|
+
export function runEnvironmentEvaluation(options: {
|
|
512
|
+
baseOptions: PaddockEnvironmentOptions;
|
|
513
|
+
policy: PaddockPolicyLike;
|
|
514
|
+
cases?: PaddockEvaluationCase[];
|
|
515
|
+
createEnvironment?: (options: PaddockEnvironmentOptions) => PaddockEnvironment;
|
|
516
|
+
}): PaddockEvaluationReport;
|
|
517
|
+
export function createEnvironmentWorkerProtocol(env: PaddockEnvironment): PaddockEnvironmentWorkerProtocol;
|
|
518
|
+
export function handleEnvironmentMessage(
|
|
519
|
+
env: PaddockEnvironment,
|
|
520
|
+
message: PaddockEnvironmentWorkerMessage
|
|
521
|
+
): PaddockEnvironmentWorkerResponse;
|