-
Notifications
You must be signed in to change notification settings - Fork 2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
FEATURE - Negamax for Tic-Tac-Toe #1
Changes from 12 commits
0a6dc61
8f29e43
17da5fe
3e2481e
ac2dc7a
5e47cdf
f0be36c
5a7f778
07ce6aa
3dc4b46
57fb572
4fd14f3
434dc45
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
#include <math.h> | ||
|
||
namespace BitMath | ||
{ | ||
int reverseBits(const int value, int maxBits) | ||
{ | ||
int reversedBits = 0; | ||
maxBits = (maxBits % 2 == 0) ? maxBits : maxBits - 1; | ||
int halfBits = floor(maxBits / 2); | ||
for (int bit = 0; bit < halfBits; bit++) | ||
{ | ||
int transposeDifference = (maxBits - (bit * 2) - 1); | ||
|
||
int rBit = value & (1 << bit); | ||
int lBit = rBit << transposeDifference; | ||
reversedBits |= lBit; | ||
|
||
lBit = value & (1 << (maxBits - 1 - bit)); | ||
rBit = lBit >> transposeDifference; | ||
reversedBits |= rBit; | ||
} | ||
return reversedBits; | ||
}; | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
#include "MinimaxBenchmarker.h" | ||
#include "BitMath.h" | ||
|
||
int main() | ||
{ | ||
TicTacToeMinimax* minimax = new TicTacToeMinimax(); | ||
MinimaxBenchmarker* benchmarker = new MinimaxBenchmarker(); | ||
|
||
benchmarker -> benchmarkMinimaxVsMinimax(minimax, 0, true); | ||
benchmarker -> benchmarkEvaluate(minimax, 0, true); | ||
benchmarker -> benchmarkEvaluate(minimax, 0b100000110000100011, true); | ||
benchmarker -> benchmarkEvaluate(minimax, 0b100000110011100011, false); | ||
benchmarker -> benchmarkEvaluate(minimax, 0b100000110011101011, true); | ||
|
||
return 0; | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
#include "MinimaxBenchmarker.h" | ||
|
||
void MinimaxBenchmarker::benchmarkMinimaxVsMinimax(TicTacToeMinimax* ticTacToeMinimax, int board, bool isMaxTurn) | ||
{ | ||
auto start = chrono::high_resolution_clock::now(); | ||
int firstBoard = board; | ||
|
||
srand (time(NULL)); | ||
|
||
int currentBoard = firstBoard; | ||
int nextBoard; | ||
|
||
int state = ticTacToeMinimax -> getState(currentBoard); | ||
while (state == Playing) | ||
{ | ||
vector<int> bestMoves = ticTacToeMinimax -> evaluateAll(currentBoard, isMaxTurn); | ||
isMaxTurn = !isMaxTurn; | ||
|
||
int randomIndex = rand() % bestMoves.size(); | ||
currentBoard = bestMoves[randomIndex]; | ||
|
||
state = ticTacToeMinimax -> getState(currentBoard); | ||
assert(state == Playing || state == Draw); | ||
} | ||
|
||
nextBoard = currentBoard; | ||
auto finish = chrono::high_resolution_clock::now(); | ||
|
||
printBoard(firstBoard); | ||
printBoard(nextBoard); | ||
printState(ticTacToeMinimax -> getState(nextBoard)); | ||
|
||
auto milliseconds = chrono::duration_cast<chrono::milliseconds>(finish - start); | ||
auto nanoseconds = chrono::duration_cast<chrono::nanoseconds>(finish - start); | ||
cout << "benchmarkMinimaxVsMinimax: " << milliseconds.count() << "ms / " << nanoseconds.count() << "ns" << endl; | ||
cout << endl << endl; | ||
}; | ||
|
||
void MinimaxBenchmarker::benchmarkEvaluate(TicTacToeMinimax* ticTacToeMinimax, int board, bool isMaxTurn) | ||
{ | ||
auto start = chrono::high_resolution_clock::now(); | ||
int nextBoard = ticTacToeMinimax -> evaluate(board, isMaxTurn); | ||
auto finish = chrono::high_resolution_clock::now(); | ||
auto milliseconds = chrono::duration_cast<chrono::milliseconds>(finish - start); | ||
auto nanoseconds = chrono::duration_cast<chrono::nanoseconds>(finish - start); | ||
|
||
printBoard(board); | ||
printBoard(nextBoard); | ||
cout << endl << endl; | ||
|
||
cout << "benchmarkEvaluate: " << milliseconds.count() << "ms / " << nanoseconds.count() << "ns" << endl; | ||
}; | ||
|
||
void MinimaxBenchmarker::benchmarkEvaluateAll(TicTacToeMinimax* ticTacToeMinimax, int board, bool isMaxTurn) | ||
{ | ||
auto start = chrono::high_resolution_clock::now(); | ||
vector<int> bestBoards = ticTacToeMinimax -> evaluateAll(board, isMaxTurn); | ||
auto finish = chrono::high_resolution_clock::now(); | ||
|
||
printBoard(board); | ||
for (int x = 0; x < bestBoards.size(); x++) | ||
{ | ||
printBoard(bestBoards[x]); | ||
} | ||
|
||
auto milliseconds = chrono::duration_cast<chrono::milliseconds>(finish - start); | ||
auto nanoseconds = chrono::duration_cast<chrono::nanoseconds>(finish - start); | ||
cout << "benchmarkEvaluateAll: " << milliseconds.count() << "ms / " << nanoseconds.count() << "ns" << endl; | ||
cout << endl << endl; | ||
}; | ||
|
||
void MinimaxBenchmarker::printBoard(int board) | ||
{ | ||
int crossMask = 3; | ||
int circleMask = 2; | ||
|
||
cout << endl; | ||
for (int x = 0; x < 9; x++) | ||
{ | ||
if (x > 0 && x % 3 == 0) cout << endl; | ||
|
||
if ((board & crossMask) == 0) | ||
{ | ||
cout << "[ ]"; | ||
} | ||
else | ||
{ | ||
if ((board & crossMask) == crossMask) cout << "[X]"; | ||
else cout << "[O]"; | ||
} | ||
|
||
crossMask <<= 2; | ||
circleMask <<= 2; | ||
} | ||
cout << endl; | ||
}; | ||
|
||
void MinimaxBenchmarker::printState(int state) | ||
{ | ||
if (state == Playing) cout << "Playing" << endl; | ||
else if (state == Draw) cout << "Draw" << endl; | ||
else if (state == CrossWins) cout << "CrossWins" << endl; | ||
else cout << "CircleWins" << endl; | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
#include <vector> | ||
#include <random> | ||
#include <time.h> | ||
#include <iostream> | ||
#include <stdio.h> | ||
#include <chrono> | ||
#include "TicTacToeMinimax.h" | ||
|
||
using namespace std; | ||
|
||
class MinimaxBenchmarker | ||
{ | ||
public: | ||
void benchmarkMinimaxVsMinimax(TicTacToeMinimax* ticTacToeMinimax, int board, bool isMaxTurn); | ||
void benchmarkEvaluate(TicTacToeMinimax* ticTacToeMinimax, int board, bool isMaxTurn); | ||
void benchmarkEvaluateAll(TicTacToeMinimax* ticTacToeMinimax, int board, bool isMaxTurn); | ||
void printBoard(int board); | ||
void printState(int state); | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,203 @@ | ||
#include "TicTacToeMinimax.h" | ||
|
||
const int DrawBoardState = 0b101010101010101010; | ||
|
||
const int winnerMasks[8] = | ||
{ | ||
0b101010000000000000, | ||
0b000000101010000000, | ||
0b000000000000101010, | ||
0b100000100000100000, | ||
0b001000001000001000, | ||
0b000010000010000010, | ||
0b100000001000000010, | ||
0b000010001000100000 | ||
}; | ||
|
||
int TicTacToeMinimax::evaluate(int board, bool isMaxTurn) | ||
{ | ||
return isMaxTurn ? evaluateMaxDecision(board) | ||
: evaluateMinDecision(board); | ||
}; | ||
|
||
std::vector<int> TicTacToeMinimax::evaluateAll(int board, bool isMaxTurn) | ||
{ | ||
return isMaxTurn ? evaluateMaxDecisions(board) | ||
: evaluateMinDecisions(board); | ||
}; | ||
|
||
std::vector<int> TicTacToeMinimax::evaluateMaxDecisions(int board) | ||
{ | ||
std::vector<int> bestMoves; | ||
int bestMoveScore = INT_MIN; | ||
int mask = 3; | ||
|
||
for (int x = 0; x < 9; x++) | ||
{ | ||
if ((board & mask) == 0) | ||
{ | ||
int newBoard = (board | mask); | ||
int score = getMinTreeScore(newBoard, -100, 100); | ||
|
||
if (score > bestMoveScore) | ||
{ | ||
bestMoveScore = score; | ||
|
||
bestMoves.clear(); | ||
bestMoves.push_back(newBoard); | ||
} | ||
else if (score == bestMoveScore) | ||
{ | ||
bestMoves.push_back(newBoard); | ||
} | ||
} | ||
mask <<= 2; | ||
} | ||
return bestMoves; | ||
}; | ||
|
||
std::vector<int> TicTacToeMinimax::evaluateMinDecisions(int board) | ||
{ | ||
std::vector<int> bestMoves; | ||
int bestMoveScore = INT_MAX; | ||
int mask = 2; | ||
|
||
for (int x = 0; x < 9; x++) | ||
{ | ||
if ((board & mask) == 0) | ||
{ | ||
int newBoard = (board | mask); | ||
int score = getMaxTreeScore(newBoard, -100, 100); | ||
|
||
if (score < bestMoveScore) | ||
{ | ||
bestMoveScore = score; | ||
|
||
bestMoves.clear(); | ||
bestMoves.push_back(newBoard); | ||
} | ||
else if (score == bestMoveScore) | ||
{ | ||
bestMoves.push_back(newBoard); | ||
} | ||
} | ||
mask <<= 2; | ||
} | ||
return bestMoves; | ||
}; | ||
|
||
int TicTacToeMinimax::evaluateMaxDecision(int board) | ||
{ | ||
int bestMove = 0; | ||
int bestMoveScore = INT_MIN; | ||
int mask = 3; | ||
|
||
for (int x = 0; x < 9; x++) | ||
{ | ||
if ((board & mask) == 0) | ||
{ | ||
int score = getMinTreeScore(board | mask, -100, 100); | ||
|
||
if (score > bestMoveScore) | ||
{ | ||
bestMoveScore = score; | ||
bestMove = (board | mask); | ||
} | ||
} | ||
mask <<= 2; | ||
} | ||
return bestMove; | ||
}; | ||
|
||
int TicTacToeMinimax::evaluateMinDecision(int board) | ||
{ | ||
int bestMove = 0; | ||
int bestMoveScore = INT_MAX; | ||
int mask = 2; | ||
|
||
for (int x = 0; x < 9; x++) | ||
{ | ||
if ((board & mask) == 0) | ||
{ | ||
int score = getMaxTreeScore(board | mask, -100, 100); | ||
|
||
if (score < bestMoveScore) | ||
{ | ||
bestMoveScore = score; | ||
bestMove = (board | mask); | ||
} | ||
} | ||
mask <<= 2; | ||
} | ||
return bestMove; | ||
}; | ||
|
||
int TicTacToeMinimax::getMaxTreeScore(int board, int alpha, int beta) | ||
{ | ||
int state = getState(board); | ||
if (state != Playing) return getScore(state); | ||
|
||
int bestScore = INT_MIN; | ||
int mask = 3; | ||
|
||
for (int x = 0; x < 9; x++) | ||
{ | ||
if ((board & mask) == 0) | ||
{ | ||
int score = getMinTreeScore(board | mask, alpha, beta); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I suggest make this bitshift operation a constant variable inside the loop so it is easier to debug its value |
||
if (score >= beta) return score; | ||
alpha = fmax(alpha, score); | ||
bestScore = fmax(bestScore, score); | ||
} | ||
mask <<= 2; | ||
} | ||
return bestScore; | ||
}; | ||
|
||
int TicTacToeMinimax::getMinTreeScore(int board, int alpha, int beta) | ||
{ | ||
int state = getState(board); | ||
if (state != Playing) return getScore(state); | ||
|
||
int bestScore = INT_MAX; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. make it const. It can make the compiler use the raw value instead of a pointer to a variable |
||
int mask = 2; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. same thing here. Make it const There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can't make this field constant because we are using it on bitshift operations inside this method. |
||
|
||
for (int x = 0; x < 9; x++) | ||
{ | ||
if ((board & mask) == 0) | ||
{ | ||
int score = getMaxTreeScore(board | mask, alpha, beta); | ||
if (score <= alpha) return score; | ||
beta = fmin(beta, score); | ||
bestScore = fmin(bestScore, score); | ||
} | ||
mask <<= 2; | ||
} | ||
return bestScore; | ||
}; | ||
|
||
int TicTacToeMinimax::getState(int board) | ||
{ | ||
for(int x = 0; x < 8; x++) | ||
{ | ||
// check if the board represent a winner state | ||
int winnerMask = winnerMasks[x]; | ||
if ((winnerMask & board) != winnerMask) continue; | ||
|
||
// check if pieces are the same | ||
winnerMask = winnerMask >> 1; | ||
int piecesXor = (winnerMask ^ board) & winnerMask; | ||
|
||
if (piecesXor == 0) return CrossWins; | ||
if (piecesXor == winnerMask) return CircleWins; | ||
} | ||
if ((board & DrawBoardState) == DrawBoardState) return Draw; | ||
return Playing; | ||
}; | ||
|
||
int TicTacToeMinimax::getScore(int state) | ||
{ | ||
if (state == CrossWins) return 1; | ||
if (state == CircleWins) return -1; | ||
return 0; | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Make this a constant