My design goal was to try and make a "clean" and well-designed Tic-Tac-Toe game but also to try and use concepts and programming constructs that I'm new to.
I'm planning to implement an "AI Player" class later on. I know that Tic-Tac-Toe is a solved game so there are more effective approaches for AI logic but I've never programmed an AI before so my idea is to at runtime generate a "database" of all possible gamestates, and then have the AI rank each "intermediate gamestate" by the amount of positive outcomes (that is where the AI wins) - negative outcomes when it decides where to place its marker. Would this be a good idea and would it work at all?
Both positive and negative feedback is welcome.
Game.cs
public partial class Game : Form
{
#region Fields & Properties
internal static VisualCell[,] vGrid;
internal static Player[] players;
#endregion
#region Constructors
public Game()
{
InitializeComponent();
}
#endregion
#region Instanced Methods
private void Game_Load(object sender, EventArgs e)
{
players = new Player[2];
players[0] = new Player(); // Set to human player or AI player
players[1] = new Player(); // Always human
GameRound.Complete += ShowCompletionDialog;
GenerateGridBtns();
Task roundTask = Task.Factory.StartNew(() => new GameRound().Start());
}
private void ShowCompletionDialog(OutcomeType gameOutcome)
{
long pID = (long)gameOutcome + 1;
string WonText = "Player " + pID + " won!";
if (gameOutcome == OutcomeType.Draw)
{ //*HACK*: If the marker is empty it means that the passed player was a dummy and game was a draw
WonText = "The game ended in a draw!";
}
DialogResult dialogResult = MessageBox.Show("Play again?", WonText, MessageBoxButtons.YesNo);
if (dialogResult == DialogResult.Yes)
{
RestartGame();
// TODO: Add a bunch of resets here...
} else if (dialogResult == DialogResult.No)
{
//do something else
}
}
private void GenerateGridBtns()
{
vGrid = new VisualCell[3, 3];
for (int x = 0; x < 3; x++)
{
for (int y = 0; y < 3; y++)
{
vGrid[x, y] = new VisualCell()
{
AutoSize = true,
Location = new Point(x*110, y*110),
Size = new Size(124, 124),
Enabled = false,
Font = new Font(Font.FontFamily, 40),
CellPos = new Tuple<int,int>(x, y)
};
Controls.Add(vGrid[x, y]);
}
}
}
private void RestartGame()
{
for (int x = 0; x < 3; x++)
{
for (int y = 0; y < 3; y++)
{
vGrid[x, y].Invoke(new Action(() => vGrid[x, y].Text = String.Empty));
vGrid[x, y].Invoke(new Action(() => vGrid[x, y].Enabled = false));
}
}
var round = new GameRound();
Task roundTask = Task.Factory.StartNew(() => round.Start());
}
#endregion
}
VCell.cs
class VisualCell : Button
{
#region Fields & Properties
public Tuple<int, int> CellPos { get; set; }
protected override bool ShowFocusCues
{
get
{
return false;
}
}
#endregion
#region Constructors
public VisualCell(): base()
{
this.SetStyle(ControlStyles.Selectable, false);
}
#endregion
}
Cell.cs
class Cell
{
#region Properties & Fields
private Mark markType = Mark.Empty; /* Have to use old way of defining properties here unfortunately due to lack of support for,
default values in auto-implemented properties. */
public Mark MarkType {
get { return markType; }
set
{ //Only allow the type of the cell to change if it it's empty
if (MarkType == Mark.Empty)
{
markType = value;
}
}
}
#endregion
#region Constructors
public Cell()
{
MarkType = Mark.Empty;
}
#endregion
}
Grid.cs
enum OutcomeType
{
None = -1, CrossWin, NoughtWin, Draw
}
class Grid
{
const int MAX_CELLS = 3;
#region Properties & Fields
public Cell[,] cells { get; set; }
public OutcomeType Outcome { get; private set; }
#endregion
#region Constructors
public Grid()
{
Outcome = OutcomeType.None;
cells = new Cell[MAX_CELLS, MAX_CELLS];
for (int x = 0; x < MAX_CELLS; x++)
{
for (int y = 0; y < MAX_CELLS; y++)
{
cells[x, y] = new Cell();
}
}
}
#endregion
#region Instanced Methods
public bool CheckOutcome(Tuple<int, int> coords, Player player)
{
//Check for draws first
if (cells.GetEmptyCells().Length == 0)
{
Outcome = OutcomeType.Draw;
return true;
}
//Now check for player wins
var corners = new Tuple<int, int>[] { Tuple.Create(0,0), Tuple.Create(2,0), Tuple.Create(0,2), Tuple.Create(2,2) };
var sides = new Tuple<int, int>[] { Tuple.Create(1,0), Tuple.Create(0,1), Tuple.Create(1,2), Tuple.Create(2,1) };
var middle = new Tuple<int, int>(1,1);
var checkDiagonals = false;
//If the cell is at the corner or the middle we have to check for diagonal wins too
if (corners.Any(e => e.Equals(coords)) || middle.Equals(coords)) { checkDiagonals = true; }
if (player.PlayerWon(cells[coords.Item1, coords.Item2], this, checkDiagonals))
{
switch (player.marker)
{
case Mark.Cross:
Outcome = OutcomeType.CrossWin;
break;
case Mark.Nought:
Outcome = OutcomeType.NoughtWin;
break;
}
return true;
}
//If execution reaches this point then no one has won, return false
return false;
}
#endregion
}
Player.cs
class Player
{
#region Fields & Properties
private static int ctr;
private static Game form;
public Mark marker;
public readonly int ID;
public int Score { get; set; }
#endregion
#region Constructors
public Player()
{
ID = ctr++;
if (ID == 0) { // X is the starting player
marker = Mark.Cross; }
else {
marker = Mark.Nought; }
}
#endregion
#region Instanced Methods
public virtual Tuple<int, int> GetPlayerChoice(Grid grid, Player player) // Virtual because we want the option to override this for AIPlayer later.
{
var pMark = (player.ID == 0) ? "X" : "O";
var choice = new Tuple<int,int>(-1, -1);
var waitBtns = new List<Task>() { new Task(() => new SpinWait().SpinOnce()) };
//Enable eligble buttons for player
for (int x = 0; x < grid.cells.GetLength(0); x++)
{
for (int y = 0; y < grid.cells.GetLength(1); y++)
{
if (grid.cells[x, y].MarkType == Mark.Empty)
{
Game.vGrid[x, y].Invoke(new Action(() => Game.vGrid[x, y].Enabled = true));
Game.vGrid[x, y].Click += new EventHandler(
(a,b) =>
{
var pos = new Tuple<int,int>((a as VisualCell).CellPos.Item1, (a as VisualCell).CellPos.Item2);
choice = Tuple.Create(pos.Item1, pos.Item2);
Game.vGrid[pos.Item1, pos.Item2].Invoke(new Action(() => Game.vGrid[pos.Item1, pos.Item2].Text = pMark));
Game.vGrid[pos.Item1, pos.Item2].Invoke(new Action(() => Game.vGrid[pos.Item1, pos.Item2].Enabled = false));
waitBtns.Add(new Task(() => new SpinWait().SpinOnce()));
waitBtns[waitBtns.FindIndex(t => t.Status == TaskStatus.Created)].Start();
});
}
}
}
Task.WaitAny(waitBtns.ToArray());
return choice;
}
public bool PlayerWon(Cell cell, Grid grid, bool checkDiagonals)
{
if (cell.HorizontalRelatives(grid) == 2 || cell.VerticalRelatives(grid) == 2)
{
return true;
}
if (checkDiagonals)
{
if (cell.DiagonalRelatives(grid) == 2) { return true; }
else if (cell.DiagonalRelatives2(grid) == 2) { return true; }
}
return false;
}
#endregion
}
Gameround.cs
class GameRound
{
public delegate void RoundEndHandler(OutcomeType gameOutcome);
#region Fields & Properties
private static int ctr;
private Grid grid;
public readonly int ID;
public static event RoundEndHandler Complete;
public Player CurrentPlayer { get; private set; }
#endregion
#region Constructors
public GameRound()
{
ID = ctr++;
grid = new Grid();
}
#endregion
#region Instanced Methods
public void Start()
{
bool doLoop = true;
while (doLoop)
{
for (int i = 0; i < Game.players.Length; i++)
{
CurrentPlayer = Game.players[i];
var coord = Game.players[i].GetPlayerChoice(grid, CurrentPlayer);
grid.cells[coord.Item1, coord.Item2].MarkType = CurrentPlayer.marker;
//check if game is over
if (grid.CheckOutcome(coord, CurrentPlayer))
{
doLoop = false;
break;
}
}
}
Complete(grid.Outcome);
}
#endregion
}
ExtensionMethods.cs
static class ExtensionMethods
{
#region Cell Extension Methods
public static int DiagonalRelatives(this Cell cell, Grid grid)
{
int relatives = new int();
for (int x = 0; x < 3; x++)
{
if (grid.cells[x, x].MarkType.Equals(cell.MarkType)) { relatives++; }
}
return relatives - 1;
}
public static int DiagonalRelatives2(this Cell cell, Grid grid)
{
int relatives = new int();
for (int x = 0; x < 3; x++)
{
if (grid.cells[x, 2 - x].MarkType.Equals(cell.MarkType)) { relatives++; }
}
return relatives - 1;
}
public static int HorizontalRelatives(this Cell cell, Grid grid)
{
int relatives = new int();
int rowNum = grid.cells.IndexOf(cell).Item2;
for (int x = 0; x < 3; x++)
{
//Find row of cell
if (grid.cells[x, rowNum].MarkType.Equals(cell.MarkType)) { relatives++; }
}
return relatives - 1;
}
public static int VerticalRelatives(this Cell cell, Grid grid)
{
int relatives = new int();
int colNum = grid.cells.IndexOf(cell).Item1;
for (int y = 0; y < 3; y++)
{
//Find row of cell
if (grid.cells[colNum, y].MarkType.Equals(cell.MarkType)) { relatives++; }
}
return relatives - 1;
}
#endregion
#region Cell[] Extension Methods
public static Cell[] GetEmptyCells(this Cell[,] cells)
{
List<Cell> emptyCells = new List<Cell>();
foreach (Cell cell in cells)
{
if (cell.MarkType == Mark.Empty)
{
emptyCells.Add(cell);
}
}
return emptyCells.ToArray();
}
public static Tuple<int, int> IndexOf(this Cell[,] cells, Cell cell)
{
for (int x = 0; x < cells.GetLength(0); x++)
{
for (int y = 0; y < cells.GetLength(1); y++)
{
if (cells[x, y].Equals(cell))
{
return new Tuple<int,int>(x, y);
}
}
}
//If code reaches this point, then it didn't find anything, return -1
return new Tuple<int,int>(-1, -1);
}
#endregion
}