A maneuver test class for a vehicle, implementing a figure-8 driving routine. It steers the car in alternating lobes, ramps steering in on entry, detects spin-outs, accumulates per-lobe yaw to flip steering, and reports lateral G, yaw rate and spin-out results.
namespace VehicleProto;
/// <summary>
/// Figure-8 (paired skidpad circles). Like <see cref="SkidpadManeuver"/> but reverses
/// the steering each lobe, so the car circles one way, transitions through neutral (the combined-slip
/// direction change the maneuver exists to exercise), then circles the other way. The accumulator reads
/// out steady-state <c>lateralGAvg</c> / <c>yawRateAvgDeg</c> over the whole run; <c>lateralGPeak</c> is
/// the lobe-consistency proxy (frozen contract has no per-lobe field); <c>spunOut</c> latches on a yaw
/// runaway, exactly as skidpad.
///
/// A run's assist level may be pinned per spec (params.assist) — the pilot re-applies that pin every
/// tick (docs/testing-harness.md §7.1); this maneuver only stages steer/throttle. Determinism law: no RNG.
/// </summary>
public sealed class Figure8Maneuver : ManeuverBase
{
public override string Name => "figure8";
int _lobe;
int _sign;
public override void Start( ManeuverContext ctx )
{
_lobe = 0;
_sign = -1; // first lobe turns right (negative steer), matching skidpad's default hand
}
public override bool Tick( ManeuverContext ctx, float dt )
{
int lobes = (int)MathF.Round( ctx.Param( "lobes", 2f ) );
float steer = _sign * 0.55f;
// ramp steer in over the first 2.5 s (skidpad's entry transient), then hold the circle.
if ( ctx.RunTime < 2.5f )
ctx.Drive( 0.7f, steer * (ctx.RunTime / 2.5f), false );
else
ctx.Drive( 0.45f, steer, false );
// spin-out detection: yaw rate runs away (same threshold as skidpad).
if ( MathF.Abs( ctx.Body.AngularVelocity.z.RadianToDegree() ) > 150f )
VehicleBridge.SpunOut = true;
// each lobe ~ one full circle; when this lobe's accumulated |yaw| nears 360°, flip the hand and
// re-baseline the yaw tracking for the next lobe (keep the running peak — resetMax:false).
if ( MathF.Abs( ctx.Telemetry.YawAccumDeg ) >= 330f )
{
_lobe++;
_sign = -_sign;
ctx.Telemetry.ResetYawTracking( ctx.Car, resetMax: false );
}
return _lobe >= lobes; // done once both lobes are complete (else ends on maxRunS = a DNF)
}
public override void Report( RunVerdict v )
{
v.Add( $"lateralGAvg={VehicleBridge.LateralGAvg:F2}", "Lateral g avg", $"{VehicleBridge.LateralGAvg:F2} g" );
v.Add( $"lateralGPeak={VehicleBridge.LateralGPeak:F2}", "Lateral g peak", $"{VehicleBridge.LateralGPeak:F2} g" );
v.Add( $"yawRateAvgDeg={VehicleBridge.YawRateAvgDeg:F1}", "Yaw rate avg", $"{VehicleBridge.YawRateAvgDeg:F1} deg/s" );
v.Add( $"spunOut={VehicleBridge.SpunOut}", "Spun out", VehicleBridge.SpunOut ? "yes" : "no" );
}
}