I am relitively new to C#.
On thursday I started out writing a Tic-tac-toe game. Just a basic gridbuilder, ask the user where they want to go, alternate the users, as player2 where he wabts to go (checking if the move is 'legal'). I've just kept adding to it... This leads to a set of well labelled functions, but not always in the most logically arranged order.
Today i attempted to write AI that was impossible to beat, using the MiniMax alogorithm. This new code can be found in the methods:
MiniMax
ValueCell
GridSpace
RotateGo
CheckCurrentWin
I have placed these at the very bottom of the code for ease.
There is a bug somewhere in this program that makes the AI play out not just it's move, but the entire game (after you have taken your starting go).
MiniMax is called by the SinglePlayerUpdate Method (Ln650).
Note: When asked for a grid location to go in,
a,b,c respond to the 3 vertical columns respectively.
1,2,3 respond to the 3 horizontal rows respectively.
E.g. entering "c3" would place your marker in the bottom right of the grid.
Thankyou in advance for any help!
/*
* Tic-Tac-Toe
* J Keegan
* 10/12/09
*/
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace OXO
{
class Program
{
static int width = 3;
static int height = 3;
static char[] OX = new char[width * height];
static string spacer = " | ";
static char go;
static char computerPlayer = 'O';
static int games;
static int gameNumber;
static int scoreX;
static int scoreO;
static char lastWinner;
static void Main(string[] args)
{
bool playing = true;
while (playing)
{
Initialise();
//Would you like to play single player or multiplayer?
int userChoice = MenuLevel0();
Console.Clear();
//Multi-Player
if (userChoice == 1)
{
while (gameNumber <= games)
{
Initialise();
//If we are into the second round or higher of a tournament, set the starting user to the player who loast last time:
if (gameNumber > 1)
{
go = lastWinner;
}
string grid = GridBuilder();
Console.Write(grid);
bool inPlay = true;
while (inPlay)
{
Update();
inPlay = !GameOver(userChoice);
}
gameNumber++;
Wait();
}
//If it was a tounament, declare the victor!
TourniVictor();
}
//Single-Player
else if (userChoice == 2)
{
while (gameNumber <= games)
{
Initialise();
//If we are into the second round or higher of a tournament, set the starting user to the player who loast last time:
if (gameNumber > 1)
{
go = lastWinner;
}
string grid = GridBuilder();
Console.Write(grid);
bool inPlay = true;
while (inPlay)
{
SinglePlayerUpdate();
inPlay = !GameOver(userChoice);
}
gameNumber++;
Wait();
}
//If it was a tounament, declare the victor!
TourniVictor();
}
else if (userChoice == -1)
{
Console.WriteLine("Thankyou for playing!\nGoodbye!");
playing = false;
}
}
}
static int MenuLevel0()
{
string userChoice;
//Console.Clear();
Console.WriteLine("Welcome to Tic-Tac-Toe!");
Console.WriteLine("1. Play Multi-Player \n2. Play Single-Player\nQ. Quit Playing");
do
{
Console.Write("Please enter your choice: ");
userChoice = Console.ReadLine();
}
while (!(userChoice == "1" || userChoice == "2" || userChoice.ToLower() == "q"));
//convert to integer value
int selection;
if (int.TryParse(userChoice, out selection))
{
}
//For values that are actually letters, assign an equivelent integer value, for simplicity
//KEY:
//QUIT q -1
else
{
selection = -1;
}
//if the user wants to play:
if (selection > 0)
{
MenuLevel1();
}
return selection;
}
static void MenuLevel1()
{
int userInput;
Console.Clear();
Console.WriteLine("How many games would you like to play?");
Console.WriteLine("1. Just the one game thanks!");
Console.WriteLine("2. A mini-tournament please!");
do
{
Console.Write("Enter your selection: ");
}
while (!(int.TryParse(Console.ReadLine(), out userInput)) && !(userInput<=2 && userInput>=1));
if (userInput == 2)
{
do
{
Console.Clear();
Console.Write("Enter How Many Games You'd Like To Play: ");
}
while (!int.TryParse(Console.ReadLine(), out userInput));
//Set up the tourni values
InitialiseTournament(userInput);
}
}
static void TourniVictor()
{
//If we just finished a tournament, declare the victor:
if (games > 1)
{
Console.Clear();
char winner;
//if X won
if (scoreX > scoreO)
{
winner = 'X';
}
//If O won
else if (scoreO > scoreX)
{
winner = 'O';
}
//If it was a draw
else
{
winner = 'd';
}
//If it was a draw:
if (winner == 'd')
{
Console.WriteLine("Alas, a draw!");
}
//Else declave the victor:
else
{
Console.WriteLine("Congratulations {0}! You won the Tournament :)", winner);
}
//reset the values
InitialiseTournament(1);
Wait();
}
}
static void InitialiseTournament(int numberOfGames)
{
games = numberOfGames;
gameNumber = 1;
scoreO = 0;
scoreX = 0;
}
static void Wait()
{
Console.WriteLine("\nPress any key to continue . . .");
Console.ReadKey();
Console.Clear();
}
static int AI()
{
AlternateUsers(); char enemy = go; AlternateUsers();
int[] winPossibilityPriority = new int[8];
int k = 0;
//Horizontal Wins:
for (int i = 0; i < OX.Length; i+=width, k++)
{
//construct a string which contains the char array
string contents = "";
for (int j = 0; j < width; j++)
{
contents += OX[i + j];
}
//search for an empty cell. If it has one continue checking. Else move on.
if (contents.Contains(" "))
{
if (contents.Contains(enemy))
{
int numberOfEnemyPositions = 0;
int numberOfFriendlyPositions = 0;
//count how many enemies && friendlies are in the 3 cells.
for (int j = 0; j < width; j++)
{
if (contents[j] == enemy)
{
numberOfEnemyPositions++;
}
if (contents[j] == go)
{
numberOfFriendlyPositions++;
}
}
//Prioritise this 3 cell group (win possibility)
winPossibilityPriority[k] = numberOfEnemyPositions - numberOfFriendlyPositions;
}
}
else
{
//we cannot go here.
winPossibilityPriority[k] = 0;
}
}
//Vertical Wins:
for (int i = 0; i < width; i++, k++)
{
//construct a string which contains the char array
string contents = "";
for (int j = 0; j < width; j++)
{
contents += OX[i + (j*width)];
}
//search for an empty cell. If it has one continue checking. Else move on.
if (contents.Contains(" "))
{
if (contents.Contains(enemy))
{
int numberOfEnemyPositions = 0;
int numberOfFriendlyPositions = 0;
//count how many enemies && friendlies are in the 3 cells.
for (int j = 0; j < width; j++)
{
if (contents[j] == enemy)
{
numberOfEnemyPositions++;
}
if (contents[j] == go)
{
numberOfFriendlyPositions++;
}
}
//Prioritise this 3 cell group (win possibility)
winPossibilityPriority[k] = numberOfEnemyPositions - numberOfFriendlyPositions;
}
}
else
{
//we cannot go here.
winPossibilityPriority[k] = 0;
}
}
//Diagonal Wins:
//Top Left to Bottom Right:
//construct a string which contains the char array
for (int i = 0; i < 1; i++, k++)//C# won't let me use contents string unless it's inside a child here... C# is great...
{
string contents = "";
for (int j = 0; j < width; j++)
{
contents += OX[j * 4];
}
//search for an empty cell. If it has one continue checking. Else move on.
if (contents.Contains(" "))
{
if (contents.Contains(enemy))
{
int numberOfEnemyPositions = 0;
int numberOfFriendlyPositions = 0;
//count how many enemies && friendlies are in the 3 cells.
for (int j = 0; j < width; j++)
{
if (contents[j] == enemy)
{
numberOfEnemyPositions++;
}
if (contents[j] == go)
{
numberOfFriendlyPositions++;
}
}
//Prioritise this 3 cell group (win possibility)
winPossibilityPriority[k] = numberOfEnemyPositions - numberOfFriendlyPositions;
}
}
else
{
//we cannot go here.
winPossibilityPriority[k] = 0;
}
}
//Top Right to Bottom Left:
//construct a string which contains the char array
for (int i = 0; i < 1; i++, k++)//C# won't let me use contents string unless it's inside a child here... C# is great...
{
string contents = "";
for (int j = 2; j <= 6 ; j+=2)
{
contents += OX[j];
}
//search for an empty cell. If it has one continue checking. Else move on.
if (contents.Contains(" "))
{
if (contents.Contains(enemy))
{
int numberOfEnemyPositions = 0;
int numberOfFriendlyPositions = 0;
//count how many enemies && friendlies are in the 3 cells.
for (int j = 0; j < width; j++)
{
if (contents[j] == enemy)
{
numberOfEnemyPositions++;
}
if (contents[j] == go)
{
numberOfFriendlyPositions++;
}
}
//Prioritise this 3 cell group (win possibility)
winPossibilityPriority[k] = numberOfEnemyPositions - numberOfFriendlyPositions;
}
}
else
{
//we cannot go here.
winPossibilityPriority[k] = 0;
}
}
//NOW ALL POSSIBLE WAYS TO WIN HAVE BEEN EVALUATED ONCE.
//Everything has been giver a value of 2, 1, 0, -1 or 2
//WIN OR LOSE EVENTS:
//-2 means there are 2 allies in this sector. Any sector with this value means that one move will win the game!
//2 means there are 2 enemies in this sector and so is High Priority! One move will stop the enemy winning via this sector.
//NORMAL GAMEPLAY:
//-1 means there is one ally in this sector. Going here will make this -2 for the enemy, during standard gameplay this is the highest value sector.
//1 means there is an enemy in this sector. Low priority event.
//0 means there is no value to playing in this sector. Either there is one enemy & one ally, or there is neither an ally nor an enemy.
//Rearrange these into more readable priorities:
for (int i = 0; i < winPossibilityPriority.Length; i++)
{
if (winPossibilityPriority[i] == -1)
{
winPossibilityPriority[i] = 2;
}
else if (winPossibilityPriority[i] == 2)
{
winPossibilityPriority[i] = 3;
}
else if (winPossibilityPriority[i] == -2)
{
winPossibilityPriority[i] = 4;
}
}
//NEW VALUES:
//WIN OR LOSE EVENTS:
//4 means there are 2 allies in this sector. Any sector with this value means that one move will win the game!
//3 means there are 2 enemies in this sector and so is High Priority! One move will stop the enemy winning via this sector.
//NORMAL GAMEPLAY:
//2 means there is one ally in this sector. Going here will make this -2 for the enemy, during standard gameplay this is the highest value sector.
//1 means there is an enemy in this sector. Low priority event.
//0 means there is no value to playing in this sector. Either there is one enemy & one ally, or there is neither an ally nor an enemy.
//FIND THE HIGHEST PRIORITY SECTOR:
int highPriorityValue = 0;
int highPrioritySector = 0;
for (int i = 0; i < winPossibilityPriority.Length; i++)
{
if (highPriorityValue < winPossibilityPriority[i])
{
highPriorityValue = winPossibilityPriority[i];
highPrioritySector = i;
}
}
//The highest priority sector is now listed as a location in the array winPosibilityPriority
//FIND THE ACTUAL GRID LOCATIONS:
//(relative to the OX array)
int[] gridLocations = new int[3];
//If it's horizontal
if (highPrioritySector < 3)
{
gridLocations[0] = highPrioritySector * 3;
for (int i = 1; i < 3; i++)
{
gridLocations[i] = gridLocations[0] + i;
}
}
//If it's vertical
else if (highPrioritySector < 6)
{
gridLocations[0] = highPrioritySector - 3;
for (int i = 1; i < 3; i++)
{
gridLocations[i] = gridLocations[0] + (i * width);
}
}
//If it's diagonal
else if (highPrioritySector == 6)
{
gridLocations[0] = 0;
gridLocations[1] = 4;
gridLocations[2] = 8;
}
else if (highPrioritySector == 7)
{
gridLocations[0] = 2;
gridLocations[1] = 4;
gridLocations[2] = 6;
}
//NOW GO THROUGH THE INDIVIDUAL VALUES OF THE HIGH PRIORITY SECTOR AND TAKE YOUR GO
for (int i = 0; i < 3; i++)
{
if (OX[gridLocations[i]] == ' ')
{
return gridLocations[i];
}
}
//IF YOU GET THIS FAR, IT'S FUBAR
//Give an *Important* error warning
Console.Beep();
//Return a value to prevent application hanging
return 4;
}
static void KeepScore()
{
if (go == 'X')
{
scoreX++;
}
else
{
scoreO++;
}
}
static bool GameOver(int gameType)
{
if (gameType == 1)
{
if (CheckWinCriteria() == true)
{
//Write Out the new GridBuilder, as CheckWinCriteria will have placed hitmarkers on the winning cells
Console.Clear();
Console.Write(GridBuilder());
Console.WriteLine("Well done {0}, you win!", go);
KeepScore();
TournamentUpdate(false);
//Determine the player who starts next time (if its a tourni)
lastWinner = go;
return true;
}
}
else if (gameType == 2)
{
if (CheckWinCriteria() == true)
{
if (go == computerPlayer)
{
//Write Out the new GridBuilder, as CheckWinCriteria will have placed hitmarkers on the winning cells
Console.Clear();
Console.Write(GridBuilder());
Console.WriteLine("The computer won. Bad Luck!");
}
else
{
//Write Out the new GridBuilder, as CheckWinCriteria will have placed hitmarkers on the winning cells
Console.Clear();
Console.Write(GridBuilder());
Console.WriteLine("Well done, you win!");
}
KeepScore();
TournamentUpdate(true);
//Determine the player who starts next time (if its a tourni)
lastWinner = go;
return true;
}
}
if (CheckDrawCriteria() == true)
{
Console.WriteLine("Stand down, it's a draw...");
return true;
}
return false;
}
static bool CheckDrawCriteria()
{
for (int i = 0; i < OX.Length; i++)
{
if (OX[i] == ' ')
{
return false;
}
}
return true;
}
static bool CheckWinCriteria()
{
char winMarker = '*';
//Check left column, and then along the rows horizontally
for (int j = 0; j < OX.Length; j += 3)
{
//If all cell's on the horizontal row hits. Game over.
if (OX[j] == go && OX[j + 1] == go && OX[j + 2] == go)
{
OX[j] = winMarker;
OX[j + 1] = winMarker;
OX[j + 2] = winMarker;
return true;
}
}
//Check 1st row, and then down the columns vertically
for (int j = 0; j < width; j++)
{
if (OX[j] == go && OX[j + width] == go && OX[j + 2 * width] == go)
{
OX[j] = winMarker;
OX[j + width] = winMarker;
OX[j + 2 * width] = winMarker;
return true;
}
}
//Check diaganol lines
//check for the diagonal starting in the top left
if (OX[0] == go && OX[width + 1] == go && OX[2 * (width + 1)] == go)
{
OX[0] = winMarker;
OX[width + 1] = winMarker;
OX[2 * (width + 1)] = winMarker;
return true;
}
//check for the diagonal starting in the top right
if (OX[width - 1] == go && OX[2 * (width - 1)] == go && OX[3 * (width - 1)] == go)
{
OX[width - 1] = winMarker;
OX[2 * (width - 1)] = winMarker;
OX[3 * (width - 1)] = winMarker;
return true;
}
return false;
}
static void TournamentUpdate(bool singlePlayer)
{
//If we are in a 'tournament'
if (games > 1)
{
Console.WriteLine("Game {0} of {1}", gameNumber, games);
Console.WriteLine("Scores:");
string X = "";
string O = "";
if (singlePlayer)
{
X = (computerPlayer == 'X' ? "Computer" : "You");
O = (computerPlayer == 'O' ? "Computer" : "You");
}
else
{
X = "X";
O = "O";
}
Console.WriteLine("{0}: {1}", X, scoreX);
Console.WriteLine("{0}: {1}", O, scoreO);
}
}
static void Update()
{
TournamentUpdate(false);
AlternateUsers();
Console.WriteLine("{0}'s go!", go);
int i;
do
{
i = MakeArrayPosition(GetCoOrds());
}
while (OX[i] != ' ');
OX[i] = go;
Console.Clear();
Console.Write(GridBuilder());
}
static void SinglePlayerUpdate()
{
TournamentUpdate(true);
AlternateUsers();
if (go != computerPlayer)
{
Console.WriteLine("Your go! You are {0}", go);
int i;
do
{
i = MakeArrayPosition(GetCoOrds());
}
while (OX[i] != ' ');
OX[i] = go;
}
else
{
Console.WriteLine("The computer is taking it's turn...");
//OX[AI()] = go;
OX[MiniMax()] = go;
}
Console.Clear();
Console.Write(GridBuilder());
}
static void AlternateUsers()
{
if (go == 'O')
{
go = 'X';
}
else
{
go = 'O';
}
}
static int MakeArrayPosition(int[] coOrds)
{
int i = 0;
//check first row
for (int k = 0; k < coOrds[1]; k++)
{
if (k + 1 == coOrds[1])
{
for (int j = 1; j < coOrds[0]; j++, i++)
{
}
}
else
{
for (int j = 0; j < width; j++, i++)
{
}
}
}
return i;
}
static int[] GetCoOrds()
{
bool inPlay = true;
while (inPlay)
{
int[] coOrds = CheckCoOrdinates(GetCoOrdinates());
if (coOrds[0] != 0)
{
return coOrds;
}
}
int[] csDoesntWork = new int[2];
return csDoesntWork;
}
static int[] CheckCoOrdinates(string rawInput)
{
int[] coOrds = new int[2];
//horizontal co-ordinate
if (rawInput.ToLower().Contains("a"))
{
coOrds[0] = 1;
}
else if (rawInput.ToLower().Contains("b"))
{
coOrds[0] = 2;
}
else if (rawInput.ToLower().Contains("c"))
{
coOrds[0] = 3;
}
//when coOrds[0] == 0, this is an error!
else
{
coOrds[0] = 0;
}
//Vertical co-ordinate
if (rawInput.ToLower().Contains("1"))
{
coOrds[1] = 1;
}
else if (rawInput.ToLower().Contains("2"))
{
coOrds[1] = 2;
}
else if (rawInput.ToLower().Contains("3"))
{
coOrds[1] = 3;
}
//when coOrds[0] == 0, this is an error!
else
{
coOrds[0] = 0;
}
return coOrds;
}
static string GetCoOrdinates()
{
string coOrds;
Console.Write("Enter the co-ordinates you want to make an entry in: ");
coOrds = Console.ReadLine();
return coOrds;
}
static string GridBuilder()
{
string grid = "";
for (int i = 0; i < OX.Length; i++)
{
grid += OX[i];
if ((i + 1) % width == 0)
{
grid += "\n";
if ((i + 1) != OX.Length)
{
for (int j = 0; j < (spacer.Length * width); j++)
{
grid += "-";
}
}
grid += "\n";
}
else
{
grid += spacer;
}
}
return grid;
}
static void Initialise()
{
for (int i = 0; i < OX.Length; i++)
{
OX[i] = ' ';
}
go = 'O';
}
static int MiniMax()
{
int bestGo = 0;
int[] terminalValues = new int[OX.Length];
//determine the terminal value of going in each cell:
for (int i = 0; i < terminalValues.Length; i++)
{
terminalValues[i] = ValueCell(i, OX, go);
}
//FIND & RETURN THE BEST GO:
for (int i = 0; i < terminalValues.Length; i++)
{
bestGo = Math.Max(bestGo, terminalValues[i]);
}
return bestGo;
}
static int ValueCell(int cell, char[] currentGrid, char whosGo)
{
//pass OX as the current grid if it's the first time, else make ammendments to the 'current grid' where OX is left as the actual inPlay values
int cellValue = -2;
//If this would be an illegal move:
if (currentGrid[cell] != ' ')
{
return -2;
}
//Else if it's a legal move:
currentGrid[cell] = whosGo;
//If the currentGrid is full (we are at the last fork of the process)
if (!GridSpace(currentGrid))
{
//If in the boards current state teh computer wins, return a value of 1
if (CheckCurrentWin(currentGrid, computerPlayer))
{
return 1;
}
//If in the boards current state, the human wins, return a value of -1
else if (CheckCurrentWin(currentGrid, RotateGo(computerPlayer)))
{
return -1;
}
//Else we drew, return 0
else
{
return 0;
}
}
//declare an array of cell values for each possible move & make them 'impossible'
int[] goValue = new int[9];
for (int i = 0; i < goValue.Length; i++)
{
goValue[i] = -2;
}
//now set those that are possible to their actual values
for (int i = 0; i < currentGrid.Length; i++)
{
if (currentGrid[i] == ' ')
{
goValue[i] = ValueCell(i, currentGrid, RotateGo(whosGo));
}
}
//Now find the highest possible outcome by making this move:
for (int i = 0; i < goValue.Length; i++)
{
cellValue = Math.Max(cellValue, goValue[i]);
}
//return trhe cell's value to the parent fork.
return cellValue;
}
static bool GridSpace(char[] currentGrid)
{
foreach (char cell in currentGrid)
{
if (cell == ' ')
{
return true;
}
}
return false;
}
static char RotateGo(char currentGo)
{
if (currentGo == 'X')
{
currentGo = 'O';
}
else
{
currentGo = 'X';
}
return currentGo;
}
static bool CheckCurrentWin(char[] currentGrid, char whosGo)
{
//Check left column, and then along the rows horizontally
for (int j = 0; j < currentGrid.Length; j += 3)
{
//If all cell's on the horizontal row hits. Game over.
if (currentGrid[j] == whosGo && currentGrid[j + 1] == whosGo && currentGrid[j + 2] == whosGo)
{
return true;
}
}
//Check 1st row, and then down the columns vertically
for (int j = 0; j < width; j++)
{
if (currentGrid[j] == whosGo && currentGrid[j + width] == whosGo && currentGrid[j + 2 * width] == whosGo)
{
return true;
}
}
//Check diaganol lines
//check for the diagonal starting in the top left
if (currentGrid[0] == whosGo && currentGrid[width + 1] == whosGo && currentGrid[2 * (width + 1)] == whosGo)
{
return true;
}
//check for the diagonal starting in the top right
if (currentGrid[width - 1] == whosGo && currentGrid[2 * (width - 1)] == whosGo && currentGrid[3 * (width - 1)] == whosGo)
{
return true;
}
return false;
}
}
}