3
\$\begingroup\$

I recently began learning C++ and would appreciate your review of this simple tic-tac-toe game I made. I know the "3-in-a-row algorithm" is inefficient, but in what other ways could I improve the code? Thanks for the help!

#include <iostream>
#include <cstdlib>
#include <time.h>
#include <string>

std::string endText;
bool gameOver = false;
int w = 18;
int h = 18;
char numbers[3][3] = { {'1', '2', '3'}, {'4', '5', '6'}, {'7', '8', '9'} };

void drawBoard() {
    system("cls");
    for (int i = 1; i < w; i++) {
        for (int j = 1; j < h; j++) {
            if (i % (h / 3) == 0) {
                std::cout << "# ";
            }
            else if (j % (w / 3) == 0) {
                std::cout << "# ";
            }
            else if (i % 3 == 0 && j % 3 == 0) {
                int column = ((j / 3) - 1) / 2;
                int row = ((i / 3) - 1) / 2;
                std::cout << numbers[row][column] << " ";
            }
            else {
                std::cout << "  ";
            }
        }
        std::cout << std::endl;
    }
    std::cout << std::endl;
}

void playerInput() {
    int numInput;
    bool numFound = false;
    std::cout << "Select a box to mark: ";
    std::cin >> numInput;
    while (std::cin.fail()) {
        drawBoard();
        std::cin.clear();
        std::cin.ignore();
        std::cout << "Invalid input." << std::endl << std::endl;
        std::cout << "Select a box to mark: ";
        std::cin >> numInput;
    }
    for (int i = 0; i < 3; i++) {
        for (int j = 0; j < 3; j++) {
            int charInt = numbers[i][j] - '0';
            if (numInput == charInt) {
                numbers[i][j] = 'X';
                drawBoard();
                numFound = true;
                break;
            }
            else if (j == 2 && i == 2) {
                std::cout << "Invalid input." << std::endl << std::endl;
                playerInput();
            }
        }
        if (numFound) {
            break;
        }
    }
}

void compRandInput() {
    int row = rand() % 3;
    int column = rand() % 3;
    if (numbers[row][column] != 'X' && numbers[row][column] != 'O') {
        numbers[row][column] = 'O';
    }
    else {
        compRandInput();
    }
}

void checkRow(bool & madeTurn, char check) {
    for (int i = 0; i < 3; i++) {
        int symbolCount = 0;
        int columnNumber = -1;
        for (int j = 0; j < 3; j++) {
            if (numbers[i][j] == check) {
                symbolCount++;
            }
            else if (numbers[i][j] != 'X' && numbers[i][j] != 'O') {
                columnNumber = j;
            }
        }
        if (symbolCount == 2 && columnNumber >= 0) {
            numbers[i][columnNumber] = 'O';
            madeTurn = true;
            break;
        }
    }
}
void checkColumn(bool & madeTurn, char check) {
    for (int j = 0; j < 3; j++) {
        int symbolCount = 0;
        int rowNumber = -1;
        for (int i = 0; i < 3; i++) {
            if (numbers[i][j] == check) {
                symbolCount++;
            }
            else if (numbers[i][j] != 'X' && numbers[i][j] != 'O') {
                rowNumber = i;
            }
        }
        if (symbolCount == 2 && rowNumber >= 0) {
            numbers[rowNumber][j] = 'O';
            madeTurn = true;
            break;
        }
    }
}
//Diagonal from top left to bottom right
void checkTwoInDiagonalsB(bool & madeTurn, char check, char place) {
    for (int i = 0; i < 3; i++) {
        for (int j = 0; j < 3; j++) {
            if (numbers[i][j] == check) {
                if (j == 0 && i == 0) {
                    if (numbers[i + 1][j + 1] == check && numbers[i + 2][j + 2] != 'O' && numbers[i + 2][j + 2] != 'X') {
                        numbers[i + 2][j + 2] = place;
                        madeTurn = true;
                        break;
                    }
                    else if (numbers[i + 2][j + 2] == check && numbers[i + 1][j + 1] != 'O' && numbers[i + 1][j + 1] != 'X') {
                        numbers[i + 1][j + 1] = place;
                        madeTurn = true;
                        break;
                    }
                }
                else if (j == 1 && i == 1) {
                    if (numbers[i - 1][j - 1] == check && numbers[i + 1][j + 1] != 'O' && numbers[i + 1][j + 1] != 'X') {
                        numbers[i + 1][j + 1] = place;
                        madeTurn = true;
                        break;
                    }
                    else if (numbers[i + 1][j + 1] == check && numbers[i - 1][j - 1] != 'O' && numbers[i - 1][j - 1] != 'X') {
                        numbers[i - 1][j - 1] = place;
                        madeTurn = true;
                        break;
                    }
                }
                else if (j == 2 && i == 2) {
                    if (numbers[i - 2][j - 2] == check && numbers[i - 1][j - 1] != 'O' && numbers[i - 1][j - 1] != 'X') {
                        numbers[i - 1][j - 1] = place;
                        madeTurn = true;
                        break;
                    }
                    else if (numbers[i - 1][j - 1] == check && numbers[i - 2][j - 2] != 'O' && numbers[i - 2][j - 2] != 'X') {
                        numbers[i - 2][j - 2] = place;
                        madeTurn = true;
                        break;
                    }
                }
            }
        }
        if (madeTurn) {
            break;
        }
    }
}
//Diagonal from top right to bottom left
void checkTwoInDiagonalsF(bool & madeTurn, char check, char place) {
    for (int i = 0; i < 3; i++) {
        for (int j = 0; j < 3; j++) {
            if (numbers[i][j] == check) {
                if (j == 2 && i == 0) {
                    if (numbers[i + 1][j - 1] == check && numbers[i + 2][j - 2] != 'O' && numbers[i + 2][j - 2] != 'X') {
                        numbers[i + 2][j - 2] = place;
                        madeTurn = true;
                        break;
                    }
                    else if (numbers[i + 2][j - 2] == check && numbers[i + 1][j - 1] != 'O' && numbers[i + 1][j - 1] != 'X') {
                        numbers[i + 1][j - 1] = place;
                        madeTurn = true;
                        break;
                    }
                }
                else if (j == 1 && i == 1) {
                    if (numbers[i - 1][j + 1] == check && numbers[i + 1][j - 1] != 'O' && numbers[i + 1][j - 1] != 'X') {
                        numbers[i + 1][j - 1] = place;
                        madeTurn = true;
                        break;
                    }
                    else if (numbers[i + 1][j - 1] == check && numbers[i - 1][j + 1] != 'O' && numbers[i - 1][j + 1] != 'X') {
                        numbers[i - 1][j + 1] = place;
                        madeTurn = true;
                        break;
                    }
                }
                else if (j == 0 && i == 2) {
                    if (numbers[i - 2][j + 2] == check && numbers[i - 1][j + 1] != 'O' && numbers[i - 1][j + 1] != 'X') {
                        numbers[i - 1][j + 1] = place;
                        madeTurn = true;
                        break;
                    }
                    else if (numbers[i - 1][j + 1] == check && numbers[i - 2][j + 2] != 'O' && numbers[i - 2][j + 2] != 'X') {
                        numbers[i - 2][j + 2] = place;
                        madeTurn = true;
                        break;
                    }
                }
            }
        }
        if (madeTurn) {
            break;
        }
    }
}

void compInput() {
    bool turnMade = false;

    //Computer checks if it can make 3 in a row
    checkColumn(turnMade, 'O');
    if (!turnMade) {
        checkRow(turnMade, 'O');
    }
    if (!turnMade) {
        checkTwoInDiagonalsB(turnMade, 'O', 'O');
    }
    if (!turnMade) {
        checkTwoInDiagonalsF(turnMade, 'O', 'O');
    }

    //Computer checks if the player can be blocked
    if (!turnMade) {
        checkColumn(turnMade, 'X');
    }
    if (!turnMade) {
        checkRow(turnMade, 'X');
    }
    if (!turnMade) {
        checkTwoInDiagonalsB(turnMade, 'X', 'O');
    }
    if (!turnMade) {
        checkTwoInDiagonalsF(turnMade, 'X', 'O');
    }

    //Computer makes random move
    if (!turnMade) {
        compRandInput();
    }

    //Reset board with new marking
    drawBoard();
}

void checkWin(char symbol) {
    for (int i = 0; i < 3; i++) {
        for (int j = 0; j < 3; j++) {
            //Column
            if (i == 0 && numbers[i][j] == symbol) {
                if (numbers[i + 1][j] == symbol && numbers[i + 2][j] == symbol) {
                    gameOver = true;
                }
            }
            //Row
            if (j == 0 && numbers[i][j] == symbol) {
                if (numbers[i][j + 1] == symbol && numbers[i][j + 2] == symbol) {
                    gameOver = true;
                }
            }
            //Diagonal
            if (i == 0 && j == 0 && numbers[i][j] == symbol) {
                if (numbers[i + 1][j + 1] == symbol && numbers[i + 2][j + 2] == symbol) {
                    gameOver = true;
                }
            }
            if (i == 0 && j == 2 && numbers[i][j] == symbol) {
                if (numbers[i + 1][j - 1] == symbol && numbers[i + 2][j - 2] == symbol) {
                    gameOver = true;
                }
            }
        }
    }
}
void checkCatsGame() {
    bool catsGame = true;
    for (int i = 0; i < 3; i++) {
        for (int j = 0; j < 3; j++) {
            if (numbers[i][j] != 'X' && numbers[i][j] != 'O') {
                catsGame = false;
            }
        }
    }
    if (catsGame) {
        gameOver = true;
    }
}

int main() {
    srand(time(0));

    drawBoard();

    while (!gameOver) {
        checkCatsGame();
        if (gameOver) {
            endText = "Cat's game! ";
            break;
        }

        playerInput();
        checkWin('X');
        if (gameOver) {
            endText = "You win! ";
            break;
        }

        checkCatsGame();
        if (gameOver) {
            endText = "Cat's game! ";
            break;
        }

        compInput();
        checkWin('O');
        if (gameOver) {
            endText = "You lose. ";
            break;
        }
    }

    std::cout << endText << "Press enter to exit the console." << std::endl;
    std::cin.get();
    std::cin.get();
    return 0;
}
\$\endgroup\$
  • \$\begingroup\$ Looks more like c code using std::cout. Loads of things to improve. \$\endgroup\$ – πάντα ῥεῖ May 20 '18 at 18:40
  • \$\begingroup\$ See this answer for similar stuff. My comments on making the board a linear array with stride applies to you too, as does de-duplication of X and O logic, and much else. \$\endgroup\$ – JDługosz May 21 '18 at 5:49
4
\$\begingroup\$

I haven't done a deep analysis of your code, and I haven't tried to run it (because it won't run on my system). There is a lot here that needs work to meet modern C++ standards. Let's go section-by-section and see how far we get.

Headers

#include <time.h>

C headers of the form <xxx.h> are deprecated in C++. In almost all cases, you can simply do <cxxx>.

So here's you'd do #include <ctime>.

Global variables

Your whole program works via global variables. This is a terrible design, for numerous reasons. To give just one example: the global variables w and h are only used in a single function - drawBoard() - but they are visible to the entire program. It would be very easy for some other function to accidentally change the value of w or h (one letter variable names are also a bad idea!), and the next time you try to draw the board, it could be truncated, it could draw gibberish, or your program could crash.

All of those global variables should be removed, and every function should be refactored to take arguments, and return results. You don't need a single global variable in your program.

The board

You have defined the game board like this:

char numbers[3][3] = { {'1', '2', '3'}, {'4', '5', '6'}, {'7', '8', '9'} };

The first problem, already mentioned, is that this is a global variable. In addition to all the usual problems with global variables, by making the board global, you make it harder to do a lot of interesting things. It will be harder to "play again", if the user enjoyed the game. It will be harder to undo moves, if you want to add that feature. It will be harder to code an AI to make smart moves (rather than just random plays).

C++ is a strongly typed language; it's all about the types. If you get your types right... everything else just works.

What you should do is make a board class. A properly designed board class could initialize itself, and do checks to make sure you don't play out of bounds or play a square that's already been played, and so on.

But that's a big change. Let's start with small improvements for now.

For now, rather than using C arrays for the board, use proper C++ arrays. And while we're at it, define the board type:

using board_type = std::array<std::array<char, 3>, 3>;

board_type numbers = {{ {'1', '2', '3'}, {'4', '5', '6'}, {'7', '8', '9'} }};

Just this change makes your code safer (C arrays have dangerous properties, like silent decay to pointers). Other benefits will be clear soon.

drawBoard()

First, as with all of your functions, you should modify it to avoid global variables. In this case, you'd need to take the board to draw as a parameter. Something like this:

void drawBoard(board_type const& numbers) {

Even better, rather that always writing to std::cout, you can take the output as a parameter:

void drawBoard(board_type const& numbers, std::ostream& out) {

If you do that, you can even write boards to files, for logging/debugging, or saving games.

This function doesn't need to return anything but it does need to use w and h. There's no reason those can't be local variables, though - they're not even used anywhere else.

Even better, you could calculate w and h from the board dimensions. You could also make them const, because they won't change. For example:

auto const w = numbers[0].size() * 6;
auto const h = numbers.size() * 6;

Next:

system("cls");

First, this should probably be std::system("cls");. Second, you should be aware that using std::system() is generally unportable. In fact, I couldn't run your code if I wanted to, because my system doesn't have "cls".

From this and other clues, I guess you're probably trying this program in a Windows GUI. If that's the only place you want the program to work, that's fine, but if you want portable code, you can't use system-specific stuff, or assume that the console will exit after your program is done (or even that there is a console).

std::cout << std::endl;

Using std::endl is almost always wrong. If all you're doing is writing a new line, just do std::cout << '\n';. It can be hundreds of times faster.

You should especially never do std::cout << std::endl << std::endl;. That should almost always be std::cout << '\n' << '\n'; or std::cout << "\n\n";.

playerInput()

Once again, to avoid global variables, you could take board as a reference parameter:

void playerInput(board_type& numbers)

Even better, you could make it work for either X or O:

void playerInput(board_type& numbers, char play)

The main problem with this function is that it does too much stuff. It reads a number in from cin, it checks to see if it's a valid move, and then it does the move. That's 3 different jobs, that should be 3 different functions, like this.

void playerInput(board_type& numbers, char play) {
    while (true) {
        // Read number from cin and make sure it's in range
        auto numInput = getPlayerMove();

        // Check that number is available to play
        if (isValidMove(numbers, numInput)) {
            // Make the play and return.
            makeMove(numbers, numInput, play);
            return;
        }

        // If we get here, the number was not available to play,
        // so print the error message, and the loop will start over
        std::cout << "Invalid input.\n\n";
    }
}

If you did split out those 3 functions, they would be reusable elsewhere. For example, when the AI is making a move, it could reuse isValidMove() and makeMove() functions.

As for the function as it's currently written....

int numInput;
bool numFound = false;

Declaring all your variables at the start of the function is archaic C practice. In C++ (and C after 1999), declare your variables when they're needed.

std::cin >> numInput;
while (std::cin.fail()) {
    drawBoard();
    std::cin.clear();
    std::cin.ignore();
    std::cout << "Invalid input." << std::endl << std::endl;
    std::cout << "Select a box to mark: ";
    std::cin >> numInput;
}

This could be simplified by taking advantage of istream's conversion to bool, like this:

int numInput = 0;
while (!(std::cin >> numInput)) {
    drawBoard(); // or perhaps 
    std::cin.clear();
    std::cin.ignore();
    std::cout << "Invalid input.\n\n";
    std::cout << "Select a box to mark: ";
}

This is technically not a portable way to convert a character to a digit:

int charInt = numbers[i][j] - '0';

But I've never used a platform where it fails. However, it does cause a bug in your code because not everything in numbers is a digit - some characters are X or O. To see the bug in action, start a game, play one good move, then on your second move, enter either 40 or 31.

if (numInput == charInt) {
    numbers[i][j] = 'X';
    drawBoard();
    numFound = true;
    break;
}

The only purpose of numFound in this function is to break out of nested loops. Instead, once the number is found, you can just return.

else if (j == 2 && i == 2) {
    std::cout << "Invalid input." << std::endl << std::endl;
    playerInput();
}

Using recursion to handle errors like this is unwise. In an unbounded recursion, it's possible to get stack overflow. (Although in this case, the compiler might be able to use a tail call to avoid nested stacks.) This function should be refactored to use a loop, not recursion.

compRandInput()

As always, this function should take parameters and return values to avoid using global variables.

Don't use rand(). It can be a garbage implementation, and it's hard to use correctly. (In fact, you're using it wrong.) Use the <random> library.

Once again, this recursion is unbounded, so you should probably use iteration instead.

void compRandInput(board_type& numbers, char play, random_engine_type& rnd_eng) {
    auto col_dist = std::uniform_int_distribution<>{0, numbers[0].size() - 1};
    auto row_dist = std::uniform_int_distribution<>{0, numbers.size() - 1};

    int row = 0;
    int col = 0
    do {
        row = row_dist(rnd_eng);
        col = col_dist(rnd_eng);
    } while (numbers[row][column] == 'X' || numbers[row][column] == 'O');
    // Or, even better, reuse the function:
    // while (!isValidMove....

    numbers[row][col] = play;
}

And so on...

That should be enough to keep you busy refactoring for a while. The most important recommendations are:

  • Make a class for the board. That class can do all the busywork to keep things sane and avoid bugs.
  • Don't use global variables. Have your functions take parameters and return values instead.
  • Break tasks into functions, and reuse them. Each function should have one job; functions that do multiple things should be broken up. Look especially for opportunities to reuse code. For example, whether it's a human player or AI player, there are a ton of things that need to be done for both, like checking whether a move is valid, actually making a move, and checking for a win.
\$\endgroup\$
  • \$\begingroup\$ Thanks a lot for all of your tips. As I made this program, I wasn't sure if I should use classes, but I've come to understand that classes can be used for general processes, not only specific objects. It also did feel wrong having only void functions without parameters, so I agree with what you said about those. \$\endgroup\$ – Matthew Taylor May 22 '18 at 0:27

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.