A C# App From Start to Finish – Part 2

Continued

So we have a repository containing our 3rd party tools, build scripts, and two empty C# class library projects. Now we can write some code.

Commit 5 – A Class (and Test Fixture)

A logical starting point seems like coming up with a model for our puzzle. What classes would best represent our puzzle? Tile comes to mind, but what would it contain? An image? Are we sure we always want to use images for tiles? What about numbers?

I’m not so sure about a Tile class yet, lets try thinking more top-down. A Game class? On second thought I’m not 100% sure how I’d want a game to go at the moment. Do we want one game to be one puzzle? Do we want to solve many, successively more difficult puzzles as a part of one game?

A Puzzle class seems like the right place to start. This represents a single sliding tile puzzle. One game type could use just one puzzle, another game type could use lots of them. I’ll start by adding a ‘PuzzleTestFixture’ class to my test project and consider some tests.

There are a few things this class needs to do:

  • Allow the tiles in the puzzle to be defined.
  • Allow tiles to be moved around in a legal fashion.
  • Work out if the puzzle is solved.

Let’s consider the first point – how do we define the tiles in the puzzle? I guess for now, we could just accept Objects as the tiles. All objects support an equality comparison which is all we really need to determine if a puzzle is solved. We won’t bother with generics as I’m not sure there’ll be much point. If it seems to make sense later we can always change it.

Now – how should we supply the tiles to the Puzzle class? Do the tiles themselves change while a puzzle’s being played? Not normally. Therefore, the constructor would be a good place to pass them in. How should we pass them in? Well, the puzzles are typically 2D (although they don’t necessarily have to be at this point), so we’ll accept a 2D array (ahem). Let’s write some tests.

We need to consider which values are valid to pass in the the constructor:

  • Is null a valid value?
  • What about an empty 2D array?
  • What about a 2D array which contains a null?
  • What about a 2D array which contains non-unique items?

All these things should be considered. For each one, a test case needs to be implemented. Then the corresponding code needs to be written in the class under test to make the code compile and the test pass. Code coverage can be used to indicate any possible missing tests or unneeded code. I end up with these tests to start with:

  1. public class PuzzleTestFixture
  2. {
  3.     [Test, TestCaseSource("GetConstructorTestCases")]
  4.     public void TestConstructor(Object[,] tiles)
  5.     {
  6.         new Puzzle(tiles);
  7.     }
  8.  
  9.     public IEnumerable<ITestCaseData> GetConstructorTestCases()
  10.     {
  11.         yield return new TestCaseData(null)
  12.             .Throws(typeof(ArgumentNullException))
  13.             .SetName("When we pass null to the constructor then an ArgumentNullException is thrown");
  14.  
  15.         yield return new TestCaseData(new Object[,] { })
  16.             .Throws(typeof(ArgumentException))
  17.             .SetName("When we pass a 0x0 tile array to the constructor then an ArgumentException is thrown");
  18.  
  19.         yield return new TestCaseData(new Object[,] { { 1 }, { 2 } })
  20.             .Throws(typeof(ArgumentException))
  21.             .SetName("When we pass a 2×1 tile array to the constructor then an ArgumentException is thrown");
  22.  
  23.         yield return new TestCaseData(new Object[,] { { 1, 2 }, { 3, null } })
  24.             .Throws(typeof(ArgumentException))
  25.             .SetName("When we pass a tile array which contains a null to the constructor then an ArgumentException is thrown");
  26.  
  27.         yield return new TestCaseData(new Object[,] { { 1, 2 }, { 3, 1 } })
  28.             .Throws(typeof(ArgumentException))
  29.             .SetName("When we pass a tile array which contains duplicate tiles to the constructor then an ArgumentException is thrown");
  30.  
  31.         yield return new TestCaseData(new Object[,] { { 1, 2 }, { 3, 4 } })
  32.             .SetName("When we pass a valid tile array to the constructor then an instance of Puzzle is created successfully");
  33.     }
  34. }

Note I’m using NUnit’s TestCaseSource attribute so I can focus on test cases for the constructor. This also gives us an oppurtunity to give meaningful names to the test cases. I like the when…then… format, as you can see.

My implementation of Puzzle which satisfies these test cases is:

  1. public class Puzzle
  2. {
  3.     public Puzzle(Object[,] tiles)
  4.     {
  5.         if (tiles == null)
  6.         {
  7.             throw new ArgumentNullException("tiles");
  8.         }
  9.         if (tiles.GetLength(0) < 2 || tiles.GetLength(1) < 2)
  10.         {
  11.             throw new ArgumentException("tiles must be an array of at least 2 x 2", "tiles");
  12.         }
  13.         if (tiles.Cast<Object>().Any(tile => tile == null))
  14.         {
  15.             throw new ArgumentException("tiles cannot contain any null tiles", "tiles");
  16.         }
  17.  
  18.         var numberOfTiles = tiles.GetLength(0) * tiles.GetLength(1);
  19.         var numberOfUniqueTiles = tiles.Cast<Object>().Distinct().Count();
  20.  
  21.         if (numberOfTiles != numberOfUniqueTiles)
  22.         {
  23.             throw new ArgumentException("tiles cannot contain duplicate tiles", "tiles");
  24.         }
  25.     }
  26. }

OpenCover and ReportGenerator tell us that I’ve got 100% code coverage, so this seems like a good time to commit.

  1. C:\_hg\slidy\build [default ~2 ?2 tip]> hg add
  2. adding ..\Slidy\ShyAlex.Tiles.Tests\PuzzleTestFixture.cs
  3. adding ..\Slidy\ShyAlex.Tiles\Puzzle.cs
  4.  
  5. C:\_hg\slidy\build [default +2 ~2 tip]> hg st
  6. M Slidy\ShyAlex.Tiles.Tests\ShyAlex.Tiles.Tests.csproj
  7. M Slidy\ShyAlex.Tiles\ShyAlex.Tiles.csproj
  8. A Slidy\ShyAlex.Tiles.Tests\PuzzleTestFixture.cs
  9. A Slidy\ShyAlex.Tiles\Puzzle.cs
  10.  
  11. C:\_hg\slidy\build [default +2 ~2 tip]> hg com -m "Added Puzzle class with constructor and PuzzleTestFixture tests."
  12.  
  13. C:\_hg\slidy\build [default tip]>

Commit 6 – Observing the Tiles

So far, our Puzzle class is little more than a validator for a 2D array. Lets extend its functionality by allowing consumers to look at the tiles it contains. There are a few ways we could do this – the easiest would be to store the 2D array in a member variable and expose it via a property. However, 2D arrays are mutable, and we don’t really want to encourage modification of the array whilst our Puzzle class owns it. We can do two things to avoid this – one is to copy the contents of the array in the constructor to a collection Puzzle owns, and to expose the tiles as something other than a 2D array. There’s not a great deal we can do to stop consumers from altering the tiles themselves. We could force the tiles we accept to be ICloneable – but even then we’re relying on a correct Clone implementation.

At this point I feel very much like I’m overthinking this. A balance needs to be struck between stopping a user of the API from shooting themselves in the foot, and going to ridiculous lengths to stop an error that no-one’s never going to encounter. I think I’ll settle on storing the 2D array from the constructor in a member variable, and expose the array via an implementation of IEnumerable. My test looks like this:

  1. [Test]
  2. public void WhenWeConstructAPuzzleWithValidTilesThenWeCanAccessTheCorrectTiles()
  3. {
  4.     var tiles = new Object[,]
  5.     {
  6.         { 1, 2, 3 },
  7.         { 4, 5, 6 },
  8.         { 7, 8, 9 }
  9.     };
  10.  
  11.     var puzzle = new Puzzle(tiles);
  12.            
  13.     CollectionAssert.AreEqual(new Object[][]
  14.     {
  15.         new Object[] { 1, 2, 3 },
  16.         new Object[] { 4, 5, 6 },
  17.         new Object[] { 7, 8, 9 }
  18.     }, puzzle);
  19. }

Unfortunately, an Object[,] won’t work like an IEnumerable<IEnumerable<Object>>, so we have to duplicate the tiles when comparing with the Puzzle instance in a jagged array. It’s not too bad of a hack though. I give rectangular arrays a mental -1 and press on.

I annotate my Puzzle class declaration with the interface I’ll implement – IEnumerable<IEnumerable<Object>> and add code to implement that interface.

  1. public class Puzzle : IEnumerable<IEnumerable<Object>>
  2. {
  3.     private readonly Object[,] tiles;
  4.  
  5.     public Puzzle(Object[,] tiles)
  6.     {
  7.         [Parameter checking for tiles parameter]
  8.         this.tiles = tiles;
  9.     }
  10.  
  11.     public IEnumerator<IEnumerable<Object>> GetEnumerator()
  12.     {
  13.         for (var x = 0; x < tiles.GetLength(0); x++)
  14.         {
  15.             yield return GetEnumeratorForColumn(x);  
  16.         }
  17.     }
  18.  
  19.     private IEnumerable<Object> GetEnumeratorForColumn(Int32 x)
  20.     {
  21.         for (var y = 0; y < tiles.GetLength(1); y++)
  22.         {
  23.             yield return tiles[x, y];
  24.         }
  25.     }
  26.  
  27.     IEnumerator IEnumerable.GetEnumerator()
  28.     {
  29.         return ((IEnumerable<IEnumerable<Object>>)this).GetEnumerator();
  30.     }
  31. }

I build, test and produce the coverage reports – it shows the new methods are covered, so I commit.

  1. C:\_hg\slidy [default ~2 tip]> cd build
  2. C:\_hg\slidy\build [default ~2 tip]> .\build;.\test;.\report;reports\index.htm
  3.  
  4. [build/test/report output]
  5.  
  6. C:\_hg\slidy\build [default ~2 tip]> hg com -m "Puzzle now implements IEnumerable<IEnumerable<Object>> and we can correctly retreive the tiles."

Commit 7 – Moving Tiles

We have an initial definition of a puzzle, but we can’t do anything with it – we can’t slide the tiles around. So let’s consider how we’d do that. The puzzle in the real world always has one gap, and we simply slide an adjacent tile into that gap. So I guess all we need to know is which tile the user wants to slide into the gap.

We’re getting ahead of ourselves though – our puzzle doesn’t have a gap at the moment! Let’s fix that. What can we use to represent the gap, or empty tile? We could perhaps use null? That could work, but I reckon we’re gonna want to compare quite a few tiles, and those null checks might become a pain. I think I’ll try using a special Object for the empty tile and see where that gets me.

Should we expect the user to pass in the empty tile in their 2D array? That would give us more validation to do in the constructor. Maybe it’s easier for now just to replace one of the tiles in the passed in array. That would mean making a copy of the array, as I consider modifying a passed in variable to be fairly bad form. I’ll choose the first option, as it allows puzzles to choose which tile is missing. If we went for option 2 and decided to always remove the bottom right tile, and we were using an image for the tiles where significant detail was in the bottom right, we wouldn’t really be making the best decision.

I’ll start by updating some tests. These are the updated constructor test cases:

  1. yield return new TestCaseData(new Object[,] { { 1, 2 }, { 3, 4 } })
  2.     .Throws(typeof(ArgumentException))
  3.     .SetName("When we pass a tile array to the constructor that doesn't contain an empty tile then an ArgumentException is thrown");
  4.  
  5. yield return new TestCaseData(new Object[,] { { 1, 2 }, { 3, Puzzle.EmptyTile } })
  6.     .SetName("When we pass in a large enough tile array to the constructor which contains one empty tile then the Puzzle is constructed successfully");

The updated IEnumerable test is:

  1. var tiles = new Object[,]
  2. {
  3.     { 1, 2, 3 },
  4.     { 4, 5, 6 },
  5.     { 7, 8, Puzzle.EmptyTile }
  6. };
  7.  
  8. var puzzle = new Puzzle(tiles);
  9.            
  10. CollectionAssert.AreEqual(new Object[][]
  11. {
  12.     new Object[] { 1, 2, 3 },
  13.     new Object[] { 4, 5, 6 },
  14.     new Object[] { 7, 8, Puzzle.EmptyTile }
  15. }, puzzle);

I attempt a build, add Puzzle.EmptyTile, then build and test to see the following:

  1. C:\_hg\slidy [default ~2 tip]> cd build
  2. C:\_hg\slidy\build [default ~2 tip]> .\build
  3.  
  4. [build output]
  5.  
  6. C:\_hg\slidy\build [default ~2 tip]> .\test
  7. NUnit version 2.5.10.11092
  8. Copyright (C) 2002-2009 Charlie Poole.
  9. Copyright (C) 2002-2004 James W. Newkirk, Michael C. Two, Alexei A. Vorontsov.
  10. Copyright (C) 2000-2002 Philip Craig.
  11. All Rights Reserved.
  12.  
  13. Runtime Environment –
  14.    OS Version: Microsoft Windows NT 6.1.7601 Service Pack 1
  15.   CLR Version: 2.0.50727.5448 ( Net 2.0 )
  16.  
  17. ProcessModel: Default    DomainUsage: Single
  18. Execution Runtime: Default
  19. ……F..
  20. Tests run: 8, Errors: 0, Failures: 1, Inconclusive: 0, Time: 0.4860166 seconds
  21.   Not run: 0, Invalid: 0, Ignored: 0, Skipped: 0
  22.  
  23. Errors and Failures:
  24. 1) Test Failure : ShyAlex.Tiles.Tests.PuzzleTestFixture.When we pass a tile array to the constructor that doesn't contain an emp
  25. ty tile then an ArgumentException is thrown
  26.    System.ArgumentException was expected
  27.  
  28. Committing….
  29. Visited Classes 3 of 3 (100)
  30. Visited Methods 6 of 6 (100)
  31. Visited Points 39 of 39 (100)
  32. Visited Branches 22 of 26 (84.6153846153846)
  33.  
  34. ==== Alternative Results (includes all methods including those without corresponding source) ====
  35. Alternative Visited Classes 3 of 3 (100)
  36. Alternative Visited Methods 12 of 20 (60)
  37. C:\_hg\slidy\build [default ~2 tip]>

Our constructor is not throwing when we don’t supply an empty tile. Let’s fix that. The constructor and member variables of Puzzle are now:

  1. public static readonly Object EmptyTile = new Object();
  2.  
  3. private readonly Object[,] tiles;
  4.  
  5. public Puzzle(Object[,] tiles)
  6. {
  7.     if (tiles == null)
  8.     {
  9.         throw new ArgumentNullException("tiles");
  10.     }
  11.     if (tiles.GetLength(0) < 2 || tiles.GetLength(1) < 2)
  12.     {
  13.         throw new ArgumentException("tiles must be an array of at least 2 x 2", "tiles");
  14.     }
  15.     if (tiles.Cast<Object>().Any(tile => tile == null))
  16.     {
  17.         throw new ArgumentException("tiles cannot contain any null tiles", "tiles");
  18.     }
  19.  
  20.     var numberOfTiles = tiles.GetLength(0) * tiles.GetLength(1);
  21.     var numberOfUniqueTiles = tiles.Cast<Object>().Distinct().Count();
  22.  
  23.     if (numberOfTiles != numberOfUniqueTiles)
  24.     {
  25.         throw new ArgumentException("tiles cannot contain duplicate tiles", "tiles");
  26.     }
  27.     if (tiles.Cast<Object>().Count(tile => tile == EmptyTile) != 1)
  28.     {
  29.         throw new ArgumentException("tiles must contain a single reference to Puzzle.EmptyTile", "tiles");
  30.     }
  31.  
  32.     this.tiles = tiles;
  33. }

I build, test, and produce coverage reports. I’m satisfied with the test passes and 100% code coverage, so I commit.

  1. C:\_hg\slidy\build [default ~2 tip]> .\build;.\test;.\report;reports\index.htm
  2.  
  3. [Build, test, and report output]
  4.  
  5. C:\_hg\slidy\build [default ~2 tip]> hg com -m "Arrays used to construct a puzzle instance must now containg a single reference to Puzzle.EmptyTile."

Commit 8 – Really Moving Tiles

Now we have a space to move tiles into, we can focus on providing a method for doing it. As mentioned earlier, all we need to know from the user is which tile they want to slide into the gap – we can add a method which accepts a single Object parameter representing the tile they want to move.

What do we do if they want to move an invalid tile? This to me seems like it would be quite a common case – something that we expect the user might try if they’re clicking on GUI controls. Because of that, I wouldn’t really say it was an exceptional case – it wouldn’t really be a misuse of the API to try and move a tile that can’t be moved. For that reason, I think returning a Boolean from the method indicating whether or not the tile was moved would be appropriate. Based on that, a method name like TryMove(Object tile) would perhaps indicate the purpose of the method best.

Here are some test cases:

  1. [Test, TestCaseSource("GetTryMoveTestCases")]
  2. public Boolean TestTryMove(Puzzle puzzle, Object tile, Object[][] expectedTiles)
  3. {
  4.  var actualResult = puzzle.TryMove(tile);
  5.  CollectionAssert.AreEqual(expectedTiles, puzzle);
  6.  return actualResult;
  7. }
  8.  
  9. public IEnumerable<ITestCaseData> GetTryMoveTestCases()
  10. {
  11.  var e = Puzzle.EmptyTile;
  12.  var noChangeToTiles = new Object[][]
  13.  {
  14.   new Object[] { 1, 2, 3 },
  15.   new Object[] { 4, e, 5 },
  16.   new Object[] { 6, 7, 8 }
  17.  };
  18.  Func<Puzzle> getInitialPuzzle = () => new Puzzle(
  19.   new Object[,]
  20.   {
  21.    { 1, 2, 3 },
  22.    { 4, e, 5 },
  23.    { 6, 7, 8 }
  24.   });
  25.  
  26.  yield return new TestCaseData(getInitialPuzzle(), null, noChangeToTiles)
  27.   .Returns(false)
  28.   .SetName("When we try to move a null tile then nothing happens");
  29.  
  30.  yield return new TestCaseData(getInitialPuzzle(), e, noChangeToTiles)
  31.   .Returns(false)
  32.   .SetName("When we try to move the empty tile then nothing happens");
  33.  
  34.  yield return new TestCaseData(getInitialPuzzle(), 1, noChangeToTiles)
  35.   .Returns(false)
  36.   .SetName("When we try to move a tile above and to the left of the empty tile then nothing happens");
  37.  
  38.  yield return new TestCaseData(getInitialPuzzle(), 3, noChangeToTiles)
  39.   .Returns(false)
  40.   .SetName("When we try to move a tile above and to the right of the empty tile then nothing happens");
  41.  
  42.  yield return new TestCaseData(getInitialPuzzle(), 6, noChangeToTiles)
  43.   .Returns(false)
  44.   .SetName("When we try to move a tile below and to the left of the empty tile then nothing happens");
  45.  
  46.  yield return new TestCaseData(getInitialPuzzle(), 8, noChangeToTiles)
  47.   .Returns(false)
  48.   .SetName("When we try to move a tile below and to the right of the empty tile then nothing happens");
  49.  
  50.  yield return new TestCaseData(getInitialPuzzle(), 2, new Object[][]
  51.  {
  52.   new Object[] { 1, e, 3 },
  53.   new Object[] { 4, 2, 5 },
  54.   new Object[] { 6, 7, 8 }
  55.  }).Returns(true)
  56.  .SetName("When we try to move a tile directly above the empty tile then it moves successfully");
  57.  
  58.  yield return new TestCaseData(getInitialPuzzle(), 4, new Object[][]
  59.  {
  60.   new Object[] { 1, 2, 3 },
  61.   new Object[] { e, 4, 5 },
  62.   new Object[] { 6, 7, 8 }
  63.  }).Returns(true)
  64.  .SetName("When we try to move a tile directly to the left of the empty tile then it moves successfully");
  65.  
  66.  yield return new TestCaseData(getInitialPuzzle(), 5, new Object[][]
  67.  {
  68.   new Object[] { 1, 2, 3 },
  69.   new Object[] { 4, 5, e },
  70.   new Object[] { 6, 7, 8 }
  71.  }).Returns(true)
  72.  .SetName("When we try to move a tile directly to the right of the empty tile then it moves successfully");
  73.  
  74.  yield return new TestCaseData(getInitialPuzzle(), 7, new Object[][]
  75.  {
  76.   new Object[] { 1, 2, 3 },
  77.   new Object[] { 4, 7, 5 },
  78.   new Object[] { 6, e, 8 }
  79.  }).Returns(true)
  80.  .SetName("When we try to move a tile directly below the empty tile then it moves successfully");
  81. }

And now the code to get those test cases to pass.

  1. public Boolean TryMove(Object tile)
  2. {
  3.  if (tile == null || tile == EmptyTile)
  4.  {
  5.   return false;
  6.  }
  7.  if (!AreTilesAdjacent(tile, EmptyTile))
  8.  {
  9.   return false;
  10.  }
  11.  
  12.  SwapTiles(tile, EmptyTile);
  13.  return true;
  14. }
  15.  
  16. private Boolean AreTilesAdjacent(Object tile1, Object tile2)
  17. {
  18.  Int32 x1, y1, x2, y2;
  19.  IndexOf(tile1, out x1, out y1);
  20.  IndexOf(tile2, out x2, out y2);
  21.  var dx = Math.Abs(x2 x1);
  22.  var dy = Math.Abs(y2 y1);
  23.  return dx + dy <= 1;
  24. }
  25.  
  26. private void SwapTiles(Object tile1, Object tile2)
  27. {
  28.  Int32 x1, y1, x2, y2;
  29.  IndexOf(tile1, out x1, out y1);
  30.  IndexOf(tile2, out x2, out y2);
  31.  var tempTile = tiles[x1, y1];
  32.  tiles[x1, y1] = tiles[x2, y2];
  33.  tiles[x2, y2] = tempTile;
  34. }
  35.  
  36. private void IndexOf(Object tile, out Int32 x, out Int32 y)
  37. {
  38.  var query = from tx in Enumerable.Range(0, tiles.GetLength(0))
  39.     from ty in Enumerable.Range(0, tiles.GetLength(1))
  40.     where tiles[tx, ty].Equals(tile)
  41.     select new { X = tx, Y = ty };
  42.  
  43.  var result = query.Single();
  44.  x = result.X;
  45.  y = result.Y;
  46. }

As a side note, doing cartesian products is one of the few places I prefer LINQ query comprehension syntax over using extension methods directly. The equivalent SelectMany syntax is pretty nasty.

Running a build/test/report cycle shows that the tests pass and I have full code coverage, so I commit.

  1. C:\_hg\slidy\build [default ~2 tip]> hg com -m "Added TryMove method allowing tiles to be moved around."

Commit 9 – A Quick Refactor

Looking at Puzzle, I can see at least one method implemented in the last commit that would perhaps be better suited somewhere more generic. It’s the IndexOf method. That method could be used to get the indices of any item in any rectangular array. Of course, we can’t change the methods defined for an array as we don’t own the source, but we can define an extension method which is nearly as good as.

I add a new class named ArrayExtensions and move IndexOf to it. I take the chance to rename the method. As we’re getting an x and y index, I rename it to GetIndicesOf and update the call sites in Puzzle.

  1. public static class ArrayExtensions
  2. {
  3.     public static void GetIndicesOf<T>(this T[,] array, T item, out Int32 x, out Int32 y)
  4.     {
  5.         var query = from tx in Enumerable.Range(0, array.GetLength(0))
  6.                     from ty in Enumerable.Range(0, array.GetLength(1))
  7.                     where array[tx, ty].Equals(item)
  8.                     select new { X = tx, Y = ty };
  9.  
  10.         var result = query.Single();
  11.         x = result.X;
  12.         y = result.Y;
  13.     }
  14. }

A build/test/report cycle reveals we still have 100% coverage, even if we don’t have any tests which are focussed on GetIndicesOf. It’s good enough for me, so I commit.

That’s enough for this article, next time we’ll look at defining what it means for a puzzle to be solved and unsolved.

Share and Enjoy:
  • Print
  • Digg
  • StumbleUpon
  • del.icio.us
  • Facebook
  • Yahoo! Buzz
  • Twitter
  • Google Bookmarks
  • email
  • LinkedIn
  • Technorati

Leave a Reply

Your email address will not be published. Required fields are marked *

 

This site uses Akismet to reduce spam. Learn how your comment data is processed.