Now that we have our move class setup lets start by adding an IGame.cs file to our interfaces older and a Game.cs file into our models folder.
next lets open up our IGame interface and define it.
using System;
using System.Collections.Generic;
namespace tictactoe.game.interfaces{
public interface IGame
{
IList<IMove> Moves {get; set;}
void ExecuteMove(IMove move);
Nullable<char> Winner {get;}
Nullable<char> GameOver();
IMove RandomResponse();
IMove SimpleResponse();
IMove ExpertResponse();
}
}
we track all the moves in a sequence, this will help us ensure that a duplicate move isn't submitted and that all the moves appear in the correct order.
next we have a function that tries to execute a move
we have a property to store who won the game
and a function to check if the game is over and return a winner if it has
then we have our three response moves that we would suggest based on the input.
- random: a random valid move
- simple: a move that will either win the game, or try to block the opponent
- expert: will make the best possible move to either win or tie the game
with our interface definition complete lets implement it in our game class, initially we are going to leave out our three IMove functions and only focus on building our game.
I tried to focus on keeping the code as compact as possible to limit how much we have to go over.
using System;
using System.Linq;
using System.Collections.Generic;
using tictactoe.game.interfaces;
namespace tictactoe.game.models{
public class Game : IGame
{
protected int[,] grid;
public IList<IMove> Moves { get; set; }
public char? Winner {get; private set;}
public Game(){
this.grid = new int[3,3];
this.Moves = new List<IMove>();
this.Winner = null;
}
public Game(IMove[] moves) : this() {
for(var i = 0; i< moves.Length; i++){
this.ExecuteMove(moves[i]);
if(i > 3 && (Winner = GameOver()) != null)
break;
}
}
public void ExecuteMove(IMove move) {
if(Moves.Count() > 0){
if(Moves.Contains(move))
throw new Exception("Tic Tac Toe duplicate move violation");
if(Moves.Last().Symbol == move.Symbol)
throw new Exception($"Tic Tac Toe series violation; two {move.Symbol} moves cannot be made in a row");
}
Moves.Add(move);
int x = move.Coordinates.X;
int y = move.Coordinates.Y;
var symbol = Char.ToLower(move.Symbol);
grid[x,y] = symbol == 'x' ? 1 : -1;
}
public char? GameOver() {
//test rows
for(var x = 0; x < 3; x++){
var total = 0;
for(var y = 0; y < 3; y++)
if(Math.Abs(total += grid[x,y]) == 3)
return total == 3 ? 'x' : 'o';
}
//test cols
for(var y = 0; y < 3; y++) {
var total = 0;
for(var x = 0; x < 3; x++)
if(Math.Abs(total += grid[x,y]) ==3)
return total == 3 ? 'x' : 'o';
}
//test / diagnal
var forwardSlashTotal = grid[0,2] + grid[1,1] + grid[2,0];
if(forwardSlashTotal == 3)
return 'x';
else if(forwardSlashTotal == -3)
return 'o';
//test \ diagnal
var backSlashTotal = grid[0,0] + grid[1,1] + grid[2,2];
if(backSlashTotal == 3)
return 'x';
else if(backSlashTotal == -3)
return 'o';
return null;
}
public IMove ExpertResponse() { throw new System.NotImplementedException(); }
public IMove RandomResponse() { throw new System.NotImplementedException(); }
public IMove SimpleResponse() { throw new System.NotImplementedException(); }
}
}
Lets break these down bit by bit by first starting with our parameter-less constructor
public Game(){
this.grid = new int[3,3];
this.Moves = new List<IMove>();
this.Winner = null;
}
To test this little guy we use the following
[Fact]
public void Test_Game_Creation_No_Paramters() {
var game = new Game();
Assert.Empty(game.Moves);
Assert.False(game.Winner.HasValue);
BindingFlags bindFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static;
FieldInfo gridField = typeof(Game).GetField("grid", bindFlags);
Assert.Equal(new int[3,3], gridField.GetValue(game));
}
What we do here is ensure that when we create a new Game it's instantiated correctly, that is
- the Moves collection is empty
- No winner is assigned by default
- Our grid is zero'd out
Now a TDD zealot would tell you that you should never test private variables, to which my response is go F@#k yourself.
enough said? let's continue to our IMoves[] constructor
public Game(IMove[] moves) : this() {
for(var i = 0; i< moves.Length; i++){
this.ExecuteMove(moves[i]);
if(i > 3 && (Winner = GameOver()) != null)
break;
}
}
Now we test this constructor basically in every other test we'll write, but in essence all it does is take in an array of moves and executes them then once you have executed at least 5 moves checks if there is a winner, if so it stops executing moves and stores the winner in the Winner property. you may wonder why 5? because there has to be a minimal of 5 moves for there to be a winner, if you are wondering how five, well that is because 0 counts thus [0,1,2,3,4].
Next lets take a look at our ExecuteMove function,
public void ExecuteMove(IMove move) {
if(Moves.Count() > 0){
if(Moves.Contains(move))
throw new Exception("Tic Tac Toe duplicate move violation");
if(Moves.Last().Symbol == move.Symbol)
throw new Exception($"Tic Tac Toe series violation; two {move.Symbol} moves cannot be made in a row");
}
Moves.Add(move);
int x = move.Coordinates.X;
int y = move.Coordinates.Y;
var symbol = Char.ToLower(move.Symbol);
grid[x,y] = symbol == 'x' ? 1 : -1;
}
now this is pretty straight forward, we check if this is the first move, if it is we skip the error checking at the game level and and just update our grid with a 1 for an X or a -1 for a Y.
and that's it so lets take a look at how we test our edge cases
[Fact]
public void Test_Game_Creation_with_series_violation(){
var expected = "Tic Tac Toe series violation; two x moves cannot be made in a row";
Exception ex = Assert.Throws<Exception>(() => new Game(new [] { new Move("0x"), new Move("1x") }));
Assert.Equal(expected, ex.Message);
}
[Fact]
public void Test_Game_Creation_with_duplication_violation(){
var expected = "Tic Tac Toe duplicate move violation";
Exception ex = Assert.Throws<Exception>(() => new Game(new [] { new Move("5O"), new Move("5o") }));
Assert.Equal(expected, ex.Message);
}
if we take a look at the first check we ensure that we do not submit duplicates moves, that is two player place a symbol at the same xy coordinates. Now if you are wondering how this is accomplished well its done by implementing the IEquatable<IMove> interface at the interface level
using System;
namespace tictactoe.game.interfaces{
public interface IMove : IEquatable<IMove> {
char Symbol {get; set;}
(int X, int Y) Coordinates {get; set;}
}
}
Rather simple, we just specify that the IMove interface will implement the IEquatable<IMove> interface, then whenever we implement IMove we have to also implement Equatable<IMove> .
using tictactoe.game.interfaces;
using System;
using System.Text.RegularExpressions;
using System.Diagnostics.CodeAnalysis;
namespace tictactoe.game.models
{
public class Move : IMove
{
// removed for brevity
public bool Equals([AllowNull] IMove other)
{
return this.Coordinates.X == other.Coordinates.X &&
this.Coordinates.Y == other.Coordinates.Y;
}
}
}
I Removed the IMove implementation for brevity, basically this will do a check that ensures that there is no move X or Y that has the same xy coordinates.
next lets look at the second check, which by ensuring that the previously added move does not have the same symbol it ensures that we do not submit to X's or to O's in a row
now finally in this post lets look at the GameOver function
public char? GameOver() {
//test rows
for(var x = 0; x < 3; x++){
var total = 0;
for(var y = 0; y < 3; y++)
if(Math.Abs(total += grid[x,y]) == 3)
return total == 3 ? 'x' : 'o';
}
//test cols
for(var y = 0; y < 3; y++) {
var total = 0;
for(var x = 0; x < 3; x++)
if(Math.Abs(total += grid[x,y]) ==3)
return total == 3 ? 'x' : 'o';
}
//test / diagnal
var forwardSlashTotal = grid[0,2] + grid[1,1] + grid[2,0];
if(forwardSlashTotal == 3)
return 'x';
else if(forwardSlashTotal == -3)
return 'o';
//test \ diagnal
var backSlashTotal = grid[0,0] + grid[1,1] + grid[2,2];
if(backSlashTotal == 3)
return 'x';
else if(backSlashTotal == -3)
return 'o';
return null;
}
this simply checks to see if there is a winner between each move, next lets look at our entire test class including the tests we already covered.
using System;
using System.Reflection;
using tictactoe.game.models;
using Xunit;
namespace tictactoe.unitTests
{
public class GameTests
{
[Fact]
public void Test_Game_Creation_No_Paramters()
{
var game = new Game();
Assert.Empty(game.Moves);
Assert.False(game.Winner.HasValue);
BindingFlags bindFlags = BindingFlags.Instance | BindingFlags.Public |
BindingFlags.NonPublic | BindingFlags.Static;
FieldInfo gridField = typeof(Game).GetField("grid", bindFlags);
Assert.Equal(new int[3,3], gridField.GetValue(game));
}
[Fact]
public void Test_Game_Creation_with_series_violation(){
var expected = "Tic Tac Toe series violation; two x moves cannot be made in a row";
Exception ex = Assert.Throws<Exception>(() => new Game(new [] { new Move("0x"), new Move("1x") }));
Assert.Equal(expected, ex.Message);
}
[Fact]
public void Test_Game_Creation_with_duplication_violation(){
var expected = "Tic Tac Toe duplicate move violation";
Exception ex = Assert.Throws<Exception>(() => new Game(new [] { new Move("5O"), new Move("5o") }));
Assert.Equal(expected, ex.Message);
}
[Fact]
public void Test_Game_X_wins_Top_Row(){
var game = new Game(new[]{
new Move("0X"), new Move("5o"), new Move("1x"), new Move("8o"), new Move("2x") });
Assert.True(game.Winner.HasValue);
Assert.Equal('x', game.Winner.Value);
}
[Fact]
public void Test_Game_O_wins_Top_Row(){
var game = new Game(new[]{
new Move("0o"), new Move("5x"), new Move("1o"), new Move("8x"), new Move("2o") });
Assert.True(game.Winner.HasValue);
Assert.Equal('o', game.Winner.Value);
}
[Fact]
public void Test_Game_X_wins_middle_column(){
var game = new Game(new[]{
new Move("1X"), new Move("5o"), new Move("4x"), new Move("8o"), new Move("7x")});
Assert.True(game.Winner.HasValue);
Assert.Equal('x', game.Winner.Value);
}
[Fact]
public void Test_Game_O_wins_Forward_Diagnal(){
var game = new Game(new[]{ new Move("6o"),
new Move("5x"), new Move("4o"), new Move("8x"), new Move("2O")});
Assert.True(game.Winner.HasValue);
Assert.Equal('o', game.Winner.Value);
}
[Fact]
public void Test_Game_X_wins_Backword_Diagnal(){
var game = new Game(new[]{
new Move("8X"), new Move("5o"), new Move("4x"), new Move("2o"), new Move("0x")});
Assert.True(game.Winner.HasValue);
Assert.Equal('x', game.Winner.Value);
}
}
}
but lets pay attention to the second 6 tests that check our winner function.
now lets execute our tests
and we see that our tests are passing, letting us code up our back-end without the need to constantly hit postman.
that is enough for this post, in the next post we will create our movement functions.
but first, lets checkin our changes, again no need for a tag
all we did was execute
git add .
git commit -m 'implemented game creation login'
git push
and now our code is safely stored up on our repository.