Class ScenarioRunner

java.lang.Object
org.horizon36596.simloop.sim.ScenarioRunner

public final class ScenarioRunner extends Object
Runs one headless sim scenario end-to-end: owns the entire PsiKit lifecycle (sim-harness §2-3) and the fixed-timestep tick loop, so a test does not hand-write it (BACKLOG "scenario runner" / area B). Every sim test up to this point (Batch 1.4's DrivetrainSimTest, Batch 2.2's SlideSimTest) re-typed the same ~20 lines: create the RLOG directory, reset/wire/start PsiKit's Logger, tick a FakeTimer inside a periodicBeforeUser()/periodicAfterUser() bracket, call Logger.end(), then re-check by hand that the writer thread didn't overflow or emit nothing. This class is that boilerplate, written once.

What stays with the caller. This class is season-agnostic core (architecture §3, domain R7): it knows nothing about a specific robot's motors, plants, or subsystems. Building the fake hardware, commanding the robot, advancing its plants, and deciding what to log each tick is scenario-specific behavior the caller supplies as a ScenarioRunner.ScenarioBody. The SimRobotConfig parameter is not read by this class at all — it is recorded as PsiKit metadata purely so a human opening the RLOG in AdvantageScope (or a future Phase-3 scorer) can see which robot config a run used, the same way opModeName is.

Trustworthy by construction. run(java.lang.String, org.horizon36596.simloop.config.SimRobotConfig, org.horizon36596.simloop.sim.FakeTimer, int, double, java.nio.file.Path, org.horizon36596.simloop.sim.ScenarioRunner.ScenarioBody) throws if PsiKit's async writer queue overflowed (BACKLOG B3 — cycles silently dropped) or if the resulting RLOG is missing or empty (BACKLOG B11) — a scenario whose log cannot be trusted for replay is a defect the moment it happens, not something every caller should have to re-check after the fact.

One logged run at a time per JVM. PsiKit's Logger is one static object, so two runs logging at once would write into each other's RLOG, and a run could return normally with a log that is mostly the other's. run(java.lang.String, org.horizon36596.simloop.config.SimRobotConfig, org.horizon36596.simloop.sim.FakeTimer, int, double, java.nio.file.Path, org.horizon36596.simloop.sim.ScenarioRunner.ScenarioBody) therefore takes a JVM-wide lock: a second run started on another thread waits for the first to finish, and one started from inside a run's own tick fails at once. Runs also share OpMode.blackboard, which OpModeRunner clears at the start of each run, so tests that run OpModes must not run in parallel within one JVM; Gradle's separate test JVMs are fine.