using NUnit.Framework; using ProjectM.Simulation; namespace ProjectM.Tests { /// /// MC-2 pure-math tests for the 4-type weighted composition (ZoneEnemyMath.WaveSlots / KindForSlot / /// PackSizeForSlot) shared by both enemy directors. Deterministic integer math (no ECS world). The PARITY test /// pins that the legacy band reproduces the old 2-type WaveSize/IsChargerSlot EXACTLY, so the base-siege size + /// composition is provably controlled where it must be (the fork-4a safety net). /// public class ZoneEnemyMixTests { static MixBands Bands(int g, int c, int sp, int sw, int cPer, int spPer, int swPer, int packPer = 0) => new MixBands { GruntBase = g, ChargerBase = c, SpitterBase = sp, SwarmerSlotBase = sw, ChargerPerEpoch = cPer, SpitterPerEpoch = spPer, SwarmerSlotPerEpoch = swPer, SwarmerPackPerEpoch = packPer, }; [Test] public void WaveSlots_LowerBoundedAtOne_AndSumsTheBands() { Assert.AreEqual(1, ZoneEnemyMath.WaveSlots(1, Bands(0, 0, 0, 0, 0, 0, 0)), "empty band still yields a fight"); Assert.AreEqual(5, ZoneEnemyMath.WaveSlots(1, Bands(4, 1, 0, 0, 1, 0, 0)), "4 grunts + 1 charger at epoch 1"); Assert.AreEqual(7, ZoneEnemyMath.WaveSlots(3, Bands(4, 1, 0, 0, 1, 0, 0)), "epoch 3: +1 charger/epoch -> 4+(1+2)"); Assert.AreEqual(4 + 2 + 1 + 1, ZoneEnemyMath.WaveSlots(2, Bands(4, 1, 0, 0, 1, 1, 1)), "epoch 2: 4 grunts + 2 chargers + 1 spitter + 1 swarmer-slot"); } [Test] public void KindForSlot_Deterministic() { var b = Bands(4, 1, 1, 1, 1, 1, 1); for (int slot = 0; slot < 30; slot++) Assert.AreEqual(ZoneEnemyMath.KindForSlot(5, slot, b), ZoneEnemyMath.KindForSlot(5, slot, b), "stable per (epoch,slot)"); } [Test] public void KindForSlot_GruntFloorFixed_ThreatsGrowWithEpoch() { var b = Bands(4, 1, 0, 0, 1, 0, 0); // grunts fixed at 4, chargers grow CountKinds(b, 1, out int g1, out int c1, out int _, out int _); Assert.AreEqual(4, g1); Assert.AreEqual(1, c1); CountKinds(b, 5, out int g5, out int c5, out int _, out int _); Assert.AreEqual(4, g5, "grunt count is a fixed floor"); Assert.AreEqual(5, c5, "chargers = base + (epoch-1)"); } [Test] public void KindForSlot_ParityWithLegacyIsChargerSlot() { for (int g = 0; g <= 6; g++) for (int c = 0; c <= 4; c++) for (int e = 1; e <= 6; e++) { var b = Bands(g, c, 0, 0, 1, 0, 0); // legacy band: charger ramps +1/epoch, no spitter/swarmer int size = ZoneEnemyMath.WaveSlots(e, b); Assert.AreEqual(ZoneEnemyMath.WaveSize(e, g, c), size, $"WaveSlots vs WaveSize g{g} c{c} e{e}"); for (int slot = 0; slot < size + 3; slot++) { bool legacy = ZoneEnemyMath.IsChargerSlot(e, slot, g, c); bool now = ZoneEnemyMath.KindForSlot(e, slot, b) == ZoneEnemyMath.KindCharger; Assert.AreEqual(legacy, now, $"parity g{g} c{c} e{e} slot{slot}"); } } } [Test] public void PackSizeForSlot_FixedByDefault_RampsWhenSet() { var fixedBand = Bands(0, 0, 0, 1, 0, 0, 1); Assert.AreEqual(4, ZoneEnemyMath.PackSizeForSlot(1, 0, fixedBand, 4), "base pack"); Assert.AreEqual(4, ZoneEnemyMath.PackSizeForSlot(5, 0, fixedBand, 4), "no ramp -> fixed across epochs"); var rampBand = Bands(0, 0, 0, 1, 0, 0, 1, packPer: 2); Assert.AreEqual(4 + 2 * 2, ZoneEnemyMath.PackSizeForSlot(3, 0, rampBand, 4), "epoch 3 ramp +2*(3-1)"); Assert.GreaterOrEqual(ZoneEnemyMath.PackSizeForSlot(1, 0, fixedBand, 0), 1, "lower-bounded at 1"); } static void CountKinds(MixBands b, int epoch, out int g, out int c, out int sp, out int sw) { g = c = sp = sw = 0; int size = ZoneEnemyMath.WaveSlots(epoch, b); for (int slot = 0; slot < size; slot++) { byte k = ZoneEnemyMath.KindForSlot(epoch, slot, b); if (k == ZoneEnemyMath.KindGrunt) g++; else if (k == ZoneEnemyMath.KindCharger) c++; else if (k == ZoneEnemyMath.KindSpitter) sp++; else sw++; } } } }