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>
82 lines
2.9 KiB
C#
82 lines
2.9 KiB
C#
using Theriapolis.Core;
|
|
using Theriapolis.Core.Data;
|
|
using Theriapolis.Core.Loot;
|
|
using Theriapolis.Core.Util;
|
|
using Xunit;
|
|
|
|
namespace Theriapolis.Tests.Loot;
|
|
|
|
public sealed class LootRollerTests
|
|
{
|
|
private readonly ContentResolver _content = new(new ContentLoader(TestHelpers.DataDirectory));
|
|
|
|
[Fact]
|
|
public void LootTables_LoadCleanly()
|
|
{
|
|
Assert.True(_content.LootTables.Count >= 9, $"expected ≥9 loot tables, got {_content.LootTables.Count}");
|
|
Assert.Contains("loot_brigand_low", _content.LootTables.Keys);
|
|
Assert.Contains("loot_brigand_high", _content.LootTables.Keys);
|
|
Assert.Contains("loot_wild_low", _content.LootTables.Keys);
|
|
}
|
|
|
|
[Fact]
|
|
public void Roll_UnknownTable_ReturnsEmpty()
|
|
{
|
|
var rng = new SeededRng(0xCAFEUL);
|
|
var drops = LootRoller.Roll("not_a_table", _content.LootTables, _content.Items, rng);
|
|
Assert.Empty(drops);
|
|
}
|
|
|
|
[Fact]
|
|
public void Roll_EmptyTableId_ReturnsEmpty()
|
|
{
|
|
var rng = new SeededRng(0xCAFEUL);
|
|
var drops = LootRoller.Roll("", _content.LootTables, _content.Items, rng);
|
|
Assert.Empty(drops);
|
|
}
|
|
|
|
[Fact]
|
|
public void Roll_SameSeed_ProducesSameDrops()
|
|
{
|
|
var a = LootRoller.Roll("loot_brigand_high", _content.LootTables, _content.Items, new SeededRng(42UL));
|
|
var b = LootRoller.Roll("loot_brigand_high", _content.LootTables, _content.Items, new SeededRng(42UL));
|
|
Assert.Equal(a.Count, b.Count);
|
|
for (int i = 0; i < a.Count; i++)
|
|
{
|
|
Assert.Equal(a[i].Def.Id, b[i].Def.Id);
|
|
Assert.Equal(a[i].Qty, b[i].Qty);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Roll_QtyAlwaysWithinBounds()
|
|
{
|
|
// Roll many times; verify no result has qty outside the table-defined range.
|
|
var table = _content.LootTables["loot_brigand_high"];
|
|
var bounds = table.Drops.ToDictionary(d => d.ItemId, d => (d.QtyMin, d.QtyMax));
|
|
for (int seed = 0; seed < 50; seed++)
|
|
{
|
|
var drops = LootRoller.Roll("loot_brigand_high", _content.LootTables, _content.Items, new SeededRng((ulong)seed));
|
|
foreach (var drop in drops)
|
|
{
|
|
var (mn, mx) = bounds[drop.Def.Id];
|
|
Assert.InRange(drop.Qty, mn, mx);
|
|
}
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Roll_OneHundredSamples_AverageDropCountIsReasonable()
|
|
{
|
|
// The "high" brigand table has 5 drops with cumulative chance summing
|
|
// around 1.95. Across 100 samples expect ≥50 total drops.
|
|
int totalDrops = 0;
|
|
for (int seed = 0; seed < 100; seed++)
|
|
{
|
|
var drops = LootRoller.Roll("loot_brigand_high", _content.LootTables, _content.Items, new SeededRng((ulong)seed));
|
|
totalDrops += drops.Count;
|
|
}
|
|
Assert.True(totalDrops >= 50, $"expected ≥50 drops in 100 samples, got {totalDrops}");
|
|
}
|
|
}
|