86 lines
2.7 KiB
C#
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;
|
||
|
|
}
|
||
|
|
}
|