@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.
Files changed (54) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/INSTALL_AND_UPDATE.md +3 -1
  3. package/README.md +142 -9
  4. package/docs/architecture.md +49 -15
  5. package/docs/concepts.md +20 -3
  6. package/docs/data_contract.md +185 -27
  7. package/docs/data_model.md +1 -1
  8. package/docs/learnings.md +1 -1
  9. package/docs/rules.md +140 -19
  10. package/docs/system_specs.md +39 -16
  11. package/docs/training.md +103 -1
  12. package/examples/train-basic-policy.mjs +5 -6
  13. package/examples/trainingData.mjs +133 -0
  14. package/package.json +5 -2
  15. package/src/api/PaddockSimulatorController.js +32 -2
  16. package/src/app/F1SimulatorApp.js +1101 -128
  17. package/src/app/domBindings.js +10 -1
  18. package/src/config/defaultOptions.js +4 -4
  19. package/src/environment/actions.js +25 -2
  20. package/src/environment/evaluation.js +184 -0
  21. package/src/environment/index.d.ts +281 -1
  22. package/src/environment/index.js +13 -0
  23. package/src/environment/observationOptions.js +12 -0
  24. package/src/environment/observations.js +189 -17
  25. package/src/environment/options.js +7 -0
  26. package/src/environment/recorder.js +28 -0
  27. package/src/environment/runtime.js +41 -6
  28. package/src/environment/scenarios.js +220 -0
  29. package/src/environment/sensors.js +145 -35
  30. package/src/environment/specs.js +92 -8
  31. package/src/environment/workerProtocol.js +52 -0
  32. package/src/index.d.ts +366 -3
  33. package/src/index.js +21 -0
  34. package/src/rendering/proceduralTrackAsset.js +336 -23
  35. package/src/simulation/collisionGeometry.js +214 -0
  36. package/src/simulation/driverController.js +431 -65
  37. package/src/simulation/raceSimulation.js +2132 -171
  38. package/src/simulation/rules/collisionSteward.js +69 -0
  39. package/src/simulation/rules/penaltyLedger.js +153 -0
  40. package/src/simulation/rules/pitLaneSpeedingSteward.js +71 -0
  41. package/src/simulation/rules/tireRequirementSteward.js +29 -0
  42. package/src/simulation/rules/trackLimitsSteward.js +85 -0
  43. package/src/simulation/rulesConfig.js +352 -0
  44. package/src/simulation/trackModel.js +1154 -76
  45. package/src/simulation/units.js +6 -4
  46. package/src/simulation/vehicleGeometry.js +247 -0
  47. package/src/simulation/vehiclePhysics.js +82 -50
  48. package/src/simulation/wheelSurface.js +338 -0
  49. package/src/styles.css +300 -42
  50. package/src/ui/bannerTemplates.js +69 -0
  51. package/src/ui/componentTemplates.js +36 -96
  52. package/src/ui/raceControlStatusBanner.js +39 -0
  53. package/src/ui/shellTemplate.js +1 -1
  54. package/src/ui/templateUtils.js +23 -0
@@ -0,0 +1,338 @@
1
+ import { nearestTrackState } from './trackModel.js';
2
+ import { metersToSimUnits } from './units.js';
3
+ import { getVehicleGeometryState } from './vehicleGeometry.js';
4
+
5
+ const SURFACE_PRIORITY = {
6
+ track: 0,
7
+ 'pit-entry': 1,
8
+ 'pit-exit': 1,
9
+ 'pit-lane': 2,
10
+ 'pit-box': 3,
11
+ kerb: 4,
12
+ grass: 5,
13
+ gravel: 6,
14
+ barrier: 7,
15
+ };
16
+ const PIT_CONNECTOR_FULL_SAMPLE_WINDOW = metersToSimUnits(35);
17
+
18
+ function finiteSignatureValue(value) {
19
+ return Number.isFinite(value) ? Number(value).toFixed(4) : '';
20
+ }
21
+
22
+ function centerStateSignature(centerState) {
23
+ if (!centerState) return 'auto';
24
+ return [
25
+ finiteSignatureValue(centerState.distance),
26
+ finiteSignatureValue(centerState.signedOffset),
27
+ finiteSignatureValue(centerState.crossTrackError),
28
+ centerState.surface ?? '',
29
+ centerState.inPitLane ? 1 : 0,
30
+ centerState.pitLanePart ?? '',
31
+ centerState.pitBoxId ?? '',
32
+ ].join(':');
33
+ }
34
+
35
+ function priority(surface) {
36
+ return SURFACE_PRIORITY[surface] ?? SURFACE_PRIORITY.barrier;
37
+ }
38
+
39
+ function worstState(states) {
40
+ return states.reduce((worst, state) => (
41
+ priority(state.surface) > priority(worst.surface) ? state : worst
42
+ ), states[0]);
43
+ }
44
+
45
+ function patchSamples(patch) {
46
+ return [patch.center, ...patch.corners];
47
+ }
48
+
49
+ function wrappedDistanceDelta(first, second, length) {
50
+ if (!Number.isFinite(first) || !Number.isFinite(second) || !Number.isFinite(length) || length <= 0) return Infinity;
51
+ const delta = Math.abs(first - second);
52
+ return Math.min(delta, length - delta);
53
+ }
54
+
55
+ function isNearPitConnector(track, centerState) {
56
+ const pitLane = track.pitLane;
57
+ if (!pitLane?.enabled) return false;
58
+ const distance = centerState.distance;
59
+ const entryDistance = pitLane.entry?.trackDistance ?? pitLane.entry?.distanceFromStart;
60
+ const exitDistance = pitLane.exit?.trackDistance ?? pitLane.exit?.distanceFromStart;
61
+ return wrappedDistanceDelta(distance, entryDistance, track.length) <= PIT_CONNECTOR_FULL_SAMPLE_WINDOW ||
62
+ wrappedDistanceDelta(distance, exitDistance, track.length) <= PIT_CONNECTOR_FULL_SAMPLE_WINDOW;
63
+ }
64
+
65
+ function mainTrackSurface(track, crossTrackError) {
66
+ const trackEdge = track.width / 2;
67
+ const kerbEdge = trackEdge + (track.kerbWidth ?? 0);
68
+ const gravelEdge = kerbEdge + track.gravelWidth;
69
+ const runoffEdge = gravelEdge + track.runoffWidth;
70
+ if (crossTrackError <= trackEdge) return 'track';
71
+ if (crossTrackError <= kerbEdge) return 'kerb';
72
+ if (crossTrackError <= gravelEdge) return 'gravel';
73
+ if (crossTrackError <= runoffEdge) return 'grass';
74
+ return 'barrier';
75
+ }
76
+
77
+ function localTrackState(track, centerState, point) {
78
+ const signedOffset =
79
+ (point.x - centerState.x) * centerState.normalX +
80
+ (point.y - centerState.y) * centerState.normalY;
81
+ const crossTrackError = Math.abs(signedOffset);
82
+ const surface = mainTrackSurface(track, crossTrackError);
83
+ return {
84
+ ...centerState,
85
+ signedOffset,
86
+ crossTrackError,
87
+ surface,
88
+ onTrack: surface === 'track' || surface === 'kerb',
89
+ inPitLane: false,
90
+ pitLanePart: null,
91
+ pitBoxId: null,
92
+ };
93
+ }
94
+
95
+ function wheelFullyOutside(samples, trackLimit) {
96
+ const rightOutside = samples.every((state) => !state.inPitLane && state.signedOffset > trackLimit);
97
+ const leftOutside = samples.every((state) => !state.inPitLane && state.signedOffset < -trackLimit);
98
+ return {
99
+ rightOutside,
100
+ leftOutside,
101
+ fullyOutsideWhiteLine: rightOutside || leftOutside,
102
+ outsideSide: rightOutside ? 1 : leftOutside ? -1 : 0,
103
+ };
104
+ }
105
+
106
+ function wheelOutsideFromOffsets(minimumSignedOffset, maximumSignedOffset, trackLimit) {
107
+ const rightOutside = minimumSignedOffset > trackLimit;
108
+ const leftOutside = maximumSignedOffset < -trackLimit;
109
+ return {
110
+ rightOutside,
111
+ leftOutside,
112
+ fullyOutsideWhiteLine: rightOutside || leftOutside,
113
+ outsideSide: rightOutside ? 1 : leftOutside ? -1 : 0,
114
+ };
115
+ }
116
+
117
+ function stateFromSignedOffset(centerState, track, signedOffset) {
118
+ const crossTrackError = Math.abs(signedOffset);
119
+ const surface = mainTrackSurface(track, crossTrackError);
120
+ return {
121
+ ...centerState,
122
+ signedOffset,
123
+ crossTrackError,
124
+ surface,
125
+ onTrack: surface === 'track' || surface === 'kerb',
126
+ inPitLane: false,
127
+ pitLanePart: null,
128
+ pitBoxId: null,
129
+ };
130
+ }
131
+
132
+ function analyticWheelState(patch, centerState, track, trackLimit) {
133
+ const wheelCenterOffset =
134
+ (patch.center.x - centerState.x) * centerState.normalX +
135
+ (patch.center.y - centerState.y) * centerState.normalY;
136
+ const projectedHalfWidth =
137
+ Math.abs(patch.forward.x * centerState.normalX + patch.forward.y * centerState.normalY) * patch.halfLength +
138
+ Math.abs(patch.right.x * centerState.normalX + patch.right.y * centerState.normalY) * patch.halfWidth;
139
+ const minimumSignedOffset = wheelCenterOffset - projectedHalfWidth;
140
+ const maximumSignedOffset = wheelCenterOffset + projectedHalfWidth;
141
+ const signedOffset = Math.abs(minimumSignedOffset) > Math.abs(maximumSignedOffset)
142
+ ? minimumSignedOffset
143
+ : maximumSignedOffset;
144
+ const state = stateFromSignedOffset(centerState, track, signedOffset);
145
+ const outside = wheelOutsideFromOffsets(minimumSignedOffset, maximumSignedOffset, trackLimit);
146
+
147
+ return {
148
+ id: patch.id,
149
+ x: patch.center.x,
150
+ y: patch.center.y,
151
+ signedOffset: state.signedOffset,
152
+ crossTrackError: state.crossTrackError,
153
+ surface: state.surface,
154
+ onTrack: Boolean(state.onTrack),
155
+ inPitLane: false,
156
+ pitLanePart: null,
157
+ pitBoxId: null,
158
+ minimumSignedOffset,
159
+ maximumSignedOffset,
160
+ fullyOutsideWhiteLine: outside.fullyOutsideWhiteLine,
161
+ outsideSide: outside.outsideSide,
162
+ sampledStates: [state],
163
+ };
164
+ }
165
+
166
+ function patchPitOffsetRange(patch, centerState) {
167
+ const wheelCenterOffset =
168
+ (patch.center.x - centerState.x) * centerState.normalX +
169
+ (patch.center.y - centerState.y) * centerState.normalY;
170
+ const projectedHalfWidth =
171
+ Math.abs(patch.forward.x * centerState.normalX + patch.forward.y * centerState.normalY) * patch.halfLength +
172
+ Math.abs(patch.right.x * centerState.normalX + patch.right.y * centerState.normalY) * patch.halfWidth;
173
+ return {
174
+ minimum: wheelCenterOffset - projectedHalfWidth,
175
+ maximum: wheelCenterOffset + projectedHalfWidth,
176
+ };
177
+ }
178
+
179
+ function canUseAnalyticPitWheels(geometry, centerState) {
180
+ const roadWidth = Number(centerState?.pitLaneRoadWidth);
181
+ if (!centerState?.inPitLane || !Number.isFinite(roadWidth) || roadWidth <= 0) return false;
182
+ const halfRoadWidth = roadWidth / 2;
183
+ return geometry.contactPatches.every((patch) => {
184
+ const range = patchPitOffsetRange(patch, centerState);
185
+ return range.minimum >= -halfRoadWidth - 0.001 && range.maximum <= halfRoadWidth + 0.001;
186
+ });
187
+ }
188
+
189
+ function analyticPitWheelState(patch, centerState) {
190
+ const range = patchPitOffsetRange(patch, centerState);
191
+ return {
192
+ id: patch.id,
193
+ x: patch.center.x,
194
+ y: patch.center.y,
195
+ signedOffset: centerState.mainTrackSignedOffset ?? centerState.signedOffset,
196
+ crossTrackError: centerState.mainTrackCrossTrackError ?? centerState.crossTrackError,
197
+ surface: centerState.surface,
198
+ onTrack: true,
199
+ inPitLane: true,
200
+ pitLanePart: centerState.pitLanePart ?? null,
201
+ pitBoxId: centerState.pitBoxId ?? null,
202
+ minimumSignedOffset: centerState.mainTrackSignedOffset ?? centerState.signedOffset,
203
+ maximumSignedOffset: centerState.mainTrackSignedOffset ?? centerState.signedOffset,
204
+ pitLaneMinimumSignedOffset: range.minimum,
205
+ pitLaneMaximumSignedOffset: range.maximum,
206
+ fullyOutsideWhiteLine: false,
207
+ outsideSide: 0,
208
+ sampledStates: [centerState],
209
+ };
210
+ }
211
+
212
+ export function getEffectiveSurface(wheels = []) {
213
+ if (!wheels.length) return 'track';
214
+ return wheels.reduce((surface, wheel) => (
215
+ priority(wheel.surface) > priority(surface) ? wheel.surface : surface
216
+ ), 'track');
217
+ }
218
+
219
+ export function isWholeCarOutsideTrackLimits(wheels = [], track, relaxedMargin = 0) {
220
+ if (!wheels.length || !track) {
221
+ return {
222
+ violating: false,
223
+ side: 0,
224
+ outsideBy: 0,
225
+ };
226
+ }
227
+ const trackLimit = track.width / 2 + relaxedMargin;
228
+ const right = wheels.every((wheel) => wheel.outsideSide === 1);
229
+ const left = wheels.every((wheel) => wheel.outsideSide === -1);
230
+ if (!right && !left) {
231
+ return {
232
+ violating: false,
233
+ side: 0,
234
+ outsideBy: 0,
235
+ };
236
+ }
237
+ const side = right ? 1 : -1;
238
+ const outsideBy = right
239
+ ? Math.min(...wheels.map((wheel) => wheel.minimumSignedOffset)) - trackLimit
240
+ : -trackLimit - Math.max(...wheels.map((wheel) => wheel.maximumSignedOffset));
241
+
242
+ return {
243
+ violating: outsideBy > 0,
244
+ side,
245
+ outsideBy: Math.max(0, outsideBy),
246
+ };
247
+ }
248
+
249
+ export function calculateWheelSurfaceState({ car, track, centerState: providedCenterState = null }) {
250
+ const geometry = getVehicleGeometryState(car);
251
+ const trackLimit = track.width / 2;
252
+ const centerState = providedCenterState ?? nearestTrackState(track, car, car.progress);
253
+ const useAnalyticPitSampling = canUseAnalyticPitWheels(geometry, centerState);
254
+ const useFullSampling = !useAnalyticPitSampling && Boolean(centerState.inPitLane || isNearPitConnector(track, centerState));
255
+ const wheels = useAnalyticPitSampling
256
+ ? geometry.contactPatches.map((patch) => analyticPitWheelState(patch, centerState))
257
+ : useFullSampling
258
+ ? geometry.contactPatches.map((patch) => {
259
+ const sampleState = (point) => nearestTrackState(track, point, car.progress);
260
+ const sampledStates = patchSamples(patch).map(sampleState);
261
+ const state = worstState(sampledStates);
262
+ const offsets = sampledStates.map((sample) => sample.signedOffset);
263
+ const outside = wheelFullyOutside(sampledStates, trackLimit);
264
+
265
+ return {
266
+ id: patch.id,
267
+ x: patch.center.x,
268
+ y: patch.center.y,
269
+ signedOffset: state.signedOffset,
270
+ crossTrackError: state.crossTrackError,
271
+ surface: state.surface,
272
+ onTrack: Boolean(state.onTrack),
273
+ inPitLane: Boolean(state.inPitLane),
274
+ pitLanePart: state.pitLanePart ?? null,
275
+ pitBoxId: state.pitBoxId ?? null,
276
+ minimumSignedOffset: Math.min(...offsets),
277
+ maximumSignedOffset: Math.max(...offsets),
278
+ fullyOutsideWhiteLine: outside.fullyOutsideWhiteLine,
279
+ outsideSide: outside.outsideSide,
280
+ sampledStates,
281
+ };
282
+ })
283
+ : geometry.contactPatches.map((patch) => analyticWheelState(patch, centerState, track, trackLimit));
284
+ const effectiveSurface = getEffectiveSurface(wheels);
285
+ const representative = wheels.reduce((best, wheel) => (
286
+ Math.abs(wheel.signedOffset) > Math.abs(best.signedOffset) ? wheel : best
287
+ ), wheels[0]);
288
+ const trackLimits = isWholeCarOutsideTrackLimits(wheels, track);
289
+
290
+ return {
291
+ wheels,
292
+ effectiveSurface,
293
+ trackLimits,
294
+ sampleMode: useAnalyticPitSampling ? 'pit-analytic' : useFullSampling ? 'full' : 'analytic',
295
+ representativeState: representative ? {
296
+ ...centerState,
297
+ surface: effectiveSurface,
298
+ onTrack: wheels.every((wheel) => wheel.onTrack || wheel.inPitLane),
299
+ inPitLane: wheels.every((wheel) => wheel.inPitLane),
300
+ signedOffset: representative.signedOffset,
301
+ crossTrackError: representative.crossTrackError,
302
+ } : nearestTrackState(track, car, car.progress),
303
+ };
304
+ }
305
+
306
+ export function applyWheelSurfaceState(car, track, options = {}) {
307
+ const geometry = getVehicleGeometryState(car);
308
+ const cacheKey = {
309
+ track,
310
+ geometrySignature: geometry.signature,
311
+ centerSignature: centerStateSignature(options.centerState),
312
+ };
313
+ const cached = car.wheelSurfaceCache;
314
+ if (
315
+ cached?.track === cacheKey.track &&
316
+ cached.geometrySignature === cacheKey.geometrySignature &&
317
+ cached.centerSignature === cacheKey.centerSignature
318
+ ) {
319
+ car.wheelStates = cached.result.wheels;
320
+ car.trackLimitState = cached.result.trackLimits;
321
+ car.trackState = cached.trackState;
322
+ return cached.result;
323
+ }
324
+
325
+ const result = calculateWheelSurfaceState({ car, track, centerState: options.centerState });
326
+ car.wheelStates = result.wheels;
327
+ car.trackLimitState = result.trackLimits;
328
+ car.trackState = {
329
+ ...result.representativeState,
330
+ wheelSurface: result.effectiveSurface,
331
+ };
332
+ car.wheelSurfaceCache = {
333
+ ...cacheKey,
334
+ result,
335
+ trackState: car.trackState,
336
+ };
337
+ return result;
338
+ }