@inventure71/paddockjs 1.0.0 → 2.0.1

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 (203) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/INSTALL_AND_UPDATE.md +4 -3
  3. package/README.md +219 -12
  4. package/docs/architecture.md +126 -45
  5. package/docs/concepts.md +9 -5
  6. package/docs/custom_model_controller.md +329 -0
  7. package/docs/data_contract.md +317 -22
  8. package/docs/data_model.md +1 -1
  9. package/docs/index.md +10 -0
  10. package/docs/learnings.md +22 -0
  11. package/docs/rules.md +71 -14
  12. package/docs/sense_contract.md +59 -0
  13. package/docs/system_specs.md +75 -13
  14. package/docs/training.md +280 -32
  15. package/examples/python/README.md +62 -0
  16. package/examples/python/base_policy_server.py +191 -0
  17. package/examples/python/requirements.txt +2 -0
  18. package/package.json +12 -4
  19. package/src/api/PaddockSimulatorController.js +6 -33
  20. package/src/app/BrowserExpertAdapter.js +76 -9
  21. package/src/app/BrowserExpertExternalRenderer.js +303 -0
  22. package/src/app/F1SimulatorApp.js +291 -1630
  23. package/src/app/banners/bannerFormatters.js +55 -0
  24. package/src/app/banners/raceDataBannerController.js +141 -0
  25. package/src/app/banners/stewardMessageController.js +99 -0
  26. package/src/app/camera/cameraConstants.js +21 -0
  27. package/src/app/camera/cameraController.js +265 -0
  28. package/src/app/camera/cameraGeometry.js +176 -0
  29. package/src/app/domBindings.js +4 -0
  30. package/src/app/readouts/carOverviewRenderer.js +109 -0
  31. package/src/app/readouts/controlStateRenderer.js +37 -0
  32. package/src/app/readouts/raceStatusRenderer.js +133 -0
  33. package/src/app/readouts/readoutFormatters.js +58 -0
  34. package/src/app/readouts/telemetryRenderer.js +113 -0
  35. package/src/app/readouts/timingTowerRenderer.js +177 -0
  36. package/src/app/rendering/appAssets.js +38 -0
  37. package/src/app/rendering/carRenderer.js +286 -0
  38. package/src/app/rendering/displayUtils.js +22 -0
  39. package/src/app/rendering/drsTrailRenderer.js +90 -0
  40. package/src/app/rendering/expertSensorRenderer.js +126 -0
  41. package/src/app/rendering/pitLaneStatusRenderer.js +50 -0
  42. package/src/app/rendering/replayGhostRenderer.js +124 -0
  43. package/src/app/rendering/trackRenderer.js +25 -0
  44. package/src/app/runtime/frameLoop.js +66 -0
  45. package/src/app/runtime/runtimeTiming.js +34 -0
  46. package/src/app/runtime/runtimeVisibility.js +45 -0
  47. package/src/config/defaultOptions.js +20 -0
  48. package/src/config/restartOptions.js +34 -0
  49. package/src/environment/actions.js +13 -5
  50. package/src/environment/controllerLoop.js +261 -0
  51. package/src/environment/episode.js +98 -2
  52. package/src/environment/evaluation.js +54 -32
  53. package/src/environment/index.d.ts +376 -10
  54. package/src/environment/index.js +1 -0
  55. package/src/environment/metrics.js +159 -0
  56. package/src/environment/observationOptions.js +22 -1
  57. package/src/environment/observationVector.js +200 -0
  58. package/src/environment/observations.js +199 -141
  59. package/src/environment/options.js +37 -5
  60. package/src/environment/recorder.js +13 -6
  61. package/src/environment/rewards.js +13 -2
  62. package/src/environment/runtime.js +272 -23
  63. package/src/environment/scenarios.js +57 -9
  64. package/src/environment/sensors/bodySenses.js +23 -0
  65. package/src/environment/sensors/boundarySenses.js +17 -0
  66. package/src/environment/sensors/carRays.js +75 -0
  67. package/src/environment/sensors/contactSenses.js +32 -0
  68. package/src/environment/sensors/index.js +157 -0
  69. package/src/environment/sensors/indexedRayBands.js +103 -0
  70. package/src/environment/sensors/nearbyCars.js +52 -0
  71. package/src/environment/sensors/opponentRadar.js +75 -0
  72. package/src/environment/sensors/rayConfig.js +110 -0
  73. package/src/environment/sensors/rayDefaults.js +6 -0
  74. package/src/environment/sensors/rayGeometry.js +60 -0
  75. package/src/environment/sensors/rayGuards.js +19 -0
  76. package/src/environment/sensors/sensorTargets.js +66 -0
  77. package/src/environment/sensors/surfaceRays.js +360 -0
  78. package/src/environment/sensors/trackRays.js +424 -0
  79. package/src/environment/sensors.js +1 -367
  80. package/src/environment/specs.js +159 -26
  81. package/src/environment/workerProtocol.js +13 -1
  82. package/src/index.d.ts +171 -6
  83. package/src/index.js +6 -32
  84. package/src/rendering/proceduralTrackAsset.js +20 -823
  85. package/src/rendering/renderSnapshot.js +38 -12
  86. package/src/rendering/track/finishLineRenderer.js +34 -0
  87. package/src/rendering/track/gridRenderer.js +47 -0
  88. package/src/rendering/track/offsetStrokeSafety.js +136 -0
  89. package/src/rendering/track/pitLaneRenderer.js +248 -0
  90. package/src/rendering/track/trackMaterialRenderer.js +326 -0
  91. package/src/rendering/track/trackRenderConstants.js +38 -0
  92. package/src/rendering/track/trackRenderGeometry.js +94 -0
  93. package/src/simulation/collisionGeometry.js +1 -1
  94. package/src/simulation/driver/driverControlConstants.js +28 -0
  95. package/src/simulation/driver/driverController.js +47 -0
  96. package/src/simulation/driver/driverInput.js +28 -0
  97. package/src/simulation/driver/driverMath.js +6 -0
  98. package/src/simulation/driver/driverPersonality.js +25 -0
  99. package/src/simulation/driver/edgeRecovery.js +48 -0
  100. package/src/simulation/driver/racingControls.js +399 -0
  101. package/src/simulation/driver/racingLinePlan.js +166 -0
  102. package/src/simulation/driver/recoveryDynamics.js +38 -0
  103. package/src/simulation/driver/rejoinControls.js +213 -0
  104. package/src/simulation/driver/safetyCarControls.js +35 -0
  105. package/src/simulation/driver/trafficScan.js +51 -0
  106. package/src/simulation/driverController.js +4 -619
  107. package/src/simulation/participants/participantInteractions.js +120 -0
  108. package/src/simulation/pit/pitFlow.js +239 -0
  109. package/src/simulation/pit/pitIntent.js +92 -0
  110. package/src/simulation/pit/pitKinematics.js +24 -0
  111. package/src/simulation/pit/pitOccupancy.js +47 -0
  112. package/src/simulation/pit/pitPenaltyService.js +71 -0
  113. package/src/simulation/pit/pitQueue.js +60 -0
  114. package/src/simulation/pit/pitRouteMovement.js +167 -0
  115. package/src/simulation/pit/pitRouting.js +319 -0
  116. package/src/simulation/pit/pitService.js +43 -0
  117. package/src/simulation/pit/pitServiceConstants.js +21 -0
  118. package/src/simulation/pit/pitServiceProfile.js +45 -0
  119. package/src/simulation/pit/pitSnapshots.js +61 -0
  120. package/src/simulation/pit/pitState.js +113 -0
  121. package/src/simulation/pit/pitTireService.js +94 -0
  122. package/src/simulation/race/classification.js +104 -0
  123. package/src/simulation/race/gridPenalties.js +37 -0
  124. package/src/simulation/race/raceControlState.js +38 -0
  125. package/src/simulation/race/raceDistance.js +32 -0
  126. package/src/simulation/race/raceFinish.js +65 -0
  127. package/src/simulation/race/raceLifecycle.js +67 -0
  128. package/src/simulation/race/raceOrder.js +113 -0
  129. package/src/simulation/race/raceProgress.js +129 -0
  130. package/src/simulation/race/raceSetup.js +93 -0
  131. package/src/simulation/race/raceStep.js +80 -0
  132. package/src/simulation/race/redFlag.js +15 -0
  133. package/src/simulation/race/retirements.js +44 -0
  134. package/src/simulation/race/safetyCar.js +92 -0
  135. package/src/simulation/race/stalledDnf.js +77 -0
  136. package/src/simulation/raceSimulation.js +231 -2817
  137. package/src/simulation/replay/replayGhosts.js +128 -0
  138. package/src/simulation/rules/moduleConfig.js +72 -0
  139. package/src/simulation/rules/penaltyConfig.js +99 -0
  140. package/src/simulation/rules/penaltyEvents.js +18 -0
  141. package/src/simulation/rules/penaltyStats.js +26 -0
  142. package/src/simulation/rules/ruleConfigMerge.js +41 -0
  143. package/src/simulation/rules/ruleDefaults.js +85 -0
  144. package/src/simulation/rules/rulesReview.js +144 -0
  145. package/src/simulation/rules/rulesetPresets.js +47 -0
  146. package/src/simulation/rules/trackLimitsSteward.js +1 -1
  147. package/src/simulation/rulesConfig.js +5 -323
  148. package/src/simulation/snapshots/raceSnapshots.js +193 -0
  149. package/src/simulation/timing/drsTiming.js +87 -0
  150. package/src/simulation/timing/gapEstimation.js +38 -0
  151. package/src/simulation/timing/lapTelemetry.js +182 -0
  152. package/src/simulation/timing/raceTiming.js +15 -0
  153. package/src/simulation/timing/sectorPerformance.js +60 -0
  154. package/src/simulation/timing/timingConstants.js +5 -0
  155. package/src/simulation/timing/timingHistory.js +46 -0
  156. package/src/simulation/timing/timingLines.js +79 -0
  157. package/src/simulation/track/centerlineSpline.js +59 -0
  158. package/src/simulation/track/drsZones.js +67 -0
  159. package/src/simulation/track/pitLaneAccess.js +103 -0
  160. package/src/simulation/track/pitLaneLayout.js +395 -0
  161. package/src/simulation/track/pitLaneState.js +160 -0
  162. package/src/simulation/track/proceduralCenterline.js +237 -0
  163. package/src/simulation/track/sampleGeometry.js +49 -0
  164. package/src/simulation/track/spatialQueries.js +154 -0
  165. package/src/simulation/track/startStraight.js +141 -0
  166. package/src/simulation/track/trackConstants.js +109 -0
  167. package/src/simulation/track/trackGenerationOptions.js +283 -0
  168. package/src/simulation/track/trackMath.js +241 -0
  169. package/src/simulation/track/trackModel.js +240 -0
  170. package/src/simulation/track/trackQueryGrid.js +94 -0
  171. package/src/simulation/track/trackQueryIndex.js +659 -0
  172. package/src/simulation/track/trackQueryPitIndex.js +140 -0
  173. package/src/simulation/track/trackQueryScratch.js +46 -0
  174. package/src/simulation/track/trackQueryStats.js +28 -0
  175. package/src/simulation/track/trackSectors.js +19 -0
  176. package/src/simulation/track/trackStatePolicy.js +24 -0
  177. package/src/simulation/track/trackValidation.js +98 -0
  178. package/src/simulation/trackModel.js +1 -1706
  179. package/src/simulation/vehicle/contactResolution.js +181 -0
  180. package/src/simulation/vehicle/mainTrackWheelSurface.js +82 -0
  181. package/src/simulation/vehicle/pitWheelSurface.js +69 -0
  182. package/src/simulation/vehicle/runoffResponse.js +87 -0
  183. package/src/simulation/vehicle/surfacePriority.js +28 -0
  184. package/src/simulation/vehicle/vehicleGeometry.js +247 -0
  185. package/src/simulation/vehicle/vehicleKinematics.js +37 -0
  186. package/src/simulation/vehicle/vehiclePhysics.js +364 -0
  187. package/src/simulation/vehicle/vehicleSnapshots.js +327 -0
  188. package/src/simulation/vehicle/vehicleState.js +220 -0
  189. package/src/simulation/vehicle/wheelSurface.js +152 -0
  190. package/src/simulation/vehicleGeometry.js +1 -247
  191. package/src/simulation/vehiclePhysics.js +1 -136
  192. package/src/simulation/warmup/runtimeWarmup.js +224 -0
  193. package/src/simulation/wheelSurface.js +1 -338
  194. package/src/styles.css +42 -0
  195. package/src/ui/cameraControlsTemplate.js +20 -0
  196. package/src/ui/carOverviewTemplate.js +33 -0
  197. package/src/ui/componentTemplates.js +14 -360
  198. package/src/ui/raceCanvasTemplate.js +69 -0
  199. package/src/ui/raceControlsTemplate.js +36 -0
  200. package/src/ui/shellTemplate.js +5 -4
  201. package/src/ui/telemetryDrawerTemplate.js +55 -0
  202. package/src/ui/telemetryTemplates.js +136 -0
  203. package/src/ui/timingTowerTemplate.js +34 -0
@@ -0,0 +1,191 @@
1
+ #!/usr/bin/env python3
2
+ """Neutral PaddockJS policy server base example.
3
+
4
+ This file is intentionally minimal and transport-focused:
5
+ - HTTP policy endpoints:
6
+ - POST /policy/reset
7
+ - POST /policy/reset-state
8
+ - POST /policy/decide-batch
9
+ - WebSocket preview endpoint:
10
+ - /preview (broadcasts {snapshot, observation, meta})
11
+
12
+ Extend `BasePolicyServer` and override hook methods to plug your own model/runtime.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import asyncio
18
+ import time
19
+ from dataclasses import dataclass, field
20
+ from typing import Any, Dict, Iterable, List, Mapping, MutableMapping, Optional
21
+
22
+ from fastapi import FastAPI, WebSocket, WebSocketDisconnect
23
+ from fastapi.middleware.cors import CORSMiddleware
24
+ from fastapi.responses import JSONResponse
25
+ import uvicorn
26
+
27
+
28
+ def _zero_action() -> Dict[str, float]:
29
+ return {
30
+ "steering": 0.0,
31
+ "throttle": 0.0,
32
+ "brake": 0.0,
33
+ }
34
+
35
+
36
+ @dataclass
37
+ class ServerState:
38
+ session_id: int = 0
39
+ controlled_drivers: List[str] = field(default_factory=list)
40
+ connections: List[WebSocket] = field(default_factory=list)
41
+ latest_preview_frame: Optional[Dict[str, Any]] = None
42
+
43
+
44
+ class BasePolicyServer:
45
+ """Base extension surface for custom policy/runtime integrations."""
46
+
47
+ def __init__(self) -> None:
48
+ self.app = FastAPI(title="PaddockJS Base Policy Server")
49
+ self.app.add_middleware(
50
+ CORSMiddleware,
51
+ allow_origins=["*"],
52
+ allow_methods=["*"],
53
+ allow_headers=["*"],
54
+ )
55
+ self.state = ServerState()
56
+ self._broadcast_lock = asyncio.Lock()
57
+ self._register_routes()
58
+
59
+ # ----- Extension hooks -------------------------------------------------
60
+
61
+ async def init_policy(self, context: Mapping[str, Any]) -> None:
62
+ """Initialize policy/runtime resources on full reset."""
63
+
64
+ async def reset_policy(self, context: Mapping[str, Any]) -> None:
65
+ """Reset all policy runtime state for a new run/session."""
66
+
67
+ async def reset_policy_state(self, driver_ids: Iterable[str]) -> None:
68
+ """Reset only selected driver-local model state."""
69
+
70
+ async def decide_batch(self, context: Mapping[str, Any]) -> Dict[str, Dict[str, float]]:
71
+ """Return per-driver controls for one batched policy step."""
72
+ driver_ids = list(context.get("driverIds") or [])
73
+ return {driver_id: _zero_action() for driver_id in driver_ids}
74
+
75
+ async def publish_preview_frame(self, frame: Mapping[str, Any]) -> None:
76
+ """Optional extension hook called before broadcasting preview frames."""
77
+
78
+ # ----- Public helpers --------------------------------------------------
79
+
80
+ async def broadcast_preview_frame(
81
+ self,
82
+ snapshot: Mapping[str, Any],
83
+ observation: Mapping[str, Any],
84
+ meta: Optional[Mapping[str, Any]] = None,
85
+ ) -> None:
86
+ """Broadcast one authoritative preview frame to websocket clients."""
87
+ frame = {
88
+ "type": "preview:snapshot",
89
+ "snapshot": dict(snapshot),
90
+ "observation": dict(observation),
91
+ "meta": dict(meta or {}),
92
+ }
93
+ self.state.latest_preview_frame = frame
94
+ await self.publish_preview_frame(frame)
95
+ payload = frame
96
+ async with self._broadcast_lock:
97
+ stale: List[WebSocket] = []
98
+ for ws in self.state.connections:
99
+ try:
100
+ await ws.send_json(payload)
101
+ except Exception:
102
+ stale.append(ws)
103
+ if stale:
104
+ self.state.connections = [ws for ws in self.state.connections if ws not in stale]
105
+
106
+ # ----- Internal routing ------------------------------------------------
107
+
108
+ def _register_routes(self) -> None:
109
+ @self.app.get("/preview/frame")
110
+ async def preview_frame() -> JSONResponse:
111
+ if self.state.latest_preview_frame:
112
+ return JSONResponse(self.state.latest_preview_frame)
113
+ return JSONResponse(
114
+ {
115
+ "type": "preview:status",
116
+ "status": "waiting-for-frame",
117
+ "session": {"id": self.state.session_id},
118
+ }
119
+ )
120
+
121
+ @self.app.post("/policy/reset")
122
+ async def policy_reset(body: MutableMapping[str, Any]) -> JSONResponse:
123
+ self.state.session_id += 1
124
+ self.state.controlled_drivers = list(body.get("driverIds") or [])
125
+ await self.init_policy(body)
126
+ await self.reset_policy(body)
127
+ return JSONResponse(
128
+ {
129
+ "ok": True,
130
+ "session": {
131
+ "id": self.state.session_id,
132
+ "controlledDrivers": self.state.controlled_drivers,
133
+ },
134
+ }
135
+ )
136
+
137
+ @self.app.post("/policy/reset-state")
138
+ async def policy_reset_state(body: MutableMapping[str, Any]) -> JSONResponse:
139
+ driver_ids = list(body.get("driverIds") or [])
140
+ await self.reset_policy_state(driver_ids)
141
+ return JSONResponse(
142
+ {
143
+ "ok": True,
144
+ "session": {"id": self.state.session_id},
145
+ "resetDrivers": driver_ids,
146
+ }
147
+ )
148
+
149
+ @self.app.post("/policy/decide-batch")
150
+ async def policy_decide_batch(body: MutableMapping[str, Any]) -> JSONResponse:
151
+ actions = await self.decide_batch(body)
152
+ if not isinstance(actions, dict):
153
+ actions = {}
154
+ now_ms = int(time.time() * 1000)
155
+ return JSONResponse(
156
+ {
157
+ "ok": True,
158
+ "session": {"id": self.state.session_id},
159
+ "actions": actions,
160
+ "meta": {"serverTsMs": now_ms},
161
+ }
162
+ )
163
+
164
+ @self.app.websocket("/preview")
165
+ async def preview_stream(ws: WebSocket) -> None:
166
+ await ws.accept()
167
+ self.state.connections.append(ws)
168
+ try:
169
+ await ws.send_json(
170
+ {
171
+ "type": "preview:status",
172
+ "status": "connected",
173
+ "session": {"id": self.state.session_id},
174
+ }
175
+ )
176
+ while True:
177
+ # Keep connection alive; ignore client messages.
178
+ await ws.receive_text()
179
+ except WebSocketDisconnect:
180
+ pass
181
+ finally:
182
+ self.state.connections = [conn for conn in self.state.connections if conn is not ws]
183
+
184
+
185
+ def run(host: str = "127.0.0.1", port: int = 8787) -> None:
186
+ server = BasePolicyServer()
187
+ uvicorn.run(server.app, host=host, port=port, log_level="info")
188
+
189
+
190
+ if __name__ == "__main__":
191
+ run()
@@ -0,0 +1,2 @@
1
+ fastapi
2
+ uvicorn
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inventure71/paddockjs",
3
- "version": "1.0.0",
3
+ "version": "2.0.1",
4
4
  "type": "module",
5
5
  "description": "A data-driven browser F1 simulator component for host websites.",
6
6
  "license": "Apache-2.0",
@@ -53,7 +53,11 @@
53
53
  "LICENSE",
54
54
  "assets",
55
55
  "docs/*.md",
56
- "examples",
56
+ "examples/python/README.md",
57
+ "examples/python/base_policy_server.py",
58
+ "examples/python/requirements.txt",
59
+ "examples/train-basic-policy.mjs",
60
+ "examples/trainingData.mjs",
57
61
  "src/api",
58
62
  "src/app",
59
63
  "src/config",
@@ -68,12 +72,16 @@
68
72
  "README.md"
69
73
  ],
70
74
  "scripts": {
71
- "test": "vitest run src",
75
+ "test": "node scripts/run-vitest.mjs --slow",
76
+ "test:fast": "node scripts/run-vitest.mjs",
72
77
  "types:check": "tsc -p tsconfig.types.json --noEmit",
73
- "check": "npm test && npm run types:check && npm run pack:dry && npm run consumer:smoke && npm run showcase:ci && npm run browser:smoke",
78
+ "check": "npm run test:fast && npm run types:check && npm run pack:dry && npm run consumer:smoke && npm run showcase:ci && npm run browser:smoke:quick -- --skip-build",
79
+ "check:release": "npm test && npm run types:check && npm run pack:dry && npm run consumer:smoke && npm run showcase:ci && npm run browser:smoke:full -- --skip-build",
74
80
  "pack:dry": "npm pack --dry-run",
75
81
  "consumer:smoke": "node scripts/consumer-package-smoke.mjs",
76
82
  "browser:smoke": "node smoke/browser-smoke.mjs",
83
+ "browser:smoke:quick": "node smoke/browser-smoke.mjs --quick",
84
+ "browser:smoke:full": "node smoke/browser-smoke.mjs --full",
77
85
  "showcase:install": "npm --prefix local-preview ci",
78
86
  "showcase:dev": "npm --prefix local-preview run dev",
79
87
  "showcase:build": "npm --prefix local-preview run build",
@@ -18,6 +18,7 @@ import {
18
18
  createTimingTowerMarkup,
19
19
  } from '../ui/componentTemplates.js';
20
20
  import { applyPaddockThemeCssVariables, resolveF1SimulatorOptions } from '../config/defaultOptions.js';
21
+ import { mergeRestartOptions } from '../config/restartOptions.js';
21
22
 
22
23
  function assertMountTarget(root, label) {
23
24
  if (!root || typeof root !== 'object' || !('innerHTML' in root)) {
@@ -31,38 +32,6 @@ function setPackageCssVariables(root, assets, theme) {
31
32
  applyPaddockThemeCssVariables(root, theme);
32
33
  }
33
34
 
34
- function mergeResolvedOptions(previousOptions, nextOptions) {
35
- const resetFromPreset = Object.hasOwn(nextOptions, 'preset');
36
- const previousUi = resetFromPreset ? {} : previousOptions.ui;
37
- const previousTheme = resetFromPreset ? {} : previousOptions.theme;
38
- return {
39
- ...previousOptions,
40
- ...nextOptions,
41
- ui: {
42
- ...previousUi,
43
- ...(nextOptions.ui ?? {}),
44
- raceDataBanners: {
45
- ...(previousUi.raceDataBanners ?? {}),
46
- ...(nextOptions.ui?.raceDataBanners ?? {}),
47
- },
48
- },
49
- theme: {
50
- ...previousTheme,
51
- ...(nextOptions.theme ?? {}),
52
- },
53
- assets: {
54
- ...previousOptions.assets,
55
- ...(nextOptions.assets ?? {}),
56
- trackTextures: {
57
- ...previousOptions.assets.trackTextures,
58
- ...(nextOptions.assets?.trackTextures ?? {}),
59
- },
60
- },
61
- drivers: nextOptions.drivers ?? previousOptions.drivers,
62
- entries: nextOptions.entries ?? previousOptions.entries,
63
- };
64
- }
65
-
66
35
  function createCompositeRoot(getRoots, getOptions) {
67
36
  return {
68
37
  style: {
@@ -203,7 +172,7 @@ export class PaddockSimulatorController {
203
172
  }
204
173
 
205
174
  restart(nextOptions = {}) {
206
- const nextResolvedOptions = resolveF1SimulatorOptions(mergeResolvedOptions(this.options, nextOptions));
175
+ const nextResolvedOptions = resolveF1SimulatorOptions(mergeRestartOptions(this.options, nextOptions));
207
176
  this.app?.restart(nextResolvedOptions);
208
177
  this.options = nextResolvedOptions;
209
178
  this.compositeRoot.applyCssVariables();
@@ -250,6 +219,10 @@ export class PaddockSimulatorController {
250
219
  return this.app?.getPitTargetCompound?.(driverId) ?? null;
251
220
  }
252
221
 
222
+ getSimulationSpeed() {
223
+ return this.app?.simulationSpeed ?? 1;
224
+ }
225
+
253
226
  servePenalty(penaltyId) {
254
227
  return this.app?.servePenalty(penaltyId) ?? null;
255
228
  }
@@ -1,14 +1,52 @@
1
1
  import { createEnvironmentRuntime } from '../environment/runtime.js';
2
2
  import { resolveEnvironmentOptions } from '../environment/options.js';
3
+ import { createBrowserExpertExternalRendererBridge } from './BrowserExpertExternalRenderer.js';
3
4
 
4
5
  export function createBrowserExpertAdapter(app, expertOptions = {}) {
5
- let resolvedOptions = resolveEnvironmentOptions({
6
- ...app.options,
7
- ...expertOptions,
8
- controlledDrivers: expertOptions.controlledDrivers,
9
- });
6
+ let resolvedOptions = resolveBrowserExpertOptions(app.options, expertOptions);
7
+ let frameRenderSuppressed = false;
8
+ const externalRenderer = createBrowserExpertExternalRendererBridge(app);
9
+
10
+ function resolveBrowserExpertOptions(appOptions, nextExpertOptions = {}) {
11
+ const options = resolveEnvironmentOptions({
12
+ ...appOptions,
13
+ ...nextExpertOptions,
14
+ controlledDrivers: nextExpertOptions.controlledDrivers,
15
+ });
16
+ if (options.result.stateOutput !== 'none') return options;
17
+ return {
18
+ ...options,
19
+ result: {
20
+ ...options.result,
21
+ stateOutput: 'minimal',
22
+ },
23
+ };
24
+ }
25
+
26
+ function renderableSnapshot(result) {
27
+ return result?.state?.snapshot ?? app.sim?.snapshotObservation?.() ?? app.sim?.snapshot?.() ?? null;
28
+ }
29
+
30
+ /*
31
+ * Browser expert mode is a visual adapter. Compact headless options may ask for
32
+ * no state payload, but the canvas still needs a snapshot to render.
33
+ */
34
+ function renderExpertResult(result, renderOptions = {}) {
35
+ const snapshot = renderableSnapshot(result);
36
+ if (!snapshot) return;
37
+ app.renderExpertFrame(snapshot, {
38
+ ...renderOptions,
39
+ observation: result?.observation,
40
+ });
41
+ }
42
+
43
+ function initialResolvedOptions() {
44
+ return resolveBrowserExpertOptions(app.options, expertOptions);
45
+ }
46
+
47
+ resolvedOptions = initialResolvedOptions();
10
48
 
11
- return createEnvironmentRuntime({
49
+ const runtime = createEnvironmentRuntime({
12
50
  getSimulation: () => app.sim,
13
51
  setSimulation(nextSim) {
14
52
  app.sim = nextSim;
@@ -19,14 +57,43 @@ export function createBrowserExpertAdapter(app, expertOptions = {}) {
19
57
  },
20
58
  getOptions: () => resolvedOptions,
21
59
  setOptions(nextOptions) {
22
- resolvedOptions = nextOptions;
60
+ resolvedOptions = nextOptions.result?.stateOutput === 'none'
61
+ ? { ...nextOptions, result: { ...nextOptions.result, stateOutput: 'minimal' } }
62
+ : nextOptions;
23
63
  },
24
64
  afterReset(result) {
25
65
  app.renderTrack();
26
- app.renderExpertFrame(result.state.snapshot, { observation: result.observation });
66
+ renderExpertResult(result, { forceDomUpdate: true });
27
67
  },
28
68
  afterStep(result) {
29
- app.renderExpertFrame(result.state.snapshot, { observation: result.observation });
69
+ if (frameRenderSuppressed) return;
70
+ renderExpertResult(result);
30
71
  },
31
72
  });
73
+
74
+ return {
75
+ ...runtime,
76
+ reset(options = {}) {
77
+ externalRenderer.assertDetached('reset');
78
+ return runtime.reset(options);
79
+ },
80
+ step(actions = {}) {
81
+ externalRenderer.assertDetached('step');
82
+ return runtime.step(actions);
83
+ },
84
+ resetDrivers(placements = {}, resultOptions = {}) {
85
+ externalRenderer.assertDetached('resetDrivers');
86
+ return runtime.resetDrivers(placements, resultOptions);
87
+ },
88
+ destroy() {
89
+ externalRenderer.detach();
90
+ runtime.destroy();
91
+ },
92
+ setFrameRenderSuppressed(suppressed) {
93
+ frameRenderSuppressed = Boolean(suppressed);
94
+ },
95
+ attachExternalRenderer: externalRenderer.attach,
96
+ detachExternalRenderer: externalRenderer.detach,
97
+ getExternalRendererState: externalRenderer.getState,
98
+ };
32
99
  }