2
\$\begingroup\$

After a substantial amount of programming experience in high level languages like Javascript and Python, I decided to try learning a low level language for once, so I did C++. As my first project I made a graphical connect 4 game, which also has a computer ai implemented in it, using the Monte Carlo Tree Search algorithm. I represent the connect 4 board using 64 bit numbers (1 means there is a piece there, 0 means there isn't). I do this just to make the game logic simpler to make (it is easier to calculate moves, check wins, etc).

Here are the files:

common.h (this is just global variables)

#pragma once
#include <cstdint>
extern uint64_t bits[42];
extern uint64_t columns[7];
extern uint64_t combinations[69];
extern int aiPlayer;

computerPlayer.cpp (my implimentation of the Monte Carlo Tree Search algorithm)

#include "common.h"
#include "node.h"
#include "game.h"
#include "computerPlayer.h"
#include <cstdint>
#include <vector>
#include <cstdlib>
#include <math.h>
int ComputerPlayer::bestMove(uint64_t redBoard, uint64_t yellowBoard, uint64_t bothBoards, int searcherSide) {
  nodes.clear();
  selectedNodeIndex = 0;
  node rootNode; 
  rootNode.redBoard = redBoard;
  rootNode.yellowBoard = yellowBoard;
  rootNode.bothBoards = bothBoards;
  rootNode.wins = 0.0;
  rootNode.simulations = 0.0;
  rootNode.sideToPlay = searcherSide;
  // Assume that the position we are searching to begin with is not given to be terminal.
  rootNode.terminalStatus = 0;
  rootNode.parentNodeIndex = -1;
  nodes.push_back(rootNode);
  for (int i = 0; i < 100000; i++) {
    expand();
    simulate();
    propogate();
    select();
  }
  std::vector<int> rootNodeChildNodesIndices = nodes[0].childNodesIndices;
  uint64_t bestBothBoards;
  double bestSimulations = 0.0;
  for (auto rootNodeChildNodeIndex : rootNodeChildNodesIndices) {
    node childNode = nodes[rootNodeChildNodeIndex];
    uint64_t childNodeBothBoards = childNode.bothBoards;
    double childNodeSimulations = childNode.simulations;
    if (childNodeSimulations > bestSimulations) {
      bestBothBoards = childNodeBothBoards;
      bestSimulations = childNodeSimulations;
    }
  }
  std::vector<int> rootMoves = moves(bothBoards);
  int bestMove;
  for (auto rootMove : rootMoves) {
    uint64_t rootMoveBit = bits[moveTargetSquare(rootMove, bothBoards)];
    if ((bothBoards | rootMoveBit) == bestBothBoards) {
      bestMove = rootMove;
      break;
    }
  }
  return bestMove;
}
void ComputerPlayer::expand() {
  node selectedNode = nodes[selectedNodeIndex];
  if (selectedNode.terminalStatus != 0) {
    return;
  }
  uint64_t selectedNodeRedBoard = selectedNode.redBoard;
  uint64_t selectedNodeYellowBoard = selectedNode.yellowBoard;
  uint64_t selectedNodeBothBoards = selectedNode.bothBoards;
  int selectedNodeSideToPlay = selectedNode.sideToPlay;
  std::vector<int> selectedNodeMoves = moves(selectedNodeBothBoards);
  for (auto selectedNodeMove : selectedNodeMoves) {
    node childNode;
    uint64_t childNodeRedBoard = selectedNodeRedBoard;
    uint64_t childNodeYellowBoard = selectedNodeYellowBoard;
    uint64_t childNodeBothBoards = selectedNodeBothBoards;
    uint64_t childNodeMoveBit = bits[moveTargetSquare(selectedNodeMove, childNodeBothBoards)];
    if (selectedNodeSideToPlay == 0) {
      childNodeRedBoard |= childNodeMoveBit;
    } else {
      childNodeYellowBoard |= childNodeMoveBit;
    }
    childNodeBothBoards |= childNodeMoveBit;
    int childNodeTerminalStatus = 0;
    if (childNodeBothBoards == 4398046511103ULL) {
      childNodeTerminalStatus = 1;
    } else if (selectedNodeSideToPlay == 0) {
      if (isWinning(childNodeRedBoard)) {
        childNodeTerminalStatus = 2;
      }
    } else {
      if (isWinning(childNodeYellowBoard)) {
        childNodeTerminalStatus = 3;
      }
    }
    childNode.redBoard = childNodeRedBoard;
    childNode.yellowBoard = childNodeYellowBoard;
    childNode.bothBoards = childNodeBothBoards;
    childNode.wins = 0.0;
    childNode.simulations = 0.0;
    childNode.sideToPlay = selectedNodeSideToPlay ^ 1;
    childNode.terminalStatus = childNodeTerminalStatus;
    childNode.parentNodeIndex = selectedNodeIndex;
    nodes[selectedNodeIndex].childNodesIndices.push_back(nodes.size());
    nodes.push_back(childNode);
  } 
}
void ComputerPlayer::simulate() {
  node selectedNode = nodes[selectedNodeIndex];
  int selectedNodeTerminalStatus = selectedNode.terminalStatus;
  if (selectedNodeTerminalStatus == 1) {
    nodes[selectedNodeIndex].wins += 0.5;
    nodes[selectedNodeIndex].simulations += 1.0;
    return;
  } else if (selectedNodeTerminalStatus == 2) {
    nodes[selectedNodeIndex].wins += 1.0;
    nodes[selectedNodeIndex].simulations += 1.0;
    return;
  } else if (selectedNodeTerminalStatus == 3) {
    nodes[selectedNodeIndex].simulations += 1.0;
    return;
  }
  std::vector<int> selectedNodeChildNodesIndices = selectedNode.childNodesIndices;
  for (auto selectedNodeChildNodeIndex : selectedNodeChildNodesIndices) {
    node childNode = nodes[selectedNodeChildNodeIndex];
    uint64_t currentNodeRedBoard = childNode.redBoard;
    uint64_t currentNodeYellowBoard = childNode.yellowBoard;
    uint64_t currentNodeBothBoards = childNode.bothBoards;
    int currentNodeSideToPlay = childNode.sideToPlay;
    while (true) {
      if (currentNodeBothBoards == 4398046511103ULL) {
        nodes[selectedNodeChildNodeIndex].wins += 0.5;
        nodes[selectedNodeChildNodeIndex].simulations += 1.0;
        break;
      } else if (currentNodeSideToPlay == 0) {
        if (isWinning(currentNodeYellowBoard)) {
          nodes[selectedNodeChildNodeIndex].simulations += 1.0;
          break;
        }
      } else {
        if (isWinning(currentNodeRedBoard)) {
          nodes[selectedNodeChildNodeIndex].wins += 1.0;
          nodes[selectedNodeChildNodeIndex].simulations += 1.0;
          break;
        }
      }
      std::vector<int> currentNodeMoves = moves(currentNodeBothBoards);
      int currentNodeMove = currentNodeMoves[rand() % currentNodeMoves.size()];
      uint64_t currentNodeMoveBit = bits[moveTargetSquare(currentNodeMove, currentNodeBothBoards)];
      if (currentNodeSideToPlay == 0) {
        currentNodeRedBoard |= currentNodeMoveBit;
      } else {
        currentNodeYellowBoard |= currentNodeMoveBit;
      }
      currentNodeBothBoards |= currentNodeMoveBit;
      currentNodeSideToPlay ^= 1;
    }
  } 
}
void ComputerPlayer::propogate() {
  node selectedNode = nodes[selectedNodeIndex];
  int selectedNodeTerminalStatus = selectedNode.terminalStatus;
  double terminalWinToAdd;
  bool isTerminalNode = false;
  if (selectedNodeTerminalStatus == 1) {
    terminalWinToAdd = 0.5;
    isTerminalNode = true;
  } else if (selectedNodeTerminalStatus == 2) {
    terminalWinToAdd = 1.0;
    isTerminalNode = true;
  } else if (selectedNodeTerminalStatus == 3) {
    terminalWinToAdd = 0.0;
    isTerminalNode = true;
  }
  if (isTerminalNode) {
    int currentNodeIndex = selectedNodeIndex;
    while (true) {
      int currentNodeParentNodeIndex = nodes[currentNodeIndex].parentNodeIndex;
      if (currentNodeParentNodeIndex == -1) {
        break;
      } 
      nodes[currentNodeParentNodeIndex].wins += terminalWinToAdd;
      nodes[currentNodeParentNodeIndex].simulations += 1.0;
      currentNodeIndex = currentNodeParentNodeIndex;
    }
    return;
  }
  std::vector<int> selectedNodeChildNodesIndices = selectedNode.childNodesIndices;
  for (auto selectedNodeChildNodeIndex : selectedNodeChildNodesIndices) {
    double winToAdd = nodes[selectedNodeChildNodeIndex].wins;
    int currentNodeIndex = selectedNodeChildNodeIndex;
    while (true) {
      int currentNodeParentNodeIndex = nodes[currentNodeIndex].parentNodeIndex;
      if (currentNodeParentNodeIndex == -1) {
        break;
      } 
      nodes[currentNodeParentNodeIndex].wins += winToAdd;
      nodes[currentNodeParentNodeIndex].simulations += 1.0;
      currentNodeIndex = currentNodeParentNodeIndex;
    }
  }
}
void ComputerPlayer::select() {
  int currentNodeIndex = 0;
  while (true) {
    node currentNode = nodes[currentNodeIndex];
    std::vector<int> currentNodeChildNodesIndices = currentNode.childNodesIndices;
    if (currentNodeChildNodesIndices.size() == 0) {
      selectedNodeIndex = currentNodeIndex;
      break;
    }
    double currentNodeSimulations = currentNode.simulations;
    int currentNodeSideToPlay = currentNode.sideToPlay;
    int bestChildNodeIndex;
    double bestUcb = 0.0;
    for (auto currentNodeChildNodeIndex : currentNodeChildNodesIndices) {
      node childNode = nodes[currentNodeChildNodeIndex];
      double childNodeWins = childNode.wins;
      double childNodeSimulations = childNode.simulations;
      double exploitation = childNodeWins / childNodeSimulations;
      // Account for perspective when selecting nodes.
      if (currentNodeSideToPlay == 1) {
        exploitation = 1.0 - exploitation;
      }
      double ucb = exploitation + sqrt(2.0 * log(currentNodeSimulations) / childNodeSimulations);
      if (ucb > bestUcb) {
        bestChildNodeIndex = currentNodeChildNodeIndex;
        bestUcb = ucb;
      }
    }
    currentNodeIndex = bestChildNodeIndex;
  }
}

computerPlayer.h

#pragma once
#include "node.h"
class ComputerPlayer {
  private:
    std::vector<node> nodes;
    int selectedNodeIndex;
    void expand();
    void simulate();
    void propogate();
    void select();
  public:
    int bestMove(uint64_t redBoard, uint64_t yellowBoard, uint64_t bothBoards, int sideToPlay);
};

game.cpp (handles logic for generating moves, validating moves, executing moves)

#include "common.h"
#include <cstdint>
#include <vector>
int popcount(uint64_t n) {
  int count = 0;
  while (n) {
    if ((n & 1ULL) == 1ULL) {
      count++;
    }
    n >>= 1ULL;
  }
  return count;
}
std::vector<int> moves(uint64_t bothBoards) {
  uint64_t topRow = (bothBoards & 4363686772736ULL) >> 35ULL;
  std::vector<int> moves;
  for (int i = 0; i < 7; i++) { 
    if ((topRow & 1ULL) == 0ULL) {
      moves.push_back(6 - i);
    }
    topRow >>= 1ULL;
  }
  return moves;
}
int moveTargetSquare(int move, uint64_t bothBoards) {
  return 7 * (5 - popcount(bothBoards & columns[move])) + move;
}
bool isWinning(uint64_t ourBoard) {
  for (int i = 0; i < 69; i++) {
    uint64_t combination = combinations[i];
    if ((ourBoard & combination) == combination) {
      return true;
    }
  }
  return false;
}
bool verifyMove(int move, uint64_t bothBoards) {
  if (popcount(bothBoards & columns[move]) == 6) {
    return false;
  }
  return true;
}

game.h

#pragma once
std::vector<int> moves(uint64_t bothBoards);
int moveTargetSquare(int move, uint64_t bothBoards);
bool isWinning(uint64_t ourBoard);
bool verifyMove(int move, uint64_t bothBoards);

main.cpp (the main game loop, graphics, and initialisation for global variables)

#include <math.h>
#include <unistd.h>
#include <SFML/Graphics.hpp>
#include "common.h"
#include "game.h"
#include "computerPlayer.h"
uint64_t bits[42];
uint64_t columns[7];
uint64_t combinations[69];
uint64_t redBoard = 0ULL;
uint64_t yellowBoard = 0ULL;
uint64_t bothBoards = 0ULL;
int humanPlayer;
int aiPlayer;
int gameState = 0;
sf::Color gray = sf::Color(128, 128, 128, 255);
sf::Color cyan = sf::Color(0, 255, 255, 255);
sf::RenderWindow window(sf::VideoMode(639, 553), "Connect Four", sf::Style::Close | sf::Style::Titlebar);
sf::Texture boardTexture;
sf::Sprite boardSprite;
sf::Font comicSans;
sf::Text rematch("Rematch?", comicSans);
void initializeGlobals() {
  uint64_t bit = 2199023255552ULL;
  for (int y = 0; y < 6; y++) {
    for (int x = 0; x < 7; x++) {
      int index = 7 * y + x;
      bits[index] = bit;
      bit >>= 1ULL;
    }
  }
  uint64_t column = 2216338399296ULL;
  for (int x = 0; x < 7; x++) {
    columns[x] = column;
    column >>= 1ULL;
  }
  int combinationIndex = 0;
  for (int y = 0; y < 6; y++) {
    for (int x = 0; x < 7; x++) {
      int index = 7 * y + x;
      if (x <= 3) {
        uint64_t combination = 0ULL;
        combination |= bits[index];
        combination |= bits[index + 1];
        combination |= bits[index + 2];
        combination |= bits[index + 3];
        combinations[combinationIndex] = combination;
        combinationIndex++;
      } 
      if (y <= 2) {
        uint64_t combination = 0ULL;
        combination |= bits[index];
        combination |= bits[index + 7];
        combination |= bits[index + 14];
        combination |= bits[index + 21];
        combinations[combinationIndex] = combination;
        combinationIndex++;
      } 
      if (x <= 3 && y <= 2) {
        uint64_t combination = 0ULL;
        combination |= bits[index];
        combination |= bits[index + 8];
        combination |= bits[index + 16];
        combination |= bits[index + 24];
        combinations[combinationIndex] = combination;
        combinationIndex++;
      }  
      if (x <= 3 && y >= 3) {
        uint64_t combination = 0ULL;
        combination |= bits[index];
        combination |= bits[index - 6];
        combination |= bits[index - 12];
        combination |= bits[index - 18];
        combinations[combinationIndex] = combination;
        combinationIndex++;
      }
    }
  }
}
void renderBoard() {
  for (int y = 0; y < 6; y++) {
    for (int x = 0; x < 7; x++) {
      int index = 7 * y + x;
      uint64_t bit = bits[index];
      float xPosition = 29 + x * 87;
      float yPosition = 29 + y * 87;
      if ((redBoard & bit) == bit) {
        sf::CircleShape redCell;
        redCell.setRadius(29);
        redCell.setFillColor(sf::Color::Red);
        redCell.setPosition(xPosition, yPosition);
        window.draw(redCell);
      } else if ((yellowBoard & bit) == bit) {
        sf::CircleShape yellowCell;
        yellowCell.setRadius(29);
        yellowCell.setFillColor(sf::Color::Yellow);
        yellowCell.setPosition(xPosition, yPosition);
        window.draw(yellowCell);
      }
    }
  }
}
bool makeMoveAndReturnIfTerminal(int move, int side) {
  int pieceCenterX = 29 + move * 87;
  int pieceCenterY = -29;
  int targetSquare = moveTargetSquare(move, bothBoards);
  int targetRow = (targetSquare - move) / 7;
  int pieceTargetY = 29 + targetRow * 87;
  sf::CircleShape gameCell;
  gameCell.setRadius(29);
  if (side == 0) {
    gameCell.setFillColor(sf::Color::Red);
  } else {
    gameCell.setFillColor(sf::Color::Yellow);
  }
  while (pieceCenterY < pieceTargetY) {
    gameCell.setPosition(pieceCenterX, pieceCenterY);
    window.clear(gray);
    window.draw(gameCell);
    renderBoard();
    window.draw(boardSprite);
    window.display();
    pieceCenterY++;
    usleep(1000000 / 553);
  }
  uint64_t bit = bits[targetSquare];
  if (side == 0) {
    redBoard |= bit;
  } else {
    yellowBoard |= bit;
  }
  bothBoards |= bit;
  bool isTerminal = false;
  int terminalState;
  if (bothBoards == 4398046511103ULL) {
    terminalState = 0;
    isTerminal = true;
  } else if (side == 0) {
    if (isWinning(redBoard)) {
      if (humanPlayer == 0) {
        terminalState = 1;
      } else {
        terminalState = 2;
      }
      isTerminal = true;
    }
  } else if (side == 1) {
    if (isWinning(yellowBoard)) {
      if (humanPlayer == 1) {
        terminalState = 1;
      } else {
        terminalState = 2;
      }
      isTerminal = true;
    }
  }
  window.clear(gray);
  renderBoard();
  window.draw(boardSprite);
  if (isTerminal) {
    sf::Text verdict;
    if (terminalState == 0) {
      verdict.setString("Draw!");
    } else if (terminalState == 1) {
      verdict.setString("You win!");
    } else if (terminalState == 2) {
      verdict.setString("Computer wins!");
    }
    verdict.setFont(comicSans);
    verdict.setCharacterSize(40);
    verdict.setFillColor(cyan);
    verdict.setOutlineColor(sf::Color::Black);
    verdict.setOutlineThickness(2.0);
    sf::FloatRect verdictRectangle = verdict.getLocalBounds();
    verdict.setOrigin(verdictRectangle.width / 2.0f, 
                    verdictRectangle.height / 2.0f);
    verdict.setPosition(sf::Vector2f(319.5f, 232.0f));
    window.draw(verdict);
    window.draw(rematch);
  }
  window.display();
  return isTerminal;
}
int main() {
  initializeGlobals();
  boardTexture.loadFromFile("connectFourBoard.png");
  boardSprite.setTexture(boardTexture);  
  comicSans.loadFromFile("COMIC.TTF");

  sf::Text heading("C++ Connect-4", comicSans);
  heading.setCharacterSize(40);
  heading.setFillColor(cyan);
  heading.setOutlineColor(sf::Color::Black);
  heading.setOutlineThickness(2.0);
  sf::FloatRect headingRectangle = heading.getLocalBounds();
  heading.setOrigin(headingRectangle.width / 2.0f, 
                    headingRectangle.height / 2.0f);
  heading.setPosition(sf::Vector2f(319.5f, 232.0f));

  sf::Text prompt("Choose a Color!", comicSans);
  prompt.setCharacterSize(26);
  prompt.setFillColor(cyan);
  prompt.setOutlineColor(sf::Color::Black);
  prompt.setOutlineThickness(2.0);
  sf::FloatRect promptRectangle = prompt.getLocalBounds();
  prompt.setOrigin(promptRectangle.width / 2.0f, 
                   promptRectangle.height / 2.0f);
  prompt.setPosition(sf::Vector2f(319.5f, 283.0f));

  sf::Text red("Red", comicSans);
  red.setCharacterSize(26);
  red.setFillColor(sf::Color::Red);
  red.setOutlineColor(sf::Color::Black);
  red.setOutlineThickness(2.0);
  sf::FloatRect redRectangle = red.getLocalBounds();
  red.setOrigin(redRectangle.width / 2.0f, 
                redRectangle.height / 2.0f);
  red.setPosition(sf::Vector2f(319.5f - redRectangle.width / 2.0f - 5.5f, 320.0f));
  
  float redLeft = 319.5f - redRectangle.width - 5.5f;
  float redRight = redLeft + redRectangle.width;
  float redTop = 320.0f - redRectangle.height / 2.0f;
  float redBottom = redTop + redRectangle.height;

  sf::Text yellow("Yellow", comicSans);
  yellow.setCharacterSize(26);
  yellow.setFillColor(sf::Color::Yellow);
  yellow.setOutlineColor(sf::Color::Black);
  yellow.setOutlineThickness(2.0);
  sf::FloatRect yellowRectangle = yellow.getLocalBounds();
  yellow.setOrigin(yellowRectangle.width / 2.0f, 
                   yellowRectangle.height / 2.0f);
  yellow.setPosition(sf::Vector2f(319.5f + yellowRectangle.width / 2.0f + 5.5f, 320.0f));

  float yellowLeft = 325.0f;
  float yellowRight = yellowLeft + yellowRectangle.width;
  float yellowTop = 320.0f - yellowRectangle.height / 2.0f;
  float yellowBottom = yellowTop + yellowRectangle.height;

  rematch.setCharacterSize(26);
  rematch.setFillColor(cyan);
  rematch.setOutlineColor(sf::Color::Black);
  rematch.setOutlineThickness(2.0);
  sf::FloatRect rematchRectangle = rematch.getLocalBounds();
  rematch.setOrigin(rematchRectangle.width / 2.0f, 
                    rematchRectangle.height / 2.0f);
  rematch.setPosition(sf::Vector2f(319.5f, 283.0f));
  float rematchLeft = 319.5f - rematchRectangle.width / 2.0f;
  float rematchRight = rematchLeft + rematchRectangle.width;
  float rematchTop = 283.0f - rematchRectangle.height / 2.0f;
  float rematchBottom = rematchTop + rematchRectangle.height;

  ComputerPlayer computerPlayer;

  window.clear(gray);
  window.draw(boardSprite);
  window.draw(heading);
  window.draw(prompt);
  window.draw(red);
  window.draw(yellow);
  window.display();

  while (window.isOpen()) {
    sf::Vector2i mousePosition = sf::Mouse::getPosition(window);
    int move = (int)floor(7.0 * (double)mousePosition.x / 639.0);
    sf::Event sfmlEvent;
    while (window.pollEvent(sfmlEvent)) {
      if (sfmlEvent.type == sf::Event::Closed) {
        window.close();
      } else if (sfmlEvent.type == sf::Event::MouseButtonReleased) {
        if (gameState == 0) {
          if (
            mousePosition.x > (int)redLeft && 
            mousePosition.x < (int)redRight && 
            mousePosition.y > (int)redTop && 
            mousePosition.y < (int)redBottom
          ) {
            gameState = 1;
            humanPlayer = 0;
            aiPlayer = 1;
            window.clear(gray);
            window.draw(boardSprite);
            window.display();
          } else if (
            mousePosition.x > (int)yellowLeft && 
            mousePosition.x < (int)yellowRight && 
            mousePosition.y > (int)yellowTop && 
            mousePosition.y < (int)yellowBottom
          ) {
            gameState = 1;
            humanPlayer = 1;
            aiPlayer = 0;
            window.clear(gray);
            window.draw(boardSprite);
            window.display();
            int computerMove = computerPlayer.bestMove(redBoard, yellowBoard, bothBoards, aiPlayer);
            bool isComputerMoveTerminal = makeMoveAndReturnIfTerminal(computerMove, aiPlayer);
            if (isComputerMoveTerminal) {
              gameState = 2;
            }
          }
        } else if (gameState == 1) {
          if (verifyMove(move, bothBoards)) {
            bool isHumanMoveTerminal = makeMoveAndReturnIfTerminal(move, humanPlayer);
            if (isHumanMoveTerminal) {
              gameState = 2;
            } else {
              int computerMove = computerPlayer.bestMove(redBoard, yellowBoard, bothBoards, aiPlayer);
              bool isComputerMoveTerminal = makeMoveAndReturnIfTerminal(computerMove, aiPlayer);
              if (isComputerMoveTerminal) {
                gameState = 2;
              }
            }
          }
        } else if (gameState == 2) {
          if (
            mousePosition.x > (int)rematchLeft && 
            mousePosition.x < (int)rematchRight && 
            mousePosition.y > (int)rematchTop && 
            mousePosition.y < (int)rematchBottom
          ) {
            redBoard = 0ULL;
            yellowBoard = 0ULL;
            bothBoards = 0ULL;
            window.clear(gray);
            window.draw(boardSprite);
            window.draw(heading);
            window.draw(prompt);
            window.draw(red);
            window.draw(yellow);
            window.display();
            gameState = 0;
          }
        }
      }
    }
  }
  return 0;
}

node.h (the node data structure for the Monte Carlo Tree Search algorithm)

#pragma once
#include <cstdint>
#include <vector>
struct node {
  uint64_t redBoard;
  uint64_t yellowBoard;
  uint64_t bothBoards;
  double wins;
  double simulations; 
  int sideToPlay;
  // 0 if normal, 1 if draw, 2 if red won, 3 if yellow won.
  int terminalStatus; 
  std::vector<int> childNodesIndices;
  int parentNodeIndex;
};

I feel that especially my game loop is unclear, as thanks to my javascript experience I am used to having used event listeners. But in c++, it seems I have put all of the logic into one loop. So I would like some advice on how gameloops are managed in C++. Also the computerPlayer seems to run quite slow (similar to the javascript version of this game I wrote a year ago). So I would like some optimisation tips in C++, as I am unfamiliar with them as of yet.

\$\endgroup\$
3
  • \$\begingroup\$ Micro-review - you've written uint64_t instead of std::uint64_t throughout. And do you need exactly 64 bits, or would std::uint_fast64_t be a better choice? \$\endgroup\$ Commented Jun 29, 2022 at 6:54
  • \$\begingroup\$ @TobySpeight I actually use only 42 bits, the rest are zeroes, so I will try this! \$\endgroup\$ Commented Jun 29, 2022 at 6:58
  • \$\begingroup\$ Also, it's strange that you include <math.h> rather than <cmath>. I recommend only using the C-compatibility headers in (rare) code that needs to be valid C as well as C++. \$\endgroup\$ Commented Jul 5, 2022 at 6:52

1 Answer 1

2
\$\begingroup\$

First we have to get this out of the way:

I decided to try learning a low level language for once, so I did C++.

C++ is definitely a high-level language. If you want a low-level language, you should write your code in assembly. The main difference between the languages you mentioned is that C++ is a compiled language, whereas Python and JavaScript are interpreted languages.

Add more structure to your code

The overall impression I get from your code is that functions are too long and that you have way too many global variables. You have made a class ComputerPlayer and an associated struct node for its tree search algorithm, but the rest of the code is mainly in free functions. I strongly recommend you add more structure by grouping relevant data into classes, and try to split more functionality into their own (member) functions.

At the very least, I would expect a class Board that represents a Connect 4 board, with member functions that allow you to query and change the state of the board. A class Game for the game itself, that not only contains the board but also keeps track of who is playing, and allows you to query who is winning, and so on. A class UI could be added that has everything related to the user interface.

Names like Board, Game, node and UI are very generic, so I would put everything except the main() function inside a namespace Connect4.

Avoid magic numbers

I see a lot of magic numbers in your code, like 4363686772736ULL. It's hard for someone reading your code to understand what that number means. Create constants for these numbers, so they have a name that explains what they mean. Also, since these are bit masks, it helps to write them in hexadecimal instead:

static constexpr uint64_t topRowMask = 0x3f800000000;
static constexpr uint64_t topRowOffset = 35;
...
uint64_t topRow = (bothBoards & topRowMask) >> topRowOffset;

Similarly, what does gameState == 1 mean? In this case, create an enum class to give all the states a name. Instead of a sequence of if-else statements, use switch (gameState). In combination with an enum, the compiler will then be able to warn you if you forgot to handle one of the possible states.

Consider using std::bitset

If you want to manipulate sets of bits, the standard library comes with std::bitset. It has several nice features, such as being able to exactly specify how many bits there are in a set, and allowing you to construct a bit set from a string of zeroes and ones, so for example you can write:

static const std::bitset<42> topRowMask("1111111"
                                        "0000000"
                                        "0000000"
                                        "0000000"
                                        "0000000"
                                        "0000000");

Don't bother with bits[] and rows[]

You are pre-computing these arrays, but in fact the combination of a multiplication, add and shift operation is so cheap on today's hardware that it is probably not worth making them. Instead, I would create functions:

static constexpr uint64_t bit(int x, int y) {
    static constexpr uint64_t firstBit = 0x20000000000;
    return firstBit >> (7 * y + x);
}

static constexpr uint64_t column(int x) {
    static constexpr uint64_t firstColumn = 0x20408102040;
    return firstColumn >> x;
}

Similarly, is it really necessary to have a separate bothBoards? If it's always just meant to be the combination of redBoard and yellowBoard, then just writing this when you need it:

uint64_t bothBoards = redBoard | yellowBoard;

Is faster than passing it around as a parameter.

Avoid using obsolete and platform-specific functions

You call usleep() in your code, but that is an obsolete POSIX function, and doesn't work on non-POSIX operating systems, like Windows. Either use the proper standard C++ function, like std::this_thread::sleep_for(), or use SFML's platform-independent functions, like sf::sleep().

\$\endgroup\$

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.