diff --git a/ConnectFourGame.hpp b/ConnectFourGame.hpp new file mode 100755 index 0000000..157ea7c --- /dev/null +++ b/ConnectFourGame.hpp @@ -0,0 +1,619 @@ +#ifndef CONNECTFOURGAME +#define CONNECTFOURGAME + +#include +#include + +#include "GameBoard.hpp" +#include "GameSlot.hpp" + +#include +#include +#include +#include +#include + +namespace controller { + + class ConnectFourGame { + + private: + + //Constants + const static bool DEFAULT_FIRST_PLAYER_IS_USER = true; + const static bool DEFAULT_MODE_IS_PARALLEL = true; + const static int DEFAULT_DIFFICULTY_LEVEL = 2; + const static int HEURISTIC_SCORE_FOR_ONE_IN_ROW = 1; + const static int HEURISTIC_SCORE_FOR_TWO_IN_ROW = 3; + const static int HEURISTIC_SCORE_FOR_THREE_IN_ROW = 9; + const static int HEURISTIC_SCORE_FOR_FOUR_IN_ROW = INT_MAX; + const static int COLUMN_OR_ROW_DIFFERENCE_FOR_FOUR_IN_A_ROW = 3; + const static int HEURISTIC_SCORE_DIRECTIONS = 4; + + int gameDifficultyLevel; + bool firstPlayerIsUser, gameModeIsParallel, gameIsOver, userWonTheGame; + model::GameBoard gameBoard; + + //Get heuristic scores for the four possible directions + void getHueristicScores(int& horizontalHueristicScore, int& verticalHueristicScore, int& positiveSlopeHueristicScore, int& negativeSlopeHueristicScore, int columnPlayed, const model::GameBoard gameBoard, bool isUserCoin) { + + if (this->gameModeIsParallel) { + + tbb::parallel_for( + tbb::blocked_range(0, gameBoard.getNumberOfColumns()), + [=, &horizontalHueristicScore, &verticalHueristicScore, &positiveSlopeHueristicScore, &negativeSlopeHueristicScore](tbb::blocked_range range) { + + for (int counter = 0; counter < HEURISTIC_SCORE_DIRECTIONS; ++counter) { + + switch (counter) { + + case 0: + horizontalHueristicScore = getHorizontalHueristicScore(columnPlayed, gameBoard, isUserCoin); + break; + + case 1: + verticalHueristicScore = getVerticalHueristicScore(columnPlayed, gameBoard, isUserCoin); + break; + + case 2: + positiveSlopeHueristicScore = getPositiveSlopeHueristicScore(columnPlayed, gameBoard, isUserCoin); + break; + + case 3: + negativeSlopeHueristicScore = getNegativeSlopeHueristicScore(columnPlayed, gameBoard, isUserCoin); + break; + + } + + } + } + ); + + } + else { + horizontalHueristicScore = getHorizontalHueristicScore(columnPlayed, gameBoard, isUserCoin); + verticalHueristicScore = getVerticalHueristicScore(columnPlayed, gameBoard, isUserCoin); + positiveSlopeHueristicScore = getPositiveSlopeHueristicScore(columnPlayed, gameBoard, isUserCoin); + negativeSlopeHueristicScore = getNegativeSlopeHueristicScore(columnPlayed, gameBoard, isUserCoin); + } + } + + //Check if the last play was a winning play + bool wasWinningPlay(int columnPlayed, bool isUserCoin) { + + //Compute hueristic scores for horizontal, vertical and diagonal four coins in a row resulting from + //coin being dropped in column + int horizontalHueristicScore, verticalHueristicScore, positiveSlopeHueristicScore, negativeSlopeHueristicScore; + getHueristicScores(horizontalHueristicScore, verticalHueristicScore, positiveSlopeHueristicScore, negativeSlopeHueristicScore, columnPlayed, this->gameBoard, isUserCoin); + //If it was a winning move, then return with indicator saying so + if (horizontalHueristicScore == INT_MAX || + verticalHueristicScore == INT_MAX || + positiveSlopeHueristicScore == INT_MAX || + negativeSlopeHueristicScore == INT_MAX) { + + return true; + } + else { + return false; + } + } + + //End the game + void endTheGame(bool userWon) { + this->gameIsOver = true; + this->userWonTheGame = userWon; + } + + int bestHeuristicScoreForOpponentMoveParallel(int depth, bool isUserCoin, const model::GameBoard gameBoard) { + + //Do a map to find the move with the highest score + std::vector moveScores(gameBoard.getNumberOfColumns()); + + tbb::parallel_for( + tbb::blocked_range(0, gameBoard.getNumberOfColumns()), + [=, &moveScores](tbb::blocked_range range) { + + for (int columnCounter = range.begin(); columnCounter != range.end(); ++columnCounter) { + if (gameBoard.isValidPlay(columnCounter)) { + model::GameBoard whatIfGameBoard{ gameBoard.getGameBoardVector(), this->gameBoard.getNumberOfRows(), this->gameBoard.getNumberOfColumns() }; + whatIfGameBoard.forceDropCoin(columnCounter, isUserCoin); + moveScores.at(columnCounter) = getMoveHueristicScore(depth, columnCounter, isUserCoin, whatIfGameBoard); + } + else { + moveScores.at(columnCounter) = -1 * INT_MAX; + } + } + } + ); + + int bestScore = -1 * INT_MAX; + for (int moveCounter = 0; moveCounter < gameBoard.getNumberOfColumns(); ++moveCounter) { + if (moveScores.at(moveCounter) > bestScore) { + bestScore = moveScores.at(moveCounter); + } + } + + return bestScore; + + } + + int bestHeuristicScoreForOpponentMoveSeries(int depth, bool isUserCoin, const model::GameBoard gameBoard) { + + int currentScore, bestScore = -1 * INT_MAX; + for (int columnCounter = 0; columnCounter < gameBoard.getNumberOfColumns(); ++columnCounter) { + + if (gameBoard.isValidPlay(columnCounter)) { + //Make a copy of the current gameboard to simulate a dropped coin + model::GameBoard whatIfGameBoard{ gameBoard.getGameBoardVector(), this->gameBoard.getNumberOfRows(), this->gameBoard.getNumberOfColumns() }; + whatIfGameBoard.forceDropCoin(columnCounter, isUserCoin); + + currentScore = getMoveHueristicScore(depth, columnCounter, isUserCoin, whatIfGameBoard); + if (currentScore > bestScore) { + bestScore = currentScore; + } + + } + } + + return bestScore; + } + + //Compute best heuristic score for opponent move + int bestHeuristicScoreForOpponentMove(int depth, bool isUserCoin, const model::GameBoard gameBoard) { + + if (this->gameModeIsParallel) { + return bestHeuristicScoreForOpponentMoveParallel(depth, isUserCoin, gameBoard); + } + else { + return bestHeuristicScoreForOpponentMoveSeries(depth, isUserCoin, gameBoard); + } + + } + + //Compute and return the hueristic score for the move + int getMoveHueristicScore(int depth, int columnPlayed, bool isUserCoin, model::GameBoard gameBoard) { + + //If maximum depth has been reached, then return + if (depth == 0) { + return 0; + } + + //Compute hueristic scores for horizontal, vertical and diagonal four coins in a row resulting from + //coin being dropped in column + int horizontalHueristicScore, verticalHueristicScore, positiveSlopeHueristicScore, negativeSlopeHueristicScore; + getHueristicScores(horizontalHueristicScore, verticalHueristicScore, positiveSlopeHueristicScore, negativeSlopeHueristicScore, columnPlayed, gameBoard, isUserCoin); + + //If it was a winning move, then return with indicator saying so + if (horizontalHueristicScore == INT_MAX || + verticalHueristicScore == INT_MAX || + positiveSlopeHueristicScore == INT_MAX || + negativeSlopeHueristicScore == INT_MAX) { + + return INT_MAX; + } + + int heuristicScoreForCurrentMove = horizontalHueristicScore + + verticalHueristicScore + + positiveSlopeHueristicScore + + negativeSlopeHueristicScore; + + int bestOpponentMoveScore = bestHeuristicScoreForOpponentMove(depth - 1, isUserCoin ? false : true, gameBoard); + + if (bestOpponentMoveScore == INT_MAX || bestOpponentMoveScore == -INT_MAX) { + return -1 * bestOpponentMoveScore; + } + else { + return heuristicScoreForCurrentMove - bestOpponentMoveScore; + } + + } + + //Check if the cells between from and to index are of the required type or empty. + //Also count the number of cells of the required type. + bool isEmptyOrRequiredType(int fromIndex, int toIndex, bool userCoinPlayed, int& hueristicScore, model::GameBoard gameBoard) { + + int coinCount = 0, nextRow, nextColumn, nextIndex; + bool firstTime = true; + + int fromRow = gameBoard.getRowNumber(fromIndex); + int fromColumn = gameBoard.getColumnNumber(fromIndex); + int toRow = gameBoard.getRowNumber(toIndex); + int toColumn = gameBoard.getColumnNumber(toIndex); + + for (int counter = 0; counter <= COLUMN_OR_ROW_DIFFERENCE_FOR_FOUR_IN_A_ROW; ++counter) { + + if (fromRow == toRow) {//horizontal sequence + nextRow = fromRow; + nextColumn = fromColumn + counter; + nextIndex = gameBoard.getBoardIndex(nextRow, nextColumn); + } + else if (fromColumn == toColumn) {//vertical sequence + nextRow = fromRow + counter; + nextColumn = fromColumn; + nextIndex = gameBoard.getBoardIndex(nextRow, nextColumn); + } + else if (fromRow > toRow && fromColumn < toColumn) {//diagonal going up + if (firstTime) { + firstTime = false; + nextIndex = fromIndex; + } + else { + nextIndex = gameBoard.getDiagonalCellToRightGoingUp(nextIndex); + } + + } + else if (fromRow < toRow && fromColumn < toColumn) {//diagonal going down + if (firstTime) { + firstTime = false; + nextIndex = fromIndex; + } + else { + nextIndex = gameBoard.getDiagonalCellToRightGoingDown(nextIndex); + } + + } + + if (gameBoard.getGameSlot(nextIndex).hasComputerCoin()) { + if (userCoinPlayed) { + return false; + } + else { + ++coinCount; + } + } + else if (gameBoard.getGameSlot(nextIndex).hasUserCoin()) { + if (userCoinPlayed) { + ++coinCount; + } + else { + return false; + } + } + + } + + if (coinCount == 1) { + hueristicScore = HEURISTIC_SCORE_FOR_ONE_IN_ROW; + } + else if (coinCount == 2) { + hueristicScore = HEURISTIC_SCORE_FOR_TWO_IN_ROW; + } + else if (coinCount == 3) { + hueristicScore = HEURISTIC_SCORE_FOR_THREE_IN_ROW; + } + else if (coinCount == 4) { + hueristicScore = HEURISTIC_SCORE_FOR_FOUR_IN_ROW; + } + else { + hueristicScore = 0; + } + + return true; + } + + //Heuristic score for dropping a coin in the column played for all potential four-in-a-row horizontal configurations. + int getHorizontalHueristicScore(int columnPlayed, model::GameBoard gameBoard, bool isUserCoin) { + + int slidingWindowStartPosition, hueristicScore, totalHueristicScore = 0, endColumn; + if (columnPlayed >= COLUMN_OR_ROW_DIFFERENCE_FOR_FOUR_IN_A_ROW) { + slidingWindowStartPosition = columnPlayed - COLUMN_OR_ROW_DIFFERENCE_FOR_FOUR_IN_A_ROW; + } + else { + slidingWindowStartPosition = 0; + } + + //Consider each four-in-a-row window starting from three before dropped column and ending at the dropped position + for (int startColumn = slidingWindowStartPosition; startColumn <= columnPlayed; ++startColumn) { + + if (startColumn + COLUMN_OR_ROW_DIFFERENCE_FOR_FOUR_IN_A_ROW > gameBoard.getNumberOfColumns() - 1) { + break; + } + else { + endColumn = startColumn + COLUMN_OR_ROW_DIFFERENCE_FOR_FOUR_IN_A_ROW; + } + + int rowContainingDroppedCoin = gameBoard.getRowNumber(gameBoard.getPlayedSlot(columnPlayed)); + if (isEmptyOrRequiredType(gameBoard.getBoardIndex(rowContainingDroppedCoin, startColumn), gameBoard.getBoardIndex(rowContainingDroppedCoin, endColumn), isUserCoin, hueristicScore, gameBoard)) { + if (hueristicScore == INT_MAX) { + return INT_MAX; + } + else { + totalHueristicScore += hueristicScore; + } + } + } + + return totalHueristicScore; + } + + //Heuristic score for dropping a coin in the column played for all potential four-in-a-row vertical configurations. + int getVerticalHueristicScore(int columnPlayed, model::GameBoard gameBoard, bool isUserCoin) { + + int slidingWindowStartPosition, hueristicScore, totalHueristicScore = 0, endRow; + int coinDroppedInRow = gameBoard.getRowNumber(gameBoard.getPlayedSlot(columnPlayed)); + + if (coinDroppedInRow >= COLUMN_OR_ROW_DIFFERENCE_FOR_FOUR_IN_A_ROW) { + slidingWindowStartPosition = coinDroppedInRow - COLUMN_OR_ROW_DIFFERENCE_FOR_FOUR_IN_A_ROW; + } + else { + slidingWindowStartPosition = 0; + } + + //Consider each four-in-a-row window in the vertical column + for (int startRow = slidingWindowStartPosition; startRow <= coinDroppedInRow; ++startRow) { + + if (startRow + COLUMN_OR_ROW_DIFFERENCE_FOR_FOUR_IN_A_ROW > gameBoard.getNumberOfRows() - 1) { + break; + } + else { + endRow = startRow + COLUMN_OR_ROW_DIFFERENCE_FOR_FOUR_IN_A_ROW; + } + + if (isEmptyOrRequiredType(gameBoard.getBoardIndex(startRow, columnPlayed), gameBoard.getBoardIndex(endRow, columnPlayed), isUserCoin, hueristicScore, gameBoard)) { + if (hueristicScore == INT_MAX) { + return INT_MAX; + } + else { + totalHueristicScore += hueristicScore; + } + } + } + + return totalHueristicScore; + } + + int getPositiveSlopeHueristicScore(int columnPlayed, model::GameBoard gameBoard, bool isUserCoin) { + + int slidingWindowStartRowPosition, slidingWindowStartColumnPosition, hueristicScore, totalHueristicScore = 0, endRow, endColumn; + int coinDroppedInRow = gameBoard.getRowNumber(gameBoard.getPlayedSlot(columnPlayed)); + + if (gameBoard.getNumberOfRows() - 1 - coinDroppedInRow >= COLUMN_OR_ROW_DIFFERENCE_FOR_FOUR_IN_A_ROW && columnPlayed >= COLUMN_OR_ROW_DIFFERENCE_FOR_FOUR_IN_A_ROW) { + //If you can go three down and left to start + slidingWindowStartRowPosition = coinDroppedInRow + COLUMN_OR_ROW_DIFFERENCE_FOR_FOUR_IN_A_ROW; + slidingWindowStartColumnPosition = columnPlayed - COLUMN_OR_ROW_DIFFERENCE_FOR_FOUR_IN_A_ROW; + } + else { + //You can only got the minimum of two values down and left + int rowDistanceFromBottom = gameBoard.getNumberOfRows() - 1 - coinDroppedInRow; + int columnDistanceFromLeft = columnPlayed; + + slidingWindowStartRowPosition = coinDroppedInRow + std::min(rowDistanceFromBottom, columnDistanceFromLeft); + slidingWindowStartColumnPosition = columnPlayed - std::min(rowDistanceFromBottom, columnDistanceFromLeft); + } + + //Consider each four-in-a-row window starting from three before dropped column and ending at the dropped position + for (int startRow = slidingWindowStartRowPosition, startColumn = slidingWindowStartColumnPosition; startColumn <= columnPlayed; --startRow, ++startColumn) { + + if (startColumn + COLUMN_OR_ROW_DIFFERENCE_FOR_FOUR_IN_A_ROW > gameBoard.getNumberOfColumns() - 1 || + startRow < COLUMN_OR_ROW_DIFFERENCE_FOR_FOUR_IN_A_ROW) { + break; + } + else { + endRow = startRow - COLUMN_OR_ROW_DIFFERENCE_FOR_FOUR_IN_A_ROW; + endColumn = startColumn + COLUMN_OR_ROW_DIFFERENCE_FOR_FOUR_IN_A_ROW; + } + + if (isEmptyOrRequiredType(gameBoard.getBoardIndex(startRow, startColumn), gameBoard.getBoardIndex(endRow, endColumn), isUserCoin, hueristicScore, gameBoard)) { + if (hueristicScore == INT_MAX) { + return INT_MAX; + } + else { + totalHueristicScore += hueristicScore; + } + } + } + + return totalHueristicScore; + } + + int getNegativeSlopeHueristicScore(int columnPlayed, model::GameBoard gameBoard, bool isUserCoin) { + + int slidingWindowStartRowPosition, slidingWindowStartColumnPosition, hueristicScore, totalHueristicScore = 0, endRow, endColumn; + int coinDroppedInRow = gameBoard.getRowNumber(gameBoard.getPlayedSlot(columnPlayed)); + + if (coinDroppedInRow >= COLUMN_OR_ROW_DIFFERENCE_FOR_FOUR_IN_A_ROW && columnPlayed >= COLUMN_OR_ROW_DIFFERENCE_FOR_FOUR_IN_A_ROW) { + slidingWindowStartRowPosition = coinDroppedInRow - COLUMN_OR_ROW_DIFFERENCE_FOR_FOUR_IN_A_ROW; + slidingWindowStartColumnPosition = columnPlayed - COLUMN_OR_ROW_DIFFERENCE_FOR_FOUR_IN_A_ROW; + } + else { + int rowDistanceFromTop = coinDroppedInRow; + int columnDistanceFromLeft = columnPlayed; + + slidingWindowStartRowPosition = coinDroppedInRow - std::min(rowDistanceFromTop, columnDistanceFromLeft); + slidingWindowStartColumnPosition = columnPlayed - std::min(rowDistanceFromTop, columnDistanceFromLeft); + } + + //Consider each four-in-a-row window starting from three before dropped column and ending at the dropped position + for (int startRow = slidingWindowStartRowPosition, startColumn = slidingWindowStartColumnPosition; startColumn <= columnPlayed; ++startRow, ++startColumn) { + + if (startColumn + COLUMN_OR_ROW_DIFFERENCE_FOR_FOUR_IN_A_ROW > gameBoard.getNumberOfColumns() - 1 || + startRow + COLUMN_OR_ROW_DIFFERENCE_FOR_FOUR_IN_A_ROW > gameBoard.getNumberOfRows() - 1) { + break; + } + else { + endRow = startRow + COLUMN_OR_ROW_DIFFERENCE_FOR_FOUR_IN_A_ROW; + endColumn = startColumn + COLUMN_OR_ROW_DIFFERENCE_FOR_FOUR_IN_A_ROW; + } + + if (isEmptyOrRequiredType(gameBoard.getBoardIndex(startRow, startColumn), gameBoard.getBoardIndex(endRow, endColumn), isUserCoin, hueristicScore, gameBoard)) { + if (hueristicScore == INT_MAX) { + return INT_MAX; + } + else { + totalHueristicScore += hueristicScore; + } + } + } + + return totalHueristicScore; + } + + //Find best move by considering all columns in parallel using the Map pattern + int evaluatePotentialMovesInParallel() { + + int depth = this->gameDifficultyLevel; + std::vector moveScores(this->gameBoard.getNumberOfColumns()); + + tbb::parallel_for( + tbb::blocked_range(0, this->gameBoard.getNumberOfColumns()), + [=, &moveScores](tbb::blocked_range range) { + + for (int columnCounter = range.begin(); columnCounter != range.end(); ++columnCounter) { + if (this->gameBoard.isValidPlay(columnCounter)) { + + //Make a copy of the current gameboard to simulate a dropped coin + model::GameBoard whatIfGameBoard{ this->gameBoard.getGameBoardVector(), this->gameBoard.getNumberOfRows(), this->gameBoard.getNumberOfColumns() }; + whatIfGameBoard.forceDropCoin(columnCounter, false); + moveScores.at(columnCounter) = getMoveHueristicScore(depth, columnCounter, false, whatIfGameBoard); + + } + } + } + ); + + int bestMove = 0, bestScore = -1 * INT_MAX; + for (int moveCounter = 0; moveCounter < this->gameBoard.getNumberOfColumns(); ++moveCounter) { + if (moveScores.at(moveCounter) > bestScore) { + bestScore = moveScores.at(moveCounter); + bestMove = moveCounter; + } + } + + return bestMove; + + } + + //Find best move by considering all columns in one after the other + int evaluatePotentialMovesInSeries() { + + int depth = this->gameDifficultyLevel, currentScore, bestScore = -1 * INT_MAX, bestMove = 0; + for (int columnCounter = 0; columnCounter < this->gameBoard.getNumberOfColumns(); ++columnCounter) { + + if (this->gameBoard.isValidPlay(columnCounter)) { + //Make a copy of the current gameboard to simulate a dropped coin + model::GameBoard whatIfGameBoard(this->gameBoard.getGameBoardVector(), this->gameBoard.getNumberOfRows(), this->gameBoard.getNumberOfColumns()); + whatIfGameBoard.forceDropCoin(columnCounter, false); + currentScore = getMoveHueristicScore(depth, columnCounter, false, whatIfGameBoard); + + if (currentScore > bestScore) { + bestScore = currentScore; + bestMove = columnCounter; + } + } + } + return bestMove; + } + + //Consider all possible moves and play the one with the best hueristic score that maximizes the chance of winning + int counterUserMove() { + if (this->gameModeIsParallel) { + return evaluatePotentialMovesInParallel(); + } + else { + return evaluatePotentialMovesInSeries(); + } + } + + //Drop a coin into one of the columns + void dropCoin(int dropInColumn, bool isUserCoin) { + + //First check if the play is a valid one + if (this->gameBoard.isValidPlay(dropInColumn)) { + + //Place a user coin in the top available position + GameSlot& gameSlot = this->gameBoard.getGameSlot(this->gameBoard.getAvailableSlot(dropInColumn)); + gameSlot.putCoin(isUserCoin); + + if (wasWinningPlay(dropInColumn, true) || isGameBoardFull()) { + endTheGame(true); + } + else { + //Make a move to best counter the user move + int columnToPlay = counterUserMove(); + //std::cout << "User move countered by dropping in column " << columnToPlay << std::endl; + int rowToPlay = this->gameBoard.getRowNumber(this->gameBoard.getAvailableSlot(columnToPlay)); + GameSlot& gameSlot = this->gameBoard.getGameSlot(this->gameBoard.getBoardIndex(rowToPlay, columnToPlay)); + gameSlot.putCoin(false); + if (wasWinningPlay(columnToPlay, false) || isGameBoardFull()) { + endTheGame(false); + } + } + } + } + + + public: + + //Default constructor will set game parameters using default values + ConnectFourGame() { + + gameBoard = model::GameBoard(); + this->firstPlayerIsUser = DEFAULT_FIRST_PLAYER_IS_USER; + this->gameDifficultyLevel = DEFAULT_DIFFICULTY_LEVEL; + this->gameModeIsParallel = DEFAULT_MODE_IS_PARALLEL; + this->gameIsOver = false; + + } + + ConnectFourGame(int numberOfRows, int numberOfColumns) { + + gameBoard = model::GameBoard(numberOfRows, numberOfColumns); + this->firstPlayerIsUser = DEFAULT_FIRST_PLAYER_IS_USER; + this->gameDifficultyLevel = DEFAULT_DIFFICULTY_LEVEL; + this->gameModeIsParallel = DEFAULT_MODE_IS_PARALLEL; + this->gameIsOver = false; + + } + + //First player will be determined by user selection + void setWhoPlaysFirst(bool firstPlayerIsUser) { + this->firstPlayerIsUser = firstPlayerIsUser; + } + + //Difficulty level will be set by user + void setgameDifficultyLevel(int gameDifficultyLevel) { + this->gameDifficultyLevel = gameDifficultyLevel; + } + + bool isGameOver() { + return this->gameIsOver; + } + + bool didUserWinTheGame() { + return this->userWonTheGame; + } + + //Set the serial/parallel computation mode depending on user preference + void setComputationModeToParallel(bool parallelMode) { + this->gameModeIsParallel = parallelMode; + } + + const model::GameBoard& getGameBoard() const { + return this->gameBoard; + } + + //User method to drop a coin into one of the columns + void dropCoin(int dropInColumn) { + + if (!this->gameIsOver) { + dropCoin(dropInColumn, true); + } + } + + //Method to check if the board has been filled and play cannot continue + bool isGameBoardFull() { + + //Check to see that at least one slot in the top row is empty + for (int counter = 0; counter < this->gameBoard.getNumberOfColumns(); ++counter) { + if (this->gameBoard.getGameSlot(counter).isEmpty()) { + return false; + } + } + + return true; + } + }; + +} + +#endif \ No newline at end of file diff --git a/GameBoard.hpp b/GameBoard.hpp new file mode 100755 index 0000000..74152e8 --- /dev/null +++ b/GameBoard.hpp @@ -0,0 +1,248 @@ +#ifndef GAMEBOARD +#define GAMEBOARD + +#include +#include +#include +#include + +#include "GameSlot.hpp" + +namespace model { + + class GameBoard { + + private: + + //Members + std::vector gameBoard; + int numberOfRows, numberOfColumns; + bool forceDropAllowed; + + //Constants for default values + const static int DEFAULT_NUMBER_OF_ROWS = 6; + const static int DEFAULT_NUMBER_OF_COLUMNS = 7; + + public: + + //Default constructor + GameBoard() { + + this->gameBoard = std::vector(DEFAULT_NUMBER_OF_ROWS * DEFAULT_NUMBER_OF_COLUMNS, GameSlot::GameSlot()); + this->numberOfRows = DEFAULT_NUMBER_OF_ROWS; + this->numberOfColumns = DEFAULT_NUMBER_OF_COLUMNS; + this->forceDropAllowed = false; + + } + + //Constructor with required number of rows and columns + GameBoard(int numberOfRows, int numberOfColumns) { + + this->gameBoard = std::vector(numberOfRows * numberOfColumns, GameSlot::GameSlot()); + this->numberOfRows = numberOfRows; + this->numberOfColumns = numberOfColumns; + this->forceDropAllowed = false; + + } + + //Constructor with a gameboard as parameter + GameBoard(std::vector gameBoard, int numberOfRows, int numberOfColumns) { + this->gameBoard = gameBoard; + this->numberOfRows = numberOfRows; + this->numberOfColumns = numberOfColumns; + this->forceDropAllowed = true; + } + + int getNumberOfRows() { + return this->numberOfRows; + } + + const int getNumberOfColumns() const { + return this->numberOfColumns; + } + + //Check if this is a valid play given the game board dimensions and coins already played + bool isValidPlay(int dropInColumn) const { + + //First check if the column number is valid as per the dimensions of the game board + if (isValidColumn(dropInColumn)) { + + //Next check if there is at least one empty slot in the column. i.e. check if the top slot is empty + if (isEmptyAt(dropInColumn)) { + return true; + } + else { + return false; + } + + } + else { + return false; + } + + } + + //Return the index corresponding to the top available position + int getAvailableSlot(int columnNumber) { + + //Make sure that this is a valid play + assert(isValidPlay(columnNumber)); + + //Find the top and bottom rows in the columns + int bottomRowInColumn = columnNumber + getNumberOfColumns() * (getNumberOfRows() - 1); + int topRowInColumn = columnNumber; + + //Start with bottom row and go one cell above at a time till an empty one is found + for (int slotCounter = bottomRowInColumn; slotCounter >= topRowInColumn; slotCounter -= getNumberOfColumns()) { + if (isEmptyAt(slotCounter)) { + return slotCounter; + } + } + + //Exit the program as if it reaches here as this should never happen + assert(false); + + return 0; + } + + + //Return the index corresponding to the top position with a coin + int getPlayedSlot(int columnNumber) { + + //Find the top and bottom rows in the columns + int bottomRowInColumn = columnNumber + getNumberOfColumns() * (getNumberOfRows() - 1); + int topRowInColumn = columnNumber; + + //Start with bottom row and go one cell above at a time till an empty one is found + for (int slotCounter = topRowInColumn; slotCounter <= bottomRowInColumn; slotCounter += getNumberOfColumns()) { + if (!isEmptyAt(slotCounter)) { + return slotCounter; + } + } + + //Exit the program as if it reaches here as this should never happen + std::cerr << "Error getting played slot in column " << columnNumber << std::endl; + assert(false); + + return 0; + } + + + //Check if the column is valid according to the board dimensions + bool isValidColumn(int columnNumber) const { + + if (columnNumber >= 0 && columnNumber < this->numberOfColumns) { + return true; + } + else { + return false; + } + } + + //Return the row number starting with zero + int getRowNumber(int boardIndex) { + + return boardIndex / this->numberOfColumns; + + } + + //Return the column number starting with zero + int getColumnNumber(int boardIndex) { + + return boardIndex % this->numberOfColumns; + + } + + //Return index corresponding to row and column numbers passed in as parameters + int getBoardIndex(int rowNumber, int columnNumber) { + if (rowNumber <= this->numberOfRows - 1 && columnNumber <= this->numberOfColumns - 1) { + return rowNumber * this->numberOfColumns + columnNumber; + } + else { + std::stringstream errorMessage; + errorMessage << "Row " << rowNumber << " and column " << columnNumber << " is not a valid combination."; + throw std::logic_error(errorMessage.str()); + } + } + + int getDiagonalCellToRightGoingUp(int boardIndex) { + + int rowNumber = getRowNumber(boardIndex); + int columnNumber = getColumnNumber(boardIndex); + if (rowNumber != 0 && columnNumber != this->numberOfColumns - 1) { + return getBoardIndex(rowNumber - 1, columnNumber + 1); + } + else { + std::stringstream errorMessage; + errorMessage << "Cell at index " << boardIndex << " is on row " << rowNumber << " and column " << columnNumber << ". Cannot get a diagonal cell going right and up."; + throw std::logic_error(errorMessage.str()); + } + + } + + int getDiagonalCellToRightGoingDown(int boardIndex) { + + int rowNumber = getRowNumber(boardIndex); + int columnNumber = getColumnNumber(boardIndex); + if (rowNumber != this->numberOfRows - 1 && columnNumber != this->numberOfColumns - 1) { + return getBoardIndex(rowNumber + 1, columnNumber + 1); + } + else { + std::stringstream errorMessage; + errorMessage << "Cell at index " << boardIndex << " is on row " << rowNumber << " and column " << columnNumber << ". Cannot get a diagonal cell going right and down."; + throw std::logic_error(errorMessage.str()); + } + + } + + bool isEmptyAt(int boardIndex) const { + + if (this->gameBoard.at(boardIndex).isEmpty()) { + return true; + } + else { + return false; + } + + } + + GameSlot& getGameSlot(int boardIndex) { + + return this->gameBoard.at(boardIndex); + + } + + const std::vector& getGameBoardVector() const { + return this->gameBoard; + } + + void forceDropCoin(int columnNumber, bool isUserCoin) { + + if (this->forceDropAllowed) { + + //Find the top and bottom rows in the columns + int bottomRowInColumn = columnNumber + this->numberOfColumns * (this->numberOfRows - 1); + int topRowInColumn = columnNumber; + + //Start with bottom row and go one cell above at a time till an empty one is found + for (int slotCounter = bottomRowInColumn; slotCounter >= topRowInColumn; slotCounter -= this->numberOfColumns) { + if (this->gameBoard.at(slotCounter).isEmpty()) { + GameSlot& gameSlot = this->gameBoard.at(slotCounter); + gameSlot.putCoin(isUserCoin); + break; + } + } + + } + else { + throw std::logic_error("Force drop is not allowed for this game board"); + } + + + } + + }; + +} + +#endif \ No newline at end of file diff --git a/GameSlot.hpp b/GameSlot.hpp new file mode 100755 index 0000000..c002613 --- /dev/null +++ b/GameSlot.hpp @@ -0,0 +1,61 @@ +#ifndef GAMESLOT +#define GAMESLOT + +#include +#include + +//This class represents a slot in the connect four game that can either be empty, have a user coin or a system coin +class GameSlot { + +private: + + enum class SlotStates { empty, hasUserCoin, hasComputerCoin }; + + SlotStates slotState; + +public: + + //Constructor will create an empty slot + GameSlot() { + this->slotState = SlotStates::empty; + } + + //Put a coin into the slot. This is allowed only if the slot is empty + void putCoin(bool isUserCoin) { + + //If the slot is not empty then this is an unexpected error + if (this->slotState != SlotStates::empty) { + throw std::logic_error("The slot is not empty"); + } + + //Put the coin into the slot + if (isUserCoin) { + this->slotState = SlotStates::hasUserCoin; + } + else { + this->slotState = SlotStates::hasComputerCoin; + } + } + + //Check if slot is empty + bool isEmpty() const { + + if (this->slotState == SlotStates::empty) { + return true; + } + else { + return false; + } + } + + bool hasUserCoin() { + return this->slotState == SlotStates::hasUserCoin; + } + + bool hasComputerCoin() { + return this->slotState == SlotStates::hasComputerCoin; + } + +}; + +#endif \ No newline at end of file diff --git a/QuickSortDemo.cpp b/QuickSortDemo.cpp new file mode 100644 index 0000000..a2fc76d --- /dev/null +++ b/QuickSortDemo.cpp @@ -0,0 +1,87 @@ +#include "sorting.hpp" + +//Ask user to enter the number of elements to sort +int getNumberOfElementsToSort() { + + int numberOfElementsToSort; + std::string userResponse; + + std::cout << "How many elements do you want to sort? "; + std::cin >> userResponse; + + try { + numberOfElementsToSort = std::stoi(userResponse); + } + catch (std::invalid_argument&) { + std::cout << "Could not parse " << userResponse << " as an integer." << std::endl; + exit(0); + } + + return numberOfElementsToSort; +} + +int main() { + + //Get user preference for seeing data in standard output + bool showData = false; + std::cout << "Do you want to see data before and after sorting? Answer Y or N: "; + std::string userResponse; + std::cin >> userResponse; + std::ofstream outputFile; + + if (userResponse.compare("Y") == 0 || userResponse.compare("y") == 0) { + showData = true; + outputFile.open("outputFile.txt"); + } + + //Get user preference on number of data elements to sort + int numberOfElementsToSort = getNumberOfElementsToSort(); + + //Do serial sorting + SortableCollection sortableCollectionForSerial(numberOfElementsToSort); + + if (showData) { + std::vector dataToBeSerialSorted(sortableCollectionForSerial.getData()); + outputFile << "This is the sequential input data:" << std::endl; + for (int input : dataToBeSerialSorted) { + outputFile << input << " " << std::endl; + } + } + + sortableCollectionForSerial.doSequentialSort(); + + if (showData) { + outputFile << "This is the sequential sorted data:" << std::endl; + std::vector serialSortedData = sortableCollectionForSerial.getData(); + for (std::vector::iterator it = serialSortedData.begin(); it != serialSortedData.end(); ++it) { + outputFile << *it << " " << std::endl; + } + } + + std::cout << "Sequential sorting took " << sortableCollectionForSerial.getRunningTime() << " milliseconds" << std::endl; + + //Do parallel sorting + SortableCollection sortableCollectionForParallel(numberOfElementsToSort); + + if (showData) { + std::vector dataToBeSortedInParallel(sortableCollectionForParallel.getData()); + outputFile << "This is the parallel input data:" << std::endl; + for (int input : dataToBeSortedInParallel) { + outputFile << input << " " << std::endl; + } + } + + sortableCollectionForParallel.doParallelSort(); + + if (showData) { + outputFile << "This is the parallel sorted data:" << std::endl; + std::vector parallelSortedData = sortableCollectionForParallel.getData(); + + for (std::vector::iterator it = parallelSortedData.begin(); it != parallelSortedData.end(); ++it) { + outputFile << *it << " " << std::endl; + } + outputFile.close(); + } + + std::cout << "Parallel sorting took " << sortableCollectionForParallel.getRunningTime() << " milliseconds" << std::endl; +} \ No newline at end of file diff --git a/RunGame.cpp b/RunGame.cpp new file mode 100755 index 0000000..7c89942 --- /dev/null +++ b/RunGame.cpp @@ -0,0 +1,86 @@ +#include "ConnectFourGame.hpp" +#include "GameBoard.hpp" + +#include +#include + +int getColumnPlayedByUser() { + + int columnSelectedByUser; + std::cout << std::endl << "Which column do you want to drop the coin in (1 to 7): "; + while (true) { + std::cin >> columnSelectedByUser; + if (columnSelectedByUser >= 1 && columnSelectedByUser <= 7) { + return columnSelectedByUser - 1; + } + } +} + +//Show the game board to the user +void showGameBoard(const std::vector& gameBoardVector, int numberOfRowsToDisplay, int numberOfColumnsToDisplay) { + + std::cout << std::endl; + for (int rowCounter = 0; rowCounter < numberOfRowsToDisplay; ++rowCounter) { + for (int columnCounter = 0; columnCounter < numberOfColumnsToDisplay; ++columnCounter) { + + GameSlot gameSlot = gameBoardVector.at(rowCounter * numberOfColumnsToDisplay + columnCounter); + if (gameSlot.isEmpty()) { + std::cout << "_|"; + } + else if (gameSlot.hasUserCoin()) { + std::cout << "U|"; + } + else { + std::cout << "C|"; + } + } + std::cout << std::endl; + } + +} + +int main() { + + //Create a connect four game with default parameters + controller::ConnectFourGame connectFourGame{}; + + //Set difficulty level + int difficultyLevel; + std::cout << "What difficulty level do you want (1 to 10): "; + std::cin >> difficultyLevel; + if (difficultyLevel >= 1 && difficultyLevel <= 10) { + connectFourGame.setgameDifficultyLevel(difficultyLevel); + } + else { + std::cout << std::endl << "Difficulty level set to default value" << std::endl; + } + + //std::string computationsInParallel; + //std::cout << "Do you want to run parallel computations? "; + //std::cin >> computationsInParallel; + //if (computationsInParallel.compare("N") == 0 || computationsInParallel.compare("n") == 0) { + // connectFourGame.setComputationModeToParallel(false); + //} + + model::GameBoard gameBoard; + + while (true) { + + //Drop a coin into the column selected by the user + connectFourGame.dropCoin(getColumnPlayedByUser()); + gameBoard = connectFourGame.getGameBoard(); + showGameBoard(gameBoard.getGameBoardVector(), gameBoard.getNumberOfRows(), gameBoard.getNumberOfColumns()); + if (connectFourGame.isGameOver()) { + if (!connectFourGame.isGameBoardFull()) { + if (connectFourGame.didUserWinTheGame()) { + std::cout << std::endl << "Congratulations, you won!!!" << std::endl; + } + else { + std::cout << std::endl << "Sorry, better luck next time" << std::endl; + } + } + break; + } + } + +} \ No newline at end of file diff --git a/readme.txt b/readme.txt new file mode 100644 index 0000000..c19b906 --- /dev/null +++ b/readme.txt @@ -0,0 +1,27 @@ +Description of Work Done: + +I had some confusion on the understanding of the Map pattern and proposed doing a parallel QuickSort with the sorting of the data to the left and right of the pivot element being done in parallel. The proposal got accepted with the comment that I needed to use the Fork-Join pattern and that the book included an example of such a sort. + +So my code is heavily influenced by the code in the book. Had I known the book had QuickSort, I would have chosen something else. + +Here are the run time statistics for the serial and parallel sorts. + +Elements Serial Sort (ms) Parallel Sort (ms) Speedup +10 0 0.003 0.000 +100 0 0.002 0.000 +1000 0 0.002 0.000 +10000 0.002 0.005 0.400 +100000 0.033 0.014 2.357 +1000000 0.108 0.048 2.250 +10000000 1.315 0.415 3.169 + +I did not get sufficient time to make a GUI for this. And so this is a command line application. I have not covered the comma separated input data as without a GUI, it was cumbersome to choose between randomly generated data and importing comma separated data for sorting. + +Here is what the running application user interaction looks like: + +Do you want to see data before and after sorting? Answer Y or N: n +How many elements do you want to sort? 10000000 +Sequential sorting took 1.315 milliseconds +Parallel sorting took 0.415 milliseconds + +If the user answers Y to the first question, the program puts the input unsorted data and the sorted data into an outfile. \ No newline at end of file diff --git a/sorting.hpp b/sorting.hpp new file mode 100644 index 0000000..566d21b --- /dev/null +++ b/sorting.hpp @@ -0,0 +1,215 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +class SortableCollection { + +private: + + //Threshold for parallel sorting + ptrdiff_t PARALLEL_SORT_THESHOLD = 500; + + //Running time for the last sort operation + double runningTime; + + //Input data to be sorted + std::vector inputData; + + ////Sorted data + std::vector sortedData; + + //Choose median of three keys from values to be sorted + int* medianOfThree(int* x, int* y, int* z) { + + return *x < *y ? *y < *z ? y : *x < *z ? z : x : *z < *y ? y : *z < *x ? z : x; + + } + + //Choose a partition as median of medians + int* choosePartitionKey(int* first, int* last) { + + size_t offset = (last - first) / 8; + + return medianOfThree(medianOfThree(first, first + offset, first + offset * 2), + medianOfThree(first + offset * 3, first + offset * 4, last - (3 * offset + 1)), + medianOfThree(last - (2 * offset + 1), last - (offset + 1), last - 1)); + + } + + //Partition and return the position of the key + int* divide(int* first, int* last) { + + //Move the partition key to the front of the array + std::swap(*first, *choosePartitionKey(first, last)); + + //Partition the array + int key = *first; + int* middle = std::partition(first + 1, last, [=](const int& data) {return data < key; }) - 1; + + if (middle != first) { + //Move the key between the two partitions + std::swap(*first, *middle); + } + else { + //Return null if all keys are equal since there is no need to sort + if (last == std::find_if(first + 1, last, [=](const int& data){return key < data; })) { + return nullptr; + } + } + return middle; + } + + void parallelQuickSort(int* firstElement, int* lastElement) { + + tbb::task_group parallelSortGroup; + + //Do parallel sort for larger data size + while (lastElement - firstElement > PARALLEL_SORT_THESHOLD) { + + //Partition the array + int* middleElement = divide(firstElement, lastElement); + //If all elements are same, no more partitioning is required + if (middleElement == nullptr) { + parallelSortGroup.wait(); + return; + } + + //The array has now been partitioned into two + if (middleElement - firstElement < lastElement - (middleElement + 1)) { + + //The left partition is smaller and so spawn its sort + parallelSortGroup.run([=]{parallelQuickSort(firstElement, middleElement); }); + + //The next iteration will sort the right part of the array + firstElement = middleElement + 1; + + } + else { + + //The right partition is smaller and so spawn its sort + parallelSortGroup.run([=]{parallelQuickSort(middleElement + 1, lastElement); }); + + //The next iteration will sort the left part of the array + lastElement = middleElement; + } + + } + + //Number of elements is below the parallel threshold. So do serial sort. + std::sort(firstElement, lastElement + 1); + parallelSortGroup.wait(); + } + +public: + + //Constructor having path and file name of input data as parameter + SortableCollection(std::string inputDataFilePath) { + + //Open the input file + std::string inputLine, inputNumber; + std::ifstream inputFile(inputDataFilePath); + if (inputFile.is_open()) { + + //Read each line from input data text file + while (getline(inputFile, inputLine)) { + + //Process the comma separated tokens + std::istringstream inputStream(inputLine); + while (std::getline(inputStream, inputNumber, ',')) { + + //Put the number to be sorted into the input vector + try { + inputData.push_back(std::stoi(inputNumber)); + } + catch (std::invalid_argument&) { + std::cout << "Could not parse " << inputNumber << " as an integer." << std::endl; + exit(0); + } + } + + } + + inputFile.close(); + + } + else { + std::cout << "Could not open input data file " << inputDataFilePath << "." << std::endl; + exit(0); + } + + } + + //Constructor with number of input data elements to be generated as parameter + SortableCollection(int dataSize) { + + //Random number generator with uniform distribution + std::default_random_engine randomNumberGenerator; + std::uniform_int_distribution distribution(0, INT_MAX); + + //Fill input vector with random numbers to be sorted + for (int dataSizeCounter = 0; dataSizeCounter < dataSize; ++dataSizeCounter) { + inputData.push_back(distribution(randomNumberGenerator)); + } + + } + + //Do a sequential QuickSort on the input data and return the sorted result + void doSequentialSort() { + + //Get current time before sorting + auto start = std::chrono::high_resolution_clock::now(); + + std::sort(this->inputData.begin(), this->inputData.end()); + + //Find time spent in sorting + std::chrono::milliseconds runTimeInMilliseconds = std::chrono::duration_cast(std::chrono::high_resolution_clock::now() - start); + + //Store the last run time in seconds + this->runningTime = runTimeInMilliseconds.count() * 1.0; + } + + std::vector getData() { + + //Return a copy of the input data + return std::vector(this->inputData); + } + + //Do a parallel QuickSort on the input data and return the sorted result + void doParallelSort() { + + //Get current time before sorting + auto start = std::chrono::high_resolution_clock::now(); + + //Get internal array representation of the vector to be sorted + int numberOfElements = this->inputData.size(); + int* elements = this->inputData.data(); + int* firstElement = &elements[0]; + int* lastElement = &elements[numberOfElements - 1]; + + //Do the sorting in parallel + parallelQuickSort(firstElement, lastElement); + + //Find time spent in sorting + std::chrono::milliseconds runTimeInMilliseconds = std::chrono::duration_cast(std::chrono::high_resolution_clock::now() - start); + + //Store the last run time in seconds + this->runningTime = runTimeInMilliseconds.count() * 1.0; + + } + + //Return the run time of the last sort operation + double getRunningTime() { + + return this->runningTime; + } + +}; \ No newline at end of file