From 247076e2927f42749a5a37cd26819e642efd09e9 Mon Sep 17 00:00:00 2001 From: gopalmenon Date: Sat, 24 Jan 2015 19:14:17 -0700 Subject: [PATCH 1/4] Adding sorting.hpp, QuickSortDemo.cpp and readme.txt --- QuickSortDemo.cpp | 87 +++++++++++++++++++ readme.txt | 27 ++++++ sorting.hpp | 215 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 329 insertions(+) create mode 100644 QuickSortDemo.cpp create mode 100644 readme.txt create mode 100644 sorting.hpp 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/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..53289ae --- /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() / 1000.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() / 1000.0; + + } + + //Return the run time of the last sort operation + double getRunningTime() { + + return this->runningTime; + } + +}; \ No newline at end of file From 27001f02ce735c414340cfa4f5292ffb8ed034d6 Mon Sep 17 00:00:00 2001 From: gopalmenon Date: Sat, 14 Feb 2015 21:44:53 -0700 Subject: [PATCH 2/4] Added headers and test file for connect four game. Fixed bug in parallel sort run time computation. --- ConnectFourGame.hpp | 207 ++++++++++++++++++++++++++++++++++++++++++++ GameSlot.hpp | 48 ++++++++++ RunGame.cpp | 11 +++ sorting.hpp | 4 +- 4 files changed, 268 insertions(+), 2 deletions(-) create mode 100644 ConnectFourGame.hpp create mode 100644 GameSlot.hpp create mode 100644 RunGame.cpp diff --git a/ConnectFourGame.hpp b/ConnectFourGame.hpp new file mode 100644 index 0000000..fa54f42 --- /dev/null +++ b/ConnectFourGame.hpp @@ -0,0 +1,207 @@ +#include +#include +#include + +#include "GameSlot.hpp" + +#include +#include +#include +#include +#include + +class ConnectFourGame { + +private: + + //Constants + const static int DEFAULT_NUMBER_OF_ROWS = 6; + const static int DEFAULT_NUMBER_OF_COLUMNS = 7; + const static bool DEFAULT_FIRST_PLAYER_IS_USER = true; + const static bool DEFAULT_DIFFICULTY_LEVEL = 0; + + int numberOfRows, numberOfColumns, gameDifficultyLevel; + bool firstPlayerIsUser; + std::vector gameBoard; + + //Check if this is a valid play given the game board dimensions and coins already played + bool isValidPlay(int dropInColumn) { + + //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 (gameBoard.at(dropInColumn).isEmpty()) { + return true; + } + else { + return false; + } + + } + else { + return false; + } + + } + + //Check if the column is valid according to the board dimensions + bool isValidColumn(int columnNumber) { + + if (columnNumber >= 0 && columnNumber < this->numberOfColumns) { + return true; + } + else { + return false; + } + } + + //Called by the constructors. Fill the game board with empty slots + void fillGameBoardWithEmptySlots(int numberOfRows, int numberOfColumns) { + + int numberOfSlotsOnGameBoard = numberOfRows * numberOfColumns; + //TODO check if this is worth doing in parallel + for (int slotCounter = 0; slotCounter < numberOfSlotsOnGameBoard; ++slotCounter) { + gameBoard.emplace_back(GameSlot()); + } + + } + + //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 + 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()) { + return slotCounter; + } + } + + //Exit the program as if it reaches here as this should never happen + assert(false); + + return 0; + } + + //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->isValidPlay(dropInColumn)) { + + //Place a user coin in the top available position + this->gameBoard.at(getAvailableSlot(dropInColumn)).putCoin(isUserCoin); + + if (wasWinningPlay(dropInColumn, true)) { + endTheGame(true); + } + else { + //Make a move to best counter the user move + counterUserMove(); + } + } + } + + + //Check if the last play was a winning play + bool wasWinningPlay(int columnPlayed, bool playedByUser) { + + //TODO check for horizontal, vertical and two diagonal winning play + return false; + + } + + //Show message saying who won and disable game controls + void endTheGame(bool userWon) { + + //TODO display message saying the user won/lost and disable game playing controls + } + + //Consider all possible moves and play the one with the best hueristic score that maximizes the chance of winning + void counterUserMove() { + + std::vector moveScores; + + //Find best move by considering all columns in parallel using the Map pattern + tbb::parallel_for( + tbb::blocked_range(0, this->numberOfColumns), + [&](tbb::blocked_range range) { + for (size_t coulumnCounter = range.begin(); coulumnCounter != range.end(); ++coulumnCounter) { + moveScores.emplace_back(getMoveHueristicScore(coulumnCounter, true, this->gameDifficultyLevel)); + } + } + ); + + //Do a parallel reduction to find the move with the highest score + tbb::parallel_reduce( + tbb::blocked_range(0, this->numberOfColumns), + 0, + [=](const tbb::blocked_range& range, int moveColumn)->int { + for (int columnCounter = range.begin(); columnCounter != range.end(); ++columnCounter) { + if (moveScores.at(columnCounter) > moveColumn) { + moveColumn = moveScores.at(columnCounter); + } + } + return moveColumn; + }, + [](int moveColumn1, int moveColumn2)->int { + if (moveColumn1 > moveColumn2) { + return moveColumn1; + } + else { + return moveColumn2; + } + } + ); + } + + int getMoveHueristicScore(int columnPlayed, bool isRespondingToUserPlay, int recursionDepth) { + + //TODO implement hueristics to return score of the move + return 1; + + } + +public: + + //Default constructor will set game parameters using default values + ConnectFourGame() { + this->numberOfRows = DEFAULT_NUMBER_OF_ROWS; + this->numberOfColumns = DEFAULT_NUMBER_OF_COLUMNS; + this->firstPlayerIsUser = DEFAULT_FIRST_PLAYER_IS_USER; + this->gameDifficultyLevel = DEFAULT_DIFFICULTY_LEVEL; + fillGameBoardWithEmptySlots(this->numberOfRows, this->numberOfColumns); + //TODO computer to go first depending on user setting + } + + ConnectFourGame(int numberOfRows, int numberOfColumns) { + this->numberOfRows = numberOfRows; + this->numberOfColumns = numberOfColumns; + fillGameBoardWithEmptySlots(this->numberOfRows, this->numberOfColumns); + //TODO computer to go first depending on user setting + } + + //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; + } + + //User method to drop a coin into one of the columns + void dropCoin(int dropInColumn) { + + dropCoin(dropInColumn, true); + } + +}; \ No newline at end of file diff --git a/GameSlot.hpp b/GameSlot.hpp new file mode 100644 index 0000000..9fcd096 --- /dev/null +++ b/GameSlot.hpp @@ -0,0 +1,48 @@ +#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() { + + if (this->slotState == SlotStates::empty) { + return true; + } + else { + return false; + } + } + +}; \ No newline at end of file diff --git a/RunGame.cpp b/RunGame.cpp new file mode 100644 index 0000000..3990b8a --- /dev/null +++ b/RunGame.cpp @@ -0,0 +1,11 @@ +#include "ConnectFourGame.hpp" + +int main() { + + //Create a connect four game with default parameters + ConnectFourGame connectFourGame{}; + + //Drop a coin into the middle column + connectFourGame.dropCoin(3); + +} \ No newline at end of file diff --git a/sorting.hpp b/sorting.hpp index 53289ae..566d21b 100644 --- a/sorting.hpp +++ b/sorting.hpp @@ -174,7 +174,7 @@ class SortableCollection { 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() / 1000.0; + this->runningTime = runTimeInMilliseconds.count() * 1.0; } std::vector getData() { @@ -202,7 +202,7 @@ class SortableCollection { 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() / 1000.0; + this->runningTime = runTimeInMilliseconds.count() * 1.0; } From 84a0ebf1e338604be26df05d0d5e69668d004ffc Mon Sep 17 00:00:00 2001 From: gopalmenon Date: Tue, 14 Apr 2015 17:41:34 -0600 Subject: [PATCH 3/4] Added namespaces as per MVC pattern, added logic to run computations in serial and parallel modes. Tested serial mode. Parallel mode to be tested. --- ConnectFourGame.hpp | 781 ++++++++++++++++++++++++++++++++------------ GameBoard.hpp | 248 ++++++++++++++ GameSlot.hpp | 109 ++++--- RunGame.cpp | 92 +++++- 4 files changed, 965 insertions(+), 265 deletions(-) mode change 100644 => 100755 ConnectFourGame.hpp create mode 100755 GameBoard.hpp mode change 100644 => 100755 GameSlot.hpp mode change 100644 => 100755 RunGame.cpp diff --git a/ConnectFourGame.hpp b/ConnectFourGame.hpp old mode 100644 new mode 100755 index fa54f42..f883d88 --- a/ConnectFourGame.hpp +++ b/ConnectFourGame.hpp @@ -1,207 +1,574 @@ -#include -#include -#include - -#include "GameSlot.hpp" - -#include -#include -#include -#include -#include - -class ConnectFourGame { - -private: - - //Constants - const static int DEFAULT_NUMBER_OF_ROWS = 6; - const static int DEFAULT_NUMBER_OF_COLUMNS = 7; - const static bool DEFAULT_FIRST_PLAYER_IS_USER = true; - const static bool DEFAULT_DIFFICULTY_LEVEL = 0; - - int numberOfRows, numberOfColumns, gameDifficultyLevel; - bool firstPlayerIsUser; - std::vector gameBoard; - - //Check if this is a valid play given the game board dimensions and coins already played - bool isValidPlay(int dropInColumn) { - - //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 (gameBoard.at(dropInColumn).isEmpty()) { - return true; - } - else { - return false; - } - - } - else { - return false; - } - - } - - //Check if the column is valid according to the board dimensions - bool isValidColumn(int columnNumber) { - - if (columnNumber >= 0 && columnNumber < this->numberOfColumns) { - return true; - } - else { - return false; - } - } - - //Called by the constructors. Fill the game board with empty slots - void fillGameBoardWithEmptySlots(int numberOfRows, int numberOfColumns) { - - int numberOfSlotsOnGameBoard = numberOfRows * numberOfColumns; - //TODO check if this is worth doing in parallel - for (int slotCounter = 0; slotCounter < numberOfSlotsOnGameBoard; ++slotCounter) { - gameBoard.emplace_back(GameSlot()); - } - - } - - //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 + 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()) { - return slotCounter; - } - } - - //Exit the program as if it reaches here as this should never happen - assert(false); - - return 0; - } - - //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->isValidPlay(dropInColumn)) { - - //Place a user coin in the top available position - this->gameBoard.at(getAvailableSlot(dropInColumn)).putCoin(isUserCoin); - - if (wasWinningPlay(dropInColumn, true)) { - endTheGame(true); - } - else { - //Make a move to best counter the user move - counterUserMove(); - } - } - } - - - //Check if the last play was a winning play - bool wasWinningPlay(int columnPlayed, bool playedByUser) { - - //TODO check for horizontal, vertical and two diagonal winning play - return false; - - } - - //Show message saying who won and disable game controls - void endTheGame(bool userWon) { - - //TODO display message saying the user won/lost and disable game playing controls - } - - //Consider all possible moves and play the one with the best hueristic score that maximizes the chance of winning - void counterUserMove() { - - std::vector moveScores; - - //Find best move by considering all columns in parallel using the Map pattern - tbb::parallel_for( - tbb::blocked_range(0, this->numberOfColumns), - [&](tbb::blocked_range range) { - for (size_t coulumnCounter = range.begin(); coulumnCounter != range.end(); ++coulumnCounter) { - moveScores.emplace_back(getMoveHueristicScore(coulumnCounter, true, this->gameDifficultyLevel)); - } - } - ); - - //Do a parallel reduction to find the move with the highest score - tbb::parallel_reduce( - tbb::blocked_range(0, this->numberOfColumns), - 0, - [=](const tbb::blocked_range& range, int moveColumn)->int { - for (int columnCounter = range.begin(); columnCounter != range.end(); ++columnCounter) { - if (moveScores.at(columnCounter) > moveColumn) { - moveColumn = moveScores.at(columnCounter); - } - } - return moveColumn; - }, - [](int moveColumn1, int moveColumn2)->int { - if (moveColumn1 > moveColumn2) { - return moveColumn1; - } - else { - return moveColumn2; - } - } - ); - } - - int getMoveHueristicScore(int columnPlayed, bool isRespondingToUserPlay, int recursionDepth) { - - //TODO implement hueristics to return score of the move - return 1; - - } - -public: - - //Default constructor will set game parameters using default values - ConnectFourGame() { - this->numberOfRows = DEFAULT_NUMBER_OF_ROWS; - this->numberOfColumns = DEFAULT_NUMBER_OF_COLUMNS; - this->firstPlayerIsUser = DEFAULT_FIRST_PLAYER_IS_USER; - this->gameDifficultyLevel = DEFAULT_DIFFICULTY_LEVEL; - fillGameBoardWithEmptySlots(this->numberOfRows, this->numberOfColumns); - //TODO computer to go first depending on user setting - } - - ConnectFourGame(int numberOfRows, int numberOfColumns) { - this->numberOfRows = numberOfRows; - this->numberOfColumns = numberOfColumns; - fillGameBoardWithEmptySlots(this->numberOfRows, this->numberOfColumns); - //TODO computer to go first depending on user setting - } - - //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; - } - - //User method to drop a coin into one of the columns - void dropCoin(int dropInColumn) { - - dropCoin(dropInColumn, true); - } - -}; \ No newline at end of file +#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; + + int gameDifficultyLevel; + bool firstPlayerIsUser, defaultModeIsParallel, gameIsOver, userWonTheGame; + model::GameBoard gameBoard; + + //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)) { + 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)) { + endTheGame(false); + } + } + } + } + + //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 = getHorizontalHueristicScore(columnPlayed, this->gameBoard, isUserCoin); + int verticalHueristicScore = getVerticalHueristicScore(columnPlayed, this->gameBoard, isUserCoin); + int positiveSlopeHueristicScore = getPositiveSlopeHueristicScore(columnPlayed, this->gameBoard, isUserCoin); + int negativeSlopeHueristicScore = getNegativeSlopeHueristicScore(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 parallel reduction to find the move with the highest score + + return tbb::parallel_reduce( + tbb::blocked_range(0, gameBoard.getNumberOfColumns()), + 0, + [=](const tbb::blocked_range& range, int bestScore)->int { + int currentScore; + bestScore = -1 * INT_MAX; + for (int columnCounter = range.begin(); columnCounter != range.end(); ++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; + }, + [](int bestScore1, int bestScore2)->int { + return std::max(bestScore1, bestScore2); + } + ); + } + + 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->defaultModeIsParallel) { + 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) { + + //std::cout << "depth " << depth << ", column " << columnPlayed << (isUserCoin ? ", user coin" : ", computer coin") << std::endl; + //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 = getHorizontalHueristicScore(columnPlayed, gameBoard, isUserCoin); + int verticalHueristicScore = getVerticalHueristicScore(columnPlayed, gameBoard, isUserCoin); + int positiveSlopeHueristicScore = getPositiveSlopeHueristicScore(columnPlayed, gameBoard, isUserCoin); + int negativeSlopeHueristicScore = getNegativeSlopeHueristicScore(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; + + return tbb::parallel_reduce( + tbb::blocked_range(0, this->gameBoard.getNumberOfColumns()), + 0, + [=](const tbb::blocked_range& range, int bestMove)->int { + int currentScore, bestScore = -1 * INT_MAX; + for (int columnCounter = range.begin(); columnCounter != range.end(); ++columnCounter) { + //std::cout << "Column " << columnCounter << std::endl; + 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); + std::cout << "Score is " << currentScore << " for column " << columnCounter << std::endl; + if (currentScore > bestScore) { + bestScore = currentScore; + bestMove = columnCounter; + std::cout << "Best score is " << currentScore << " for column " << columnCounter << std::endl; + } + } + else { + //std::cout << "Column " << columnCounter << " is not valid." << std::endl; + } + } + std::cout << "Return best move " << bestMove << std::endl; + return bestMove; + }, + [](int bestMove1, int bestMove2)->int { + std::cout << "Best move to play is " << std::max(bestMove1, bestMove2) << std::endl; + return std::max(bestMove1, bestMove2); + } + ); + + } + + //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) { + //std::cout << "Series Column " << columnCounter << std::endl; + 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); + //std::cout << "Series score is " << currentScore << " for column " << columnCounter << std::endl; + if (currentScore > bestScore) { + bestScore = currentScore; + bestMove = columnCounter; + //std::cout << "Best score is " << currentScore << " for column " << columnCounter << std::endl; + } + } + else { + //std::cout << "Column " << columnCounter << " is not valid." << std::endl; + } + } + 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->defaultModeIsParallel) { + return evaluatePotentialMovesInParallel(); + } + else { + return evaluatePotentialMovesInSeries(); + } + } + + 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->defaultModeIsParallel = DEFAULT_MODE_IS_PARALLEL; + this->gameIsOver = false; + //TODO computer to go first depending on user setting + } + + ConnectFourGame(int numberOfRows, int numberOfColumns) { + + gameBoard = model::GameBoard(numberOfRows, numberOfColumns); + this->firstPlayerIsUser = DEFAULT_FIRST_PLAYER_IS_USER; + this->gameDifficultyLevel = DEFAULT_DIFFICULTY_LEVEL; + this->defaultModeIsParallel = DEFAULT_MODE_IS_PARALLEL; + this->gameIsOver = false; + //TODO computer to go first depending on user setting + } + + //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->defaultModeIsParallel = 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); + } + } + + }; + +} + +#endif \ No newline at end of file diff --git a/GameBoard.hpp b/GameBoard.hpp new file mode 100755 index 0000000..75bbca9 --- /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; + } + + 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 old mode 100644 new mode 100755 index 9fcd096..c002613 --- a/GameSlot.hpp +++ b/GameSlot.hpp @@ -1,48 +1,61 @@ -#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() { - - if (this->slotState == SlotStates::empty) { - return true; - } - else { - return false; - } - } - -}; \ No newline at end of file +#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/RunGame.cpp b/RunGame.cpp old mode 100644 new mode 100755 index 3990b8a..0470d76 --- a/RunGame.cpp +++ b/RunGame.cpp @@ -1,11 +1,83 @@ -#include "ConnectFourGame.hpp" - -int main() { - - //Create a connect four game with default parameters - ConnectFourGame connectFourGame{}; - - //Drop a coin into the middle column - connectFourGame.dropCoin(3); - +#include "ConnectFourGame.hpp" +#include "GameBoard.hpp" + +#include +#include + +int getColumnPlayedByUser() { + + int columnSelectedByUser; + std::cout << "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) { + + 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); + } + else { + std::cout << std::endl << "Computations will be in parallel" << std::endl; + } + + 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()) { + break; + } + } + + + connectFourGame.dropCoin(3); + } \ No newline at end of file From 4db04d7649526c2cd8ea3d8b559e04ec86e945a9 Mon Sep 17 00:00:00 2001 From: gopalmenon Date: Sun, 19 Apr 2015 15:10:56 -0600 Subject: [PATCH 4/4] Fixed bug in parallel computation. Replaced Parallel Reduce with Parallel For and if statement as Parallel Reduce was not working as expected. Will continue working on that issue. Computation of hueristic score in four directions also made parallel. Made the program text output more readable by putting the pipe character between tokens. --- ConnectFourGame.hpp | 219 ++++++++++++++++++++++++++------------------ GameBoard.hpp | 2 +- RunGame.cpp | 35 +++---- 3 files changed, 152 insertions(+), 104 deletions(-) diff --git a/ConnectFourGame.hpp b/ConnectFourGame.hpp index f883d88..157ea7c 100755 --- a/ConnectFourGame.hpp +++ b/ConnectFourGame.hpp @@ -2,7 +2,7 @@ #define CONNECTFOURGAME #include -#include +#include #include "GameBoard.hpp" #include "GameSlot.hpp" @@ -28,35 +28,53 @@ namespace controller { 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, defaultModeIsParallel, gameIsOver, userWonTheGame; + bool firstPlayerIsUser, gameModeIsParallel, gameIsOver, userWonTheGame; model::GameBoard gameBoard; - //Drop a coin into one of the columns - void dropCoin(int dropInColumn, bool isUserCoin) { + //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) { - //First check if the play is a valid one - if (this->gameBoard.isValidPlay(dropInColumn)) { + if (this->gameModeIsParallel) { - //Place a user coin in the top available position - GameSlot& gameSlot = this->gameBoard.getGameSlot(this->gameBoard.getAvailableSlot(dropInColumn)); - gameSlot.putCoin(isUserCoin); + 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; + + } - if (wasWinningPlay(dropInColumn, true)) { - 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)) { - endTheGame(false); } } + ); + + } + else { + horizontalHueristicScore = getHorizontalHueristicScore(columnPlayed, gameBoard, isUserCoin); + verticalHueristicScore = getVerticalHueristicScore(columnPlayed, gameBoard, isUserCoin); + positiveSlopeHueristicScore = getPositiveSlopeHueristicScore(columnPlayed, gameBoard, isUserCoin); + negativeSlopeHueristicScore = getNegativeSlopeHueristicScore(columnPlayed, gameBoard, isUserCoin); } } @@ -65,11 +83,8 @@ namespace controller { //Compute hueristic scores for horizontal, vertical and diagonal four coins in a row resulting from //coin being dropped in column - int horizontalHueristicScore = getHorizontalHueristicScore(columnPlayed, this->gameBoard, isUserCoin); - int verticalHueristicScore = getVerticalHueristicScore(columnPlayed, this->gameBoard, isUserCoin); - int positiveSlopeHueristicScore = getPositiveSlopeHueristicScore(columnPlayed, this->gameBoard, isUserCoin); - int negativeSlopeHueristicScore = getNegativeSlopeHueristicScore(columnPlayed, this->gameBoard, isUserCoin); - + 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 || @@ -90,35 +105,36 @@ namespace controller { } int bestHeuristicScoreForOpponentMoveParallel(int depth, bool isUserCoin, const model::GameBoard gameBoard) { - //Do a parallel reduction to find the move with the highest score - return tbb::parallel_reduce( - tbb::blocked_range(0, gameBoard.getNumberOfColumns()), - 0, - [=](const tbb::blocked_range& range, int bestScore)->int { - int currentScore; - bestScore = -1 * INT_MAX; - for (int columnCounter = range.begin(); columnCounter != range.end(); ++columnCounter) { + //Do a map to find the move with the highest score + std::vector moveScores(gameBoard.getNumberOfColumns()); - 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); + tbb::parallel_for( + tbb::blocked_range(0, gameBoard.getNumberOfColumns()), + [=, &moveScores](tbb::blocked_range range) { - currentScore = getMoveHueristicScore(depth, columnCounter, isUserCoin, whatIfGameBoard); - if (currentScore > bestScore) { - bestScore = currentScore; + 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; } - } } + ); - return bestScore; - }, - [](int bestScore1, int bestScore2)->int { - return std::max(bestScore1, bestScore2); + 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) { @@ -145,7 +161,7 @@ namespace controller { //Compute best heuristic score for opponent move int bestHeuristicScoreForOpponentMove(int depth, bool isUserCoin, const model::GameBoard gameBoard) { - if (this->defaultModeIsParallel) { + if (this->gameModeIsParallel) { return bestHeuristicScoreForOpponentMoveParallel(depth, isUserCoin, gameBoard); } else { @@ -157,7 +173,6 @@ namespace controller { //Compute and return the hueristic score for the move int getMoveHueristicScore(int depth, int columnPlayed, bool isUserCoin, model::GameBoard gameBoard) { - //std::cout << "depth " << depth << ", column " << columnPlayed << (isUserCoin ? ", user coin" : ", computer coin") << std::endl; //If maximum depth has been reached, then return if (depth == 0) { return 0; @@ -165,10 +180,8 @@ namespace controller { //Compute hueristic scores for horizontal, vertical and diagonal four coins in a row resulting from //coin being dropped in column - int horizontalHueristicScore = getHorizontalHueristicScore(columnPlayed, gameBoard, isUserCoin); - int verticalHueristicScore = getVerticalHueristicScore(columnPlayed, gameBoard, isUserCoin); - int positiveSlopeHueristicScore = getPositiveSlopeHueristicScore(columnPlayed, gameBoard, isUserCoin); - int negativeSlopeHueristicScore = getNegativeSlopeHueristicScore(columnPlayed, gameBoard, isUserCoin); + 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 || @@ -439,39 +452,35 @@ namespace controller { int evaluatePotentialMovesInParallel() { int depth = this->gameDifficultyLevel; - - return tbb::parallel_reduce( + std::vector moveScores(this->gameBoard.getNumberOfColumns()); + + tbb::parallel_for( tbb::blocked_range(0, this->gameBoard.getNumberOfColumns()), - 0, - [=](const tbb::blocked_range& range, int bestMove)->int { - int currentScore, bestScore = -1 * INT_MAX; + [=, &moveScores](tbb::blocked_range range) { + for (int columnCounter = range.begin(); columnCounter != range.end(); ++columnCounter) { - //std::cout << "Column " << columnCounter << std::endl; 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); - std::cout << "Score is " << currentScore << " for column " << columnCounter << std::endl; - if (currentScore > bestScore) { - bestScore = currentScore; - bestMove = columnCounter; - std::cout << "Best score is " << currentScore << " for column " << columnCounter << std::endl; - } - } - else { - //std::cout << "Column " << columnCounter << " is not valid." << std::endl; + moveScores.at(columnCounter) = getMoveHueristicScore(depth, columnCounter, false, whatIfGameBoard); + } } - std::cout << "Return best move " << bestMove << std::endl; - return bestMove; - }, - [](int bestMove1, int bestMove2)->int { - std::cout << "Best move to play is " << std::max(bestMove1, bestMove2) << std::endl; - return std::max(bestMove1, bestMove2); } ); + 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 @@ -479,29 +488,25 @@ namespace controller { int depth = this->gameDifficultyLevel, currentScore, bestScore = -1 * INT_MAX, bestMove = 0; for (int columnCounter = 0; columnCounter < this->gameBoard.getNumberOfColumns(); ++columnCounter) { - //std::cout << "Series Column " << columnCounter << std::endl; + 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); - //std::cout << "Series score is " << currentScore << " for column " << columnCounter << std::endl; + if (currentScore > bestScore) { bestScore = currentScore; bestMove = columnCounter; - //std::cout << "Best score is " << currentScore << " for column " << columnCounter << std::endl; } } - else { - //std::cout << "Column " << columnCounter << " is not valid." << std::endl; - } } 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->defaultModeIsParallel) { + if (this->gameModeIsParallel) { return evaluatePotentialMovesInParallel(); } else { @@ -509,6 +514,34 @@ namespace controller { } } + //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 @@ -517,9 +550,9 @@ namespace controller { gameBoard = model::GameBoard(); this->firstPlayerIsUser = DEFAULT_FIRST_PLAYER_IS_USER; this->gameDifficultyLevel = DEFAULT_DIFFICULTY_LEVEL; - this->defaultModeIsParallel = DEFAULT_MODE_IS_PARALLEL; + this->gameModeIsParallel = DEFAULT_MODE_IS_PARALLEL; this->gameIsOver = false; - //TODO computer to go first depending on user setting + } ConnectFourGame(int numberOfRows, int numberOfColumns) { @@ -527,9 +560,9 @@ namespace controller { gameBoard = model::GameBoard(numberOfRows, numberOfColumns); this->firstPlayerIsUser = DEFAULT_FIRST_PLAYER_IS_USER; this->gameDifficultyLevel = DEFAULT_DIFFICULTY_LEVEL; - this->defaultModeIsParallel = DEFAULT_MODE_IS_PARALLEL; + this->gameModeIsParallel = DEFAULT_MODE_IS_PARALLEL; this->gameIsOver = false; - //TODO computer to go first depending on user setting + } //First player will be determined by user selection @@ -552,7 +585,7 @@ namespace controller { //Set the serial/parallel computation mode depending on user preference void setComputationModeToParallel(bool parallelMode) { - this->defaultModeIsParallel = parallelMode; + this->gameModeIsParallel = parallelMode; } const model::GameBoard& getGameBoard() const { @@ -567,6 +600,18 @@ namespace controller { } } + //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; + } }; } diff --git a/GameBoard.hpp b/GameBoard.hpp index 75bbca9..74152e8 100755 --- a/GameBoard.hpp +++ b/GameBoard.hpp @@ -57,7 +57,7 @@ namespace model { return this->numberOfRows; } - int getNumberOfColumns() const { + const int getNumberOfColumns() const { return this->numberOfColumns; } diff --git a/RunGame.cpp b/RunGame.cpp index 0470d76..7c89942 100755 --- a/RunGame.cpp +++ b/RunGame.cpp @@ -7,7 +7,7 @@ int getColumnPlayedByUser() { int columnSelectedByUser; - std::cout << "Which column do you want to drop the coin in (1 to 7): "; + 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) { @@ -19,18 +19,19 @@ int getColumnPlayedByUser() { //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 << "_"; + std::cout << "_|"; } else if (gameSlot.hasUserCoin()) { - std::cout << "U"; + std::cout << "U|"; } else { - std::cout << "C"; + std::cout << "C|"; } } std::cout << std::endl; @@ -54,15 +55,12 @@ int main() { 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); - } - else { - std::cout << std::endl << "Computations will be in parallel" << 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; @@ -73,11 +71,16 @@ int main() { 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; } } - - connectFourGame.dropCoin(3); - } \ No newline at end of file