24 game/CSharp: Difference between revisions

From Rosetta Code
Content added Content deleted
mNo edit summary
m (removed some duplication caused by copy/paste, edited intro text, homogenized open bracket placement)
Line 4: Line 4:
You could, for example, use the CodeDOM to dynamically compile an object that contains the expression string.
You could, for example, use the CodeDOM to dynamically compile an object that contains the expression string.


While not necessarily a good coding practice, but certainly a short and simple route, would be to use System.Xml.XPath.XPathNavigator.Evaluate(string xpath) as shown here:
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>
<lang csharp>
public class XPathEval : I24MathParser {
public class XPathEval : I24MathParser {
Line 33: Line 33:
Here is a more verbose, native solution - a lightweight math expression parser for evaluating 24 Game user input:
Here is a more verbose, native solution - a lightweight math expression parser for evaluating 24 Game user input:
<lang csharp>
<lang csharp>
/// <summary>
/// Lightweight math parser - C# does not have an Evaluate function
/// </summary>
public class MathParser : I24MathParser {
/// <summary>
/// <summary>
/// Lightweight math parser - C# does not have an Evaluate function
/// Lightweight math parser - C# does not have an Evaluate function
Line 77: Line 73:




float Solve(string equation){
float Solve(string equation) {
//carry out order of operations
//carry out order of operations
// bracketed subexpressions - for any operator
// bracketed subexpressions - for any operator
Line 96: Line 92:
Match match = subExpression.Match(equation);
Match match = subExpression.Match(equation);


while (match.Success)
while (match.Success) {
if (match.Groups[1].Length > 0) {
{
if (match.Groups[1].Length > 0)
{
//recursively solve for subexpressions -- match group 1 excludes outer brackets
//recursively solve for subexpressions -- match group 1 excludes outer brackets
subResult = Solve(match.Groups[1].Value);
subResult = Solve(match.Groups[1].Value);
}
}
else
else {
{
//no more nested expressions - get final result for this subExpression
//no more nested expressions - get final result for this subExpression
subResult = ParseEquation(match.Value);
subResult = ParseEquation(match.Value);

Revision as of 23:18, 28 April 2010

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: