Skip to main content
1 of 4
Overly Excessive
  • 1.4k
  • 3
  • 11
  • 22

Tic-Tac-Toe feedback requested

I just started learning coding about 4 months ago, I think Tic-tac-toe was the first application that I've ever written in C# not counting the mandatory "Hello, World!".

So for me, I really wanted to revisit this game and code a new one, unfortunately I don't have the original code anymore, but I can assure you that it was a lot worse than whatever mess I've thrown together now ;).

Anyhow, as always both positive and negative feedback is welcome. 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.

Bonus question: 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 it's marker. Would this be a good idea? / Work at all?

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
}
Overly Excessive
  • 1.4k
  • 3
  • 11
  • 22