Last time, we walked through how the operations in the tray get generated, based on the current equations. Now we’ll see how those get applied to the equation to solve it.
Let’s square away some UI-terminology first. The equation to be solved is arranged in a series of UI "dominos".
Each domino has two parts. The upper portion is a "term" of the equation, and the lower portion is the "operation" to be applied to it.
The process begins when the player drags his or her first operation off the tray onto a domino. As soon as they select an operation, all of the others in the operations tray are disabled, preventing them from trying to apply two different operations in the same pass.
Once the user clicks the "Go" button, the system pulls together the list of operations to be applied. For the dominos that didn’t get an operation, a special "NoOperation" object is used as a placeholder.
The first thing ApplyOperations does is call ValidateOperationsBeingApplied, passing it both the list of Terms and the list of Operations to apply. The method does just what you think it would – makes sure the user hasn’t done anything invalid like trying to apply an operation to only one side of the equation. The logic for this is not trivial, so I’ll cover that in my next blog post. If the operations being applied are not valid, an error is returned to the UI, and the player can then correct it. If the operations are valid, then it moves on to actually applying them.
The majority of ApplyOperations is a loop that walks through each Term/Operation pair, to evaluate what the "new" equation state should be. With each term-operation pair, the code determines whether to carry forward 0, 1, or 2 terms into the new equation.
NewEquationState = new List<IAmATerm>();
for (int i = 0; i < this.Terms.Count; i++)
{
IAmATerm CurrentTerm;
IAmAnOperation CurrentOperation;
CurrentTerm = this.Terms[i];
CurrentOperation = OperationsToApply[i];
// Check the possible scenarios
}
Let’s get into the scenarios, one at a time:
Current term is the equal sign. Simply add an EqualSign to NewEquationState:
if (CurrentTerm is EqualSign)
{
NewEquationState.Add(new EqualSign());
}
The current operation is a NoOperation. The user didn’t apply anything to this term, so we’ll carry that term forward into NewEquationState, unchanged:
else if (CurrentOperation is NoOperation)
{
NewEquationState.Add(CurrentTerm.Clone());
}
The current operation is a constant, and it’s being applied to a constant term. Apply that operation to the term. If the resulting value is 0, then don’t add anything to the NewEquationState; otherwise, include the newly-calculated constant:
else if (CurrentTerm is Constant && CurrentOperation is ConstantOperation)
{
NewCoefficient = this.CalculateNewCoefficient(CurrentTerm.Numerator, CurrentTerm.Denominator, CurrentOperation);
if (NewCoefficient.Numerator != 0) { NewEquationState.Add(NewCoefficient.Clone()); }
}
The current operation is a variable, and it’s being applied to a variable term. Apply that operation to the term. If the resulting value is 0, then don’t add anything to the NewEquationState; otherwise, include the newly-calculated variable:
else if (CurrentTerm is Variable && CurrentOperation is VariableOperation)
{
NewCoefficient = this.CalculateNewCoefficient(CurrentTerm.Numerator, CurrentTerm.Denominator, CurrentOperation);
if (NewCoefficient.Numerator != 0) { NewEquationState.Add(new Variable(NewCoefficient.Numerator, NewCoefficient.Denominator, ((Variable)CurrentTerm).Var)); }
}
Those were the easy cases. Now comes the fun ones. What happens if the player tries to apply a constant operation to a variable?
else if (CurrentTerm is Variable && CurrentOperation is ConstantOperation)
{
switch (CurrentOperation.Operand)
{
case Operands.Add:
NewEquationState.Add(CurrentTerm.Clone());
NewEquationState.Add(new Constant(CurrentOperation.Numerator, CurrentOperation.Denominator));
break;
case Operands.Subtract:
NewEquationState.Add(CurrentTerm.Clone());
NewEquationState.Add(new Constant(-1 * CurrentOperation.Numerator, CurrentOperation.Denominator));
break;
case Operands.Multiply:
NewCoefficient = new Constant(CurrentTerm.Numerator * CurrentOperation.Numerator, CurrentTerm.Denominator * CurrentOperation.Denominator);
NewEquationState.Add(new Variable(NewCoefficient.Numerator, NewCoefficient.Denominator, ((Variable)CurrentTerm).Var));
break;
case Operands.Divide:
NewCoefficient = new Constant(CurrentTerm.Numerator * CurrentOperation.Denominator, CurrentTerm.Denominator * CurrentOperation.Numerator);
NewEquationState.Add(new Variable(NewCoefficient.Numerator, NewCoefficient.Denominator, ((Variable)CurrentTerm).Var));
break;
default:
throw new UnsupportedOperandException(CurrentOperation.Operand.ToString());
}
}
In the cases of addition and subtraction, both the term and the operation are carried forward into the NewEquationState, since these really don’t combine. In the cases of multiplication and division, the constant does get applied to the variable, and the resulting variable is included in the NewEquationState.
And then, what about the case where the user applies a variable operation to a constant?
else if (CurrentTerm is Constant && CurrentOperation is VariableOperation)
{
switch (CurrentOperation.Operand)
{
case Operands.Add:
// When adding a variable operation to a constant terms, since the Term and Operation don’t line up, just carry them
// both forward to the next equation state. The exception is when the term is 0. That shouldn’t be carried forward.
if (CurrentTerm.Numerator != 0) { NewEquationState.Add(CurrentTerm.Clone()); }
NewEquationState.Add(new Variable(CurrentOperation.Numerator, CurrentOperation.Denominator, ((VariableOperation)CurrentOperation).Var));
break;
case Operands.Subtract:
// When adding a variable operation to a constant terms, since the Term and Operation don’t line up, just carry them
// both forward to the next equation state, but flip the sign of the coefficient. The exception is when the term is
// 0. That shouldn’t be carried forward.
if (CurrentTerm.Numerator != 0) { NewEquationState.Add(CurrentTerm.Clone()); }
NewEquationState.Add(new Variable(-1 * CurrentOperation.Numerator, CurrentOperation.Denominator, ((VariableOperation)CurrentOperation).Var));
break;
default:
// Multiplying and Dividing variable operations is not supported at this time
throw new UnsupportedOperandException(CurrentOperation.Operand.ToString());
}
}
In this case, if the constant term is non-0, addition and subtraction work the same as above – these don’t combine, so both the term and the operation are carried forward.
If the term is 0, however, only carry the operation forward in its place. This addresses the scenario where you have something like:
2x – 1 = 0
And you apply a "-2x" to both sides to get
-1 = -2x
Finally, trying to multiply or divide by a variable is not allowed in this version of the game, so those just throw errors. The player doesn’t even have the option to apply a variable operations involving multiplication or division (GenerateOptions method doesn’t currently generate these), but I included the check here for completeness and safety.
At this point, we have our tentative new equation state. The last pieces of ApplyOperations tidies things up a bit:
…
// Check the possible scenarios
// If the equation is left with no terms on the right, add a 0
if (NewEquationState.Last() is EqualSign) { NewEquationState.Add(new Constant(0, 1)); }
// If the equation is left with no terms on the left, add a 0
if (NewEquationState.First() is EqualSign) { NewEquationState.Insert(0, new Constant(0, 1)); }
IsFirstTermOnThisSideOfEquation = true;
for (int i=0; i<NewEquationState.Count; i++)
{
if (NewEquationState[i] is EqualSign) { IsFirstTermOnThisSideOfEquation = true; }
else if(IsFirstTermOnThisSideOfEquation)
{
NewEquationState[i].SetAsFirst();
IsFirstTermOnThisSideOfEquation = false;
}
}
this._Terms.Clear();
this._Terms.AddRange(NewEquationState);
this.EvaluateEquationToSeeIfItHasBeenSolved();
First, it makes sure there is at least something on both sides of the equals sign. If not, it adds a constant term of 0. Then it runs through the new terms, and flags the first one on each side as officially "first" (needed so the UI can render the signs correctly). Finally, it updates _Terms with the new equation state, and checks to see if it has, in fact, been solved.
In my next post, I’ll return to how the operations are validated before being applied.