Last active
April 28, 2020 02:42
-
-
Save srishanbhattarai/bb50d0c3320aad24e9ba9be1b10a913e to your computer and use it in GitHub Desktop.
Tic Tac Toe AI using AB pruning
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Tic Tac Toe using Minmax search with Alpha Beta Pruning. | |
* | |
* Author: Srishan Bhattarai | |
* | |
* Compile: g++ -g --std=c++11 ttt.cc -o ttt | |
* | |
* Follow the "get_ai_move" function to see the algorithm in action, rest of | |
* the code is to get user input, render board into terminal, check for goal | |
* states etc. | |
*/ | |
#include <iostream> | |
#include <tuple> | |
#include <vector> | |
#define PAIR make_pair | |
using namespace std; | |
// bounds checked move typedef. | |
using Move = pair<int, int>; | |
// States the game can end up in. | |
enum Result { | |
HumanWins, | |
AiWins, | |
Draw, | |
Incomplete // <- if the game is still running, all other results except this | |
// is a terminal state | |
}; | |
// Board characters. | |
const char HUMAN = 'O'; | |
const char AI = 'X'; | |
const char EMPTY = '.'; | |
// Scores for alpha beta pruning | |
const int AI_SCORE = 1000; // ai tries to maximize | |
const int DRAW_SCORE = 0; | |
const int HUMAN_SCORE = -1000; // human tries to minimize | |
// Current game state, 3x3, initially all empty. | |
vector<vector<char>> state(3, vector<char>(3, EMPTY)); | |
// forward declaration. | |
Result eval_board_state(vector<vector<char>> = state); | |
// Prints the current game state (3x3 assumed) | |
void print_state() { | |
cout << endl; | |
cout << " " | |
<< " " | |
<< "0" | |
<< " " | |
<< "1" | |
<< " " | |
<< "2" | |
<< " " << endl; | |
cout << "0 " | |
<< " | " << state[0][0] << " | " << state[0][1] << " | " << state[0][2] | |
<< " | " << endl; | |
cout << " -------------" << endl; | |
cout << "1 " | |
<< " | " << state[1][0] << " | " << state[1][1] << " | " << state[1][2] | |
<< " | " << endl; | |
cout << " -------------" << endl; | |
cout << "2 " | |
<< " | " << state[2][0] << " | " << state[2][1] << " | " << state[2][2] | |
<< " | " << endl | |
<< endl; | |
} | |
// Checks if a piece can be placed at the coords specified by row and col. | |
bool is_valid_placement(const int row, const int col) { | |
bool inBounds = row >= 0 && row <= 2 && col >= 0 && col <= 2; | |
if (!inBounds) | |
return false; | |
// bounds ok. Check if occupied by someone else | |
return state[row][col] == EMPTY; | |
} | |
// Gets valid input from the user on next move to make. | |
Move get_user_move() { | |
while (true) { | |
int row, col; | |
cout << "Enter your move (e.g. 0 1): "; | |
cin >> row >> col; | |
if (!is_valid_placement(row, col)) { | |
cout << "Slot occupied or out of bounds; try again!" << endl << endl; | |
continue; | |
} | |
return PAIR(row, col); | |
} | |
// unreachable; but make compiler happy. | |
return PAIR(-1, -1); | |
} | |
// Get a list of all possible moves. | |
vector<Move> possible_moves(vector<vector<char>> state) { | |
vector<Move> moves; | |
for (int i = 0; i < 3; i++) { | |
for (int j = 0; j < 3; j++) { | |
if (state[i][j] == EMPTY) | |
moves.push_back(PAIR(i, j)); | |
} | |
} | |
return moves; | |
} | |
// turn: Player to run the search for. At the top level, this will always be AI, | |
// but due to recursion this might end up being called for HUMAN too. | |
// alpha,beta: usual alpha beta pruning params. | |
// depth: Recursion depth, defaults to zero. this factors into the score we | |
// assign to any board state. | |
// | |
// Returns the best score for the search, and a pair indicating the best move. | |
pair<int, Move> alpha_beta_search(vector<vector<char>> curr_state, char turn, | |
int alpha, int beta, int depth = 0) { | |
Result r = eval_board_state(curr_state); | |
/** | |
* Base cases, leaf node cases reached where someone has won, or a draw has | |
* been reached. | |
*/ | |
if (r == Result::HumanWins) | |
return PAIR(HUMAN_SCORE, PAIR(-1, -1)); | |
else if (r == Result::AiWins) | |
return PAIR(AI_SCORE, PAIR(-1, -1)); | |
else if (r == Result::Draw) | |
return PAIR(DRAW_SCORE, PAIR(-1, -1)); | |
// A board state that does not have a winner is encountered! (aka | |
// Result::Incomplete state) | |
int ideal_score = turn == AI ? HUMAN_SCORE : AI_SCORE; | |
// Get all possible moves | |
vector<Move> pmoves = possible_moves(curr_state); | |
Move bestMove = PAIR(-1, 1); | |
for (auto pmove : pmoves) { | |
// Simulate the possible move to happen, then recompute the score a/c to | |
// minmax for the other player. | |
curr_state[pmove.first][pmove.second] = turn; | |
// If current turn is Ai, then compute Human's score for next turn, | |
// otherwise it's humans turn so compute Ai score. | |
int score = alpha_beta_search(curr_state, turn == AI ? HUMAN : AI, alpha, | |
beta, depth + 1) | |
.first; | |
// Maximizing. | |
if (turn == AI) { | |
if (ideal_score < score) { | |
ideal_score = score - 10 * depth; | |
bestMove = pmove; | |
// compute new alpha | |
alpha = max(alpha, ideal_score); | |
// Undo the simulation. | |
curr_state[pmove.first][pmove.second] = EMPTY; | |
// Prune! | |
if (beta <= alpha) | |
break; | |
} | |
} else { | |
if (ideal_score > score) { | |
ideal_score = score + 10 * depth; | |
bestMove = pmove; | |
// compute new beta | |
beta = min(beta, ideal_score); | |
curr_state[pmove.first][pmove.second] = EMPTY; | |
// prune! | |
if (beta <= alpha) | |
break; | |
} | |
} | |
// Undo the simulation for cases where beta > alpha. | |
curr_state[pmove.first][pmove.second] = EMPTY; | |
} | |
return std::make_pair(ideal_score, bestMove); | |
} | |
// Gets the next Ai move. | |
pair<int, Move> get_ai_move() { | |
return alpha_beta_search(state, AI, HUMAN_SCORE, AI_SCORE); | |
} | |
// Checks if state is a terminal state. Works with current state by default. | |
Result eval_board_state(vector<vector<char>> state) { | |
// horizontal sequences that can win. | |
vector<vector<Move>> horizontals = { | |
vector<Move>{PAIR(0, 0), PAIR(0, 1), PAIR(0, 2)}, | |
vector<Move>{PAIR(1, 0), PAIR(1, 1), PAIR(1, 2)}, | |
vector<Move>{PAIR(2, 0), PAIR(2, 1), PAIR(2, 2)}}; | |
// vertical sequences that can win. | |
vector<vector<Move>> verticals = { | |
vector<Move>{PAIR(0, 0), PAIR(1, 0), PAIR(2, 0)}, | |
vector<Move>{PAIR(0, 1), PAIR(1, 1), PAIR(2, 1)}, | |
vector<Move>{PAIR(0, 2), PAIR(1, 2), PAIR(2, 2)}}; | |
// diagonal sequences that can win. | |
vector<vector<Move>> diagonals = { | |
vector<Move>{PAIR(0, 0), PAIR(1, 1), PAIR(2, 2)}, | |
vector<Move>{PAIR(2, 0), PAIR(1, 1), PAIR(0, 2)}}; | |
// Is atleast one state empty? Helps determine a draw state. | |
bool contains_empty = false; | |
// Can win horizontally, vertically or diagonally. | |
vector<vector<vector<Move>>> winning_seqs = {horizontals, verticals, | |
diagonals}; | |
for (auto ws : winning_seqs) { | |
for (auto seq : ws) { | |
auto m1 = seq[0]; | |
auto m2 = seq[1]; | |
auto m3 = seq[2]; | |
// Values at each position. | |
char m1v = state[m1.first][m1.second]; | |
char m2v = state[m2.first][m2.second]; | |
char m3v = state[m3.first][m3.second]; | |
// if all equal, someone has maybe won if they are not all empty. | |
if (m1v == m2v && m2v == m3v) { | |
if (m1v == HUMAN) | |
return Result::HumanWins; | |
else if (m1v == AI) | |
return Result::AiWins; | |
} | |
// if not, check if any of them was empty. | |
if (m1v == EMPTY || m2v == EMPTY || m3v == EMPTY) { | |
contains_empty = true; | |
} | |
} | |
} | |
// Went through all sequences, no winner! | |
// If we saw an empty space somewhere, it means there are more moves to make. | |
// Otherwise, all slots are full and there is no winner which is a draw. | |
if (!contains_empty) { | |
return Result::Draw; | |
} | |
return Result::Incomplete; | |
} | |
int main(int _argc, char **_argv) { | |
print_state(); | |
Result board_state = Result::Incomplete; | |
while (true) { | |
// Move human. | |
auto hmove = get_user_move(); | |
state[hmove.first][hmove.second] = HUMAN; | |
// Has a terminal state been reached with this human move? | |
board_state = eval_board_state(); | |
if (board_state != Result::Incomplete) { | |
print_state(); | |
break; | |
} | |
// Move Ai | |
auto next = get_ai_move(); | |
Move amove = next.second; | |
state[amove.first][amove.second] = AI; | |
cout << "Ai moved: (" << amove.first << ", " << amove.second << ")" << endl; | |
// Re-render | |
print_state(); | |
// Has a terminal state been reached with this Ai move? | |
board_state = eval_board_state(); | |
if (board_state != Result::Incomplete) | |
break; | |
} | |
// Print result. | |
switch (board_state) { | |
case Result::HumanWins: | |
cout << "Human has won! But, this should not have been possible :(" << endl; | |
break; | |
case Result::AiWins: | |
cout << "Ai has won, sorry human!" << endl; | |
break; | |
case Result::Draw: | |
cout << "Game has ended in a draw!" << endl; | |
break; | |
case Result::Incomplete: | |
cout << "There is a bug in the code; game should never end in an " | |
"incomplete state" | |
<< endl; | |
} | |
return 0; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment