Single Action Controllers with ASP.Net MVC

On April 29, 2011, in Uncategorized, by derekgreer

While I’ve enjoyed working with the ASP.Net MVC Framework, one thing I wish that it provided is the ability to create controller-less actions. Why you ask? Because I’d rather my controllers only have one reason to change. While this isn’t provided out-of-the-box with ASP.Net MVC, single action controllers can be facilitated pretty easy with ASP.Net Routing.

Let’s say that we have a single CustomerController that has several actions for adding and retrieving customers which we’d like to split up. We could simply move these actions to separate controllers and register routes for each action to maintain the existing URL structure, but this could pose a maintenance issue down the road if you end up with many small controllers. There are some clever things we could do with dependency injection to provide a pretty nice solution, but I’m going to show you a really simple way of achieving this without a lot of extra wiring.

First, create a new class which extends System.Web.Routing.Route. Next, override the GetRouteData() method and paste in the following code:

public override RouteData GetRouteData(HttpContextBase httpContext) { RouteData routeData = base.GetRouteData(httpContext); if (routeData != null) { var controller = routeData.Values["controller"] as string; var action = routeData.Values["action"] as string; string controllerAction = string.Format("{0}{1}", controller, action); routeData.Values["controller"] = controllerAction; routeData.Values["action"] = "Execute"; } return routeData; }

Next, define a new route using your new custom Route class. To match the registration API provided by the framework, you can use the following extension method:

public static class RouteCollectionExtensions { public static Route MapActionRoute(this RouteCollection routes, string name, string url, object defaults, object constraints) { var route = new ActionRoute(url, new MvcRouteHandler()) { Defaults = new RouteValueDictionary(defaults), Constraints = new RouteValueDictionary(constraints) }; routes.Add(name, route); return route; } }

With this in place, the path “~/Customer/Get/1” will route to the following controller:

public class CustomerGetController : Controller { public ActionResult Execute(string id) { var customer = CustomerRepository.Get(id); return View("CustomerView", customer); } }

To see a full working example, I’ve provided a source-only NuGet package here. If you have the NuGet Command Line tool, open up a shell and run the following where you want the source:

>nuget install ActionControllerExampleSource

If you have NuGet.exe in your path, you should be able to just build the solution. Otherwise, you’ll need to edit the batch file in the script directory to set the NuGet path so it can grab a few dependencies it uses. Enjoy.

Tagged with:  

In part 7 of our series, we finished the initial implementation of our Tic-tac-toe component. After we finished, a team in charge of creating a host application was able to get everything integrated (though rumor has it that there was a bit of complaining) and the application made its way to the QA team for some acceptance testing. Surprisingly, there were several issues reported that got assigned to us. Here are the issues we’ve been assigned:

Issue Description Owner
Defect The player can always win by choosing positions 1, 5, 2, and 8. The game should prevent the player from winning. QA Team
Defect The game throws an InvalidOperationException when choosing positions 1, 2, 5, and 9 QA Team
Defect The game makes a move after the player wins QA Team
Defect After letting the game win by choosing positions 4, 7, 8, and 6, choosing the last position of 3 throws an InvalidOperationException QA Team
Defect When trying to let the game win by choosing positions 1, 7, and 8, the game chose positions 4, 5, and 9 instead of completing the winning sequence 4, 5, 6. QA Team
New Feature Add a method to the Game class for retrieving the last position selected by the game. GUI Team
New Feature Please modify the ChoosePosition method to throw exceptions for errors rather than returning strings. Additionally, please provide an event we can subscribe to when one of the players wins or when there is a draw. GUI Team

As you may have discerned by now, following Test-Driven Development doesn’t ensure the code we produce will be free of errors. It does, however, ensure that our code meets the executable specifications we create to guide the application’s design and aids in the creation of code that’s maintainable and relevant (that is, to the extent we adhere to Test-Driven Development methodologies). Of course, the Test-Driven Development process is a framework into which we pour both our requirements and ourselves. The quality of both of these ingredients certainly affects the overall outcome. As we become better at gathering requirements, translating these requirements into executable specifications, identifying simple solutions and factoring out duplication, our yield from the TDD process will increase.

Since our issues have no particular priority assigned, let’s read through them before we get started to see if any make sense to tackle first. Given a couple of the issues pertain to changes to the API, it might be best to address these first to minimize the possibility of writing new tests that could need to be modified later.

The first of these issues pertain to adding a new method. Here’s the issue:

Issue Description Owner
New Feature Add a method to the Game class for retrieving the last position selected by the game. GUI Team

Upon integrating our component, the GUI team discovered there wasn’t an easy way to tell what positions the game was selecting in order to reflect this to the user. They were able to use techniques similar to those we used in the course of implementing our tests, but they didn’t consider this to be a very friendly API. While such an oversight may seem obvious within the context of the entire application, such issues occur when components are development in isolation. Fundamentally, the problem wasn’t with the Test-Driven Development methodologies we were following, but with the scope in which we were applying them. Later in our series, we’ll discuss an alternative to the approach we’ve taken with this effort that can help avoid misalignments such as this. Until then, we’ll address these issues the best we can with our existing approach.

To address this issue, we’ll create a new test that describes the behavior for the requested method:

[TestClass] public class When_retrieving_the_last_selected_position_for_the_game { [TestMethod] public void it_should_return_the_last_position() { } }

As our method of validation, we’ll set up our assertion to verify that some expected position was selected:

[TestClass] public class When_retrieving_the_last_selected_position_for_the_game { [TestMethod] public void it_should_return_the_last_position() { Assert.AreEqual(1, selection); } }

Next, let’s choose what our API is going to look like and then set up the context of our test. Let’s call our new method GetLastChoiceBy(). We can make use of our existing Player enumeration as the parameter type:

[TestClass] public class When_retrieving_the_last_selected_position_for_the_game { [TestMethod] public void it_should_return_the_last_position() { Game game = new Game(new GameAdvisorStub(new [] { 1 })); game.GoFirst(); var selection = game.GetLastChoiceBy(Player.Game); Assert.AreEqual(1, selection); } }

Next, let’s add the new method to our Game class so this will compile:

public class Game { // snip public int GetLastChoiceBy(Player player) { return 0; } }

Now we’re ready to run the tests:

 
When_retrieving_the_last_selected_position_for_the_game Failed it_should_return_the_last_position Assert.AreEqual failed. Expected:<1>. Actual:<0>.

We can make the test pass by just returning a 1:

public int GetLastChoiceBy(Player player) { return 1; }
 

 

Now, we’ll refactor the method to retrieve the value from a new dictionary field which we’ll set in the SelectAPositionFor() method:

public class Game { readonly Dictionary<Player, char> _tokenAssignments = new Dictionary<Player, char>(); ... void SelectAPositionFor(Player player) { int recommendedPosition = _advisor.WithLayout(new string(_layout)).SelectBestMoveForPlayer(GetTokenFor(player)); _layout[recommendedPosition - 1] = GetTokenFor(player); _lastPositionDictionary[player] = recommendedPosition; } public int GetLastChoiceBy(Player player) { return _lastPositionDictionary[player]; } }
 

 

That was fairly simple. On to our next feature request:

Issue Description Owner
New Feature Please modify the ChoosePosition method to throw exceptions for errors rather than returning strings. Additionally, please provide an event we can subscribe to when one of the players wins or when there is a draw. GUI Team

This is slightly embarrassing. While we’ve been striving to guide the design of our public interface from a consumer’s perspective, we seem to have made a poor choice in how the game communicates errors and the concluding status of the game. If our Game class had evolved within the context of the consuming application, perhaps we would have seen these choices in a different light.

Let’s go ahead and get started on the first part of the request which involves changing how errors are reported. First, let’s take inventory of our existing tests which relate to reporting errors. We have two tests which pertain to error conditions:

[TestClass] public class When_the_player_attempts_to_select_an_invalid_position { [TestMethod] public void it_should_tell_the_player_the_position_is_invalid() { var game = new Game(); string message = game.ChoosePosition(10); Assert.AreEqual("That spot is invalid!", message); } }

[TestClass] public class When_the_player_attempts_to_select_an_occupied_position { [TestMethod] public void it_should_tell_the_player_the_position_is_occupied() { var game = new Game(new GameAdvisorStub(new[] { 1, 4, 7 })); game.ChoosePosition(2); string message = game.ChoosePosition(1); Assert.AreEqual("That spot is taken!", message); } }

Starting with the first method, let’s modify it to check that an exception was thrown:

[TestClass] public class When_the_player_attempts_to_select_an_invalid_position { [TestMethod] public void it_should_tell_the_player_the_position_is_invalid() { var game = new Game(); string message = game.ChoosePosition(10); Assert.AreEqual("The position '10' was invalid.", exception.Message); } }

Next, let’s wrap the call to the ChoosePosition() method with a try/catch block. We’ll call our exception an InvalidPositionException:

[TestClass] public class When_the_player_attempts_to_select_an_invalid_position { [TestMethod] public void it_should_tell_the_player_the_position_is_invalid() { var exception = new InvalidPositionException(string.Empty); var game = new Game(); try { game.ChoosePosition(10); } catch (InvalidPositionException ex) { exception = ex; } Assert.AreEqual("The position '10' was invalid.", exception.Message); } }

Next, let’s create our new Exception class:

public class InvalidPositionException : Exception { public InvalidPositionException(string message) : base(message) { } }

Now, let’s run our tests:

 
When_the_player_attempts_to_select_an_invalid_position Failed it_should_tell_the_player_the_position_is_invalid Assert.AreEqual failed. Expected:. Actual:<>.

Since all we need to do is to throw our new exception, we’ll use an Obvious Implementation:

public string ChoosePosition(int position) { if (IsOutOfRange(position)) { throw new InvalidPositionException( string.Format("The position \'{0}\' was invalid.", position)); } if (_layout[position - 1] != '\0') { return "That spot is taken!"; } _layout[position - 1] = GetTokenFor(Player.Human); SelectAPositionFor(Player.Game); if (WinningPlayerIs(Player.Human)) return "Player wins!"; if (WinningPlayerIs(Player.Game)) return "Game wins."; return string.Empty; }
 

 

In this case we haven’t introduced any duplication that I can see, so let’s move on to our next test. We’ll modify it to follow the same pattern as our previous one:

[TestClass] public class When_the_player_attempts_to_select_an_occupied_position { [TestMethod] public void it_should_tell_the_player_the_position_is_occupied() { var exception = new OccupiedPositionException(string.Empty); var game = new Game(new GameAdvisorStub(new[] {1, 4, 7})); game.ChoosePosition(2); try { game.ChoosePosition(1); } catch (OccupiedPositionException ex) { exception = ex; } Assert.AreEqual("The position '1' is already occupied.", exception.Message); } }

Here is our new exception:

public class OccupiedPositionException : Exception { public OccupiedPositionException(string message): base(message) { } }

Here’s the results of our test execution:

 
When_the_player_attempts_to_select_an_occupied_position Failed it_should_tell_the_player_the_position_is_occupied Assert.AreEqual failed. Expected:. Actual:<>.

To provide the implementation, we’ll throw our new exception:

public string ChoosePosition(int position) { if (IsOutOfRange(position)) { throw new InvalidPositionException(string.Format("The position \'{0}\' was invalid.", position)); } if (_layout[position - 1] != '\0') { throw new OccupiedPositionException( string.Format("The position \'{0}\' is already occupied.", position)); } _layout[position - 1] = GetTokenFor(Player.Human); SelectAPositionFor(Player.Game); if (WinningPlayerIs(Player.Human)) return "Player wins!"; if (WinningPlayerIs(Player.Game)) return "Game wins."; return string.Empty; }
 

 

Again, there isn’t any noticeable duplication this time. Our next task is to send notifications to observers when the Game class detects a winner. Here are the existing tests we used for indicating a winner:

[TestClass] public class When_the_player_gets_three_in_a_row { [TestMethod] public void it_should_announce_the_player_as_the_winner() { var game = new Game(new GameAdvisorStub(new[] {1, 2, 3})); game.ChoosePosition(4); game.ChoosePosition(5); string message = game.ChoosePosition(6); Assert.AreEqual("Player wins!", message); } }

[TestClass] public class When_the_game_gets_three_in_a_row { [TestMethod] public void it_should_announce_the_game_as_the_winner() { var game = new Game(); game.ChoosePosition(4); game.ChoosePosition(6); string message = game.ChoosePosition(8); Assert.AreEqual("Game wins.", message); } }

We’ll start with the first one by modifying our Assert call. First, let’s change the value we’re comparing against from a string to a new GameResult enumeration with a value of PlayerWins:

[TestClass] public class When_the_player_gets_three_in_a_row { [TestMethod] public void it_should_announce_the_player_as_the_winner() { var game = new Game(new GameAdvisorStub(new[] {1, 2, 3})); game.ChoosePosition(4); game.ChoosePosition(5); string message = game.ChoosePosition(6); Assert.AreEqual(GameResult.PlayerWins, result); } }

Next, let’s create an instance of our as yet created GameResult enumeration and initialize it’s value to something we aren’t expecting:

[TestClass] public class When_the_player_gets_three_in_a_row { [TestMethod] public void it_should_announce_the_player_as_the_winner() { var game = new Game(new GameAdvisorStub(new[] {1, 2, 3})); var result = (GameResult) (-1); game.ChoosePosition(4); game.ChoosePosition(5); string message = game.ChoosePosition(6); Assert.AreEqual(GameResult.PlayerWins, result); } }

Next, we need to decide how we would like to receive this value. Let’s assume we can subscribe to a GameComplete event. When invoked, we’ll assume the value can be retrieved from a property on the EventArgs supplied with the event:

[TestClass] public class When_the_player_gets_three_in_a_row { [TestMethod] public void it_should_announce_the_player_as_the_winner() { var game = new Game(new GameAdvisorStub(new[] {1, 2, 3})); var result = (GameResult) (-1); game.GameComplete += (s, e) => result = e.Result; game.ChoosePosition(4); game.ChoosePosition(5); game.ChoosePosition(6); Assert.AreEqual(GameResult.PlayerWins, result); } }

Our next steps are to create the new enum type and to add an event to our Game class. First, let’s create the enum:

public enum GameResult { PlayerWins, GameWins, Draw }

I went ahead and added values for the other two possible states: GameWins and Draw. “Aren’t we getting ahead of ourselves”, you might ask? Perhaps, but we already know we have upcoming tests that will require these states and our GameResult represents the state of our game, not its behavior. We’ve been pretty good about not prematurely adding anything thus far, so this seems like a safe enough step to take without sending us down a slippery slope.

Here’s our new Game event:

public class Game { ... public event EventHandler GameComplete; ... }

Now that we’ve created this, we’ll also need to create a GameCompleteEventArgs:

public class GameCompleteEventArgs : EventArgs { public GameResult Result { get; private set; } }

Now we’re ready to compile and run our tests:

 
When_the_player_gets_three_in_a_row Failed it_should_announce_the_player_as_the_winner Assert.AreEqual failed. Expected:. Actual:<-1>.

There are well established patterns for raising events in .Net, so we’ll follow the standard pattern for this:

public string ChoosePosition(int position) { if (IsOutOfRange(position)) { throw new InvalidPositionException(string.Format("The position \'{0}\' was invalid.", position)); } if (_layout[position - 1] != '\0') { throw new OccupiedPositionException(string.Format("The position \'{0}\' is already occupied.", position)); } _layout[position - 1] = GetTokenFor(Player.Human); SelectAPositionFor(Player.Game); if (WinningPlayerIs(Player.Human)) InvokeGameComplete(new GameCompleteEventArgs(GameResult.PlayerWins)); if (WinningPlayerIs(Player.Game)) return "Game wins."; return string.Empty; }

Now we need to create our InvokeGameComplete() method and a GameCompleteEventArgs constructor that initializes the Result property:

public class Game { ... public event EventHandler GameComplete; public void InvokeGameComplete(GameCompleteEventArgs e) { EventHandler handler = GameComplete; if (handler != null) handler(this, e); } ... }
public class GameCompleteEventArgs : EventArgs { public GameCompleteEventArgs(GameResult result) { Result = result; } public GameResult Result { get; private set; } }
 

 

Again, I don’t see any duplication to worry about. Next, we’ll follow similar steps for notifying the game as a winner:

[TestClass] public class When_the_game_gets_three_in_a_row { [TestMethod] public void it_should_announce_the_game_as_the_winner() { var game = new Game(new GameAdvisorStub(new[] {1, 2, 3})); var result = (GameResult) (-1); game.GameComplete += (s, e) => result = e.Result; game.ChoosePosition(4); game.ChoosePosition(6); game.ChoosePosition(8); Assert.AreEqual(GameResult.GameWins, result); } }
 
When_the_game_gets_three_in_a_row Failed it_should_announce_the_game_as_the_winner Assert.AreEqual failed. Expected:. Actual:<-1>.

To make the test pass, we should only need to modify the Game class to raise the event this time:

public string ChoosePosition(int position) { if (IsOutOfRange(position)) { throw new InvalidPositionException(string.Format("The position \'{0}\' was invalid.", position)); } if (_layout[position - 1] != '\0') { throw new OccupiedPositionException(string.Format("The position \'{0}\' is already occupied.", position)); } _layout[position - 1] = GetTokenFor(Player.Human); SelectAPositionFor(Player.Game); if (WinningPlayerIs(Player.Human)) InvokeGameComplete(new GameCompleteEventArgs(GameResult.PlayerWins)); if (WinningPlayerIs(Player.Game)) InvokeGameComplete(new GameCompleteEventArgs(GameResult.GameWins)); return string.Empty; }

Let’s run the tests again:

 
When_the_player_gets_three_in_a_row Failed it_should_announce_the_player_as_the_winner Assert.AreEqual failed. Expected:. Actual:.

Our target test passed, but we broke our previous test. Looking at our implementation, the problem seems to be that both the player and game select positions on the board before we check to see if anyone is a winner. Additionally, we should return from the method once a winner is determined. Let’s fix this:

public string ChoosePosition(int position) { if (IsOutOfRange(position)) { throw new InvalidPositionException(string.Format("The position \'{0}\' was invalid.", position)); } if (_layout[position - 1] != '\0') { throw new OccupiedPositionException(string.Format("The position \'{0}\' is already occupied.", position)); } _layout[position - 1] = GetTokenFor(Player.Human); if (WinningPlayerIs(Player.Human)) { InvokeGameComplete(new GameCompleteEventArgs(GameResult.PlayerWins)); return string.Empty; } SelectAPositionFor(Player.Game); if (WinningPlayerIs(Player.Game)) { InvokeGameComplete(new GameCompleteEventArgs(GameResult.GameWins)); return string.Empty; } return string.Empty; }
 

 

Let’s refactor now. First, let’s remove our unused return type:

public void ChoosePosition(int position) { if (IsOutOfRange(position)) { throw new InvalidPositionException(string.Format("The position \'{0}\' was invalid.", position)); } if (_layout[position - 1] != '\0') { throw new OccupiedPositionException(string.Format("The position \'{0}\' is already occupied.", position)); } _layout[position - 1] = GetTokenFor(Player.Human); if (WinningPlayerIs(Player.Human)) { InvokeGameComplete(new GameCompleteEventArgs(GameResult.PlayerWins)); return; } SelectAPositionFor(Player.Game); if (WinningPlayerIs(Player.Game)) { InvokeGameComplete(new GameCompleteEventArgs(GameResult.GameWins)); return; } }
 

 

Now that we’ve rearranged our code, we have a sequence of steps that are repeated between the player and the game. First we use a strategy for moving the player, then we check to see if the player wins. Let’s distill this down to checking for the first winning play from a collection of player strategies:

public void ChoosePosition(int position) { if (IsOutOfRange(position)) { throw new InvalidPositionException(string.Format("The position \'{0}\' was invalid.", position)); } if (_layout[position - 1] != '\0') { throw new OccupiedPositionException(string.Format("The position \'{0}\' is already occupied.", position)); } new Func<bool>[] { () => CheckPlayerStrategy(Player.Human, () => _layout[position - 1] = GetTokenFor(Player.Human)), () => CheckPlayerStrategy(Player.Game, () => SelectAPositionFor(Player.Game)) }.Any(winningPlay => winningPlay()); }

Here’s our new CheckPlayerStrategy() method:

bool CheckPlayerStrategy(Player player, Action strategy) { strategy(); if (WinningPlayerIs(player)) { var result = (player == Player.Human) ? GameResult.PlayerWins : GameResult.GameWins; InvokeGameComplete(new GameCompleteEventArgs(result)); return true; } return false; }
 

 

Our final step for this issue is to raise an event when there is a draw. Following our normal procession, here’s the test we come up with:

[TestClass] public class When_a_move_results_in_a_draw { [TestMethod] public void it_should_announce_the_game_is_a_draw() { var game = new Game(new GameAdvisorStub(new[] { 2, 3, 4, 9 })); var result = (GameResult)(-1); game.GameComplete += (s, e) => result = e.Result; new[] {1, 5, 6, 7, 8}.ToList().ForEach(game.ChoosePosition); Assert.AreEqual(GameResult.Draw, result); } }
 
When_a_move_results_in_a_draw Failed it_should_announce_the_game_is_a_draw TestFirstExample.When_a_move_results_in_a_draw.it_should_announce_the_game_is_a_draw threw exception: System.IndexOutOfRangeException: Index was outside the bounds of the array.

This test isn’t failing for the right reason, so let’s address this before moving on. After investigating the exception, the issue is that we never accounted for the fact that their won’t be a position to choose when the player chooses the last remaining position. Let’s correct this issue by ensuring there is an empty spot left before selecting a position for the game:

void SelectAPositionFor(Player player) { if (_layout.Any(position => position == '\0')) { int recommendedPosition = _advisor.WithLayout(new string(_layout)).SelectBestMoveForPlayer(GetTokenFor(player)); _layout[recommendedPosition - 1] = GetTokenFor(player); _lastPositionDictionary[player] = recommendedPosition; } }
 
When_a_move_results_in_a_draw Failed it_should_announce_the_game_is_a_draw Failed it_should_announce_the_game_is_a_draw Assert.AreEqual failed. Expected:. Actual:<-1>.

Now our test is failing for the right reason. To make the test pass, we can fire the event unless someone won or unless there’s any empty positions left:

public void ChoosePosition(int position) { if (IsOutOfRange(position)) { throw new InvalidPositionException(string.Format("The position \'{0}\' was invalid.", position)); } if (_layout[position - 1] != '\0') { throw new OccupiedPositionException(string.Format("The position \'{0}\' is already occupied.", position)); } var someoneWon = new Func<bool>[] { () => CheckPlayerStrategy(Player.Human, () => _layout[position - 1] = GetTokenFor(Player.Human)), () => CheckPlayerStrategy(Player.Game, () => SelectAPositionFor(Player.Game)) }.Any(winningPlay => winningPlay()); if (!(someoneWon || _layout.Any(pos => pos == '\0'))) { InvokeGameComplete(new GameCompleteEventArgs(GameResult.Draw)); } }
 

 

Time to refactor. It looks like we have the same comparison for checking that the game is a draw and our new guard for selecting a position for the game. Let’s create a method for these which expresses the meaning of this check:

bool PositionsAreLeft() { return _layout.Any(pos => pos == '\0'); }

Now we can replace the previous calls in the ChoosePosition() and SelectAPositionFor() methods:

public void ChoosePosition(int position) { if (IsOutOfRange(position)) { throw new InvalidPositionException(string.Format("The position \'{0}\' was invalid.", position)); } if (_layout[position - 1] != '\0') { throw new OccupiedPositionException(string.Format("The position \'{0}\' is already occupied.", position)); } bool someoneWon = new Func<[] { () => CheckPlayerStrategy(Player.Human, () => _layout[position - 1] = GetTokenFor(Player.Human)), () => CheckPlayerStrategy(Player.Game, () => SelectAPositionFor(Player.Game)) }.Any(winningPlay => winningPlay()); if (!(someoneWon || PositionsAreLeft())) { InvokeGameComplete(new GameCompleteEventArgs(GameResult.Draw)); } }
void SelectAPositionFor(Player player) { if (PositionsAreLeft()) { int recommendedPosition = _advisor.WithLayout(new string(_layout)).SelectBestMoveForPlayer(GetTokenFor(player)); _layout[recommendedPosition - 1] = GetTokenFor(player); _lastPositionDictionary[player] = recommendedPosition; } }
 

 

One thing that occurred to me while implementing this feature is that using a null character to represent an empty position isn’t particularly clear. Let’s define a constant named EmptyValue which we’ll substitute for our use of the null character:

public class Game { ... const char EmptyValue = char.MinValue; ... public void ChoosePosition(int position) { ... if (_layout[position - 1] != EmptyValue) { throw new OccupiedPositionException(string.Format("The position \'{0}\' is already occupied.", position)); } ... } bool PositionsAreLeft() { return _layout.Any(pos => pos == EmptyValue ); } string GetLayoutFor(Player player) { return new string(_layout.ToList() .Select(c => (c.Equals(GetTokenFor(player))) ? GetTokenFor(player) : EmptyValue ) .ToArray()); } … }
 

 

That wraps up the two issues from the UI team. We’ll stop here and address the issues from the QA team next time.

Tagged with:  

In part 6 of our series, we continued the implementation of our Tic-tac-toe game using a Test-First approach. This time, we’ll finish out our requirements.

Here’s where we left things:

When the player goes first it should put their mark in the selected position it should make the next move When the player gets three in a row it should announce the player as the winner When the game gets three in a row it should announce the game as the winner When the player attempts to select an occupied position it should tell the player the position is occupied When the player attempts to select an invalid position it should tell the player the position is invalid When the game goes first it should put an X in one of the available positions When the player can not win on the next turn it should try to get three in a row When the player can win on the next turn it should block the player

Our last two requirements pertain to making the game try to win. The first requirement concerns the game trying to get three in a row while the second pertains to the game trying to keep the opponent from getting three in a row. Let’s get started on the first test:

[TestClass] public class When_the_player_can_not_win_on_the_next_turn { [TestMethod] public void it_should_try_to_get_three_in_a_row() { } }

Let’s assume we’ll be validating that the game gets three in a row by completing a sequence ending with the bottom right position being selected:

[TestMethod] public void it_should_try_to_get_three_in_a_row() { Assert.AreEqual(9, selection); }

Next, let’s establish a scenario were the bottom right corner should always be the position we would expect the game to choose (as opposed to a scenario where the game might have multiple intelligent moves). The following illustrates a layout where the game has gone first and has already achieved two in a row:

 

tic-tac-toe-game-win

 

Next, we need to determine how we can force the game into the desired state so we can validate the next position selected. We won’t be able to use the same technique as before, so we’ll need to find a new way of manipulating the state. One way would be to just make the Game’s _layout field public and manipulate it directly, but that would break encapsulation. Another way would be to set the _layout field through reflection, but this would result in our test being more tightly coupled to the implementation details of our Game. To make our Game testable, we need to adapt its interface. If our game relied upon a separate class for choosing the positions, we would then have a seam we could use to influence the layout. Hey … once this is in place we’ll have a way to fix our test coupling problem!

For now, let’s comment out the test we’ve been working on and start on a new test describing how the Game class will interact with this new dependency. Let’s think of the dependency as an “advisor” and describe the interaction as receiving a recommendation by the advisor:

[TestClass] public class When_the_game_selects_a_position { [TestMethod] public void it_should_select_the_position_recommended_by_the_advisor() { } }

Next, let’s establish an assertion that validates the selection made by the game. We’ll stick with the same scenario we established earlier, expecting the game to choose the bottom right position:

[TestClass] public class When_the_game_selects_a_position { [TestMethod] public void it_should_select_the_position_recommended_by_the_advisor() { Assert.AreEqual(9, selection); } }

Next, we need a way of determining the last position chosen by the game. As we’ve done in our previous tests, we’ll use the single GetPosition() method and a little bit of LINQ goodness to help us out. To figure out what the last move was, we can get a list of all the game positions before and after its next turn. We can then use the two lists to determine which new position was selected:

[TestClass] public class When_the_game_selects_a_position { [TestMethod] public void it_should_select_the_position_recommended_by_the_advisor() { IEnumerable<int> beforeLayout = (Enumerable.Range(1, 9) .Where(position => game.GetPosition(position).Equals('X')) .Select(position => position)).ToList(); // make move here IEnumerable<int> afterLayout = (Enumerable.Range(1, 9) .Where(position => game.GetPosition(position).Equals('X')) .Select(position => position)).ToList(); int selection = afterLayout.Except(beforeLayout).Single(); Assert.AreEqual(9, selection); } }

Next, let’s establish the Game context along with the call we’re interested in:

[TestClass] public class When_the_game_selects_a_position { [TestMethod] public void it_should_select_the_position_recommended_by_the_advisor() { var game = new Game(advisor); game.GoFirst(); game.ChoosePosition(1); IEnumerable<int> beforeLayout = (Enumerable.Range(1, 9) .Where(position => game.GetPosition(position).Equals('X')) .Select(position => position)).ToList(); game.ChoosePosition(8); IEnumerable<int> afterLayout = (Enumerable.Range(1, 9) .Where(position => game.GetPosition(position).Equals('X')) .Select(position => position)).ToList(); int selection = afterLayout.Except(beforeLayout).Single(); Assert.AreEqual(9, selection); } }

Next, let’s establish our GameAdvisor stub. To get our GameAdvisorStub to recommend the positions we’d like, we’ll pass an array of integers to denote the progression we want the game to use:

[TestMethod] public void it_should_select_the_position_recommended_by_the_advisor() { IGameAdvisor advisor = new GameAdvisorStub(new[] { 3, 6, 9 }); var game = new Game(advisor); game.GoFirst(); game.ChoosePosition(1); IEnumerable<int> beforeLayout = (Enumerable.Range(1, 9) .Where(position => game.GetPosition(position).Equals('X')) .Select(position => position)).ToList(); game.ChoosePosition(8); IEnumerable<int> afterLayout = (Enumerable.Range(1, 9) .Where(position => game.GetPosition(position).Equals('X')) .Select(position => position)).ToList(); int selection = afterLayout.Except(beforeLayout).Single(); Assert.AreEqual(9, selection); }

To get our test to compile, we’ll need to create our new IGameAdvisor interface, GameAdvisorStub class and add a constructor to our existing Game class. Let’s start with the advisor types:

public interface IGameAdvisor { } public class GameAdvisorStub : IGameAdvisor { readonly int[] _positions; public GameAdvisorStub(int[] positions) { _positions = positions; } }

Next, let’s create the new constructor for our Game class which takes an IGameAdvisor. We’ll also supply a default no argument constructor to keep our existing tests compiling:

public class Game { public Game() { } public Game(IGameAdvisor advisor) { } ... }

Everything should now compile. Let’s run our tests:

 

When_the_game_selects_a_position Failed it_should_select_the_position_recommended_by_the_advisor Assert.AreEqual failed. Expected:<9>. Actual:<2>. ...

Before we move on, our test could stand a little cleaning up. The verbosity of the LINQ extension method calls we’re using are obscuring the intent of our test a bit. Let’s write a test helper in the form of an extension method to help clarify the intentions of our test:

public static class GameExtensions { public static int GetSelectionAfter(this Game game, Action action) { IEnumerable<int> beforeLayout = (Enumerable.Range(1, 9) .Where(position => game.GetPosition(position).Equals('X')) .Select(position => position)).ToList(); action(); IEnumerable<int> afterLayout = (Enumerable.Range(1, 9) .Where(position => game.GetPosition(position).Equals('X')) .Select(position => position)).ToList(); return afterLayout.Except(beforeLayout).Single(); } }

Now we can change our test to the following:

[TestClass] public class When_the_game_selects_a_position { [TestMethod] public void it_should_select_the_position_recommended_by_the_advisor() { IGameAdvisor advisor = new GameAdvisorStub(new[] {3, 6, 9}); var game = new Game(advisor); game.GoFirst(); game.ChoosePosition(1); int selection = game.GetSelectionAfter(() => game.ChoosePosition(8)); Assert.AreEqual(9, selection); } }

Let’s run the test again to make sure it still validates correctly:

 

When_the_game_selects_a_position Failed it_should_select_the_position_recommended_by_the_advisor Assert.AreEqual failed. Expected:<9>. Actual:<2>. ...

Good, now let’s work on making the test pass. Something simple we can do to force our test to pass is to play off of a bit of new information we have at our disposal. Since our new test is the only one using the overloaded constructor, we can use the advisor field as a sort of flag to perform some behavior specific to this test. First, let’s assign the parameter to a field:

readonly IGameAdvisor _advisor; public Game(IGameAdvisor advisor) { _advisor = advisor; }

Next, let’s modify the ChoosePosition() method to set the ninth position to an ‘X’ if the _advisor field is set and the player chooses position 8:

public string ChoosePosition(int position) { if( _advisor != null && position == 8 ) { _layout[8] = 'X'; return string.Empty; } if (IsOutOfRange(position)) { return "That spot is invalid!"; } if (_layout[position - 1] != '\0') { return "That spot is taken!"; } _layout[position - 1] = GetTokenFor(Player.Human); SelectAPositionFor(Player.Game); if (WinningPlayerIs(Player.Human)) return "Player wins!"; if (WinningPlayerIs(Player.Game)) return "Game wins."; return string.Empty; }

Now, let’s run our tests:

 

 

Now, let’s refactor. To eliminate our fake implementation, let’s start by modifying the Game’s SelectAPositionFor() method to call our new IGameAdvisor field. Well assume the IGameAdvisor supports a SelectAPositionFor() method which allows us to pass in the token and the current layout as a string:

void SelectAPositionFor(Player player) { int recommendedPosition = _advisor.SelectAPositionFor(GetTokenFor(player), new string(_layout)); _layout[recommendedPosition] = GetTokenFor(player); }

Next, let’s define the new method on our interface:

public interface IGameAdvisor { int SelectAPositionFor(char player, string layout); }

Next, we need to implement the new method on our stub. To have our stub return the expected positions, we’ll keep track of how many times the method has been called and use that as the offset into the array setup in our test:

public class GameAdvisorStub : IGameAdvisor { readonly int[] _positions; int _count; public GameAdvisorStub(int[] positions) { _positions = positions; } public int SelectAPositionFor(char player, string layout) { return _positions[_count++]; } }

Lastly, we can delete our fake implementation and run the tests:

 

When_the_player_goes_first Failed it_should_put_their_choice_in_the_selected_position TestFirstExample.When_the_player_goes_first.establish_context threw exception. System.NullReferenceException: System.NullReferenceException: Object reference not set to an instance of an object.. Failed it_should_make_the_next_move TestFirstExample.When_the_player_goes_first.establish_context threw exception. System.NullReferenceException: System.NullReferenceException: Object reference not set to an instance of an object.. ...

Oh no, we broke a bunch of tests! They all seem to be failing due to a NullReferenceException. Looking further, this is being caused by the IGameAdvisor field not being assigned when using the default constructor. Let’s fix that by changing our default constructor to call the overloaded constructor with a default implementation of the IGameAdvisor interface:

public Game() : this(new GameAdvisor()) { }

Next, we’ll create the GameAdvisor class and provide an implementation that mirrors the former behavior:

class GameAdvisor : IGameAdvisor { public int SelectAPositionFor(char player, string layout) { return Enumerable.Range(1, layout.Length) .First(p => layout[p - 1].Equals('\0')); } }
 

 

Our Game class works the same way as before, but we now have a new seam we can influence the layout selection with.

We can now turn our attention back to the requirements. Let’s go back and un-comment the test we started with, but this time we’ll use it to drive the behavior of our GameAdvisor:

[TestClass] public class When_the_player_can_not_win_on_the_next_turn { [TestMethod] public void it_should_try_to_get_three_in_a_row() { Assert.AreEqual(9, selection); } }

Next, let’s declare an instance of our GameAdvisor class and ask it to select a position for player ‘X’:

[TestClass] public class When_the_player_can_not_win_on_the_next_turn { [TestMethod] public void it_should_try_to_get_three_in_a_row() { IGameAdvisor advisor = new GameAdvisor(); var selection = advisor.SelectAPositionFor('X', "OXXO"); Assert.AreEqual(9, selection); } }

Before moving on, let’s consider our initial API. Given any approach, is this really the API we want to work with? While the SelectAPositionFor() method seems like a good start, the parameters feel more like an afterthought than a part of the request. It reads more like a “Do something, and oh by the way, here’s some data”. I didn’t notice when we called it from the Game class, but looking back, that call was aided by the context of its usage. We don’t have any variables telling us what ‘X’ and “OA…” mean.

One of the advantages of Test-Driven Development is that it forces us to look at our API from a consumer’s perspective. When we build things from the inside out, we often don’t consider how intuitive the components will be to work with by our consumers. Once we’ve started implementation, our perspective can be prejudiced by our understanding of how the system works. TDD helps to address this issue by forcing us to consider how the components will be used. This in turn guides us to adapt the design to how the system is being used rather than the other way around. Let’s see if we can improve upon this a bit:

[TestClass] public class When_the_player_can_not_win_on_the_next_turn { [TestMethod] public void it_should_try_to_get_three_in_a_row() { IGameAdvisor advisor = new GameAdvisor(); var selection = advisor.WithLayout("OXXO").SelectBestMoveForPlayer('X'); Assert.AreEqual(9, selection); } }

That seems to express how I’d like to interact with our advisor more clearly. This breaks our code though, so we’ll need to make some adjustments to our IGameAdvisor interface and GameAdvisor class:

public interface IGameAdvisor { int SelectBestMoveForPlayer(char player); IGameAdvisor WithLayout(string layout); } class GameAdvisor : IGameAdvisor { string _layout; public int SelectBestMoveForPlayer(char player) { return Enumerable.Range(1, _layout.Length) .First(p => _layout[p - 1].Equals('\0')); } public IGameAdvisor WithLayout(string layout) { _layout = layout; return this; } }

That would work, but this implementation would allow us to call the advisor without specifying the layout. Let’s take just a little more time to clear that up by moving the SelectBestMoveForPlayer() method to an inner class to prevent it from being called directly:

public interface IGameAdvisor { IPositionSelector WithLayout(string layout); } public interface IPositionSelector { int SelectBestMoveForPlayer(char player); } class GameAdvisor : IGameAdvisor { public IPositionSelector WithLayout(string layout) { return new PositionSelector(layout); } class PositionSelector : IPositionSelector { readonly string _layout; public PositionSelector(string layout) { _layout = layout; } public int SelectBestMoveForPlayer(char player) { return Enumerable.Range(1, _layout.Length) .First(p => _layout[p - 1].Equals('\0')); } } }

Next, let’s fix up our GameAdvisorStub:

public class GameAdvisorStub : IGameAdvisor { readonly int[] _positions; int _count; public GameAdvisorStub(int[] positions) { _positions = positions; } public IPositionSelector WithLayout(string layout) { return new PositionSelector(layout, _positions, _count++); } class PositionSelector : IPositionSelector { readonly int[] _positions; readonly int _count; public PositionSelector(string layout, int[] positions, int count) { _positions = positions; _count = count; } public int SelectBestMoveForPlayer(char player) { return _positions[_count]; } } }

We also need to fix the SelectAPositionFor() method in our Game class:

void SelectAPositionFor(Player player) { int recommendedPosition = _advisor.WithLayout(new string(_layout)) .SelectBestMoveForPlayer(GetTokenFor(player)); _layout[recommendedPosition - 1] = GetTokenFor(player); }

Now, let’s run our tests and make sure our new test fails for the right reason and that we haven’t broken any of the other behavior:

 

When_the_player_can_not_win_on_the_next_turn Failed it_should_try_to_get_three_in_a_row Assert.AreEqual failed. Expected:<9>. Actual:<2>.

Only our new test fails, which is what we were hoping for. Now, let’s use the Fake It approach to get our test to pass quickly. Since only one of our tests ever call this method with the token ‘X’ and it doesn’t care about which actual position it is, we can change the GameAdvisor’s PositionAdvisor.SelectBestMoveForPlayer() method to always return 9 for player ‘X’:

public int SelectBestMoveForPlayer(char player) { if (player == 'X') return 9; return Enumerable.Range(0, _layout.Length) .First(p => _layout[p].Equals('\0')) + 1; }

Now, let’s refactor to eliminate our duplication. In order to calculate which position should be selected, we’ll need to know what the winning patterns are and which paths in the layout are closest to the winning patterns. My first thought was that we might be able to reuse the winning pattern regular expressions we defined over in our Game class. Let’s go back and look at that again:

readonly string[] _winningPatterns = new[] { "[XO][XO][XO]......", "...[XO][XO][XO]...", "......[XO][XO][XO]", "[XO]..[XO]..[XO]..", ".[XO]..[XO]..[XO].", "..[XO]..[XO]..[XO]", "[XO]...[XO]...[XO]", "..[XO].[XO].[XO]..", };

While these patterns define what the winning paths are, I don’t think this will work for our needs because this only matches winning patterns. What we need is a way of examining each of the eight winning paths within the layout to see which is the closest to winning. Let’s start by define a new regular expression that we can use to filter out the paths that can’t win:

class PositionSelector : IPositionSelector { readonly Regex _availablePathPattern = new Regex(@"[X]{3}"); readonly string _layout; public PositionSelector(string layout) { _layout = layout; } public int SelectBestMoveForPlayer(char player) { if (player == 'X') return 9; return Enumerable.Range(0, _layout.Length) .First(p => _layout[p].Equals('\0')) + 1; } }

This regular expression will match any three characters where each of the characters can be either an ‘X’ or a null. If you’re unfamiliar with regular expressions, the brackets are referred to as Character Classes or Character Sets and allow us to define a group of characters we’re interested in. The curly braces with the number is how we define how many times the pattern should repeat to be a match.

Now, we need to apply this to each of the eight possible paths within our layout. To do so, we’ll need to slice up our layout into the eight possible winning paths. Let’s define an array similar to the one in our Game class, but using the winning positions instead of winning patterns:

class PositionSelector : IPositionSelector { readonly Regex _availablePathPattern = new Regex(@"[X]{3}"); readonly string _layout; static readonly int[][] _winningPositions = new[] { new[] {1, 2, 3}, new[] {4, 5, 6}, new[] {7, 8, 9}, new[] {1, 4, 7}, new[] {2, 5, 8}, new[] {3, 6, 9}, new[] {1, 5, 9}, new[] {3, 5, 7}, }; public PositionSelector(string layout) { _layout = layout; } public int SelectBestMoveForPlayer(char player) { if (player == 'X') return 9; return Enumerable.Range(0, _layout.Length) .First(p => _layout[p].Equals('\0')) + 1; } }

Next, we can loop over each of the _winningPositions, retrieve the slice, compare it to the _availablePathPattern and add the matches to a list of availablePaths:

public int SelectBestMoveForPlayer(char player) { var availablePaths = new List<int[]>(); foreach (var winningSlice in _winningPositions) { var slice = new string(winningSlice.ToList() .Select(p => _layout.ElementAt(p - 1)).ToArray()); if (_availablePathPattern.IsMatch(slice)) availablePaths.Add(winningSlice); } if (player == 'X') return 9; return Enumerable.Range(0, _layout.Length) .First(p => _layout[p].Equals('\0')) + 1; }

Now that we have the available paths, we can sort them in descending order based on how many ‘O’s they already have, find the first available slot in the slice and return that as the position:

public int SelectBestMoveForPlayer(char player) { var availablePaths = new List<int[]>(); foreach (var winningSlice in _winningPositions) { var slice = new string(winningSlice.ToList() .Select(p => _layout.ElementAt(p - 1)).ToArray()); if (_availablePathPattern.IsMatch(slice)) availablePaths.Add(winningSlice); } var bestSlice = availablePaths .OrderByDescending(path => path .Count(p => _layout[p - 1] == 'X')).First(); return bestSlice.First(p => _layout[p - 1] == '\0'); }

I think we’re almost done refactoring, but we still have some duplication to eliminate. Both our test and our implementation are using the value of ‘X’ as a constant to represent the player. Let’s fix this by replacing the player’s token to a more neutral value and change our regular expression and sorting call to use the neutral value instead:

readonly Regex _availablePathPattern = new Regex(@"[T]{3}"); public int SelectBestMoveForPlayer(char player) { string layout = _layout.Replace(player, 'T'); var availablePaths = new List<int[]>(); foreach (var winningSlice in _winningPositions) { var slice = new string(winningSlice.ToList() .Select(p => layout.ElementAt(p - 1)).ToArray()); if (_availablePathPattern.IsMatch(slice)) availablePaths.Add(winningSlice); } int[] bestSlice = availablePaths .OrderByDescending(path => path .Count(p => layout[p - 1] == 'T')).First(); return bestSlice.First(p => _layout[p - 1] == '\0'); }

Everything should be good to go. Let’s run our tests and see how we did:

 

When_the_player_attempts_to_select_an_occupied_positionFailed it_should_tell_the_player_the_position_is_occupied Assert.AreEqual failed. Expected:<That spot is taken!>. Actual:<>.

Our target test passed, but we broke the test for how the game responds when the player chooses a position that is already occupied. Let’s review the test again:

[TestClass] public class When_the_player_attempts_to_select_an_occupied_position { [TestMethod] public void it_should_tell_the_player_the_position_is_occupied() { var game = new Game(); game.ChoosePosition(2); string message = game.ChoosePosition(1); Assert.AreEqual("That spot is taken!", message); } }

Because we are choosing the second position in this test, the GameAdvisor avoids recommending positions within the first winning pattern. We could fix this test pretty easily by avoiding positions two or three, but now that we now have a seam to control exactly what positions are selected, let’s use our new GameAdvisorStub to correct this test in a more explicit way:

[TestClass] public class When_the_player_attempts_to_select_an_occupied_position { [TestMethod] public void it_should_tell_the_player_the_position_is_occupied() { var game = new Game(new GameAdvisorStub(new [] {1, 4, 7})); game.ChoosePosition(2); string message = game.ChoosePosition(1); Assert.AreEqual("That spot is taken!", message); } }
 

 

The last requirement concerns how the game reacts when the player is about to win. Here’s our skeleton:

[TestClass] public class When_the_player_can_win_on_the_next_turn { [TestMethod] public void it_should_block_the_player() { } }

As always, we’ll start by deciding what observable outcome we want to depend upon to know the behavior is working correctly. Since we’re expecting the game to block the player, let’s come up with a scenario we know wouldn’t result from the existing behavior of trying to get three in a row. Let’s say the player goes first and has one position left in the center vertical row to win:

 

tic-tac-toe-game-block

 

To validate this scenario, we’ll check that the GameAdvisor chooses the eighth position:

[TestClass] public class When_the_player_can_win_on_the_next_turn { [TestMethod] public void it_should_block_the_player() { Assert.AreEqual(8, selection); } }

Next, let’s setup the rest of the context to declare the instance of our SUT and establish the layout:

[TestClass] public class When_the_player_can_win_on_the_next_turn { [TestMethod] public void it_should_block_the_player() { IGameAdvisor advisor = new GameAdvisor(); int selection = advisor.WithLayout("XOX").SelectBestMoveForPlayer('O'); Assert.AreEqual(8, selection); } }

Now, let’s run our test:

 

When_the_player_can_win_on_the_next_turn Failed it_should_block_the_player Assert.AreEqual failed. Expected:<8>. Actual:<1>.

Now, let’s make the test pass. This time, I’ll pass the test by testing specifically for the layout we’re after:

public int SelectBestMoveForPlayer(char player) { string layout = _layout.Replace(player, 'T'); var availablePaths = new List<int[]>(); foreach (var winningSlice in _winningPositions) { var slice = new string(winningSlice.ToList() .Select(p => layout.ElementAt(p - 1)).ToArray()); if (_availablePathPattern.IsMatch(slice)) availablePaths.Add(winningSlice); } if (layout == "XTX") { return 8; } int[] bestSlice = availablePaths .OrderByDescending(path => path .Count(p => layout[p - 1] == 'T')).First(); return bestSlice.First(p => _layout[p - 1] == '\0'); }
 

 

Now, let’s refactor. To get the GameAdvisor to choose the eighth position because it recognizes it’s vulnerable to losing, we’ll need to check how close the player is. To do this, we can find all of the available paths for the player and check if any of them already have two positions occupied. First, we’ll need to know which token the player is using:

public int SelectBestMoveForPlayer(char player) { string layout = _layout.Replace(player, 'T'); var availablePaths = new List<int[]>(); foreach (var winningSlice in _winningPositions) { var slice = new string(winningSlice.ToList() .Select(p => layout.ElementAt(p - 1)).ToArray()); if (_availablePathPattern.IsMatch(slice)) availablePaths.Add(winningSlice); } char opponentValue = (player == 'X') ? 'O' : 'X'; if (layout == "XTX") { return 8; } int[] bestSlice = availablePaths .OrderByDescending(path => path .Count(p => layout[p - 1] == 'T')).First(); return bestSlice.First(p => _layout[p - 1] == '\0'); } }

Next, let’s create a new local layout based on the player’s positions:

public int SelectBestMoveForPlayer(char player) { string layout = _layout.Replace(player, 'T'); var availablePaths = new List<int[]>(); foreach (var winningSlice in _winningPositions) { var slice = new string(winningSlice.ToList() .Select(p => layout.ElementAt(p - 1)).ToArray()); if (_availablePathPattern.IsMatch(slice)) availablePaths.Add(winningSlice); } char opponentValue = (player == 'X') ? 'O' : 'X'; string playerLayout = _layout.Replace(opponentValue, 'T'); if (layout == "XTX") { return 8; } int[] bestSlice = availablePaths .OrderByDescending(path => path .Count(p => layout[p - 1] == 'T')).First(); return bestSlice.First(p => _layout[p - 1] == '\0'); }

Now, let’s copy the logic we created before and use it to find the available paths for the opponent:

public int SelectBestMoveForPlayer(char player) { string layout = _layout.Replace(player, 'T'); var availablePaths = new List<int[]>(); foreach (var winningSlice in _winningPositions) { var slice = new string(winningSlice.ToList() .Select(p => layout.ElementAt(p - 1)).ToArray()); if (_availablePathPattern.IsMatch(slice)) availablePaths.Add(winningSlice); } char opponentValue = (player == 'X') ? 'O' : 'X'; string opponentLayout = _layout.Replace(opponentValue, 'T'); List<int[]> availableOpponentPaths = new List<int[]>(); foreach (var winningSlice in _winningPositions) { var slice = new string(winningSlice.ToList() .Select(p => opponentLayout.ElementAt(p - 1)).ToArray()); if (_availablePathPattern.IsMatch(slice)) availableOpponentPaths.Add(winningSlice); } if (layout == "XTX") { return 8; } int[] bestSlice = availablePaths .OrderByDescending(path => path .Count(p => layout[p - 1] == 'T')).First(); return bestSlice.First(p => _layout[p - 1] == '\0'); }

Lastly, let’s find all the available paths for which the opponent already has two positions filled and remove our fake implementation:

public int SelectBestMoveForPlayer(char player) { string layout = _layout.Replace(player, 'T'); var availablePaths = new List<int[]>(); foreach (var winningSlice in _winningPositions) { var slice = new string(winningSlice.ToList() .Select(p => layout.ElementAt(p - 1)).ToArray()); if (_availablePathPattern.IsMatch(slice)) availablePaths.Add(winningSlice); } char opponentValue = (player == 'X') ? 'O' : 'X'; string opponentLayout = _layout.Replace(opponentValue, 'T'); List<int[]> availableOpponentPaths = new List<int[]>(); foreach (var winningSlice in _winningPositions) { var slice = new string(winningSlice.ToList() .Select(p => opponentLayout.ElementAt(p - 1)).ToArray()); if (_availablePathPattern.IsMatch(slice)) availableOpponentPaths.Add(winningSlice); } int[] threatingPath = availableOpponentPaths .Where(path => new string( path.Select(p => opponentLayout[p - 1]).ToArray()) .Count(c => c == 'T') == 2).FirstOrDefault(); if (threatingPath != null) { return threatingPath .First(position => opponentLayout[position - 1] == '\0'); } int[] bestSlice = availablePaths.OrderByDescending( path => path.Count(p => layout[p - 1] == 'T')).First(); return bestSlice.First(p => _layout[p - 1] == '\0'); }

Let’s run our test and see what happens:

 

When_the_player_gets_three_in_a_row Failed it_should_announce_the_player_as_the_winner Assert.AreEqual failed. Expected:<Player wins!>. Actual:<That spot is taken!>.

Our test still passes, but for some reason we broke the test for testing that the player wins when getting three in a row. Let’s have a look:

[TestClass] public class When_the_player_gets_three_in_a_row { [TestMethod] public void it_should_announce_the_player_as_the_winner() { var game = new Game(); game.ChoosePosition(4); game.ChoosePosition(5); string message = game.ChoosePosition(6); Assert.AreEqual("Player wins!", message); } }

We wrote this test to assume we could pick positions without worrying about getting blocked. That behavior has changed, so we’ll need to adapt our test. Again, we can use our new seam to plug in the path we want the Game to follow to stay out of our way:

[TestClass] public class When_the_player_gets_three_in_a_row { [TestMethod] public void it_should_announce_the_player_as_the_winner() { var game = new Game(new GameAdvisorStub(new int[] { 1, 2, 3})); game.ChoosePosition(4); game.ChoosePosition(5); string message = game.ChoosePosition(6); Assert.AreEqual("Player wins!", message); } }
 

 

Now that we’ve fixed that test, let’s continue our refactoring effort. In generalizing our code, we introduced some duplication. Let’s fix this by extracting a method for determining the available paths for a given player:

List<int[]> GetAvailablePathsFor(char player) { string layout = _layout.Replace(player, 'T'); var availablePaths = new List<int[]>(); foreach (var winningSlice in _winningPositions) { var slice = new string(winningSlice.ToList() .Select(p => layout.ElementAt(p - 1)).ToArray()); if (_availablePathPattern.IsMatch(slice)) availablePaths.Add(winningSlice); } return availablePaths; }

Now our SelectBestPlayerFor() method becomes:

public int SelectBestMoveForPlayer(char player) { string layout = _layout.Replace(player, 'T'); var availablePaths = GetAvailablePathsFor(player); char opponentValue = (player == 'X') ? 'O' : 'X'; string opponentLayout = _layout.Replace(opponentValue, 'T'); List<int[]> availableOpponentPaths = GetAvailablePathsFor(opponentValue); int[] threatingPath = availableOpponentPaths .Where(path => new string( path.Select(p => opponentLayout[p - 1]).ToArray()) .Count(c => c == 'T') == 2).FirstOrDefault(); if (threatingPath != null) { return threatingPath .First(position => opponentLayout[position - 1] == '\0'); } int[] bestSlice = availablePaths.OrderByDescending( path => path.Count(p => layout[p - 1] == 'T')).First(); return bestSlice.First(p => _layout[p - 1] == '\0'); }
 

 

Next, let’s reorganize some of these operations so we can see how things group together:

public int SelectBestMoveForPlayer(char player) { char opponentValue = (player == 'X') ? 'O' : 'X'; string opponentLayout = _layout.Replace(opponentValue, 'T'); List<int[]> availableOpponentPaths = GetAvailablePathsFor(opponentValue); int[] threatingPath = availableOpponentPaths .Where(path => new string( path.Select(p => opponentLayout[p - 1]).ToArray()) .Count(c => c == 'T') == 2).FirstOrDefault(); if (threatingPath != null) { return threatingPath .First(position => opponentLayout[position - 1] == '\0'); } string layout = _layout.Replace(player, 'T'); List<int[]> availablePaths = GetAvailablePathsFor(player); int[] bestSlice = availablePaths.OrderByDescending( path => path.Count(p => layout[p - 1] == 'T')).First(); return bestSlice.First(p => _layout[p - 1] == '\0'); }
 

 

The first section is all about checking for threating opponent paths, but this isn’t very descriptive at the moment. Let’s move all of that into a method that describes exactly what we’re doing:

public int SelectBestMoveForPlayer(char player) { int? threatingPosition = GetPositionThreateningPlayer(player); if (threatingPosition != null) return threatingPosition.Value; string layout = _layout.Replace(player, 'T'); List<int[]> availablePaths = GetAvailablePathsFor(player); int[] bestSlice = availablePaths.OrderByDescending( path => path.Count(p => layout[p - 1] == 'T')).First(); return bestSlice.First(p => _layout[p - 1] == '\0'); } int? GetPositionThreateningPlayer(char player) { char opponentValue = (player == 'X') ? 'O' : 'X'; string opponentLayout = _layout.Replace(opponentValue, 'T'); List<int[]> availableOpponentPaths = GetAvailablePathsFor(opponentValue); int[] threatingPath = availableOpponentPaths .Where(path => new string( path.Select(p => opponentLayout[p - 1]).ToArray()) .Count(c => c == 'T') == 2).FirstOrDefault(); if (threatingPath != null) { return threatingPath .First(position => opponentLayout[position - 1] == '\0'); } return null; }
 

 

Next, let’s extract the code for selecting the next winning path position for the player into a separate method:

public int SelectBestMoveForPlayer(char player) { int? threatingPosition = GetPositionThreateningPlayer(player); if (threatingPosition != null) return threatingPosition.Value; return GetNextWinningMoveForPlayer(player); } int GetNextWinningMoveForPlayer(char player) { string layout = _layout.Replace(player, 'T'); List<int[]> availablePaths = GetAvailablePathsFor(player); int[] bestSlice = availablePaths.OrderByDescending( path => path.Count(p => layout[p - 1] == 'T')).First(); return bestSlice.First(p => _layout[p - 1] == '\0'); }
 

 

Now, we can reduce our SelectBestMoveForPlayer() method down to one fairly descriptive line:

public int SelectBestMoveForPlayer(char player) { return GetPositionThreateningPlayer(player) ?? GetNextWinningMoveForPlayer(player); }
 

 

We’re done! Here’s our GameAdvisor implementation:

class GameAdvisor : IGameAdvisor { public IPositionSelector WithLayout(string layout) { return new PositionSelector(layout); } class PositionSelector : IPositionSelector { static readonly int[][] _winningPositions = new[] { new[] {1, 2, 3}, new[] {4, 5, 6}, new[] {7, 8, 9}, new[] {1, 4, 7}, new[] {2, 5, 8}, new[] {3, 6, 9}, new[] {1, 5, 9}, new[] {3, 5, 7}, }; readonly Regex _availablePathPattern = new Regex(@"[T]{3}"); readonly string _layout; public PositionSelector(string layout) { _layout = layout; } public int SelectBestMoveForPlayer(char player) { return GetPositionThreateningPlayer(player) ?? GetNextWinningMoveForPlayer(player); } int GetNextWinningMoveForPlayer(char player) { string layout = _layout.Replace(player, 'T'); List<int[]> availablePaths = GetAvailablePathsFor(player); int[] bestSlice = availablePaths.OrderByDescending( path => path.Count(p => layout[p - 1] == 'T')).First(); return bestSlice.First(p => _layout[p - 1] == '\0'); } int? GetPositionThreateningPlayer(char player) { char opponentValue = (player == 'X') ? 'O' : 'X'; string opponentLayout = _layout.Replace(opponentValue, 'T'); List<int[]> availableOpponentPaths = GetAvailablePathsFor(opponentValue); int[] threatingPath = availableOpponentPaths .Where(path => new string( path.Select(p => opponentLayout[p - 1]).ToArray()) .Count(c => c == 'T') == 2).FirstOrDefault(); if (threatingPath != null) { return threatingPath .First(position => opponentLayout[position - 1] == '\0'); } return null; } List<int[]> GetAvailablePathsFor(char player) { string layout = _layout.Replace(player, 'T'); var availablePaths = new List<int[]>(); foreach (var winningSlice in _winningPositions) { var slice = new string(winningSlice.ToList() .Select(p => layout.ElementAt(p - 1)).ToArray()); if (_availablePathPattern.IsMatch(slice)) availablePaths.Add(winningSlice); } return availablePaths; } } }

While we’ve been working on our component, another team has been putting together a host application with a nice user interface. We’re now ready to hand our component over so it can be integrated into the rest of the application. Afterward, the full application will be passed on to a Quality Assurance Team to receive some acceptance testing. Next time we’ll take a look at any issues that come out of the integration and QA testing processes.

Tagged with:  

In part 4 of our series, we discussed the Test-Driven Development philosophy in more detail and started a Test First implementation of a Tic-tac-toe game component.

Here’s the progress we’ve made on our requirements so far:

When the player goes first it should put their mark in the selected position it should make the next move When the player gets three in a row it should announce the player as the winner When the game gets three in a row it should announce the game as the winner When the player attempts to select an occupied position it should tell the player the position is occupied When the player attempts to select an invalid position it should tell the player the position is invalid When the player can not win on the next turn it should try to get three in a row When the player can win on the next turn it should block the player

Also, here is what our Game class implementation looks like so far:

public class Game { readonly char[] _layout = new char[9]; readonly string[] _winningPatterns = new[] { "[XO][XO][XO]......", "...[XO][XO][XO]...", "......[XO][XO][XO]", "[XO]..[XO]..[XO]..", ".[XO]..[XO]..[XO].", "..[XO]..[XO]..[XO]", "[XO]...[XO]...[XO]", "..[XO].[XO].[XO]..", }; public string ChoosePosition(int position) { _layout[position - 1] = 'X'; int firstUnoccupied = Enumerable.Range(0, _layout.Length) .First(p => _layout[p].Equals('\0')); _layout[firstUnoccupied] = 'O'; if (WinningPlayerIs('X')) return "Player wins!"; if (WinningPlayerIs('O')) return "Game wins."; return string.Empty; } bool WinningPlayerIs(char player) { return _winningPatterns .Any(pattern => Regex.IsMatch(GetLayoutFor(player), pattern)); } string GetLayoutFor(char player) { return new string(_layout.ToList() .Select(c => (c.Equals(player)) ? player : '\0') .ToArray()); } public char GetPosition(int position) { return _layout[position - 1]; } }

Picking up from here, let’s create our next test skeleton:

[TestClass] public class When_the_player_attempts_to_select_an_occupied_position { [TestMethod] public void it_should_tell_the_player_the_position_is_occupied() { } }

Again, we’ll start by determining how we want to validate our requirements. Let’s assume we’ll get a message of “That spot is taken!” if we try to choose a position that’s already occupied:

[TestMethod] public void it_should_tell_the_player_the_position_is_occupied() { Assert.AreEqual("That spot is taken!", message); }

Since our game is choosing positions sequentially, something easy we can do is to choose the second position, leaving the first open for the game to select. We can then attempt to choose the first position which should result in an error message. I wonder whether depending on the game to behave this way is going to cause any issues in the future though. Let’s move forward with this strategy for now:

[TestMethod] public void it_should_tell_the_player_the_position_is_occupied() { var game = new Game(); game.ChoosePosition(2); string message = game.ChoosePosition(1); Assert.AreEqual("That spot is taken!", message); }
 

When_the_player_attempts_to_select_an_occupied_position Failed it_should_tell_the_player_the_position_is_occupied Assert.AreEqual failed. Expected:<That spot is taken!>. Actual:<>.

As a reminder, we want to get our test to pass quickly. Since we can do this with an Obvious Implementation of checking if the position already has a value other than null and returning the expected error message, let’s do that this time:

public string ChoosePosition(int position) { if(_layout[position -1] != '\0') { return "That spot is taken!"; } _layout[position - 1] = 'X'; int firstUnoccupied = Enumerable.Range(0, _layout.Length) .First(p => _layout[p].Equals('\0')); _layout[firstUnoccupied] = 'O'; if (WinningPlayerIs('X')) return "Player wins!"; if (WinningPlayerIs('O')) return "Game wins."; return string.Empty; }

It’s time to run the tests again:

 

When_the_player_gets_three_in_a_row Failed it_should_announce_the_player_as_the_winner Assert.AreEqual failed. Expected:<Player wins!>. Actual:<>.

Our target test passed, but our changes broke one of the previous tests. The failing test was the one that checks that the player wins when getting three in a row. For our context setup, we just selected the first three positions without worrying about whether the positions were occupied or not. This was our third test and at that point we weren’t concerned with how the game was going to determine its moves, but it seems this decision wasn’t without some trade-offs. For now, we can just avoid the first three positions, but I’m starting to wonder if another strategy is in order. Perhaps a solution will reveal itself in time. To avoid the conflict, we’ll select positions from the middle row:

[TestMethod] public void it_should_announce_the_player_as_the_winner() { var game = new Game(); game.ChoosePosition(4); game.ChoosePosition(5); string message = game.ChoosePosition(6); Assert.AreEqual("Player wins!", message); }
 

 

We’re green again for now. Let’s move on to our next test:

[TestClass] public class When_the_player_attempts_to_select_an_invalid_position { [TestMethod] public void it_should_tell_the_player_the_position_is_invalid() { } }

Similar to our previous test, let’s assume a message is returned of “That spot is invalid!”:

[TestClass] public class When_the_player_attempts_to_select_an_invalid_position { [TestMethod] public void it_should_tell_the_player_the_position_is_invalid() { Assert.AreEqual("That spot is invalid!", message); } }

Now, let’s establish a context which should result in this behavior:

[TestClass] public class When_the_player_attempts_to_select_an_invalid_position { [TestMethod] public void it_should_tell_the_player_the_position_is_invalid() { var game = new Game(); string message = game.ChoosePosition(10); Assert.AreEqual("That spot is invalid!", message); } }

Time to run the tests:

 

When_the_player_attempts_to_select_an_invalid_position Failed it_should_tell_the_player_the_position_is_invalid TestFirstExample.When_the_player_attempts_to_select_an_invalid_position .it_should_tell_the_player_the_position_is_invalid threw exception: System.IndexOutOfRangeException: Index was outside the bounds of the array.

The test failed, but not for the right reason. Let’s modify the Game class to return an unexpected value to validate our test:

public string ChoosePosition(int position) { if (position == 10) { return string.Empty; } if (_layout[position - 1] != '\0') { return "That spot is taken!"; } _layout[position - 1] = 'X'; int firstUnoccupied = Enumerable.Range(0, _layout.Length) .First(p => _layout[p].Equals('\0')); _layout[firstUnoccupied] = 'O'; if (WinningPlayerIs('X')) return "Player wins!"; if (WinningPlayerIs('O')) return "Game wins."; return string.Empty; }
 

When_the_player_attempts_to_select_an_invalid_position Failed it_should_tell_the_player_the_position_is_invalid Assert.AreEqual failed. Expected:<That spot is invalid!>. Actual:<>.

Now we can work on getting the test to pass. We can modify the Game class to check that the position falls within the allowable range about as quickly as we could use a fake implementation, so let’s just do that:

public string ChoosePosition(int position) { if (position < 1 || position > 9) { return "That spot is invalid!"; } if (_layout[position - 1] != '\0') { return "That spot is taken!"; } _layout[position - 1] = 'X'; int firstUnoccupied = Enumerable.Range(0, _layout.Length) .First(p => _layout[p].Equals('\0')); _layout[firstUnoccupied] = 'O'; if (WinningPlayerIs('X')) return "Player wins!"; if (WinningPlayerIs('O')) return "Game wins."; return string.Empty; }

 

 

 

Now, let’s refactor. While other issues may exist, the only duplication I see right now is that our new error checking duplicates knowledge about the size of the board. Since we need to modify this anyway, let’s go ahead and pull this section out into a separate method which describes what our intentions are:

public string ChoosePosition(int position) { if (IsOutOfRange(position)) { return "That spot is invalid!"; } if (_layout[position - 1] != '\0') { return "That spot is taken!"; } _layout[position - 1] = 'X'; int firstUnoccupied = Enumerable.Range(0, _layout.Length) .First(p => _layout[p].Equals('\0')); _layout[firstUnoccupied] = 'O'; if (WinningPlayerIs('X')) return "Player wins!"; if (WinningPlayerIs('O')) return "Game wins."; return string.Empty; } bool IsOutOfRange(int position) { return position < 1 || position > _layout.Count(); }
 

 

Let’s move on to our next test to describe what happens when the game goes first:

[TestClass] public class When_the_game_goes_first { [TestMethod] public void it_should_put_an_X_in_one_of_the_available_positions() { } }

To check that the game puts an ‘X’ in one of the positions, let’s use another enumerable range to check all of the positions for the expected value:

[TestMethod] public void it_should_put_an_X_in_one_of_the_available_positions() { Assert.IsTrue(Enumerable.Range(1, 9) .Any(position => game.GetPosition(position).Equals('X'))); }

Right now, our game only moves after we’ve chosen a position. We need a way of telling the game to go first, so let’s call a method called GoFirst():

[TestMethod] public void it_should_put_an_X_in_one_of_the_available_positions() { var game = new Game(); game.GoFirst(); Assert.IsTrue(Enumerable.Range(1, 9) .Any(position => game.GetPosition(position).Equals('X'))); }

Next, we’ll need to add our new method:

public class Game { // ... public void GoFirst() { } }

We’re ready to run the tests:

 

When_the_game_goes_first Failed it_should_put_an_X_in_one_of_the_available_positions Assert.IsTrue failed.

At this point we may have some ideas about how we might implement this, but there isn’t an obvious way I can think of that would only take a few seconds to write, so let’s Fake It again:

public void GoFirst() { _layout[0] = 'X'; }
 

 

Refactor time! As a first step, let’s copy the code we’re using in the ChoosePosition() to find the first available position and use it to assign the value ‘X’:

public void GoFirst() { int firstUnoccupied = Enumerable.Range(0, _layout.Length) .First(p => _layout[p].Equals('\0')); _layout[firstUnoccupied] = 'X'; }
 

 

Next, let’s factor out a method to remove the duplication between these two methods:

void SelectAPositionFor(char value) { int firstUnoccupied = Enumerable.Range(0, _layout.Length) .First(p => _layout[p].Equals('\0')); _layout[firstUnoccupied] = value; }

Now we can replace the locations in the ChoosePosition() and GoFirst() methods to call our new method:

public string ChoosePosition(int position) { if (IsOutOfRange(position)) { return "That spot is invalid!"; } if (_layout[position - 1] != '\0') { return "That spot is taken!"; } _layout[position - 1] = 'X'; SelectAPositionFor('O'); if (WinningPlayerIs('X')) return "Player wins!"; if (WinningPlayerIs('O')) return "Game wins."; return string.Empty; } public void GoFirst() { SelectAPositionFor('X'); }
 

 

We now have two places where the game determines what token it’s using, so let’s fix this. Let’s add a new method called GetTokenFor() which will determine whether the game is assigned an ‘X’ or an ‘O’. We’ll pass it a string of “game”, but we’ll just hard-code it to assign ‘X’ for now and see where this takes us:

public void GoFirst() { char token = GetTokenFor("game"); SelectAPositionFor(token); } char GetTokenFor(string player) { return 'X'; }
 

 

In order for our GetTokenFor() method to assign a token conditionally, it will need some way of figuring out who’s going first. If we keep track of the assignments in a dictionary, then this should be fairly straight forward:

Dictionary<string, char> _tokenAssignments = new Dictionary<string, char>(); char GetTokenFor(string player) { var nextToken = (_tokenAssignments.Count == 0) ? 'X' : 'O'; if (_tokenAssignments.ContainsKey(player)) return _tokenAssignments[player]; return _tokenAssignments[player] = nextToken; } }
 

 

Next, let’s change the ChoosePosition() method to use our new method instead of the hard-coded assignments:

public string ChoosePosition(int position) { if (IsOutOfRange(position)) { return "That spot is invalid!"; } if (_layout[position - 1] != '\0') { return "That spot is taken!"; } _layout[position - 1] = GetTokenFor("player"); SelectAPositionFor(GetTokenFor("game")); if (WinningPlayerIs('X')) return "Player wins!"; if (WinningPlayerIs('O')) return "Game wins."; return string.Empty; }

 

 

 

These changes have introduced some duplication in the form of magic strings, so let’s get rid of that. We can define an Enum to identify our players rather than using strings:

public enum Player { Human, Game }

Now we can change our dictionary, the GetTokenFor() method parameter type and the calls to GetTokenFor() from the ChoosePosition() and GoFirst() methods to use the new Enum:

readonly Dictionary<Player, char> _tokenAssignments = new Dictionary<Player, char>(); char GetTokenFor(Player player) { char nextToken = (_tokenAssignments.Count == 0) ? 'X' : 'O'; if (_tokenAssignments.ContainsKey(player)) return _tokenAssignments[player]; return _tokenAssignments[player] = nextToken; } public string ChoosePosition(int position) { if (IsOutOfRange(position)) { return "That spot is invalid!"; } if (_layout[position - 1] != '\0') { return "That spot is taken!"; } _layout[position - 1] = GetTokenFor(Player.Human); SelectAPositionFor(GetTokenFor(Player.Game)); if (WinningPlayerIs('X')) return "Player wins!"; if (WinningPlayerIs('O')) return "Game wins."; return string.Empty; } public void GoFirst() { char token = GetTokenFor(Player.Game); SelectAPositionFor(token); }

 

 

 

Now that we know this works, let’s refactor the rest of the methods that are still relying upon character values to identify the player along with their associated calls:

bool WinningPlayerIs(Player player) { return _winningPatterns .Any(pattern => Regex.IsMatch(GetLayoutFor(player), pattern)); } string GetLayoutFor(Player player) { return new string(_layout.ToList() .Select(c => (c.Equals(GetTokenFor(player))) ? GetTokenFor(player) : '\0') .ToArray()); } void SelectAPositionFor(Player player) { int firstUnoccupied = Enumerable.Range(0, _layout.Length) .First(p => _layout[p].Equals('\0')); _layout[firstUnoccupied] = GetTokenFor(player); } public void GoFirst() { SelectAPositionFor(Player.Game); } public string ChoosePosition(int position) { if (IsOutOfRange(position)) { return "That spot is invalid!"; } if (_layout[position - 1] != '\0') { return "That spot is taken!"; } _layout[position - 1] = GetTokenFor(Player.Human); SelectAPositionFor(Player.Game); if (WinningPlayerIs(Player.Human)) return "Player wins!"; if (WinningPlayerIs(Player.Game)) return "Game wins."; return string.Empty; }
 

 

Here’s what we have so far:

public class Game { readonly char[] _layout = new char[9]; readonly Dictionary<Player, char> _tokenAssignments = new Dictionary<Player, char>(); readonly string[] _winningPatterns = new[] { "[XO][XO][XO]......", "...[XO][XO][XO]...", "......[XO][XO][XO]", "[XO]..[XO]..[XO]..", ".[XO]..[XO]..[XO].", "..[XO]..[XO]..[XO]", "[XO]...[XO]...[XO]", "..[XO].[XO].[XO]..", }; public string ChoosePosition(int position) { if (IsOutOfRange(position)) { return "That spot is invalid!"; } if (_layout[position - 1] != '\0') { return "That spot is taken!"; } _layout[position - 1] = GetTokenFor(Player.Human); SelectAPositionFor(Player.Game); if (WinningPlayerIs(Player.Human)) return "Player wins!"; if (WinningPlayerIs(Player.Game)) return "Game wins."; return string.Empty; } bool IsOutOfRange(int position) { return position < 1 || position > _layout.Count(); } bool WinningPlayerIs(Player player) { return _winningPatterns .Any(pattern => Regex.IsMatch(GetLayoutFor(player), pattern)); } string GetLayoutFor(Player player) { return new string(_layout.ToList() .Select(c => (c.Equals(GetTokenFor(player))) ? GetTokenFor(player) : '\0') .ToArray()); } public char GetPosition(int position) { return _layout[position - 1]; } public void GoFirst() { SelectAPositionFor(Player.Game); } char GetTokenFor(Player player) { char nextToken = (_tokenAssignments.Count == 0) ? 'X' : 'O'; if (_tokenAssignments.ContainsKey(player)) return _tokenAssignments[player]; return _tokenAssignments[player] = nextToken; } void SelectAPositionFor(Player player) { int firstUnoccupied = Enumerable.Range(0, _layout.Length) .First(p => _layout[p].Equals('\0')); _layout[firstUnoccupied] = GetTokenFor(player); } }

We’ve only got two more requirements to go, but we’ll leave things here for now. Next time, we’ll complete our requirements by tackling what looks to be the most interesting portion of our game and perhaps we’ll discover a solution to our coupling woes in the process.

Tagged with: