Formatting Terms and Operations

I mentioned in my last post that the logic for displaying terms and operations was much more complicated than I originally anticipated.  The logic evolved as the game development progressed, and CJ and I identified new and interesting edge cases to be addressed.

I’ll begin with the logic behind the operations.  Both ConstantOperation and VariableOperation have their ToString() methods overridden.  ConstantOperation is the simpler of the two:

public override string ToString()
{
    return Utility.ToConstantOperationString(this.Numerator, this.Denominator, this.Operand);
}

The ToConstantOperationString method does all of the heaving lifting here:

public static string ToConstantOperationString(int Numerator, int Denominator, Operands Operand)
{
    string OperandSymbol, Sign;
    OperandSymbol = "";
    Sign = "";
    switch (Operand)
    {
        case Operands.Add:
            OperandSymbol = ((float)Numerator / (float)Denominator < 0 ? "-" : "+");
            Sign = "";
            break;
        case Operands.Subtract:
            OperandSymbol = ((float)Numerator / (float)Denominator < 0 ? "+" : "-");
            Sign = "";
             break;
        case Operands.Multiply:
            OperandSymbol = "*";
            Sign = ((float)Numerator / (float)Denominator < 0 ? "-" : "");
            break;
        case Operands.Divide:
            OperandSymbol = "/";
            Sign = ((float)Numerator / (float)Denominator < 0 ? "-" : "");
            break;
    }

    return string.Format("{0}{1}{2}{3}{4}{5}{6}",
                  OperandSymbol,
                 Sign,
                  (Math.Abs(Denominator) == 1 ? "" : "("),
                 Math.Abs(Numerator),
                 (Math.Abs(Denominator) == 1 ? "" : "/"),
                 (Math.Abs(Denominator) == 1 ? "" : Math.Abs(Denominator).ToString()),
                 (Math.Abs(Denominator) == 1 ? "" : ")"));
}

The switch statement sorts out which operand and which sign will be displayed.  The latter is dependent on the former.  For adds & subtracts, the operand and the sign end up effectively merged with each other.

  • Adding a positive 4: "+4"
  • Adding a negative 4: "-4"
  • Subtracting a positive 4: "-4"
  • Subtracting a negative 4: "+4"

Multiplication and division, however, preserve both the operand and the sign (at least when the number is negative):

  • Multiplying a positive 4: "*4"
  • Multiplying a negative 4: "*-4"
  • Dividing by a positive 4: "/4"
  • Dividing by a negative 4: "/-4"

Then we need to format the final value, which includes the operand, the sign, and the number itself.  Whole numbers like the above examples are straightforward.  Fractional numbers present an additional challenge.  I made the decision to surround fractional operations in parentheses, to set the absolute value apart from the sign, operand, and (as we’ll see later this post), the variable letter:

  • +(4/3)
  • -(4/3)
  • /-(4/3)
  • *-(4/3)

As the Format() function at the end suggests, each constant is made up of 7 pieces:

  • The operand itself (+, -, *, or /)
  • The sign of the constant
  • An open parenthesis, if this is a fraction (denoted by a non-1 denominator)
  • The numerator
  • A slash, if this is a fraction
  • The denominator, if this is a fraction
  • A closing parenthesis, if this is a fraction

It is also important to note that the numerator, the denominator, or both, may be negative.  However, it all cases, the presentation of that value will never be something like "-4/-3".  The signs of the two components effective get evaluated in the expression:

((float)Numerator / (float)Denominator < 0 ? … : … )

VariableOperation.ToString() use the exact same logic for coming up with the coefficient, but includes two additional pieces of logic:

public override string ToString()
{
    String InitialCoefficientString;

    InitialCoefficientString = Utility.ToConstantOperationString(this.Numerator, this.Denominator, this.Operand);
    if(Math.Abs(this.Numerator) == 1 && Math.Abs(this.Denominator) == 1) { InitialCoefficientString = InitialCoefficientString.Replace("1", ""); }

    return string.Format("{0}{1}", InitialCoefficientString, this.Var);
}

First, it appends the variable letter itself, in the Format() call.  Second, it evaluates the coefficient to see if it is exact "1".  If so, it drops the coefficient completely, so that the player sees "+x" rather than "+1x", by simply replacing the "1" with an empty string.  (However, since this version of the game does not allow you to "multiply by x" or "divide by x", the player wouldn’t see something like "*x" or "/x" anyway, so this bit of logic is really just future-proofing.)

That’s all there is for rendering operations.  Let’s move on to how terms are presented. 

***

Terms have a slightly different set of concerns than operations.  The operations have to show one of four operands, every time, while terms only have to worry about rendering "+" and "-".  In some cases, an operand is not shown at all, as in the case of leading, positive values:

4x – 1 = 15

This concept of "leading term" crops up a couple of times in the other methods we’ve looked at.  The signs for the first term on either side of the equal sign get treated a little differently.

As with the operations, the Constant and Variable classes have their ToString() methods overridden.  Let’s start with Constant:

public override string ToString()
{
    if(this.Numerator == 0) { return "0"; }

    return string.Format("{0}{1}{2}{3}",
                         (((float)this.Numerator / (float)this.Denominator) > 0f ? (this.IsFirstOnThisSide ? "" : "+ ") : (this.IsFirstOnThisSide ? "-" : "- ")),
                         Math.Abs(this.Numerator),
                          (Math.Abs(this.Denominator) == 1 ? "" : "/"),
                         (Math.Abs(this.Denominator) == 1 ? "" : Math.Abs(this.Denominator).ToString()));
}

Much of this should look familiar from ConstantOperation.  The notable exceptions being the lack of parentheses, and the presentation of the sign.  The latter gets subtly modified depending on whether this is the first term in the equation or not.  First-term additions show no sign:

4 + 3x = 2

While other additions show a "+" followed by a space.

3x + 4 = 2

The space improves the look of the equation.  Without it, it would look like

3x +4 = 2

First-term subtractions show the minus sign with no intervening space (the "-3" here), but other subtractions (the "- 9" here) include the space:

4x = -3 – 9

****

Now let’s look at Variable terms.  The Variable version of ToString() combines these the approaches:

public override string ToString()
{
    return string.Format("{0}{1}{2}{3}{4}{5}{6}",
                         (((float)this.Numerator / (float)this.Denominator) > 0f ? (this.IsFirstOnThisSide ? "" : "+ ") : (this.IsFirstOnThisSide ? "-" : "- ")),
                          (Math.Abs(this.Denominator) == 1 ? "" : "("),
                          (Math.Abs(this.Numerator) == 1 && Math.Abs(this.Denominator) == 1 ? "" : Math.Abs(this.Numerator).ToString()),
                          (Math.Abs(this.Denominator) == 1 ? "" : "/"),
                         (Math.Abs(this.Denominator) == 1 ? "" : Math.Abs(this.Denominator).ToString()),
                          (Math.Abs(this.Denominator) == 1 ? "" : ")"),
                          this.Var);
}

Parentheses are included (to distinguish the sign, coefficient, and variable more clearly), as is the "first term" logic.

***

At this point, you might be asking why I ended up with three different approaches to formatting the values.  The requirements for operations, constant terms, and variable terms have overlap, but none of them were complete subsets of the others.  That makes it trickier to factor anything out.  I think you can make the argument, though, that showing a constant operation of "-(3/4)", and a constant term of "-3/4" (as the game currently does) is inconsistent and should be normalized.  Doing so would go a long way to making it easier to refactor the logic together.  Another improvement for a later version.

This concludes the series on the algebra game.  There are still plenty of things that we’d like to do with the game before we consider submitting it to one or both app stores: replace the temporary graphics, add usage tracking, add error logging, and so on.  I’ve had a blast building it to this point and (for the most part) loved the discussions with CJ about its direction.  (We won’t speak of the "I’m sorry dear; I really think you need to represent numbers as fractions" conversation again.)

Advertisement

Validating the Operations Applied

In my last post, I walked through how the player’s operations get applied to the equation.  The first step in that was to validate that the operations applied were actually valid.  Today, I’m going to go through that validation method: ValidatateOperationsBeingApplied.

The method is passed a list of the operations to apply, and a boolean called ShouldForceOperationsToMatchTerms.  The general design is that it makes a single pass through the list of operations, calculating statistics as it goes, and then does a series of comparisons to verify everything is in order.

it begins by performing a couple of sanitation checks on the list:

if (OperationsToApply == null) { throw new OperationsValidityException(OperationsValidityException.InvalidStates.ListIsNull); }
if (OperationsToApply.Count != this.Terms.Count) { throw new OperationsValidityException(OperationsValidityException.InvalidStates.ListCountMismatch); }

In theory, the application should not allow either of these cases to occur, but #BugsHappen.

Next, the method calculates a couple of quick statistics on the terms.  These will be used as the baseline that the operations will be compared to.

NumberOfTermsOnLeft = 0;
NumberOfTermsOnRight = 0;
FoundEqualSign = false;
foreach (IAmATerm CurrentTerm in this.Terms)
{
    if (CurrentTerm is EqualSign) { FoundEqualSign = true; }
    else if (FoundEqualSign) { NumberOfTermsOnRight++; }
     else { NumberOfTermsOnLeft++; }
}

Next is the main loop:

NumberOfOperationsOnLeft = 0;
ListContainsEqualSign = false;
FirstOperation = null;
NumberASsOnLeft = 0;
NumberASsOnRight = 0;
NumberMDsOnLeft = 0;
NumberMDsOnRight = 0;
TermIndex = 0;
foreach (IAmAnOperation CurrentOperation in OperationsToApply)
{
    …
}

The variables that begin "NumberAS" refer to the number of Addition/Subtraction operations.  The variables that begin "NumberMD" refer to the number of Multiplication/Division operations.  The loop-logic begins by checking whether the equation was configured to require the variable operations to line up with the variable terms, and constant operations with constant terms:

if(ShouldForceOperationsToMatchTerms)
{
    if (this.Terms[TermIndex] is Variable &&
        CurrentOperation is ConstantOperation &&
        (CurrentOperation.Operand == Operands.Add || CurrentOperation.Operand == Operands.Subtract))
    {
        throw new TermOperationMismatchException();
    }

    if (this.Terms[TermIndex] is Constant
        && CurrentOperation is VariableOperation)
    {
        throw new TermOperationMismatchException();
    }
}

The first conditional checks to see if a constant operation is being applied to a variable one.  That’s ok when the player is multiplying or dividing by that constant, but not when they’re trying to add or subtract it.  The second conditional checks to see if a variable operation is being applied to a constant one, which is never allowed.  In both cases, a TermOperationMismatchException is thrown.

If the equation is configured to allow mismatches, then the player can try it, but ApplyOperations will simply carry both the original term and the new operation forward into the next equation state.  For the current iteration of the game, all of the levels are configured to allow mismatches to occur, so this logic is not actually executed during regular gameplay.

Next, the logic looks to see if it’s encountered the equal sign yet in the list of operations.  If not, then it’s still going through the terms on the left side of the equal sign, and it increments a counter to that effect:

if (CurrentOperation is EqualSignOperation) { ListContainsEqualSign = true; }
else if (!ListContainsEqualSign) { NumberOfOperationsOnLeft++; }

Next it looks at what kind of operations (addition, division, etc.) is being applied.

if (!(CurrentOperation is EqualSignOperation) && !(CurrentOperation is NoOperation))
{
    if (FirstOperation == null) { FirstOperation = CurrentOperation; }
    else if (!FirstOperation.Equals(CurrentOperation)) { throw new OperationsValidityException(OperationsValidityException.InvalidStates.ListContainsMoreThanOneOperation); }

    if (FirstOperation.Operand == Operands.Add || FirstOperation.Operand == Operands.Subtract)
    {
        NumberASsOnLeft += (!ListContainsEqualSign ? 1 : 0);
        NumberASsOnRight += (ListContainsEqualSign ? 1 : 0);
    }

    if (FirstOperation.Operand == Operands.Multiply || FirstOperation.Operand == Operands.Divide)
    {
        NumberMDsOnLeft += (!ListContainsEqualSign ? 1 : 0);
        NumberMDsOnRight += (ListContainsEqualSign ? 1 : 0);
    }
}

Recall from "Applying Operations” that the player is only allowed to apply a single operation to the equation at a time.  The if-then-else-if verifies that that is the case.  It looks for the "first" operation being applied, and makes sure that all of the other ones match it.

Then it increments 2 of 4 counters, depending on what kind of operation is being applied, and whether or not it’s encountered the equal sign already.

  • NumberASsOnLeft
  • NumberASsOnRight
  • NumberMDsOnLeft
  • NumberMDsOnRight

Finally, it increments the index it uses to keep track of which term it is inspecting:

TermIndex++;

After the loop, it evaluates the statistics that it’s been collecting:

if (!ListContainsEqualSign)
{
    throw new OperationsValidityException(OperationsValidityException.InvalidStates.ListLacksEqualSign);
}

First, if the list of operations didn’t even include an equal sign, it’s clearly invalid.

if (NumberOfOperationsOnLeft != NumberOfTermsOnLeft)
{
    throw new OperationsValidityException(OperationsValidityException.InvalidStates.ListIsUnbalanced);
}

This verifies that the number of operations on the left matches the number of terms on the left.  I only have to verify that the left sides match in number.  At this point in the logic, I can safely assume that the right sides also match since a) I’ve already verified that the total number of operations matches the total number of terms, and b) I’ve verified that the former contains an equal sign.

if (NumberASsOnLeft == 0 && NumberASsOnRight == 0 && NumberMDsOnLeft == 0 && NumberMDsOnRight == 0)
{
    throw new NoOperationException();
}

Now it verifies that the player applied SOMETHING to the equation – at least one of these statistics (and ideally 2) should be non-0.

if (NumberASsOnLeft != NumberASsOnRight) { throw new AddSubtractOperationException(); }

This confirms that the number of addition/subtraction operations applied on the left match those on the right…

if (NumberASsOnLeft > 1) { throw new AddSubtractOperationException(); }

…and that there was at most 1 operation applied on each side (again, if the left matches the right, I only have to check the count on the left).

if (NumberASsOnLeft == 0)
{
    if (NumberMDsOnLeft != NumberOfTermsOnLeft) { throw new MultiplyDivideOperationException(); }
    if (NumberMDsOnRight != NumberOfTermsOnRight) { throw new MultiplyDivideOperationException(); }
}

Finally, if the player didn’t apply an addition/subtraction operation, then they must have applied a multiplication/division operation.  Check that the number of multiplication operations applied on the left matches the ones on the right.  Then check that the number of division operations match left and right.

If all of these checks pass, then the operations being applied are valid, and the player can proceed.

In my next post, I’ll go through the logic involved in displaying an operation in the UI.  As it turns out, it’s not as simple as "is the number negative or not?"  It was surprising how many scenarios had to be accounted for to get it right.

Applying Operations

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".

10

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.

20

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.

30

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.

Generating Operations

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.

Building an Equation

In my previous post, I covered how the complexity of each of the game’s levels are configured.  This post will walk through how that configuration gets turned into an actual equation.

After the Equation constructor verifies that the configuration is valid, it generates the list of terms for the new equation, in three steps:

this._Terms.AddRange(this.BuildTerms(CurrentConfig.NumVarsLeft, CurrentConfig.NumConstsLeft, CurrentConfig.MaxAbsCoefficient, CurrentConfig.MaxAbsConstant, CurrentConfig.AreVariableCoefficientsOf1Allowed));

this._Terms.Add(new EqualSign());

this._Terms.AddRange(this.BuildTerms(CurrentConfig.NumVarsRight, CurrentConfig.NumConstsRight, CurrentConfig.MaxAbsCoefficient, CurrentConfig.MaxAbsConstant, CurrentConfig.AreVariableCoefficientsOf1Allowed));

The first step is to generate the terms on the left side, then it adds an EqualSign, and finally generates the terms on the right side.  The _Terms private member is defined as a List<IAmATerm>, so it can hold constants, variables, and the equal sign, alike.  The BuildTerms() method is as follows:

private List<IAmATerm> BuildTerms(int NumVars, int NumConsts, int MaxAbsCoefficient, int MaxAbsConstant, bool AreVariableCoefficientsOf1Allowed)
{
List<IAmATerm> ListOfTerms;
int CurrentVariableNumerator, CurrentConstantNumerator;

    ListOfTerms = new List<IAmATerm>();

    if(NumVars == 0 && NumConsts == 0)
{
ListOfTerms.Add(new Constant(0, 1));
}
else
{
for (int i = 0; i < NumVars; i++)
{
if(this._AreMultiplicationAndDivisionNeeded)
{
CurrentVariableNumerator = Random.Range(-1 * MaxAbsCoefficient, MaxAbsCoefficient);
if (CurrentVariableNumerator == 0) { CurrentVariableNumerator++; }
}
else
{
CurrentVariableNumerator = 1;
}

            if (!AreVariableCoefficientsOf1Allowed && CurrentVariableNumerator == 1) { CurrentVariableNumerator++; }

            ListOfTerms.Add(new Variable(CurrentVariableNumerator, 1, this._VariableLetter));
}

        for (int i = 0; i < NumConsts; i++)
{
CurrentConstantNumerator = Random.Range(-1 * MaxAbsConstant, MaxAbsConstant);
if((NumConsts + NumVars) > 1 && CurrentConstantNumerator == 0) { CurrentConstantNumerator++; }
ListOfTerms.Add(new Constant(CurrentConstantNumerator, 1));
}
}

    // TODO: Randomize the terms in the list

    if(ListOfTerms.Count > 0) { ListOfTerms[0].SetAsFirst(); }

    return ListOfTerms;
}

First, please note the use of “numerator” throughout this logic.  The code maintains the coefficients and constants in fractional form, rather than decimal.  I’ll discuss this at length in a later blog post.  Currently, BuildTerms will only generate whole-number coefficients and constants – that is, numbers with a denominator of “1”.

First, it checks to see if the number of variables and constants it is supposed to add to this side are both zero.  If so, it adds a single constant of “0” to the equation and skips to the end.  The main conditional branch generates the requested number of variables first:

  • For variables, it checks to see if multiplication/division are needed.  If not, the coefficient for the new variable is “1”.  If those operations are needed, it generates a number in the range [-MaxAbsCoefficient, MaxAbsCoefficient].  If it generates a “0”, it increments it to force it to be “1” instead (variables with a coefficient of “0” are never allowed).
  • Next, it checks to see if coefficients of “1” are even allowed.  If they are not, it will increment any “1” coefficients, making them “2”.
  • Finally, it adds a new Variable object to the list using the generated coefficient and the chosen variable letter (which was selected in the constructor).

For constants:

  • It generates the new numerator in the range [-MaxAbsConstant, MaxAbsConstant].
  • Constants of “0” are allowed if there is only 1 constant (and 0 variables) allowed on this side of the equation.  If there are supposed to be more than that, the new numerator is incremented to make it non-0.
  • Lastly, it generates a new Constant from the new numerator, and adds it to the list.

After all of the terms have been generated, the logic marks the first term in the list explicitly as “first”, allowing the UI to render the number correctly (something else that I will cover in a later post).

You’ll see by the TODO that I originally wanted to have the method shuffle the terms randomly, so that variables are not always listed before constants.  However, I encountered problems getting the randomized list to actually display in the UI randomized.  I fought with it for a bit, and then just moved on.  Another issue for the future.

You may also notice that I’m using a mix of values passed in as parameters to the method, and also accessing a private member of the class directly.  I need to pass in “NumVars” and “NumConsts” explicitly because I’m calling this method twice – once for the left side of the equation and once for the right.  The other values apply equally to both sides of the equation (no pun intended), so I had a choice of how to make them available to the method.  The hybrid approach is something else on my list of things to clean up.

In my next post, I’ll cover generating the operations that the player can apply to the equation.

Algebra Game – Configuring Complexity

From the beginning, CJ and I wanted the algebra game to have a series of levels.  The first level would show the player the simplest technique – combining like terms.  For example:

x = 3 + 4

Combining the “3” and the “4” into 7 would be enough to solve this equation.  The second and subsequent levels would teach the player additional techniques that he or she may need to solve more complicated equations, and then, of course, generate a series of problems that required that technique.

What this meant for the app itself was that it would need to be able to not only generate random equations, but equations that met certain, configurable parameters.  I created a class called “EquationConfig”:

using System;

[Serializable]
public class EquationConfig
{
public EquationConfig()
{
this.AreMultiplicationAndDivisionNeeded = true;
this.AreVariableCoefficientsOf1Allowed = true;
this.VariableLetterPool = VariableLetters.X;
}

    public class VariableLetters
{
public const string X = “x”;
public const string AnyValidLetterExceptX = “abcdfghjkmnpqrstuvwyz”;
public const string AnyValidLetter = “abcdfghjkmnpqrstuvwxyz”;
}

    public int NumVarsLeft;
public int NumVarsRight;
public int MaxAbsCoefficient;
public int NumConstsLeft;
public int NumConstsRight;
public int MaxAbsConstant;

    public bool AreMultiplicationAndDivisionNeeded;
public string VariableLetterPool;
public bool AreVariableCoefficientsOf1Allowed;
}

An EquationConfig object gets passed into the “Equation” class (which does the work of actually generating the new equation, based on the configuration passed in; I’ll go through this class in depth in my next post).

The core of EquationConfig are these properties:

  • NumVarsLeft – the number of variables to generate on the left side
  • NumVarsRight – the number of variables to generate on the right side
  • MaxAbsCoefficient – the maximum absolute value of the coefficients on each variable
  • NumConstsLeft – the number of constants to generate on the left side
  • NumConstsRight – the number of constants to generate on the right side
  • MaxAbsConstant – the maximum absolute value of each constant

Each equation will consist of some number of variables and constants on the left side of the equal side, and some number of each on the right side.  Each variable will have a randomly generated coefficient in the range [-MaxAbsCoefficient, MaxAbsCoefficient], except for 0.  Each constant will have a randomly generated value in the range [-MaxAbsConstant, MaxAbsConstant].  Constants of 0 are sometimes allowed (as we’ll see in the next post).

In addition to these, there are three more specialized properties of an EquationConfig:

  • AreMultiplicationAndDivisionNeeded: If this is true, then non-1 variable coefficients are allowed and operations involving multiplication and division are included in the tray; otherwise they aren’t.  These are needed to generate the lower levels where we want players to focus on learning to use addition and subtraction operations to solve equations.  Multiplication and division will come later.
  • VariableLetterPool: A string that determines what will be allowed for the variable – just “x”, most letters of the alphabet, or most letters of the alphabet except for “x”.  I say “most” letters of the alphabet because we initially left out the letters “o” and “i” – those looked too much like the numbers “0” and “1”.  Once we started playing, though, we quickly realized that the letters “e” and “i” should be left out as well, since those have special meaning in math (Euler’s number, and the square root of -1, respectively).  This is needed with the level that involves solving equations that have something other than “x” for the variable.
  • AreVariableCoefficientsOf1Allowed: If this is false, then all variables are required to have a non-1 coefficient.  This is used on the level that requires the player to use multiplication and division to simplify the variable.  Having something like “x = 2” right out of the gate would defeat that purpose.

There are combinations of settings in EquationConfig that are not valid, so Equation needs to vet the config passed in before using it.  For example, if multiplication/division are not needed, allowing more than one variable term on the left could lead to situations like this:

x + x = 2
2x = 2

Without a “divide by 2” operation, the player couldn’t solve this.

Here is the core validation logic, currently contained in the Equation constructor:

this._AreMultiplicationAndDivisionNeeded = CurrentConfig.AreMultiplicationAndDivisionNeeded;
if (!this._AreMultiplicationAndDivisionNeeded && CurrentConfig.NumVarsLeft > 1)
{
throw new EquationConfigurationException(EquationConfigurationException.InvalidStates.TooManyVariableTermsRequestedOnLeft);
}
if (!this._AreMultiplicationAndDivisionNeeded && CurrentConfig.NumVarsRight > 1)
{
throw new EquationConfigurationException(EquationConfigurationException.InvalidStates.TooManyVariableTermsRequestedOnRight);
}
if (!this._AreMultiplicationAndDivisionNeeded && CurrentConfig.NumVarsLeft > 0 && CurrentConfig.NumVarsRight > 0)
{
throw new EquationConfigurationException(EquationConfigurationException.InvalidStates.TooManyVariableTermsRequested);
}

this._VariableLetter = this.PickALetter(CurrentConfig.VariableLetterPool);

// Step 2. Build the equation here (covered in next blog post)

this.EnsureTheVariablesAreNotBalanced();

The invalid states are all around variables.  If multiplication/division are not needed:

  • make sure there is at most 1 variable on the left.
  • make sure there is at most 1 variable on the right.
  • make sure there is only 1 variable in the entire equation (don’t allow variables on both sides of the equal sign).

In most ways, the last check covers the first two.  However, having all three allows me to record a more specific error.

Next, we need to pick a letter to use as the variable in the new equation.

Next, build the left and right sides of the equation, based on the newly vetted configuration.  (I’ll cover this later.)

Finally, make sure the variables in the equation are NOT balanced.  One of the earlier iterations of the game generated an equation that looked like this:

x = x – 9

There’s no valid answer for the value of “x”.  The final step of the validation, then, is to ensure that the sum of the variable coefficients on the left does NOT equal the sum on the right.  If Equation random generates an equation where left and right are equal, it flips the sign of the last variable in the equation, so the above would become:

x = -x – 9

Which does have a solution.

In retrospect, this validation logic probably makes more sense as a method on EquationConfig (VerifyThyself, or something like it) than in the Equation constructor – a refactoring for the future.

These configuration options allowed CJ and I to define the initial five levels of the game as follows:

  • Level 1: Combining constants on the same side of the equal sign.  Shows the user that they can drag constants to combine them.  Requires a single variable with a coefficient of 1 on one side, and two constants on the other.
  • Level 2: Moving all of the constants to one side of the equal sign.  Introduces addition and subtraction operations, and teaches the player to apply those operations to both sides equally.  Requires a single variable with a coefficient of 1 on one side, and constants on both sides.
  • Level 3: Reducing the variable’s coefficient to 1.  Introduces multiplication and division operations, and teaches the player to apply those operations to every term on both sides.  Requires a variable on one side with a coefficient of something other than 1, and a constant on the other side.
  • Level 4: Reducing the number of constants, and reducing the variable’s coefficient.  Combines the previous lessons, and teaches the player that they sometimes need to perform multiple steps to solve the equation.
  • Level 5: Solving for variables other than “x”.  Similar to Level 4, except that now the variable in use is something other than “x”.

These level-configs are currently stored in a JSON file that gets loaded with the game, but the groundwork is laid for future versions to call a web service to pull them down.

In my next post, I’ll go through the logic that uses the configuration to actually generate an equation, including the edge cases it handles.

Giving the game a voice – TextBoxDribble

One day on a car trip, CJ and I were brainstorming about the algebra game, and my two daughters got into the act.  The three girls started working out a storyline for the game, and how there should be a guide of some sort that leads you through the levels.  The girls did some sketches, and I quickly threw one in to see what it would look like.

10

A lot of the game is (we think) intuitive, or is explained to you in the tutorials.  However, there are a handful of messages that can appear during gameplay, mostly around telling you when you tried to do something invalid (like applying an operation to only one side of the equation).  I thought it would be cool to have this wizardly-looking mentor fellow “speak” those feedback messages to the user.  To make that look a little more convincing, I wanted the message to “dribble” in, one word at a time, instead of having the full message appear over the image, to mimic someone speaking them.

TextBoxDribble

To do that, I wrote a script called TextBoxDribble.  As the name implies, this script will make the message you assign to a textbox appear to dribble in.  Here is the script, in its entirety:

using UnityEngine;
using UnityEngine.UI;

[RequireComponent(typeof(Text))]
public class TextBoxDribble : MonoBehaviour
{

[Tooltip(“Must be greater than 0; defaults to 1”)]
public int FramesBetweenEachWord = 1;

private string _PreviousValue;
private string _NewValue;
private string[] _Words;
private bool _ShouldWatchForChanges;
private bool _ShouldDribbleTheWordsIn;
private int _WordIndex;

private void Start()
{

this._ShouldWatchForChanges = true;
this._ShouldDribbleTheWordsIn = false;
this._WordIndex = 0;
this._PreviousValue = this.GetComponent<Text>().text;

        // Make sure FramesBetweenEachWord is always something valid;
if (FramesBetweenEachWord <= 0) { FramesBetweenEachWord = 1; }

}

void Update ()
{

if(this._ShouldWatchForChanges && !this._PreviousValue.Equals(this.GetComponent<Text>().text))
{

this._ShouldWatchForChanges = false;
this._NewValue = this.GetComponent<Text>().text;
this.GetComponent<Text>().text = “”;
this._Words = this._NewValue.Split(‘ ‘);
this._WordIndex = 0;
this._ShouldDribbleTheWordsIn = true;

}

if(this._ShouldDribbleTheWordsIn)
{

if (this._WordIndex < this._Words.Length)
{

if(Time.frameCount % FramesBetweenEachWord == 0)
{

this.GetComponent<Text>().text = string.Format(“{0} {1}”, this.GetComponent<Text>().text, this._Words[this._WordIndex]);
this._WordIndex++;

}

}
else
{

this._PreviousValue = this.GetComponent<Text>().text;
this._NewValue = “”;
this._ShouldDribbleTheWordsIn = false;
this._ShouldWatchForChanges = true;

}

}

}

}

}

The script’s Update() method has two main pieces.  The first conditional looks for changes to the textbox .text property.  When the textbox is assigned a new value, the script pulls it out, breaks it into an array of words, clears the textbox, and sets _ShouldDribbleTheWordsIn to true.

That signals the second block to go into action.  This block begins to reconstruct the new message, one word at a time.  Once the entire original value has been assigned, it goes back to watching for new changes.

The user can control how quickly the words appear through the public FramesBetweenEachWord property, which gets exposed in the Unity editor.  The name, hopefully, explains how it will work – the higher the value, the more time passes between each word getting appended, and the slower the overall phrase will come in.

Drag-and-drop, Event-ually

I mentioned in my previous Unity post that the logic powering the dragging and dropping on the equation screen was not straightforward to develop.  I had multiple false starts looking for a pattern that could support everything I needed it to:

* Dragging operations out of the OperationsTray and dropping them onto a domino to place it
* Dragging operations out of the OperationsTray or a domino, and dropping it onto the screen to delete it
* Dragging operations out of a domino to place it into another domino
* Dragging dominos to merge them with another
* Etc.

What I finally landed on was evaluating the Input.GetMouseButtonDown, Input.GetMouseButton, and Input.GetMouseButtonUp methods in the EquationScreenController.Update event handler.  (“Mouse button” here covers both clicking a mouse button as well as touching the screen with your finger.)  These methods return true when the user first clicks the mouse button to grab something, holds the mouse button down to move it, and then releases the button to drop it, respectively.

Once the user grabbed something, I needed logic to determine what they grabbed and were now dragging.  As they were dragging it, I had to refresh the UI so that the thing being dragged would actually track with their finger.  Once they dropped it, I had to evaluate if they dropped it on a valid spot or not, and do something in both cases.

My first working iteration of this required pages and pages of conditional logic – very ugly and hard to debug.  I needed a better way.  Luckily, history repeats itself – or is it “recurses”?

Years ago, when my colleagues and I were playing with the Microsoft Kinect, we started a project where the user would dress a paper doll on screen.  The user would “pick up” some article of clothing, move it over the doll, and then “drop it”.  “Pick up” in this game translated into “hold your hand over the article of clothing on the screen for a second or two until you convinced the software that you wanted THAT one”.  At that point, it would pop it off of the “wardrobe” and being to track with your hand.  “Drop it” translated into the user pushing their hand toward the Kinect, as if he or she were pushing a piece of paper onto a corkboard.

What that app taught me was the value in custom events.  The Kinect reports the three-dimensional position of 20 different joints for the user, 30 times a second.  I ended up writing a “manager” class that would translate the raw data coming off the Kinect into a series of events being raised – specifically things like hover, pick up a piece of clothing, drop a piece of clothing, etc.  That translation layer greatly simplified writing, and especially debugging, the rest of the app.

I decided to go the same route here.  Here is a slightly simplified version of the new EquationScreenController.Update() logic:

public void Update()
{

if (Input.GetMouseButtonDown(0))
{

this._DragDropManager.EvaluateInputDown(Input.mousePosition);

}
else if (Input.GetMouseButton(0))
{

this._DragDropManager.EvaluateInputMove(Input.mousePosition);

}
else if (Input.GetMouseButtonUp(0))
{

this._DragDropManager.EvaluateInputUp(Input.mousePosition);

}

}

The DragDropManager.Evaluate… methods would translate the raw UI input into a series of events to be raised:

  • OperationPickedUp
  • DominoPickedUp
  • OperationMoved
  • DominoMoved
  • DroppedOnNothing
  • DroppedOnDomino

My EquationScreenController, then, would handle those events and do all of the necessary UI steps.

This structure allowed me to not only abstract the messy details of the translation away, but it broke up the monolithic Update() method I had going into smaller, and much easier to debug, methods.

As a bonus, the refactored structure made it possible to unit-test the three DragDropManager methods.  I could set up various scenarios, and verify that the proper events were being raised given that input.

Staying on top of things

One of the first things I tackled for the algebra game was the drag-n-drop logic for the equation screen.  The path to that logic was not straightforward at all, and I’ll go into more detail about that in my next post.  One of the first oddities I encountered testing this was that the gameobject I was dragging didn’t always appear on top of everything else.  Sometimes it looked like my operation, for example, was sliding behind its siblings.

On the equation screen, there are two types of things that are draggable – operations:

10

And the terms of the equation, which appear in a UI control CJ dubbed “dominos”:

20

Initially, I found that the operation/domino being dragged would appear behind the operations/dominos to its right, along with several other controls rendered on the page.  After a bit, I realized what was happening.  Unity determines the z-order (depth) of the gameobjects on the screen in the order they appear in the Hierarchy tab.

30

Things that appear higher up in the hierarchy will be rendered visually behind things further down in the hierarchy.  When I populate the list of operations, I add them one after another to the OperationsPanel, which meant that the first one would appear visually behind the second, the second behind the third, and so on.

To solve this, as soon as I start dragging an operation or a domino, I re-parent it to the top-level canvas object, and then I make it the last sibling of the canvas:

this._ParentCanvas = transform.root.gameObject;
this._OperationBeingDragged.transform.SetParent(this._ParentCanvas.transform);
this._OperationBeingDragged.transform.SetAsLastSibling();

Since I can only drag one gameobject at a time, these two steps guarantee that the item being dragged will appear below every other gameobject in the hierachy, which means it will appear visually on top of everything else.  When I drop it onto a new container, I re-parent it to that container, which effectively resets it to the “proper” place in the hierarchy.

Unit-Testing in Unity

CJ and I are collaborating to build an app that teaches you how to solve single-variable algebraic equations like this:

2x – 4 = 16

Our target audiences are kids and teachers, so it was obvious to us that this would be a mobile app.  For me, the choice of development platform was also a no-brainer – Unity:

  • Katherine and I had tinkered with Unity a while ago, so I was already familiar with it
  • It can generate binaries for every platform we’re considering, and then some (XBox or PS4 anyone?)
  • One of the primary programming languages supported was C#
  • Unity integrates with Visual Studio (you can also use MonoDevelop, but if you know and love VS, why?)
  • Unity has a flourishing user community
  • I could get started for free

One of the very first things I figure out with any new development language / platform / IDE is how to build unit-tests with it.  As it turns out, Unity has a built-in test-runner based on NUnit (yet another point of familiarity for me).  The runner is available under the Window menu, but normally I leave it docked on the right side of the IDE.

10

Now the next step was figure out WHERE my tests should actually live.  After a little digging I found an article on the Unity Test Runner.  The test runner has two modes – Play and Edit.  I started with Edit, and (as the article shows), there was a button for “Created EditMode Test”.  I clicked it, and it created a folder called Assets/UnitTests/Editor.  I tried renaming and moving the folders in that string, and kept on eye on the test runner – would it find my test if I put it in a folder called this?.  As it turns out, the “Editor” folder has special meaning for Unity.  I settled on a folder structure of Assets/Editor/UnitTests (inverting what Unity created by default), so I could add other Editor scripts later if I needed.

20

Now that I had the basic structure, it was time to get down to business, and start writing the logic for the game.  Fast-forward several weeks and 450+ unit tests later, and I have a few observations about unit testing in Unity.

The integration between the Unity IDE and Visual Studio is fairly seamless.  I can add a new test in the latter, switch back to the former (where it kicks off a compilation in the background automatically), and see it appear a second or two later.  In Studio, there is a preset button for attaching to Unity, allowing me to debug through my test and business logic easily.  The one minor quirk is how Unity reports compilation errors – subtly, at the very bottom of the Unity UI:

30

This stung me a couple of times – I would make some typographical error, miss this clue that my new code wasn’t actually compiling, and re-ran my unit tests expecting a change, only to find that the same test was failing because my new code wasn’t actually being used yet.  To avoid this, I now do an explicit compilation in Studio, and let Studio throw up the errors and warnings I’m used to seeing.

As a whole, the test runner does its job reliably.  One thing I’d change, though, is how it reports the current state of the tests.  If you’ve already run your test suite once, and want to run it again, Unity doesn’t clear all of the green (and red) flags for the already-run tests.  Unless something switches from red to green or vice versa, you really don’t have any indication which test is being run at the moment, how far it is through the suite, etc.  There IS a progress bar that appears while the tests are being executed:

40

And once that disappears you know it’s done, but I’d prefer the test statuses to be cleared before each run so I can watch it work through the suite again, fresh.

I’ve also noticed that the tests run much faster if you keep the focus on Unity the whole time.  More than once I would kick off a series of tests, and switch to some other window while they ran, and found my tests were still running long beyond when they should have (I’ve seen suites take 2-7 times longer to complete this way).  Again, a relatively easy issue to avoid – don’t switch away from Unity while the tests are running – but an odd one.

Overall, I’m very happy with my choice of Unity.