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

86 lines
2.7 KiB
C#

using Theriapolis.Core.Rules.Combat;
using Theriapolis.Core.Rules.Stats;
using Theriapolis.Core.Util;
using Xunit;
namespace Theriapolis.Tests.Combat;
public sealed class DamageRollTests
{
[Theory]
[InlineData("1d6", 1, 6, 0)]
[InlineData("2d8+2", 2, 8, 2)]
[InlineData("1d4-1", 1, 4, -1)]
[InlineData("3d6", 3, 6, 0)]
[InlineData("d8", 1, 8, 0)]
[InlineData(" 1 d 6 + 1", 1, 6, 1)]
public void Parse_ProducesExpectedShape(string expr, int n, int sides, int mod)
{
var d = DamageRoll.Parse(expr, DamageType.Slashing);
Assert.Equal(n, d.DiceCount);
Assert.Equal(sides, d.DiceSides);
Assert.Equal(mod, d.FlatMod);
}
[Fact]
public void Parse_PureFlatNumber_HasNoDice()
{
var d = DamageRoll.Parse("5", DamageType.Bludgeoning);
Assert.Equal(0, d.DiceCount);
Assert.Equal(0, d.DiceSides);
Assert.Equal(5, d.FlatMod);
}
[Fact]
public void Parse_BadExpressionThrows()
{
Assert.Throws<System.FormatException>(() => DamageRoll.Parse("1d", DamageType.Slashing));
Assert.Throws<System.ArgumentException>(() => DamageRoll.Parse("", DamageType.Slashing));
}
[Fact]
public void Roll_Range_StaysWithinMinAndMax()
{
var rng = new SeededRng(0xCAFEUL);
var d = DamageRoll.Parse("2d6+3", DamageType.Slashing);
for (int i = 0; i < 1000; i++)
{
int v = d.Roll(sides => (int)(rng.NextUInt64() % (ulong)sides) + 1);
Assert.InRange(v, d.Min(), d.Max());
}
}
[Fact]
public void Roll_Crit_DoublesDiceButNotFlatMod()
{
var roller = new FixedRoller(new[] { 1 }); // every die rolls 1
var d = DamageRoll.Parse("2d6+3", DamageType.Slashing);
// Normal: 2 dice ⇒ 2*1 + 3 = 5
Assert.Equal(5, d.Roll(roller.Next));
// Crit: 4 dice ⇒ 4*1 + 3 = 7 (NOT 4*1 + 6 = 10 — flat mod doesn't double)
roller.Reset();
Assert.Equal(7, d.Roll(roller.Next, isCrit: true));
}
[Fact]
public void Roll_NeverNegative()
{
var d = DamageRoll.Parse("1d4-10", DamageType.Slashing);
for (int i = 0; i < 50; i++)
{
// Force the die to roll 1 (the worst case): result would be 1-10 = -9, clamped to 0.
int v = d.Roll(_ => 1);
Assert.True(v >= 0);
}
}
private sealed class FixedRoller
{
private readonly int[] _values;
private int _idx;
public FixedRoller(int[] values) { _values = values; }
public int Next(int _) { int v = _values[_idx % _values.Length]; _idx++; return v; }
public void Reset() => _idx = 0;
}
}