using Theriapolis.Core; using Theriapolis.Core.Data; using Theriapolis.Core.Rules.Reputation; using Theriapolis.Core.World; using Xunit; namespace Theriapolis.Tests.Reputation; /// /// Phase 6 M5 — propagation correctness: distance-band decay, opposition /// cascade, frontier coin-flip determinism, NEMESIS/CHAMPION bypass. /// public sealed class RepPropagationTests { private static IReadOnlyDictionary Factions() => new ContentResolver(new ContentLoader(TestHelpers.DataDirectory)).Factions; private static Settlement Sett(int id, int x, int y) => new() { Id = id, Name = $"S{id}", Tier = 3, TileX = x, TileY = y, }; [Fact] public void BandFor_MapsTilesToBands() { Assert.Equal(RepPropagation.DistanceBand.Origin, RepPropagation.BandFor(0)); Assert.Equal(RepPropagation.DistanceBand.Adjacent, RepPropagation.BandFor(1)); Assert.Equal(RepPropagation.DistanceBand.Adjacent, RepPropagation.BandFor(C.REP_ADJACENT_DIST_TILES)); Assert.Equal(RepPropagation.DistanceBand.Regional, RepPropagation.BandFor(C.REP_ADJACENT_DIST_TILES + 1)); Assert.Equal(RepPropagation.DistanceBand.Regional, RepPropagation.BandFor(C.REP_REGIONAL_DIST_TILES)); Assert.Equal(RepPropagation.DistanceBand.Continental, RepPropagation.BandFor(C.REP_REGIONAL_DIST_TILES + 1)); Assert.Equal(RepPropagation.DistanceBand.Continental, RepPropagation.BandFor(C.REP_CONTINENTAL_DIST_TILES)); Assert.Equal(RepPropagation.DistanceBand.Frontier, RepPropagation.BandFor(C.REP_CONTINENTAL_DIST_TILES + 1)); } [Fact] public void DecayPctFor_HasMonotonicallyDecreasingValues() { Assert.True(RepPropagation.DecayPctFor(RepPropagation.DistanceBand.Origin) > RepPropagation.DecayPctFor(RepPropagation.DistanceBand.Adjacent)); Assert.True(RepPropagation.DecayPctFor(RepPropagation.DistanceBand.Adjacent) > RepPropagation.DecayPctFor(RepPropagation.DistanceBand.Regional)); Assert.True(RepPropagation.DecayPctFor(RepPropagation.DistanceBand.Regional) > RepPropagation.DecayPctFor(RepPropagation.DistanceBand.Continental)); Assert.True(RepPropagation.DecayPctFor(RepPropagation.DistanceBand.Continental) > RepPropagation.DecayPctFor(RepPropagation.DistanceBand.Frontier)); } [Fact] public void LocalStanding_AtOrigin_FullMagnitude() { var ledger = new RepLedger(); ledger.Append(new RepEvent { FactionId = "inheritors", Magnitude = 20, OriginTileX = 100, OriginTileY = 100, }); var s = Sett(1, 100, 100); Assert.Equal(20, RepPropagation.LocalStandingFor("inheritors", s, 0xCAFEUL, ledger, Factions())); } [Fact] public void LocalStanding_DecaysWithDistance() { var f = Factions(); var ledger = new RepLedger(); ledger.Append(new RepEvent { FactionId = "inheritors", Magnitude = 50, // (just under the bypass threshold) OriginTileX = 100, OriginTileY = 100, }); // Adjacent: 80% of 50 = 40 // Wait — 50 is exactly at threshold; let's use 49 to test decay. ledger = new RepLedger(); ledger.Append(new RepEvent { FactionId = "inheritors", Magnitude = 49, OriginTileX = 100, OriginTileY = 100, }); int origin = RepPropagation.LocalStandingFor("inheritors", Sett(1, 100, 100), 0xCAFEUL, ledger, f); int adjacent = RepPropagation.LocalStandingFor("inheritors", Sett(2, 110, 100), 0xCAFEUL, ledger, f); // 10 tiles int regional = RepPropagation.LocalStandingFor("inheritors", Sett(3, 150, 100), 0xCAFEUL, ledger, f); // 50 tiles int continental= RepPropagation.LocalStandingFor("inheritors", Sett(4, 250, 100), 0xCAFEUL, ledger, f); // 150 tiles Assert.Equal(49, origin); Assert.Equal((int)System.Math.Round(49 * 0.80f), adjacent); Assert.Equal((int)System.Math.Round(49 * 0.60f), regional); Assert.Equal((int)System.Math.Round(49 * 0.40f), continental); } [Fact] public void Cascade_AppliesOppositionMatrix() { // Player gains +20 with Inheritors at (100, 100). The Enforcers // hate this (mult -0.5) → -10 should cascade to Enforcer standing // even at origin. var f = Factions(); var ledger = new RepLedger(); ledger.Append(new RepEvent { FactionId = "inheritors", Magnitude = 20, OriginTileX = 100, OriginTileY = 100, }); int enforcerLocal = RepPropagation.LocalStandingFor("covenant_enforcers", Sett(1, 100, 100), 0xCAFEUL, ledger, f); Assert.Equal(-10, enforcerLocal); } [Fact] public void ExtremeMagnitude_BypassesDecay() { var f = Factions(); var ledger = new RepLedger(); ledger.Append(new RepEvent { FactionId = "inheritors", Magnitude = 60, // ≥ REP_EXTREME_BYPASS_MAGNITUDE OriginTileX = 0, OriginTileY = 0, }); // Settlement on the frontier (>200 tiles away). int far = RepPropagation.LocalStandingFor("inheritors", Sett(99, 250, 250), 0xCAFEUL, ledger, f); Assert.Equal(60, far); } [Fact] public void FrontierDelivered_IsDeterministic() { // Same seed, same event id, same settlement id → same answer. bool a = RepPropagation.FrontierDelivered(0xCAFEUL, 1, 5); bool b = RepPropagation.FrontierDelivered(0xCAFEUL, 1, 5); Assert.Equal(a, b); // Vary one input → may differ; just confirm we don't always hit one branch. int trues = 0, falses = 0; for (int i = 1; i <= 100; i++) { if (RepPropagation.FrontierDelivered(0xCAFEUL, i, 5)) trues++; else falses++; } Assert.True(trues > 20 && falses > 20, $"Expected roughly 50/50 distribution; got trues={trues}, falses={falses}"); } [Fact] public void FrontierEvent_OnlyAppliesWhenDelivered() { var f = Factions(); var ledger = new RepLedger(); ledger.Append(new RepEvent { SequenceId = 0, // assigned by Append → 1 FactionId = "inheritors", Magnitude = 20, OriginTileX = 0, OriginTileY = 0, }); // Frontier settlement. var s = Sett(1, 250, 250); int got = RepPropagation.LocalStandingFor("inheritors", s, 0xCAFEUL, ledger, f); // Either 0 (not delivered) or +4 (20 × 20%). Assert.True(got == 0 || got == (int)System.Math.Round(20 * 0.20f), $"frontier delivery should be 0 or {System.Math.Round(20 * 0.20f)}, got {got}"); } [Fact] public void ExplainLocalStanding_ReturnsRecentEvents() { var f = Factions(); var ledger = new RepLedger(); ledger.Append(new RepEvent { FactionId = "inheritors", Magnitude = 10, Note = "first", OriginTileX = 100, OriginTileY = 100, }); ledger.Append(new RepEvent { FactionId = "inheritors", Magnitude = -5, Note = "second", OriginTileX = 100, OriginTileY = 100, }); var s = Sett(1, 100, 100); var explained = RepPropagation.ExplainLocalStanding("inheritors", s, 0xCAFEUL, ledger, f, max: 8).ToList(); Assert.Equal(2, explained.Count); // Most-recent-first. Assert.Equal("second", explained[0].Event.Note); Assert.Equal("first", explained[1].Event.Note); } }