A sophisticated, high-performance dice expression parser and evaluator for .NET. RollCraft supports complex dice notation with modifiers, mathematical functions, variables, and conditional expressions.
dotnet add package RollCraftusing RollCraft;
// Create an evaluator with random dice rolls
var evaluator = DiceExpressionEvaluator<int>.CreateRandom();
// Evaluate a dice expression
var result = evaluator.Evaluate("2d6 + 5");
result.Switch(
onSuccess: r => Console.WriteLine($"Result: {r.Result}"),
onFailure: e => Console.WriteLine($"Error: {e.Message}")
);- Dice Notation: Standard dice syntax (
NdM) with extensive modifier support - Math Operations: Full arithmetic support with proper operator precedence
- Dice Modifiers: Exploding dice, keep highest/lowest, reroll, min/max clamping
- Math Functions:
floor,ceil,round,min,max,abs,sqrt - Variables: Dynamic variable substitution with
[VariableName]syntax - Conditionals: Ternary-style conditional expressions with
if(condition, trueValue, falseValue) - Generic Numeric Types: Support for
short,int,long,float,double, anddecimalresult types - Functional Error Handling: Uses
Result<TError, TValue>pattern (no exceptions) - Multiple Roller Strategies: Random, seeded random, minimum, maximum, fixed average, or custom
RollCraft parses dice expressions into a DiceExpression<TNumber> AST that can be evaluated multiple times.
using RollCraft;
// Parse returns a Result type
var parseResult = DiceExpressionParser.Parse<int>("2d6 + 3");
parseResult.Switch(
onSuccess: expression => Console.WriteLine("Parsed successfully!"),
onFailure: error => Console.WriteLine($"Parse error: {error.Message}")
);For a more traditional approach, use TryParse:
ParserError? error = DiceExpressionParser.TryParse<int>("2d6 + 3", out var expression);
if (error is null)
{
// Use expression
}
else
{
Console.WriteLine($"Error: {error.Value.Message} at position {error.Value.Position}");
}RollCraft supports multiple generic numeric types:
| Type | Description | Use Case |
|---|---|---|
short |
16-bit integer | Memory-constrained scenarios |
int |
32-bit integer | Standard whole number results |
long |
64-bit integer | Large whole number results |
float |
Single-precision floating-point | Decimal results with lower precision |
double |
Double-precision floating-point | Decimal results with high precision |
decimal |
128-bit decimal | Financial/high-precision calculations |
// Integer results (most common)
var intResult = DiceExpressionParser.Parse<int>("2d6 + 3");
// Double results (useful for division)
var doubleResult = DiceExpressionParser.Parse<double>("2d6 / 2.5");
// Decimal results (high precision)
var decimalResult = DiceExpressionParser.Parse<decimal>("1d100 / 3");
// Long results (large numbers)
var longResult = DiceExpressionParser.Parse<long>("100d1000");Create a DiceExpressionEvaluator<TNumber> to evaluate expressions:
var evaluator = DiceExpressionEvaluator<int>.CreateRandom();
// Evaluate a string directly
var result = evaluator.Evaluate("4d6kh3"); // Roll 4d6, keep highest 3
// Or evaluate a pre-parsed expression (more efficient for repeated evaluations)
var expression = DiceExpressionParser.Parse<int>("4d6kh3");
var result = evaluator.Evaluate(expression.Value);var evaluator = DiceExpressionEvaluator<double>.CreateRandom();
// For pre-parsed expressions, returns EvaluatorError?
EvaluatorError? error = evaluator.TryEvaluate(expression, out var result);
// For string expressions, returns IRollError? (can be ParserError or EvaluatorError)
IRollError? error = evaluator.TryEvaluate("2d6 + 3", out var result);var evaluator = DiceExpressionEvaluator<int>.CreateRandom();
var variables = new Dictionary<string, int>
{
["STR"] = 5,
["Level"] = 10
};
var result = evaluator.Evaluate("1d20 + [STR] + [Level]", variables);Evaluate the same expression multiple times:
var evaluator = DiceExpressionEvaluator<int>.CreateRandom();
var expression = DiceExpressionParser.Parse<int>("1d20").Value;
// Roll 10 times
var results = evaluator.Evaluate(expression, repeatCount: 10);
foreach (var result in results)
{
result.Switch(
onSuccess: r => Console.WriteLine(r.Result),
onFailure: e => Console.WriteLine(e.Message)
);
}| Syntax | Description | Example |
|---|---|---|
NdM |
Roll N dice with M sides | 2d6 (roll 2 six-sided dice) |
dM |
Roll 1 die with M sides | d20 (roll 1 twenty-sided die) |
The number of dice and sides can be expressions:
(2+1)d6 → Roll 3d6
2d(4+2) → Roll 2d6
[DICE]d[SIDES] → Use variables for dice count and sides
| Operator | Description | Precedence |
|---|---|---|
+ |
Addition | Low |
- |
Subtraction | Low |
* |
Multiplication | High |
/ |
Division | High |
% |
Modulo (remainder) | High |
- (unary) |
Negation | Highest |
() |
Grouping | - |
Examples:
2d6 + 5 → Add 5 to the roll
2d6 * 2 → Double the roll
(2d6 + 3) * 2 → Add 3, then double
-1d6 → Negate the roll
10 % 3 → Remainder of 10/3 = 1
1d20 % 2 → 0 if even, 1 if odd
Modifiers are applied to dice expressions in the order they appear.
Dice "explode" (roll again and add) when they hit the maximum value or a specified condition.
| Syntax | Description |
|---|---|
4d6! |
Explode on maximum (6) |
4d6!=5 |
Explode on exactly 5 |
4d6!>4 |
Explode on greater than 4 |
4d6!>=4 |
Explode on 4 or greater |
4d6!<3 |
Explode on less than 3 |
4d6!<=3 |
Explode on 3 or less |
4d6!<>1 |
Explode on anything except 1 |
Keep only some of the rolled dice.
| Syntax | Description |
|---|---|
4d6k3 |
Keep highest 3 (same as kh) |
4d6kh3 |
Keep highest 3 |
4d6kl3 |
Keep lowest 3 |
Reroll dice that meet a condition.
| Syntax | Description |
|---|---|
4d6r |
Reroll 1s (indefinitely) |
4d6r=1 |
Reroll 1s (indefinitely) |
4d6r<3 |
Reroll 1s and 2s (indefinitely) |
4d6ro |
Reroll 1s (once only) |
4d6ro<=2 |
Reroll 1s and 2s (once only) |
Clamp individual die results to a minimum or maximum value.
| Syntax | Description |
|---|---|
4d6min2 |
Treat any roll below 2 as 2 |
4d6max5 |
Treat any roll above 5 as 5 |
4d6min2max5 |
Clamp rolls between 2 and 5 |
Modifiers can be combined:
4d6kh3 → Roll 4d6, keep highest 3
4d6!kh3 → Roll 4d6 exploding, keep highest 3
4d6r<3kh3 → Roll 4d6, reroll <3, keep highest 3
4d6min2max5!k3 → Complex modifier chain
All function names are case-insensitive.
| Function | Description | Example |
|---|---|---|
floor(x) |
Round down | floor(3.7) → 3 |
ceil(x) |
Round up | ceil(3.2) → 4 |
round(x) |
Round to nearest (banker's rounding) | round(3.5) → 4 |
abs(x) |
Absolute value | abs(-5) → 5 |
sqrt(x) |
Square root | sqrt(16) → 4 |
min(a, b, ...) |
Minimum value (2+ args) | min(1d6, 1d8, 1d10) |
max(a, b, ...) |
Maximum value (2+ args) | max(1d6, 5) |
Functions can be nested and combined with expressions:
floor(2d6 / 2) → Roll 2d6, divide by 2, round down
max(1d6, 1d8) → Roll both, take higher
min(1d20 + 5, 20) → Cap result at 20
sqrt(abs(-16)) → Nested functions
Note:
min()andmax()as functions require parentheses and commas. The modifier syntax (1d6min3) is different and clamps individual die values.
Variables use bracket syntax [VariableName] and are case-insensitive.
var variables = new Dictionary<string, int>
{
["STR"] = 18,
["ProfBonus"] = 3
};
// All of these work (case-insensitive):
// [STR], [str], [Str], [sTR]
evaluator.Evaluate("1d20 + [STR] + [ProfBonus]", variables);Variables can be used anywhere a number is expected:
[DICE]d[SIDES] → Variable dice count and sides
1d20 + [Modifier] → Variable modifier
if([HP] <= 0, 0, [HP]) → Variables in conditionals
Use if(condition, trueValue, falseValue) for conditional logic.
Syntax: if(left OPERATOR right, valueIfTrue, valueIfFalse)
Comparison Operators:
| Operator | Description |
|---|---|
= |
Equal |
<> |
Not equal |
> |
Greater than |
>= |
Greater than or equal |
< |
Less than |
<= |
Less than or equal |
Examples:
if(1d20 >= 10, 2d6, 1d6) → Critical hit logic
if([HP] <= 0, 0, [HP]) → Clamp HP to minimum 0
if(1d20 = 20, 2 * 2d6, 2d6) → Double damage on nat 20
if(1d6 > 1d6, 1, 0) → Compare two rolls
Conditionals can be nested:
if([Level] >= 5, if([Level] >= 11, 3d6, 2d6), 1d6) → Scaling damage
Create evaluators with different rolling strategies:
// Random rolls (default for games)
var random = DiceExpressionEvaluator<int>.CreateRandom();
// Seeded random (reproducible results)
var seeded = DiceExpressionEvaluator<int>.CreateSeededRandom(42);
// Always roll minimum (1)
var minimum = DiceExpressionEvaluator<int>.CreateMinimum();
// Always roll maximum
var maximum = DiceExpressionEvaluator<int>.CreateMaximum();
// Fixed average ((min + max) / 2, rounded down)
var average = DiceExpressionEvaluator<int>.CreateFixedAverage();Implement IRoller for custom rolling behavior:
public class LoadedDiceRoller : IRoller
{
public int RollDice(int dieSize)
{
// Always roll high!
return dieSize - 1 + Random.Shared.Next(1, 3);
}
}
var evaluator = DiceExpressionEvaluator<int>.CreateCustom(new LoadedDiceRoller());RollCraft uses the Result<TError, TValue> pattern from the MonadCraft library for functional error handling without exceptions.
| Error Type | Description |
|---|---|
ParserError |
Syntax errors during parsing (includes position) |
EvaluatorError |
Runtime errors during evaluation |
var result = evaluator.Evaluate("2d6 + 3");
// Pattern matching with Switch
result.Switch(
onSuccess: r => Console.WriteLine($"Result: {r.Result}"),
onFailure: e => Console.WriteLine($"Error: {e.Message}")
);
// Async version
await result.SwitchAsync(
onSuccess: async r => await SaveResultAsync(r),
onFailure: async e => await LogErrorAsync(e)
);
// Check success/failure
if (result.IsSuccess)
{
var value = result.Value;
}
// Chain operations with Bind
var finalResult = parseResult.Bind(expr => evaluator.Evaluate(expr));Parser Errors:
- Invalid token
- Unexpected end of input
- Missing parenthesis
- Invalid function arguments
Evaluator Errors:
- Division by zero
- Non-integer dice count/sides
- Invalid modifier values
- Undefined variables
- Negative sqrt argument
The DiceExpressionResult<TError, TNumber> contains both the final result and detailed roll information.
var result = evaluator.Evaluate("4d6kh3");
result.Switch(
onSuccess: r =>
{
Console.WriteLine($"Total: {r.Result}");
foreach (var roll in r.Rolls)
{
Console.WriteLine($" d{roll.Sides}: {roll.Roll} {GetModifiers(roll.Modifier)}");
}
},
onFailure: e => Console.WriteLine(e.Message)
);
string GetModifiers(DiceModifier mod)
{
var parts = new List<string>();
if ((mod & DiceModifier.Dropped) != 0) parts.Add("dropped");
if ((mod & DiceModifier.Exploded) != 0) parts.Add("exploded");
if ((mod & DiceModifier.Rerolled) != 0) parts.Add("rerolled");
if ((mod & DiceModifier.Minimum) != 0) parts.Add("min-clamped");
if ((mod & DiceModifier.Maximum) != 0) parts.Add("max-clamped");
return parts.Count > 0 ? $"({string.Join(", ", parts)})" : "";
}| Flag | Description |
|---|---|
None |
No modifier applied |
Minimum |
Roll was clamped to minimum |
Maximum |
Roll was clamped to maximum |
Exploded |
Roll triggered an explosion |
Dropped |
Roll was dropped (keep modifier) |
Rerolled |
Roll was rerolled |
MIT License - see LICENSE for details.