b451f83174
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>
196 lines
7.7 KiB
C#
196 lines
7.7 KiB
C#
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);
|
||
}
|
||
}
|