Gravity: the Planet guard refuses an unphysical well; tests pin the Roche rule, per-body multipliers, the core band and Ship's order
A WellStrength that evaluates negative, NaN or infinite now throws at construction (it built an antimass well with NaN height). New tests: the core-inside-Roche guard judged at every mass on curves that cross in the generated range; per-body multipliers move neither strength, core nor Roche limit; potential and force consistency inside the cores' outer bands (0.5 R to R); a moving Ship feels the field where it has moved to. Not built or run locally; verified in the Yggdrasil job. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
8013c2deec
commit
c4dfd905ff
3 changed files with 131 additions and 3 deletions
|
|
@ -712,6 +712,8 @@ public class Planet
|
|||
WellStrength = Settings.WellStrength.Evaluate(BodyData.Mass);
|
||||
CoreRadius = Settings.CoreRadius.Evaluate(BodyData.Mass);
|
||||
RocheLimit = Settings.RocheLimit.Evaluate(BodyData.Mass);
|
||||
if (!(WellStrength >= 0f) || float.IsInfinity(WellStrength))
|
||||
throw new InvalidOperationException("A body's well strength must be finite and not negative (PlanetSettings.WellStrength).");
|
||||
if (WellStrength > 0 && !(0 < CoreRadius && CoreRadius < RocheLimit))
|
||||
throw new InvalidOperationException("A body's core radius must be positive and inside its Roche limit (PlanetSettings.CoreRadius, RocheLimit).");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -227,4 +227,25 @@ public sealed class FloatingBodyTests : IDisposable
|
|||
Assert.True(length(up - normal) < 1e-4f, "the ship tilts to the zone's surface normal");
|
||||
}
|
||||
}
|
||||
|
||||
// Ship.Update's order: position first, then gravity sampled where the ship now is (leapfrog's kick after the drift). A
|
||||
// ship already moving falls toward the centre through the field at its new place, not the one it left.
|
||||
[Fact]
|
||||
public void AMovingShipFeelsTheFieldWhereItHasMoved()
|
||||
{
|
||||
var ship = ShipAt(400f, out var zone);
|
||||
ship.Velocity = float2(-200f, 0f);
|
||||
const float delta = .25f;
|
||||
|
||||
ship.Update(delta);
|
||||
|
||||
var moved = float2(400f, 0f) + float2(-200f, 0f) * delta;
|
||||
Assert.Equal(moved.x, ship.Position.x, 1e-3f);
|
||||
var pulled = (ship.Velocity - float2(-200f, 0f)) / delta;
|
||||
var expected = zone.GetForce(moved);
|
||||
Assert.True(expected.x < 0f, "the well pulls inward at the ship's new place");
|
||||
Assert.InRange(pulled.x, expected.x * (1f + 1e-3f), expected.x * (1f - 1e-3f));
|
||||
Assert.InRange(pulled.y, -1e-3f, 1e-3f);
|
||||
Assert.NotEqual(zone.GetForce(float2(400f, 0f)).x, expected.x); // the premise: the field differs between the two places
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,13 +54,18 @@ public sealed class PotentialOwnerTests : IDisposable
|
|||
|
||||
private int _bodies;
|
||||
|
||||
private Zone ZoneOf(PlanetSettings planets, params (float mass, float2 at)[] bodies)
|
||||
private Zone ZoneOf(PlanetSettings planets, params (float mass, float2 at)[] bodies) => ZoneOf(planets, null, bodies);
|
||||
|
||||
// `author` edits each body's record before it is stored: the per-body knobs a body record carries.
|
||||
private Zone ZoneOf(PlanetSettings planets, Action<PlanetData> author, params (float mass, float2 at)[] bodies)
|
||||
{
|
||||
var pack = new ZonePack { Radius = PackRadius };
|
||||
foreach (var (mass, at) in bodies)
|
||||
{
|
||||
var orbit = _cache.Upsert(new OrbitData { FixedPosition = at }).Key;
|
||||
var body = _cache.Upsert(new PlanetData { Name = "Body " + ++_bodies, Orbit = new CultRecordRef<OrbitData>(orbit), Mass = mass }).Key;
|
||||
var data = new PlanetData { Name = "Body " + ++_bodies, Orbit = new CultRecordRef<OrbitData>(orbit), Mass = mass };
|
||||
author?.Invoke(data);
|
||||
var body = _cache.Upsert(data).Key;
|
||||
pack.Orbits.Add(new CultRecordRef<OrbitData>(orbit));
|
||||
pack.Planets.Add(new CultRecordRef<BodyData>(body));
|
||||
}
|
||||
|
|
@ -143,6 +148,100 @@ public sealed class PotentialOwnerTests : IDisposable
|
|||
ZoneOf(Constants(20f, 50f), (100f, float2.zero)); // inside the limit, and past the body's radius
|
||||
}
|
||||
|
||||
// core-inside-roche-curve at every mass: the guard compares the two authored curves for each body, light or heavy.
|
||||
// Each pair of curves crosses somewhere in the generated mass range (1e-3 to 1e8), so the sweep holds both outcomes.
|
||||
[Theory]
|
||||
[InlineData(30f, .25f, 7.32f, .5f)] // the core curve starts above the Roche curve and is overtaken: light bodies throw
|
||||
[InlineData(3f, .5f, 7.32f, .25f)] // the core curve overtakes the Roche curve: heavy bodies throw
|
||||
public void TheGuardJudgesEveryMassByItsOwnCurves(float coreMultiplier, float coreExponent, float rocheMultiplier, float rocheExponent)
|
||||
{
|
||||
var planets = new PlanetSettings
|
||||
{
|
||||
WellStrength = new ExponentialCurve { Constant = 100f },
|
||||
CoreRadius = new ExponentialCurve { Multiplier = coreMultiplier, Exponent = coreExponent },
|
||||
RocheLimit = new ExponentialCurve { Multiplier = rocheMultiplier, Exponent = rocheExponent },
|
||||
BodyRadius = new ExponentialCurve { Constant = 1f },
|
||||
GravityRadius = new ExponentialCurve(), GravityDepth = new ExponentialCurve(),
|
||||
LightRadius = new ExponentialCurve(), OrbitPeriod = new ExponentialCurve()
|
||||
};
|
||||
var threw = 0;
|
||||
var built = 0;
|
||||
for (var exponent10 = -3f; exponent10 <= 8f; exponent10 += .25f)
|
||||
{
|
||||
var mass = pow(10f, exponent10);
|
||||
var core = planets.CoreRadius.Evaluate(mass);
|
||||
var roche = planets.RocheLimit.Evaluate(mass);
|
||||
if (0 < core && core < roche)
|
||||
{
|
||||
ZoneOf(planets, (mass, float2.zero));
|
||||
built++;
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.Throws<InvalidOperationException>(() => ZoneOf(planets, (mass, float2.zero)));
|
||||
threw++;
|
||||
}
|
||||
}
|
||||
Assert.True(threw > 0 && built > 0, "the sweep holds masses on both sides of the crossing");
|
||||
}
|
||||
|
||||
// The shipped curves keep the core inside the Roche limit over the whole generated mass range.
|
||||
[Fact]
|
||||
public void TheAuthoredCurvesBuildAtEveryMass()
|
||||
{
|
||||
var planets = Curves();
|
||||
for (var exponent10 = -3f; exponent10 <= 8f; exponent10 += .25f)
|
||||
ZoneOf(planets, (pow(10f, exponent10), float2.zero));
|
||||
}
|
||||
|
||||
// A well that is negative, not a number or infinite is an authoring error, not an antimass body. Zero is the unauthored
|
||||
// default and digs nothing (BodiesWithNoAuthoredWellPullAndDigNothing).
|
||||
[Theory]
|
||||
[InlineData(-10f)]
|
||||
[InlineData(float.NaN)]
|
||||
[InlineData(float.PositiveInfinity)]
|
||||
[InlineData(float.NegativeInfinity)]
|
||||
public void AnUnphysicalWellStrengthThrowsAtConstruction(float strength)
|
||||
{
|
||||
var planets = Curves();
|
||||
planets.WellStrength = new ExponentialCurve { Constant = strength };
|
||||
Assert.Throws<InvalidOperationException>(() => ZoneOf(planets, (100f, float2.zero)));
|
||||
planets.CoreRadius = new ExponentialCurve();
|
||||
planets.RocheLimit = new ExponentialCurve();
|
||||
Assert.Throws<InvalidOperationException>(() => ZoneOf(planets, (100f, float2.zero)));
|
||||
}
|
||||
|
||||
// The well is authored once, by curves of mass (target: no per-body authoring of a core or Roche limit): a body whose
|
||||
// record carries other radius, gravity-radius or gravity-depth multipliers has the same strength, core and Roche limit,
|
||||
// and pulls and digs exactly as its plain twin. The multipliers are brush inputs for the renderer, nothing else.
|
||||
[Theory]
|
||||
[InlineData(2.5f, 3f, .4f)]
|
||||
[InlineData(.5f, .5f, 3f)]
|
||||
public void PerBodyMultipliersDoNotMoveTheWell(float bodyRadius, float gravityRadius, float gravityDepth)
|
||||
{
|
||||
var planets = Curves();
|
||||
foreach (var mass in new[] { 29.2f, 14152.4f })
|
||||
{
|
||||
var plain = ZoneOf(planets, (mass, float2.zero));
|
||||
var scaled = ZoneOf(planets, data =>
|
||||
{
|
||||
data.BodyRadiusMultiplier = bodyRadius;
|
||||
data.GravityRadiusMultiplier = gravityRadius;
|
||||
data.GravityDepthMultiplier = gravityDepth;
|
||||
}, (mass, float2.zero));
|
||||
var a = plain.PlanetInstances.Values.Single();
|
||||
var b = scaled.PlanetInstances.Values.Single();
|
||||
Assert.Equal(a.WellStrength, b.WellStrength);
|
||||
Assert.Equal(a.CoreRadius, b.CoreRadius);
|
||||
Assert.Equal(a.RocheLimit, b.RocheLimit);
|
||||
foreach (var p in new[] { float2(.4f * a.CoreRadius, 0), float2(0, 3f * a.CoreRadius), float2(400f, 300f) })
|
||||
{
|
||||
Assert.Equal(plain.GetForce(p), scaled.GetForce(p));
|
||||
Assert.Equal(plain.GetHeight(p), scaled.GetHeight(p));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TheTailIsAPointMassWithNoRimAndNoGate()
|
||||
{
|
||||
|
|
@ -201,7 +300,13 @@ public sealed class PotentialOwnerTests : IDisposable
|
|||
var c1 = planets.WellStrength.Evaluate(14152.4f);
|
||||
var core1 = planets.CoreRadius.Evaluate(14152.4f);
|
||||
const float step = .5f;
|
||||
foreach (var p in new[] { float2(-200f, 120f), float2(0f, 0f), float2(100f, -300f), float2(500f, 40f), float2(-290f, 12f), float2(310f, 20f), float2(700f, 700f), float2(-80f, -500f) })
|
||||
// Inside the two cores as well, out to their outer bands (0.5 R to R), where the potential is the quadratic bowl.
|
||||
var inCore = new[]
|
||||
{
|
||||
float2(-300f + .5f * core0, 0f), float2(-300f, .65f * core0), float2(-300f - .86f * core0, 0f), float2(-300f + .6f * core0, .6f * core0),
|
||||
float2(300f + .55f * core1, 0f), float2(300f, -.7f * core1), float2(300f - .86f * core1, 0f), float2(300f + .9f * core1, 0f)
|
||||
};
|
||||
foreach (var p in inCore.Concat(new[] { float2(-200f, 120f), float2(0f, 0f), float2(100f, -300f), float2(500f, 40f), float2(-290f, 12f), float2(310f, 20f), float2(700f, 700f), float2(-80f, -500f) }))
|
||||
{
|
||||
var expected = Phi(c0, core0, length(p - float2(-300f, 0f))) - Phi(c0, core0, PackRadius)
|
||||
+ Phi(c1, core1, length(p - float2(300f, 0f))) - Phi(c1, core1, PackRadius);
|
||||
|
|
|
|||
Loading…
Reference in a new issue