Mining Cuts 1-2: pin five chunk-surface rules that survived mutation

Every mutant of the Cut 1-2 chunk surface (Zone.ChunkExists, ChunkRadius,
Wear, EvaluateBelt, ChunkPose, PackZone/unpack, AsteroidBelt.Pose and its
sizes, ChunkId) was run against MiningCut1Tests and MiningCut2Tests. Six
survived; five are behaviour and get a test each:

- ChunkExists is false for a body that is not a debris field (a planet) and
  for a key naming no body.
- A hit on the exact respawn instant lands on the live chunk (Wear agrees
  with ChunkExists and ChunkRadius about the boundary).
- A chunk worn after respawning packs with no BrokenUntil.
- ChunkPose carries the worn size, and zero for a broken chunk.
- Chunk rotation wraps at one turn (the fixture never reached one before).

The Cut 2 fixture gains a non-belt body, and its settings the three curves
that body's constructor reads. The sixth survivor, a ChunkId hash that
ignores the index, changes no observable behaviour.

Tests only; no production change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-09-30 00:00:30 +02:00
commit 22fb402139
2 changed files with 107 additions and 3 deletions

View file

@ -149,6 +149,28 @@ public sealed class MiningCut1Tests : IDisposable
}
}
// The fixture's rotation speeds are under 1.1 rad/s, so the accumulated ~2 s of the test above never
// reaches one turn and cannot tell a wrapped rotation from an unwrapped one. Forty seconds turns the
// faster asteroids more than once.
[Fact]
public void ChunkRotationWrapsAtOneTurn()
{
var (zone, beltKey, asteroids, settings) = BuildFixture();
zone.Update(40f);
var parentPosition = OrbitPosition(40.0, RootDistance, RootPhase, settings);
var wrapped = 0;
for (var i = 0; i < asteroids.Length; i++)
{
if (40.0 * asteroids[i].RotationSpeed > PI * 2) wrapped++;
var expected = ExpectedPose(40.0, asteroids[i], parentPosition, settings, settings.AsteroidSize.Evaluate(asteroids[i].Size));
var actual = zone.ChunkPose(beltKey, i);
AssertClose(expected.z, actual.z, 0.001f, $"asteroid {i} rotation");
Assert.InRange(actual.z, 0f, PI * 2);
}
Assert.True(wrapped > 0, "sanity: the fixture must turn at least one asteroid past one full rotation");
}
[Fact]
public void ChunkPoseDoesNotDependOnUpdateCadence()
{

View file

@ -43,7 +43,11 @@ public sealed class MiningCut2Tests : IDisposable
OrbitPeriod = new ExponentialCurve { Multiplier = 0.6f, Exponent = 1.3f, Constant = 40f },
AsteroidSize = new ExponentialLerp { Minimum = 2f, Maximum = 9f, Exponent = 1.7f },
AsteroidHitpoints = new ExponentialLerp { Minimum = 20f, Maximum = 220f, Exponent = 1.6f },
AsteroidRespawnTime = new ExponentialLerp { Minimum = 8f, Maximum = 96f, Exponent = 1.2f }
AsteroidRespawnTime = new ExponentialLerp { Minimum = 8f, Maximum = 96f, Exponent = 1.2f },
// The zone builds a Planet for the fixture's non-belt body; its constructor reads these three.
BodyRadius = new ExponentialCurve(),
GravityRadius = new ExponentialCurve(),
GravityDepth = new ExponentialCurve()
};
private sealed class Fixture
@ -53,6 +57,7 @@ public sealed class MiningCut2Tests : IDisposable
public PlanetSettings Settings;
public CultRecordKey BeltA;
public CultRecordKey BeltB;
public CultRecordKey Planet;
public Asteroid[] AsteroidsA;
public Asteroid[] AsteroidsB;
}
@ -92,6 +97,8 @@ public sealed class MiningCut2Tests : IDisposable
var beltAKey = MakeBelt(9, 700f, 0.05f);
var beltBKey = MakeBelt(5, 1600f, 0.61f);
// A body that is not a debris field: a chunk of it must not exist, whatever its index.
var planetKey = cache.Upsert(new PlanetData { Orbit = new CultRecordRef<OrbitData>(rootKey) }).Key;
var beltAData = (AsteroidBeltData) cache.Get(beltAKey);
var beltBData = (AsteroidBeltData) cache.Get(beltBKey);
@ -99,7 +106,7 @@ public sealed class MiningCut2Tests : IDisposable
var pack = new ZonePack
{
Orbits = { new CultRecordRef<OrbitData>(rootKey) },
Planets = { new CultRecordRef<BodyData>(beltAKey), new CultRecordRef<BodyData>(beltBKey) },
Planets = { new CultRecordRef<BodyData>(beltAKey), new CultRecordRef<BodyData>(beltBKey), new CultRecordRef<BodyData>(planetKey) },
Radius = 5000f,
Mass = 10000f,
Time = 0
@ -114,7 +121,7 @@ public sealed class MiningCut2Tests : IDisposable
return new Fixture
{
Zone = zone, Items = items, Settings = settings,
BeltA = beltAKey, BeltB = beltBKey,
BeltA = beltAKey, BeltB = beltBKey, Planet = planetKey,
AsteroidsA = beltAData.Asteroids, AsteroidsB = beltBData.Asteroids
};
}
@ -428,6 +435,81 @@ public sealed class MiningCut2Tests : IDisposable
var stillPacked = pack.ChunkWear?.Exists(w => w.Field.Equals(chunk.Field) && w.Index == chunk.Index) ?? false;
Assert.False(stillPacked, "at exactly the respawn instant the entry already carries no live information");
}
// Soul-debt pass (mining-cut-refresh A.1 item 11): the rules below were unpinned when every mutant of the
// chunk surface was run against the Cut 1-2 tests. Each fails under its own mutation.
[Fact]
public void OnlyADebrisFieldHasChunks()
{
var f = BuildFixture();
Assert.True(f.Zone.Planets.ContainsKey(f.Planet), "sanity: the fixture's planet is a body of the zone");
Assert.False(f.Zone.ChunkExists(new ChunkId(f.Planet, 0)), "a planet is a body, not a field of chunks");
Assert.False(f.Zone.ChunkExists(new ChunkId(new CultRecordKey(Guid.NewGuid().ToString("N")), 0)), "a key that names no body has no chunks");
}
// The respawn instant belongs to the live chunk on every reader: ChunkExists and ChunkRadius already say so
// (RespawnAndPruneBoundaryIsInclusiveOfExactlyNow); Wear, the writer, must agree, or a hit landing on the
// instant is swallowed by a chunk every reader shows.
[Fact]
public void AHitOnTheExactRespawnInstantLandsOnTheLiveChunk()
{
var f = BuildFixture();
const int index = 2;
var chunk = new ChunkId(f.BeltA, index);
var hp = f.Settings.AsteroidHitpoints.Evaluate(f.AsteroidsA[index].Size);
var respawn = f.Settings.AsteroidRespawnTime.Evaluate(f.AsteroidsA[index].Size);
Assert.True(f.Zone.Wear(chunk, hp + 1f));
f.Zone.Update(respawn);
Assert.True(f.Zone.ChunkExists(chunk));
Assert.True(f.Zone.Wear(chunk, hp + 1f), "a hit on the instant of respawn breaks the respawned chunk again");
Assert.False(f.Zone.ChunkExists(chunk));
}
// A chunk that has respawned and been worn again is damaged and not broken. Its saved wear says so: no
// respawn time is carried over from the break it already recovered from.
[Fact]
public void WearAfterRespawnPacksWithoutTheOldRespawnTime()
{
var f = BuildFixture();
const int index = 1;
var chunk = new ChunkId(f.BeltA, index);
var hp = f.Settings.AsteroidHitpoints.Evaluate(f.AsteroidsA[index].Size);
var respawn = f.Settings.AsteroidRespawnTime.Evaluate(f.AsteroidsA[index].Size);
Assert.True(f.Zone.Wear(chunk, hp + 1f));
f.Zone.Update(respawn + 1f);
Assert.False(f.Zone.Wear(chunk, hp * 0.3f));
var packed = f.Zone.PackZone().ChunkWear.Find(w => w.Field.Equals(chunk.Field) && w.Index == index);
Assert.NotNull(packed);
Assert.Equal(hp * 0.3f, packed.Damage, 4);
Assert.Null(packed.BrokenUntil);
}
// ChunkPose is the sim's read of a chunk's size as well as its place: worn chunks are smaller, broken ones
// have none. Reading the undamaged size here would show a shot-out rock at full size to everything that
// asks through ChunkPose.
[Fact]
public void ChunkPoseCarriesTheChunksWornSize()
{
var f = BuildFixture();
const int index = 3;
var chunk = new ChunkId(f.BeltA, index);
var hp = f.Settings.AsteroidHitpoints.Evaluate(f.AsteroidsA[index].Size);
var undamaged = f.Zone.ChunkPose(f.BeltA, index).w;
Assert.Equal(f.Zone.AsteroidBelts[f.BeltA].UndamagedSize(index, f.Settings), undamaged, 4);
Assert.False(f.Zone.Wear(chunk, hp * 0.6f));
var worn = f.Zone.ChunkPose(f.BeltA, index).w;
Assert.Equal(f.Zone.ChunkRadius(chunk), worn, 4);
Assert.True(worn < undamaged, "a worn chunk is smaller than an untouched one");
Assert.True(f.Zone.Wear(chunk, hp));
Assert.Equal(0f, f.Zone.ChunkPose(f.BeltA, index).w);
}
}
internal static class MiningCut2TestExtensions