Add Rolemaster ruleset parsing scaffolding
This commit is contained in:
@@ -3,7 +3,8 @@ namespace RpgRoller.Domain;
|
||||
public enum RulesetKind
|
||||
{
|
||||
D6,
|
||||
Dnd5e
|
||||
Dnd5e,
|
||||
Rolemaster
|
||||
}
|
||||
|
||||
public enum RollVisibility
|
||||
@@ -87,4 +88,12 @@ public sealed class RollLogEntry
|
||||
public required DateTimeOffset TimestampUtc { get; init; }
|
||||
}
|
||||
|
||||
public sealed record DiceExpression(int DiceCount, int Sides, int Modifier, string Canonical);
|
||||
public enum DiceExpressionKind
|
||||
{
|
||||
Standard,
|
||||
RolemasterInitiative,
|
||||
RolemasterPercentile,
|
||||
RolemasterOpenEndedPercentile
|
||||
}
|
||||
|
||||
public sealed record DiceExpression(int DiceCount, int Sides, int Modifier, string Canonical, DiceExpressionKind Kind = DiceExpressionKind.Standard);
|
||||
|
||||
@@ -13,6 +13,9 @@ public static partial class DiceRules
|
||||
if (string.Equals(rulesetId, "dnd5e", StringComparison.OrdinalIgnoreCase))
|
||||
return RulesetKind.Dnd5e;
|
||||
|
||||
if (string.Equals(rulesetId, "rolemaster", StringComparison.OrdinalIgnoreCase))
|
||||
return RulesetKind.Rolemaster;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -20,9 +23,10 @@ public static partial class DiceRules
|
||||
{
|
||||
return ruleset switch
|
||||
{
|
||||
RulesetKind.D6 => "d6",
|
||||
RulesetKind.Dnd5e => "dnd5e",
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(ruleset), ruleset, "Unknown ruleset.")
|
||||
RulesetKind.D6 => "d6",
|
||||
RulesetKind.Dnd5e => "dnd5e",
|
||||
RulesetKind.Rolemaster => "rolemaster",
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(ruleset), ruleset, "Unknown ruleset.")
|
||||
};
|
||||
}
|
||||
|
||||
@@ -34,9 +38,10 @@ public static partial class DiceRules
|
||||
var trimmed = expression.Trim();
|
||||
return ruleset switch
|
||||
{
|
||||
RulesetKind.D6 => ParseD6(trimmed),
|
||||
RulesetKind.Dnd5e => ParseDnd5e(trimmed),
|
||||
_ => ServiceResult<DiceExpression>.Failure("invalid_ruleset", "Unknown ruleset.")
|
||||
RulesetKind.D6 => ParseD6(trimmed),
|
||||
RulesetKind.Dnd5e => ParseDnd5e(trimmed),
|
||||
RulesetKind.Rolemaster => ParseRolemaster(trimmed),
|
||||
_ => ServiceResult<DiceExpression>.Failure("invalid_ruleset", "Unknown ruleset.")
|
||||
};
|
||||
}
|
||||
|
||||
@@ -71,7 +76,37 @@ public static partial class DiceRules
|
||||
return ServiceResult<DiceExpression>.Success(new(diceCount, sides, modifier, $"{diceCount}d{sides}{FormatModifier(modifier)}"));
|
||||
}
|
||||
|
||||
private static ServiceResult<bool> ValidateDiceParts(int diceCount, int sides, int modifier)
|
||||
private static ServiceResult<DiceExpression> ParseRolemaster(string expression)
|
||||
{
|
||||
var initiativeMatch = RolemasterInitiativeRegex().Match(expression);
|
||||
if (initiativeMatch.Success)
|
||||
{
|
||||
var modifier = ParseModifier(initiativeMatch.Groups["modifier"].Value);
|
||||
var validation = ValidateDiceParts(2, 10, modifier, -MaxModifier, MaxModifier);
|
||||
if (!validation.Succeeded)
|
||||
return ServiceResult<DiceExpression>.Failure(validation.Error!.Code, validation.Error.Message);
|
||||
|
||||
return ServiceResult<DiceExpression>.Success(new(2, 10, modifier, $"2d10{FormatModifier(modifier)}", DiceExpressionKind.RolemasterInitiative));
|
||||
}
|
||||
|
||||
var percentileMatch = RolemasterPercentileRegex().Match(expression);
|
||||
if (percentileMatch.Success)
|
||||
{
|
||||
var modifier = ParseModifier(percentileMatch.Groups["modifier"].Value);
|
||||
var validation = ValidateDiceParts(1, 100, modifier, -MaxModifier, MaxModifier);
|
||||
if (!validation.Succeeded)
|
||||
return ServiceResult<DiceExpression>.Failure(validation.Error!.Code, validation.Error.Message);
|
||||
|
||||
var isOpenEnded = percentileMatch.Groups["openEnded"].Success;
|
||||
var canonical = isOpenEnded ? $"d100!{FormatModifier(modifier)}" : $"d100{FormatModifier(modifier)}";
|
||||
var kind = isOpenEnded ? DiceExpressionKind.RolemasterOpenEndedPercentile : DiceExpressionKind.RolemasterPercentile;
|
||||
return ServiceResult<DiceExpression>.Success(new(1, 100, modifier, canonical, kind));
|
||||
}
|
||||
|
||||
return ServiceResult<DiceExpression>.Failure("invalid_expression", "Expected Rolemaster format like 2d10+48, d100+4, or d100!+85.");
|
||||
}
|
||||
|
||||
private static ServiceResult<bool> ValidateDiceParts(int diceCount, int sides, int modifier, int minModifier = 0, int maxModifier = MaxModifier)
|
||||
{
|
||||
if (diceCount < 1 || diceCount > MaxDiceCount)
|
||||
return ServiceResult<bool>.Failure("invalid_expression", $"Dice count must be between 1 and {MaxDiceCount}.");
|
||||
@@ -79,8 +114,8 @@ public static partial class DiceRules
|
||||
if (sides < 2 || sides > MaxSides)
|
||||
return ServiceResult<bool>.Failure("invalid_expression", $"Dice sides must be between 2 and {MaxSides}.");
|
||||
|
||||
if (modifier < 0 || modifier > MaxModifier)
|
||||
return ServiceResult<bool>.Failure("invalid_expression", $"Modifier must be between 0 and {MaxModifier}.");
|
||||
if (modifier < minModifier || modifier > maxModifier)
|
||||
return ServiceResult<bool>.Failure("invalid_expression", $"Modifier must be between {minModifier} and {maxModifier}.");
|
||||
|
||||
return ServiceResult<bool>.Success(true);
|
||||
}
|
||||
@@ -92,7 +127,12 @@ public static partial class DiceRules
|
||||
|
||||
private static string FormatModifier(int modifier)
|
||||
{
|
||||
return modifier > 0 ? $"+{modifier}" : string.Empty;
|
||||
return modifier switch
|
||||
{
|
||||
> 0 => $"+{modifier}",
|
||||
< 0 => modifier.ToString(),
|
||||
_ => string.Empty
|
||||
};
|
||||
}
|
||||
|
||||
[GeneratedRegex("^(?<count>\\d+)D(?:\\+(?<modifier>\\d+))?$", RegexOptions.CultureInvariant | RegexOptions.IgnoreCase)]
|
||||
@@ -101,6 +141,12 @@ public static partial class DiceRules
|
||||
[GeneratedRegex("^(?<count>\\d+)d(?<sides>\\d+)(?:\\+(?<modifier>\\d+))?$", RegexOptions.CultureInvariant | RegexOptions.IgnoreCase)]
|
||||
private static partial Regex Dnd5eRegex();
|
||||
|
||||
[GeneratedRegex("^2d10(?<modifier>[+-]\\d+)?$", RegexOptions.CultureInvariant | RegexOptions.IgnoreCase)]
|
||||
private static partial Regex RolemasterInitiativeRegex();
|
||||
|
||||
[GeneratedRegex("^(?:1)?d100(?<openEnded>!)?(?<modifier>[+-]\\d+)?$", RegexOptions.CultureInvariant | RegexOptions.IgnoreCase)]
|
||||
private static partial Regex RolemasterPercentileRegex();
|
||||
|
||||
private const int MaxDiceCount = 50;
|
||||
private const int MaxSides = 1000;
|
||||
private const int MaxModifier = 1000;
|
||||
@@ -108,6 +154,7 @@ public static partial class DiceRules
|
||||
public static readonly IReadOnlyList<(RulesetKind Kind, string Id, string Name, string DiceSyntax)> SupportedRulesets =
|
||||
[
|
||||
(RulesetKind.D6, "d6", "D6 System", "countD(+modifier), e.g. 5D+4"),
|
||||
(RulesetKind.Dnd5e, "dnd5e", "D&D 5e", "countdSides(+modifier), e.g. 2d12+2")
|
||||
(RulesetKind.Dnd5e, "dnd5e", "D&D 5e", "countdSides(+modifier), e.g. 2d12+2"),
|
||||
(RulesetKind.Rolemaster, "rolemaster", "Rolemaster", "2d10+48, d100+4, d100!+85")
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user