24 game/CSharp
C_sharp
The C# language does not directly contain an Eval function for evaluating a string as a math expression, though there are still a number of ways to go about it.
You could, for example, use the CodeDOM to dynamically compile an object that contains the expression string.
Or, while not necessarily a good coding practice, but certainly a short and simple route, you could use System.Xml.XPath.XPathNavigator.Evaluate(string xpath) as shown here: <lang csharp> public class XPathEval : I24MathParser { public float Evaluate(string expression) { System.Xml.XPath.XPathNavigator navigator = new System.Xml.XPath.XPathDocument(new System.IO.StringReader("<r/>")).CreateNavigator();
//expath evaluator needs // '/' expressed as "div" // '%' expressed as "mod" string xpathExpression = expression.Replace("/", " div ").Replace("%", " mod "); float answer = Convert.ToSingle(navigator.Evaluate(String.Format("number({0})", xpathExpression)));
return answer; } } </lang>
The XPathEval class is implementing this interface to facilitate swapping out Evaluate providers:
<lang csharp>
interface I24MathParser {
float Evaluate(string expression);
}
</lang>
Here is a more verbose, native solution - a lightweight math expression parser for evaluating 24 Game user input:
<lang csharp>
/// <summary>
/// Lightweight math parser - C# does not have an Evaluate function
/// </summary>
public class MathParser : I24MathParser {
//used to translate brackets to implied multiplication - i.e. "3(4)5(6)" will be interpreted as "3*(4)*5*(6)"
private const string bracketsPattern = @"(?<=[0-9)])(?<rightSide>\()|(?<=\))(?<rightSide>[0-9])";
//finds multiplication or division sub expression - i.e. "4*8-4*2)" yields {"4*8", "4*2"} private const string multiplyDividePattern = @"[0-9]+[/*][0-9]+";
//finds bracketed expressions - i.e. "(4+30)(10-1)" yields {"4+30", "10-1"} private const string subExpressionPattern = @"\(([0-9/*\-+]*)\)";
//splits expression into it elements - i.e. "4+-30-4.123" yields {"4", "+", "-30" ,"-", "4.123"} private const string tokenPattern = @"(?:(?<=[+-]|^)[+-]?)?(?:[0-9]+(?:\.[0-9]*)?)|[/*\-+]";
Regex brackets; Regex multiplyDivide; Regex subExpression; Regex token;
public MathParser() {
//initialize reusable regular expressions
brackets = new Regex(bracketsPattern, RegexOptions.Compiled);
subExpression = new Regex(subExpressionPattern, RegexOptions.Compiled);
token = new Regex(tokenPattern, RegexOptions.Compiled);
multiplyDivide = new Regex(multiplyDividePattern, RegexOptions.Compiled);
}
public float Evaluate(string input) {
//brackets with no operator implies multiplication
string equation = brackets.Replace(input, "*${rightSide}");
float answer = Solve(equation);
return answer; }
float Solve(string equation) {
//carry out order of operations
// bracketed subexpressions - for any operator
equation = SolveSubExpressions(subExpression, equation);
// multiplication and division equation = SolveSubExpressions(multiplyDivide, equation);
// addition and subtraction float answer = ParseEquation(equation);
return answer; }
string SolveSubExpressions(Regex subExpression, string equation) {
float subResult;
Match match = subExpression.Match(equation);
while (match.Success) { if (match.Groups[1].Length > 0) { //recursively solve for subexpressions -- match group 1 excludes outer brackets subResult = Solve(match.Groups[1].Value); } else { //no more nested expressions - get final result for this subExpression subResult = ParseEquation(match.Value); }
//replace subexpression with resolved answer
equation = equation.Replace(match.Value, subResult.ToString());
//retest updated equation string match = subExpression.Match(equation); }
return equation; }
float ParseEquation(string equation) {
Match match = token.Match(equation);
float leftSide = leftSide = float.Parse(match.Value);
string symbol;
float rightSide;
match = match.NextMatch();
while (match.Success) { symbol = match.Value; match = match.NextMatch();
if (match.Success) { rightSide = float.Parse(match.Value); leftSide = Calculate(leftSide, symbol, rightSide); match = match.NextMatch(); } }
return leftSide; }
float Calculate(float leftSide, string symbol, float rightSide) {
float answer;
switch (symbol) { case "/": answer = leftSide / rightSide; break;
case "*": answer = leftSide * rightSide; break;
case "-": answer = leftSide - rightSide; break;
case "+": answer = leftSide + rightSide; break;
default: throw new ArgumentException(); }
return answer; } } </lang>
This is the main class that handles puzzle generation and user interaction
<lang csharp>
/// <summary>
/// The Game. Handles user interaction and puzzle generation.
/// </summary>
class TwentyFourGame {
//puzzle parameters
private const int listSize = 4;
private const int minValue = 1;
private const int maxValue = 9;
//signals end of game private const string quitToken = "Q";
//the only valid puzzle solution private const float targetValue = 24;
//Regular Expressions for evaluating math input private const string dictionaryBlacklistPattern = @"[^1-9/*\-+()]"; private const string inputDigitsPattern = @"(?:(?<=[+-]|^)[+-]?)?(?:[0-9]+(?:\.[0-9]*)?)"; Regex dictionaryBlackList; Regex inputDigits; I24MathParser mathParser;
public TwentyFourGame() { //initialize reusable regular expressions dictionaryBlackList = new Regex(dictionaryBlacklistPattern, RegexOptions.Compiled); inputDigits = new Regex(inputDigitsPattern, RegexOptions.Compiled);
//define instance of math evaluator provider //custom parser //mathParser = new MathParser();
//xpath parser mathParser = new XPathEval(); }
static void Main(string[] args) {
TwentyFourGame game = new TwentyFourGame();
game.PrintInstructions();
game.PlayGame();
}
void PlayGame() {
string input;
bool endGame = false;
//repeat play cycle until user signals the end
do {
string puzzle = GetPuzzle();
bool isValid = false;
//continue prompting user until valid input is received do { float answer; string message = String.Empty;
try { //show user puzzle and get read their solution input = GetInput(puzzle);
if (input.Length == 0) { //skip current puzzle - perhaps there is no solution isValid = true; message = "Skipping this puzzle"; } else if (String.Compare(input, quitToken, true) == 0) { //user wishes to quit isValid = true; message = "End Game"; } else if (ValidateInput(input, puzzle)) { //interpret user input and calculate answer answer = mathParser.Evaluate(input);
if (answer == targetValue) { isValid = true; message = String.Format("Good work. {0} = {1}.", input, answer); } else { isValid = false; message = String.Format("Incorrect. {0} = {1}. Try again.", input, answer); } } else { isValid = false; message = "Invalid input. Try again."; } } catch { message = "An error occurred. Check your input and try again."; isValid = false; } finally { PrintMessage(message); PrintMessage(String.Empty); PrintMessage(String.Empty); } } while (!isValid); } while (!endGame);
//pause GetInput(String.Empty); }
bool ValidateInput(string input, string puzzle) {
bool isValid;
if (dictionaryBlackList.IsMatch(input)) { //illegal characters used isValid = false; } else { //get inputted digits and compare to those in puzzle string inputNumbers = String.Join(" ", from Match m in inputDigits.Matches(input) orderby float.Parse(m.Value) select m.Value);
isValid = inputNumbers.CompareTo(puzzle) == 0; }
return isValid; }
string GetPuzzle() {
int[] digits = new int[listSize];
//randomly choose 4 digits (from 1 to 9) Random rand = new Random();
for (int i = 0; i < digits.Length; i++) { digits[i] = rand.Next(minValue, maxValue); }
//format for display Array.Sort(digits); string puzzle = String.Join(" ", digits); return puzzle; }
string GetInput(string prompt) {
Console.Write(String.Concat(prompt, ": "));
return Console.ReadLine();
}
void PrintMessage(string message) {
Console.WriteLine(message);
}
void PrintInstructions() {
PrintMessage("--------------------------------- 24 Game ---------------------------------");
PrintMessage(String.Empty);
PrintMessage("------------------------------- Instructions ------------------------------");
PrintMessage("Four digits will be displayed.");
PrintMessage("Enter an equation using all of those four digits that evaluates to 24");
PrintMessage("Only * / + - operators and () are allowed");
PrintMessage("Digits can only be used once, but in any order you need.");
PrintMessage("Digits cannot be combined - i.e.: 12 + 12 when given 1,2,2,1 is not allowed");
PrintMessage("Submit a blank line to skip the current puzzle.");
PrintMessage("Type 'Q' to quit");
PrintMessage(String.Empty);
PrintMessage("Example: given 2 3 8 2, answer should resemble 8*3-(2-2)");
PrintMessage("---------------------------------------------------------------------------");
PrintMessage(String.Empty);
PrintMessage(String.Empty);
}
}
</lang>
Example output:
--------------------------------- 24 Game --------------------------------- ------------------------------- Instructions ------------------------------ Four digits will be displayed. Enter an equation using all of those four digits that evaluates to 24 Only * / + - operators and () are allowed Digits can only be used once, but in any order you need. Digits cannot be combined - i.e.: 12 + 12 when given 1,2,2,1 is not allowed Submit a blank line to skip the current puzzle. Type 'Q' to quit Example: given 2 3 8 2, answer should resemble 8*3-(2-2) --------------------------------------------------------------------------- 1 3 3 4: 4*(3-1)*3 Good work. 4*(3-1)*3 = 24. 1 3 5 6: