Completed Tic Tac Toe - Critical Critiques and Advice

Started by
10 comments, last by Khatharr 11 years, 4 months ago
Hello, my name is Maurice Harris and recently while learning my first language(C#) I have completed my first game, Tic Tac Toe. I am here before you all asking for critique and advice on my design of the game Tic Tac Toe. Although I am relieved to have finally completed my first game I feel as though it could use serious improvement design-wise and would like helpful advice on how to improve not only my game but also my programming skills so I can become a much better programmer and avoid the headaches I faced making what I thought would be a simple game to create. So without further ado I present my version of the classic Tic Tac Toe.

[source lang="csharp"]/*
* Tic Tac Toe - by Maurice Harris
* Last Modified: 11/15/12
*/

using System;

namespace TicTacToeGame
{
class TicTacToe
{
static bool gameOver = false;

// Draws a tic tac toe board
static void DrawBoard(char[] board)
{
Console.WriteLine("\n\n " + board[6] + " | " + board[7] + " | " + board[8]);
Console.WriteLine(" ---+---+---");
Console.WriteLine(" " + board[3] + " | " + board[4] + " | " + board[5]);
Console.WriteLine(" ---+---+---");
Console.WriteLine(" " + board[0] + " | " + board[1] + " | " + board[2] + "\n\n");
}

// Clears a tic tac toe board
static void ClearBoard(char[] board)
{
// Loop through char array elements and set them equal to ' '
for (int i = 0; i < board.Length; i++)
{
board = ' ';
}
}

// Draws a tic tac toe board with numbers in the spaces to correspond with the numpad
static void DrawExampleBoard()
{
Console.WriteLine("\n\n 7 | 8 | 9 ");
Console.WriteLine(" ---+---+---");
Console.WriteLine(" 4 | 5 | 6 ");
Console.WriteLine(" ---+---+---");
Console.WriteLine(" 1 | 2 | 3\n\n");
}

// Checks the board to see if the space is taken up by an O or X
static bool SpaceFree(char[] board, int move)
{
return board[move] == ' ';
}

// Marks spot on the tic tac toe board with a marker if the space is free
static bool MakeMove(char[] board, char marker, int move)
{
if (SpaceFree(board, move))
{
board[move] = marker;
return true;
}
else
{
Console.WriteLine("\nThat spot is taken.");
return false;
}
}

// Take input from either the numpad or numerical keys 1-9 and mark the board
static void TakeInput(char[] board, char marker)
{
// Keep track of whether or not the user has pressed a valid key for marking the board
bool validKeyPressed = false;

// Keep track of the key pressed by the user
ConsoleKeyInfo keyPressed;

// Do-While loop to catch valid input from the user
do
{
keyPressed = Console.ReadKey(true);

// If the key pressed is valid and the spot is unmarked, mark it,
// Otherwise break the switch statement and take new key input
switch (keyPressed.Key)
{
case ConsoleKey.NumPad1:
case ConsoleKey.D1:
if (MakeMove(board, marker, 0))
{
validKeyPressed = true;
break;
}
break;

case ConsoleKey.NumPad2:
case ConsoleKey.D2:
if (MakeMove(board, marker, 1))
{
validKeyPressed = true;
break;
}
break;

case ConsoleKey.NumPad3:
case ConsoleKey.D3:
if (MakeMove(board, marker, 2))
{
validKeyPressed = true;
break;
}
break;

case ConsoleKey.NumPad4:
case ConsoleKey.D4:
if (MakeMove(board, marker, 3))
{
validKeyPressed = true;
break;
}
break;

case ConsoleKey.NumPad5:
case ConsoleKey.D5:
if (MakeMove(board, marker, 4))
{
validKeyPressed = true;
break;
}
break;

case ConsoleKey.NumPad6:
case ConsoleKey.D6:
if (MakeMove(board, marker, 5))
{
validKeyPressed = true;
break;
}
break;

case ConsoleKey.NumPad7:
case ConsoleKey.D7:
if (MakeMove(board, marker, 6))
{
validKeyPressed = true;
break;
}
break;

case ConsoleKey.NumPad8:
case ConsoleKey.D8:
if (MakeMove(board, marker, 7))
{
validKeyPressed = true;
break;
}
break;

case ConsoleKey.NumPad9:
case ConsoleKey.D9:
if (MakeMove(board, marker, 8))
{
validKeyPressed = true;
break;
}
break;

default:
Console.WriteLine("\nPlease use your numpad or the keys 1-9 to choose a spot to mark.");
break;
}
} while (!validKeyPressed);
}


// Check to see if the board has no unmarked spots left
static bool BoardFull(char[] board)
{
for (int i = 0; i < board.Length; i++)
{
if (board == ' ')
{
return false;
}
}
return true;
}

// Check to see if someone has won or if there is a tie
// Return a string to annouce the game's final state
static string CheckWinner(char[] board, char marker)
{
// Horizontal Win
if ((board[0] == marker) && (board[1] == marker) && (board[2] == marker))
{
gameOver = true;
return marker + " wins!\n";
}
else if ((board[3] == marker) && (board[4] == marker) && (board[5] == marker))
{
gameOver = true;
return marker + " wins!\n";
}
else if ((board[6] == marker) && (board[7] == marker) && (board[8] == marker))
{
gameOver = true;
return marker + " wins!\n";
}

// Vertical Win
else if ((board[0] == marker) && (board[3] == marker) && (board[6] == marker))
{
gameOver = true;
return marker + " wins!\n";
}
else if ((board[1] == marker) && (board[4] == marker) && (board[7] == marker))
{
gameOver = true;
return marker + " wins!\n";
}
else if ((board[2] == marker) && (board[5] == marker) && (board[8] == marker))
{
gameOver = true;
return marker + " wins!\n";
}

// Diagonal Win
else if ((board[0] == marker) && (board[4] == marker) && (board[8] == marker))
{
gameOver = true;
return marker + " wins!\n";
}
else if ((board[2] == marker) && (board[4] == marker) && (board[6] == marker))
{
gameOver = true;
return marker + " wins!\n";
}

// Tie Game
else if (BoardFull(board))
{
gameOver = true;
return "No one wins, it's a tie!\n";
}

return null;
}

// Introduce the game to users and explain how to play
static void Intro()
{
Console.WriteLine("\n\n Welcome to Tic Tac Toe!\n\n");
Console.WriteLine(" Currently you can only play against another human player.");
Console.WriteLine(" To pick a spot on the Tic Tac Toe board use the keypad or the numbers 1-9.");
Console.WriteLine(" You must have num-lock on to be able to use the keypad for input.");
Console.WriteLine(" The keypad positions correspond to the Tic Tac Toe board.");

DrawExampleBoard();

Console.WriteLine(" Press any key to start the game!");
Console.ReadKey(true);
Console.Clear();
}

// The Tic Tac Toe Game's heart
static void TicTacToeGame(char[] board)
{
int playerTurn = 1;
string winner = "";

while (!gameOver)
{
// Player 1/O's
if (playerTurn == 1)
{
Console.WriteLine("It is O's turn to move!");
TakeInput(board, 'O');
DrawBoard(board);
winner = CheckWinner(board, 'O');
playerTurn = 2;
}

// Player 2/X's turn
else
{
Console.WriteLine("It is X's turn to move!");
TakeInput(board, 'X');
DrawBoard(board);
winner = CheckWinner(board, 'X');
playerTurn = 1;
}
}
Console.WriteLine(winner);
}


static void Main()
{
// The Tic Tac Toe board
char[] board = {' ',' ',' ',' ',' ',' ',' ',' ',' '};

// Used to keep track of whether a new game has started
bool newGame = true;

// Do-While loop that runs through the intro and a full game of human vs. human tic tac toe
// The loop continues until the user responds with a n or no to another game of tic tac toe
do
{
Intro();
DrawBoard(board);
TicTacToeGame(board);

Console.WriteLine("Would you like to play again? [Y]es/[N]o?\n");

string response = Console.ReadLine();

switch (response.ToLower())
{
case "yes":
case "y":
gameOver = false;
Console.Clear();
ClearBoard(board);
break;

case "no":
case "n":
newGame = false;
Console.Clear();
ClearBoard(board);
break;

default:
break;
}
} while (newGame == true);
}
}
}
[/source]
Advertisement
SpaceFree() could be IsSpaceFree(), for more clarity that it's checking rather than freeing.

Looking mostly good so far. (I don't like your formatting but that's a preference issue, lol.)

Will edit here after looking at the rest.

In the main fn, why do-while rather than just while? You set the condition prior to the loop. Also, it's not an issue in this case since the code is so short and clean, but in terms of design the 'play again' block could be segregated to its own function.

DrawExampleBoard() seems superfluous. All it does is write some lines and its only call is in the midst of a series of drawn lines. Do you intend to call it elsewhere in a later version?

You call DrawBoard() prior to calling TicTacToeGame(). Why not move it into the beginning of TicTacToeGame()? It seems more natural for it to be there.

I see main()'s board getting passed into a lot of member functions. Why not make it a member variable?

TakeInput() - huge switch statement in here. DRY. Convert the enum to your target value (It feels really gross doing this with a switch, but idk enough about C# console input to do it by character value.) and then run the code once on the derived value. If you want to change the action taken on the value later you won't need to change it nine times. Your code will also be smaller.

[source lang="csharp"]
int slot = -1;
switch(keyPressed.Key) {
//...
case ConsoleKey.NumPad9:
case ConsoleKey.D9:
slot = 8;
break;
default:
Console.WriteLine("\nPlease use your numpad or the keys 1-9 to choose a spot to mark.");
break;
}
if(slot != -1) {
validKeyPressed = MakeMove(board, marker, slot);
}[/source]

TakeInput() has another do-while that could be a while.


[source lang="csharp"] if(MakeMove(board, marker, 0)) {
validKeyPressed = true;
break; //this line is not necessary
}
break;[/source]

Still looking.

Going by function name, TakeInput() should not call MakeMove(). It should return the derived value and then the caller of TakeInput() should call MakeMove() with that value. This would technically be the better thing to do here, but in this case you could get away with just renaming TakeInput() to TakeTurn(), since that's what it's really doing here.

Did you not get a compiler warning for MakeMove()'s return paths? This is how to avoid it:
[source lang="csharp"] // Marks spot on the tic tac toe board with a marker if the space is free
static bool MakeMove(char[] board, char marker, int move) {
if(SpaceFree(board, move)) {
board[move] = marker;
return true;
}
Console.WriteLine("\nThat spot is taken.");
return false;
}[/source]

Try to find ways to make your data work for you, instead of you working for the data:
[source lang="csharp"] // The Tic Tac Toe Game's heart
static void TicTacToeGame(char[] board) {
char playerTurn = 'O';
string winner = "";

while(!gameOver) {
Console.WriteLine("It is " + playerTurn + "'s turn to move!");
TakeInput(board, playerTurn);
DrawBoard(board);
winner = CheckWinner(board, playerTurn);
playerTurn = (playerTurn == 'O') ? 'X' : 'O';
}
Console.WriteLine(winner);
}[/source]

This may save you some room:
[source lang="csharp"] static bool CheckWinSet(char board[], int a, int b, int c) {
return (board[a] == board) && (board == board[c]);
}

static string CheckWinner(char[] board, char marker) {
while(true) { //for breaking
// Horizontal Win
if(gameOver = CheckWinSet(board, 0, 1, 2)) {break;}
if(gameOver = CheckWinSet(board, 3, 4, 5)) {break;}
if(gameOver = CheckWinSet(board, 6, 7, 8)) {break;}
// Vertical Win
if(gameOver = CheckWinSet(board, 0, 3, 6)) {break;}
if(gameOver = CheckWinSet(board, 1, 4, 7)) {break;}
if(gameOver = CheckWinSet(board, 2, 5, 8)) {break;}
// Diagonal Win
if(gameOver = CheckWinSet(board, 0, 4, 8)) {break;}
if(gameOver = CheckWinSet(board, 2, 4, 6)) {break;}
break;
}

if(gameOver) {
return marker + " wins!\n";
}

// Tie Game
if(BoardFull(board)) {
gameOver = true;
return "No one wins, it's a tie!\n";
}

return null;
}[/source]

You may be able to find a stdlib function that searches for a specific character in a string. I dunno if C# has it, but if so you can tack a zero on to the end of your board array and then search it for ' ' rather than using BoardFull().


Overall, this is pretty good. It's clean and easy to read and I was able to track the flow of the program with no difficulty. I think you're quite ready to move on to the next level of complexity. You may want to look into some articles about general program structure and etc, but really you did quite well here. I have no significant complaints. I know I said a lot, but I don't want you to think that your own version was 'wrong'. It's just that there's always a 'better' way. (I'm resisting the temptation to change CheckWinner() to a loop that runs through static int[3] arrays) I'm sure that there are far better ways than what I showed you as well. You'll naturally pick up better flow habits as you practice and see more code and come across interesting ideas.

Good job! biggrin.png
void hurrrrrrrr() {__asm sub [ebp+4],5;}

There are ten kinds of people in this world: those who understand binary and those who don't.
I think you will get the most in terms of learning experience and better design by not assuming a fixed board size and hard coding every possible win condition. After all, the real core of programming isn't knowing language X or memorizing whole libraries, but about problem solving.

How would you write it, if the board size can be configured to 4 or 5? Ironically, you should find that your code will get a lot shorter, cleaner and contain way fewer messy if-else and switch-case constructs.

Once that is working, how would you handle the win condition not being n in a row (where n equals the board size), but a value that can be configured independently? You should find this to be a very small adjustment to your existing code (okay, I'm lying, unless you didn't realize that only two diagonals need to be checked for n = board size).

What if the board doesn't have to be square? Again, this should end up being only a minor change.
f@dzhttp://festini.device-zero.de
First, that is pretty well done, very easy to read.

I noticed this:

I see main()'s board getting passed into a lot of member functions. Why not make it a member variable?

Which brings me to the issue that this whole program has only static methods. So the suggestion of adding a member variable wont work.

How about trying to create a class called Board and instantiate it? It wont make the source code shorter, but it will focus some code to specific place. It will increase the so called cohesion (http://en.wikipedia.org/wiki/Cohesion_%28computer_science%29) for different elements (or compononents/modules/concepts) of your program. Basically most of you operations of the board data would be abstracted away there.

The Board class could have methods such as:
Clear()
Draw()
IsElementFree() (SpaceFree)
MakeMove()
IsFull()

Also, you can add loops inside of your CheckWinner() method.

[quote name='Khatharr' timestamp='1353036021' post='5001432']
I see main()'s board getting passed into a lot of member functions. Why not make it a member variable?

Which brings me to the issue that this whole program has only static methods. So the suggestion of adding a member variable wont work.
[/quote]

It seems to work okay for 'gameOver'.

Moving toward a more object-oriented approach would definitely be the next step, though. I agree with that.
void hurrrrrrrr() {__asm sub [ebp+4],5;}

There are ten kinds of people in this world: those who understand binary and those who don't.
I made the suggestions you gave to me and my program feels and looks less bloated. I feel once I get further along in reading The C# Yellow Book by Robert Miles, I'll be able to improve upon my design little by little until I'm satisfied. I just recently got to the 4th chapter of the book and it's starting to talk about objects and how to represent them so hopefully I'll be able to come back very soon and further improve the design of this and future projects. Trienco, I've been thinking about what you said about having a board that's 4x4, 5x5, or any random 2 values and I'm having trouble thinking about how I could handle this without thinking myself into a corner.

Updated Source:
[source lang="csharp"]/*
* Tic Tac Toe - by Maurice Harris
* Last Modified: 11/16/12
*/

using System;

namespace TicTacToeGame
{
class TicTacToe
{
// The Tic Tac Toe board
static char[] board = { ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ' };

// Used to keep track of whether the current game is over
static bool gameOver = false;

// Used to keep track of whether a new game has started
static bool newGame = true;

// Draws a tic tac toe board
static void DrawBoard()
{
Console.WriteLine("\n\n " + board[6] + " | " + board[7] + " | " + board[8]);
Console.WriteLine(" ---+---+---");
Console.WriteLine(" " + board[3] + " | " + board[4] + " | " + board[5]);
Console.WriteLine(" ---+---+---");
Console.WriteLine(" " + board[0] + " | " + board[1] + " | " + board[2] + "\n\n");
}

// Clears a tic tac toe board
static void ClearBoard()
{
// Loop through char array elements and set them equal to ' '
for (int i = 0; i < board.Length; i++)
{
board = ' ';
}
}

// Checks the board to see if the space is taken up by an O or X
static bool IsSpaceFree(int move)
{
return board[move] == ' ';
}

// Marks spot on the tic tac toe board with a marker if the space is free
static bool MakeMove(char marker, int move)
{
if (IsSpaceFree(move))
{
board[move] = marker;
return true;
}
else
{
Console.WriteLine("\nThat spot is taken.");
return false;
}
}

// Take input from either the numpad or numerical keys 1-9 and mark the board
static void TakeInput(char marker)
{
// Keep track of whether or not the user has pressed a valid key for marking the board
bool validKeyPressed = false;

// Keep track of the key pressed by the user
ConsoleKeyInfo keyPressed;

// The spot picked by the user
int spot = -1;

// While loop to catch valid input from the user
while (!validKeyPressed)
{
keyPressed = Console.ReadKey(true);

// If the key pressed is valid and the spot is unmarked, mark it,
// Otherwise break the switch statement and take new key input
switch (keyPressed.Key)
{
case ConsoleKey.NumPad1:
case ConsoleKey.D1:
spot = 0;
break;

case ConsoleKey.NumPad2:
case ConsoleKey.D2:
spot = 1;
break;

case ConsoleKey.NumPad3:
case ConsoleKey.D3:
spot = 2;
break;

case ConsoleKey.NumPad4:
case ConsoleKey.D4:
spot = 3;
break;

case ConsoleKey.NumPad5:
case ConsoleKey.D5:
spot = 4;
break;

case ConsoleKey.NumPad6:
case ConsoleKey.D6:
spot = 5;
break;

case ConsoleKey.NumPad7:
case ConsoleKey.D7:
spot = 6;
break;

case ConsoleKey.NumPad8:
case ConsoleKey.D8:
spot = 7;
break;

case ConsoleKey.NumPad9:
case ConsoleKey.D9:
spot = 8;
break;

default:
Console.WriteLine("\nPlease use your numpad or the keys 1-9 to choose a spot to mark.");
break;
}
if (spot != -1)
{
Console.Clear();
validKeyPressed = MakeMove(marker, spot);
}
}
}


// Check to see if the board has no unmarked spots left
static bool IsBoardFull()
{
for (int i = 0; i < board.Length; i++)
{
if (board == ' ')
{
return false;
}
}
return true;
}

// Check a set of numbers
static bool CheckWinSet(char marker, int a, int b, int c)
{
return ((board[a] == marker) && (board == marker) && (board[c] == marker));
}

// Check to see if someone has won or if there is a tie
// Return a string to annouce the game's final state
static string CheckWinner(char marker)
{
while(true)
{
// Horizontal Win
if (gameOver = CheckWinSet(marker, 0, 1, 2)) { break; }
if (gameOver = CheckWinSet(marker, 3, 4, 5)) { break; }
if (gameOver = CheckWinSet(marker, 6, 7, 8)) { break; }

// Vertical Win
if (gameOver = CheckWinSet(marker, 0, 3, 6)) { break; }
if (gameOver = CheckWinSet(marker, 1, 4, 7)) { break; }
if (gameOver = CheckWinSet(marker, 2, 5, 8)) { break; }

// Diagonal Win
if (gameOver = CheckWinSet(marker, 0, 4, 8)) { break; }
if (gameOver = CheckWinSet(marker, 2, 4, 6)) { break; }
break;
}

if (gameOver)
{
return marker + " wins!\n";
}

// Tie Game
if (IsBoardFull())
{
gameOver = true;
return "No one wins, it's a tie!\n";
}

return null;
}

// Introduce the game to users and explain how to play
static void Intro()
{
Console.WriteLine("\n\n Welcome to Tic Tac Toe!\n\n");
Console.WriteLine(" Currently you can only play against another human player.");
Console.WriteLine(" To pick a spot on the Tic Tac Toe board use the keypad or the numbers 1-9.");
Console.WriteLine(" You must have num-lock on to be able to use the keypad for input.");
Console.WriteLine(" The keypad positions correspond to the Tic Tac Toe board.");

// Draws a tic tac toe board with numbers in the spaces to correspond with the numpad
Console.WriteLine("\n\n 7 | 8 | 9 ");
Console.WriteLine(" ---+---+---");
Console.WriteLine(" 4 | 5 | 6 ");
Console.WriteLine(" ---+---+---");
Console.WriteLine(" 1 | 2 | 3\n\n");

Console.WriteLine(" Press any key to start the game!");
Console.ReadKey(true);
Console.Clear();
}

// The Tic Tac Toe Game's heart
static void TicTacToeGame()
{
char playerTurn = 'O';
string winner = "";

Intro();
DrawBoard();

while (!gameOver)
{
Console.WriteLine("It is " + playerTurn + "'s turn to move!");
TakeInput(playerTurn);
DrawBoard();
winner = CheckWinner(playerTurn);
playerTurn = (playerTurn == 'O') ? 'X' : 'O';
}
Console.WriteLine(winner);
}

static void PlayAgain()
{
Console.WriteLine("Would you like to play again? [Y]es/[N]o?\n");

string response = Console.ReadLine();

switch (response.ToLower())
{
case "yes":
case "y":
gameOver = false;
Console.Clear();
ClearBoard();
break;

case "no":
case "n":
newGame = false;
Console.Clear();
ClearBoard();
break;

default:
break;
}
}

static void Main()
{
// While loop that runs through the intro and a full game of human vs. human tic tac toe
// The loop continues until the user responds with a n or no to another game of tic tac toe
while (newGame)
{
TicTacToeGame();
PlayAgain();
}
}
}
}
[/source]
Once you're comfortable with OOP you should see some ways to do things like that.

The text formatting may give you a little bit of a challenge, but his suggestion will lead you into the realm of object-based encapsulation, which will let you have re-usable, adjustable parts like he's suggesting.
void hurrrrrrrr() {__asm sub [ebp+4],5;}

There are ten kinds of people in this world: those who understand binary and those who don't.
Great, I'll go back to reading for now. Thank you all for giving me advice and help, I hope to share better projects in the future.
I wouldn't actually focus on OOP for the things I described. Those are simply applications of loops.

For a weird comparison, think of the difference between "calculating" and actual "math". Math isn't about calculating the result of 2+2, but is more often dealing in abstractions and finding generic solutions to problems. For example n+n = n*n. You can now go and "hard code" it for all numbers from 0 to 1000000 and see which one works (basically the equivalent to hard coding all possible win conditions for tic tac toe), or you can apply math (in programming: come up with an algorithm and apply it):

(n+n)/n = n
n/n + n/n = n
1 + 1 = 0
n = 2

In the same way, when you write code and find yourself doing something really tedious (like writing down all winning combinations) and especially when you start copy/pasting things all over the place, it is time to stop, take a step back and try to understand the pattern or system behind the problem you're solving. That's completely independent of any programming language. The actual "coding" is usually the easy part, where you just need to translate the solution you came up with into your language of choice.

Back to topic:

You already know how to use loops, so in pseudo code, you can start by not saying "if it is this, this, this, this, .... or this state, the player wins, but instead "if in any row or column or diagonal, the same symbol appears 3 times in a row...". Most people end up hard coding it for Tic Tac Toe, because it's quick and easy and unfortunately the board is so small that it's a feasible solution and a generic approach might look like overkill.

So think ahead. There are lots of games where gems, symbols or whatever must be placed in a row to score points. Those grids are usually large enough that hard coding every possible occurrence of 3 in a row/column/diagonal would be pretty insane (and lead to murder, if someone decides to change the number of row or columns on short notice). Point is: if you solve it generically for Tic Tac Toe, you will have a reusable piece of code that will work just as well for all kinds of games or situations where you need to find chains of length n in a grid of size x*y.

Also, whenever you look at a problem and think "the solution is going to be really complicated", find ways to break it down into manageable steps. For example, don't try to handle chains in rows, columns and diagonals all at once. Solve it step by step.

One first step might actually be to write a helper function that will turn x/y coordinates into an array index (and maybe the other way round), so you won't get additional headache from your board being represented by a 1 dimensional array.
f@dzhttp://festini.device-zero.de
Small tip:


keyPressed = Console.ReadKey(true);

switch (keyPressed.Key)
{
case ConsoleKey.NumPad1:
case ConsoleKey.D1:
// etc...
}


Can be shortened if you test ConsoleKeyInfo.KeyChar instead:


keyPressed = Console.ReadKey(true);

switch (keyPressed.KeyChar)
{
case '1':
// etc...
}

This topic is closed to new replies.

Advertisement