In my last post, I walked through how a new equation gets generated. The game also generates a list of operations that the player could apply to the equation to simplify it. An equation starts out with coefficients and constants that are randomly generated. Then, as the player works through the problem, those values shift. As a result, the game needs to refresh the list of possible operations each time a new equation is generated, the player applies an operation to the equation, and so on.
The Equation class has a method called GenerateOperations() that generates a list of IAmAnOperation objects based on the current state of the equation. (IAmAnOperation objects are similar to IAmATerm objects, but they also include the operand to apply – add this, multiply this, etc.). This method is used at the beginning when the equation is first generated and every time the equation is updated as the result of a player’s action.
***
Before I get into how the operations are generated, though, I need to discuss how the coefficients/constants are stored. The initial version of the game stored these values as floats. As the development progressed and I played the game more, I began to realize that that was a poor design decision.
First, there were the problems with the math. During one of my tests, I managed to get an equation looking something like this:
1.1x = 5
I divided by 1.1 to get rid of the coefficient, but ended up with this instead:
1.0x = 4.5
“1.0x”? There was code in the game specifically designed to drop the “1” for variables, but what I found in this case was that the coefficient on the variable was not “1”, but “1.0000000001”, or something similarly silly. I started down the road of implementing logic that would treat something “very close to 1” as 1, but hit a lot of problems getting the rounding to work.
Then there was the UI itself. If you solve equations like I do, you reduce the equation until there is a single variable term on one side and a single constant term on the other, and then apply a division operation to get rid of the variable’s coefficient. That means you have to divide at most once, which means any fractional values will be remain relatively simple as you work through the equation. However, the game doesn’t prevent you from going off the rails by applying division operations over and over again, which will quickly lead to crazy-fractional numbers. With the floating-point approach, those numbers became much smaller than 1, and required a lot of space on the screen to display, e.g., “0.0012”. I couldn’t increase the size of the UI elements, and I could only shrink the font so far before it became unreadable.
Finally, CJ made the argument that keeping the values in fractional form would make it easier to read and simplify the equation. If you’re presented with “0.328”, it may or may not be obvious that multiplying that by 125 will leave you with a simple “41”. However, if you were presented with “(41/125)x”, your next steps become a lot easier to see.
For all of these reasons, I refactored the system to store everything in fractions rather than floating-point decimals. That means storing separate integers for numerators and denominators, and having to deal with applying fraction operations to fractional terms. As we’ll see next time, it ends up being a little more work, but not nearly what I had put in trying to get the floating-point approach operational. (See what I did there?)
This change would also have a direct impact to how I generated potential operations for the player to apply.
***
GenerateOperations generates one or more operations for each term in the current equation:
public List<IAmAnOperation> GenerateOperations()
{
List<IAmAnOperation> Operations, DeDupedSortedOperations;
int CurrentNumerator, CurrentDenominator;
string CurrentVariable;Operations = new List<IAmAnOperation>();
foreach (IAmATerm CurrentTerm in this.Terms)
{
CurrentNumerator = CurrentTerm.Numerator;
CurrentDenominator = CurrentTerm.Denominator;
if (CurrentNumerator == 0) { continue; }if (CurrentTerm is Variable && this._AreMultiplicationAndDivisionNeeded)
{
CurrentVariable = ((Variable)CurrentTerm).Var;Operations.Add(new VariableOperation(new Variable(CurrentNumerator, CurrentDenominator, CurrentVariable), Operands.Add));
Operations.Add(new VariableOperation(new Variable(CurrentNumerator, CurrentDenominator, CurrentVariable), Operands.Subtract));
}
else if (CurrentTerm is Constant)
{
Operations.Add(new ConstantOperation(new Constant(CurrentNumerator, CurrentDenominator), Operands.Add));
Operations.Add(new ConstantOperation(new Constant(CurrentNumerator, CurrentDenominator), Operands.Subtract));
}if (CurrentNumerator != 1 && this._AreMultiplicationAndDivisionNeeded)
{
Operations.Add(new ConstantOperation(new Constant(CurrentNumerator, CurrentDenominator), Operands.Multiply));
Operations.Add(new ConstantOperation(new Constant(CurrentNumerator, CurrentDenominator), Operands.Divide));
}if (CurrentDenominator != 1 && this._AreMultiplicationAndDivisionNeeded)
{
Operations.Add(new ConstantOperation(new Constant(CurrentDenominator, 1), Operands.Multiply));
}}
// Return a sorted, de-duped list
DeDupedSortedOperations = Enumerable.ToList(Enumerable.Distinct(Operations));
DeDupedSortedOperations.Sort();
return DeDupedSortedOperations;
}
For each term in the list:
- If the numerator is 0, skip this term and move on.
- If the term is a variable, and multiplication/division are needed, append two operations to the list – one that adds the current term, and a second that subtracts it. Why append *add/subtract* operations if multiplication/division are needed? Because if the latter are needed, it means the possibility of multiple variable terms in the equation. If those terms happen to be on the same side of the equation, the player can directly combine them. However, if there are variable terms on both sides, then the player will need to add or subtract one to simplify it.
- If the term is a constant, append two operations to the list – one that adds the current term, and a second that subtracts it.
- If the numerator is not 1, and multiplication/division are needed, append two operations to the list – one that multiplies by the coefficient, and another that divides by it.
- Finally, if the denominator is not 1, and multiplication/division are needed, append an operation to the list that multiplies by that denominator. This gives the player a way to clear out the fractional denominators.
It’s quite possible that there would be duplicate operations added as a result of all of these rules, so one of GenerateOperations’ final step is to dedup the list.
In my next post, I’ll look at the ApplyOperations() method, which does the work of validating that the player has applied a given operation correctly, and if so, does the work of transforming the equation as a result of that operation.
[…] 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 […]