Please Review my Tic-Tac-Toe Game

Started by
8 comments, last by dmreichard 15 years, 10 months ago
Hello everyone! I finally decided to pick up my C# book again, and decided to try a shot at creating tic-tac-toe ala ConsoleApp. I haven't actually read any material on game design itself or how to approach something such as this, I just used the knowledge I gained of the C# programming language to solve the "tic-tac-toe problem." If there is one thing I learned from this though, is that a preliminary design would have made my life easier creating it (especially calculating if anyone won). Any comments, criticisms or even compliments would be greatly appreciated, thanks! How to play: When prompted to enter a position, you enter two numbers, eg. "12" 1 is the column, 2 is the row The top left block starts at 00 Numbers are "drawn" to the console window for the grid as guidance during play. Program.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace TicTacToe
{
    class Program
    {
        static void Main(string[] args)
        {
            Program prog = new Program();
            try
            {
                prog.Entrance();
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
            }
        }

        private void Entrance()
        {
            Console.WriteLine("Welcome to Tic-Tac-Toe!");
            GameLoop();
        }

        private void GameLoop()
        {
            Game game = new Game();
            while (!game.GameOver)
            {
                game.PlayRound();
            }
        }
    }
}



Game.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace TicTacToe
{
    partial class Game
    {
        public Game()
        {
            board = new GameBoard();
        }

        public bool GameOver
        {
            get { return gameOver; }
        }

        public void PlayRound()
        {
            Console.Clear();
            board.Draw();
            MakeMove('X');
            if (CalculateWins(board))
                return;
            Console.Clear();
            board.Draw();
            MakeMove('O');
            CalculateWins(board);
        }

        // returns true if someone won, otherwise false
        private bool CalculateWins(GameBoard board)
        {
            char[,] sectors = board.Sectors;
            char token = ' ';
            int count = 0;
            // loop through the columns
            for (int i = 0; i < sectors.Length / 3 && count < 3; i++)
            {
                if (sectors[i, 0] != ' ') // check if column has a token to start with
                {
                    token = sectors[i, 0];
                    // first column check
                    if (i == 0)
                    {
                        // vertical from 0,0
                        for (int y = 0; y < sectors.Length / 3; y++)
                        {
                            if (sectors[i, y] == token)
                                count++;
                        }
                        if (count < 3)
                        {
                            count = 0;
                            //horizontal from 0,0
                            for (int y = 0; y < sectors.Length / 3; y++)
                            {
                                if (sectors[y, 0] == token)
                                    count++;
                            }
                        }
                        if (count < 3)
                        {
                            count = 0;
                            //diagonal from 0,0
                            for (int y = 0, b = 0; y < sectors.Length / 3; y++, b++)
                            {
                                if (sectors[b, y] == token)
                                    count++;
                            }
                        }
                        if (count < 3)
                            count = 0;
                    } // end first column check
                    // second column check
                    
                    if (i == 1)
                    {
                        for (int y = 0; y < sectors.Length / 3; y++)
                        {
                            if (sectors[i, y] == token)
                                count++;
                        }
                        if (count < 3)
                            count = 0;
                    } // end second column check
                    // third column check
                    if (i == 2)
                    {
                        for (int y = 0; y < sectors.Length/3; y++)
                        {
                            if (sectors[i, y] == token)
                                count++;
                        }
                        if (count < 3)
                            count = 0;
                    } // end third column check
                }
            } // end column loop
            // begin row loop starting at second row, if the count is less than 3
            for (int i = 1; i < sectors.Length / 3 && count < 3; i++)
            {
                if (sectors[0,i] != ' ') // check if row has a token to begin with
                {
                    token = sectors[0,i];
                    // second row check
                    if (i == 1)
                    {
                        //check horizontally
                        for (int y = 0; y < sectors.Length / 3; y++)
                        {
                            if (sectors[y, i] == token)
                                count++;
                        }
                        if (count < 3)
                            count = 0;
                    }
                    // third row check
                    if (i == 2)
                    {
                        // check horizontally
                        for (int y = 0; y < sectors.Length/3; y++)
                        {
                            if (sectors[y,i] == token)
                                count++;
                        }
                        // next try diagonally to top right
                        if (count < 3)
                        {
                            count = 0;
                            for (int y = 0, b = 2; y < sectors.Length / 3; y++, b--)
                            {
                                if (sectors[y,b] == token)
                                    count++;
                            }
                        }
                        if (count < 3)
                            count = 0;
                    } // end third row check
                } 
            } // end row loop

            if (count == 3)
            {
                Console.Clear();
                board.Draw();
                Console.WriteLine("{0} won!", token);
                gameOver = true;
                return true;
            }
            return false;
        }

        private void MakeMove(char token)
        {
            bool invalidMove;
            int x = 0, y = 0;
            do
            {
                Console.Write("{0}, make your move: ", token);
                invalidMove = false;
                string input = Console.ReadLine();
                try
                {
                    if (input.Length != 2)
                        throw new FormatException("Need two numbers");
                    x = int.Parse(input.Substring(0, 1));
                    y = int.Parse(input.Substring(1, 1));
                }
                catch (FormatException fex)
                {
                    Console.WriteLine(fex.Message);
                    invalidMove = true;
                    continue;
                }

                if (x > 2 || x < 0 || y > 2 || y < 0 || !board.CheckPosition(x, y))
                {
                    Console.WriteLine("Invalid position!");
                    invalidMove = true;
                }
            } while (invalidMove);
            board.AddPiece(x, y, token);
        }

        bool gameOver = false;
        private GameBoard board;
    }
}

GameBoard.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace TicTacToe
{
    partial class Game
    {
        private class GameBoard
        {
            public void Draw()
            {
                Console.WriteLine(" 0 1 2\n0{0}|{1}|{2}\n -----\n1{3}|{4}|{5}\n -----\n2{6}|{7}|{8}",
                    sectors[0, 0], sectors[1, 0], sectors[2, 0],
                    sectors[0, 1], sectors[1, 1], sectors[2, 1],
                    sectors[0, 2], sectors[1, 2], sectors[2, 2]);
            }

            public void AddPiece(int x, int y, char type)
            {
                if (type != 'X' && type != 'O')
                    throw new FormatException("parameter \"type\" of AddPiece must be either 'X' or 'O'");
                sectors[x, y] = type;
            }

            // sets the character to ' ' to "remove" a gamepiece
            // currently unused, will be used later for asking if player
            // wants another game
            public void RemovePiece(int x, int y)
            {
                sectors[x, y] = ' ';
            }

            // checks if a position is available
            public bool CheckPosition(int x, int y)
            {
                if (sectors[x, y] != ' ')
                    return false;
                return true;
            }

            // ability to retrieve current sector status, used for calculating wins
            // returns a clone so that the actual array is still encapsulated, otherwise
            // we would be returning a reference (technically, anyhow)
            public char[,] Sectors
            {
                get { return (char[,])sectors.Clone(); }
            }

            private char[,] sectors = { { ' ', ' ', ' ' }, { ' ', ' ', ' ' }, { ' ', ' ', ' ' } };
        }
    }
}



After a couple hours of debugging and tweaking, it seems to work flawlessly with elegant exception handling(I hope). Thank you again for checking out my work. [smile] [Edited by - dmreichard on June 19, 2008 6:57:37 AM]
Advertisement
No downloadables?
I've tried it. It has a bug. When I had all '0' in the last column, it didn't say '0' wins.
Looking at your CalculateWins() in Game.cs, you have a lot of duplicate code especially within your for-loops. I think you can modify it by adding or updating a variable.

You probably should try checking columns, rows, and diagonals separately to make the code cleaner. I'm also looking at the condition-statements for the forloops. what does i < sectors.Length / 3; mean in your code?
Quote:Original post by unknownProdigy
I've tried it. It has a bug. When I had all '0' in the last column, it didn't say '0' wins.


Indeed it does! I have found the source of the bug and Game.cs has been updated accordingly.

Quote:Original post by unknownProdigy
Looking at your CalculateWins() in Game.cs, you have a lot of duplicate code especially within your for-loops. I think you can modify it by adding or updating a variable.

You probably should try checking columns, rows, and diagonals separately to make the code cleaner. I'm also looking at the condition-statements for the forloops. what does i < sectors.Length / 3; mean in your code?


I'm not exactly sure what you mean by adding/updating a variable, I already use the count variable to determine if there are three tokens in a row. Could you please elaborate on that?

i < sectors.Length / 3 is because in a two dimensional array, the length property will return the count of all the elements, in this case 9. Since it is a 3x3 grid, I divide by 3. I'm not sure if this is the best way of doing it however...

As far as the duplicate code, if you run it through the debugger it should be checking everything seperately. I'm really not sure what you mean, if I had any less code I would end up missing a row/column/diagonal. I never read any tutorials or material on how to approach it though, so if you could provide an example of a cleaner version that I may learn from, it would be greatly appreciated. Thanks!
Quote:i < sectors.Length / 3 is because in a two dimensional array, the length property will return the count of all the elements, in this case 9. Since it is a 3x3 grid, I divide by 3. I'm not sure if this is the best way of doing it however...
You might take a look at the GetLength() function.
A strange game, the only winning move is not to play.

/bornander
The code in CalculateWins() is needlessly long. I would do something like this:

    for (int r = 0; r < 3; ++r)        if (CheckRow(r))            // Whoever just made a move wins    for (int c = 0; c < 3; ++c)        if (CheckCol(c))            // Whoever just made a move wins    if (sectors[0, 0] != ' ')    {        if (sectors[0, 0] == sectors[1, 1] &&            sectors[1, 1] == sectors[2, 2])            // Whoever just made a move wins    }        if (sectors[0, 2] != ' ')    {        if (sectors[0, 2] == sectors[1, 1] &&            sectors[1, 1] == sectors[2, 0])            // Whoever just made a move wins    }


EDIT: Removed needless loop.
Quote:Original post by jyk
Quote:i < sectors.Length / 3 is because in a two dimensional array, the length property will return the count of all the elements, in this case 9. Since it is a 3x3 grid, I divide by 3. I'm not sure if this is the best way of doing it however...
You might take a look at the GetLength() function.


Exactly what I was looking for, thanks!


Quote:Original post by Gage64
The code in CalculateWins() is needlessly long. I would do something like this:
...
EDIT: Removed needless loop.


When I first created it, I knew there had to be a better way. Thank you very much! I will refactor and update my code listing as soon as I get the chance to see if I can get any more critics. [smile]
I just got around to refactoring the code and it is much cleaner now thanks to everyones help!

EDIT: Added a check for draw possibility.

Program.cs
using System;using System.Collections.Generic;using System.Linq;using System.Text;namespace TicTacToe{    class Program    {        static void Main(string[] args)        {            Program prog = new Program();            try            {                prog.Entrance();            }            catch (Exception ex)            {                Console.WriteLine(ex.Message);            }        }        private void Entrance()        {            Console.WriteLine("Welcome to Tic-Tac-Toe!");            GameLoop();        }        private void GameLoop()        {            Game game = new Game();            while (!game.GameOver)            {                game.PlayRound();            }        }    }}


Game.cs
using System;using System.Collections.Generic;using System.Linq;using System.Text;namespace TicTacToe{    partial class Game    {        public Game()        {            board = new GameBoard();        }        public bool GameOver        {            get { return gameOver; }        }        public void PlayRound()        {            Console.Clear();            board.Draw();            MakeMove('X');            if (CalculateWins(board))            {                AnnounceWin('X');                gameOver = true;                return;            }            if (board.BoardFull())            {                AnnounceDraw();                gameOver = true;                return;            }            Console.Clear();            board.Draw();            MakeMove('O');            if (CalculateWins(board))            {                AnnounceWin('O');                gameOver = true;                return;            }            if (board.BoardFull())            {                AnnounceDraw();                gameOver = true;                return;            }        }        private void AnnounceDraw()        {            Console.Clear();            board.Draw();            Console.WriteLine("The match is a draw!");        }        private void AnnounceWin(char token)        {            Console.Clear();            board.Draw();            Console.WriteLine("{0} won!", token);        }        // returns true if someone won, otherwise false        private bool CalculateWins(GameBoard board)        {            char[,] sectors = board.Sectors;            // new condensed loop, checks rows and columns for a win            for (int i = 0; i < 3; i++)            {                if (CheckRow(i, sectors))                    return true;                if (CheckColumn(i, sectors))                    return true;            }            // now check both diagonals            if (sectors[0, 2] != ' ')            {                if (sectors[0, 2] == sectors[1, 1] &&                    sectors[1, 1] == sectors[2, 0])                    return true;            }            if (sectors[0, 0] != ' ')            {                if (sectors[0, 0] == sectors[1, 1] &&                    sectors[1, 1] == sectors[2, 2])                    return true;            }            // finally, if no win is found then return false            return false;        }        // returns true if each element in the row is equal        private bool CheckRow(int row, char[,] sectors)        {            // if the first element in the row is empty, don't bother checking            if (sectors[0, row] == ' ')                return false;            if (sectors[0, row] == sectors[1, row] &&                sectors[1, row] == sectors[2, row])                return true;            return false;        }        // returns true if each element in the column is equal        private bool CheckColumn(int column, char[,] sectors)        {            // if the first element in the column is empty, don't bother checking            if (sectors[column, 0] == ' ')                return false;            if (sectors[column, 0] == sectors[column, 1] &&                sectors[column, 1] == sectors[column, 2])                return true;            return false;        }        private void MakeMove(char token)        {            bool invalidMove;            int x = 0, y = 0;            do            {                Console.Write("{0}, make your move: ", token);                invalidMove = false;                string input = Console.ReadLine();                try                {                    if (input.Length != 2)                        throw new FormatException("Need two numbers");                    x = int.Parse(input.Substring(0, 1));                    y = int.Parse(input.Substring(1, 1));                }                catch (FormatException fex)                {                    Console.WriteLine(fex.Message);                    invalidMove = true;                    continue;                }                if (x > 2 || x < 0 || y > 2 || y < 0 || !board.CheckPosition(x, y))                {                    Console.WriteLine("Invalid position!");                    invalidMove = true;                }            } while (invalidMove);            board.AddPiece(x, y, token);        }        bool gameOver = false;        private GameBoard board;    }}


GameBoard.cs
using System;using System.Collections.Generic;using System.Linq;using System.Text;namespace TicTacToe{    partial class Game    {        private class GameBoard        {            public void Draw()            {                Console.WriteLine(" 0 1 2\n0{0}|{1}|{2}\n -----\n1{3}|{4}|{5}\n -----\n2{6}|{7}|{8}",                    sectors[0, 0], sectors[1, 0], sectors[2, 0],                    sectors[0, 1], sectors[1, 1], sectors[2, 1],                    sectors[0, 2], sectors[1, 2], sectors[2, 2]);            }            public void AddPiece(int x, int y, char type)            {                if (type != 'X' && type != 'O')                    throw new FormatException("parameter \"type\" of AddPiece must be either 'X' or 'O'");                sectors[x, y] = type;            }            // sets the character to ' ' to "remove" a gamepiece            // currently unused, will be used later for asking if player            // wants another game            public void RemovePiece(int x, int y)            {                sectors[x, y] = ' ';            }            // checks if a position is available            public bool CheckPosition(int x, int y)            {                if (sectors[x, y] != ' ')                    return false;                return true;            }            public bool BoardFull()            {                int count = 0;                for (int i = 0; i < 3; i++)                {                    for (int y = 0; y < 3; y++)                    {                        if (sectors[i, y] != ' ')                            count++;                    }                }                if (count == sectors.Length)                    return true;                return false;            }            // ability to retrieve current sector status, used for calculating wins            // returns a clone so that the actual array is still encapsulated, otherwise            // we would be returning a reference (technically, anyhow)            public char[,] Sectors            {                get { return (char[,])sectors.Clone(); }            }            private char[,] sectors = { { ' ', ' ', ' ' }, { ' ', ' ', ' ' }, { ' ', ' ', ' ' } };        }    }}


[Edited by - dmreichard on June 19, 2008 9:49:28 AM]

This topic is closed to new replies.

Advertisement