├── DnDGen.RollGen ├── 18531-twenty-sided-dice-vector-thumb.png ├── Limits.cs ├── Expressions │ ├── ExpressionEvaluator.cs │ ├── AlbatrossExpressionEvaluator.cs │ └── RegexConstants.cs ├── PartialRolls │ ├── PartialRollFactory.cs │ ├── DomainPartialRollFactory.cs │ └── Roll.cs ├── IoC │ ├── RollGenModuleLoader.cs │ ├── DiceFactory.cs │ └── Modules │ │ └── CoreModule.cs ├── RollPrototype.cs ├── Utils.cs ├── DnDGen.RollGen.csproj ├── Dice.cs ├── PartialRoll.cs ├── RollCollection.cs ├── DomainDice.cs └── RollHelper.cs ├── DnDGen.RollGen.Tests.Integration.Stress ├── d2Tests.cs ├── d3Tests.cs ├── d4Tests.cs ├── d6Tests.cs ├── d8Tests.cs ├── d10Tests.cs ├── d12Tests.cs ├── d20Tests.cs ├── PercentileTests.cs ├── DnDGen.RollGen.Tests.Integration.Stress.csproj ├── StressTests.cs ├── RandomSeedTests.cs ├── ProvidedDiceTests.cs ├── RollHelperTests.cs ├── dTests.cs ├── KeepTests.cs ├── ExplodeTests.cs ├── TransformTests.cs ├── OperatorTests.cs └── ChainTests.cs ├── .gitattributes ├── DnDGen.RollGen.Tests.Unit ├── DnDGen.RollGen.Tests.Unit.csproj ├── LimitsTests.cs ├── UtilsTests.cs ├── RollPrototypeTests.cs ├── PartialRolls │ └── DomainPartialRollFactoryTests.cs └── Expressions │ ├── AlbatrossExpressionEvaluatorTests.cs │ └── RegexConstantsTests.cs ├── DnDGen.RollGen.Tests.Integration ├── DnDGen.RollGen.Tests.Integration.csproj ├── IntegrationTests.cs ├── IsValidTests.cs ├── Expressions │ └── ExpressionEvaluatorTests.cs ├── DescribeTests.cs ├── ReadmeTests.cs └── RollHelperTests.cs ├── DnDGen.RollGen.Tests.Integration.IoC ├── RollGenModuleLoaderTests.cs ├── DnDGen.RollGen.Tests.Integration.IoC.csproj ├── DiceFactoryTests.cs └── Modules │ └── CoreModuleTests.cs ├── cicd ├── release.yml └── build.yml ├── LICENSE.md ├── .gitignore ├── DnDGen.RollGen.sln └── README.md /DnDGen.RollGen/18531-twenty-sided-dice-vector-thumb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DnDGen/DnDGen.RollGen/HEAD/DnDGen.RollGen/18531-twenty-sided-dice-vector-thumb.png -------------------------------------------------------------------------------- /DnDGen.RollGen/Limits.cs: -------------------------------------------------------------------------------- 1 | namespace DnDGen.RollGen 2 | { 3 | public static class Limits 4 | { 5 | public const int Quantity = 10_000; 6 | public const int Die = 10_000; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /DnDGen.RollGen/Expressions/ExpressionEvaluator.cs: -------------------------------------------------------------------------------- 1 | namespace DnDGen.RollGen.Expressions 2 | { 3 | internal interface ExpressionEvaluator 4 | { 5 | T Evaluate(string expression); 6 | bool IsValid(string expression); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /DnDGen.RollGen/PartialRolls/PartialRollFactory.cs: -------------------------------------------------------------------------------- 1 | namespace DnDGen.RollGen.PartialRolls 2 | { 3 | internal interface PartialRollFactory 4 | { 5 | PartialRoll Build(int quantity); 6 | PartialRoll Build(string rollExpression); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /DnDGen.RollGen/IoC/RollGenModuleLoader.cs: -------------------------------------------------------------------------------- 1 | using DnDGen.RollGen.IoC.Modules; 2 | using Ninject; 3 | using System.Linq; 4 | 5 | namespace DnDGen.RollGen.IoC 6 | { 7 | public class RollGenModuleLoader 8 | { 9 | public void LoadModules(IKernel kernel) 10 | { 11 | var modules = kernel.GetModules(); 12 | 13 | if (!modules.Any(m => m is CoreModule)) 14 | kernel.Load(); 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /DnDGen.RollGen.Tests.Integration.Stress/d2Tests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | 3 | namespace DnDGen.RollGen.Tests.Integration.Stress 4 | { 5 | [TestFixture] 6 | public class d2Tests : ProvidedDiceTests 7 | { 8 | protected override int die => 2; 9 | protected override PartialRoll GetRoll(int quantity) => Dice.Roll(quantity).d2(); 10 | 11 | [Test] 12 | public void StressD2() 13 | { 14 | stressor.Stress(AssertRoll); 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /DnDGen.RollGen.Tests.Integration.Stress/d3Tests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | 3 | namespace DnDGen.RollGen.Tests.Integration.Stress 4 | { 5 | [TestFixture] 6 | public class d3Tests : ProvidedDiceTests 7 | { 8 | protected override int die => 3; 9 | protected override PartialRoll GetRoll(int quantity) => Dice.Roll(quantity).d3(); 10 | 11 | [Test] 12 | public void StressD3() 13 | { 14 | stressor.Stress(AssertRoll); 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /DnDGen.RollGen.Tests.Integration.Stress/d4Tests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | 3 | namespace DnDGen.RollGen.Tests.Integration.Stress 4 | { 5 | [TestFixture] 6 | public class d4Tests : ProvidedDiceTests 7 | { 8 | protected override int die => 4; 9 | protected override PartialRoll GetRoll(int quantity) => Dice.Roll(quantity).d4(); 10 | 11 | [Test] 12 | public void StressD4() 13 | { 14 | stressor.Stress(AssertRoll); 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /DnDGen.RollGen.Tests.Integration.Stress/d6Tests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | 3 | namespace DnDGen.RollGen.Tests.Integration.Stress 4 | { 5 | [TestFixture] 6 | public class d6Tests : ProvidedDiceTests 7 | { 8 | protected override int die => 6; 9 | protected override PartialRoll GetRoll(int quantity) => Dice.Roll(quantity).d6(); 10 | 11 | [Test] 12 | public void StressD6() 13 | { 14 | stressor.Stress(AssertRoll); 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /DnDGen.RollGen.Tests.Integration.Stress/d8Tests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | 3 | namespace DnDGen.RollGen.Tests.Integration.Stress 4 | { 5 | [TestFixture] 6 | public class d8Tests : ProvidedDiceTests 7 | { 8 | protected override int die => 8; 9 | protected override PartialRoll GetRoll(int quantity) => Dice.Roll(quantity).d8(); 10 | 11 | [Test] 12 | public void StressD8() 13 | { 14 | stressor.Stress(AssertRoll); 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /DnDGen.RollGen.Tests.Integration.Stress/d10Tests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | 3 | namespace DnDGen.RollGen.Tests.Integration.Stress 4 | { 5 | [TestFixture] 6 | public class d10Tests : ProvidedDiceTests 7 | { 8 | protected override int die => 10; 9 | protected override PartialRoll GetRoll(int quantity) => Dice.Roll(quantity).d10(); 10 | 11 | [Test] 12 | public void StressD10() 13 | { 14 | stressor.Stress(AssertRoll); 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /DnDGen.RollGen.Tests.Integration.Stress/d12Tests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | 3 | namespace DnDGen.RollGen.Tests.Integration.Stress 4 | { 5 | [TestFixture] 6 | public class d12Tests : ProvidedDiceTests 7 | { 8 | protected override int die => 12; 9 | protected override PartialRoll GetRoll(int quantity) => Dice.Roll(quantity).d12(); 10 | 11 | [Test] 12 | public void StressD12() 13 | { 14 | stressor.Stress(AssertRoll); 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /DnDGen.RollGen.Tests.Integration.Stress/d20Tests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | 3 | namespace DnDGen.RollGen.Tests.Integration.Stress 4 | { 5 | [TestFixture] 6 | public class d20Tests : ProvidedDiceTests 7 | { 8 | protected override int die => 20; 9 | protected override PartialRoll GetRoll(int quantity) => Dice.Roll(quantity).d20(); 10 | 11 | [Test] 12 | public void StressD20() 13 | { 14 | stressor.Stress(AssertRoll); 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /DnDGen.RollGen.Tests.Integration.Stress/PercentileTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | 3 | namespace DnDGen.RollGen.Tests.Integration.Stress 4 | { 5 | [TestFixture] 6 | public class PercentileTests : ProvidedDiceTests 7 | { 8 | protected override int die => 100; 9 | protected override PartialRoll GetRoll(int quantity) => Dice.Roll(quantity).Percentile(); 10 | 11 | [Test] 12 | public void StressPercentile() 13 | { 14 | stressor.Stress(AssertRoll); 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | *.sln merge=union 7 | *.csproj merge=union 8 | *.vbproj merge=union 9 | *.fsproj merge=union 10 | *.dbproj merge=union 11 | 12 | # Standard to msysgit 13 | *.doc diff=astextplain 14 | *.DOC diff=astextplain 15 | *.docx diff=astextplain 16 | *.DOCX diff=astextplain 17 | *.dot diff=astextplain 18 | *.DOT diff=astextplain 19 | *.pdf diff=astextplain 20 | *.PDF diff=astextplain 21 | *.rtf diff=astextplain 22 | *.RTF diff=astextplain 23 | -------------------------------------------------------------------------------- /DnDGen.RollGen.Tests.Unit/DnDGen.RollGen.Tests.Unit.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net8.0 4 | false 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /DnDGen.RollGen/IoC/DiceFactory.cs: -------------------------------------------------------------------------------- 1 | using Ninject; 2 | 3 | namespace DnDGen.RollGen.IoC 4 | { 5 | public static class DiceFactory 6 | { 7 | private static readonly IKernel kernel = new StandardKernel(); 8 | private static bool modulesLoaded = false; 9 | 10 | public static Dice Create() 11 | { 12 | if (!modulesLoaded) 13 | { 14 | var rollGenModuleLoader = new RollGenModuleLoader(); 15 | rollGenModuleLoader.LoadModules(kernel); 16 | modulesLoaded = true; 17 | } 18 | 19 | return kernel.Get(); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /DnDGen.RollGen.Tests.Integration/DnDGen.RollGen.Tests.Integration.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net8.0 4 | false 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /DnDGen.RollGen.Tests.Integration.IoC/RollGenModuleLoaderTests.cs: -------------------------------------------------------------------------------- 1 | using DnDGen.RollGen.IoC; 2 | using NUnit.Framework; 3 | 4 | namespace DnDGen.RollGen.Tests.Integration.IoC 5 | { 6 | [TestFixture] 7 | public class RollGenModuleLoaderTests : IntegrationTests 8 | { 9 | [Test] 10 | public void ModuleLoaderCanBeRunTwice() 11 | { 12 | //INFO: First time was in the IntegrationTest one-time setup 13 | var rollGenLoader = new RollGenModuleLoader(); 14 | rollGenLoader.LoadModules(kernel); 15 | 16 | var dice = GetNewInstanceOf(); 17 | Assert.That(dice, Is.Not.Null); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /DnDGen.RollGen/IoC/Modules/CoreModule.cs: -------------------------------------------------------------------------------- 1 | using Albatross.Expression; 2 | using DnDGen.RollGen.Expressions; 3 | using DnDGen.RollGen.PartialRolls; 4 | using Ninject.Modules; 5 | using System; 6 | 7 | namespace DnDGen.RollGen.IoC.Modules 8 | { 9 | internal class CoreModule : NinjectModule 10 | { 11 | public override void Load() 12 | { 13 | Bind().ToSelf().InSingletonScope(); 14 | Bind().To(); 15 | Bind().To(); 16 | Bind().To(); 17 | Bind().ToMethod(c => Factory.Instance.Create()); 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /DnDGen.RollGen/RollPrototype.cs: -------------------------------------------------------------------------------- 1 | namespace DnDGen.RollGen 2 | { 3 | internal class RollPrototype 4 | { 5 | public int Quantity { get; set; } 6 | public int Die { get; set; } 7 | public int Multiplier { get; set; } = 1; 8 | 9 | public string Build() 10 | { 11 | if (Multiplier > 1) 12 | return $"({Quantity}d{Die}-{Quantity})*{Multiplier}"; 13 | 14 | return $"{Quantity}d{Die}"; 15 | } 16 | 17 | public override string ToString() 18 | { 19 | return Build(); 20 | } 21 | 22 | public bool IsValid => Quantity > 0 23 | && Quantity <= Limits.Quantity 24 | && Die > 0 25 | && Die <= Limits.Die; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /DnDGen.RollGen.Tests.Integration.IoC/DnDGen.RollGen.Tests.Integration.IoC.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net8.0 4 | false 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /DnDGen.RollGen/Utils.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace DnDGen.RollGen 4 | { 5 | public static class Utils 6 | { 7 | public static T ChangeType(object rawEvaluatedExpression) 8 | { 9 | if (rawEvaluatedExpression is T) 10 | return (T)rawEvaluatedExpression; 11 | 12 | return (T)Convert.ChangeType(rawEvaluatedExpression, typeof(T)); 13 | } 14 | 15 | /// Returns a string of the provided object as boolean, if it is one, otherwise of Type T. 16 | public static string BooleanOrType(object rawEvaluatedExpression) 17 | { 18 | if (rawEvaluatedExpression is bool) 19 | return rawEvaluatedExpression.ToString(); 20 | 21 | return ChangeType(rawEvaluatedExpression).ToString(); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /DnDGen.RollGen.Tests.Integration.Stress/DnDGen.RollGen.Tests.Integration.Stress.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net8.0 4 | false 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /DnDGen.RollGen.Tests.Integration/IntegrationTests.cs: -------------------------------------------------------------------------------- 1 | using DnDGen.RollGen.IoC; 2 | using Ninject; 3 | using NUnit.Framework; 4 | 5 | namespace DnDGen.RollGen.Tests.Integration 6 | { 7 | [TestFixture] 8 | public abstract class IntegrationTests 9 | { 10 | protected IKernel kernel; 11 | 12 | [OneTimeSetUp] 13 | public void IntegrationTestsFixtureSetup() 14 | { 15 | kernel = new StandardKernel(new NinjectSettings() { InjectNonPublic = true }); 16 | 17 | var rollGenLoader = new RollGenModuleLoader(); 18 | rollGenLoader.LoadModules(kernel); 19 | } 20 | 21 | protected T GetNewInstanceOf() 22 | { 23 | return kernel.Get(); 24 | } 25 | 26 | protected T GetNewInstanceOf(string name) 27 | { 28 | return kernel.Get(name); 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /DnDGen.RollGen.Tests.Unit/LimitsTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | 3 | namespace DnDGen.RollGen.Tests.Unit 4 | { 5 | [TestFixture] 6 | public class LimitsTests 7 | { 8 | [Test] 9 | public void QuantityLimit() 10 | { 11 | Assert.That(Limits.Quantity, Is.EqualTo(10_000)); 12 | } 13 | 14 | [Test] 15 | public void DieLimit() 16 | { 17 | Assert.That(Limits.Die, Is.EqualTo(10_000)); 18 | } 19 | 20 | [Test] 21 | public void ProductOfLimitsIsValid() 22 | { 23 | Assert.That(Limits.Quantity * Limits.Die, Is.LessThanOrEqualTo(int.MaxValue)); 24 | } 25 | 26 | [Test] 27 | public void ProductOfExplodedLimitsIsValid() 28 | { 29 | Assert.That(Limits.Quantity * Limits.Die * 10, Is.LessThanOrEqualTo(int.MaxValue)); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /DnDGen.RollGen.Tests.Integration.IoC/DiceFactoryTests.cs: -------------------------------------------------------------------------------- 1 | using DnDGen.RollGen.IoC; 2 | using NUnit.Framework; 3 | 4 | namespace DnDGen.RollGen.Tests.Integration.IoC 5 | { 6 | [TestFixture] 7 | public class DiceFactoryTests 8 | { 9 | [Test] 10 | public void DiceFactoryReturnsDice() 11 | { 12 | var dice = DiceFactory.Create(); 13 | Assert.That(dice, Is.Not.Null); 14 | Assert.That(dice, Is.InstanceOf()); 15 | } 16 | 17 | [Test] 18 | public void DiceFactoryGeneratesMultipleDiceFromSameKernel() 19 | { 20 | var dice1 = DiceFactory.Create(); 21 | var dice2 = DiceFactory.Create(); 22 | 23 | var roll1 = dice1.Roll().d(Limits.Die).AsSum(); 24 | var roll2 = dice2.Roll().d(Limits.Die).AsSum(); 25 | 26 | Assert.That(roll1, Is.Not.EqualTo(roll2)); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /DnDGen.RollGen.Tests.Integration.Stress/StressTests.cs: -------------------------------------------------------------------------------- 1 | using DnDGen.Stress; 2 | using NUnit.Framework; 3 | using System.Reflection; 4 | 5 | namespace DnDGen.RollGen.Tests.Integration.Stress 6 | { 7 | [TestFixture] 8 | public abstract class StressTests : IntegrationTests 9 | { 10 | protected Stressor stressor; 11 | 12 | [OneTimeSetUp] 13 | public void StressSetup() 14 | { 15 | var options = new StressorOptions() 16 | { 17 | RunningAssembly = Assembly.GetExecutingAssembly(), 18 | #if STRESS 19 | IsFullStress = true, 20 | #else 21 | IsFullStress = false, 22 | #endif 23 | }; 24 | 25 | //INFO: Non-stress operations can take up to 12 minutes, or 20% of the 60 minute runtime. 26 | options.TimeLimitPercentage = .80; 27 | 28 | stressor = new Stressor(options); 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /DnDGen.RollGen/PartialRolls/DomainPartialRollFactory.cs: -------------------------------------------------------------------------------- 1 | using DnDGen.RollGen.Expressions; 2 | using System; 3 | 4 | namespace DnDGen.RollGen.PartialRolls 5 | { 6 | internal class DomainPartialRollFactory : PartialRollFactory 7 | { 8 | private readonly Random random; 9 | private readonly ExpressionEvaluator expressionEvaluator; 10 | 11 | public DomainPartialRollFactory(Random random, ExpressionEvaluator expressionEvaluator) 12 | { 13 | this.random = random; 14 | this.expressionEvaluator = expressionEvaluator; 15 | } 16 | 17 | public PartialRoll Build(int quantity) 18 | { 19 | return new DomainPartialRoll(quantity, random, expressionEvaluator); 20 | } 21 | 22 | public PartialRoll Build(string quantity) 23 | { 24 | return new DomainPartialRoll(quantity, random, expressionEvaluator); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /cicd/release.yml: -------------------------------------------------------------------------------- 1 | pool: 2 | vmImage: 'windows-latest' 3 | 4 | trigger: none 5 | pr: none 6 | 7 | resources: 8 | pipelines: 9 | - pipeline: DnDGen.RollGen 10 | source: 'DnDGen.RollGen - Build' 11 | trigger: 12 | branches: 13 | - main 14 | 15 | jobs: 16 | 17 | - deployment: RollGen_Nuget 18 | displayName: Deploy RollGen NuGet Package 19 | environment: Prod 20 | strategy: 21 | runOnce: 22 | deploy: 23 | steps: 24 | - task: NuGetCommand@2 25 | displayName: 'NuGet push' 26 | inputs: 27 | command: push 28 | packagesToPush: '$(Pipeline.Workspace)/**/DnDGen.RollGen.*.nupkg' 29 | nuGetFeedType: external 30 | publishFeedCredentials: NuGet.org 31 | - task: GitHubRelease@1 32 | displayName: 'GitHub release (create)' 33 | inputs: 34 | gitHubConnection: 'github.com_cidthecoatrack' 35 | assets: '$(Pipeline.Workspace)/**/DnDGen.RollGen.*.nupkg' 36 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 DnDGen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /DnDGen.RollGen/DnDGen.RollGen.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16.0.3 4 | net8.0 5 | Karl M. Speer 6 | DnDGen 7 | RollGen 8 | This gives a basic, fluid interface for rolling dice from the D20 system - commonly known to work with Dungeons and Dragons. Die included are d2, d3, d4, d6, d8, d10, d12, d20, Percentile (d100), and custom die rolls. 9 | Apache-2.0 10 | https://github.com/DnDGen/RollGen 11 | https://github.com/DnDGen/RollGen 12 | dice dungeons dragons d20 dnd adnd ioc ninject dependency inject injection roll random 13 | 18531-twenty-sided-dice-vector-thumb.png 14 | 15 | true 16 | Verify CI triggers 17 | 18 | 19 | 20 | True 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /DnDGen.RollGen.Tests.Unit/UtilsTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using System; 3 | 4 | namespace DnDGen.RollGen.Tests.Unit 5 | { 6 | [TestFixture] 7 | public class UtilsTests 8 | { 9 | // Work around NUnit not supporting Generics, by using this function. 10 | private object GenericTester(string methodName, Type T, object[] parameters) 11 | { 12 | var utilsType = typeof(Utils); 13 | var method = utilsType.GetMethod(methodName); 14 | var genericMethod = method.MakeGenericMethod(T); 15 | 16 | return genericMethod.Invoke(null, parameters); 17 | } 18 | 19 | [TestCase(typeof(double), 8)] 20 | [TestCase(typeof(int), true)] 21 | [TestCase(typeof(string), 8)] 22 | public void ChangeType(Type T, object arg) 23 | { 24 | var result = GenericTester("ChangeType", T, new[] { arg }); 25 | Assert.That(result, Is.TypeOf(T)); 26 | } 27 | 28 | [TestCase(typeof(string), "true", "true")] 29 | [TestCase(typeof(double), true, "True")] 30 | [TestCase(typeof(string), false, "False")] 31 | [TestCase(typeof(float), 0.8f, "0.8")] 32 | [TestCase(typeof(int), 0.8f, "1")] 33 | [TestCase(typeof(int), 0.3f, "0")] 34 | public void BooleanOrType(Type T, object arg, string expected) 35 | { 36 | var result = GenericTester("BooleanOrType", T, new[] { arg }); 37 | Assert.That(result, Is.EqualTo(expected)); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /DnDGen.RollGen.Tests.Integration.Stress/RandomSeedTests.cs: -------------------------------------------------------------------------------- 1 | using DnDGen.RollGen.IoC; 2 | using NUnit.Framework; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | 6 | namespace DnDGen.RollGen.Tests.Integration.Stress 7 | { 8 | [TestFixture] 9 | public class RandomSeedTests : StressTests 10 | { 11 | private Dice dice1; 12 | private Dice dice2; 13 | private List dice1Rolls; 14 | private List dice2Rolls; 15 | 16 | [SetUp] 17 | public void Setup() 18 | { 19 | dice1 = GetNewInstanceOf(); 20 | dice2 = GetNewInstanceOf(); 21 | 22 | dice1Rolls = new List(); 23 | dice2Rolls = new List(); 24 | } 25 | 26 | [Test] 27 | public void RollsAreDifferentBetweenDice() 28 | { 29 | stressor.Stress(() => PopulateRolls(dice1, dice2)); 30 | 31 | Assert.That(dice1Rolls, Is.Not.EqualTo(dice2Rolls)); 32 | Assert.That(dice1Rolls.Distinct().Count(), Is.InRange(1000, Limits.Die)); 33 | Assert.That(dice2Rolls.Distinct().Count(), Is.InRange(1000, Limits.Die)); 34 | } 35 | 36 | private void PopulateRolls(Dice dice1, Dice dice2) 37 | { 38 | var firstRoll = dice1.Roll().d(Limits.Die).AsSum(); 39 | dice1Rolls.Add(firstRoll); 40 | 41 | var secondRoll = dice2.Roll().d(Limits.Die).AsSum(); 42 | dice2Rolls.Add(secondRoll); 43 | } 44 | 45 | [Test] 46 | public void RollsAreDifferentBetweenDiceFromFactory() 47 | { 48 | var dice1 = DiceFactory.Create(); 49 | var dice2 = DiceFactory.Create(); 50 | 51 | stressor.Stress(() => PopulateRolls(dice1, dice2)); 52 | 53 | Assert.That(dice1Rolls, Is.Not.EqualTo(dice2Rolls)); 54 | Assert.That(dice1Rolls.Distinct().Count(), Is.InRange(1000, Limits.Die)); 55 | Assert.That(dice2Rolls.Distinct().Count(), Is.InRange(1000, Limits.Die)); 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /DnDGen.RollGen.Tests.Integration.IoC/Modules/CoreModuleTests.cs: -------------------------------------------------------------------------------- 1 | using Albatross.Expression; 2 | using DnDGen.RollGen.Expressions; 3 | using DnDGen.RollGen.PartialRolls; 4 | using DnDGen.RollGen.Tests.Integration; 5 | using Ninject; 6 | using NUnit.Framework; 7 | using System; 8 | 9 | namespace DnDGen.RollGen.Tests.Bootstrap.Modules 10 | { 11 | [TestFixture] 12 | public class CoreModuleTests : IntegrationTests 13 | { 14 | [Test] 15 | public void DiceAreNotCreatedAsSingletons() 16 | { 17 | var dice1 = GetNewInstanceOf(); 18 | var dice2 = GetNewInstanceOf(); 19 | Assert.That(dice1, Is.Not.EqualTo(dice2)); 20 | } 21 | 22 | [Test] 23 | public void RandomIsCreatedAsSingleton() 24 | { 25 | var random1 = GetNewInstanceOf(); 26 | var random2 = GetNewInstanceOf(); 27 | Assert.That(random1, Is.EqualTo(random2)); 28 | } 29 | 30 | [Test] 31 | public void CannotInjectPartialRoll() 32 | { 33 | Assert.That(() => GetNewInstanceOf(), Throws.InstanceOf()); 34 | } 35 | 36 | [Test] 37 | public void ExpressionEvaluatorIsInjected() 38 | { 39 | var evaluator = GetNewInstanceOf(); 40 | Assert.That(evaluator, Is.Not.Null); 41 | Assert.That(evaluator, Is.InstanceOf()); 42 | } 43 | 44 | [Test] 45 | public void PartialRollFactoryIsInjected() 46 | { 47 | var factory = GetNewInstanceOf(); 48 | Assert.That(factory, Is.Not.Null); 49 | Assert.That(factory, Is.InstanceOf()); 50 | } 51 | 52 | [Test] 53 | public void AlbatrossParserIsInjected() 54 | { 55 | var parser = GetNewInstanceOf(); 56 | Assert.That(parser, Is.Not.Null); 57 | Assert.That(parser, Is.InstanceOf()); 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /DnDGen.RollGen.Tests.Integration.Stress/ProvidedDiceTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using System; 3 | using System.Linq; 4 | 5 | namespace DnDGen.RollGen.Tests.Integration.Stress 6 | { 7 | [TestFixture] 8 | public abstract class ProvidedDiceTests : StressTests 9 | { 10 | protected Dice Dice; 11 | private Random random; 12 | 13 | [SetUp] 14 | public void Setup() 15 | { 16 | Dice = GetNewInstanceOf(); 17 | random = GetNewInstanceOf(); 18 | } 19 | 20 | protected abstract int die { get; } 21 | 22 | protected abstract PartialRoll GetRoll(int quantity); 23 | 24 | protected void AssertRoll() 25 | { 26 | var quantity = random.Next(Limits.Quantity) + 1; 27 | var percentageThreshold = random.NextDouble(); 28 | var rollThreshold = random.Next(quantity * die) + 1; 29 | 30 | var roll = GetRoll(quantity); 31 | 32 | AssertRoll(roll, quantity, percentageThreshold, rollThreshold); 33 | } 34 | 35 | private void AssertRoll(PartialRoll roll, int quantity, double percentageThreshold, int rollThreshold) 36 | { 37 | var average = quantity * (die + 1) / 2.0d; 38 | 39 | Assert.That(roll.IsValid(), Is.True); 40 | Assert.That(roll.AsSum(), Is.InRange(quantity, quantity * die)); 41 | Assert.That(roll.AsPotentialMinimum(), Is.EqualTo(quantity)); 42 | Assert.That(roll.AsPotentialMaximum(false), Is.EqualTo(quantity * die)); 43 | Assert.That(roll.AsPotentialMaximum(), Is.EqualTo(quantity * die)); 44 | Assert.That(roll.AsPotentialAverage(), Is.EqualTo(average)); 45 | Assert.That(roll.AsTrueOrFalse(percentageThreshold), Is.True.Or.False, "Percentage"); 46 | Assert.That(roll.AsTrueOrFalse(rollThreshold), Is.True.Or.False, "Roll"); 47 | 48 | var rolls = roll.AsIndividualRolls(); 49 | 50 | Assert.That(rolls.Count(), Is.EqualTo(quantity)); 51 | Assert.That(rolls, Has.All.InRange(1, die)); 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /DnDGen.RollGen/Expressions/AlbatrossExpressionEvaluator.cs: -------------------------------------------------------------------------------- 1 | using Albatross.Expression; 2 | using System; 3 | using System.Text.RegularExpressions; 4 | 5 | namespace DnDGen.RollGen.Expressions 6 | { 7 | internal class AlbatrossExpressionEvaluator : ExpressionEvaluator 8 | { 9 | private readonly IParser parser; 10 | private readonly Regex strictRollRegex; 11 | 12 | public AlbatrossExpressionEvaluator(IParser parser) 13 | { 14 | this.parser = parser; 15 | strictRollRegex = new Regex(RegexConstants.StrictRollPattern); 16 | } 17 | 18 | public T Evaluate(string expression) 19 | { 20 | var match = strictRollRegex.Match(expression); 21 | if (match.Success) 22 | throw new ArgumentException($"Cannot evaluate unrolled die roll {match.Value}"); 23 | 24 | try 25 | { 26 | var unevaluatedMatch = parser.Compile(expression).EvalValue(null); 27 | var evaluatedExpression = Utils.BooleanOrType(unevaluatedMatch); 28 | 29 | return Utils.ChangeType(evaluatedExpression); 30 | } 31 | catch (Exception e) 32 | { 33 | throw new InvalidOperationException($"Expression '{expression}' is invalid", e); 34 | } 35 | } 36 | 37 | public bool IsValid(string expression) 38 | { 39 | //HACK: The "IsValidExpression" method doesn't always handle this correctly and return TRUE when it shouldn't. 40 | //So, have to use ugly error catching instead. 41 | //return parser.IsValidExpression(expression); 42 | 43 | var match = strictRollRegex.Match(expression); 44 | if (match.Success) 45 | return false; 46 | 47 | try 48 | { 49 | var unevaluatedMatch = parser.Compile(expression).EvalValue(null); 50 | return unevaluatedMatch != null; 51 | } 52 | catch 53 | { 54 | return false; 55 | } 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /DnDGen.RollGen.Tests.Integration.Stress/RollHelperTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using System; 3 | using System.Diagnostics; 4 | 5 | namespace DnDGen.RollGen.Tests.Integration.Stress 6 | { 7 | [TestFixture] 8 | public class RollHelperTests : StressTests 9 | { 10 | private Dice dice; 11 | private Random random; 12 | private Stopwatch stopwatch; 13 | 14 | [SetUp] 15 | public void Setup() 16 | { 17 | dice = GetNewInstanceOf(); 18 | random = GetNewInstanceOf(); 19 | stopwatch = new Stopwatch(); 20 | } 21 | 22 | [Test] 23 | public void StressRollWithFewestDice() 24 | { 25 | stressor.Stress(() => AssertGetRoll((int l, int u) => RollHelper.GetRollWithFewestDice(l, u))); 26 | } 27 | 28 | [TestCase(false, false)] 29 | [TestCase(false, true)] 30 | [TestCase(true, false)] 31 | [TestCase(true, true)] 32 | public void StressRollWithMostEvenDistribution(bool multiplier, bool nonstandard) 33 | { 34 | stressor.Stress(() => AssertGetRoll((int l, int u) => RollHelper.GetRollWithMostEvenDistribution(l, u, multiplier, nonstandard))); 35 | } 36 | 37 | private void AssertGetRoll(Func getRoll) 38 | { 39 | var upperLimit = random.Next(2) == 1 ? int.MaxValue : Limits.Die; 40 | var upper = random.Next(upperLimit) + 1; 41 | var lower = random.Next(upper); 42 | 43 | Assert.That(lower, Is.LessThan(upper)); 44 | 45 | stopwatch.Restart(); 46 | var roll = getRoll(lower, upper); 47 | stopwatch.Stop(); 48 | 49 | Assert.That(dice.Roll(roll).IsValid(), Is.True, $"Min: {lower}; Max: {upper}; Roll: {roll}"); 50 | Assert.That(dice.Roll(roll).AsPotentialMinimum(), Is.EqualTo(lower), $"Min: {lower}; Max: {upper}; Roll: {roll}"); 51 | Assert.That(dice.Roll(roll).AsPotentialMaximum(), Is.EqualTo(upper), $"Min: {lower}; Max: {upper}; Roll: {roll}"); 52 | Assert.That(stopwatch.Elapsed, Is.LessThan(TimeSpan.FromSeconds(1)), $"Min: {lower}; Max: {upper}; Roll: {roll}"); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /DnDGen.RollGen.Tests.Integration.Stress/dTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using System; 3 | using System.Linq; 4 | 5 | namespace DnDGen.RollGen.Tests.Integration.Stress 6 | { 7 | [TestFixture] 8 | public class dTests : StressTests 9 | { 10 | private Dice dice; 11 | private Random random; 12 | 13 | [SetUp] 14 | public void Setup() 15 | { 16 | dice = GetNewInstanceOf(); 17 | random = GetNewInstanceOf(); 18 | } 19 | 20 | [Test] 21 | public void StressDie() 22 | { 23 | stressor.Stress(AssertDie); 24 | } 25 | 26 | protected void AssertDie() 27 | { 28 | var quantity = random.Next(Limits.Quantity) + 1; 29 | var die = random.Next(Limits.Die) + 1; 30 | var percentageThreshold = random.NextDouble(); 31 | var rollThreshold = random.Next(quantity * die) + 1; 32 | 33 | var roll = GetRoll(quantity, die); 34 | 35 | AssertRoll(roll, quantity, die, percentageThreshold, rollThreshold); 36 | } 37 | 38 | private void AssertRoll(PartialRoll roll, int quantity, int die, double percentageThreshold, int rollThreshold) 39 | { 40 | var average = quantity * (die + 1) / 2.0d; 41 | var min = quantity; 42 | var max = quantity * die; 43 | 44 | Assert.That(roll.IsValid(), Is.True); 45 | Assert.That(roll.AsSum(), Is.InRange(min, max)); 46 | Assert.That(roll.AsPotentialMinimum(), Is.EqualTo(min)); 47 | Assert.That(roll.AsPotentialMaximum(), Is.EqualTo(max)); 48 | Assert.That(roll.AsPotentialMaximum(true), Is.EqualTo(max)); 49 | Assert.That(roll.AsPotentialAverage(), Is.EqualTo(average)); 50 | Assert.That(roll.AsTrueOrFalse(percentageThreshold), Is.True.Or.False, "Percentage"); 51 | Assert.That(roll.AsTrueOrFalse(rollThreshold), Is.True.Or.False, "Roll"); 52 | 53 | var rolls = roll.AsIndividualRolls(); 54 | 55 | Assert.That(rolls.Count(), Is.EqualTo(quantity)); 56 | Assert.That(rolls, Has.All.InRange(1, die)); 57 | } 58 | 59 | private PartialRoll GetRoll(int quantity, int die) => dice.Roll(quantity).d(die); 60 | } 61 | } -------------------------------------------------------------------------------- /DnDGen.RollGen/Expressions/RegexConstants.cs: -------------------------------------------------------------------------------- 1 | using System.Text.RegularExpressions; 2 | 3 | namespace DnDGen.RollGen.Expressions 4 | { 5 | internal static class RegexConstants 6 | { 7 | public const string NumberPattern = "-?\\d+"; 8 | public const string CommonRollRegexPattern = "d *" + NumberPattern + "(?: *(" 9 | + "( *!)" //explode default 10 | + "|( *(e *" + NumberPattern + "))" //explode specific 11 | + "|( *(t *" + NumberPattern + " *: *" + NumberPattern + "))" //transform specific 12 | + "|( *(t *" + NumberPattern + "))" //transform 13 | + "|( *(k *" + NumberPattern + "))" //keep 14 | + ")*)"; 15 | public const string StrictRollPattern = "(?:(?:\\d* +)|(?:(?<=\\D|^)" + NumberPattern + " *)|^)" + CommonRollRegexPattern; 16 | public const string ExpressionWithoutDieRollsPattern = "(?:[-+]?\\d*\\.?\\d+[%/\\+\\-\\*])+(?:[-+]?\\d*\\.?\\d+)"; 17 | public const string LenientRollPattern = "\\d* *" + CommonRollRegexPattern; 18 | public const string BooleanExpressionPattern = "[<=>]"; 19 | 20 | public static (bool IsMatch, string Match, int Index, int MatchCount) GetRepeatedRoll(string roll, string source) 21 | { 22 | var repeatedRollRegex = new Regex($"(^|[\\+\\(]+){roll}(\\+{roll})+([\\+\\-\\)]+|$)"); 23 | var repeatedMatch = repeatedRollRegex.Match(source); 24 | var matchCount = 0; 25 | var trimmedMatch = string.Empty; 26 | var index = -1; 27 | 28 | if (repeatedMatch.Success) 29 | { 30 | matchCount = new Regex(roll).Matches(repeatedMatch.Value).Count; 31 | trimmedMatch = repeatedMatch.Value; 32 | index = repeatedMatch.Index; 33 | 34 | while (trimmedMatch.EndsWith('+') || trimmedMatch.EndsWith('-') || trimmedMatch.EndsWith(')')) 35 | trimmedMatch = trimmedMatch[..^1]; 36 | 37 | while (trimmedMatch.StartsWith('+') || trimmedMatch.StartsWith('-') || trimmedMatch.StartsWith('(')) 38 | { 39 | trimmedMatch = trimmedMatch[1..trimmedMatch.Length]; 40 | index++; 41 | } 42 | } 43 | 44 | return (repeatedMatch.Success, trimmedMatch, index, matchCount); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /DnDGen.RollGen/Dice.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | [assembly: InternalsVisibleTo("DnDGen.RollGen.Tests.Integration")] 4 | [assembly: InternalsVisibleTo("DnDGen.RollGen.Tests.Integration.IoC")] 5 | [assembly: InternalsVisibleTo("DnDGen.RollGen.Tests.Unit")] 6 | [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] 7 | namespace DnDGen.RollGen 8 | { 9 | public interface Dice 10 | { 11 | PartialRoll Roll(int quantity = 1); 12 | PartialRoll Roll(string rollExpression); 13 | PartialRoll Roll(PartialRoll roll); 14 | /// Replaces dice rolls and other evaluatable expressions in a string that are wrapped by special strings. 15 | /// Type to return Evaluated expressions in 16 | /// Contains expressions to replace and other text. 17 | /// Comes before expressions to replace. Mustn't be empty. 18 | /// Ends expression to replace. Mustn't be empty. 19 | /// Prefix openexpr with this to have the replacer ignore it. If null, there's no escape. 20 | string ReplaceWrappedExpressions(string str, string openexpr = "{", string closeexpr = "}", char? openexprescape = '\\'); 21 | string ReplaceExpressionWithTotal(string expression, bool lenient = false); 22 | string ReplaceRollsWithSumExpression(string expression, bool lenient = false); 23 | bool ContainsRoll(string expression, bool lenient = false); 24 | /// Will return a description of the roll, comparing it percentage-wise to possible rolls for the given roll expression 25 | /// The roll expression that generated the value. 26 | /// The value to be described. 27 | /// A list of descriptions, starting with the worst and ending with the best. Defaults to [Bad, Good] 28 | string Describe(string rollExpression, int roll, params string[] descriptions); 29 | /// Will say whether the roll expression is valid to be parsed by RollGen 30 | /// The roll expression to be validated. 31 | bool IsValid(string rollExpression); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /cicd/build.yml: -------------------------------------------------------------------------------- 1 | pool: 2 | vmImage: 'windows-latest' 3 | 4 | variables: 5 | solution: 'DnDGen.RollGen.sln' 6 | buildPlatform: 'Any CPU' 7 | buildConfiguration: 'Release' 8 | 9 | steps: 10 | - checkout: self 11 | displayName: 'Checkout Code' 12 | persistCredentials: true 13 | - task: DotNetCoreCLI@2 14 | displayName: Build 15 | inputs: 16 | command: build 17 | projects: 'DnDGen.RollGen/DnDGen.RollGen.csproj' 18 | arguments: '--configuration $(buildConfiguration)' 19 | - task: DotNetCoreCLI@2 20 | displayName: Unit Tests 21 | inputs: 22 | command: test 23 | projects: 'DnDGen.RollGen.Tests.Unit/DnDGen.RollGen.Tests.Unit.csproj' 24 | arguments: '-v normal' 25 | - task: DotNetCoreCLI@2 26 | displayName: IoC Tests 27 | inputs: 28 | command: test 29 | projects: 'DnDGen.RollGen.Tests.Integration.IoC/DnDGen.RollGen.Tests.Integration.IoC.csproj' 30 | arguments: '-v normal' 31 | - task: DotNetCoreCLI@2 32 | displayName: Integration Tests 33 | inputs: 34 | command: test 35 | projects: 'DnDGen.RollGen.Tests.Integration/DnDGen.RollGen.Tests.Integration.csproj' 36 | arguments: '-v normal' 37 | - task: DotNetCoreCLI@2 38 | displayName: Stress Tests 39 | inputs: 40 | command: test 41 | projects: 'DnDGen.RollGen.Tests.Integration.Stress/DnDGen.RollGen.Tests.Integration.Stress.csproj' 42 | arguments: '-v normal --configuration Stress' 43 | 44 | - task: PowerShell@2 45 | condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main')) 46 | displayName: 'Get Project Version' 47 | inputs: 48 | targetType: 'inline' 49 | script: | 50 | $xml = [Xml] (Get-Content ./DnDGen.RollGen/DnDGen.RollGen.csproj) 51 | $version = $xml.Project.PropertyGroup.Version 52 | echo $version 53 | echo "##vso[task.setvariable variable=version]$version" 54 | 55 | - script: | 56 | echo "Tagging with version $(version)" 57 | git tag $(version) 58 | git push origin $(version) 59 | condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main')) 60 | displayName: 'Tag Version' 61 | workingDirectory: $(Build.SourcesDirectory) 62 | 63 | - task: PublishBuildArtifacts@1 64 | condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main')) 65 | displayName: Publish Artifacts 66 | inputs: 67 | pathtoPublish: './DnDGen.RollGen/bin/Release' 68 | artifactName: 'dndgen-rollgen' 69 | -------------------------------------------------------------------------------- /DnDGen.RollGen.Tests.Integration.Stress/KeepTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using System; 3 | using System.Linq; 4 | 5 | namespace DnDGen.RollGen.Tests.Integration.Stress 6 | { 7 | [TestFixture] 8 | public class KeepTests : StressTests 9 | { 10 | private Dice dice; 11 | private Random random; 12 | 13 | [SetUp] 14 | public void Setup() 15 | { 16 | dice = GetNewInstanceOf(); 17 | random = GetNewInstanceOf(); 18 | } 19 | 20 | [TestCase(true)] 21 | [TestCase(false)] 22 | public void StressKeep(bool common) 23 | { 24 | stressor.Stress(() => AssertKeep(common)); 25 | } 26 | 27 | protected void AssertKeep(bool common) 28 | { 29 | var quantityLimit = common ? 100 : Limits.Quantity; 30 | var dieLimit = common ? 100 : Limits.Die; 31 | 32 | var quantity = random.Next(quantityLimit) + 1; 33 | var die = random.Next(dieLimit) + 1; 34 | var keep = random.Next(quantity - 1) + 1; 35 | var percentageThreshold = random.NextDouble(); 36 | var rollThreshold = random.Next(quantity * die) + 1; 37 | 38 | var roll = GetRoll(quantity, die, keep); 39 | 40 | AssertRoll(roll, quantity, die, keep, percentageThreshold, rollThreshold); 41 | } 42 | 43 | private void AssertRoll(PartialRoll roll, int quantity, int die, int keep, double percentageThreshold, int rollThreshold) 44 | { 45 | var qMin = Math.Min(quantity, keep); 46 | var average = qMin * (die + 1) / 2.0d; 47 | var min = qMin; 48 | var max = qMin * die; 49 | 50 | Assert.That(roll.IsValid(), Is.True); 51 | Assert.That(roll.AsSum(), Is.InRange(min, max)); 52 | Assert.That(roll.AsPotentialMinimum(), Is.EqualTo(min)); 53 | Assert.That(roll.AsPotentialMaximum(false), Is.EqualTo(max)); 54 | Assert.That(roll.AsPotentialMaximum(), Is.EqualTo(max)); 55 | Assert.That(roll.AsPotentialAverage(), Is.EqualTo(average)); 56 | Assert.That(roll.AsTrueOrFalse(percentageThreshold), Is.True.Or.False, "Percentage"); 57 | Assert.That(roll.AsTrueOrFalse(rollThreshold), Is.True.Or.False, "Roll"); 58 | 59 | var rolls = roll.AsIndividualRolls(); 60 | 61 | Assert.That(rolls.Count(), Is.EqualTo(qMin)); 62 | Assert.That(rolls, Has.All.InRange(1, die)); 63 | } 64 | 65 | private PartialRoll GetRoll(int quantity, int die, int keep) => dice.Roll(quantity).d(die).Keeping(keep); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /DnDGen.RollGen.Tests.Integration.Stress/ExplodeTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using System; 3 | using System.Linq; 4 | 5 | namespace DnDGen.RollGen.Tests.Integration.Stress 6 | { 7 | [TestFixture] 8 | public class ExplodeTests : StressTests 9 | { 10 | private Dice dice; 11 | private Random random; 12 | 13 | [SetUp] 14 | public void Setup() 15 | { 16 | dice = GetNewInstanceOf(); 17 | random = GetNewInstanceOf(); 18 | } 19 | 20 | [TestCase(true)] 21 | [TestCase(false)] 22 | public void StressExplode(bool common) 23 | { 24 | stressor.Stress(() => AssertExplode(common)); 25 | } 26 | 27 | protected void AssertExplode(bool common) 28 | { 29 | var quantityLimit = common ? 100 : Limits.Quantity; 30 | var dieLimit = common ? 100 : Limits.Die; 31 | 32 | var quantity = random.Next(quantityLimit) + 1; 33 | var die = random.Next(dieLimit - 1) + 2; //INFO: Can't allow d1, as explode fails on that 34 | var explode = random.Next(die - 1) + 1; 35 | var percentageThreshold = random.NextDouble(); 36 | var rollThreshold = random.Next(quantity * die) + 1; 37 | 38 | Assert.That(die, Is.InRange(2, dieLimit)); 39 | 40 | var roll = GetRoll(quantity, die, explode); 41 | 42 | AssertRoll(roll, quantity, die, explode, percentageThreshold, rollThreshold); 43 | } 44 | 45 | private void AssertRoll(PartialRoll roll, int quantity, int die, int explode, double percentageThreshold, int rollThreshold) 46 | { 47 | var rollMin = explode == 1 && die != 1 ? 2 : 1; 48 | var min = quantity * rollMin; 49 | var max = quantity * die; 50 | var average = (min + max) / 2.0d; 51 | 52 | Assert.That(roll.IsValid(), Is.True); 53 | Assert.That(roll.AsSum(), Is.InRange(min, max * 10)); 54 | Assert.That(roll.AsPotentialMinimum(), Is.EqualTo(min)); 55 | Assert.That(roll.AsPotentialMaximum(false), Is.EqualTo(max)); 56 | Assert.That(roll.AsPotentialMaximum(), Is.EqualTo(max * 10)); 57 | Assert.That(roll.AsPotentialAverage(), Is.EqualTo(average)); 58 | Assert.That(roll.AsTrueOrFalse(percentageThreshold), Is.True.Or.False, "Percentage"); 59 | Assert.That(roll.AsTrueOrFalse(rollThreshold), Is.True.Or.False, "Roll"); 60 | 61 | var rolls = roll.AsIndividualRolls(); 62 | 63 | Assert.That(rolls.Count(), Is.AtLeast(quantity)); 64 | Assert.That(rolls, Has.All.InRange(1, die)); 65 | } 66 | 67 | private PartialRoll GetRoll(int quantity, int die, int explode) => dice.Roll(quantity).d(die).ExplodeOn(explode); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ################# 2 | ## Eclipse 3 | ################# 4 | 5 | *.pydevproject 6 | .project 7 | .metadata 8 | bin/ 9 | tmp/ 10 | *.tmp 11 | *.bak 12 | *.swp 13 | *~.nib 14 | local.properties 15 | .classpath 16 | .settings/ 17 | .vs/ 18 | .loadpath 19 | 20 | # External tool builders 21 | .externalToolBuilders/ 22 | 23 | # Locally stored "Eclipse launch configurations" 24 | *.launch 25 | 26 | # CDT-specific 27 | .cproject 28 | 29 | # PDT-specific 30 | .buildpath 31 | 32 | 33 | ################# 34 | ## Visual Studio 35 | ################# 36 | 37 | ## Ignore Visual Studio temporary files, build results, and 38 | ## files generated by popular Visual Studio add-ons. 39 | 40 | # User-specific files 41 | *.suo 42 | *.user 43 | *.sln.docstates 44 | 45 | # Build results 46 | [Dd]ebug/ 47 | [Rr]elease/ 48 | *_i.c 49 | *_p.c 50 | *.ilk 51 | *.meta 52 | *.obj 53 | *.pch 54 | *.pdb 55 | *.pgc 56 | *.pgd 57 | *.rsp 58 | *.sbr 59 | *.tlb 60 | *.tli 61 | *.tlh 62 | *.tmp 63 | *.dll 64 | *.vspscc 65 | .builds 66 | *.dotCover 67 | 68 | ## TODO: If you have NuGet Package Restore enabled, uncomment this 69 | #packages/ 70 | 71 | # Visual C++ cache files 72 | ipch/ 73 | *.aps 74 | *.ncb 75 | *.opensdf 76 | *.sdf 77 | 78 | # Visual Studio profiler 79 | *.psess 80 | *.vsp 81 | 82 | # ReSharper is a .NET coding add-in 83 | _ReSharper* 84 | 85 | # Installshield output folder 86 | [Ee]xpress 87 | 88 | # DocProject is a documentation generator add-in 89 | DocProject/buildhelp/ 90 | DocProject/Help/*.HxT 91 | DocProject/Help/*.HxC 92 | DocProject/Help/*.hhc 93 | DocProject/Help/*.hhk 94 | DocProject/Help/*.hhp 95 | DocProject/Help/Html2 96 | DocProject/Help/html 97 | 98 | # Click-Once directory 99 | publish 100 | 101 | # Others 102 | [Bb]in 103 | [Oo]bj 104 | [Pp]ackages 105 | sql 106 | TestResults 107 | *.Cache 108 | ClientBin 109 | stylecop.* 110 | ~$* 111 | *.dbmdl 112 | Generated_Code #added for RIA/Silverlight projects 113 | *.log 114 | 115 | # Backup & report files from converting an old project file to a newer 116 | # Visual Studio version. Backup files are not needed, because we have git ;-) 117 | _UpgradeReport_Files/ 118 | Backup*/ 119 | UpgradeLog*.XML 120 | 121 | 122 | 123 | ############ 124 | ## Windows 125 | ############ 126 | 127 | # Windows image file caches 128 | Thumbs.db 129 | 130 | # Folder config file 131 | Desktop.ini 132 | 133 | 134 | ############# 135 | ## Python 136 | ############# 137 | 138 | *.py[co] 139 | 140 | # Packages 141 | *.egg 142 | *.egg-info 143 | dist 144 | build 145 | eggs 146 | parts 147 | bin 148 | var 149 | sdist 150 | develop-eggs 151 | .installed.cfg 152 | 153 | # Installer logs 154 | pip-log.txt 155 | 156 | # Unit test / coverage reports 157 | .coverage 158 | .tox 159 | 160 | #Translations 161 | *.mo 162 | 163 | #Mr Developer 164 | .mr.developer.cfg 165 | 166 | # Mac crap 167 | .DS_Store 168 | -------------------------------------------------------------------------------- /DnDGen.RollGen.Tests.Integration.Stress/TransformTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using System; 3 | using System.Linq; 4 | 5 | namespace DnDGen.RollGen.Tests.Integration.Stress 6 | { 7 | [TestFixture] 8 | public class TransformTests : StressTests 9 | { 10 | private Dice dice; 11 | private Random random; 12 | 13 | [SetUp] 14 | public void Setup() 15 | { 16 | dice = GetNewInstanceOf(); 17 | random = GetNewInstanceOf(); 18 | } 19 | 20 | [TestCase(true)] 21 | [TestCase(false)] 22 | public void StressTransform(bool common) 23 | { 24 | stressor.Stress(() => AssertTransform(common)); 25 | } 26 | 27 | protected void AssertTransform(bool common) 28 | { 29 | var quantityLimit = common ? 100 : Limits.Quantity; 30 | var dieLimit = common ? 100 : Limits.Die; 31 | 32 | var quantity = random.Next(quantityLimit) + 1; 33 | var die = random.Next(dieLimit) + 1; 34 | var transform = random.Next(die - 1) + 1; 35 | var transformTarget = random.Next(die - 1) + 1; 36 | var percentageThreshold = random.NextDouble(); 37 | var rollThreshold = random.Next(quantity * die) + 1; 38 | 39 | var roll = GetRoll(quantity, die, transform, transformTarget); 40 | 41 | AssertRoll(roll, quantity, die, transform, transformTarget, percentageThreshold, rollThreshold); 42 | } 43 | 44 | private void AssertRoll(PartialRoll roll, int quantity, int die, int transform, int target, double percentageThreshold, int rollThreshold) 45 | { 46 | var rollMin = transform == 1 && die != 1 && target != 1 ? 2 : 1; 47 | var min = quantity * rollMin; 48 | var max = quantity * die; 49 | var average = (min + max) / 2.0d; 50 | 51 | Assert.That(min, Is.LessThanOrEqualTo(max), roll.CurrentRollExpression); 52 | Assert.That(roll.IsValid(), Is.True, roll.CurrentRollExpression); 53 | Assert.That(roll.AsSum(), Is.InRange(min, max), roll.CurrentRollExpression); 54 | Assert.That(roll.AsPotentialMinimum(), Is.EqualTo(min), roll.CurrentRollExpression); 55 | Assert.That(roll.AsPotentialMaximum(false), Is.EqualTo(max), roll.CurrentRollExpression); 56 | Assert.That(roll.AsPotentialMaximum(), Is.EqualTo(max), roll.CurrentRollExpression); 57 | Assert.That(roll.AsPotentialAverage(), Is.EqualTo(average), roll.CurrentRollExpression); 58 | Assert.That(roll.AsTrueOrFalse(percentageThreshold), Is.True.Or.False, "Percentage"); 59 | Assert.That(roll.AsTrueOrFalse(rollThreshold), Is.True.Or.False, "Roll"); 60 | 61 | var rolls = roll.AsIndividualRolls(); 62 | 63 | Assert.That(rolls.Count(), Is.EqualTo(quantity)); 64 | Assert.That(rolls, Has.All.InRange(rollMin, max)); 65 | } 66 | 67 | private PartialRoll GetRoll(int quantity, int die, int transform, int target) => dice.Roll(quantity).d(die).Transforming(transform, target); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /DnDGen.RollGen.Tests.Unit/RollPrototypeTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | 3 | namespace DnDGen.RollGen.Tests.Unit 4 | { 5 | [TestFixture] 6 | public class RollPrototypeTests 7 | { 8 | private RollPrototype prototype; 9 | 10 | [SetUp] 11 | public void Setup() 12 | { 13 | prototype = new RollPrototype(); 14 | } 15 | 16 | [Test] 17 | public void RollPrototypeInitialized() 18 | { 19 | Assert.That(prototype.Die, Is.Zero); 20 | Assert.That(prototype.Quantity, Is.Zero); 21 | Assert.That(prototype.Multiplier, Is.EqualTo(1)); 22 | } 23 | 24 | [Test] 25 | public void BuildRoll() 26 | { 27 | prototype.Quantity = 9266; 28 | prototype.Die = 90210; 29 | 30 | var roll = prototype.Build(); 31 | Assert.That(roll, Is.EqualTo("9266d90210")); 32 | } 33 | 34 | [Test] 35 | public void BuildRoll_WithMultiplier() 36 | { 37 | prototype.Quantity = 9266; 38 | prototype.Die = 90210; 39 | prototype.Multiplier = 42; 40 | 41 | var roll = prototype.Build(); 42 | Assert.That(roll, Is.EqualTo("(9266d90210-9266)*42")); 43 | } 44 | 45 | [Test] 46 | public void PrototypeDescription() 47 | { 48 | prototype.Quantity = 9266; 49 | prototype.Die = 90210; 50 | 51 | Assert.That(prototype.ToString(), Is.EqualTo("9266d90210")); 52 | } 53 | 54 | [Test] 55 | public void PrototypeDescription_WithMultiplier() 56 | { 57 | prototype.Quantity = 9266; 58 | prototype.Die = 90210; 59 | prototype.Multiplier = 42; 60 | 61 | Assert.That(prototype.ToString(), Is.EqualTo("(9266d90210-9266)*42")); 62 | } 63 | 64 | [Test] 65 | public void IsValid() 66 | { 67 | prototype.Quantity = 9266; 68 | prototype.Die = 42; 69 | 70 | Assert.That(prototype.IsValid, Is.True); 71 | } 72 | 73 | [Test] 74 | public void IsNotValid_QuantityTooLow() 75 | { 76 | prototype.Quantity = 0; 77 | prototype.Die = 42; 78 | 79 | Assert.That(prototype.IsValid, Is.False); 80 | } 81 | 82 | [Test] 83 | public void IsNotValid_QuantityTooHigh() 84 | { 85 | prototype.Quantity = Limits.Quantity + 1; 86 | prototype.Die = 42; 87 | 88 | Assert.That(prototype.IsValid, Is.False); 89 | } 90 | 91 | [Test] 92 | public void IsNotValid_DieTooLow() 93 | { 94 | prototype.Quantity = 9266; 95 | prototype.Die = 0; 96 | 97 | Assert.That(prototype.IsValid, Is.False); 98 | } 99 | 100 | [Test] 101 | public void IsNotValid_DieTooHigh() 102 | { 103 | prototype.Quantity = 9266; 104 | prototype.Die = Limits.Die + 1; 105 | 106 | Assert.That(prototype.IsValid, Is.False); 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /DnDGen.RollGen.Tests.Unit/PartialRolls/DomainPartialRollFactoryTests.cs: -------------------------------------------------------------------------------- 1 | using DnDGen.RollGen.Expressions; 2 | using DnDGen.RollGen.PartialRolls; 3 | using Moq; 4 | using NUnit.Framework; 5 | using System; 6 | 7 | namespace DnDGen.RollGen.Tests.Unit.PartialRolls 8 | { 9 | [TestFixture] 10 | public class DomainPartialRollFactoryTests 11 | { 12 | private PartialRollFactory partialRollFactory; 13 | private Mock mockRandom; 14 | private Mock mockExpressionEvaluator; 15 | 16 | [SetUp] 17 | public void Setup() 18 | { 19 | mockRandom = new Mock(); 20 | mockExpressionEvaluator = new Mock(); 21 | partialRollFactory = new DomainPartialRollFactory(mockRandom.Object, mockExpressionEvaluator.Object); 22 | 23 | var count = 0; 24 | mockRandom.Setup(r => r.Next(It.IsAny())).Returns((int max) => count++ % max); 25 | mockExpressionEvaluator.Setup(e => e.Evaluate(It.IsAny())).Returns((string s) => DefaultIntValue(s)); 26 | } 27 | 28 | private int DefaultIntValue(string source) 29 | { 30 | if (int.TryParse(source, out var output)) 31 | return output; 32 | 33 | throw new ArgumentException($"{source} was not configured to be evaluated"); 34 | } 35 | 36 | [Test] 37 | public void ReturnPartialRollFromNumericQuantity() 38 | { 39 | var partialRoll = partialRollFactory.Build(9266); 40 | Assert.That(partialRoll, Is.InstanceOf()); 41 | Assert.That(partialRoll.CurrentRollExpression, Is.EqualTo("9266")); 42 | } 43 | 44 | [Test] 45 | public void ReturnPartialRollFromQuantityExpression() 46 | { 47 | var partialRoll = partialRollFactory.Build("roll expression"); 48 | Assert.That(partialRoll, Is.InstanceOf()); 49 | Assert.That(partialRoll.CurrentRollExpression, Is.EqualTo("(roll expression)")); 50 | } 51 | 52 | [Test] 53 | public void UseSameInstanceOfRandomForAllPartialRolls() 54 | { 55 | mockRandom.SetupSequence(r => r.Next(9266)) 56 | .Returns(1337) 57 | .Returns(600); 58 | 59 | var firstPartialRoll = partialRollFactory.Build(1); 60 | var secondPartialRoll = partialRollFactory.Build(1); 61 | 62 | var firstRoll = firstPartialRoll.d(9266).AsSum(); 63 | var secondRoll = secondPartialRoll.d(9266).AsSum(); 64 | 65 | Assert.That(firstRoll, Is.EqualTo(1338)); 66 | Assert.That(secondRoll, Is.EqualTo(601)); 67 | } 68 | 69 | [Test] 70 | public void UseSameInstanceOfRandomForAllPartialRollsBasedOnExpression() 71 | { 72 | mockRandom.SetupSequence(r => r.Next(1336)) 73 | .Returns(1337) 74 | .Returns(600); 75 | 76 | var firstPartialRoll = partialRollFactory.Build("1d1336"); 77 | var secondPartialRoll = partialRollFactory.Build("1d1336"); 78 | 79 | var firstRoll = firstPartialRoll.AsSum(); 80 | var secondRoll = secondPartialRoll.AsSum(); 81 | 82 | Assert.That(firstRoll, Is.EqualTo(1338)); 83 | Assert.That(secondRoll, Is.EqualTo(601)); 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /DnDGen.RollGen.Tests.Integration.Stress/OperatorTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using System; 3 | using System.Linq; 4 | 5 | namespace DnDGen.RollGen.Tests.Integration.Stress 6 | { 7 | [TestFixture] 8 | public class OperatorTests : StressTests 9 | { 10 | private Dice dice; 11 | private Random random; 12 | 13 | [SetUp] 14 | public void Setup() 15 | { 16 | dice = GetNewInstanceOf(); 17 | random = GetNewInstanceOf(); 18 | } 19 | 20 | [Test] 21 | public void StressPlus() 22 | { 23 | stressor.Stress(AssertPlus); 24 | } 25 | 26 | private void AssertPlus() 27 | { 28 | var quantity = random.Next(Limits.Quantity) + 1; 29 | var plus = random.Next(int.MaxValue - quantity) + random.Next(100) / 100d; 30 | 31 | var roll = dice.Roll(quantity).Plus(plus); 32 | 33 | AssertTotal(roll, quantity + plus); 34 | } 35 | 36 | private void AssertTotal(PartialRoll roll, double total) 37 | { 38 | Assert.That(roll.IsValid(), Is.True, roll.CurrentRollExpression); 39 | Assert.That(roll.AsSum(), Is.EqualTo(total), roll.CurrentRollExpression); 40 | Assert.That(roll.AsPotentialMinimum(), Is.EqualTo(total), roll.CurrentRollExpression); 41 | Assert.That(roll.AsPotentialMaximum(false), Is.EqualTo(total), roll.CurrentRollExpression); 42 | Assert.That(roll.AsPotentialMaximum(), Is.EqualTo(total), roll.CurrentRollExpression); 43 | Assert.That(roll.AsPotentialAverage(), Is.EqualTo(total), roll.CurrentRollExpression); 44 | 45 | var rolls = roll.AsIndividualRolls(); 46 | 47 | Assert.That(rolls.Count(), Is.EqualTo(1), roll.CurrentRollExpression); 48 | Assert.That(rolls, Has.All.EqualTo(total), roll.CurrentRollExpression); 49 | } 50 | 51 | [Test] 52 | public void StressMinus() 53 | { 54 | stressor.Stress(AssertMinus); 55 | } 56 | 57 | private void AssertMinus() 58 | { 59 | var quantity = random.Next(Limits.Quantity) + 1; 60 | var minus = random.Next(quantity * 2) + random.Next(100) / 100d; 61 | 62 | var roll = dice.Roll(quantity).Minus(minus); 63 | 64 | AssertTotal(roll, quantity - minus); 65 | } 66 | 67 | [Test] 68 | public void StressTimes() 69 | { 70 | stressor.Stress(AssertTimes); 71 | } 72 | 73 | private void AssertTimes() 74 | { 75 | var quantity = random.Next(Limits.Quantity) + 1; 76 | var times = random.Next(int.MaxValue / quantity) + 1 + random.Next(100) / 100d; 77 | 78 | var roll = dice.Roll(quantity).Times(times); 79 | 80 | AssertTotal(roll, quantity * times); 81 | } 82 | 83 | [Test] 84 | public void StressDividedBy() 85 | { 86 | stressor.Stress(AssertDividedBy); 87 | } 88 | 89 | private void AssertDividedBy() 90 | { 91 | var quantity = random.Next(Limits.Quantity) + 1; 92 | //HACK: Making sure divisor is >= 1, because when it is less, sometimes the division expression ends up in something Albatross doesn't like, 93 | //such as '3/9.12030227907016E-05' (it doesn't like the scientific notation) 94 | var dividedBy = random.Next(Limits.Quantity) + 1 + random.Next(100) / 100d; 95 | 96 | var roll = dice.Roll(quantity).DividedBy(dividedBy); 97 | 98 | AssertTotal(roll, quantity / dividedBy); 99 | } 100 | 101 | [Test] 102 | public void StressModulos() 103 | { 104 | stressor.Stress(AssertModulos); 105 | } 106 | 107 | private void AssertModulos() 108 | { 109 | var quantity = random.Next(Limits.Quantity) + 1; 110 | var mod = random.Next(); 111 | 112 | var roll = dice.Roll(quantity).Modulos(mod); 113 | 114 | AssertTotal(roll, quantity % mod); 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /DnDGen.RollGen.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.29613.14 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DnDGen.RollGen", "DnDGen.RollGen\DnDGen.RollGen.csproj", "{F7A023C3-F9D4-4E4F-97FE-088386E4052A}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DnDGen.RollGen.Tests.Unit", "DnDGen.RollGen.Tests.Unit\DnDGen.RollGen.Tests.Unit.csproj", "{0C2BE08D-5A3C-4138-B7F5-17F3900E5AB8}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DnDGen.RollGen.Tests.Integration", "DnDGen.RollGen.Tests.Integration\DnDGen.RollGen.Tests.Integration.csproj", "{A3938051-33F4-4284-9A4B-9DC0BB29E6FC}" 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DnDGen.RollGen.Tests.Integration.IoC", "DnDGen.RollGen.Tests.Integration.IoC\DnDGen.RollGen.Tests.Integration.IoC.csproj", "{A13979CD-A8BF-4DD4-938D-466C29EDC586}" 13 | EndProject 14 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DnDGen.RollGen.Tests.Integration.Stress", "DnDGen.RollGen.Tests.Integration.Stress\DnDGen.RollGen.Tests.Integration.Stress.csproj", "{08F3948A-47A1-451A-AA56-79F0740471BD}" 15 | EndProject 16 | Global 17 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 18 | Debug|Any CPU = Debug|Any CPU 19 | Release|Any CPU = Release|Any CPU 20 | Stress|Any CPU = Stress|Any CPU 21 | EndGlobalSection 22 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 23 | {F7A023C3-F9D4-4E4F-97FE-088386E4052A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 24 | {F7A023C3-F9D4-4E4F-97FE-088386E4052A}.Debug|Any CPU.Build.0 = Debug|Any CPU 25 | {F7A023C3-F9D4-4E4F-97FE-088386E4052A}.Release|Any CPU.ActiveCfg = Release|Any CPU 26 | {F7A023C3-F9D4-4E4F-97FE-088386E4052A}.Release|Any CPU.Build.0 = Release|Any CPU 27 | {F7A023C3-F9D4-4E4F-97FE-088386E4052A}.Stress|Any CPU.ActiveCfg = Debug|Any CPU 28 | {F7A023C3-F9D4-4E4F-97FE-088386E4052A}.Stress|Any CPU.Build.0 = Debug|Any CPU 29 | {0C2BE08D-5A3C-4138-B7F5-17F3900E5AB8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 30 | {0C2BE08D-5A3C-4138-B7F5-17F3900E5AB8}.Debug|Any CPU.Build.0 = Debug|Any CPU 31 | {0C2BE08D-5A3C-4138-B7F5-17F3900E5AB8}.Release|Any CPU.ActiveCfg = Release|Any CPU 32 | {0C2BE08D-5A3C-4138-B7F5-17F3900E5AB8}.Release|Any CPU.Build.0 = Release|Any CPU 33 | {0C2BE08D-5A3C-4138-B7F5-17F3900E5AB8}.Stress|Any CPU.ActiveCfg = Debug|Any CPU 34 | {0C2BE08D-5A3C-4138-B7F5-17F3900E5AB8}.Stress|Any CPU.Build.0 = Debug|Any CPU 35 | {A3938051-33F4-4284-9A4B-9DC0BB29E6FC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 36 | {A3938051-33F4-4284-9A4B-9DC0BB29E6FC}.Debug|Any CPU.Build.0 = Debug|Any CPU 37 | {A3938051-33F4-4284-9A4B-9DC0BB29E6FC}.Release|Any CPU.ActiveCfg = Release|Any CPU 38 | {A3938051-33F4-4284-9A4B-9DC0BB29E6FC}.Release|Any CPU.Build.0 = Release|Any CPU 39 | {A3938051-33F4-4284-9A4B-9DC0BB29E6FC}.Stress|Any CPU.ActiveCfg = Debug|Any CPU 40 | {A3938051-33F4-4284-9A4B-9DC0BB29E6FC}.Stress|Any CPU.Build.0 = Debug|Any CPU 41 | {A13979CD-A8BF-4DD4-938D-466C29EDC586}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 42 | {A13979CD-A8BF-4DD4-938D-466C29EDC586}.Debug|Any CPU.Build.0 = Debug|Any CPU 43 | {A13979CD-A8BF-4DD4-938D-466C29EDC586}.Release|Any CPU.ActiveCfg = Release|Any CPU 44 | {A13979CD-A8BF-4DD4-938D-466C29EDC586}.Release|Any CPU.Build.0 = Release|Any CPU 45 | {A13979CD-A8BF-4DD4-938D-466C29EDC586}.Stress|Any CPU.ActiveCfg = Debug|Any CPU 46 | {A13979CD-A8BF-4DD4-938D-466C29EDC586}.Stress|Any CPU.Build.0 = Debug|Any CPU 47 | {08F3948A-47A1-451A-AA56-79F0740471BD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 48 | {08F3948A-47A1-451A-AA56-79F0740471BD}.Debug|Any CPU.Build.0 = Debug|Any CPU 49 | {08F3948A-47A1-451A-AA56-79F0740471BD}.Release|Any CPU.ActiveCfg = Release|Any CPU 50 | {08F3948A-47A1-451A-AA56-79F0740471BD}.Release|Any CPU.Build.0 = Release|Any CPU 51 | {08F3948A-47A1-451A-AA56-79F0740471BD}.Stress|Any CPU.ActiveCfg = Debug|Any CPU 52 | {08F3948A-47A1-451A-AA56-79F0740471BD}.Stress|Any CPU.Build.0 = Debug|Any CPU 53 | EndGlobalSection 54 | GlobalSection(SolutionProperties) = preSolution 55 | HideSolutionNode = FALSE 56 | EndGlobalSection 57 | GlobalSection(ExtensibilityGlobals) = postSolution 58 | SolutionGuid = {24D1AB46-4A56-476D-9F4D-C72E8E8E8AE2} 59 | EndGlobalSection 60 | EndGlobal 61 | -------------------------------------------------------------------------------- /DnDGen.RollGen/PartialRoll.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace DnDGen.RollGen 4 | { 5 | public abstract class PartialRoll 6 | { 7 | public string CurrentRollExpression { get; protected set; } 8 | 9 | public abstract PartialRoll Plus(string expression); 10 | public abstract PartialRoll Plus(double value); 11 | public PartialRoll Plus(PartialRoll roll) => Plus(roll.CurrentRollExpression); 12 | public abstract PartialRoll Minus(string expression); 13 | public abstract PartialRoll Minus(double value); 14 | public PartialRoll Minus(PartialRoll roll) => Minus(roll.CurrentRollExpression); 15 | public abstract PartialRoll Times(string expression); 16 | public abstract PartialRoll Times(double value); 17 | public PartialRoll Times(PartialRoll roll) => Times(roll.CurrentRollExpression); 18 | public abstract PartialRoll DividedBy(string expression); 19 | public abstract PartialRoll DividedBy(double value); 20 | public PartialRoll DividedBy(PartialRoll roll) => DividedBy(roll.CurrentRollExpression); 21 | public abstract PartialRoll Modulos(string expression); 22 | public abstract PartialRoll Modulos(double value); 23 | public PartialRoll Modulos(PartialRoll roll) => Modulos(roll.CurrentRollExpression); 24 | 25 | public abstract PartialRoll d(int die); 26 | public abstract PartialRoll d(string die); 27 | public PartialRoll d(PartialRoll roll) => d(roll.CurrentRollExpression); 28 | public abstract PartialRoll Keeping(int amountToKeep); 29 | public abstract PartialRoll Keeping(string amountToKeep); 30 | public PartialRoll Keeping(PartialRoll roll) => Keeping(roll.CurrentRollExpression); 31 | public abstract PartialRoll Explode(); 32 | public abstract PartialRoll ExplodeOn(int rollToExplode); 33 | public abstract PartialRoll ExplodeOn(string rollToExplode); 34 | public PartialRoll ExplodeOn(PartialRoll roll) => ExplodeOn(roll.CurrentRollExpression); 35 | public abstract PartialRoll Transforming(int rollToTransform, int? transformTarget = null); 36 | public abstract PartialRoll Transforming(string rollToTransform, string transformTarget = null); 37 | public PartialRoll Transforming(PartialRoll roll, PartialRoll transformTarget = null) => Transforming(roll.CurrentRollExpression, transformTarget?.CurrentRollExpression); 38 | 39 | public int AsSum() => AsSum(); 40 | public abstract T AsSum(); 41 | public IEnumerable AsIndividualRolls() => AsIndividualRolls(); 42 | public abstract IEnumerable AsIndividualRolls(); 43 | public abstract double AsPotentialAverage(); 44 | public int AsPotentialMinimum() => AsPotentialMinimum(); 45 | public abstract T AsPotentialMinimum(); 46 | public int AsPotentialMaximum(bool includeExplode = true) => AsPotentialMaximum(includeExplode); 47 | public abstract T AsPotentialMaximum(bool includeExplode = true); 48 | 49 | /// 50 | /// Return the value as True or False, depending on if it is higher or lower then the threshold. 51 | /// A value less than or equal to the threshold is false. 52 | /// A value higher than the threshold is true. 53 | /// As an example, on a roll of a 1d10 with threshold = .7, rolling a 7 produces False, while 8 produces True. 54 | /// 55 | /// The non-inclusive lower-bound percentage of success 56 | /// 57 | public abstract bool AsTrueOrFalse(double threshold = .5); 58 | 59 | /// 60 | /// Return the value as True or False, depending on if it is higher or lower then the threshold. 61 | /// A value less than the threshold is false. 62 | /// A value equal to or higher than the threshold is true. 63 | /// As an example, on a roll of a 1d10 with threshold = 7, rolling a 6 produces False, while 7 produces True. 64 | /// 65 | /// The inclusive lower-bound roll value of success 66 | /// 67 | public abstract bool AsTrueOrFalse(int threshold); 68 | 69 | public abstract bool IsValid(); 70 | 71 | public PartialRoll d2() => d(2); 72 | public PartialRoll d3() => d(3); 73 | public PartialRoll d4() => d(4); 74 | public PartialRoll d6() => d(6); 75 | public PartialRoll d8() => d(8); 76 | public PartialRoll d10() => d(10); 77 | public PartialRoll d12() => d(12); 78 | public PartialRoll d20() => d(20); 79 | public PartialRoll Percentile() => d(100); 80 | } 81 | } -------------------------------------------------------------------------------- /DnDGen.RollGen.Tests.Unit/Expressions/AlbatrossExpressionEvaluatorTests.cs: -------------------------------------------------------------------------------- 1 | using Albatross.Expression; 2 | using Albatross.Expression.Tokens; 3 | using DnDGen.RollGen.Expressions; 4 | using Moq; 5 | using NUnit.Framework; 6 | using System; 7 | using System.Collections.Generic; 8 | 9 | namespace DnDGen.RollGen.Tests.Unit.Expressions 10 | { 11 | [TestFixture] 12 | public class AlbatrossExpressionEvaluatorTests 13 | { 14 | private const string Expression = "expression"; 15 | 16 | private ExpressionEvaluator expressionEvaluator; 17 | private Mock mockParser; 18 | 19 | [SetUp] 20 | public void Setup() 21 | { 22 | mockParser = new Mock(); 23 | expressionEvaluator = new AlbatrossExpressionEvaluator(mockParser.Object); 24 | 25 | SetUpExpression(Expression, 9266); 26 | } 27 | 28 | private void SetUpExpression(string expression, object result) 29 | { 30 | var mockToken = new Mock(); 31 | var queue = new Queue(); 32 | var stack = new Stack(); 33 | 34 | mockParser.Setup(p => p.Tokenize(expression)).Returns(queue); 35 | mockParser.Setup(p => p.BuildStack(queue)).Returns(stack); 36 | mockParser.Setup(p => p.CreateTree(stack)).Returns(mockToken.Object); 37 | 38 | mockToken.Setup(t => t.EvalValue(null)).Returns(result); 39 | } 40 | 41 | [Test] 42 | public void EvaluateExpression() 43 | { 44 | var result = expressionEvaluator.Evaluate(Expression); 45 | Assert.That(result, Is.EqualTo(9266)); 46 | } 47 | 48 | [Test] 49 | public void EvaluateMultipleExpressions() 50 | { 51 | var result = expressionEvaluator.Evaluate(Expression); 52 | Assert.That(result, Is.EqualTo(9266)); 53 | 54 | SetUpExpression("other expression", 902.1); 55 | 56 | result = expressionEvaluator.Evaluate("other expression"); 57 | Assert.That(result, Is.EqualTo(902)); 58 | } 59 | 60 | [Test] 61 | public void EvaluateDouble() 62 | { 63 | SetUpExpression("other expression", 902.1); 64 | 65 | var result = expressionEvaluator.Evaluate("other expression"); 66 | Assert.That(result, Is.EqualTo(902.1)); 67 | } 68 | 69 | [Test] 70 | public void IfDieRollIsInExpression_ThrowArgumentException() 71 | { 72 | Assert.That(() => expressionEvaluator.Evaluate("expression with 3 d 4+2"), 73 | Throws.ArgumentException.With.Message.EqualTo("Cannot evaluate unrolled die roll 3 d 4")); 74 | } 75 | 76 | [Test] 77 | public void EvaluateTrueBooleanExpression() 78 | { 79 | SetUpExpression("boolean expression", bool.TrueString); 80 | 81 | var result = expressionEvaluator.Evaluate("boolean expression"); 82 | Assert.That(result, Is.True); 83 | } 84 | 85 | [Test] 86 | public void EvaluateFalseBooleanExpression() 87 | { 88 | SetUpExpression("boolean expression", bool.FalseString); 89 | 90 | var result = expressionEvaluator.Evaluate("boolean expression"); 91 | Assert.That(result, Is.False); 92 | } 93 | 94 | [Test] 95 | public void EvaluateExpressionThrowsException_WhenAlbatrossFailsToEvaluate() 96 | { 97 | var expression = "wrong expression"; 98 | 99 | var mockToken = new Mock(); 100 | var queue = new Queue(); 101 | var stack = new Stack(); 102 | 103 | mockParser.Setup(p => p.Tokenize(expression)).Returns(queue); 104 | mockParser.Setup(p => p.BuildStack(queue)).Returns(stack); 105 | mockParser.Setup(p => p.CreateTree(stack)).Returns(mockToken.Object); 106 | 107 | var exception = new Exception("I failed"); 108 | mockToken.Setup(t => t.EvalValue(null)).Throws(exception); 109 | 110 | Assert.That(() => expressionEvaluator.Evaluate(expression), 111 | Throws.InvalidOperationException 112 | .With.Message.EqualTo($"Expression 'wrong expression' is invalid") 113 | .And.InnerException.EqualTo(exception)); 114 | } 115 | 116 | [Test] 117 | public void IsValid_ReturnsTrue() 118 | { 119 | //mockParser.Setup(p => p.IsValidExpression(Expression)).Returns(true); 120 | 121 | var result = expressionEvaluator.IsValid(Expression); 122 | Assert.That(result, Is.True); 123 | } 124 | 125 | [Test] 126 | public void IsValid_ReturnsFalse() 127 | { 128 | //mockParser.Setup(p => p.IsValidExpression(Expression)).Returns(false); 129 | 130 | var mockToken = new Mock(); 131 | var queue = new Queue(); 132 | var stack = new Stack(); 133 | 134 | mockParser.Setup(p => p.Tokenize(Expression)).Returns(queue); 135 | mockParser.Setup(p => p.BuildStack(queue)).Returns(stack); 136 | mockParser.Setup(p => p.CreateTree(stack)).Returns(mockToken.Object); 137 | 138 | var exception = new Exception("I failed"); 139 | mockToken.Setup(t => t.EvalValue(null)).Throws(exception); 140 | 141 | var result = expressionEvaluator.IsValid(Expression); 142 | Assert.That(result, Is.False); 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /DnDGen.RollGen.Tests.Integration/IsValidTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | 3 | namespace DnDGen.RollGen.Tests.Integration 4 | { 5 | [TestFixture] 6 | public class IsValidTests : IntegrationTests 7 | { 8 | private Dice dice; 9 | 10 | [SetUp] 11 | public void Setup() 12 | { 13 | dice = GetNewInstanceOf(); 14 | } 15 | 16 | [TestCase("3d6", true)] 17 | [TestCase("3d6+1", true)] 18 | [TestCase("1+3d6", true)] 19 | [TestCase("(3)d(6)+1", true)] 20 | [TestCase("(1d2)d(3d4)+5d6", true)] 21 | [TestCase("10000d10000", true)] 22 | [TestCase("100d100d100", true)] 23 | [TestCase("1d100d100d100", true)] 24 | [TestCase("2d100d100d100", false)] 25 | [TestCase("100d2d100d100", false)] 26 | [TestCase("100d100d2d100", false)] 27 | [TestCase("100d100d100d2", false)] 28 | [TestCase("100d100d100d100", false)] 29 | [TestCase("(100d100)d(100d100)", true)] 30 | [TestCase("100d100d(100d100)", true)] 31 | [TestCase("(0)d(6)+1", false)] 32 | [TestCase("(-3)d(6)+1", false)] 33 | [TestCase("(3.1)d(6)+1", false)] 34 | [TestCase("0d6+1", false)] 35 | [TestCase("10001d10000", false)] 36 | [TestCase("100d100d2d2", false)] 37 | [TestCase("100d100d(2d2)", true)] 38 | [TestCase("(100d100+1)d(100d100)", false)] 39 | [TestCase("(3)d(-6)+1", false)] 40 | [TestCase("(3)d(6.1)+1", false)] 41 | [TestCase("3d0+1", false)] 42 | [TestCase("10000d10001", false)] 43 | [TestCase("(100d100)d(100d100+1)", false)] 44 | [TestCase("4d6k3", true)] 45 | [TestCase("(4)d(6)k(3)", true)] 46 | [TestCase("(4)d(6)k(-1)", false)] 47 | [TestCase("(4)d(6)k(3.1)", false)] 48 | [TestCase("4d6k10000", true)] 49 | [TestCase("4d6k10001", false)] 50 | [TestCase("4d6k(100d100+1)", false)] 51 | [TestCase("3d6t1", true)] 52 | [TestCase("3d6t1t2", true)] 53 | [TestCase("3d6t7", true)] 54 | [TestCase("3d6t0", false)] 55 | [TestCase("3d6t(-1)", false)] 56 | [TestCase("3d6t6:0", true)] 57 | [TestCase("3d6t10001", false)] 58 | [TestCase("3d6t6:10001", true)] 59 | [TestCase("3d6t6:0", true)] 60 | [TestCase("3d6t6:(-1)", true)] 61 | [TestCase("avg(1d12, 2d6, 3d4, 4d3, 6d2)", true)] 62 | [TestCase("bad(1d12, 2d6, 3d4, 4d3, 6d2)", false)] 63 | [TestCase("this is not a roll", false)] 64 | [TestCase("this contains 3d6, but is not a roll", false)] 65 | [TestCase("9266+90210-42*600/1337%1336+96d(783d82%45+922-2022/337)-min(1d2, 3d4, 5d6)+max(1d2, 3d4, 5d6)*avg(1d2, 3d4, 5d6)", true)] 66 | [TestCase("9266+90210-42*600/1337%1336+96d(783d82%45+922-2022/337)-min(max(avg(1d2, 3d4, 5d6)))", false)] 67 | [TestCase("9266+90210-42*600/1337%1336+96d(783d82%45+922-2022/227)-min(max(avg(1d2, 3d4, 5d6)))", false)] 68 | [TestCase("9266+90210-42*600/1337%1336+96d(783d8245+922-2022/227)-min(max(avg(1d2, 3d4, 5d6)))", false)] 69 | [TestCase("4d6", true)] 70 | [TestCase("92d66", true)] 71 | [TestCase("5+3d4*2", true)] 72 | [TestCase("(5+3d4*2)", true)] 73 | [TestCase("((5+3d4*2))", true)] 74 | [TestCase("1d2d5k1d6", true)] 75 | [TestCase("((1d2)d5k1)d6", true)] 76 | [TestCase("3d4k2", true)] 77 | [TestCase("5+3d4*3", true)] 78 | [TestCase("1d2", true)] 79 | [TestCase("9266", true)] 80 | [TestCase("1", true)] 81 | [TestCase("2", true)] 82 | [TestCase("1d3", true)] 83 | [TestCase("1d4", true)] 84 | [TestCase("1d6", true)] 85 | [TestCase("1d8", true)] 86 | [TestCase("1d10", true)] 87 | [TestCase("1d12", true)] 88 | [TestCase("1d20", true)] 89 | [TestCase("1d100", true)] 90 | [TestCase("1d100>50", true)] 91 | [TestCase("1d100>90", true)] 92 | [TestCase("1d100>=90", true)] 93 | [TestCase("1d100=>90", false)] 94 | [TestCase("5+3d4*2>20", true)] 95 | [TestCase("5+3d4*2<20", true)] 96 | [TestCase("5+3d4*2=20", true)] 97 | [TestCase("2d6 >= 1d12", true)] 98 | [TestCase("2d6 <= 1d12", true)] 99 | [TestCase("2d6 = 1d12", true)] 100 | [TestCase("2d6 => 1d12", false)] 101 | [TestCase("2d6 =< 1d12", false)] 102 | [TestCase("This contains a roll of 4d6k3 for rolling stats", false)] 103 | [TestCase("min(4d6,10) + 0.5", true)] 104 | [TestCase("Fireball does {min(4d6,10) + 0.5} damage", false)] 105 | [TestCase("max(4d6,10) + 0.5", true)] 106 | [TestCase("Fireball does {max(4d6,10) + 0.5} damage", false)] 107 | [TestCase("1d6+3", true)] 108 | [TestCase("1d8+1d2-1", true)] 109 | [TestCase("4d3-3", true)] 110 | [TestCase("4d6!", true)] 111 | [TestCase("3d4!", true)] 112 | [TestCase("3d4!k2", true)] 113 | [TestCase("3d4!e3", true)] 114 | [TestCase("3d4e1e2k2", true)] 115 | [TestCase("3d6t1t5", true)] 116 | [TestCase("3d6!t1k2", true)] 117 | [TestCase("3d6t1:2", true)] 118 | [TestCase("4d3!t2k1", true)] 119 | [TestCase("4d3!k1t2", true)] 120 | [TestCase("4d3t2!k1", true)] 121 | [TestCase("4d3t2k1!", true)] 122 | [TestCase("4d3k1!t2", true)] 123 | [TestCase("4d3k1t2!", true)] 124 | [TestCase("1d1", true)] 125 | [TestCase("-1d1", false)] 126 | [TestCase("1d-1", false)] 127 | [TestCase("-1d-1", false)] 128 | [TestCase("10000d1", true)] 129 | [TestCase("10001d1", false)] 130 | [TestCase("1d10000", true)] 131 | [TestCase("1d10001", false)] 132 | [TestCase("10000d10000", true)] 133 | [TestCase("10001d10000", false)] 134 | [TestCase("10000d10001", false)] 135 | [TestCase("10001d10001", false)] 136 | [TestCase("xdy", false)] 137 | [TestCase("invalid", false)] 138 | [TestCase("this is not a roll", false)] 139 | public void IsValid_Expression(string rollExpression, bool expected) 140 | { 141 | var actual = dice.IsValid(rollExpression); 142 | Assert.That(actual, Is.EqualTo(expected)); 143 | } 144 | 145 | [TestCase(1, 1, true)] 146 | [TestCase(-1, 1, false)] 147 | [TestCase(1, -1, false)] 148 | [TestCase(-1, -1, false)] 149 | [TestCase(Limits.Quantity, 1, true)] 150 | [TestCase(Limits.Quantity + 1, 1, false)] 151 | [TestCase(1, Limits.Die, true)] 152 | [TestCase(1, Limits.Die + 1, false)] 153 | [TestCase(Limits.Quantity, Limits.Die, true)] 154 | [TestCase(Limits.Quantity + 1, Limits.Die, false)] 155 | [TestCase(Limits.Quantity, Limits.Die + 1, false)] 156 | [TestCase(Limits.Quantity + 1, Limits.Die + 1, false)] 157 | public void IsValid_Numeric(int quantity, int die, bool expected) 158 | { 159 | var actual = dice.Roll(quantity).d(die).IsValid(); 160 | Assert.That(actual, Is.EqualTo(expected)); 161 | } 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /DnDGen.RollGen/RollCollection.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace DnDGen.RollGen 6 | { 7 | internal class RollCollection 8 | { 9 | public static int[] StandardDice = new[] { 2, 3, 4, 6, 8, 10, 12, 20, 100 }; 10 | 11 | public List Rolls { get; private set; } 12 | public int Adjustment { get; set; } 13 | 14 | public IEnumerable MultipliedRolls => Rolls.Where(r => r.Multiplier > 1); 15 | public IEnumerable UnmultipliedRolls => Rolls.Where(r => r.Multiplier == 1); 16 | public int Quantities => UnmultipliedRolls.Sum(r => r.Quantity); 17 | public int Lower => Quantities + Adjustment; 18 | public int Upper => (int)longSum + Adjustment; 19 | 20 | private long longSum => MultipliedRolls.Sum(r => r.Quantity * (r.Die - 1) * (long)r.Multiplier) 21 | + UnmultipliedRolls.Sum(r => r.Quantity * (long)r.Die); 22 | 23 | public RollCollection() 24 | { 25 | Rolls = new List(); 26 | } 27 | 28 | public string Build() 29 | { 30 | if (!Rolls.Any()) 31 | return Adjustment.ToString(); 32 | 33 | var rolls = Rolls 34 | .OrderByDescending(r => r.Multiplier) 35 | .ThenByDescending(r => r.Die) 36 | .Select(r => r.Build()); 37 | var allRolls = string.Join("+", rolls); 38 | 39 | if (Adjustment == 0) 40 | return allRolls; 41 | 42 | if (Adjustment > 0) 43 | return $"{allRolls}+{Adjustment}"; 44 | 45 | return $"{allRolls}{Adjustment}"; 46 | } 47 | 48 | public bool Matches(int lower, int upper) 49 | { 50 | return lower == Lower 51 | && upper == Upper 52 | && Lower <= Upper 53 | //&& longSum < int.MaxValue 54 | && !Rolls.Any(r => !r.IsValid); 55 | } 56 | 57 | public override string ToString() 58 | { 59 | return Build(); 60 | } 61 | 62 | public override bool Equals(object obj) 63 | { 64 | if (!(obj is RollCollection)) 65 | return false; 66 | 67 | var collection = obj as RollCollection; 68 | 69 | return collection.Build() == Build(); 70 | } 71 | 72 | public override int GetHashCode() 73 | { 74 | return Build().GetHashCode(); 75 | } 76 | 77 | /// 78 | /// This computes the Distribution (D) of a set of dice rolls. 1dX has a D = 1, or perfect distribution. D = 4 means the average occurs in 4 permutations. 79 | /// The methodology this follows was explained by Jasper Flick at anydice.com 80 | /// His explanation: 81 | /// 82 | /// AnyDice is based on combinatorics and probability theory. It treats dice as sets of (value, probability) tuples. An operation like d2 + d2 goes like this: 83 | /// 84 | /// { (1, 1/2), (2, 1/2) } + { (1, 1/2), (1, 1/2) } = { (1 + 1 = 2, 1 / 2 * 1 / 2 = 1 / 4) + (1 + 2 or 2 + 1 = 3, 1 / 4 + 1 / 4 = 1 / 2) +(2 + 2 = 4, 1 / 4) } = { (2, 1 / 4), (3, 1 / 2), (4, 1 / 4) } 85 | /// 86 | /// 10d10 has 10^10 ordered permutations, but you don't care about order when summing so we're dealing with unordered sampling with replacement (19 choose 10) which is only 92378 permutations.You just need to keep track of the probabilities. That's what the set approach does. 87 | /// I already showed you d2 + d2, which yields the set for 2d2. To get 3d2 you do the exact same thing but now 2d2 + d2. The same way you can go from d10 + d10 to 10d10. It can be done with a double loop that's blazingly fast, calculating 100d10 in a few microseconds. 88 | /// 89 | /// 90 | /// 91 | public long ComputeDistribution() 92 | { 93 | if (Quantities == 0) 94 | return 0; 95 | 96 | if (Quantities == 1) 97 | return 1; 98 | 99 | if (Quantities == 2) 100 | return UnmultipliedRolls.Min(r => r.Die); 101 | 102 | var validRolls = UnmultipliedRolls.ToList(); 103 | //We want to shortcut that when 1% of the possible iterations for the first die is greater than the max long, 104 | //Equation for xdy: 0.01y^x = 2^63 105 | //Solved for x, x = (2ln(10)+63ln(2))/ln(y) 106 | var quantityLimit = (2 * Math.Log(10) + 63 * Math.Log(2)) / Math.Log(validRolls[0].Die); 107 | if (validRolls[0].Quantity >= quantityLimit) 108 | return long.MaxValue; 109 | 110 | var upper = validRolls.Sum(r => r.Quantity * r.Die); 111 | var mode = (upper + Quantities) / 2; 112 | var rolls = new Dictionary() { { mode, 1 } }; 113 | var remainingMax = upper; 114 | 115 | //HACK: This means we are going to do too many iterations below 116 | //The Die Limit is only used when computing extra-large ranges with non-standard dice 117 | //Otherwise, non-standard dice should always have a quantity of 1 118 | //Therefore, we can just shortcut this specific usecase 119 | if (validRolls[0].Die == Limits.Die) 120 | { 121 | return validRolls[0].Quantity switch 122 | { 123 | 3 => 75_000_000, //0.75% * 10,000^3 = 75,000,000 124 | 4 => 666_666_670_000, 125 | 5 => 5_989_583_343_750_000, 126 | _ => long.MaxValue 127 | }; 128 | } 129 | 130 | for (var i = 0; i < validRolls.Count; i++) 131 | { 132 | var nextRolls = Enumerable.Range(1, validRolls[i].Die); 133 | var q = validRolls[i].Quantity; 134 | 135 | while (q-- > 0) 136 | { 137 | var newRolls = new Dictionary(); 138 | 139 | foreach (var r1 in rolls.Where(r => r.Key <= remainingMax)) 140 | { 141 | foreach (var r2 in nextRolls.Where(r => r1.Key >= r)) 142 | { 143 | var newSum = r1.Key - r2; 144 | if (!newRolls.ContainsKey(newSum)) 145 | newRolls[newSum] = 0; 146 | 147 | newRolls[newSum] += r1.Value; 148 | 149 | //INFO: This means we went so high that we wrapped around 150 | if (newRolls[newSum] < 1) 151 | return long.MaxValue; 152 | } 153 | } 154 | 155 | remainingMax -= validRolls[i].Die; 156 | rolls = newRolls; 157 | } 158 | } 159 | 160 | //Since we are subtracting from the mode, the key of 0 is the cumulative number of ways we can roll the mode 161 | return rolls[0]; 162 | } 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /DnDGen.RollGen/DomainDice.cs: -------------------------------------------------------------------------------- 1 | using DnDGen.RollGen.Expressions; 2 | using DnDGen.RollGen.PartialRolls; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Text.RegularExpressions; 7 | 8 | namespace DnDGen.RollGen 9 | { 10 | internal class DomainDice : Dice 11 | { 12 | private readonly PartialRollFactory partialRollFactory; 13 | private readonly Regex expressionRegex; 14 | private readonly Regex strictRollRegex; 15 | private readonly Regex lenientRollRegex; 16 | 17 | public DomainDice(PartialRollFactory partialRollFactory) 18 | { 19 | this.partialRollFactory = partialRollFactory; 20 | 21 | expressionRegex = new Regex(RegexConstants.ExpressionWithoutDieRollsPattern); 22 | strictRollRegex = new Regex(RegexConstants.StrictRollPattern); 23 | lenientRollRegex = new Regex(RegexConstants.LenientRollPattern); 24 | } 25 | 26 | public PartialRoll Roll(int quantity = 1) 27 | { 28 | return partialRollFactory.Build(quantity); 29 | } 30 | 31 | public PartialRoll Roll(string rollExpression) 32 | { 33 | return partialRollFactory.Build(rollExpression); 34 | } 35 | 36 | public PartialRoll Roll(PartialRoll roll) 37 | { 38 | return partialRollFactory.Build(roll.CurrentRollExpression); 39 | } 40 | 41 | public string ReplaceWrappedExpressions(string source, string expressionOpen = "{", string expressionClose = "}", char? expressionOpenEscape = '\\') 42 | { 43 | var pattern = $"{Regex.Escape(expressionOpen)}(.*?){Regex.Escape(expressionClose)}"; 44 | 45 | if (expressionOpenEscape != null) 46 | pattern = $"(?:[^{Regex.Escape(expressionOpenEscape.ToString())}]|^)" + pattern; 47 | 48 | var regex = new Regex(pattern); 49 | 50 | foreach (Match match in regex.Matches(source)) 51 | { 52 | var matchGroupValue = match.Groups[1].Value; 53 | var replacement = Evaluate(matchGroupValue); 54 | 55 | var target = expressionOpen + matchGroupValue + expressionClose; 56 | source = ReplaceFirst(source, target, replacement); 57 | } 58 | 59 | if (expressionOpenEscape == null) 60 | return source; 61 | 62 | return source.Replace(expressionOpenEscape + expressionOpen, expressionOpen); 63 | } 64 | 65 | private string Evaluate(string expression) 66 | { 67 | var partialRoll = partialRollFactory.Build(expression); 68 | 69 | if (typeof(T) == typeof(bool)) 70 | return partialRoll.AsTrueOrFalse().ToString(); 71 | 72 | return partialRoll.AsSum().ToString(); 73 | } 74 | 75 | public string ReplaceRollsWithSumExpression(string expression, bool lenient = false) 76 | { 77 | var rollRegex = GetRollRegex(lenient); 78 | expression = Replace(expression, rollRegex, s => GetRollAsSumExpression(s)); 79 | 80 | return expression.Trim(); 81 | } 82 | 83 | private Regex GetRollRegex(bool lenient) 84 | { 85 | return lenient ? lenientRollRegex : strictRollRegex; 86 | } 87 | 88 | private string GetRollAsSumExpression(string roll) 89 | { 90 | var rolls = GetIndividualRolls(roll); 91 | 92 | if (rolls.Any() == false) 93 | return "0"; 94 | 95 | if (rolls.Count() == 1) 96 | return rolls.First().ToString(); 97 | 98 | var sumOfRolls = string.Join(" + ", rolls); 99 | return $"({sumOfRolls})"; 100 | } 101 | 102 | private IEnumerable GetIndividualRolls(string roll) 103 | { 104 | var partialRoll = partialRollFactory.Build(roll); 105 | return partialRoll.AsIndividualRolls(); 106 | } 107 | 108 | public bool ContainsRoll(string expression, bool lenient = false) 109 | { 110 | var regex = GetRollRegex(lenient); 111 | return regex.IsMatch(expression); 112 | } 113 | 114 | private string ReplaceDiceExpression(string expression, bool lenient = false) 115 | { 116 | var regex = GetRollRegex(lenient); 117 | return Replace(expression, regex, s => CreateTotalOfRolls(s)); 118 | } 119 | 120 | public string ReplaceExpressionWithTotal(string expression, bool lenient = false) 121 | { 122 | expression = ReplaceDiceExpression(expression, lenient); 123 | expression = Replace(expression, expressionRegex, s => Evaluate(s)); 124 | 125 | return expression; 126 | } 127 | 128 | private int CreateTotalOfRolls(string roll) 129 | { 130 | var partialRoll = partialRollFactory.Build(roll); 131 | return partialRoll.AsSum(); 132 | } 133 | 134 | private string ReplaceFirst(string source, string target, string replacement) 135 | { 136 | var index = source.IndexOf(target); 137 | source = source.Remove(index, target.Length); 138 | source = source.Insert(index, replacement); 139 | return source; 140 | } 141 | 142 | private string Replace(string expression, Regex regex, Func createReplacement) 143 | { 144 | var expressionWithReplacedRolls = expression; 145 | var match = regex.Match(expressionWithReplacedRolls); 146 | 147 | while (match.Success) 148 | { 149 | var matchValue = match.Value.Trim(); 150 | var replacement = createReplacement(matchValue); 151 | 152 | expressionWithReplacedRolls = ReplaceFirst(expressionWithReplacedRolls, matchValue, replacement.ToString()); 153 | match = regex.Match(expressionWithReplacedRolls); 154 | } 155 | 156 | return expressionWithReplacedRolls; 157 | } 158 | 159 | public string Describe(string rollExpression, int roll, params string[] descriptions) 160 | { 161 | if (descriptions.Length == 0) 162 | descriptions = new[] { "Bad", "Good" }; 163 | 164 | var minimumRoll = Roll(rollExpression).AsPotentialMinimum(); 165 | var maximumRoll = Roll(rollExpression).AsPotentialMaximum(); 166 | 167 | if (roll < minimumRoll) 168 | return descriptions.First(); 169 | 170 | if (roll > maximumRoll) 171 | return descriptions.Last(); 172 | 173 | var percentile = GetPercentile(minimumRoll, maximumRoll, roll); 174 | 175 | var rawIndex = percentile * descriptions.Length; 176 | rawIndex = Math.Floor(rawIndex); 177 | 178 | var index = Convert.ToInt32(rawIndex); 179 | index = Math.Min(index, descriptions.Count() - 1); 180 | 181 | return descriptions[index]; 182 | } 183 | 184 | private double GetPercentile(int min, int max, double value) 185 | { 186 | var totalRange = max - min; 187 | var range = value - min; 188 | 189 | if (totalRange > 0) 190 | return range / totalRange; 191 | 192 | return .5; 193 | } 194 | 195 | public bool IsValid(string rollExpression) 196 | { 197 | return Roll(rollExpression).IsValid(); 198 | } 199 | } 200 | } -------------------------------------------------------------------------------- /DnDGen.RollGen.Tests.Integration/Expressions/ExpressionEvaluatorTests.cs: -------------------------------------------------------------------------------- 1 | using DnDGen.RollGen.Expressions; 2 | using NUnit.Framework; 3 | 4 | namespace DnDGen.RollGen.Tests.Integration 5 | { 6 | [TestFixture] 7 | public class ExpressionEvaluatorTests : IntegrationTests 8 | { 9 | private ExpressionEvaluator expressionEvaluator; 10 | 11 | [SetUp] 12 | public void Setup() 13 | { 14 | expressionEvaluator = GetNewInstanceOf(); 15 | } 16 | 17 | [TestCase("-9266", -9266)] 18 | [TestCase("-2", -2)] 19 | [TestCase("-1", -1)] 20 | [TestCase("0", 0)] 21 | [TestCase("1", 1)] 22 | [TestCase("2", 2)] 23 | [TestCase("9266", 9266)] 24 | [TestCase("0", 0)] 25 | [TestCase("+1", 1)] 26 | [TestCase("+2", 2)] 27 | [TestCase("+9266", 9266)] 28 | [TestCase("9266.90210", 9266.9021)] 29 | [TestCase("9266+90210", 9266 + 90210)] 30 | [TestCase("9266 + 90210", 9266 + 90210)] 31 | [TestCase("9266-90210", 9266 - 90210)] 32 | [TestCase("9266 - 90210", 9266 - 90210)] 33 | [TestCase("9266%42", 9266 % 42)] 34 | [TestCase("9266 % 42", 9266 % 42)] 35 | [TestCase("9266/42", 9266 / 42d)] 36 | [TestCase("9266 / 42", 9266 / 42d)] 37 | [TestCase("9266*42", 9266 * 42)] 38 | [TestCase("9266 * 42", 9266 * 42)] 39 | [TestCase("3^4", 81)] 40 | [TestCase("9266+90210-42", 9266 + 90210 - 42)] 41 | [TestCase("9266 + 90210 - 42", 9266 + 90210 - 42)] 42 | [TestCase("9266+90210-42*600", 9266 + 90210 - 42 * 600)] 43 | [TestCase("9266 + 90210 - 42 * 600", 9266 + 90210 - 42 * 600)] 44 | [TestCase("9266+(90210-42)*600", 9266 + (90210 - 42) * 600)] 45 | [TestCase("9266 + (90210 - 42) * 600", 9266 + (90210 - 42) * 600)] 46 | [TestCase("9266+90210-42*600/1337", 9266 + 90210 - 42 * 600 / 1337d)] 47 | [TestCase("9266 + 90210 - 42 * 600 / 1337", 9266 + 90210 - 42 * 600 / 1337d)] 48 | [TestCase("9266+90210-42*600/1337%1336", 9266 + 90210 - 42 * 600 / 1337d % 1336)] 49 | [TestCase("9266 + 90210 - 42 * 600 / 1337 % 1336", 9266 + 90210 - 42 * 600 / 1337d % 1336)] 50 | [TestCase("avg(9266, 90210, 42, 600, 1337, 1336, 96, 783, 8245, 922, 2022, 227)", 9590.5)] 51 | [TestCase("coalesce(null, null, 9266, 90210, 42, 600, 1337, 1336, 96, 783, 8245, 922, 2022, 227)", 9266)] 52 | [TestCase("max(9266, 90210, 42, 600, 1337, 1336, 96, 783, 8245, 922, 2022, 227)", 90210)] 53 | [TestCase("min(9266, 90210, 42, 600, 1337, 1336, 96, 783, 8245, 922, 2022, 227)", 42)] 54 | public void Evaluate_ReturnsValue(string expression, double expected) 55 | { 56 | var actual = expressionEvaluator.Evaluate(expression); 57 | Assert.That(actual, Is.EqualTo(expected)); 58 | } 59 | 60 | [TestCase("9266 < 90210", true)] 61 | [TestCase("9266 < 9266", false)] 62 | [TestCase("9266 < 42", false)] 63 | [TestCase("9266 <= 90210", true)] 64 | [TestCase("9266 <= 9266", true)] 65 | [TestCase("9266 <= 42", false)] 66 | [TestCase("9266 > 90210", false)] 67 | [TestCase("9266 > 9266", false)] 68 | [TestCase("9266 > 42", true)] 69 | [TestCase("9266 >= 90210", false)] 70 | [TestCase("9266 >= 9266", true)] 71 | [TestCase("9266 >= 42", true)] 72 | [TestCase("9266 <> 90210", true)] 73 | [TestCase("9266 <> 9266", false)] 74 | [TestCase("9266 <> 42", true)] 75 | [TestCase("9266 = 90210", false)] 76 | [TestCase("9266 = 9266", true)] 77 | [TestCase("9266 = 42", false)] 78 | [TestCase("9266 < 90210 and 42 > 600", false)] 79 | [TestCase("9266 < 90210 and 42 < 600", true)] 80 | [TestCase("9266 < 90210 or 42 > 600", true)] 81 | [TestCase("9266 < 90210 or 42 < 600", true)] 82 | [TestCase("9266 = 90210 or 42 > 600", false)] 83 | public void Evaluate_ReturnsBooleanValue(string expression, bool expected) 84 | { 85 | var actual = expressionEvaluator.Evaluate(expression); 86 | Assert.That(actual, Is.EqualTo(expected)); 87 | } 88 | 89 | [TestCase("-9266", true)] 90 | [TestCase("-2", true)] 91 | [TestCase("-1", true)] 92 | [TestCase("0", true)] 93 | [TestCase("1", true)] 94 | [TestCase("2", true)] 95 | [TestCase("9266", true)] 96 | [TestCase("0", true)] 97 | [TestCase("+1", true)] 98 | [TestCase("+2", true)] 99 | [TestCase("+9266", true)] 100 | [TestCase("9266.90210", true)] 101 | [TestCase("9266+90210", true)] 102 | [TestCase("9266 + 90210", true)] 103 | [TestCase("9266-90210", true)] 104 | [TestCase("9266 - 90210", true)] 105 | [TestCase("9266%42", true)] 106 | [TestCase("9266 % 42", true)] 107 | [TestCase("9266/42", true)] 108 | [TestCase("9266 / 42", true)] 109 | [TestCase("9266*42", true)] 110 | [TestCase("9266 * 42", true)] 111 | [TestCase("3^4", true)] 112 | [TestCase("9266+90210-42", true)] 113 | [TestCase("9266 + 90210 - 42", true)] 114 | [TestCase("9266+90210-42*600", true)] 115 | [TestCase("9266 + 90210 - 42 * 600", true)] 116 | [TestCase("9266+(90210-42)*600", true)] 117 | [TestCase("9266 + (90210 - 42) * 600", true)] 118 | [TestCase("9266+90210-42*600/1337", true)] 119 | [TestCase("9266 + 90210 - 42 * 600 / 1337", true)] 120 | [TestCase("9266+90210-42*600/1337%1336", true)] 121 | [TestCase("9266 + 90210 - 42 * 600 / 1337 % 1336", true)] 122 | [TestCase("avg(9266, 90210, 42, 600, 1337, 1336, 96, 783, 8245, 922, 2022, 227)", true)] 123 | [TestCase("(9266, 90210, 42, 600, 1337, 1336, 96, 783, 8245, 922, 2022, 227)", false, Ignore = "Albatross interprets this as an array")] 124 | [TestCase("9266, 90210, 42, 600, 1337, 1336, 96, 783, 8245, 922, 2022, 227", false)] 125 | [TestCase("coalesce(null, null, 9266, 90210, 42, 600, 1337, 1336, 96, 783, 8245, 922, 2022, 227)", true)] 126 | [TestCase("max(9266, 90210, 42, 600, 1337, 1336, 96, 783, 8245, 922, 2022, 227)", true)] 127 | [TestCase("min(9266, 90210, 42, 600, 1337, 1336, 96, 783, 8245, 922, 2022, 227)", true)] 128 | [TestCase("bad(9266, 90210, 42, 600, 1337, 1336, 96, 783, 8245, 922, 2022, 227)", false)] 129 | [TestCase("9266 < 90210", true)] 130 | [TestCase("9266 < 9266", true)] 131 | [TestCase("9266 < 42", true)] 132 | [TestCase("9266 <= 90210", true)] 133 | [TestCase("9266 <= 9266", true)] 134 | [TestCase("9266 <= 42", true)] 135 | [TestCase("9266 =< 90210", false)] 136 | [TestCase("9266 =< 9266", false)] 137 | [TestCase("9266 =< 42", false)] 138 | [TestCase("9266 > 90210", true)] 139 | [TestCase("9266 > 9266", true)] 140 | [TestCase("9266 > 42", true)] 141 | [TestCase("9266 >= 90210", true)] 142 | [TestCase("9266 >= 9266", true)] 143 | [TestCase("9266 >= 42", true)] 144 | [TestCase("9266 => 90210", false)] 145 | [TestCase("9266 => 9266", false)] 146 | [TestCase("9266 => 42", false)] 147 | [TestCase("9266 <> 90210", true)] 148 | [TestCase("9266 <> 9266", true)] 149 | [TestCase("9266 <> 42", true)] 150 | [TestCase("9266 >< 90210", false)] 151 | [TestCase("9266 >< 9266", false)] 152 | [TestCase("9266 >< 42", false)] 153 | [TestCase("9266 = 90210", true)] 154 | [TestCase("9266 = 9266", true)] 155 | [TestCase("9266 = 42", true)] 156 | [TestCase("9266 == 90210", false)] 157 | [TestCase("9266 == 9266", false)] 158 | [TestCase("9266 == 42", false)] 159 | [TestCase("9266 < 90210 and 42 > 600", true)] 160 | [TestCase("9266 < 90210 and 42 < 600", true)] 161 | [TestCase("9266 < 90210 or 42 > 600", true)] 162 | [TestCase("9266 < 90210 or 42 < 600", true)] 163 | [TestCase("9266 = 90210 or 42 > 600", true)] 164 | [TestCase("1d6", false)] 165 | [TestCase("1d6+3", false)] 166 | [TestCase("1 d 6", false)] 167 | [TestCase("xdy", false)] 168 | [TestCase("invalid", false)] 169 | [TestCase("this is not a roll", false)] 170 | public void IsValid_ReturnsValidity(string expression, bool expected) 171 | { 172 | var actual = expressionEvaluator.IsValid(expression); 173 | Assert.That(actual, Is.EqualTo(expected)); 174 | } 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /DnDGen.RollGen/PartialRolls/Roll.cs: -------------------------------------------------------------------------------- 1 | using DnDGen.RollGen.Expressions; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text.RegularExpressions; 6 | 7 | namespace DnDGen.RollGen.PartialRolls 8 | { 9 | internal class Roll 10 | { 11 | public int Quantity { get; set; } 12 | public int Die { get; set; } 13 | public int AmountToKeep { get; set; } 14 | public readonly List ExplodeOn; 15 | public readonly Dictionary Transforms; 16 | 17 | public bool IsValid => QuantityValid && DieValid && KeepValid && ExplodeValid && TransformValid; 18 | private bool QuantityValid => Quantity > 0 && Quantity <= Limits.Quantity; 19 | private bool DieValid => Die > 0 && Die <= Limits.Die; 20 | private bool KeepValid => AmountToKeep > -1 && AmountToKeep <= Limits.Quantity; 21 | private bool Explode => ExplodeOn.Any(); 22 | private bool ExplodeValid => !ExplodeOn.Any() || (DieValid && Enumerable.Range(1, Die).Except(ExplodeOn).Any()); 23 | private bool TransformValid => !Transforms.Any() || Transforms.All(kvp => kvp.Key > 0 && kvp.Key <= Limits.Die); 24 | 25 | public Roll() 26 | { 27 | ExplodeOn = new List(); 28 | Transforms = new Dictionary(); 29 | } 30 | 31 | public Roll(string toParse) 32 | { 33 | var sections = new Dictionary>(); 34 | sections['q'] = new List(); 35 | sections['d'] = new List(); 36 | sections['e'] = new List(); 37 | sections['t'] = new List(); 38 | sections[':'] = new List(); 39 | sections['k'] = new List(); 40 | 41 | var key = 'q'; 42 | var number = string.Empty; 43 | var explodeDefault = false; 44 | 45 | for (var i = 0; i < toParse.Length; i++) 46 | { 47 | if (char.IsWhiteSpace(toParse[i])) 48 | continue; 49 | 50 | if (toParse[i] == '!') 51 | { 52 | explodeDefault = true; 53 | continue; 54 | } 55 | 56 | if (!char.IsDigit(toParse[i]) && !number.Any() && toParse[i] != '-') 57 | { 58 | key = toParse[i]; 59 | continue; 60 | } 61 | else if (char.IsDigit(toParse[i]) || (!number.Any() && toParse[i] == '-')) 62 | { 63 | number += toParse[i]; 64 | continue; 65 | } 66 | 67 | if (!number.Any()) 68 | continue; 69 | 70 | //INFO: This means we are using the "Default" transform to the max die value 71 | if (toParse[i] != ':' && key == 't') 72 | { 73 | sections[':'].Add(sections['d'][0]); 74 | } 75 | 76 | sections[key].Add(Convert.ToInt32(number)); 77 | 78 | number = string.Empty; 79 | key = toParse[i]; 80 | } 81 | 82 | sections[key].Add(Convert.ToInt32(number)); 83 | 84 | //INFO: This means we ended on a transform and are using the "Default" transform to the max die value 85 | if (key == 't') 86 | { 87 | sections[':'].Add(sections['d'][0]); 88 | } 89 | 90 | if (!sections['q'].Any()) 91 | sections['q'].Add(1); 92 | 93 | Quantity = sections['q'][0]; 94 | Die = sections['d'][0]; 95 | 96 | if (explodeDefault) 97 | sections['e'].Add(Die); 98 | 99 | if (sections['k'].Any()) 100 | AmountToKeep = sections['k'][0]; 101 | 102 | ExplodeOn = sections['e'].Distinct().ToList(); 103 | Transforms = new Dictionary(); 104 | 105 | for (var i = 0; i < sections['t'].Count; i++) 106 | { 107 | Transforms[sections['t'][i]] = sections[':'][i]; 108 | } 109 | } 110 | 111 | public static bool CanParse(string toParse) 112 | { 113 | if (string.IsNullOrWhiteSpace(toParse)) 114 | return false; 115 | 116 | var trimmedSource = toParse.Trim(); 117 | var strictRollRegex = new Regex(RegexConstants.StrictRollPattern); 118 | var rollMatch = strictRollRegex.Match(trimmedSource); 119 | 120 | return rollMatch.Success && rollMatch.Value == trimmedSource; 121 | } 122 | 123 | public IEnumerable GetRolls(Random random) 124 | { 125 | ValidateRoll(); 126 | 127 | var rolls = new List(Quantity); 128 | 129 | for (var i = 0; i < Quantity; i++) 130 | { 131 | var roll = random.Next(Die) + 1; 132 | 133 | if (ExplodeOn.Contains(roll)) 134 | { 135 | --i; // We're rerolling this die, so Quantity actually stays the same. 136 | } 137 | 138 | if (Transforms.ContainsKey(roll)) 139 | { 140 | roll = Transforms[roll]; 141 | } 142 | 143 | rolls.Add(roll); 144 | } 145 | 146 | if (AmountToKeep > 0 && AmountToKeep < rolls.Count()) 147 | return rolls.OrderByDescending(r => r).Take(AmountToKeep); 148 | 149 | return rolls; 150 | } 151 | 152 | private void ValidateRoll() 153 | { 154 | if (IsValid) 155 | return; 156 | 157 | var message = $"{this} is not a valid roll"; 158 | 159 | if (!QuantityValid) 160 | message += $"\n\tQuantity: 0 < {Quantity} < {Limits.Quantity}"; 161 | 162 | if (!DieValid) 163 | message += $"\n\tDie: 0 < {Die} < {Limits.Die}"; 164 | 165 | if (!KeepValid) 166 | message += $"\n\tKeep: 0 <= {AmountToKeep} < {Limits.Quantity}"; 167 | 168 | if (!ExplodeValid) 169 | message += $"\n\tExplode: Must have at least 1 non-exploded roll"; 170 | 171 | if (!TransformValid) 172 | message += $"\n\tTransform: 0 < [{string.Join(',', Transforms.Select(kvp => $"{kvp.Key}:{kvp.Value}"))}] <= {Limits.Die}"; 173 | 174 | throw new InvalidOperationException(message); 175 | } 176 | 177 | public int GetSum(Random random) 178 | { 179 | ValidateRoll(); 180 | 181 | var rolls = GetRolls(random); 182 | return rolls.Sum(); 183 | } 184 | 185 | public double GetPotentialAverage() 186 | { 187 | ValidateRoll(); 188 | 189 | var min = GetPotentialMinimum(); 190 | var max = GetPotentialMaximum(false); 191 | var average = (min + max) / 2.0d; 192 | 193 | return average; 194 | } 195 | 196 | public int GetPotentialMinimum() 197 | { 198 | ValidateRoll(); 199 | 200 | var quantity = GetEffectiveQuantity(); 201 | var min = 1; 202 | 203 | while ((Transforms.ContainsKey(min) || ExplodeOn.Contains(min)) && min < Die) 204 | { 205 | min++; 206 | } 207 | 208 | if (Transforms.Any()) 209 | { 210 | //INFO: This means we are transforming all rolls 211 | if (!Enumerable.Range(1, Die).Except(Transforms.Keys).Any()) 212 | { 213 | min = Transforms.Values.Min(); 214 | } 215 | else 216 | { 217 | var minTransform = Transforms.Values.Min(); 218 | min = Math.Min(min, minTransform); 219 | } 220 | } 221 | 222 | return min * quantity; 223 | } 224 | 225 | private int GetEffectiveQuantity() 226 | { 227 | if (AmountToKeep > 0) 228 | return Math.Min(Quantity, AmountToKeep); 229 | 230 | return Quantity; 231 | } 232 | 233 | public int GetPotentialMaximum(bool includeExplode = true) 234 | { 235 | ValidateRoll(); 236 | 237 | var quantity = GetEffectiveQuantity(); 238 | var max = Die; 239 | var multiplier = 1; 240 | 241 | while (Transforms.ContainsKey(max) && max > 1) 242 | { 243 | --max; 244 | } 245 | 246 | if (Transforms.Any()) 247 | { 248 | var maxTransform = Transforms.Values.Max(); 249 | max = Math.Max(max, maxTransform); 250 | } 251 | 252 | //INFO: Since exploded dice can in theory be infinite, we will assume 10x multiplier, 253 | //which should cover 99.9% of use cases 254 | if (Explode && includeExplode) 255 | multiplier *= 10; 256 | 257 | return quantity * max * multiplier; 258 | } 259 | 260 | public bool GetTrueOrFalse(Random random) 261 | { 262 | ValidateRoll(); 263 | 264 | var average = GetPotentialAverage(); 265 | var sum = GetSum(random); 266 | 267 | return sum >= average; 268 | } 269 | 270 | public override string ToString() 271 | { 272 | var output = $"{Quantity}d{Die}"; 273 | 274 | foreach (var explode in ExplodeOn) 275 | output += $"e{explode}"; 276 | 277 | foreach (var kvp in Transforms) 278 | output += $"t{kvp.Key}:{kvp.Value}"; 279 | 280 | if (AmountToKeep != 0) 281 | output += $"k{AmountToKeep}"; 282 | 283 | return output; 284 | } 285 | } 286 | } 287 | -------------------------------------------------------------------------------- /DnDGen.RollGen.Tests.Integration/DescribeTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | 3 | namespace DnDGen.RollGen.Tests.Integration 4 | { 5 | [TestFixture] 6 | public class DescribeTests : IntegrationTests 7 | { 8 | private Dice dice; 9 | 10 | [SetUp] 11 | public void Setup() 12 | { 13 | dice = GetNewInstanceOf(); 14 | } 15 | 16 | [TestCase("1", 1, "Good")] 17 | [TestCase("9266", 9266, "Good")] 18 | [TestCase("1d4", 1, "Bad")] 19 | [TestCase("1d4", 2, "Bad")] 20 | [TestCase("1d4", 3, "Good")] 21 | [TestCase("1d4", 4, "Good")] 22 | [TestCase("1d20", 1, "Bad")] 23 | [TestCase("1d20", 10, "Bad")] 24 | [TestCase("1d20", 11, "Good")] 25 | [TestCase("1d20", 20, "Good")] 26 | [TestCase("3d6", 3, "Bad")] 27 | [TestCase("3d6", 10, "Bad")] 28 | [TestCase("3d6", 11, "Good")] 29 | [TestCase("3d6", 12, "Good")] 30 | [TestCase("3d6", 18, "Good")] 31 | [TestCase("1d100", 1, "Bad")] 32 | [TestCase("1d100", 50, "Bad")] 33 | [TestCase("1d100", 51, "Good")] 34 | [TestCase("1d100", 100, "Good")] 35 | public void DescribeRoll_Default(string rollExpression, int roll, string expectedDescription) 36 | { 37 | var description = dice.Describe(rollExpression, roll); 38 | Assert.That(description, Is.EqualTo(expectedDescription)); 39 | } 40 | 41 | [TestCase("1", 1, "Succeed")] 42 | [TestCase("9266", 9266, "Succeed")] 43 | [TestCase("1d4", 1, "Failed")] 44 | [TestCase("1d4", 2, "Failed")] 45 | [TestCase("1d4", 3, "Succeed")] 46 | [TestCase("1d4", 4, "Succeed")] 47 | [TestCase("1d20", 1, "Failed")] 48 | [TestCase("1d20", 10, "Failed")] 49 | [TestCase("1d20", 11, "Succeed")] 50 | [TestCase("1d20", 20, "Succeed")] 51 | [TestCase("3d6", 3, "Failed")] 52 | [TestCase("3d6", 10, "Failed")] 53 | [TestCase("3d6", 11, "Succeed")] 54 | [TestCase("3d6", 12, "Succeed")] 55 | [TestCase("3d6", 18, "Succeed")] 56 | [TestCase("1d100", 1, "Failed")] 57 | [TestCase("1d100", 50, "Failed")] 58 | [TestCase("1d100", 51, "Succeed")] 59 | [TestCase("1d100", 100, "Succeed")] 60 | public void DescribeRoll_FailedSucceed(string rollExpression, int roll, string expectedDescription) 61 | { 62 | var description = dice.Describe(rollExpression, roll, "Failed", "Succeed"); 63 | Assert.That(description, Is.EqualTo(expectedDescription)); 64 | } 65 | 66 | [TestCase("1", 1, "Average")] 67 | [TestCase("9266", 9266, "Average")] 68 | [TestCase("1d4", 1, "Low")] 69 | [TestCase("1d4", 2, "Average")] 70 | [TestCase("1d4", 3, "High")] 71 | [TestCase("1d4", 4, "High")] 72 | [TestCase("1d20", 1, "Low")] 73 | [TestCase("1d20", 7, "Low")] 74 | [TestCase("1d20", 8, "Average")] 75 | [TestCase("1d20", 10, "Average")] 76 | [TestCase("1d20", 11, "Average")] 77 | [TestCase("1d20", 13, "Average")] 78 | [TestCase("1d20", 14, "High")] 79 | [TestCase("1d20", 20, "High")] 80 | [TestCase("3d6", 3, "Low")] 81 | [TestCase("3d6", 7, "Low")] 82 | [TestCase("3d6", 8, "Average")] 83 | [TestCase("3d6", 10, "Average")] 84 | [TestCase("3d6", 12, "Average")] 85 | [TestCase("3d6", 13, "High")] 86 | [TestCase("3d6", 18, "High")] 87 | [TestCase("1d100", 1, "Low")] 88 | [TestCase("1d100", 33, "Low")] 89 | [TestCase("1d100", 34, "Average")] 90 | [TestCase("1d100", 66, "Average")] 91 | [TestCase("1d100", 67, "High")] 92 | [TestCase("1d100", 100, "High")] 93 | public void DescribeRoll_LowAverageHigh(string rollExpression, int roll, string expectedDescription) 94 | { 95 | var description = dice.Describe(rollExpression, roll, "Low", "Average", "High"); 96 | Assert.That(description, Is.EqualTo(expectedDescription)); 97 | } 98 | 99 | [TestCase("1", 1, "Average")] 100 | [TestCase("9266", 42, "Very Low")] 101 | [TestCase("9266", 9266, "Average")] 102 | [TestCase("9266", 90210, "Very High")] 103 | [TestCase("1d4", 1, "Very Low")] 104 | [TestCase("1d4", 2, "Low")] 105 | [TestCase("1d4", 3, "High")] 106 | [TestCase("1d4", 4, "Very High")] 107 | [TestCase("1d20", 1, "Very Low")] 108 | [TestCase("1d20", 4, "Very Low")] 109 | [TestCase("1d20", 5, "Low")] 110 | [TestCase("1d20", 8, "Low")] 111 | [TestCase("1d20", 9, "Average")] 112 | [TestCase("1d20", 12, "Average")] 113 | [TestCase("1d20", 13, "High")] 114 | [TestCase("1d20", 16, "High")] 115 | [TestCase("1d20", 17, "Very High")] 116 | [TestCase("1d20", 20, "Very High")] 117 | [TestCase("3d6", 3, "Very Low")] 118 | [TestCase("3d6", 5, "Very Low")] 119 | [TestCase("3d6", 6, "Low")] 120 | [TestCase("3d6", 8, "Low")] 121 | [TestCase("3d6", 9, "Average")] 122 | [TestCase("3d6", 11, "Average")] 123 | [TestCase("3d6", 12, "High")] 124 | [TestCase("3d6", 14, "High")] 125 | [TestCase("3d6", 15, "Very High")] 126 | [TestCase("3d6", 18, "Very High")] 127 | [TestCase("1d100", 1, "Very Low")] 128 | [TestCase("1d100", 20, "Very Low")] 129 | [TestCase("1d100", 21, "Low")] 130 | [TestCase("1d100", 40, "Low")] 131 | [TestCase("1d100", 41, "Average")] 132 | [TestCase("1d100", 60, "Average")] 133 | [TestCase("1d100", 61, "High")] 134 | [TestCase("1d100", 80, "High")] 135 | [TestCase("1d100", 81, "Very High")] 136 | [TestCase("1d100", 100, "Very High")] 137 | public void DescribeRoll_VeryLowLowAverageHighVeryHigh(string rollExpression, int roll, string expectedDescription) 138 | { 139 | var description = dice.Describe(rollExpression, roll, "Very Low", "Low", "Average", "High", "Very High"); 140 | Assert.That(description, Is.EqualTo(expectedDescription)); 141 | } 142 | 143 | [TestCase("1d20", 1, "Critical Failure")] 144 | [TestCase("1d20", 2, "OK")] 145 | [TestCase("1d20", 19, "OK")] 146 | [TestCase("1d20", 20, "Critical Success")] 147 | public void DescribeRoll_CritFailCrit(string rollExpression, int roll, string expectedDescription) 148 | { 149 | var descriptions = new[] 150 | { 151 | "Critical Failure", 152 | "OK", 153 | "OK", 154 | "OK", 155 | "OK", 156 | "OK", 157 | "OK", 158 | "OK", 159 | "OK", 160 | "OK", 161 | "OK", 162 | "OK", 163 | "OK", 164 | "OK", 165 | "OK", 166 | "OK", 167 | "OK", 168 | "OK", 169 | "OK", 170 | "Critical Success", 171 | }; 172 | 173 | var description = dice.Describe(rollExpression, roll, descriptions); 174 | Assert.That(description, Is.EqualTo(expectedDescription)); 175 | } 176 | 177 | [TestCase("1d20", 1, "Critical Failure")] 178 | [TestCase("1d20", 2, "Fail")] 179 | [TestCase("1d20", 13, "Fail")] 180 | [TestCase("1d20", 14, "Succeed")] 181 | [TestCase("1d20", 19, "Succeed")] 182 | [TestCase("1d20", 20, "Critical Success")] 183 | public void DescribeRoll_CritFailCritWithDC14(string rollExpression, int roll, string expectedDescription) 184 | { 185 | var descriptions = new[] 186 | { 187 | "Critical Failure", 188 | "Fail", 189 | "Fail", 190 | "Fail", 191 | "Fail", 192 | "Fail", 193 | "Fail", 194 | "Fail", 195 | "Fail", 196 | "Fail", 197 | "Fail", 198 | "Fail", 199 | "Fail", 200 | "Succeed", 201 | "Succeed", 202 | "Succeed", 203 | "Succeed", 204 | "Succeed", 205 | "Succeed", 206 | "Critical Success", 207 | }; 208 | 209 | var description = dice.Describe(rollExpression, roll, descriptions); 210 | Assert.That(description, Is.EqualTo(expectedDescription)); 211 | } 212 | 213 | [TestCase("1d20", 1, "Critical Fail")] 214 | [TestCase("1d20", 3, "Critical Fail")] 215 | [TestCase("1d20", 4, "Great Fail")] 216 | [TestCase("1d20", 6, "Great Fail")] 217 | [TestCase("1d20", 7, "Fail")] 218 | [TestCase("1d20", 9, "Fail")] 219 | [TestCase("1d20", 10, "Standard")] 220 | [TestCase("1d20", 11, "Standard")] 221 | [TestCase("1d20", 12, "Success")] 222 | [TestCase("1d20", 14, "Success")] 223 | [TestCase("1d20", 15, "Great Success")] 224 | [TestCase("1d20", 17, "Great Success")] 225 | [TestCase("1d20", 18, "Critical Success")] 226 | [TestCase("1d20", 20, "Critical Success")] 227 | public void DescribeRoll_CritFailGreatFailFailStandardSuccessGreatSuccessCriticalSuccess(string rollExpression, int roll, string expectedDescription) 228 | { 229 | var descriptions = new[] 230 | { 231 | "Critical Fail", 232 | "Great Fail", 233 | "Fail", 234 | "Standard", 235 | "Success", 236 | "Great Success", 237 | "Critical Success", 238 | }; 239 | 240 | var description = dice.Describe(rollExpression, roll, descriptions); 241 | Assert.That(description, Is.EqualTo(expectedDescription)); 242 | } 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /DnDGen.RollGen.Tests.Unit/Expressions/RegexConstantsTests.cs: -------------------------------------------------------------------------------- 1 | using DnDGen.RollGen.Expressions; 2 | using NUnit.Framework; 3 | using System.Text.RegularExpressions; 4 | 5 | namespace DnDGen.RollGen.Tests.Unit.Expressions 6 | { 7 | [TestFixture] 8 | public class RegexConstantsTests 9 | { 10 | [TestCase(RegexConstants.NumberPattern, "-?\\d+")] 11 | [TestCase(RegexConstants.CommonRollRegexPattern, "d *" + RegexConstants.NumberPattern + "(?: *(" 12 | + "( *!)" //explode default 13 | + "|( *(e *" + RegexConstants.NumberPattern + "))" //explode specific 14 | + "|( *(t *" + RegexConstants.NumberPattern + " *: *" + RegexConstants.NumberPattern + "))" //transform specific 15 | + "|( *(t *" + RegexConstants.NumberPattern + "))" //transform 16 | + "|( *(k *" + RegexConstants.NumberPattern + "))" //keep 17 | + ")*)")] 18 | [TestCase(RegexConstants.StrictRollPattern, "(?:(?:\\d* +)|(?:(?<=\\D|^)" + RegexConstants.NumberPattern + " *)|^)" + RegexConstants.CommonRollRegexPattern)] 19 | [TestCase(RegexConstants.LenientRollPattern, "\\d* *" + RegexConstants.CommonRollRegexPattern)] 20 | [TestCase(RegexConstants.ExpressionWithoutDieRollsPattern, "(?:[-+]?\\d*\\.?\\d+[%/\\+\\-\\*])+(?:[-+]?\\d*\\.?\\d+)")] 21 | [TestCase(RegexConstants.BooleanExpressionPattern, "[<=>]")] 22 | public void Pattern(string constant, string value) 23 | { 24 | Assert.That(constant, Is.EqualTo(value)); 25 | } 26 | 27 | [TestCase("1d2", true)] 28 | [TestCase(" 1 d 2 ", true)] 29 | [TestCase("d2", true)] 30 | [TestCase(" d 2 ", true)] 31 | [TestCase("1 and 2", false)] 32 | [TestCase("1d2k3", true)] 33 | [TestCase("1d2k3!", true)] 34 | [TestCase(" 1 d 2 k 3 ", true)] 35 | [TestCase(" 1 d 2 k 3 ! ", true)] 36 | [TestCase("d2k3", true)] 37 | [TestCase(" d 2 k 3 ", true)] 38 | [TestCase("This is not a match", false)] 39 | [TestCase("4d6k3", true)] 40 | [TestCase(" 4 d 6 k 3 ", true)] 41 | [TestCase("1d2!", true)] 42 | [TestCase(" 1 d 2 ! ", true)] 43 | [TestCase("1d2!k3", true)] 44 | [TestCase(" 1 d 2 ! k 3 ", true)] 45 | [TestCase("3d6t1", true)] 46 | [TestCase(" 3 d 6 t 1 ", true)] 47 | [TestCase("3d6t1t2", true)] 48 | [TestCase(" 3 d 6 t 1 t 2 ", true)] 49 | [TestCase(" 3 d 6 t 1 : 2 ", true)] 50 | [TestCase("3d6t1:2t3:4k5", true)] 51 | [TestCase(" 3 d 6 t 1 : 2 t 3 : 4 k 5 ", true)] 52 | [TestCase("3d6t1t3:4k5", true)] 53 | [TestCase(" 3 d 6 t 1 t 3 : 4 k 5 ", true)] 54 | [TestCase("3d6t1:2t3k5", true)] 55 | [TestCase(" 3 d 6 t 1 : 2 t 3 k 5 ", true)] 56 | [TestCase("2d4!t1t2k3", true)] 57 | [TestCase(" 2 d 4 ! t 1 k 3 ", true)] 58 | [TestCase("1d4e2", true)] 59 | [TestCase(" 1 d 4 e 2 ", true)] 60 | [TestCase("1d4e2!", true)] 61 | [TestCase(" 1 d 4 e 2 ! ", true)] 62 | [TestCase("1d4e2e3!", true)] 63 | [TestCase(" 1 d 4 e 2 e 3 ! ", true)] 64 | [TestCase("1d4e4", true)] 65 | [TestCase(" 1 d 4 e 4 ", true)] 66 | [TestCase("1d1", true)] 67 | [TestCase("-1d1", true)] 68 | [TestCase("1d-1", true)] 69 | [TestCase("-1d-1", true)] 70 | [TestCase("-1d-2!t-3:-4k-5", true)] 71 | [TestCase("--1d2", true, false)] 72 | [TestCase("1d-2", true)] 73 | [TestCase("1d--2", false)] 74 | [TestCase("--1d--2", false)] 75 | [TestCase("-23d-45", true)] 76 | [TestCase("0d0", true)] 77 | [TestCase("00d00", true)] 78 | [TestCase("not a roll", false)] 79 | [TestCase("1d2-3d4", true, false)] 80 | [TestCase("Roll this: -5d6", true, false)] 81 | //From README 82 | [TestCase("4d6", true)] 83 | [TestCase("92d66", true)] 84 | [TestCase("5+3d4*2", true, false)] 85 | [TestCase("((1d2)d5k1)d6", true, false)] 86 | [TestCase("4d6k3", true)] 87 | [TestCase("3d4k2", true)] 88 | [TestCase("5+3d4*3", true, false)] 89 | [TestCase("1d6+3", true, false)] 90 | [TestCase("1d8+1d2-1", true, false)] 91 | [TestCase("4d3-3", true, false)] 92 | [TestCase("4d6!", true)] 93 | [TestCase("3d4!", true)] 94 | [TestCase("3d4!k2", true)] 95 | [TestCase("3d4!e3", true)] 96 | [TestCase("3d4e1e2k2", true)] 97 | [TestCase("3d6t1", true)] 98 | [TestCase("3d6t1t5", true)] 99 | [TestCase("3d6!t1k2", true)] 100 | [TestCase("3d6t1:2", true)] 101 | [TestCase("4d3t2k1", true)] 102 | [TestCase("4d3k1t2", true)] 103 | [TestCase("4d3!t2k1", true)] 104 | [TestCase("4d3!k1t2", true)] 105 | [TestCase("4d3t2!k1", true)] 106 | [TestCase("4d3k1!t2", true)] 107 | [TestCase("4d3t2k1!", true)] 108 | [TestCase("4d3k1t2!", true)] 109 | public void StrictRollRegexMatches(string source, bool isMatch, bool verifyMatchContents = true) 110 | { 111 | VerifyMatch(RegexConstants.StrictRollPattern, source, isMatch, verifyMatchContents); 112 | } 113 | 114 | private void VerifyMatch(string pattern, string source, bool isMatch, bool verifyMatchContents = true) 115 | { 116 | var regex = new Regex(pattern); 117 | var match = regex.Match(source); 118 | Assert.That(match.Success, Is.EqualTo(isMatch)); 119 | 120 | if (match.Success && verifyMatchContents) 121 | Assert.That(match.Value.Trim(), Is.EqualTo(source.Trim())); 122 | } 123 | 124 | [TestCase("1", false)] 125 | [TestCase("1d2", false)] 126 | [TestCase("1d2k3", false)] 127 | [TestCase("This is not a match", false)] 128 | [TestCase("1+2-3*4/5%6", true)] 129 | [TestCase(" 1 + 2 - 3 * 4 / 5 % 6 ", true, IgnoreReason = "Ignoring because spaces might indicate a sentence, and the expression evaluator shouldn't override that")] 130 | public void ExpressionWithoutDieRollsRegexMatches(string source, bool isMatch) 131 | { 132 | VerifyMatch(RegexConstants.ExpressionWithoutDieRollsPattern, source, isMatch); 133 | } 134 | 135 | [TestCase("1", false)] 136 | [TestCase("1d2", false)] 137 | [TestCase("1d2k3", false)] 138 | [TestCase("This is not a match", false)] 139 | [TestCase("1+2-3*4/5%6", false)] 140 | [TestCase("1d20 > 10", true)] 141 | [TestCase("1d20 < 2d10", true)] 142 | [TestCase("3d2 >= 2d3", true)] 143 | [TestCase("2d6 <= 3d4", true)] 144 | [TestCase("1d2 = 2", true)] 145 | [TestCase("1d100 > 0", true)] 146 | [TestCase("100 < 1 d 20", true)] 147 | [TestCase("100<1d20", true)] 148 | [TestCase("1d1 = 1", true)] 149 | [TestCase("9266 = 9266", true)] 150 | [TestCase("9266 = 90210", true)] 151 | [TestCase("9266=90210", true)] 152 | [TestCase("1d2 = 3", true)] 153 | public void BooleanExpressionRegexMatches(string source, bool isMatch) 154 | { 155 | VerifyMatch(RegexConstants.BooleanExpressionPattern, source, isMatch, false); 156 | } 157 | 158 | [TestCase("1d4", "2d3")] 159 | [TestCase("1d2", "1d20")] 160 | [TestCase("1d2", "1d2+1d20")] 161 | [TestCase("2d4", "12d4")] 162 | [TestCase("2d4", "12d4+2d4")] 163 | [TestCase("1d6", "1d6-1d6")] 164 | [TestCase("3d8", "3d8")] 165 | public void GetRepeatedRoll_ReturnsNoMatch(string roll, string source) 166 | { 167 | var result = RegexConstants.GetRepeatedRoll(roll, source); 168 | Assert.That(result.IsMatch, Is.False); 169 | Assert.That(result.Match, Is.Empty); 170 | Assert.That(result.Index, Is.EqualTo(-1)); 171 | Assert.That(result.MatchCount, Is.Zero); 172 | } 173 | 174 | [TestCase("1d4", "1d4+1d4", "1d4+1d4", 0, 2)] 175 | [TestCase("1d4", "2d6+1d4+1d4", "1d4+1d4", 4, 2)] 176 | [TestCase("1d4", "1d4+1d4+2d3", "1d4+1d4", 0, 2)] 177 | [TestCase("10000d6", "10000d6+10000d6+9266d6+4", "10000d6+10000d6", 0, 2)] 178 | [TestCase("1d2", "1d2+1d2+1d2+1d20", "1d2+1d2+1d2", 0, 3)] 179 | [TestCase("2d4", "12d4+2d4+2d4+2d4+2d4", "2d4+2d4+2d4+2d4", 5, 4)] 180 | [TestCase("2d4", "12d4+2d4+2d4+2d4+2d4-1d20", "2d4+2d4+2d4+2d4", 5, 4)] 181 | [TestCase("1d20", "1d20+1d20+1d20-1d20", "1d20+1d20+1d20", 0, 3)] 182 | [TestCase("3d8", "3d8+3d8+3d8-3d8", "3d8+3d8+3d8", 0, 3)] 183 | [TestCase("3d8", "3d8+3d8+3d8/2", "3d8+3d8", 0, 2)] 184 | [TestCase("3d8", "3d8+3d8+(3d8)/(3d8)", "3d8+3d8", 0, 2)] 185 | [TestCase("3d8", "3d8+3d8+3d8*2", "3d8+3d8", 0, 2)] 186 | [TestCase("3d8", "3d8+3d8+(3d8)*(3d8)", "3d8+3d8", 0, 2)] 187 | [TestCase("3d8", "3d8+3d8+3d8%2", "3d8+3d8", 0, 2)] 188 | [TestCase("3d8", "3d8+3d8+(3d8)%(3d8)", "3d8+3d8", 0, 2)] 189 | [TestCase("4d10", "2+4d10+4d10+4d10", "4d10+4d10+4d10", 2, 3)] 190 | [TestCase("4d10", "(4d10)+4d10+4d10+4d10", "4d10+4d10+4d10", 7, 3)] 191 | [TestCase("4d10", "(4d10+4d10)+4d10+4d10", "4d10+4d10", 1, 2)] 192 | [TestCase("4d10", "4d10+(4d10+4d10)+4d10+4d10", "4d10+4d10", 6, 2)] 193 | [TestCase("4d10", "(4d10+4d12)+4d10+4d10", "4d10+4d10", 12, 2)] 194 | [TestCase("4d10", "2-4d10+4d10+4d10", "4d10+4d10", 7, 2)] 195 | [TestCase("4d10", "4d10-4d10+4d10+4d10", "4d10+4d10", 10, 2)] 196 | [TestCase("4d10", "2/4d10+4d10+4d10", "4d10+4d10", 7, 2)] 197 | [TestCase("4d10", "(4d10)/(4d10)+4d10+4d10", "4d10+4d10", 14, 2)] 198 | [TestCase("4d10", "2*4d10+4d10+4d10", "4d10+4d10", 7, 2)] 199 | [TestCase("4d10", "(4d10)*(4d10)+4d10+4d10", "4d10+4d10", 14, 2)] 200 | [TestCase("4d10", "2%4d10+4d10+4d10", "4d10+4d10", 7, 2)] 201 | [TestCase("4d10", "(4d10)%(4d10)+4d10+4d10", "4d10+4d10", 14, 2)] 202 | public void GetRepeatedRoll_ReturnsMatch(string roll, string source, string expectedMatch, int expectedIndex, int expectedCount) 203 | { 204 | var result = RegexConstants.GetRepeatedRoll(roll, source); 205 | Assert.That(result.IsMatch, Is.True); 206 | Assert.That(result.Match, Is.EqualTo(expectedMatch)); 207 | Assert.That(result.Index, Is.EqualTo(expectedIndex)); 208 | Assert.That(result.MatchCount, Is.EqualTo(expectedCount)); 209 | } 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /DnDGen.RollGen.Tests.Integration.Stress/ChainTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using System; 3 | using System.Linq; 4 | 5 | namespace DnDGen.RollGen.Tests.Integration.Stress 6 | { 7 | [TestFixture] 8 | public class ChainTests : StressTests 9 | { 10 | private Dice dice; 11 | private Random random; 12 | 13 | [SetUp] 14 | public void Setup() 15 | { 16 | dice = GetNewInstanceOf(); 17 | random = GetNewInstanceOf(); 18 | } 19 | 20 | [Test] 21 | public void StressNumericChain() 22 | { 23 | stressor.Stress(AssertNumericChain); 24 | } 25 | 26 | [Test] 27 | public void StressExpressionChain() 28 | { 29 | stressor.Stress(AssertExpressionChain); 30 | } 31 | 32 | [Test] 33 | public void StressRollChain() 34 | { 35 | stressor.Stress(AssertRollChain); 36 | } 37 | 38 | protected void AssertNumericChain() 39 | { 40 | var q = GetRandomNumber(); 41 | var d = GetRandomNumber(); 42 | var k = GetRandomNumber(); 43 | var p = GetRandomNumber(); 44 | var m = GetRandomNumber(); 45 | var t = GetRandomNumber(); 46 | var tr1 = GetRandomNumber(10); 47 | var tr2 = GetRandomNumber(10); 48 | var db = GetRandomNumber(); 49 | var md = GetRandomNumber(); 50 | var percentageThreshold = random.NextDouble(); 51 | var rollThreshold = random.Next(); 52 | 53 | var roll = GetRoll(q, d, k, p, m, t, db, md, tr1, tr2); 54 | 55 | AssertRoll( 56 | roll, 57 | q.ToString(), 58 | d.ToString(), 59 | k.ToString(), 60 | p.ToString(), 61 | m.ToString(), 62 | t.ToString(), 63 | db.ToString(), 64 | md.ToString(), 65 | percentageThreshold, 66 | rollThreshold, 67 | tr1.ToString(), 68 | tr2.ToString()); 69 | } 70 | 71 | private int GetRandomNumber(int top = 2) 72 | { 73 | return random.Next(top) + 1; 74 | } 75 | 76 | private void AssertRoll( 77 | PartialRoll roll, 78 | string q, 79 | string d, 80 | string k, 81 | string p, 82 | string m, 83 | string t, 84 | string db, 85 | string md, 86 | double percentageThreshold, 87 | int rollThreshold, 88 | string tr1, 89 | string tr2) 90 | { 91 | var rollMin = GetRollMin(tr1, tr2); 92 | var min = Math.Min(ComputeMinimum(q), ComputeMinimum(k)) * 2 93 | + Math.Min(ComputeMinimum(q), ComputeMinimum(k)) * rollMin 94 | + ComputeMinimum(p) 95 | - ComputeMinimum(m) 96 | * ComputeMinimum(t) 97 | / ComputeMinimum(db) 98 | % ComputeMinimum(md); 99 | var max = ComputeMaximum(k) * 4 100 | + ComputeMaximum(k) * 10 101 | + ComputeMaximum(k) * 100 102 | + ComputeMaximum(p) 103 | - ComputeMaximum(m) 104 | * ComputeMaximum(t) 105 | / ComputeMaximum(db) 106 | % ComputeMaximum(md); 107 | var explodeMax = ComputeMaximum(k) * 4 108 | + ComputeMaximum(k) * 10 109 | + ComputeMaximum(k) * 100 * 10 110 | + ComputeMaximum(p) 111 | - ComputeMaximum(m) 112 | * ComputeMaximum(t) 113 | / ComputeMaximum(db) 114 | % ComputeMaximum(md); 115 | 116 | Assert.That(roll.IsValid(), Is.True, roll.CurrentRollExpression); 117 | Assert.That(roll.AsSum(), Is.InRange(min, max * 10), roll.CurrentRollExpression); 118 | Assert.That(roll.AsPotentialMinimum(), Is.EqualTo(min), roll.CurrentRollExpression); 119 | Assert.That(roll.AsPotentialMaximum(false), Is.EqualTo(max), roll.CurrentRollExpression); 120 | Assert.That(roll.AsPotentialMaximum(), Is.EqualTo(explodeMax), roll.CurrentRollExpression); 121 | //HACK: We are ignoring the average, since it will probably result in decimal rolls during evaluation 122 | 123 | Assert.That(roll.AsTrueOrFalse(percentageThreshold), Is.True.Or.False, roll.CurrentRollExpression); 124 | Assert.That(roll.AsTrueOrFalse(rollThreshold), Is.True.Or.False, roll.CurrentRollExpression); 125 | 126 | var rolls = roll.AsIndividualRolls(); 127 | 128 | Assert.That(rolls.Count(), Is.EqualTo(1), roll.CurrentRollExpression); 129 | Assert.That(rolls, Has.All.InRange(min, explodeMax), roll.CurrentRollExpression); 130 | } 131 | 132 | private int GetRollMin(params string[] transforms) 133 | { 134 | var minimums = transforms.Select(ComputeMinimum); 135 | 136 | var minimum = 1; 137 | while (minimums.Contains(minimum)) 138 | minimum++; 139 | 140 | return minimum; 141 | } 142 | 143 | private double ComputeMinimum(string expression) 144 | { 145 | return dice.Roll(expression).AsPotentialMinimum(); 146 | } 147 | 148 | private double ComputeMaximum(string expression) 149 | { 150 | return dice.Roll(expression).AsPotentialMaximum(); 151 | } 152 | 153 | protected void AssertExpressionChain() 154 | { 155 | var q = $"{GetRandomNumber()}d{GetRandomNumber()}"; 156 | var d = $"{GetRandomNumber()}d{GetRandomNumber()}"; 157 | var k = $"{GetRandomNumber()}d{GetRandomNumber()}"; 158 | var p = $"{GetRandomNumber()}d{GetRandomNumber()}"; 159 | var m = $"{GetRandomNumber()}d{GetRandomNumber()}"; 160 | var t = $"{GetRandomNumber()}d{GetRandomNumber()}"; 161 | var db = $"{GetRandomNumber()}d{GetRandomNumber()}"; 162 | var md = $"{GetRandomNumber()}d{GetRandomNumber()}"; 163 | var tr1 = $"{GetRandomNumber()}d{GetRandomNumber(10)}"; 164 | var tr2 = $"{GetRandomNumber()}d{GetRandomNumber(10)}"; 165 | var percentageThreshold = random.NextDouble(); 166 | var rollThreshold = random.Next(); 167 | 168 | var roll = GetRoll(q, d, k, p, m, t, db, md, tr1, tr2); 169 | 170 | AssertRoll( 171 | roll, 172 | q.ToString(), 173 | d.ToString(), 174 | k.ToString(), 175 | p.ToString(), 176 | m.ToString(), 177 | t.ToString(), 178 | db.ToString(), 179 | md.ToString(), 180 | percentageThreshold, 181 | rollThreshold, 182 | tr1, 183 | tr2); 184 | } 185 | 186 | protected void AssertRollChain() 187 | { 188 | var q = dice.Roll($"{GetRandomNumber()}d{GetRandomNumber()}"); 189 | var d = dice.Roll($"{GetRandomNumber()}d{GetRandomNumber()}"); 190 | var k = dice.Roll($"{GetRandomNumber()}d{GetRandomNumber()}"); 191 | var p = dice.Roll($"{GetRandomNumber()}d{GetRandomNumber()}"); 192 | var m = dice.Roll($"{GetRandomNumber()}d{GetRandomNumber()}"); 193 | var t = dice.Roll($"{GetRandomNumber()}d{GetRandomNumber()}"); 194 | var db = dice.Roll($"{GetRandomNumber()}d{GetRandomNumber()}"); 195 | var md = dice.Roll($"{GetRandomNumber()}d{GetRandomNumber()}"); 196 | var tr1 = dice.Roll($"{GetRandomNumber()}d{GetRandomNumber(10)}"); 197 | var tr2 = dice.Roll($"{GetRandomNumber()}d{GetRandomNumber(10)}"); 198 | var percentageThreshold = random.NextDouble(); 199 | var rollThreshold = random.Next(); 200 | 201 | var roll = GetRoll(q, d, k, p, m, t, db, md, tr1, tr2); 202 | 203 | AssertRoll(roll, 204 | q.CurrentRollExpression, 205 | d.CurrentRollExpression, 206 | k.CurrentRollExpression, 207 | p.CurrentRollExpression, 208 | m.CurrentRollExpression, 209 | t.CurrentRollExpression, 210 | db.CurrentRollExpression, 211 | md.CurrentRollExpression, 212 | percentageThreshold, 213 | rollThreshold, 214 | tr1.CurrentRollExpression, 215 | tr2.CurrentRollExpression); 216 | } 217 | 218 | private PartialRoll GetRoll(int q, int d, int k, double p, double m, double t, double db, double md, int tr1, int tr2) => dice.Roll(q) 219 | .d(d) 220 | .d2() 221 | .d3() 222 | .d4() 223 | .Keeping(k) //HACK: Keeping quantity from getting too high 224 | .Plus(q) 225 | .d6() 226 | .d8() 227 | .d10() 228 | .Keeping(k) //HACK: Keeping quantity from getting too high 229 | .Plus(q) 230 | .d12() 231 | .d20() 232 | .Percentile() 233 | .Explode() 234 | .Transforming(tr1) 235 | .Transforming(tr2) 236 | .Keeping(k) 237 | .Plus(p) 238 | .Minus(m) 239 | .Times(t) 240 | .DividedBy(db) 241 | .Modulos(md); 242 | private PartialRoll GetRoll(string q, string d, string k, string p, string m, string t, string db, string md, string tr1, string tr2) => dice.Roll(q) 243 | .d(d) 244 | .d2() 245 | .d3() 246 | .d4() 247 | .Keeping(k) //HACK: Keeping quantity from getting too high 248 | .Plus(q) 249 | .d6() 250 | .d8() 251 | .d10() 252 | .Keeping(k) //HACK: Keeping quantity from getting too high 253 | .Plus(q) 254 | .d12() 255 | .d20() 256 | .Percentile() 257 | .Explode() 258 | .Transforming(tr1) 259 | .Transforming(tr2) 260 | .Keeping(k) 261 | .Plus(p) 262 | .Minus(m) 263 | .Times(t) 264 | .DividedBy(db) 265 | .Modulos(md); 266 | private PartialRoll GetRoll( 267 | PartialRoll q, 268 | PartialRoll d, 269 | PartialRoll k, 270 | PartialRoll p, 271 | PartialRoll m, 272 | PartialRoll t, 273 | PartialRoll db, 274 | PartialRoll md, 275 | PartialRoll tr1, 276 | PartialRoll tr2) => dice.Roll(q) 277 | .d(d) 278 | .d2() 279 | .d3() 280 | .d4() 281 | .Keeping(k) //HACK: Keeping quantity from getting too high 282 | .Plus(q) 283 | .d6() 284 | .d8() 285 | .d10() 286 | .Keeping(k) //HACK: Keeping quantity from getting too high 287 | .Plus(q) 288 | .d12() 289 | .d20() 290 | .Percentile() 291 | .Explode() 292 | .Transforming(tr1) 293 | .Transforming(tr2) 294 | .Keeping(k) 295 | .Plus(p) 296 | .Minus(m) 297 | .Times(t) 298 | .DividedBy(db) 299 | .Modulos(md); 300 | } 301 | } 302 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RollGen 2 | 3 | Rolls a set of dice, as determined by the D20 die system. 4 | 5 | [![Build Status](https://dev.azure.com/dndgen/DnDGen/_apis/build/status/DnDGen.RollGen?branchName=master)](https://dev.azure.com/dndgen/DnDGen/_build/latest?definitionId=13&branchName=master) 6 | 7 | ## Syntax 8 | 9 | ### Use 10 | 11 | To use RollGen, simple adhere to the verbose, fluid syntax: 12 | 13 | ```C# 14 | var standardRoll = dice.Roll(4).d6().AsSum(); 15 | var customRoll = dice.Roll(92).d(66).AsSum(); 16 | var expressionRoll = dice.Roll("5+3d4*2").AsSum(); 17 | 18 | var chainedRolls = dice.Roll().d2().d(5).Keeping(1).d6().AsSum(); //1d2d5k1d6 Evaluated left to right -> ((1d2)d5k1)d6 19 | 20 | var individualRolls = dice.Roll(4).d6().AsIndividualRolls(); 21 | var parsedRolls = dice.Roll("5+3d4*2").AsIndividualRolls(); //NOTE: This will only return 1 roll, the same as AsSum() 22 | 23 | var keptRolls = dice.Roll(4).d6().Keeping(3).AsIndividualRolls(); //Returns the highest 3 rolls 24 | var expressionKeptRolls = dice.Roll("3d4k2").AsSum(); //Returns the sum of 2 highest rolls 25 | 26 | var averageRoll = dice.Roll(4).d6().AsPotentialAverage(); //Returns the average roll for the expression. For here, it will return 14. 27 | var expressionAverageRoll = dice.Roll("5+3d4*3").AsPotentialAverage(); //5+7.5*3, returning 27.5 28 | 29 | var minRoll = dice.Roll(4).d6().AsPotentialMinimum(); //Returns the minimum roll for the expression. For here, it will return 4. 30 | var expressionMinRoll = dice.Roll("5+3d4*3").AsPotentialMinimum(); //5+3*3, returning 14 31 | 32 | var maxRoll = dice.Roll(4).d6().AsPotentialMaximum(); //Returns the maximum roll for the expression. For here, it will return 24. 33 | var expressionMaxRoll = dice.Roll("5+3d4*3").AsPotentialMaximum(); //5+12*3, returning 41 34 | 35 | var success = dice.Roll().Percentile().AsTrueOrFalse(); //Returns true if high (51-100), false if low (1-50) 36 | var customPercentageSuccess = dice.Roll().Percentile().AsTrueOrFalse(.9); //Returns true if > 90, false if <= 90 37 | var customRollSuccess = dice.Roll().Percentile().AsTrueOrFalse(90); //Returns true if >= 90, false if < 90 38 | var expressionSuccess = dice.Roll("5+3d4*2").AsTrueOrFalse(); //Returns true if high, false if low 39 | var explicitExpressionSuccess = dice.Roll("2d6 >= 1d12").AsTrueOrFalse(); //Evalutes boolean expression after rolling 40 | 41 | var containsRoll = dice.ContainsRoll("This contains a roll of 4d6k3 for rolling stats"); //will return true here 42 | var summedSentence = dice.ReplaceRollsWithSumExpression("This contains a roll of 4d6k3 for rolling stats"); //returns "This contains a roll of (5 + 3 + 2) for rolling stats" 43 | var rolledSentence = dice.ReplaceExpressionWithTotal("This contains a roll of 4d6k3 for rolling stats"); //returns "This contains a roll of 10 for rolling stats" 44 | var rolledComplexSentenceMin = dice.ReplaceWrappedExpressions("Fireball does {min(4d6,10) + 0.5} damage"); //returns "Fireball does 7.5 damage" 45 | var rolledComplexSentenceMax = dice.ReplaceWrappedExpressions("Fireball does {max(4d6,10) + 0.5} damage"); //returns "Fireball does 15.5 damage" 46 | 47 | var optimizedRollWithFewestDice = RollHelper.GetRollWithFewestDice(1, 9); //returns "4d3-3", because it uses only 1 kind of dice compared to "1d8+1d2-1" 48 | var optimizedRoll = RollHelper.GetRollWithMostEvenDistribution(4, 9); //returns "1d6+3", which is the most evenly-distributed roll possible, whether optimizing for dice or distribution 49 | var optimizedRollWithMultipleDice = RollHelper.GetRollWithMostEvenDistribution(1, 9); //returns "1d8+1d2-1", because it more evenly-distributed than "4d3-3" 50 | var optimizedRollWithMultipliers = RollHelper.GetRollWithMostEvenDistribution(1, 36, allowMultipliers: true); //returns "(1d12-1)*3+1d3" 51 | var optimizedRollWithNonstandardDice = RollHelper.GetRollWithMostEvenDistribution(1, 36, allowNonstandardDice: true); //returns "1d36" 52 | var optimizedRollWithMultipliersAndNonstandardDice = RollHelper.GetRollWithMostEvenDistribution(1, 45, true, true); //returns "(1d3-1)*15+(1d3-1)*5+1d5" 53 | 54 | var explodedRolls = dice.Roll(4).d6().Explode().AsIndividualRolls(); //If a 6 is rolled, then an additional roll is performed. I.E., 3 + 6 + 5 + 4 + 1 55 | var expressionExplodedRolls = dice.Roll("3d4!").AsSum(); //Return the sum of the rolls, including bonus rolls from explosion on rolls of 4 56 | var expressionExplodedKeptRolls = dice.Roll("3d4!k2").AsSum(); //Returns the sum of 2 highest rolls, including bonus rolls from explosion on rolls of 4 57 | var expressionExplodedMultipleRolls = dice.Roll("3d4!e3").AsSum(); //Return the sum of the rolls, including bonus rolls from explosion on rolls of 3 or 4 58 | var expressionExplodedMultipleKeptRolls = dice.Roll("3d4e1e2k2").AsSum(); //Returns the sum of 2 highest rolls, including bonus rolls from explosion on rolls of 1 or 2 59 | 60 | var transformedRolls = dice.Roll(3).d6().Transforming(1).AsIndividualRolls(); //If a 1 is rolled, we will count it as a 6. I.E., 3 + 1 + 6 = 3 + 6 + 6 61 | var transformedMultipleRolls = dice.Roll("3d6t1t5").AsSum(); //Return the sum of the rolls, including 1's and 5's transformed to 6's 62 | var transformedExplodedKeptRolls = dice.Roll("3d6!t1k2").AsSum(); //Returns the sum of 2 highest rolls, including bonus rolls from explosion and transforming 1's to 6's. 63 | var transformedCustomRolls = dice.Roll("3d6t1:2").AsSum(); //Return the sum of the rolls, transforming 1's into 2's. 64 | 65 | //Returns a qualitative description of the roll, based on percentages 66 | var description = dice.Describe("3d6", 9); //Returns "Bad" 67 | description = dice.Describe("3d6", 13); //Returns "Good" 68 | description = dice.Describe("3d6", 12, "Bad", "Average", "Good"); //Returns "Average" 69 | description = dice.Describe("3d6", 6, "Bad", "Average", "Good"); //Returns "Bad" 70 | description = dice.Describe("3d6", 16, "Bad", "Average", "Good"); //Returns "Good" 71 | 72 | //Returns whether the roll is valid 73 | var valid = dice.IsValid("3d6+1"); //Returns TRUE 74 | valid = dice.IsValid("(3)d(6)+1"); //Returns TRUE 75 | valid = dice.IsValid("(1d2)d(3d4)+5d6"); //Returns TRUE 76 | valid = dice.IsValid("10000d10000"); //Returns TRUE 77 | valid = dice.IsValid("(100d100d100)d(100d100d100)"); //Returns TRUE 78 | 79 | valid = dice.IsValid("(0)d(6)+1"); //Returns FALSE, because quantity is too low 80 | valid = dice.IsValid("(3.1)d(6)+1"); //Returns FALSE, because decimals are not allowed for dice operations 81 | valid = dice.IsValid("0d6+1"); //Returns FALSE, because quantity is too low 82 | valid = dice.IsValid("10001d10000"); //Returns FALSE, because quantity is too high 83 | valid = dice.IsValid("(100d100d100+1)d(100d100d100)"); //Returns FALSE, because quantity could potentially be above the 10,000 limit 84 | 85 | valid = dice.IsValid("(3)d(-6)+1"); //Returns FALSE, because die is too low 86 | valid = dice.IsValid("(3)d(6.1)+1"); //Returns FALSE, because decimals are not allowed for dice operations 87 | valid = dice.IsValid("3d0+1"); //Returns FALSE, because die is too low 88 | valid = dice.IsValid("10000d10001"); //Returns FALSE, because die is too high 89 | valid = dice.IsValid("(100d100d100)d(100d100d100+1)"); //Returns FALSE, because die could potentially be above the 10,000 limit 90 | 91 | valid = dice.IsValid("4d6k3"); //Returns TRUE 92 | valid = dice.IsValid("(4)d(6)k(3)"); //Returns TRUE 93 | valid = dice.IsValid("(4)d(6)k(-1)"); //Returns FALSE, because keep value is too low 94 | valid = dice.IsValid("(4)d(6)k(3.1)"); //Returns FALSE, because decimals are not allowed for dice operations 95 | valid = dice.IsValid("4d6k10000"); //Returns TRUE 96 | valid = dice.IsValid("4d6k10001"); //Returns FALSE, because keep value is too high 97 | valid = dice.IsValid("4d6k(100d100d100+1)"); //Returns FALSE, because keep value could potentially be above the 10,000 limit 98 | 99 | valid = dice.IsValid("2d3!"); //Returns TRUE 100 | valid = dice.IsValid("2d3!e2"); //Returns TRUE 101 | valid = dice.IsValid("2d3!e2e1"); //Returns FALSE, because it explodes on all values 102 | 103 | valid = dice.IsValid("3d6t1"); //Returns TRUE 104 | valid = dice.IsValid("3d6t1t2"); //Returns TRUE 105 | valid = dice.IsValid("3d6t7"); //Returns TRUE 106 | valid = dice.IsValid("3d6t0"); //Returns FALSE, because transform target is too low 107 | valid = dice.IsValid("3d6t6:0"); //Returns TRUE 108 | valid = dice.IsValid("3d6t10001"); //Returns FALSE, because transform target is too high 109 | valid = dice.IsValid("3d6t6:10001"); //Returns TRUE 110 | 111 | valid = dice.IsValid("avg(1d12, 2d6, 3d4, 4d3, 6d2)"); //Returns TRUE, because this is a valid Albatross function 112 | valid = dice.IsValid("bad(1d12, 2d6, 3d4, 4d3, 6d2)"); //Returns FALSE, because "bad" is not a valid Albatross function 113 | 114 | valid = dice.IsValid("this is not a roll"); //Returns FALSE 115 | valid = dice.IsValid("this contains 3d6, but is not a roll"); //Returns FALSE 116 | valid = dice.IsValid("9266+90210-42*600/1337%1336+96d(783d82%45+922-2022/337)-min(1d2, 3d4, 5d6)+max(1d2, 3d4, 5d6)*avg(1d2, 3d4, 5d6)"); //Returns TRUE 117 | 118 | ``` 119 | 120 | Important things to note: 121 | 122 | 1. When retrieving individual rolls from an expression, only the sum of the expression is returned. This is because determining what an "individual roll" is within a complex expression is not certain. As an example, what would individual rolls be for `1d2+3d4`? 123 | 2. Mathematical expressions supported are `+`, `-`, `*`, `/`, and `%`, as well as die rolls as demonstrated above. Spaces are allowed in the expression strings. Paranthetical expressions are also allowed. 124 | 3. For replacement methods on `Dice`, there is an option to do "lenient" replacements (optional boolean). The difference: 125 | a. "1d2 ghouls and 2d4 zombies" strict -> "1 ghouls and 5 zombies" 126 | b. "1d2 ghouls and 2d4 zombies" lenient -> "1 ghouls an3 zombies" (it reads "and 2d4" as "an(d 2d4)") 127 | 128 | #### Order of Operations 129 | 130 | In regards to the operators one can apply to a roll (Keeping, Exploding, Transforming), this is the order of operations: 131 | 132 | 1. Explode 133 | 2. Transform 134 | 3. Keep 135 | 136 | One can specify these commands in any order, as they will be evaluated in their order of operation. For example, all of these rolls will parse the same: `4d3!t2k1`, `4d3!k1t2`, `4d3t2!k1`, `4d3t2k1!`, `4d3k1!t2`, `4d3k1t2!` - all will be evaluated as `4d3`, exploding on a `3`, then transforming `2` into `3`, then keeping the highest roll. 137 | 138 | Beyond this, order of operations is respected as outlined by the Albatross documentation: https://rushuiguan.github.io/expression/articles/operations.html#the-precedence-of-infix-operations 139 | 140 | The documentation also outlines supported functions (such as `min` and `max`) that can be used: https://rushuiguan.github.io/expression/articles/operations.html 141 | 142 | ### Getting `Dice` Objects 143 | 144 | You can obtain dice from the domain project. Because the dice are very complex and are decorated in various ways, there is not a (recommended) way to build these objects manually. Please use the ModuleLoader for Ninject. 145 | 146 | ```C# 147 | var kernel = new StandardKernel(); 148 | var rollGenModuleLoader = new RollGenModuleLoader(); 149 | 150 | rollGenModuleLoader.LoadModules(kernel); 151 | ``` 152 | 153 | Your particular syntax for how the Ninject injection should work will depend on your project (class library, web site, etc.). Each of the examples below work independently. You should use the one that is appropriate for your project. 154 | 155 | ```C# 156 | [Inject] //Ninject property 157 | public Dice MyDice { get; set; } 158 | 159 | public MyClass() 160 | { } 161 | 162 | public int Roll() 163 | { 164 | return MyDice.Roll(4).d6().Keeping(3).AsSum(); 165 | } 166 | ``` 167 | 168 | ```C# 169 | private myDice; 170 | 171 | public MyClass(Dice dice) //This works if you are calling Ninject to build MyClass 172 | { 173 | myDice = dice; 174 | } 175 | 176 | public int Roll() 177 | { 178 | return myDice.Roll(4).d6().Keeping(3).AsSum(); 179 | } 180 | ``` 181 | 182 | ```C# 183 | public MyClass() 184 | { } 185 | 186 | public int Roll() 187 | { 188 | var myDice = DiceFactory.Create(); //Located in RollGen.IoC 189 | return myDice.Roll(4).d6().Keeping(3).AsSum(); 190 | } 191 | ``` 192 | 193 | ### Installing RollGen 194 | 195 | The project is on [Nuget](https://www.nuget.org/packages/DnDGen.RollGen). Install via the NuGet Package Manager. 196 | 197 | PM > Install-Package DnDGen.RollGen 198 | 199 | -------------------------------------------------------------------------------- /DnDGen.RollGen/RollHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace DnDGen.RollGen 6 | { 7 | public static class RollHelper 8 | { 9 | internal enum RankMode 10 | { 11 | FewestDice, 12 | BestDistribution, 13 | } 14 | 15 | /// 16 | /// This will return a roll of format XdY+Z 17 | /// 18 | /// This is subtracted from the effective range of lower and upper 19 | /// The inclusive lower range 20 | /// The inclusive upper range 21 | /// 22 | public static string GetRollWithFewestDice(int baseQuantity, int lower, int upper) 23 | { 24 | var newLower = lower - baseQuantity; 25 | var newUpper = upper - baseQuantity; 26 | 27 | return GetRollWithFewestDice(newLower, newUpper); 28 | } 29 | 30 | /// 31 | /// This will return a roll of format XdY+Z 32 | /// 33 | /// The inclusive lower range 34 | /// The inclusive upper range 35 | public static string GetRollWithFewestDice(int lower, int upper) 36 | { 37 | var range = upper - lower + 1; 38 | 39 | var collections = GetRollCollections(lower, upper, RankMode.FewestDice); 40 | if (!collections.Any()) 41 | throw new ArgumentException($"Cannot generate a valid roll for range [{lower},{upper}]"); 42 | 43 | var bestMatches = collections.GroupBy(c => c.Rolls.Count).OrderBy(g => g.Key).First(); 44 | if (bestMatches.Count() == 1) 45 | { 46 | return bestMatches.Single().Build(); 47 | } 48 | 49 | var bestMatch = bestMatches.OrderBy(c => c.ComputeDistribution()).First(); 50 | return bestMatch.Build(); 51 | } 52 | 53 | /// 54 | /// This will return a roll of format AdB+EdF+...+XdY+Z 55 | /// If multipliers are allowed, it will try to factor the range and produce a roll of: (1dA-1)*BC..Y+(1dB-1)*CD..Y+...+(1dX-1)*Y+1dY+Z 56 | /// If nonstandard dice are allowed, then ranges such as [1,5] will be written as 1d5 57 | /// 58 | /// This is subtracted from the effective range of lower and upper 59 | /// The inclusive lower range 60 | /// The inclusive upper range 61 | public static string GetRollWithMostEvenDistribution(int baseQuantity, int lower, int upper, bool allowMultipliers = false, bool allowNonstandardDice = false) 62 | { 63 | var newLower = lower - baseQuantity; 64 | var newUpper = upper - baseQuantity; 65 | 66 | return GetRollWithMostEvenDistribution(newLower, newUpper, allowMultipliers, allowNonstandardDice); 67 | } 68 | 69 | /// 70 | /// This will return a roll of format AdB+CdD+...+XdY+Z 71 | /// If multipliers are allowed, it will try to factor the range and produce a roll of: (1dA-1)*BC..Y+(1dB-1)*CD..Y+...+(1dX-1)*Y+1dY+Z 72 | /// If nonstandard dice are allowed, then ranges such as [1,5] will be written as 1d5 73 | /// 74 | /// The inclusive lower range 75 | /// The inclusive upper range 76 | /// Allow the range to be factored 77 | /// Allow non-standard dice to be used 78 | public static string GetRollWithMostEvenDistribution(int lower, int upper, bool allowMultipliers = false, bool allowNonstandardDice = false) 79 | { 80 | var range = upper - lower + 1; 81 | 82 | //We asked for a constant 83 | if (range == 1) 84 | { 85 | return lower.ToString(); 86 | } 87 | 88 | var collection = new RollCollection(); 89 | if (allowMultipliers) 90 | { 91 | var factored = GetFactoredRoll(lower, upper); 92 | collection.Rolls.AddRange(factored.Rolls); 93 | range = factored.RemainingRange; 94 | } 95 | 96 | //The factoring handled the entire range 97 | if (range == 1) 98 | { 99 | collection.Adjustment = lower - collection.Quantities; 100 | return collection.Build(); 101 | } 102 | 103 | //If we are allowing non-standard dice, we don't need to do computations for Distribution - we can just make an arbitrary die. 104 | if (allowNonstandardDice) 105 | { 106 | while (range > 1) 107 | { 108 | var die = Math.Min(range, Limits.Die); 109 | var prototypes = BuildRollPrototypes(1, range, die); 110 | collection.Rolls.AddRange(prototypes); 111 | 112 | var newLower = 1 - prototypes.Sum(p => p.Quantity); 113 | var newUpper = range - prototypes.Sum(p => p.Quantity * p.Die); 114 | range = newUpper - newLower + 1; 115 | } 116 | 117 | collection.Adjustment = lower - collection.Quantities; 118 | return collection.Build(); 119 | } 120 | 121 | var adjustedUpper = lower + range - 1; 122 | 123 | var collections = GetRollCollections(lower, adjustedUpper, RankMode.BestDistribution); 124 | if (!collections.Any()) 125 | throw new ArgumentException($"Cannot generate a valid roll for range [{lower},{adjustedUpper}]"); 126 | 127 | var bestMatches = collections.GroupBy(c => c.ComputeDistribution()).OrderBy(g => g.Key).First(); 128 | if (bestMatches.Count() == 1) 129 | { 130 | var match = bestMatches.Single(); 131 | collection.Rolls.AddRange(match.Rolls); 132 | collection.Adjustment = lower - collection.Quantities; 133 | 134 | return collection.Build(); 135 | } 136 | 137 | var bestMatch = bestMatches.OrderBy(c => c.Quantities).First(); 138 | collection.Rolls.AddRange(bestMatch.Rolls); 139 | collection.Adjustment = lower - collection.Quantities; 140 | 141 | return collection.Build(); 142 | } 143 | 144 | private static IEnumerable GetRollCollections(int lower, int upper, RankMode rankMode) 145 | { 146 | var adjustmentCollection = new RollCollection(); 147 | adjustmentCollection.Adjustment = upper; 148 | 149 | if (adjustmentCollection.Matches(lower, upper)) 150 | { 151 | return new[] { adjustmentCollection }; 152 | } 153 | 154 | var collections = GetRolls(lower, upper, rankMode); 155 | return collections; 156 | } 157 | 158 | private static IEnumerable GetRolls(int lower, int upper, RankMode rankMode) 159 | { 160 | var range = upper - lower + 1; 161 | var dice = RollCollection.StandardDice 162 | .Where(d => d <= range) 163 | .OrderByDescending(d => d); 164 | 165 | var prototypes = new List(); 166 | var minRank = long.MaxValue; 167 | 168 | foreach (var die in dice) 169 | { 170 | var diePrototypes = BuildRollPrototypes(lower, upper, die); 171 | if (!diePrototypes.Any()) 172 | continue; 173 | 174 | var collection = new RollCollection(); 175 | collection.Rolls.AddRange(diePrototypes); 176 | collection.Adjustment = lower - collection.Quantities; 177 | 178 | var rank = GetRank(collection, rankMode); 179 | if (rank > minRank) 180 | continue; 181 | 182 | if (collection.Matches(lower, upper)) 183 | { 184 | minRank = Math.Min(rank, minRank); 185 | prototypes.Add(collection); 186 | 187 | continue; 188 | } 189 | 190 | collection.Adjustment = 0; 191 | var remainingUpper = upper - collection.Upper; 192 | var remainingLower = lower - collection.Lower; 193 | 194 | foreach (var subrolls in GetRolls(remainingLower, remainingUpper, rankMode)) 195 | { 196 | var subCollection = new RollCollection(); 197 | subCollection.Rolls.AddRange(diePrototypes); 198 | subCollection.Rolls.AddRange(subrolls.Rolls); 199 | subCollection.Adjustment = lower - subCollection.Quantities; 200 | 201 | rank = GetRank(subCollection, rankMode); 202 | if (rank > minRank) 203 | continue; 204 | 205 | if (subCollection.Matches(lower, upper)) 206 | { 207 | minRank = Math.Min(rank, minRank); 208 | prototypes.Add(subCollection); 209 | } 210 | } 211 | } 212 | return prototypes; 213 | } 214 | 215 | private static long GetRank(RollCollection collection, RankMode rankMode) => rankMode switch 216 | { 217 | RankMode.FewestDice => collection.Rolls.Count, 218 | RankMode.BestDistribution => collection.ComputeDistribution(), 219 | _ => throw new ArgumentOutOfRangeException(nameof(rankMode), $"Not expected Rank Mode value: {rankMode}") 220 | }; 221 | 222 | private static IEnumerable BuildRollPrototypes(int lower, int upper, int die) 223 | { 224 | var newQuantity = (upper - lower) / (die - 1); 225 | var prototypes = new List(); 226 | 227 | if (newQuantity > Limits.Quantity) 228 | { 229 | var maxRollsQuantity = newQuantity / Limits.Quantity; 230 | var maxRoll = new RollPrototype 231 | { 232 | Quantity = Limits.Quantity, 233 | Die = die, 234 | }; 235 | 236 | var maxRolls = Enumerable.Repeat(maxRoll, maxRollsQuantity); 237 | prototypes.AddRange(maxRolls); 238 | 239 | newQuantity -= maxRollsQuantity * Limits.Quantity; 240 | } 241 | 242 | if (newQuantity > 0) 243 | { 244 | prototypes.Add(new RollPrototype 245 | { 246 | Die = die, 247 | Quantity = newQuantity 248 | }); 249 | } 250 | 251 | return prototypes; 252 | } 253 | 254 | /// 255 | /// This will return a roll of format (1dA-1)*BC..Y+(1dB-1)*CD..Y+...+(1dX-1)*Y+1dY+Z, where each variable is a standard die. 256 | /// An example is [1,48] = (1d12-1)*4+1d4 257 | /// Another example is [5,40] = (1d12-1)*3+1d3+4 258 | /// This works when the total range is divisible by the standard die (2, 3, 4, 6, 8, 10, 12, 20, 100). 259 | /// If the range is not divisible by the standard dice (such as [1,7]), then non-standard dice are used (1dX+Y) 260 | /// 261 | /// The inclusive lower range 262 | /// The inclusive upper range 263 | private static (IEnumerable Rolls, int RemainingRange) GetFactoredRoll(int lower, int upper) 264 | { 265 | var range = upper - lower + 1; 266 | var dice = RollCollection.StandardDice 267 | .Where(d => d <= range) 268 | .OrderByDescending(d => d) 269 | .ToArray(); 270 | var factors = new Dictionary(); 271 | 272 | foreach (var die in dice) 273 | { 274 | while (range % die == 0) 275 | { 276 | if (!factors.ContainsKey(die)) 277 | factors[die] = 0; 278 | 279 | factors[die]++; 280 | range /= die; 281 | } 282 | } 283 | 284 | var rolls = new List(); 285 | 286 | foreach (var die in dice.Where(factors.ContainsKey)) 287 | { 288 | while (factors[die] > 0) 289 | { 290 | factors[die]--; 291 | var product = range; 292 | 293 | foreach (var f in factors.Select(kvp => Convert.ToInt32(Math.Pow(kvp.Key, kvp.Value))).Where(p => p > 0)) 294 | { 295 | product *= f; 296 | } 297 | 298 | rolls.Add(new RollPrototype 299 | { 300 | Quantity = 1, 301 | Die = die, 302 | Multiplier = product, 303 | }); 304 | } 305 | } 306 | 307 | return (rolls, range); 308 | } 309 | } 310 | } 311 | -------------------------------------------------------------------------------- /DnDGen.RollGen.Tests.Integration/ReadmeTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using System.Linq; 3 | 4 | namespace DnDGen.RollGen.Tests.Integration 5 | { 6 | [TestFixture] 7 | public class ReadmeTests : IntegrationTests 8 | { 9 | private Dice dice; 10 | 11 | [SetUp] 12 | public void Setup() 13 | { 14 | dice = GetNewInstanceOf(); 15 | } 16 | 17 | [Test] 18 | [Repeat(100)] 19 | public void StandardRoll() 20 | { 21 | var roll = dice.Roll(4).d6().AsSum(); 22 | Assert.That(roll, Is.InRange(4, 24)); 23 | } 24 | 25 | [Test] 26 | [Repeat(100)] 27 | public void CustomRoll() 28 | { 29 | var roll = dice.Roll(92).d(66).AsSum(); 30 | Assert.That(roll, Is.InRange(92, 92 * 66)); 31 | } 32 | 33 | [Test] 34 | [Repeat(100)] 35 | public void ExpressionRoll() 36 | { 37 | var roll = dice.Roll("5+3d4*2").AsSum(); 38 | Assert.That(roll, Is.InRange(11, 29)); 39 | } 40 | 41 | [Test] 42 | [Repeat(100)] 43 | public void ChainedRolls() 44 | { 45 | var roll = dice.Roll().d2().d(5).Keeping(1).d6().AsSum(); 46 | Assert.That(roll, Is.InRange(1, 30)); 47 | } 48 | 49 | [Test] 50 | [Repeat(100)] 51 | public void IndividualRolls() 52 | { 53 | var rolls = dice.Roll(4).d6().AsIndividualRolls(); 54 | Assert.That(rolls, Has.All.InRange(1, 6)); 55 | } 56 | 57 | [Test] 58 | [Repeat(100)] 59 | public void ParsedRolls() 60 | { 61 | var rolls = dice.Roll("5+3d4*2").AsIndividualRolls(); 62 | Assert.That(rolls, Has.All.InRange(11, 29)); 63 | Assert.That(rolls.Count(), Is.EqualTo(1)); 64 | } 65 | 66 | [Test] 67 | [Repeat(100)] 68 | public void KeptRolls() 69 | { 70 | var rolls = dice.Roll(4).d6().Keeping(3).AsIndividualRolls(); 71 | Assert.That(rolls, Has.All.InRange(1, 6)); 72 | Assert.That(rolls.Count(), Is.EqualTo(3)); 73 | } 74 | 75 | [Test] 76 | [Repeat(100)] 77 | public void ExpressionKeptRolls() 78 | { 79 | var roll = dice.Roll("3d4k2").AsSum(); 80 | Assert.That(roll, Is.InRange(2, 8)); 81 | } 82 | 83 | [Test] 84 | public void AverageRoll() 85 | { 86 | var roll = dice.Roll(4).d6().AsPotentialAverage(); 87 | Assert.That(roll, Is.EqualTo(14)); 88 | } 89 | 90 | [Test] 91 | public void ExpressionAverageRoll() 92 | { 93 | var roll = dice.Roll("5+3d4*3").AsPotentialAverage(); 94 | Assert.That(roll, Is.EqualTo(27.5)); 95 | } 96 | 97 | [Test] 98 | public void MinimumRoll() 99 | { 100 | var roll = dice.Roll(4).d6().AsPotentialMinimum(); 101 | Assert.That(roll, Is.EqualTo(4)); 102 | } 103 | 104 | [Test] 105 | public void ExpressionMinimumRoll() 106 | { 107 | var roll = dice.Roll("5+3d4*3").AsPotentialMinimum(); 108 | Assert.That(roll, Is.EqualTo(14)); 109 | } 110 | 111 | [Test] 112 | public void MaximumRoll() 113 | { 114 | var roll = dice.Roll(4).d6().AsPotentialMaximum(); 115 | Assert.That(roll, Is.EqualTo(24)); 116 | } 117 | 118 | [Test] 119 | public void ExpressionMaximumRoll() 120 | { 121 | var roll = dice.Roll("5+3d4*3").AsPotentialMaximum(); 122 | Assert.That(roll, Is.EqualTo(41)); 123 | } 124 | 125 | [Test] 126 | [Repeat(100)] 127 | public void Success() 128 | { 129 | var roll = dice.Roll().Percentile().AsTrueOrFalse(); 130 | Assert.That(roll, Is.True.Or.False); 131 | } 132 | 133 | [Test] 134 | [Repeat(100)] 135 | public void CustomPercentageSuccess() 136 | { 137 | var roll = dice.Roll().Percentile().AsTrueOrFalse(.9); 138 | Assert.That(roll, Is.True.Or.False); 139 | } 140 | 141 | [Test] 142 | [Repeat(100)] 143 | public void CustomRollSuccess() 144 | { 145 | var roll = dice.Roll().Percentile().AsTrueOrFalse(90); 146 | Assert.That(roll, Is.True.Or.False); 147 | } 148 | 149 | [Test] 150 | [Repeat(100)] 151 | public void ExpressionSuccess() 152 | { 153 | var roll = dice.Roll("5+3d4*2").AsTrueOrFalse(); 154 | Assert.That(roll, Is.True.Or.False); 155 | } 156 | 157 | [Test] 158 | [Repeat(100)] 159 | public void ExplicitExpressionSuccess() 160 | { 161 | var roll = dice.Roll("2d6>=1d12").AsTrueOrFalse(); 162 | Assert.That(roll, Is.True.Or.False); 163 | } 164 | 165 | [Test] 166 | public void ContainsRoll() 167 | { 168 | var containsRoll = dice.ContainsRoll("This contains a roll of 4d6k3 for rolling stats"); 169 | Assert.That(containsRoll, Is.True); 170 | } 171 | 172 | [Test] 173 | [Repeat(100)] 174 | public void SummedSentence() 175 | { 176 | var summedSentence = dice.ReplaceRollsWithSumExpression("This contains a roll of 4d6k3 for rolling stats"); 177 | Assert.That(summedSentence, Does.Match(@"This contains a roll of \([1-6] \+ [1-6] \+ [1-6]\) for rolling stats")); 178 | } 179 | 180 | [Test] 181 | [Repeat(100)] 182 | public void RolledSentence() 183 | { 184 | var rolledSentence = dice.ReplaceExpressionWithTotal("This contains a roll of 4d6k3 for rolling stats"); 185 | Assert.That(rolledSentence, Does.Match(@"This contains a roll of ([3-9]|1[0-8]) for rolling stats")); 186 | } 187 | 188 | [Test] 189 | [Repeat(100)] 190 | public void RolledComplexSentence_Minimum() 191 | { 192 | var rolledComplexSentence = dice.ReplaceWrappedExpressions("Fireball does {min(4d6,10) + 0.5} damage"); 193 | Assert.That(rolledComplexSentence, Does.Match(@"Fireball does ([4-9]|10).5 damage")); 194 | } 195 | 196 | [Test] 197 | [Repeat(100)] 198 | public void RolledComplexSentence_Maximum() 199 | { 200 | var rolledComplexSentence = dice.ReplaceWrappedExpressions("Fireball does {max(4d6,10) + 0.5} damage"); 201 | Assert.That(rolledComplexSentence, Does.Match(@"Fireball does (1[0-9]|2[0-4]).5 damage")); 202 | } 203 | 204 | [Test] 205 | public void OptimizedRollWithFewestDice() 206 | { 207 | var roll = RollHelper.GetRollWithFewestDice(1, 9); 208 | Assert.That(roll, Is.EqualTo("4d3-3")); 209 | } 210 | 211 | [Test] 212 | public void OptimizedRoll() 213 | { 214 | var roll = RollHelper.GetRollWithMostEvenDistribution(4, 9); 215 | Assert.That(roll, Is.EqualTo("1d6+3")); 216 | } 217 | 218 | [Test] 219 | public void OptimizedRollWithMultipleDice() 220 | { 221 | var roll = RollHelper.GetRollWithMostEvenDistribution(1, 9); 222 | Assert.That(roll, Is.EqualTo("1d8+1d2-1")); 223 | } 224 | 225 | [Test] 226 | public void OptimizedRollWithMultipliers() 227 | { 228 | var roll = RollHelper.GetRollWithMostEvenDistribution(1, 36, true); 229 | Assert.That(roll, Is.EqualTo("(1d12-1)*3+1d3")); 230 | } 231 | 232 | [Test] 233 | public void OptimizedRollWithNonstandardDice() 234 | { 235 | var roll = RollHelper.GetRollWithMostEvenDistribution(1, 36, false, true); 236 | Assert.That(roll, Is.EqualTo("1d36")); 237 | } 238 | 239 | [Test] 240 | public void OptimizedRollWithMultipliersAndNonstandardDice() 241 | { 242 | var roll = RollHelper.GetRollWithMostEvenDistribution(1, 45, true, true); 243 | Assert.That(roll, Is.EqualTo("(1d3-1)*15+(1d3-1)*5+1d5")); 244 | } 245 | 246 | [Test] 247 | [Repeat(100)] 248 | public void ExplodedRolls() 249 | { 250 | var explodedRolls = dice.Roll(4).d6().Explode().AsIndividualRolls(); 251 | Assert.That(explodedRolls, Has.All.InRange(1, 6)); 252 | Assert.That(explodedRolls.Count(), Is.AtLeast(4).And.EqualTo(4 + explodedRolls.Count(r => r == 6))); 253 | } 254 | 255 | [Test] 256 | [Repeat(100)] 257 | public void ExpressionExplodedRolls() 258 | { 259 | var expressionExplodedRolls = dice.Roll("3d4!").AsSum(); 260 | Assert.That(expressionExplodedRolls, Is.InRange(3, 52)); //max+10*die 261 | } 262 | 263 | [Test] 264 | [Repeat(100)] 265 | public void ExpressionExplodedKeptRolls() 266 | { 267 | var expressionExplodedRolls = dice.Roll("3d4!k2").AsSum(); 268 | Assert.That(expressionExplodedRolls, Is.InRange(2, 8)); 269 | } 270 | 271 | [Test] 272 | [Repeat(100)] 273 | public void ExpressionExplodedMultipleRolls() 274 | { 275 | var expressionExplodedMultipleRolls = dice.Roll("3d4!e3").AsSum(); 276 | Assert.That(expressionExplodedMultipleRolls, Is.InRange(3, 82)); //max+10*die 277 | } 278 | 279 | [Test] 280 | [Repeat(100)] 281 | public void ExpressionExplodedMultipleKeptRolls() 282 | { 283 | var expressionExplodedMultipleKeptRolls = dice.Roll("3d4e1e2k2").AsSum(); 284 | Assert.That(expressionExplodedMultipleKeptRolls, Is.InRange(6, 8)); 285 | } 286 | 287 | [Test] 288 | [Repeat(100)] 289 | public void TransformedRolls() 290 | { 291 | var transformedRolls = dice.Roll(3).d6().Transforming(1).AsIndividualRolls(); 292 | Assert.That(transformedRolls, Has.All.InRange(2, 6)); 293 | Assert.That(transformedRolls.Count(), Is.EqualTo(3)); 294 | } 295 | 296 | [Test] 297 | [Repeat(100)] 298 | public void TransformedMultipleRolls() 299 | { 300 | var transformedMultipleRolls = dice.Roll("3d6t1t5").AsSum(); 301 | Assert.That(transformedMultipleRolls, Is.InRange(6, 18)); 302 | } 303 | 304 | [Test] 305 | [Repeat(100)] 306 | public void TransformedExplodedKeptRolls() 307 | { 308 | var transformedExplodedKeptRolls = dice.Roll("3d6!t1k2").AsSum(); 309 | Assert.That(transformedExplodedKeptRolls, Is.InRange(4, 12)); 310 | } 311 | 312 | [Test] 313 | [Repeat(100)] 314 | public void TransformedCustomRolls() 315 | { 316 | var transformedCustomRolls = dice.Roll("3d6t1:2").AsSum(); 317 | Assert.That(transformedCustomRolls, Is.InRange(6, 18)); 318 | } 319 | 320 | [TestCase(9, "Bad")] 321 | [TestCase(13, "Good")] 322 | public void DescribeRoll(int roll, string expectedDescription) 323 | { 324 | var description = dice.Describe("3d6", roll); 325 | Assert.That(description, Is.EqualTo(expectedDescription)); 326 | } 327 | 328 | [TestCase(6, "Bad")] 329 | [TestCase(12, "Average")] 330 | [TestCase(16, "Good")] 331 | public void DescribeRoll_CustomDescriptions(int roll, string expectedDescription) 332 | { 333 | var description = dice.Describe("3d6", roll, "Bad", "Average", "Good"); 334 | Assert.That(description, Is.EqualTo(expectedDescription)); 335 | } 336 | 337 | [Test] 338 | [Repeat(100)] 339 | public void SingleIndividualRollForExpression() 340 | { 341 | var roll = dice.Roll("1d2+3d4").AsIndividualRolls(); 342 | Assert.That(roll, Has.All.InRange(4, 14)); 343 | Assert.That(roll.Count(), Is.EqualTo(1)); 344 | } 345 | 346 | [Test] 347 | [Repeat(100)] 348 | public void RolledSentence_Strict() 349 | { 350 | var rolledSentence = dice.ReplaceExpressionWithTotal("1d2 ghouls and 2d4 zombies"); 351 | Assert.That(rolledSentence, Does.Match(@"[1-2] ghouls and [2-8] zombies")); 352 | } 353 | 354 | [Test] 355 | [Repeat(100)] 356 | public void RolledSentence_Lenient() 357 | { 358 | var rolledSentence = dice.ReplaceExpressionWithTotal("1d2 ghouls and 2d4 zombies", true); 359 | Assert.That(rolledSentence, Does.Match(@"[1-2] ghouls an[1-8] zombies")); 360 | } 361 | 362 | [TestCase("4d3!t2k1")] 363 | [TestCase("4d3!k1t2")] 364 | [TestCase("4d3t2!k1")] 365 | [TestCase("4d3t2k1!")] 366 | [TestCase("4d3k1!t2")] 367 | [TestCase("4d3k1t2!")] 368 | [Repeat(100)] 369 | public void OrderOfOperations(string rollExpression) 370 | { 371 | var roll = dice.Roll(rollExpression).AsSum(); 372 | Assert.That(roll, Is.InRange(1, 4)); 373 | } 374 | 375 | [Test] 376 | [Repeat(100)] 377 | public void KeptRolls_AsSum() 378 | { 379 | var rolls = dice.Roll(4).d6().Keeping(3).AsSum(); 380 | Assert.That(rolls, Is.InRange(3, 18)); 381 | } 382 | 383 | [TestCase("3d6+1", true)] 384 | [TestCase("(3)d(6)+1", true)] 385 | [TestCase("(1d2)d(3d4)+5d6", true)] 386 | [TestCase("10000d10000", true)] 387 | [TestCase("(100d100)d(100d100)", true)] 388 | [TestCase("(100d100d2)d(100d100)", false)] 389 | [TestCase("(100d100)d(100d100d2)", false)] 390 | [TestCase("(0)d(6)+1", false)] 391 | [TestCase("(3.1)d(6)+1", false)] 392 | [TestCase("0d6+1", false)] 393 | [TestCase("10001d10000", false)] 394 | [TestCase("(100d100+1)d(100d100)", false)] 395 | [TestCase("(3)d(-6)+1", false)] 396 | [TestCase("(3)d(6.1)+1", false)] 397 | [TestCase("3d0+1", false)] 398 | [TestCase("10000d10001", false)] 399 | [TestCase("(100d100)d(100d100+1)", false)] 400 | [TestCase("4d6k3", true)] 401 | [TestCase("(4)d(6)k(3)", true)] 402 | [TestCase("(4)d(6)k(-1)", false)] 403 | [TestCase("(4)d(6)k(3.1)", false)] 404 | [TestCase("4d6k10000", true)] 405 | [TestCase("4d6k10001", false)] 406 | [TestCase("4d6k(100d100+1)", false)] 407 | [TestCase("3d6t1", true)] 408 | [TestCase("3d6t1t2", true)] 409 | [TestCase("3d6t7", true)] 410 | [TestCase("3d6t0", false)] 411 | [TestCase("3d6t6:0", true)] 412 | [TestCase("3d6t10001", false)] 413 | [TestCase("3d6t6:10001", true)] 414 | [TestCase("avg(1d12, 2d6, 3d4, 4d3, 6d2)", true)] 415 | [TestCase("bad(1d12, 2d6, 3d4, 4d3, 6d2)", false)] 416 | [TestCase("this is not a roll", false)] 417 | [TestCase("this contains 3d6, but is not a roll", false)] 418 | [TestCase("9266+90210-42*600/1337%1336+96d(783d82%45+922-2022/337)-min(1d2, 3d4, 5d6)+max(1d2, 3d4, 5d6)*avg(1d2, 3d4, 5d6)", true)] 419 | public void IsValid(string rollExpression, bool expected) 420 | { 421 | var actual = dice.IsValid(rollExpression); 422 | Assert.That(actual, Is.EqualTo(expected)); 423 | } 424 | } 425 | } 426 | -------------------------------------------------------------------------------- /DnDGen.RollGen.Tests.Integration/RollHelperTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using System; 3 | using System.Diagnostics; 4 | using System.Linq; 5 | 6 | namespace DnDGen.RollGen.Tests.Integration 7 | { 8 | [TestFixture] 9 | public class RollHelperTests : IntegrationTests 10 | { 11 | private Dice dice; 12 | private Stopwatch stopwatch; 13 | 14 | [SetUp] 15 | public void Setup() 16 | { 17 | dice = GetNewInstanceOf(); 18 | stopwatch = new Stopwatch(); 19 | } 20 | 21 | [TestCase(0, 0, "0")] 22 | [TestCase(1, 1, "1")] 23 | [TestCase(2, 2, "2")] 24 | [TestCase(9266, 9266, "9266")] 25 | [TestCase(0, 1, "1d2-1")] 26 | [TestCase(1, 2, "1d2")] 27 | [TestCase(1, 3, "1d3")] 28 | [TestCase(1, 4, "1d4")] 29 | [TestCase(1, 5, "2d3-1")] 30 | [TestCase(1, 6, "1d6")] 31 | [TestCase(1, 7, "2d4-1")] 32 | [TestCase(1, 8, "1d8")] 33 | [TestCase(1, 9, "4d3-3")] 34 | [TestCase(1, 10, "1d10")] 35 | [TestCase(1, 12, "1d12")] 36 | [TestCase(1, 20, "1d20")] 37 | [TestCase(1, 35, "17d3-16")] 38 | [TestCase(1, 36, "5d8-4")] 39 | [TestCase(1, 48, "47d2-46")] 40 | [TestCase(1, 100, "1d100")] 41 | [TestCase(1, 1_000, "111d10-110")] 42 | [TestCase(1, 10_000, "101d100-100")] 43 | [TestCase(1, 100_000, "1010d100+1d10-1010")] 44 | [TestCase(1, 1_000_000, "10000d100+101d100-10100")] 45 | [TestCase(2, 8, "2d4")] 46 | [TestCase(4, 9, "1d6+3")] 47 | [TestCase(5, 40, "5d8")] 48 | [TestCase(10, 1_000, "10d100")] 49 | [TestCase(10, 10_000, "1110d10-1100")] 50 | [TestCase(16, 50, "17d3-1")] 51 | [TestCase(100, 10_000, "100d100")] 52 | [TestCase(101, 200, "1d100+100")] 53 | [TestCase(437, 1204, "767d2-330")] 54 | [TestCase(1336, 90210, "897d100+71d2+368")] 55 | [TestCase(2714, 8095, "5381d2-2667")] 56 | [TestCase(10_000, 1_000_000, "10000d100")] 57 | public void RollWithFewestDice(int lower, int upper, string expectedRoll) 58 | { 59 | stopwatch.Start(); 60 | var roll = RollHelper.GetRollWithFewestDice(lower, upper); 61 | stopwatch.Stop(); 62 | 63 | Assert.That(roll, Is.EqualTo(expectedRoll)); 64 | Assert.That(dice.Roll(roll).IsValid(), Is.True); 65 | Assert.That(dice.Roll(roll).AsPotentialMinimum(), Is.EqualTo(lower)); 66 | Assert.That(dice.Roll(roll).AsPotentialMaximum(), Is.EqualTo(upper)); 67 | Assert.That(stopwatch.Elapsed, Is.LessThan(TimeSpan.FromSeconds(1))); 68 | } 69 | 70 | [TestCase(133_851, 2_126_438_624, "REPEAT+7825d100+14d8-21343988", "10000d100", 2147)] 71 | [TestCase(20_133_851, 2_146_438_624, "REPEAT+7825d100+14d8-1343988", "10000d100", 2147)] 72 | [TestCase(73_422_562, 1_673_270_503, "REPEAT+80d100+3d8+57262479", "10000d100", 1616)] 73 | [TestCase(239_762_129, 792_745_843, "REPEAT+5694d100+4d3+234176431", "10000d100", 558)] 74 | [TestCase(524_600_879, 1_213_158_805, "REPEAT+5130d100+8d8+517645741", "10000d100", 695)] 75 | public void RollWithFewestDice_LongRoll(int lower, int upper, string rollTemplate, string repeatTerm, int repeatCount) 76 | { 77 | var repeats = Enumerable.Repeat(repeatTerm, repeatCount); 78 | var expectedRoll = rollTemplate.Replace("REPEAT", string.Join("+", repeats)); 79 | 80 | stopwatch.Start(); 81 | var roll = RollHelper.GetRollWithFewestDice(lower, upper); 82 | stopwatch.Stop(); 83 | 84 | Assert.That(roll, Has.Length.EqualTo(expectedRoll.Length)); 85 | Assert.That(roll, Is.EqualTo(expectedRoll)); 86 | Assert.That(dice.Roll(roll).IsValid(), Is.True); 87 | Assert.That(dice.Roll(roll).AsPotentialMinimum(), Is.EqualTo(lower)); 88 | Assert.That(dice.Roll(roll).AsPotentialMaximum(), Is.EqualTo(upper)); 89 | Assert.That(stopwatch.Elapsed, Is.LessThan(TimeSpan.FromSeconds(1))); 90 | } 91 | 92 | [TestCase(0, 0, "0")] 93 | [TestCase(1, 1, "1")] 94 | [TestCase(2, 2, "2")] 95 | [TestCase(9266, 9266, "9266")] 96 | [TestCase(0, 1, "1d2-1")] 97 | [TestCase(1, 2, "1d2")] 98 | [TestCase(1, 3, "1d3")] 99 | [TestCase(1, 4, "1d4")] 100 | [TestCase(1, 5, "1d4+1d2-1")] 101 | [TestCase(1, 6, "1d6")] 102 | [TestCase(1, 7, "1d6+1d2-1")] 103 | [TestCase(1, 8, "1d8")] 104 | [TestCase(1, 10, "1d10")] 105 | [TestCase(1, 12, "1d12")] 106 | [TestCase(1, 20, "1d20")] 107 | [TestCase(1, 35, "1d20+1d12+1d4+1d2-3")] 108 | [TestCase(1, 36, "1d20+1d12+1d6-2")] 109 | [TestCase(1, 48, "2d20+1d10-2")] 110 | [TestCase(1, 100, "1d100")] 111 | [TestCase(1, 1_000, "10d100+1d10-10")] 112 | [TestCase(1, 10_000, "101d100-100")] 113 | [TestCase(1, 100_000, "1010d100+1d10-1010")] 114 | [TestCase(1, 1_000_000, "10000d100+101d100-10100")] 115 | [TestCase(1, 9, "1d8+1d2-1")] 116 | [TestCase(2, 8, "1d6+1d2")] 117 | [TestCase(4, 9, "1d6+3")] 118 | [TestCase(5, 40, "1d20+1d12+1d6+2")] 119 | [TestCase(10, 1_000, "10d100")] 120 | [TestCase(10, 10_000, "100d100+4d20+1d12+1d4-96")] 121 | [TestCase(16, 50, "1d20+1d12+1d4+1d2+12")] 122 | [TestCase(100, 10_000, "100d100")] 123 | [TestCase(101, 200, "1d100+100")] 124 | [TestCase(437, 1204, "7d100+3d20+1d12+1d6+1d2+424")] 125 | [TestCase(1336, 90210, "897d100+3d20+1d12+1d4+434")] 126 | [TestCase(2714, 8095, "54d100+1d20+1d12+1d6+2657")] 127 | [TestCase(10_000, 1_000_000, "10000d100")] 128 | public void RollWithMostEvenDistribution(int lower, int upper, string expectedRoll) 129 | { 130 | stopwatch.Start(); 131 | var roll = RollHelper.GetRollWithMostEvenDistribution(lower, upper); 132 | stopwatch.Stop(); 133 | 134 | Assert.That(roll, Is.EqualTo(expectedRoll)); 135 | Assert.That(dice.Roll(roll).IsValid(), Is.True); 136 | Assert.That(dice.Roll(roll).AsPotentialMinimum(), Is.EqualTo(lower)); 137 | Assert.That(dice.Roll(roll).AsPotentialMaximum(), Is.EqualTo(upper)); 138 | Assert.That(stopwatch.Elapsed, Is.LessThan(TimeSpan.FromSeconds(1))); 139 | } 140 | 141 | [TestCase(73422562, 1673270503, "REPEAT+80d100+1d20+1d3+57262480", "10000d100", 1616)] 142 | [TestCase(239762129, 792745843, "REPEAT+5694d100+1d8+1d2+234176433", "10000d100", 558)] 143 | [TestCase(524600879, 1213158805, "REPEAT+5130d100+2d20+1d12+1d8+517645745", "10000d100", 695)] 144 | public void RollWithMostEvenDistribution_LongRoll(int lower, int upper, string rollTemplate, string repeatTerm, int repeatCount) 145 | { 146 | var repeats = Enumerable.Repeat(repeatTerm, repeatCount); 147 | var expectedRoll = rollTemplate.Replace("REPEAT", string.Join("+", repeats)); 148 | 149 | stopwatch.Start(); 150 | var roll = RollHelper.GetRollWithMostEvenDistribution(lower, upper); 151 | stopwatch.Stop(); 152 | 153 | Assert.That(roll, Has.Length.EqualTo(expectedRoll.Length)); 154 | Assert.That(roll, Is.EqualTo(expectedRoll)); 155 | Assert.That(dice.Roll(roll).IsValid(), Is.True); 156 | Assert.That(dice.Roll(roll).AsPotentialMinimum(), Is.EqualTo(lower)); 157 | Assert.That(dice.Roll(roll).AsPotentialMaximum(), Is.EqualTo(upper)); 158 | Assert.That(stopwatch.Elapsed, Is.LessThan(TimeSpan.FromSeconds(1))); 159 | } 160 | 161 | [TestCase(0, 0, "0")] 162 | [TestCase(1, 1, "1")] 163 | [TestCase(2, 2, "2")] 164 | [TestCase(9266, 9266, "9266")] 165 | [TestCase(0, 1, "1d2-1")] 166 | [TestCase(1, 2, "1d2")] 167 | [TestCase(1, 3, "1d3")] 168 | [TestCase(1, 4, "1d4")] 169 | [TestCase(1, 5, "1d5")] 170 | [TestCase(1, 6, "1d6")] 171 | [TestCase(1, 7, "1d7")] 172 | [TestCase(1, 8, "1d8")] 173 | [TestCase(1, 9, "1d9")] 174 | [TestCase(1, 10, "1d10")] 175 | [TestCase(1, 12, "1d12")] 176 | [TestCase(1, 20, "1d20")] 177 | [TestCase(1, 35, "1d35")] 178 | [TestCase(1, 36, "1d36")] 179 | [TestCase(1, 48, "1d48")] 180 | [TestCase(1, 100, "1d100")] 181 | [TestCase(1, 1_000, "1d1000")] 182 | [TestCase(1, 10_000, "1d10000")] 183 | [TestCase(1, 100_000, "10d10000+1d10-10")] 184 | [TestCase(1, 1_000_000, "100d10000+1d100-100")] 185 | [TestCase(2, 8, "1d7+1")] 186 | [TestCase(4, 9, "1d6+3")] 187 | [TestCase(5, 40, "1d36+4")] 188 | [TestCase(10, 1_000, "1d991+9")] 189 | [TestCase(10, 10_000, "1d9991+9")] 190 | [TestCase(16, 50, "1d35+15")] 191 | [TestCase(100, 10_000, "1d9901+99")] 192 | [TestCase(101, 200, "1d100+100")] 193 | [TestCase(437, 1204, "1d768+436")] 194 | [TestCase(1336, 90210, "8d10000+1d8883+1327")] 195 | [TestCase(2714, 8095, "1d5382+2713")] 196 | [TestCase(10_000, 1_000_000, "99d10000+1d100+9900")] 197 | public void RollWithMostEvenDistribution_AllowNonstandard(int lower, int upper, string expectedRoll) 198 | { 199 | stopwatch.Start(); 200 | var roll = RollHelper.GetRollWithMostEvenDistribution(lower, upper, allowNonstandardDice: true); 201 | stopwatch.Stop(); 202 | 203 | Assert.That(roll, Is.EqualTo(expectedRoll)); 204 | Assert.That(dice.Roll(roll).IsValid(), Is.True); 205 | Assert.That(dice.Roll(roll).AsPotentialMinimum(), Is.EqualTo(lower)); 206 | Assert.That(dice.Roll(roll).AsPotentialMaximum(), Is.EqualTo(upper)); 207 | Assert.That(stopwatch.Elapsed, Is.LessThan(TimeSpan.FromSeconds(1))); 208 | } 209 | 210 | [TestCase(73422562, 1673270503, "REPEAT+1d7942+73262561", "10000d10000", 16)] 211 | [TestCase(239762129, 792745843, "REPEAT+5303d10000+1d9018+239706825", "10000d10000", 5)] 212 | [TestCase(524600879, 1213158805, "REPEAT+8862d10000+1d6789+524532016", "10000d10000", 6)] 213 | public void RollWithMostEvenDistribution_AllowNonstandard_LongRoll(int lower, int upper, string rollTemplate, string repeatTerm, int repeatCount) 214 | { 215 | var repeats = Enumerable.Repeat(repeatTerm, repeatCount); 216 | var expectedRoll = rollTemplate.Replace("REPEAT", string.Join("+", repeats)); 217 | 218 | stopwatch.Start(); 219 | var roll = RollHelper.GetRollWithMostEvenDistribution(lower, upper, allowNonstandardDice: true); 220 | stopwatch.Stop(); 221 | 222 | Assert.That(roll, Has.Length.EqualTo(expectedRoll.Length)); 223 | Assert.That(roll, Is.EqualTo(expectedRoll)); 224 | Assert.That(dice.Roll(roll).IsValid(), Is.True); 225 | Assert.That(dice.Roll(roll).AsPotentialMinimum(), Is.EqualTo(lower)); 226 | Assert.That(dice.Roll(roll).AsPotentialMaximum(), Is.EqualTo(upper)); 227 | Assert.That(stopwatch.Elapsed, Is.LessThan(TimeSpan.FromSeconds(1))); 228 | } 229 | 230 | [TestCase(0, 0, "0")] 231 | [TestCase(1, 1, "1")] 232 | [TestCase(2, 2, "2")] 233 | [TestCase(9266, 9266, "9266")] 234 | [TestCase(0, 1, "1d2-1")] 235 | [TestCase(1, 2, "1d2")] 236 | [TestCase(1, 3, "1d3")] 237 | [TestCase(1, 4, "1d4")] 238 | [TestCase(1, 5, "1d4+1d2-1")] 239 | [TestCase(1, 6, "1d6")] 240 | [TestCase(1, 7, "1d6+1d2-1")] 241 | [TestCase(1, 8, "1d8")] 242 | [TestCase(1, 9, "(1d3-1)*3+1d3")] 243 | [TestCase(1, 10, "1d10")] 244 | [TestCase(1, 12, "1d12")] 245 | [TestCase(1, 20, "1d20")] 246 | [TestCase(1, 35, "1d20+1d12+1d4+1d2-3")] 247 | [TestCase(1, 36, "(1d12-1)*3+1d3")] 248 | [TestCase(1, 48, "(1d12-1)*4+1d4")] 249 | [TestCase(1, 100, "1d100")] 250 | [TestCase(1, 1_000, "(1d100-1)*10+1d10")] 251 | [TestCase(1, 10_000, "(1d100-1)*100+1d100")] 252 | [TestCase(1, 100_000, "(1d100-1)*1000+(1d100-1)*10+1d10")] 253 | [TestCase(1, 1_000_000, "(1d100-1)*10000+(1d100-1)*100+1d100")] 254 | [TestCase(2, 8, "1d6+1d2")] 255 | [TestCase(4, 9, "1d6+3")] 256 | [TestCase(5, 40, "(1d12-1)*3+1d3+4")] 257 | [TestCase(10, 1_000, "10d100")] 258 | [TestCase(10, 10_000, "100d100+4d20+1d12+1d4-96")] 259 | [TestCase(16, 50, "1d20+1d12+1d4+1d2+12")] 260 | [TestCase(100, 10_000, "100d100")] 261 | [TestCase(101, 200, "1d100+100")] 262 | [TestCase(437, 1204, "(1d12-1)*64+(1d8-1)*8+1d8+436")] 263 | [TestCase(1336, 90210, "(1d3-1)*29625+(1d3-1)*9875+99d100+3d20+1d12+1d6+1232")] 264 | [TestCase(2714, 8095, "(1d6-1)*897+(1d3-1)*299+3d100+1d2+2710")] 265 | [TestCase(10_000, 1_000_000, "10000d100")] 266 | public void RollWithMostEvenDistribution_AllowMultipliers(int lower, int upper, string expectedRoll) 267 | { 268 | stopwatch.Start(); 269 | var roll = RollHelper.GetRollWithMostEvenDistribution(lower, upper, true); 270 | stopwatch.Stop(); 271 | 272 | Assert.That(roll, Is.EqualTo(expectedRoll)); 273 | Assert.That(dice.Roll(roll).IsValid(), Is.True); 274 | Assert.That(dice.Roll(roll).AsPotentialMinimum(), Is.EqualTo(lower)); 275 | Assert.That(dice.Roll(roll).AsPotentialMaximum(), Is.EqualTo(upper)); 276 | Assert.That(stopwatch.Elapsed, Is.LessThan(TimeSpan.FromSeconds(1))); 277 | } 278 | 279 | [TestCase(73422562, 1673270503, "(1d2-1)*799923971+REPEAT+40d100+1d10+1d2+65342520", "10000d100", 808)] 280 | [TestCase(239762129, 792745843, "(1d3-1)*184327905+(1d3-1)*61442635+REPEAT+632d100+3d20+1d10+239141493", "10000d100", 62)] 281 | [TestCase(524600879, 1213158805, "(1d3-1)*229519309+REPEAT+8376d100+4d20+1d8+1d2+522282497", "10000d100", 231)] 282 | public void RollWithMostEvenDistribution_AllowMultipliers_LongRoll(int lower, int upper, string rollTemplate, string repeatTerm, int repeatCount) 283 | { 284 | var repeats = Enumerable.Repeat(repeatTerm, repeatCount); 285 | var expectedRoll = rollTemplate.Replace("REPEAT", string.Join("+", repeats)); 286 | 287 | stopwatch.Start(); 288 | var roll = RollHelper.GetRollWithMostEvenDistribution(lower, upper, true); 289 | stopwatch.Stop(); 290 | 291 | Assert.That(roll, Has.Length.EqualTo(expectedRoll.Length)); 292 | Assert.That(roll, Is.EqualTo(expectedRoll)); 293 | Assert.That(dice.Roll(roll).IsValid(), Is.True); 294 | Assert.That(dice.Roll(roll).AsPotentialMinimum(), Is.EqualTo(lower)); 295 | Assert.That(dice.Roll(roll).AsPotentialMaximum(), Is.EqualTo(upper)); 296 | Assert.That(stopwatch.Elapsed, Is.LessThan(TimeSpan.FromSeconds(1))); 297 | } 298 | 299 | [TestCase(0, 0, "0")] 300 | [TestCase(1, 1, "1")] 301 | [TestCase(2, 2, "2")] 302 | [TestCase(9266, 9266, "9266")] 303 | [TestCase(0, 1, "1d2-1")] 304 | [TestCase(1, 2, "1d2")] 305 | [TestCase(1, 3, "1d3")] 306 | [TestCase(1, 4, "1d4")] 307 | [TestCase(1, 5, "1d5")] 308 | [TestCase(1, 6, "1d6")] 309 | [TestCase(1, 7, "1d7")] 310 | [TestCase(1, 8, "1d8")] 311 | [TestCase(1, 9, "(1d3-1)*3+1d3")] 312 | [TestCase(1, 10, "1d10")] 313 | [TestCase(1, 12, "1d12")] 314 | [TestCase(1, 20, "1d20")] 315 | [TestCase(1, 35, "1d35")] 316 | [TestCase(1, 36, "(1d12-1)*3+1d3")] 317 | [TestCase(1, 48, "(1d12-1)*4+1d4")] 318 | [TestCase(1, 100, "1d100")] 319 | [TestCase(1, 1_000, "(1d100-1)*10+1d10")] 320 | [TestCase(1, 10_000, "(1d100-1)*100+1d100")] 321 | [TestCase(1, 100_000, "(1d100-1)*1000+(1d100-1)*10+1d10")] 322 | [TestCase(1, 1_000_000, "(1d100-1)*10000+(1d100-1)*100+1d100")] 323 | [TestCase(2, 8, "1d7+1")] 324 | [TestCase(4, 9, "1d6+3")] 325 | [TestCase(5, 40, "(1d12-1)*3+1d3+4")] 326 | [TestCase(10, 1_000, "1d991+9")] 327 | [TestCase(10, 10_000, "1d9991+9")] 328 | [TestCase(16, 50, "1d35+15")] 329 | [TestCase(100, 10_000, "1d9901+99")] 330 | [TestCase(101, 200, "1d100+100")] 331 | [TestCase(437, 1204, "(1d12-1)*64+(1d8-1)*8+1d8+436")] 332 | [TestCase(1336, 90210, "(1d3-1)*29625+(1d3-1)*9875+1d9875+1335")] 333 | [TestCase(2714, 8095, "(1d6-1)*897+(1d3-1)*299+1d299+2713")] 334 | [TestCase(10_000, 1_000_000, "99d10000+1d100+9900")] 335 | public void RollWithMostEvenDistribution_AllowMultipliersAndNonstandard(int lower, int upper, string expectedRoll) 336 | { 337 | stopwatch.Start(); 338 | var roll = RollHelper.GetRollWithMostEvenDistribution(lower, upper, true, true); 339 | stopwatch.Stop(); 340 | 341 | Assert.That(roll, Is.EqualTo(expectedRoll)); 342 | Assert.That(dice.Roll(roll).IsValid(), Is.True); 343 | Assert.That(dice.Roll(roll).AsPotentialMinimum(), Is.EqualTo(lower)); 344 | Assert.That(dice.Roll(roll).AsPotentialMaximum(), Is.EqualTo(upper)); 345 | Assert.That(stopwatch.Elapsed, Is.LessThan(TimeSpan.FromSeconds(1))); 346 | } 347 | 348 | [TestCase(73422562, 1673270503, "(1d2-1)*799923971+REPEAT+1d3971+73342561", "10000d10000", 8)] 349 | [TestCase(239762129, 792745843, "(1d3-1)*184327905+(1d3-1)*61442635+6144d10000+1d8779+239755984", "REPEAT", 0)] 350 | [TestCase(524600879, 1213158805, "(1d3-1)*229519309+REPEAT+2954d10000+1d2263+524577924", "10000d10000", 2)] 351 | public void RollWithMostEvenDistribution_AllowMultipliersAndNonstandard_LongRoll(int lower, int upper, string rollTemplate, string repeatTerm, int repeatCount) 352 | { 353 | var repeats = Enumerable.Repeat(repeatTerm, repeatCount); 354 | var expectedRoll = rollTemplate.Replace("REPEAT", string.Join("+", repeats)); 355 | 356 | stopwatch.Start(); 357 | var roll = RollHelper.GetRollWithMostEvenDistribution(lower, upper, true, true); 358 | stopwatch.Stop(); 359 | 360 | Assert.That(roll, Has.Length.EqualTo(expectedRoll.Length)); 361 | Assert.That(roll, Is.EqualTo(expectedRoll)); 362 | Assert.That(dice.Roll(roll).IsValid(), Is.True); 363 | Assert.That(dice.Roll(roll).AsPotentialMinimum(), Is.EqualTo(lower)); 364 | Assert.That(dice.Roll(roll).AsPotentialMaximum(), Is.EqualTo(upper)); 365 | Assert.That(stopwatch.Elapsed, Is.LessThan(TimeSpan.FromSeconds(1))); 366 | } 367 | } 368 | } 369 | --------------------------------------------------------------------------------