在 C# 中解析骰子表达式(例如 3d6+5):从哪里开始?
Posted
技术标签:
【中文标题】在 C# 中解析骰子表达式(例如 3d6+5):从哪里开始?【英文标题】:Parsing dice expressions (e.g. 3d6+5) in C#: where to start? 【发布时间】:2010-11-18 01:41:17 【问题描述】:所以我希望能够在 C# 中解析和评估“骰子表达式”。骰子表达式的定义如下:
<expr> := <expr> + <expr>
| <expr> - <expr>
| [<number>]d(<number>|%)
| <number>
<number> := 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
对象?
【问题讨论】:
random.Next的第一个参数是包含的,第二个参数是排除的。 msdn.microsoft.com/en-us/library/2dx6wyd4%28VS.95%29.aspx 我猜你的意思是:rand.Next(1, 7) + 20 - (rand.Next(1, 4) + rand.Next(1, 4)) 嗯,我会从至少一个 +3 解析工具开始。 :-) 【参考方案1】:这是我最终想出的:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
public enum DiceExpressionOptions
None,
SimplifyStringValue
public class DiceExpression
/* <expr> := <expr> + <expr>
* | <expr> - <expr>
* | [<number>]d(<number>|%)
* | <number>
* <number> := 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<KeyValuePair<int, IDiceExpressionNode>> nodes = new List<KeyValuePair<int, IDiceExpressionNode>>();
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<int, IDiceExpressionNode>(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<int, IDiceExpressionNode>(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<int, IDiceExpressionNode>(multiplicand, new DiceRollNode(absNumDice, type));
this.nodes = (number == 0 ? normalizedDiceRollNodes
: normalizedDiceRollNodes.Concat(new[] new KeyValuePair<int, IDiceExpressionNode>(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("0d1", this.numberOfDice, this.diceType);
public int NumberOfDice
get return this.numberOfDice;
public int DiceType
get return this.diceType;
【讨论】:
【参考方案2】:一些尝试:
Evaluate dice rolling notation strings
【讨论】:
太棒了!它似乎以前出现在 SO 上,但我找不到它...我编辑了标签以使其更容易被发现。【参考方案3】:您可以在 C#(例如 antlr)的编译器编译器(类似于 Yacc)中使用您的语法,或者直接开始编写您的 recursive descent parser。
然后您构建一个内存数据结构(如果您想要除 + 之外的任意数学运算,则为树)is Visitable so you need to write a couple of visitors:
RollVisitor
: 初始化一个rand种子,然后访问每个节点,累积结果
GetMaxVisitor
:对每个骰子的上界求和
其他访客? (如PrettyPrintVisitor
、RollTwiceVisitor
等)
我认为 visitable-tree 在这里是一个有价值的解决方案。
【讨论】:
公平地说,这对我来说似乎有点矫枉过正。 @Greg:它是面向 OO 方式的解析树的标准设计......你为什么认为它是矫枉过正的?你喜欢单行充满正则表达式吗? 我还没有分析提供的语法,但它似乎足够小,以至于寻求一个成熟的代码生成解决方案可能没有问题空间所要求的那么简单。如果我有我的参考文本来记忆复习,我会很想当场画出适当的自动机。 好吧,你明白了,但是我还提到了一个递归下降解析器,对于这种语法应该很简单【参考方案4】:您应该在 CodeProject 上查看这篇文章:http://www.codeproject.com/KB/cpp/rpnexpressionevaluator.aspx。我解释了如何将中缀表达式转换为后缀表达式,然后对其进行评估。
对于解析,我想你可以用正则表达式来处理。
【讨论】:
以上是关于在 C# 中解析骰子表达式(例如 3d6+5):从哪里开始?的主要内容,如果未能解决你的问题,请参考以下文章
在 Haskell 中使用 Alex 制作解析骰子卷的词法分析器
从 5 次掷骰子中,生成一个范围为 [1 - 100] 的随机数