Skip to main content
edited tags
Link
200_success
  • 145.6k
  • 22
  • 191
  • 481
deleted 1 character in body
Source Link

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 wasas of yet.

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 was of yet.

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.

Source Link

C++ Connect 4 in SFML

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 was of yet.