Files
Christopher Wiebe b451f83174 Initial commit: Theriapolis baseline at port/godot branch point
Captures the pre-Godot-port state of the codebase. This is the rollback
anchor for the Godot port (M0 of theriapolis-rpg-implementation-plan-godot-port.md).
All Phase 0 through Phase 6.5 work is included; Phase 7 is in flight.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-30 20:40:51 -07:00

196 lines
7.7 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using Theriapolis.Core;
using Theriapolis.Core.Data;
using Theriapolis.Core.Rules.Reputation;
using Theriapolis.Core.World;
using Xunit;
namespace Theriapolis.Tests.Reputation;
/// <summary>
/// Phase 6 M5 — propagation correctness: distance-band decay, opposition
/// cascade, frontier coin-flip determinism, NEMESIS/CHAMPION bypass.
/// </summary>
public sealed class RepPropagationTests
{
private static IReadOnlyDictionary<string, FactionDef> 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);
}
}