3
\$\begingroup\$

I've made a major update to my Noughts & Crosses program, this ISN'T a duplicate! I know it's not good to use system("pause") and system("cls"), however, I just wanted to make the console screen more readable so resorted to these methods to achieve this. I would like feedback on how well I've used std::shared_ptr, the std::algorithm library, and any suggestions on how I can improve. I have also returned a std::shared_ptr null-ptr. A lot of what I've done has been intuitive and I'm unsure if what I've done is the best way to go about doing certain things. If you run this program, please, just press enter where needed, it's two robots playing against each other. If you'd like to manually play it, please change the arguments passed into the game.play function to a human.

#include <iostream>
#include <vector>
#include <map>
#include <utility>
#include <algorithm>

#include <random>
#include <ctime>
#include <cstdlib>
#include <ranges>
class Player
{
private:
    std::string m_type;
    unsigned char m_name;
    int m_winTally;
    int m_drawed;

public:
    std::string GetType()const { return m_type; };
    Player(unsigned char name, std::string&& type = "Player") :m_name(name), m_type(type), m_winTally(0), m_drawed(0) {}
    virtual unsigned char GetName()const { return m_name; }
    virtual bool ClaimSquare(std::map<int, unsigned char>& board, int move) = 0;
    virtual int NextMove(std::map<int, unsigned char>& board) = 0;
    virtual ~Player() = default;
    void AddWinToTally() { m_winTally++; }
    void Drawed() { m_drawed++; }
    int GetGameWins() const { return m_winTally; }
    int GetDraws() const { return m_drawed; }
};

class Human : public Player
{

public:
    Human(unsigned char name) :Player(name, "Human") {}
    virtual int NextMove(std::map<int, unsigned char>& board) override {
        int move;
        std::cout << "Enter a number on the board (e.g. 1): ";
        std::cin >> move;
        return move;
    }
    virtual bool ClaimSquare(std::map<int, unsigned char>& board, int move)
    {
        auto validSquare = std::find_if(board.begin(), board.end(), [&](auto pair) {
            return pair.first == move;
            });
        if (validSquare != board.end())
        {
            if (validSquare->second == '-')
            {
                validSquare->second = Player::GetName();
                return true;
            }
            else
            {
                std::cout << "This square has already been claimed. Choose a different square!" << std::endl;
                return false;
            }
        }
        return false;
    }
    virtual ~Human() = default;
};

class Robot : public Player
{

public:
    Robot(unsigned char name) :Player(name, "Robot") {}

    bool CheckAvailability(std::map<int, unsigned char>& board, int number, std::vector<int>& keys) {
        for (auto& cell : board) {
            if (cell.first == number) {
                if (cell.second == '-') {
                    return true;
                }
            }
        }
        std::remove_if(keys.begin(), keys.end(), [&](auto& key) {
            return key == number;
            });
        return false;
    }
    virtual int NextMove(std::map<int, unsigned char>& board) override
    {
        std::vector<int>number = { 1,2,3,4,5,6,7,8,9 };
        int randNum = 0;
        std::srand(std::time(0));
        do
        {
            randNum = rand() % 9 + 1;
        } 
        while (CheckAvailability(board, randNum, number) == false);
        return randNum;
    }
    virtual bool ClaimSquare(std::map<int, unsigned char>& board, int move)
    {
        auto validSquare = std::find_if(board.begin(), board.end(), [&](auto pair) {
            return pair.first == move;
            });
        if (validSquare != board.end())
        {
            if (validSquare->second == '-')
            {
                validSquare->second = Player::GetName();
                return true;
            }
            else
            {
                std::cout << "This square has already been claimed. Choose a different square!" << std::endl;
                return false;
            }
        }
        return false;
    }
    virtual ~Robot() = default;
};

class NoughtsAndCrosses
{
private:

    //std::vector<Player*> m_p;
    std::map<int, unsigned char>board;
 
    void DisplayBoard()
    {

        for (auto const& cell : board)
        {
            if (cell.first % 3 == 1) {
                std::cout << "\n\n";
            }
            if (cell.second != '-') {
                std::cout << cell.second << "        ";
            }
            else
            {
                std::cout << cell.first << "        ";
            }
        }
        std::cout << "\n\n";
    }
    auto CheckForAWinner(std::map<int, unsigned char>& board, std::shared_ptr<Player>& player)
    {
        if (board.at(1) == player->GetName() && board.at(2) == player->GetName() && board.at(3) == player->GetName()) {
            return true;
        }
        else if (board.at(4) == player->GetName() && board.at(5) == player->GetName() && board.at(6) == player->GetName()) {
            return true;
        }
        else if (board.at(7) == player->GetName() && board.at(8) == player->GetName() && board.at(9) == player->GetName()) {
            return true;
        }
        else if (board.at(1) == player->GetName() && board.at(4) == player->GetName() && board.at(7) == player->GetName()) {
            return true;
        }
        else if (board.at(2) == player->GetName() && board.at(5) == player->GetName() && board.at(8) == player->GetName()) {
            return true;
        }
        else if (board.at(3) == player->GetName() && board.at(6) == player->GetName() && board.at(9) == player->GetName()) {
            return true;
        }
        else if (board.at(1) == player->GetName() && board.at(5) == player->GetName() && board.at(9) == player->GetName()) {
            return true;
        }
        else if (board.at(7) == player->GetName() && board.at(5) == player->GetName() && board.at(3) == player->GetName()) {
            return true;
        }
        else
        {
            return false;
        }
    }
    bool CheckForDraw(std::map<int, unsigned char>& board) {

        return std::all_of(board.begin(), board.end(), [&](auto& pair) {return pair.second != '-'; });
    }
public:
    NoughtsAndCrosses()
    {
        board = { std::make_pair(1,'-'),std::make_pair(2,'-'),std::make_pair(3,'-'),
          std::make_pair(4,'-'),std::make_pair(5,'-'),std::make_pair(6,'-'),
          std::make_pair(7,'-'),std::make_pair(8,'-'),std::make_pair(9,'-') };
    }
    void ResetBoard() {
     
        std::for_each(board.begin(), board.end(), [&](auto& pair) {
            pair.second = '-'; 
            });
        
    }
    auto play(std::shared_ptr<Player>& p1, std::shared_ptr<Player>& p2)
    {
        int currentPlayer = 1;
        bool isWinner = false;
        bool isDraw = false;

        std::vector<std::shared_ptr<Player>>m_player = { p1, p2 };
        do
        {
            currentPlayer = (currentPlayer + 1) % 2;
            do
            { 
               
              
               system("cls");
               std::cout << m_player.at(currentPlayer)->GetType() << ": " << m_player.at(currentPlayer)->GetName() << " turn: " << std::endl;
               DisplayBoard();
               system("pause");
               
               
               
            } 
            while (m_player.at(currentPlayer)->ClaimSquare(board, m_player.at(currentPlayer)->NextMove(board)) == false);
            
            //std::cout << "\nPress enter to make the robot move. . .";
            //std::cin.get();
            //system("cls");
        } while (CheckForDraw(board) == false && (isWinner = CheckForAWinner(board, m_player.at(currentPlayer))) == false);
        
        
        if (isWinner == true)
        {
            return m_player.at(currentPlayer);
        }  
        m_player.at(0)->Drawed();
        m_player.at(1)->Drawed();
        DisplayBoard();
        ResetBoard();
        
        return std::shared_ptr<Player>(nullptr);
    }
    
};

int main() {


    std::shared_ptr<Player> human1 = std::make_shared<Human>('O');
    std::shared_ptr<Player> human2 = std::make_shared<Human>('X');
    std::shared_ptr<Player> robot1 = std::make_shared<Robot>('O');
    std::shared_ptr<Player> robot2 = std::make_shared<Robot>('X');
    std::vector<std::shared_ptr<Player>>player = { robot1, robot2 };
    NoughtsAndCrosses game;
    
    int round = 3;
    int roundCount = 0;
    std::shared_ptr<Player>winner;
    do 
    {
        
        int gameCount = 1;
        int totalGamesinRound = 3;
        std::cout << "START GAME!\n";
        system("pause");
        system("cls");
        std::cout << "\nROUND " << ++roundCount << ". . .\n";
        do
        {
            std::cout << "Game " << gameCount << " of round " << roundCount << "\n";
            winner = game.play(robot1, robot2);
            
            if (winner != std::shared_ptr<Player>(nullptr)) 
            {
                std::cout << "Winner of game " << gameCount << " is type: " << winner->GetType() << ": " << winner->GetName() << "\n";
                winner->AddWinToTally();
            }
            else 
            {
                system("cls");
                std::cout << "Game " << gameCount << " is a draw!\n";
                
            }
           
            gameCount++;
            totalGamesinRound--;
        } 
        while (totalGamesinRound != 0);

        /* std::cout << "Game 2: Human vs Robot\n";
         game.play(robot1, robot1);*/
        std::cout << "Wins for " << robot1->GetType() << ": Player : " << robot1->GetName() << " - " << robot1->GetGameWins() << "\n";
        std::cout << "Wins for " << robot2->GetType() << ": Player : " << robot2->GetName() << " - " << robot2->GetGameWins() << "\n";
        std::cout << "Drawed: " << robot1->GetDraws() << "\n";

        auto playerWithMostWins = std::max_element(player.begin(), player.end(),
            [](const auto& lhs, const auto& rhs)
            {
                return lhs->GetGameWins() < rhs->GetGameWins();
            });

        std::cout << "Winner of round " << roundCount << " is " << playerWithMostWins->get()->GetName() << "\n";
        round--;
    } 
    while (round != 0);
}
\$\endgroup\$
3
  • \$\begingroup\$ Well, I’ve used the std::algorithm library so I would say C++ 17. \$\endgroup\$ – George Austin Bradley Dec 6 '20 at 23:00
  • \$\begingroup\$ I’ve changed it to C++ 17. \$\endgroup\$ – George Austin Bradley Dec 6 '20 at 23:12
  • \$\begingroup\$ Just so you know, the algorithms library existed even before C++11 :) \$\endgroup\$ – G. Sliepen Dec 7 '20 at 8:51
4
\$\begingroup\$

Make system() more portable

The reason system("cls") isn't portable is because the command on windows is cls but on most other platforms it is clear. You can check for the OS using certain macros.

void clear_screen() {
#ifdef _WIN32 // windows
    system("cls");
#else // macOS and linux
    system("clear");
#endif //_WIN32
}

A similar situation for pause

void pause() {
#ifdef _WIN32 // windows
    system("pause");
#else // macOS and linux
    system("read")
#endif //_WIN32
}

I know it's not good to use system("pause") and system("cls") however, I just wanted to > make the console screen more readable

It isn't not good, but pure evil


Code structure

There is a lot of room for improvement here. Let me start with the simplest one

  • Unecessary getters and setters!
class Player
{
private:
    std::string m_type;
    unsigned char m_name;
    int m_winTally;
    int m_drawed;

public:
    std::string GetType()const { return m_type; };
    Player(unsigned char name, std::string&& type = "Player") :m_name(name), m_type(type), m_winTally(0), m_drawed(0) {}
    virtual unsigned char GetName() const { return m_name; }
    virtual bool ClaimSquare(std::map<int, unsigned char>& board, int move) = 0;
    virtual int NextMove(std::map<int, unsigned char>& board) = 0;

    void AddWinToTally() { m_winTally++; }
    void Drawed() { m_drawed++; }
    int GetGameWins() const { return m_winTally; }
    int GetDraws() const { return m_drawed; }
};

How about

class Player
{
public:
    std::string m_type;
    unsigned char m_name;
    int m_winTally;
    int m_drawed;

    Player(unsigned char name, std::string&& type = "Player")
        : m_name(name), m_type(type), m_winTally(0), m_drawed(0)
    {}

    virtual unsigned char GetName() const { return m_name; }
    virtual bool ClaimSquare(std::map<int, unsigned char>& board, int move) = 0;
    virtual int NextMove(std::map<int, unsigned char>& board) = 0;
};


  • ClaimSquare()

Your function ClaimSquare() is virtual and has been overridden in both of the derived classes, let's look at them

Human

    virtual bool ClaimSquare(std::map<int, unsigned char>& board, int move)
    {
        auto validSquare = std::find_if(board.begin(), board.end(), [&](auto pair) {
            return pair.first == move;
            });
        if (validSquare != board.end())
        {
            if (validSquare->second == '-')
            {
                validSquare->second = Player::GetName();
                return true;
            }
            else
            {
                std::cout << "This square has already been claimed. Choose a different square!" << std::endl;
                return false;
            }
        }
        return false;
    }

Robot

     virtual bool ClaimSquare(std::map<int, unsigned char>& board, int move)
     {
        auto validSquare = std::find_if(board.begin(), board.end(), [&](auto pair) {
            return pair.first == move;
            });
        if (validSquare != board.end())
        {
            if (validSquare->second == '-')
            {
                validSquare->second = Player::GetName();
                return true;
            }
            else
            {
                std::cout << "This square has already been claimed. Choose a different square!" << std::endl;
                return false;
            }
        }
        return false;
    }
  • They both are the EXACT same. The point of polymorphism is to reuse the same code i.e reduce repetition. Here you have done the EAXCT opposite, introduced more repetition.
  • Why is it virtual?
  • On the contrary, it shouldn't be a part of the player class. It should be a part of your NoughtsAndCrosses since that contains the game board. The same applies to CheckAvailibility(). What does that have to do with a player? Nothing at all. All of that belongs to NoughtsAndCrosses

With all of that being said, here is what I think it should be

struct Player {
    
    char symbol;
    const std::string type;
    int wins;
    int draws;

    virtual int nextMove() const = 0;
    
    Player (const char symbol, std::string&& type)
        : symbol(symbol), type(std::move(type)), wins(0), draws(0)
    {}
};
// Edit: As pointed out by G.Sliepen, Player::type remains unchanged, therefore `const`

struct Human : public Player {
    Human(const char symbol)
        : Player(symbol, "Human")
    {}

    int nextMove() const override {
        // get move from STDIN and return 
    }
};

struct Robot : public Player {
    Robot(const char symbol)
        : Player(symbol, "Robot")
    {}

    int nextMove() const override {
        // generate random number and return 
    }
};

Using this, your game class can have two objects, one player and one human, and you can switch between them when you need to.

class Game{
    Player* one;
    Player* two;

    Player* turn; // either points to player one or player two, switch accordingly


    
public:
    Game(Player& one, Player& two)
        : one(&one), two(&two), turn(&one)
    {}

    // board, checkWin(), checkDraw()....
    // if player one wins -> one.wins++
    // if player two wins -> two.wins++
}

With that, you can easily create games, human vs human, robot vs human, robot vs robot

int main() {
    Human h('x');
    Human h2('o');
    
    Game game1(h, h2) //  human vs human, can be anything else
}

No need for shared_ptr here.

With all that being said, I suggest you create a gameloop() function for the Game class that automatically plays the game. That is calling nextMove() until someone wins.


Represent the board with std::array

I'm not sure why you chose std::map to represent the board. Using std::array here is enough and will simplify everything further.


Pseudo code

class Game
{
    Player* one;
    Player* two;

    Player* turn; // either points to player one or player two
public:
    

    Game(Player& one, Player& two)
        : one(&one), two(&two), turn(&one)
    {}


    void gameloop(){
        for(;;){
            clear_screen();
            playerMove();
            if (win())
                ;// bla bla
            else if (draw())
                ; // bla bla bla

        }
    }

private:
    bool moveIsValid(const int move){
        // check if square is occupied, or out of range and return accordingly 
    }

    void performMove(const int move){
        // perform move on the board 
    }

    void playerMove(){
        int move = -1;

        for(;;){
            move = turn->nextMove();
            if (moveIsValid(move))
                break;
        }
        performMove(move);
        switchPlayers();
    }

    void switchPlayers(){
        turn = turn == one ? two : one;
    }

    char win(){
        // self explanatory
    }
    bool draw(){
        // self explanatory
    }


    std::array < char, 9 > board;
};
\$\endgroup\$
20
  • 1
    \$\begingroup\$ If you are using public member variables instead of getters and setters, you should make the ones that should not change after construction const. Of course they could've been made const even if you use getters and setters, but then it's slightly less important. \$\endgroup\$ – G. Sliepen Dec 7 '20 at 8:54
  • \$\begingroup\$ @G.Sliepen are you referring to type in Player? \$\endgroup\$ – Parekh Dec 7 '20 at 9:04
  • 1
    \$\begingroup\$ Thanks so much for reviewing my program! I'm now analysing all you have mentioned! I shall mark as answer after I have considered whether I need to ask any questions! \$\endgroup\$ – George Austin Bradley Dec 7 '20 at 11:00
  • 1
    \$\begingroup\$ @AryanParekh If it's desired to be able to change m_name, then of course it shouldn't be const. But in OP's code there was no way to change it after construction, that's why I assumed it was meant to be constant. \$\endgroup\$ – G. Sliepen Dec 7 '20 at 15:22
  • 1
    \$\begingroup\$ Oh crumbs, I missed the switch player part. Thanks very much for reviewing my program. I have learned so much! \$\endgroup\$ – George Austin Bradley Dec 8 '20 at 8:01

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.