@inventure71/paddockjs 0.1.0 → 0.3.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 +16 -0
- package/INSTALL_AND_UPDATE.md +163 -0
- package/README.md +96 -16
- package/docs/architecture.md +55 -11
- package/docs/concepts.md +5 -1
- package/docs/data_contract.md +324 -8
- package/docs/data_model.md +1 -0
- package/docs/index.md +1 -0
- package/docs/rules.md +2 -0
- package/docs/system_specs.md +66 -11
- package/docs/training.md +104 -0
- package/examples/train-basic-policy.mjs +277 -0
- package/package.json +14 -7
- package/src/api/PaddockSimulatorController.js +64 -3
- package/src/app/BrowserExpertAdapter.js +32 -0
- package/src/app/F1SimulatorApp.js +308 -38
- package/src/app/domBindings.js +35 -11
- package/src/config/defaultOptions.js +33 -0
- package/src/data/championship.js +13 -0
- package/src/data/normalizeDrivers.js +7 -2
- package/src/environment/actions.js +45 -0
- package/src/environment/episode.js +17 -0
- package/src/environment/events.js +17 -0
- package/src/environment/index.d.ts +241 -0
- package/src/environment/index.js +2 -0
- package/src/environment/observations.js +112 -0
- package/src/environment/options.js +150 -0
- package/src/environment/rewards.js +69 -0
- package/src/environment/runtime.js +140 -0
- package/src/environment/sensors.js +257 -0
- package/src/environment/specs.js +79 -0
- package/src/index.d.ts +97 -4
- package/src/index.js +12 -2
- package/src/rendering/proceduralTrackAsset.js +7 -1
- package/src/simulation/raceSimulation.js +214 -1
- package/src/simulation/trackModel.js +21 -0
- package/src/styles.css +495 -2
- package/src/ui/componentTemplates.js +201 -4
- package/AGENTS.md +0 -86
- package/src/standalone.js +0 -15
|
@@ -18,7 +18,14 @@ export const DEFAULT_F1_SIMULATOR_OPTIONS = {
|
|
|
18
18
|
showTimingTower: true,
|
|
19
19
|
showTelemetry: true,
|
|
20
20
|
telemetryIncludesOverview: true,
|
|
21
|
+
telemetryModules: {
|
|
22
|
+
core: true,
|
|
23
|
+
sectors: true,
|
|
24
|
+
lapTimes: true,
|
|
25
|
+
sectorTimes: true,
|
|
26
|
+
},
|
|
21
27
|
showRaceDataPanel: true,
|
|
28
|
+
raceDataTelemetryDetail: false,
|
|
22
29
|
raceDataBanners: {
|
|
23
30
|
initial: 'project',
|
|
24
31
|
enabled: ['project', 'radio'],
|
|
@@ -103,6 +110,8 @@ export const PADDOCK_THEME_CSS_VARIABLES = {
|
|
|
103
110
|
raceViewMinHeight: '--paddock-race-view-min-height',
|
|
104
111
|
};
|
|
105
112
|
|
|
113
|
+
const SUPPORTED_CAMERA_MODES = new Set(['overview', 'leader', 'selected', 'show-all']);
|
|
114
|
+
|
|
106
115
|
export function resolveF1SimulatorOptions(options = {}) {
|
|
107
116
|
const presetName = Object.hasOwn(PADDOCK_SIMULATOR_PRESETS, options.preset)
|
|
108
117
|
? options.preset
|
|
@@ -121,6 +130,7 @@ export function resolveF1SimulatorOptions(options = {}) {
|
|
|
121
130
|
},
|
|
122
131
|
};
|
|
123
132
|
ui.raceDataBanners.enabled = normalizeEnabledBanners(ui.raceDataBanners.enabled);
|
|
133
|
+
ui.telemetryModules = normalizeTelemetryModules(ui.telemetryModules);
|
|
124
134
|
if (!['project', 'radio', 'hidden'].includes(ui.raceDataBanners.initial)) {
|
|
125
135
|
ui.raceDataBanners.initial = DEFAULT_F1_SIMULATOR_OPTIONS.ui.raceDataBanners.initial;
|
|
126
136
|
}
|
|
@@ -133,6 +143,10 @@ export function resolveF1SimulatorOptions(options = {}) {
|
|
|
133
143
|
if (!['auto', 'custom'].includes(ui.raceDataBannerSize)) {
|
|
134
144
|
ui.raceDataBannerSize = DEFAULT_F1_SIMULATOR_OPTIONS.ui.raceDataBannerSize;
|
|
135
145
|
}
|
|
146
|
+
ui.raceDataTelemetryDetail = Boolean(ui.raceDataTelemetryDetail);
|
|
147
|
+
const initialCameraMode = SUPPORTED_CAMERA_MODES.has(options.initialCameraMode)
|
|
148
|
+
? options.initialCameraMode
|
|
149
|
+
: DEFAULT_F1_SIMULATOR_OPTIONS.initialCameraMode;
|
|
136
150
|
const mergedOptions = {
|
|
137
151
|
...DEFAULT_F1_SIMULATOR_OPTIONS,
|
|
138
152
|
...preset,
|
|
@@ -152,6 +166,7 @@ export function resolveF1SimulatorOptions(options = {}) {
|
|
|
152
166
|
...preset,
|
|
153
167
|
...options,
|
|
154
168
|
preset: presetName ?? options.preset,
|
|
169
|
+
initialCameraMode,
|
|
155
170
|
ui,
|
|
156
171
|
theme,
|
|
157
172
|
drivers,
|
|
@@ -174,6 +189,24 @@ function normalizeEnabledBanners(value) {
|
|
|
174
189
|
return [...new Set(value.filter((item) => item === 'project' || item === 'radio'))];
|
|
175
190
|
}
|
|
176
191
|
|
|
192
|
+
function normalizeTelemetryModules(value) {
|
|
193
|
+
const defaults = DEFAULT_F1_SIMULATOR_OPTIONS.ui.telemetryModules;
|
|
194
|
+
const names = Object.keys(defaults);
|
|
195
|
+
if (value === false) {
|
|
196
|
+
return Object.fromEntries(names.map((name) => [name, false]));
|
|
197
|
+
}
|
|
198
|
+
if (value === true || value == null) return { ...defaults };
|
|
199
|
+
if (Array.isArray(value)) {
|
|
200
|
+
const requested = new Set(value);
|
|
201
|
+
return Object.fromEntries(names.map((name) => [name, requested.has(name)]));
|
|
202
|
+
}
|
|
203
|
+
if (typeof value !== 'object') return { ...defaults };
|
|
204
|
+
return Object.fromEntries(names.map((name) => [
|
|
205
|
+
name,
|
|
206
|
+
value[name] == null ? defaults[name] : Boolean(value[name]),
|
|
207
|
+
]));
|
|
208
|
+
}
|
|
209
|
+
|
|
177
210
|
function normalizeTheme(theme) {
|
|
178
211
|
return Object.fromEntries(
|
|
179
212
|
Object.entries(DEFAULT_F1_SIMULATOR_OPTIONS.theme).map(([key, fallback]) => [
|
package/src/data/championship.js
CHANGED
|
@@ -136,6 +136,7 @@ function pickUniqueTimingCode(source, usedCodes) {
|
|
|
136
136
|
function assertUniqueDriverNumbers(entries) {
|
|
137
137
|
const seen = new Set();
|
|
138
138
|
entries.forEach((entry) => {
|
|
139
|
+
if (entry.driverNumber == null) return;
|
|
139
140
|
if (seen.has(entry.driverNumber)) {
|
|
140
141
|
throw new Error(`Duplicate championship driver number: ${entry.driverNumber}`);
|
|
141
142
|
}
|
|
@@ -143,6 +144,17 @@ function assertUniqueDriverNumbers(entries) {
|
|
|
143
144
|
});
|
|
144
145
|
}
|
|
145
146
|
|
|
147
|
+
function assertUniqueEntryDriverIds(entries) {
|
|
148
|
+
const seen = new Set();
|
|
149
|
+
entries.forEach((entry) => {
|
|
150
|
+
if (entry.driverId == null) return;
|
|
151
|
+
if (seen.has(entry.driverId)) {
|
|
152
|
+
throw new Error(`Duplicate championship entry driver id: ${entry.driverId}`);
|
|
153
|
+
}
|
|
154
|
+
seen.add(entry.driverId);
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
146
158
|
function buildDriverConstructorArgs(entry) {
|
|
147
159
|
const driver = entry.driver instanceof DriverData
|
|
148
160
|
? entry.driver
|
|
@@ -181,6 +193,7 @@ function normalizeTeam(team, driver, timingCode) {
|
|
|
181
193
|
}
|
|
182
194
|
|
|
183
195
|
export function buildChampionshipDriverGrid(drivers = DEMO_PROJECT_DRIVERS, entries = CHAMPIONSHIP_DRIVER_ENTRIES) {
|
|
196
|
+
assertUniqueEntryDriverIds(entries);
|
|
184
197
|
assertUniqueDriverNumbers(entries);
|
|
185
198
|
|
|
186
199
|
const entryByDriverId = new Map(entries.map((entry) => [entry.driverId, entry]));
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { buildChampionshipDriverGrid } from './championship.js';
|
|
2
2
|
|
|
3
|
-
export function normalizeSimulatorDrivers(drivers, { entries } = {}) {
|
|
3
|
+
export function normalizeSimulatorDrivers(drivers, { entries, caller = 'mountF1Simulator' } = {}) {
|
|
4
4
|
if (!Array.isArray(drivers) || drivers.length === 0) {
|
|
5
|
-
throw new Error(
|
|
5
|
+
throw new Error(`${caller} requires a non-empty drivers array.`);
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
+
const seenDriverIds = new Set();
|
|
8
9
|
drivers.forEach((driver, index) => {
|
|
9
10
|
if (!driver || typeof driver !== 'object') {
|
|
10
11
|
throw new Error(`Invalid simulator driver at index ${index}.`);
|
|
@@ -12,6 +13,10 @@ export function normalizeSimulatorDrivers(drivers, { entries } = {}) {
|
|
|
12
13
|
if (!driver.id) {
|
|
13
14
|
throw new Error(`Simulator driver at index ${index} is missing an id.`);
|
|
14
15
|
}
|
|
16
|
+
if (seenDriverIds.has(driver.id)) {
|
|
17
|
+
throw new Error(`Duplicate simulator driver id: ${driver.id}`);
|
|
18
|
+
}
|
|
19
|
+
seenDriverIds.add(driver.id);
|
|
15
20
|
if (!driver.name) {
|
|
16
21
|
throw new Error(`Simulator driver "${driver.id}" is missing a name.`);
|
|
17
22
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { clamp } from '../simulation/simMath.js';
|
|
2
|
+
import { VEHICLE_LIMITS } from '../simulation/vehiclePhysics.js';
|
|
3
|
+
|
|
4
|
+
export function resolveActionMap(actions = {}, controlledDrivers = [], { policy = 'strict' } = {}) {
|
|
5
|
+
const errors = [];
|
|
6
|
+
const controlsByDriver = {};
|
|
7
|
+
|
|
8
|
+
controlledDrivers.forEach((driverId) => {
|
|
9
|
+
const action = actions?.[driverId];
|
|
10
|
+
if (!action || typeof action !== 'object') {
|
|
11
|
+
handleActionError(`Missing action for controlled driver: ${driverId}`, policy, errors);
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
const normalized = normalizeAction(action, driverId, policy, errors);
|
|
15
|
+
if (normalized) controlsByDriver[driverId] = normalized;
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
return { controlsByDriver, errors };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function normalizeAction(action, driverId = 'unknown', policy = 'strict', errors = []) {
|
|
22
|
+
const steering = finiteActionValue(action.steering, `Invalid steering action for controlled driver: ${driverId}`, policy, errors);
|
|
23
|
+
const throttle = finiteActionValue(action.throttle, `Invalid throttle action for controlled driver: ${driverId}`, policy, errors);
|
|
24
|
+
const brake = finiteActionValue(action.brake, `Invalid brake action for controlled driver: ${driverId}`, policy, errors);
|
|
25
|
+
|
|
26
|
+
if (steering == null || throttle == null || brake == null) return null;
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
steering: clamp(steering, -1, 1) * VEHICLE_LIMITS.maxSteer,
|
|
30
|
+
throttle: clamp(throttle, 0, 1),
|
|
31
|
+
brake: clamp(brake, 0, 1),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function finiteActionValue(value, message, policy, errors) {
|
|
36
|
+
const number = Number(value ?? 0);
|
|
37
|
+
if (Number.isFinite(number)) return number;
|
|
38
|
+
handleActionError(message, policy, errors);
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function handleActionError(message, policy, errors) {
|
|
43
|
+
if (policy === 'strict') throw new Error(message);
|
|
44
|
+
errors.push(message);
|
|
45
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export function createEpisodeState() {
|
|
2
|
+
return {
|
|
3
|
+
step: 0,
|
|
4
|
+
previousSnapshot: null,
|
|
5
|
+
lastResult: null,
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function evaluateEpisode(snapshot, options, episodeState) {
|
|
10
|
+
if (snapshot.raceControl.finished && options.episode.endOnRaceFinish) {
|
|
11
|
+
return { terminated: true, truncated: false, endReason: 'race-finish' };
|
|
12
|
+
}
|
|
13
|
+
if (episodeState.step >= options.episode.maxSteps) {
|
|
14
|
+
return { terminated: false, truncated: true, endReason: 'max-steps' };
|
|
15
|
+
}
|
|
16
|
+
return { terminated: false, truncated: false, endReason: null };
|
|
17
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export function collectStepEvents(events = []) {
|
|
2
|
+
return events.map((event) => normalizeEnvironmentEvent(event));
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
function normalizeEnvironmentEvent(event) {
|
|
6
|
+
if (event.type === 'contact') {
|
|
7
|
+
return {
|
|
8
|
+
...event,
|
|
9
|
+
type: 'collision',
|
|
10
|
+
driverIds: [event.carId, event.otherCarId].filter(Boolean),
|
|
11
|
+
primaryDriverId: event.carId ?? null,
|
|
12
|
+
otherDriverId: event.otherCarId ?? null,
|
|
13
|
+
severity: event.severity ?? null,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
return { ...event };
|
|
17
|
+
}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
export type TireCompound = 'S' | 'M' | 'H';
|
|
2
|
+
|
|
3
|
+
export interface TeamData {
|
|
4
|
+
id?: string;
|
|
5
|
+
name?: string;
|
|
6
|
+
color?: string;
|
|
7
|
+
icon?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface SimulatorDriver {
|
|
11
|
+
id: string;
|
|
12
|
+
name: string;
|
|
13
|
+
color: string;
|
|
14
|
+
link?: string;
|
|
15
|
+
icon?: string;
|
|
16
|
+
code?: string;
|
|
17
|
+
timingName?: string;
|
|
18
|
+
tire?: TireCompound;
|
|
19
|
+
raceData?: string[];
|
|
20
|
+
customFields?: Array<{ label: string; value: string }> | Record<string, string>;
|
|
21
|
+
team?: TeamData | null;
|
|
22
|
+
driverNumber?: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface ChampionshipEntryBlueprint {
|
|
26
|
+
driverId: string;
|
|
27
|
+
driverNumber?: number;
|
|
28
|
+
timingName?: string;
|
|
29
|
+
driver?: unknown;
|
|
30
|
+
vehicle?: unknown;
|
|
31
|
+
team?: TeamData;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface RaceEvent {
|
|
35
|
+
type: string;
|
|
36
|
+
at?: number;
|
|
37
|
+
[key: string]: unknown;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface RaceSnapshot {
|
|
41
|
+
time: number;
|
|
42
|
+
totalLaps: number;
|
|
43
|
+
raceControl: {
|
|
44
|
+
mode: string;
|
|
45
|
+
finished: boolean;
|
|
46
|
+
[key: string]: unknown;
|
|
47
|
+
};
|
|
48
|
+
track: Record<string, unknown>;
|
|
49
|
+
cars: Array<Record<string, unknown>>;
|
|
50
|
+
events: RaceEvent[];
|
|
51
|
+
[key: string]: unknown;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface PaddockAction {
|
|
55
|
+
steering: number;
|
|
56
|
+
throttle: number;
|
|
57
|
+
brake: number;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export type PaddockActionMap = Record<string, PaddockAction>;
|
|
61
|
+
|
|
62
|
+
export interface PaddockSensorRayResult {
|
|
63
|
+
angleDegrees: number;
|
|
64
|
+
angleRadians: number;
|
|
65
|
+
lengthMeters: number;
|
|
66
|
+
track: {
|
|
67
|
+
hit: boolean;
|
|
68
|
+
distanceMeters: number;
|
|
69
|
+
kind: 'exit' | 'entry' | null;
|
|
70
|
+
};
|
|
71
|
+
car: {
|
|
72
|
+
hit: boolean;
|
|
73
|
+
distanceMeters: number;
|
|
74
|
+
driverId: string | null;
|
|
75
|
+
relativeSpeedKph: number;
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface PaddockNearbyCarObservation {
|
|
80
|
+
id: string;
|
|
81
|
+
relativeForwardMeters: number;
|
|
82
|
+
relativeRightMeters: number;
|
|
83
|
+
relativeDistanceMeters: number;
|
|
84
|
+
relativeSpeedKph: number;
|
|
85
|
+
relativeHeadingRadians: number;
|
|
86
|
+
ahead: boolean;
|
|
87
|
+
sameLap: boolean;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface PaddockDriverObservationObject {
|
|
91
|
+
self: {
|
|
92
|
+
id: string;
|
|
93
|
+
speedKph: number;
|
|
94
|
+
speedMetersPerSecond: number;
|
|
95
|
+
headingRadians: number;
|
|
96
|
+
steeringAngleRadians: number;
|
|
97
|
+
throttle: number;
|
|
98
|
+
brake: number;
|
|
99
|
+
lap: number;
|
|
100
|
+
completedLaps: number;
|
|
101
|
+
lapProgressMeters: number;
|
|
102
|
+
trackOffsetMeters: number;
|
|
103
|
+
trackHeadingErrorRadians: number;
|
|
104
|
+
onTrack: boolean;
|
|
105
|
+
surface: string;
|
|
106
|
+
tireEnergy: number | null;
|
|
107
|
+
};
|
|
108
|
+
race: {
|
|
109
|
+
position: number;
|
|
110
|
+
totalCars: number;
|
|
111
|
+
raceMode: string;
|
|
112
|
+
totalLaps: number;
|
|
113
|
+
};
|
|
114
|
+
rays: PaddockSensorRayResult[];
|
|
115
|
+
nearbyCars: PaddockNearbyCarObservation[];
|
|
116
|
+
events: RaceEvent[];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export interface PaddockObservationSchemaEntry {
|
|
120
|
+
name: string;
|
|
121
|
+
unit?: string;
|
|
122
|
+
scale?: string;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export interface PaddockDriverObservation {
|
|
126
|
+
object: PaddockDriverObservationObject;
|
|
127
|
+
vector: number[];
|
|
128
|
+
schema: PaddockObservationSchemaEntry[];
|
|
129
|
+
events: RaceEvent[];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export interface PaddockEnvironmentOptions {
|
|
133
|
+
drivers: SimulatorDriver[];
|
|
134
|
+
entries?: ChampionshipEntryBlueprint[];
|
|
135
|
+
controlledDrivers: string[];
|
|
136
|
+
seed?: number;
|
|
137
|
+
trackSeed?: number;
|
|
138
|
+
totalLaps?: number;
|
|
139
|
+
frameSkip?: number;
|
|
140
|
+
actionPolicy?: 'strict' | 'report';
|
|
141
|
+
scenario?: {
|
|
142
|
+
participants?: 'all' | 'controlled-only' | string[];
|
|
143
|
+
nonControlled?: 'ai';
|
|
144
|
+
};
|
|
145
|
+
sensors?: {
|
|
146
|
+
rays?: {
|
|
147
|
+
enabled?: boolean;
|
|
148
|
+
anglesDegrees?: number[];
|
|
149
|
+
lengthMeters?: number;
|
|
150
|
+
detectTrack?: boolean;
|
|
151
|
+
detectCars?: boolean;
|
|
152
|
+
};
|
|
153
|
+
nearbyCars?: {
|
|
154
|
+
enabled?: boolean;
|
|
155
|
+
maxCars?: number;
|
|
156
|
+
radiusMeters?: number;
|
|
157
|
+
};
|
|
158
|
+
};
|
|
159
|
+
sensorsByDriver?: Record<string, PaddockEnvironmentOptions['sensors']>;
|
|
160
|
+
episode?: {
|
|
161
|
+
maxSteps?: number;
|
|
162
|
+
endOnRaceFinish?: boolean;
|
|
163
|
+
};
|
|
164
|
+
reward?: (payload: PaddockRewardContext) => number;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export interface PaddockRewardContext {
|
|
168
|
+
driverId: string;
|
|
169
|
+
previous: RaceSnapshot | null;
|
|
170
|
+
current: PaddockDriverObservation;
|
|
171
|
+
action: PaddockAction | undefined;
|
|
172
|
+
events: RaceEvent[];
|
|
173
|
+
state: { snapshot: RaceSnapshot };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export interface PaddockProgressRewardWeights {
|
|
177
|
+
progress: number;
|
|
178
|
+
speed: number;
|
|
179
|
+
offTrack: number;
|
|
180
|
+
collision: number;
|
|
181
|
+
steering: number;
|
|
182
|
+
brake: number;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export interface PaddockProgressRewardOptions {
|
|
186
|
+
weights?: Partial<PaddockProgressRewardWeights>;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export interface PaddockActionSpec {
|
|
190
|
+
version: 1;
|
|
191
|
+
controlledDrivers: string[];
|
|
192
|
+
action: {
|
|
193
|
+
type: 'continuous';
|
|
194
|
+
perDriver: {
|
|
195
|
+
steering: { min: -1; max: 1; unit: 'normalized' };
|
|
196
|
+
throttle: { min: 0; max: 1; unit: 'normalized' };
|
|
197
|
+
brake: { min: 0; max: 1; unit: 'normalized' };
|
|
198
|
+
};
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export interface PaddockObservationSpec {
|
|
203
|
+
version: 1;
|
|
204
|
+
controlledDrivers: string[];
|
|
205
|
+
object: Record<string, unknown>;
|
|
206
|
+
vector: {
|
|
207
|
+
schema: PaddockObservationSchemaEntry[];
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export interface PaddockEnvironmentResult {
|
|
212
|
+
observation: Record<string, PaddockDriverObservation>;
|
|
213
|
+
reward: null | Record<string, number>;
|
|
214
|
+
terminated: boolean;
|
|
215
|
+
truncated: boolean;
|
|
216
|
+
done: boolean;
|
|
217
|
+
events: RaceEvent[];
|
|
218
|
+
state: { snapshot: RaceSnapshot };
|
|
219
|
+
info: {
|
|
220
|
+
step: number;
|
|
221
|
+
elapsedSeconds: number;
|
|
222
|
+
seed: number;
|
|
223
|
+
trackSeed: number;
|
|
224
|
+
controlledDrivers: string[];
|
|
225
|
+
actionErrors: string[];
|
|
226
|
+
endReason: string | null;
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export interface PaddockEnvironment {
|
|
231
|
+
reset(options?: Partial<PaddockEnvironmentOptions>): PaddockEnvironmentResult;
|
|
232
|
+
step(actions: PaddockActionMap): PaddockEnvironmentResult;
|
|
233
|
+
getObservation(): PaddockEnvironmentResult['observation'];
|
|
234
|
+
getState(): PaddockEnvironmentResult['state'];
|
|
235
|
+
getActionSpec(): PaddockActionSpec;
|
|
236
|
+
getObservationSpec(): PaddockObservationSpec;
|
|
237
|
+
destroy(): void;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export function createPaddockEnvironment(options: PaddockEnvironmentOptions): PaddockEnvironment;
|
|
241
|
+
export function createProgressReward(options?: PaddockProgressRewardOptions): (context: PaddockRewardContext) => number;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { simSpeedToMetersPerSecond, simUnitsToMeters } from '../simulation/units.js';
|
|
2
|
+
import { buildNearbyCars, buildRaySensors } from './sensors.js';
|
|
3
|
+
|
|
4
|
+
export function buildEnvironmentObservation({ snapshot, options, events = [] }) {
|
|
5
|
+
return Object.fromEntries(options.controlledDrivers.map((driverId) => {
|
|
6
|
+
const car = snapshot.cars.find((entry) => entry.id === driverId);
|
|
7
|
+
if (!car) return [driverId, emptyObservation(driverId)];
|
|
8
|
+
|
|
9
|
+
const driverEvents = events.filter((event) =>
|
|
10
|
+
event.driverId === driverId ||
|
|
11
|
+
event.carId === driverId ||
|
|
12
|
+
event.otherCarId === driverId ||
|
|
13
|
+
event.driverIds?.includes?.(driverId));
|
|
14
|
+
const object = buildDriverObservationObject(car, snapshot, options, driverEvents);
|
|
15
|
+
const { vector, schema } = buildDriverVector(object);
|
|
16
|
+
return [driverId, { object, vector, schema, events: driverEvents }];
|
|
17
|
+
}));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function buildDriverObservationObject(car, snapshot, options, events) {
|
|
21
|
+
const sensors = effectiveSensorOptions(options, car.id);
|
|
22
|
+
return {
|
|
23
|
+
self: {
|
|
24
|
+
id: car.id,
|
|
25
|
+
speedKph: car.speedKph,
|
|
26
|
+
speedMetersPerSecond: simSpeedToMetersPerSecond(car.speed ?? 0),
|
|
27
|
+
headingRadians: car.heading,
|
|
28
|
+
steeringAngleRadians: car.steeringAngle ?? 0,
|
|
29
|
+
throttle: car.throttle ?? 0,
|
|
30
|
+
brake: car.brake ?? 0,
|
|
31
|
+
lap: car.lap,
|
|
32
|
+
completedLaps: car.lapTelemetry?.completedLaps ?? 0,
|
|
33
|
+
lapProgressMeters: simUnitsToMeters(car.progress ?? 0),
|
|
34
|
+
trackOffsetMeters: simUnitsToMeters(car.signedOffset ?? 0),
|
|
35
|
+
trackHeadingErrorRadians: car.trackHeadingError ?? 0,
|
|
36
|
+
onTrack: (car.surface ?? 'track') === 'track',
|
|
37
|
+
surface: car.surface ?? 'track',
|
|
38
|
+
tireEnergy: car.tireEnergy ?? null,
|
|
39
|
+
},
|
|
40
|
+
race: {
|
|
41
|
+
position: car.rank,
|
|
42
|
+
totalCars: snapshot.cars.length,
|
|
43
|
+
raceMode: snapshot.raceControl.mode,
|
|
44
|
+
totalLaps: snapshot.totalLaps,
|
|
45
|
+
},
|
|
46
|
+
rays: sensors.rays.enabled ? buildRaySensors(car, snapshot, sensors.rays) : [],
|
|
47
|
+
nearbyCars: sensors.nearbyCars.enabled ? buildNearbyCars(car, snapshot, sensors.nearbyCars) : [],
|
|
48
|
+
events,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function buildDriverVector(object) {
|
|
53
|
+
const schema = [
|
|
54
|
+
{ name: 'self.speedKph', unit: 'kph', scale: 'fixed:400' },
|
|
55
|
+
{ name: 'self.trackOffsetMeters', unit: 'm', scale: 'fixed:meters' },
|
|
56
|
+
{ name: 'self.trackHeadingErrorRadians', unit: 'rad', scale: 'fixed:pi' },
|
|
57
|
+
{ name: 'self.onTrack', scale: 'boolean' },
|
|
58
|
+
{ name: 'race.position', scale: 'fixed:field-position' },
|
|
59
|
+
];
|
|
60
|
+
const vector = [
|
|
61
|
+
object.self.speedKph / 400,
|
|
62
|
+
object.self.trackOffsetMeters,
|
|
63
|
+
object.self.trackHeadingErrorRadians / Math.PI,
|
|
64
|
+
object.self.onTrack ? 1 : 0,
|
|
65
|
+
object.race.position,
|
|
66
|
+
];
|
|
67
|
+
return { vector, schema };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function effectiveSensorOptions(options, driverId) {
|
|
71
|
+
return {
|
|
72
|
+
rays: {
|
|
73
|
+
...options.sensors.rays,
|
|
74
|
+
...(options.sensorsByDriver?.[driverId]?.rays ?? {}),
|
|
75
|
+
},
|
|
76
|
+
nearbyCars: {
|
|
77
|
+
...options.sensors.nearbyCars,
|
|
78
|
+
...(options.sensorsByDriver?.[driverId]?.nearbyCars ?? {}),
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function emptyObservation(driverId) {
|
|
84
|
+
return {
|
|
85
|
+
object: {
|
|
86
|
+
self: {
|
|
87
|
+
id: driverId,
|
|
88
|
+
speedKph: 0,
|
|
89
|
+
speedMetersPerSecond: 0,
|
|
90
|
+
headingRadians: 0,
|
|
91
|
+
steeringAngleRadians: 0,
|
|
92
|
+
throttle: 0,
|
|
93
|
+
brake: 0,
|
|
94
|
+
lap: 0,
|
|
95
|
+
completedLaps: 0,
|
|
96
|
+
lapProgressMeters: 0,
|
|
97
|
+
trackOffsetMeters: 0,
|
|
98
|
+
trackHeadingErrorRadians: 0,
|
|
99
|
+
onTrack: false,
|
|
100
|
+
surface: 'missing',
|
|
101
|
+
tireEnergy: null,
|
|
102
|
+
},
|
|
103
|
+
race: { position: 0, totalCars: 0, raceMode: 'missing', totalLaps: 0 },
|
|
104
|
+
rays: [],
|
|
105
|
+
nearbyCars: [],
|
|
106
|
+
events: [],
|
|
107
|
+
},
|
|
108
|
+
vector: [],
|
|
109
|
+
schema: [],
|
|
110
|
+
events: [],
|
|
111
|
+
};
|
|
112
|
+
}
|