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.
uint64_tinstead ofstd::uint64_tthroughout. And do you need exactly 64 bits, or wouldstd::uint_fast64_tbe a better choice? \$\endgroup\$<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\$