Hacker Challenge: Gomoku (Human vs. Computer) - Solution
Learn how we can implement the Gomoku using the human vs. computer strategy.
We'll cover the following
Gomoku game (human vs. computer)
In the last lesson, we discussed the Gomoku human vs. computer approach. In this lesson, we'll implement this approach while explaining the solution in detail. The following reiterates the requirements to convert the Gomoku human vs. human approach to the human vs. computer approach:
The number of players is restricted to two; this is done to ensure that we have one human and one computer player to simplify the algorithm.
The computer can randomly place its symbol in any valid board position.
The computer should perform its turn by keeping in mind the following two points:
The computer must prioritize placing its symbol where it has a winning chance.
If that's not the case, then the computer must prioritize placing its symbol where it can block the opponent's win.
Implementation walkthrough
Restricting the number of players to two
Since we are restricting the number of players to two, there's no need for the NOP
variable. Hence, the implementation of the init()
and turnChange()
functions have to be altered because both use the NOP
variable.
Updated version of the init()
function
In the previous version, the init()
function asked for the number of players from the user and stored the value inside the NOP
variable. Based on this variable, it asked the user for the names and symbols of said players. Since we'll only have one player in this version, all of this is unnecessary. The code snippet below highlights the changes made to the init()
function:
void init(char Board[][CAPACITY], int& dim, char pName[2][30], char pSym[], int& turn, int& winCount){cout << "Dimension: ";cin >> dim;cout << "Win Count: ";cin >> winCount;cout << "Enter player's name: ";cin >> pName[0];cout << "Enter player's symbol: ";cin >> pSym[0];pSym[1] = (pSym[0] == 'X' || pSym[0] == 'x') ? 'O' : 'X';for (int ri = 0; ri < dim; ri++){for (int ci = 0; ci < dim; ci++){Board[ri][ci] = '-';}}turn = rand() % 2;}
Updated version of the turnChange()
function
In the previous version, the modulus was taken by the number of players (NOP
). However, in this case, we should take the modulus by 2
, as shown below:
void turnChange(int& turn){turn = (turn + 1) % 2;}
Secondly, it'll be useful to create an enum
to differentiate between the human and computer players. So, we declare the following before the function declarations:
enum TYPE{ HUMAN, COMPUTER };
Implementing the computer's move
For the computer's move, we have primarily two cases that we will implement in the HvsC
function:
The random case
The winning case
The random case
In the random case, we need to generate a random number for both the row and the column (within the bounds of the board) and ensure that the selected position is valid (the isValid()
function will come in handy here). The code snippet below shows how this can be done:
do{ri = rand() % dim;ci = rand() % dim;} while (!validInput(ri, ci, Board, dim));
AI Level 1: This approach is the most basic form of AI, where decisions are made by random.
Invalid points
If the computer selects the wrong coordinates, do we need to print a message of invalid points?
The winning case
This case is not as straightforward as the previous one because we have to ensure all three of the following:
If the computer can win by placing a symbol, it should make that move.
Otherwise, the computer should place a symbol to block the opponent's win.
The above two points should hold, even when the consecutive symbols are less than
winCount - 1
.
The computer must check all the valid positions of the board with respect to the first two conditions. Hence, for each valid position, we should do the following:
Place the computer's/human's symbol temporarily on the board.
Invoke the
isWinHere()
function and, iftrue
is returned, we don't need to check for further moves; therefore, we return the function.
Before we head into the implementation of the winning case, we have to keep in mind that the Board
parameter is of type const char[][]
, meaning that we can't place any symbol inside it. Therefore, the following code creates a copy of the Board
in which we can place the symbol temporarily and invoke the isWinHere()
function over it as well:
// creating an array for a new boardchar T[CAPACITY][CAPACITY];// copying the symbols from the board 'Board' to board 'T'for (int r = 0; r < dim; r++){for (int c = 0; c < dim; c++){T[r][c] = Board[r][c];}}
Now, we can use the copy of the board stored inside T
in our algorithms.
Implementation of the computer's winning move algorithm
// Iterate over all positions in the boardfor (int r = 0; r < dim; r++){for (int c = 0; c < dim; c++){// If the position (r, c) is validif (validInput(r, c, T, dim)){// Place computer's symbolT[r][c] = CS;// If the computer winsif (isWinHere(T, dim, WC, pSym, r, c)){// Remove computer's symbol from the boardT[r][c] = '-';// Return the computer's move coordinatesri = r;ci = c;return;}else{// Remove computer's symbol from the boardT[r][c] = '-';}}}}
AI Level 2: This approach is a slightly advanced form of AI, where decisions are prioritized to benefit the computer.
Implementation of the human's winning move-blocking algorithm
// Iterate over all positions in the boardfor (int r = 0; r < dim; r++){for (int c = 0; c < dim; c++){// If the position (r, c) is validif (validInput(r, c, T, dim)){// Place human's symbolT[r][c] = HS;// If the human winsif (isWinHere(T, dim, WC, pSym, r, c)){// Remove human's symbol from the boardT[r][c] = '-';// Return the human's move coordinatesri = r;ci = c;return;}else{// Remove human's symbol from the boardT[r][c] = '-';}}}}
AI Level 3: This approach is the most advanced form of AI, where decisions are not only prioritized to benefit the computer, but also to put the user at a disadvantageous position.
The outer loop iterates over all the rows in the board (from 0
to dim-1
), and the inner loop iterates over all the columns in the board (from 0
to dim-1
).
For each position (`r`,`c`) in the board, the code checks if the position is valid by calling `validInput(r, c, T, dim)`. If the position is valid, it places the human’s symbol (HS) in that position. Then, it checks if the human wins by calling `isWinHere(T, dim, WC, pSym, r, c)`. If the human wins, it returns the human’s move coordinates, which are stored in the variables `ri` and `ci`. If the human doesn’t win, the code removes the human’s symbol from the board by setting `T[r][c] = ‘-’`. This loop tries to simulate the human move and check if the human wins. If that's the case, it returns the coordinates of the winning move. Otherwise, it returns nothing.
It’s important to note that this code doesn’t guarantee that the human will win. Rather, it’s checking all the possible moves to check if the human would win in any of them.
Our algorithm for the computer's move selection is almost complete, with only one unresolved issue. Can you identify that issue? Does the computer only need to choose the coordinates when the winCount
is equal to a specific number (for example, 5
)? If this was the case, the human player could easily win, because the computer would place numbers randomly until the winCount
would not be equal to the specific number.
To resolve this issue, we'll add one more loop for winCount
that decrements the winCount
until it is equal to 2
. Hence, the computer will now perform computations for each winCount
until 2
and choose the best winning move according to the requirements listed above.
Piecing it all together, we get the following code for the HvsC()
function:
void HvsC(const char Board[][CAPACITY], int& ri, int& ci, int dim, char pSym, int winCount, char CS, char HS){// creating an array for a new boardchar T[CAPACITY][CAPACITY];// copying the symbols from the board 'Board' to board 'T'for (int r = 0; r < dim; r++){for (int c = 0; c < dim; c++){T[r][c] = Board[r][c];}}for (int WC = winCount; WC > 1; WC--) {// Iterate over all positions in the boardfor (int r = 0; r < dim; r++){for (int c = 0; c < dim; c++){// If the position (r, c) is validif (validInput(r, c, T, dim)){// Place computer's symbolT[r][c] = CS;// If the computer winsif (isWinHere(T, dim, WC, pSym, r, c)){// Remove computer's symbol from the boardT[r][c] = '-';// Return the computer's move coordinatesri = r;ci = c;return;}else{// Remove computer's symbol from the boardT[r][c] = '-';}}}}// Iterate over all positions in the boardfor (int r = 0; r < dim; r++){for (int c = 0; c < dim; c++){// If the position (r, c) is validif (validInput(r, c, T, dim)){// Place human's symbolT[r][c] = HS;// If the human winsif (isWinHere(T, dim, WC, pSym, r, c)){// Remove human's symbol from the boardT[r][c] = '-';// Return the human's move coordinatesri = r;ci = c;return;}else{// Remove human's symbol from the boardT[r][c] = '-';}}}}}do {ri = rand() % dim;ci = rand() % dim;} while (!validInput(ri, ci, Board, dim));}
Differentiating between the human's and computer's moves
Finally, all that is left for us is to invoke the logic for the human's and computer's moves separately inside the main()
function, and our game is complete. Therefore, we add the following inside the do-while
loop after printing the board:
if (turn == HUMAN){do{userInput(ri, ci, pName[turn], pSym[turn]);if (!validInput(ri, ci, Board, dim)){cout << "Invalid Input" << endl;}} while (!validInput(ri, ci, Board, dim));}else{cout << "Computer's turn..." << endl;sleep(3);HvsC(Board, ri, ci, dim, winCount, pSym[turn], pSym[COMPUTER], pSym[HUMAN]);}
Complete implementation
#include <iostream> #include <time.h> #include <unistd.h> #define CAPACITY 100 using namespace std; enum TYPE{ HUMAN, COMPUTER }; void init(char Board[][CAPACITY], int& dim, char pName[2][30], char pSym[], int& turn, int& winCount) { cout << "Dimension: "; cin >> dim; cout << "Win Count: "; cin >> winCount; cout << "Enter player's name: "; cin >> pName[0]; cout << "Enter player's symbol: "; cin >> pSym[0]; // pName[1] = (const char [30])"Computer"; pSym[1] = (pSym[0] == 'X' || pSym[0] == 'x') ? 'O' : 'X'; for (int ri = 0; ri < dim; ri++) { for (int ci = 0; ci < dim; ci++) { Board[ri][ci] = '-'; } } turn = rand() % 2; } void printBoard(char Board[][CAPACITY], int dim) { system("clear"); for (int ri = 0; ri < dim; ri++) { for (int ci = 0; ci < dim; ci++) { cout << Board[ri][ci] << " "; } cout << endl; } } void userInput(int& ri, int& ci, char pName[], char pSym) { cout << pName << "'s Turn to place '" << pSym << " ' (ri, ci): " ; cin >> ri; cin >> ci; ri--; ci--; } bool validInput(int ri, int ci, const char Board[][CAPACITY], int dim) { return ((ri >= 0 && ci >= 0) && (ri <= dim && ci <= dim) && Board[ri][ci] == '-'); } void updateBoard(char Board[][CAPACITY], int dim, int ri, int ci, char pSym) { Board[ri][ci] = pSym; } bool horizontalCheck(char Board[][CAPACITY], int dim, int winCount, char pSym, int ri, int ci) { if (ci + winCount - 1 >= dim) return false; for (int i = 0; i < winCount; i++) { if (Board[ri][ci + i] != pSym) return false; } return true; } bool verticalCheck(char Board[][CAPACITY], int dim, int winCount, char pSym, int ri, int ci) { if (ri + winCount - 1 >= dim) return false; for (int i = 0; i < winCount; i++) { if (Board[ri + i][ci] != pSym) return false; } return true; } bool rightDiagonalCheck(char Board[][CAPACITY], int dim, int winCount, char pSym, int ri, int ci) { if (ri + winCount - 1 >= dim) return false; for (int i = 0; i < winCount; i++) { if (Board[ri + i][ci + i] != pSym) return false; } return true; } bool leftDiagonalCheck(char Board[][CAPACITY], int dim, int winCount, char pSym, int ri, int ci) { if (ci - (winCount - 1) < 0) return false; for (int i = 0; i < winCount; i++) { if (Board[ri + i][ci - i] != pSym) return false; } return true; } bool isWinHere(char Board[][CAPACITY], int dim, int winCount, char pSym, int ri, int ci) { bool horizontal = horizontalCheck(Board, dim, winCount, pSym, ri, ci); bool vertical = verticalCheck(Board, dim, winCount, pSym, ri, ci); bool leftDiagonal = leftDiagonalCheck(Board, dim, winCount, pSym, ri, ci); bool rightDiagnal = rightDiagonalCheck(Board, dim, winCount, pSym, ri, ci); return (horizontal || vertical || leftDiagonal || rightDiagnal); } bool isWin(char Board[][CAPACITY], int dim, int winCount, char pSym) { for (int ri = 0; ri < dim; ri++) { for (int ci = 0; ci < dim; ci++) { if (isWinHere(Board, dim, winCount, pSym, ri, ci)) return true; } } return false; } bool isDraw(char Board[][CAPACITY], int dim) { for (int ri = 0; ri < dim; ri++) { for (int ci = 0; ci < dim; ci++) { if (Board[ri][ci] == '-') return false; } } return true; } void turnChange(int& turn) { turn = (turn + 1) % 2; } void HvsC(const char Board[][CAPACITY], int& ri, int& ci, int dim, char pSym, int winCount, char CS, char HS) { // creating an array for a new board char T[CAPACITY][CAPACITY]; // copying the symbols from the board 'Board' to board 'T' for (int r = 0; r < dim; r++) { for (int c = 0; c < dim; c++) { T[r][c] = Board[r][c]; } } for (int WC = winCount; WC > 1; WC--) { // Iterate over all positions in the board for (int r = 0; r < dim; r++) { for (int c = 0; c < dim; c++) { // If the position (r, c) is valid if (validInput(r, c, T, dim)) { // Place computer's symbol T[r][c] = CS; // If the computer wins if (isWinHere(T, dim, WC, pSym, r, c)) { // Remove computer's symbol from the board T[r][c] = '-'; // Return the computer's move coordinates ri = r; ci = c; return; } else { // Remove computer's symbol from the board T[r][c] = '-'; } } } } // Iterate over all positions in the board for (int r = 0; r < dim; r++) { for (int c = 0; c < dim; c++) { // If the position (r, c) is valid if (validInput(r, c, T, dim)) { // Place human's symbol T[r][c] = HS; // If the human wins if (isWinHere(T, dim, WC, pSym, r, c)) { // Remove human's symbol from the board T[r][c] = '-'; // Return the human's move coordinates ri = r; ci = c; return; } else { // Remove human's symbol from the board T[r][c] = '-'; } } } } } do { ri = rand() % dim; ci = rand() % dim; } while (!validInput(ri, ci, Board, dim)); } int main() { cout <<"\n\n\t\t\tThe Game of Gomoku\n\n"; cout <<"\n\n\t\t\t Let's begin!\n\n"<<endl; srand(time(0)); char choice; char Board[CAPACITY][CAPACITY]; char pName[2][30] = {"Human", "Computer"}; // "Human" will be replaced by the name of the player in init function char pSym[2]; int turn, ri, ci, dim, winCount; bool gameOver = false; int winner = -1; cout <<"Initializing..."<<endl << endl; init(Board, dim, pName, pSym, turn, winCount); do { printBoard(Board, dim); if (turn == HUMAN) { do { userInput(ri, ci, pName[turn], pSym[turn]); if (!validInput(ri, ci, Board, dim)) { cout << "Invalid Input" << endl; } } while (!validInput(ri, ci, Board, dim)); } else { cout << "Computer's turn..." << endl; sleep(3); HvsC(Board, ri, ci, dim, winCount, pSym[turn], pSym[COMPUTER], pSym[HUMAN]); } updateBoard(Board, dim, ri, ci, pSym[turn]); printBoard(Board, dim); gameOver = isWin(Board, dim, winCount, pSym[turn]); if (gameOver) winner = turn; if (!isDraw(Board, dim)) gameOver = true; if (!gameOver) turnChange(turn); } while (!gameOver); cout << endl; if (winner == -1) cout << "Game Draw!" << endl; else { cout << pName[turn] << " has won!" << endl; } return 0; }