所以我希望能够在C#中解析和评估"骰子表达式".骰子表达式定义如下:
:= + | - | [ ]d( |%) | := positive integer
所以例如d6+20-2d3
是允许的,并且应该评估为
rand.Next(1, 7) + 20 - (rand.Next(1, 4) + rand.Next(1, 4))
也d%
应该相当于d100
.
我知道我可以解决一些解决方案,但我也知道这似乎是一个非常典型的计算机科学类型的问题,所以必须有一些我应该研究的超优雅解决方案.
我希望我的解析结果具有以下功能:
我应该能够输出表达式的规范化形式; 我首先考虑骰子,按骰子大小排序,并始终使用前缀.所以例如上面的样本会变成1d6-2d3+20
.任何实例d%
也会变成d100
标准化形式.
我应该能够随意评估表达式,每次滚动不同的随机数.
我应该能够用最大化的所有骰子卷来评估表达式,因此例如上面的样本将给出(确定性地)1*6+20+2*3 = 32
.
我知道这正是Haskell的类型,可能还有其他功能类型的语言,但是如果可能的话,我想留在C#中.
我最初的想法倾向于递归,列表,也许还有一些LINQ,但是,如果我尝试没有知道事物的人的一些指示,我肯定它最终会成为一个不优雅的混乱.
另一种可能有效的策略是一些基于正则表达式的初始字符串替换,将骰子表达式转换为rand.Next
调用,然后进行即时评估或编译......这实际上有用吗?我怎么能避免rand
每次都创建一个新对象?
这是我最终提出的:
using System; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; public enum DiceExpressionOptions { None, SimplifyStringValue } public class DiceExpression { /*:= + * | - * | [ ]d( |%) * | * := positive integer * */ private static readonly Regex numberToken = new Regex("^[0-9]+$"); private static readonly Regex diceRollToken = new Regex("^([0-9]*)d([0-9]+|%)$"); public static readonly DiceExpression Zero = new DiceExpression("0"); private List > nodes = new List >(); public DiceExpression(string expression) : this(expression, DiceExpressionOptions.None) { } public DiceExpression(string expression, DiceExpressionOptions options) { // A well-formed dice expression's tokens will be either +, -, an integer, or XdY. var tokens = expression.Replace("+", " + ").Replace("-", " - ").Split(' ', StringSplitOptions.RemoveEmptyEntries); // Blank dice expressions end up being DiceExpression.Zero. if (!tokens.Any()) { tokens = new[] { "0" }; } // Since we parse tokens in operator-then-operand pairs, make sure the first token is an operand. if (tokens[0] != "+" && tokens[0] != "-") { tokens = (new[] { "+" }).Concat(tokens).ToArray(); } // This is a precondition for the below parsing loop to make any sense. if (tokens.Length % 2 != 0) { throw new ArgumentException("The given dice expression was not in an expected format: even after normalization, it contained an odd number of tokens."); } // Parse operator-then-operand pairs into this.nodes. for (int tokenIndex = 0; tokenIndex < tokens.Length; tokenIndex += 2) { var token = tokens[tokenIndex]; var nextToken = tokens[tokenIndex + 1]; if (token != "+" && token != "-") { throw new ArgumentException("The given dice expression was not in an expected format."); } int multiplier = token == "+" ? +1 : -1; if (DiceExpression.numberToken.IsMatch(nextToken)) { this.nodes.Add(new KeyValuePair (multiplier, new NumberNode(int.Parse(nextToken)))); } else if (DiceExpression.diceRollToken.IsMatch(nextToken)) { var match = DiceExpression.diceRollToken.Match(nextToken); int numberOfDice = match.Groups[1].Value == string.Empty ? 1 : int.Parse(match.Groups[1].Value); int diceType = match.Groups[2].Value == "%" ? 100 : int.Parse(match.Groups[2].Value); this.nodes.Add(new KeyValuePair (multiplier, new DiceRollNode(numberOfDice, diceType))); } else { throw new ArgumentException("The given dice expression was not in an expected format: the non-operand token was neither a number nor a dice-roll expression."); } } // Sort the nodes in an aesthetically-pleasing fashion. var diceRollNodes = this.nodes.Where(pair => pair.Value.GetType() == typeof(DiceRollNode)) .OrderByDescending(node => node.Key) .ThenByDescending(node => ((DiceRollNode)node.Value).DiceType) .ThenByDescending(node => ((DiceRollNode)node.Value).NumberOfDice); var numberNodes = this.nodes.Where(pair => pair.Value.GetType() == typeof(NumberNode)) .OrderByDescending(node => node.Key) .ThenByDescending(node => node.Value.Evaluate()); // If desired, merge all number nodes together, and merge dice nodes of the same type together. if (options == DiceExpressionOptions.SimplifyStringValue) { int number = numberNodes.Sum(pair => pair.Key * pair.Value.Evaluate()); var diceTypes = diceRollNodes.Select(node => ((DiceRollNode)node.Value).DiceType).Distinct(); var normalizedDiceRollNodes = from type in diceTypes let numDiceOfThisType = diceRollNodes.Where(node => ((DiceRollNode)node.Value).DiceType == type).Sum(node => node.Key * ((DiceRollNode)node.Value).NumberOfDice) where numDiceOfThisType != 0 let multiplicand = numDiceOfThisType > 0 ? +1 : -1 let absNumDice = Math.Abs(numDiceOfThisType) orderby multiplicand descending orderby type descending select new KeyValuePair (multiplicand, new DiceRollNode(absNumDice, type)); this.nodes = (number == 0 ? normalizedDiceRollNodes : normalizedDiceRollNodes.Concat(new[] { new KeyValuePair (number > 0 ? +1 : -1, new NumberNode(number)) })).ToList(); } // Otherwise, just put the dice-roll nodes first, then the number nodes. else { this.nodes = diceRollNodes.Concat(numberNodes).ToList(); } } public override string ToString() { string result = (this.nodes[0].Key == -1 ? "-" : string.Empty) + this.nodes[0].Value.ToString(); foreach (var pair in this.nodes.Skip(1)) { result += pair.Key == +1 ? " + " : " ? "; // NOTE: unicode minus sign, not hyphen-minus '-'. result += pair.Value.ToString(); } return result; } public int Evaluate() { int result = 0; foreach (var pair in this.nodes) { result += pair.Key * pair.Value.Evaluate(); } return result; } public decimal GetCalculatedAverage() { decimal result = 0; foreach (var pair in this.nodes) { result += pair.Key * pair.Value.GetCalculatedAverage(); } return result; } private interface IDiceExpressionNode { int Evaluate(); decimal GetCalculatedAverage(); } private class NumberNode : IDiceExpressionNode { private int theNumber; public NumberNode(int theNumber) { this.theNumber = theNumber; } public int Evaluate() { return this.theNumber; } public decimal GetCalculatedAverage() { return this.theNumber; } public override string ToString() { return this.theNumber.ToString(); } } private class DiceRollNode : IDiceExpressionNode { private static readonly Random roller = new Random(); private int numberOfDice; private int diceType; public DiceRollNode(int numberOfDice, int diceType) { this.numberOfDice = numberOfDice; this.diceType = diceType; } public int Evaluate() { int total = 0; for (int i = 0; i < this.numberOfDice; ++i) { total += DiceRollNode.roller.Next(1, this.diceType + 1); } return total; } public decimal GetCalculatedAverage() { return this.numberOfDice * ((this.diceType + 1.0m) / 2.0m); } public override string ToString() { return string.Format("{0}d{1}", this.numberOfDice, this.diceType); } public int NumberOfDice { get { return this.numberOfDice; } } public int DiceType { get { return this.diceType; } } } }
你可以在C#的编译器编译器(比如Yacc)中使用你的语法(比如antlr),或者只是开始写你的递归下降解析器.
然后,您构建一个可访问的内存数据结构(如果您想要除+之外的任意数学运算的树),那么您需要编写几个访问者:
RollVisitor
:初始化rand种子,然后访问每个节点,累积结果
GetMaxVisitor
:求和每个骰子的上限
其他访客?(如PrettyPrintVisitor
,RollTwiceVisitor
等等)
我认为可访问树在这里是一个值得解决的问题.
一些尝试:
评估骰子滚动符号字符串