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>
This commit is contained in:
@@ -0,0 +1,195 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user