Now let's tackle our simple move logic, in the algorithm we are going to try and win the game if possible or block the opponent if they can win on the next move.
lets start with our simple move function
public IMove SimpleResponse() {
if(Moves.Count == 0)
return new Move("0x");
if(Moves.Count < 3)
return RandomResponse();
char symbol = (Moves.Count & 1) == 0 ? 'x' : 'o';
//b = 0 try to win
//b = 1 try to block
for(var b = 0; b < 2; b++) {
var multiplier = ((Moves.Count + b) & 1) == 0 ? 1 : -1;
//cols
for(var x = 0; x < 3; x++) {
var total = 0;
int yCandidate = -1;
for(var y = 0; y < 3; y++){
total += grid[x,y];
if(grid[x,y] == 0)
yCandidate = y;
if(y == 2 && total == (2 * multiplier))
return new Move { Symbol = symbol, Coordinates = (x, yCandidate) };
}
}
//rows
for(var y = 0; y < 3; y++) {
var total = 0;
int xCandidate = -1;
for(var x = 0; x < 3; x++){
total += grid[x,y];
if(grid[x,y] == 0)
xCandidate = x;
if(x == 2 && total == (2 * multiplier))
return new Move { Symbol = symbol, Coordinates = (xCandidate,y) };
}
}
//front slash /
var fsTotal = 0;
(int X, int Y) fsCanddiate = (-1,-1);
for(int x = 0, y = 2; x < 3; x++, y--){
fsTotal += grid[x, y];
if(grid[x, y] == 0)
fsCanddiate = (x,y);
if(x == 2 && fsTotal == (2 * multiplier))
return new Move { Symbol = symbol, Coordinates = fsCanddiate };
}
//backslash \ win
var bsTotal = 0;
(int X, int Y) bsCanddiate = (-1,-1);
for(int x = 0; x < 3; x++){
bsTotal += grid[x, x];
if(grid[x, x] == 0)
bsCanddiate = (x,x);
if(x == 2 && bsTotal == (2 * multiplier))
return new Move { Symbol = symbol, Coordinates = bsCanddiate };
}
}
return null;
}
now it takes no parameters but uses the classes grid 2d array which represents our board, since we make the assumption that x always goes first and if no moves are made the algo will try to take the center for x otherwise since it wont be until the 3rd move that a possible winning or loosing move is available the simple algo will make a random move until there is the chance of connecting three in a row.
as for the move logic it is broken down into 4 main sections:
- Columns
- Rows
- / diagonal
- \diagonal
each one of these should be refactored into its own function, but i am not in the mood for that.
now that we have our function written let's create some unit tests to validate our function
using tictactoe.game.models;
using Xunit;
namespace tictactoe.unitTests
{
public class SimpleTests{
[Fact]
public void Test_Simple_Response_X_wins_First_Column()
{
//Given
var game = new Game(new[]{ new Move("0x"), new Move("1o"), new Move("3x"), new Move("4o")});
//When
var move = game.SimpleResponse();
//Then
Assert.Equal('x', move.Symbol);
Assert.Equal((0,2), move.Coordinates);
}
[Fact]
public void Test_Simple_Response_X_wins_Second_Column()
{
//Given
var game = new Game(new[]{ new Move("1x"), new Move("0o"), new Move("4x"), new Move("8o")});
//When
var move = game.SimpleResponse();
//Then
Assert.Equal('x', move.Symbol);
Assert.Equal((1,2), move.Coordinates);
}
[Fact]
public void Test_Simple_Response_X_wins_Third_Column()
{
//Given
var game = new Game(new[]{ new Move("2x"), new Move("3o"), new Move("8x"), new Move("4o")});
//When
var move = game.SimpleResponse();
//Then
Assert.Equal('x', move.Symbol);
Assert.Equal((2,1), move.Coordinates);
}
[Fact]
public void Test_Simple_Response_O_wins_First_Column()
{
//Given
var game = new Game(new[]{ new Move("2x"), new Move("0o"), new Move("1x"), new Move("3o"), new Move("8x")});
//When
var move = game.SimpleResponse();
//Then
Assert.Equal('o', move.Symbol);
Assert.Equal((0,2), move.Coordinates);
}
[Fact]
public void Test_Simple_Response_X_Blocks_First_Column()
{
//Given
var game = new Game(new[]{ new Move("2x"), new Move("0o"), new Move("1x"), new Move("3o"), new Move("8x"), new Move("5o")});
//When
var move = game.SimpleResponse();
//Then
Assert.Equal('x', move.Symbol);
Assert.Equal((0,2), move.Coordinates);
}
[Fact]
public void Test_Simple_Response_X_Wins_First_Row()
{
//Given
var game = new Game(new[]{ new Move("2x"), new Move("4o"), new Move("1x"), new Move("3o") });
//When
var move = game.SimpleResponse();
//Then
Assert.Equal('x', move.Symbol);
Assert.Equal((0,0), move.Coordinates);
}
[Fact]
public void Test_Simple_Response_X_Wins_Front_Diagonal()
{
//Given
var game = new Game(new[] { new Move("6x"), new Move("8o"), new Move("4x"), new Move("0o")});
//When
var move = game.SimpleResponse();
//Then
Assert.Equal('x', move.Symbol);
Assert.Equal((2,0), move.Coordinates);
}
[Fact]
public void Test_Simple_Response_O_Blocks_Front_Diagonal()
{
//Given
var game = new Game(new[] { new Move("6x"), new Move("8o"), new Move("4x")});
//When
var move = game.SimpleResponse();
//Then
Assert.Equal('o', move.Symbol);
Assert.Equal((2,0), move.Coordinates);
}
[Fact]
public void Test_Simple_Response_X_Wins_Back_Diagonal()
{
//Given
var game = new Game(new[] {new Move("0x"), new Move("1o"), new Move("4x"), new Move("2o")});
//When
var move = game.SimpleResponse();
//Then
Assert.Equal('x', move.Symbol);
Assert.Equal((2,2), move.Coordinates);
}
[Fact]
public void Test_Simple_Response_O_Wins_Back_Diagonal()
{
//Given
var game = new Game(new [] {new Move("6x"), new Move("0o"),new Move("7x"), new Move("8o"), new Move("2x") });
//When
var move = game.SimpleResponse();
//Then
Assert.Equal('o', move.Symbol);
Assert.Equal((1,1), move.Coordinates);
}
[Fact]
public void Test_Simple_Response_X_Blocks_Back_Diagonal()
{
//Given
var game = new Game(new [] {new Move("6x"), new Move("0o"),new Move("7x"), new Move("8o") });
//When
var move = game.SimpleResponse();
//Then
Assert.Equal('x', move.Symbol);
Assert.Equal((1,1), move.Coordinates);
}
}
}
again these are not exhaustive tests, but they satisfy the purpose of this post, just notice that they are broken down into three sections, basically setup, execute, test, various testing frameworks use different terminology, however they all follow the basic three sections.
Next lets create our ultimate unbeatable algorithm.