Merge eureka/aetheria-gravity-ring-roche-final-heavy (head 390a5958)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-18 01:25:54 +02:00
commit b9e16872c7
2 changed files with 36 additions and 13 deletions

View file

@ -586,8 +586,8 @@ public static class ZoneGenerator
/// outermost overflows the window, and the rest are stretched geometrically so the outermost sits at the window's edge. A
/// non-root's window ends before its own fade (Gamma x Reach x its distance); a root's, in a zone of several, ends under
/// rootReach / Beta so its support never meets another root's (root-window-floor), and is 0.75 x the area radius at most.
/// A rosette's ring is one orbit; it folds into its node, which becomes a body, if it cannot keep the heavy member outside
/// its Roche margin. Ported from docs/research/gravity-well-law/replace.py.
/// A rosette's ring is one orbit; once its members have settled (their own children folded in), it folds into its node,
/// which becomes a body, if it cannot keep its heaviest member outside its Roche margin. Ported from docs/research/gravity-well-law/replace.py.
/// </summary>
public static void Place(GeneratorPlanet root, PlanetSettings planetSettings, float radius, float rootReach)
{
@ -647,6 +647,7 @@ public static class ZoneGenerator
}
}
var settled = new HashSet<GeneratorPlanet>(); // ring members, settled by their node so the ring is judged on their final masses
var chordCap = new Dictionary<GeneratorPlanet, float>(); // a ring member's children stay within .2 of the nearest partner's chord
void Settle(GeneratorPlanet node)
{
@ -668,19 +669,20 @@ public static class ZoneGenerator
var ringDistance = Math.Min(.5f * cap, share * reach / (GravityField.Beta * GravityField.Beta * (1 + GravityField.RingReach)));
PlaceChildren(node.Children.Where(c => !ring.Contains(c)).ToList(),
() => (1 + GravityField.RingReach) * ringDistance * GravityField.Beta, upper, heavy);
// The heavy member's mass is final only now (the outer children fold into it).
if (ringDistance < RocheMargin * Roche(heavy.Mass))
{
// The pair cannot keep its heavy member outside its Roche margin inside its node's reach: the node is one body.
node.Empty = false;
node.Mass = 0;
foreach (var child in node.Children.ToList()) Fold(child, node);
return;
}
foreach (var member in ring)
{
member.Distance = ringDistance;
chordCap[member] = .2f * 2 * ringDistance * sin(PI / ring.Count);
Settle(member);
settled.Add(member);
}
// A member's mass is final only once it has settled (its own overflowing children fold into it).
if (ringDistance < RocheMargin * Roche(ring.Max(c => c.Mass)))
{
// The ring cannot keep its heaviest member outside its Roche margin inside its node's reach: the node is one body.
node.Empty = false;
node.Mass = 0;
foreach (var child in node.Children.ToList()) Fold(child, node);
}
}
@ -689,7 +691,7 @@ public static class ZoneGenerator
while (queue.Count > 0)
{
var node = queue.Dequeue();
Settle(node);
if (!settled.Contains(node)) Settle(node);
foreach (var child in node.Children) queue.Enqueue(child);
}
}

View file

@ -20,7 +20,9 @@ public sealed class ChildrenWithinReachTests : IClassFixture<ChildrenWithinReach
public readonly List<(uint Seed, Zone Zone)> All = new List<(uint, Zone)>();
public ZoneGenerationSettings Settings;
public Zones() : this(1, 6, null) { }
// Seeds 7, 11 and 12 held rings inside their heavy member's Roche margin (ring-roche-final-heavy), which seeds 1-6 do not
// show. Seed 10 holds a bodyless non-ring node whose massive children are never placed (distance 0); widen past 9 once that is fixed.
public Zones() : this(1, 9, null) { }
// Not public: xunit builds the fixture through its one public constructor; a test that wants a tuned sample builds its own.
internal Zones(uint first, uint last, Action<ZoneGenerationSettings> tune)
@ -458,6 +460,25 @@ public sealed class ChildrenWithinReachTests : IClassFixture<ChildrenWithinReach
Assert.True(Math.Abs(member.Distance - .375f * radius) <= 1e-4f * radius, $"a ring member sits at {member.Distance}, not {.375f * radius}");
}
// A ring is judged on its members' settled masses: a member's own children fold into it when it settles
// (ring-roche-fixed-in-generator-cut).
[Theory]
[InlineData(false)]
[InlineData(true)]
public void ARingIsJudgedOnItsMembersSettledMasses(bool heavyGrows)
{
var settings = AuthoredPlanetSettings();
const float mass = 1000f;
var ringDistance = RocheMargin * settings.RocheLimit.Evaluate(mass) * 1.02f;
var root = new GeneratorPlanet { Empty = true };
var heavy = Body(root, mass, 0, true);
var light = Body(root, .9f * mass, .5f, true);
Body(heavyGrows ? heavy : light, .5f * mass);
ZoneGenerator.Place(root, settings, ringDistance / .375f, float.PositiveInfinity);
Assert.True(!root.Empty && root.Children.Count == 0 && Math.Abs(root.Mass - 2.4f * mass) <= 1e-3f * mass,
$"the ring stood at {ringDistance} with a member grown to {Math.Max(heavy.Mass, light.Mass)}");
}
// A ring member's children end within a fifth of the chord to the member's nearest partner, where the chord, not the member's
// fade, is what holds them.
[Fact]