# 五子棋AI程式碼
## 簡介
這段 C++ 程式碼是用來實現 五子棋(Gomoku) 遊戲,其中一方玩家用 'O' 代表,另一方 AI 用 'X' 代表。程式使用了像是 Minimax 算法、Alpha-Beta 剪枝、執行緒分工等技術來提升 AI 的決策效率。
## 程式結構概覽
1.Minimax 演算法:
* AI 使用 Minimax 演算法來評估可能的步驟,通過遞歸模擬 AI 和玩家的移動。
* AI 嘗試最大化自己的得分,同時最小化玩家贏得比賽的機會。
2.評分函數:
* evaluateSinglePosition 函數用來評估某一位置的得分,根據連續的棋子數量和開放式的可能性(即是否可以與相鄰的空位連接)來進行計算。
* 根據不同的情況(例如連成一行五子、準備四子或三子等)給予不同的啟發式評分。
3.Alpha-Beta 剪枝:
* 為了提升效率,程式使用 Alpha-Beta 剪枝,這樣可以減少需要遍歷的搜尋樹節點數量,只保留對結果有可能影響的分支。
4.多執行緒:
* 程式使用多執行緒技術,將可能的移動評估分為多個任務並行處理,這樣可以加速 AI 做決策的速度。
5.棋盤表示:
* 棋盤使用一個 2D 字元陣列表示,並且為了在 Minimax 搜尋過程中並行計算,維護多個棋盤副本來評估不同的狀態。
## Code
```cpp=
#include<bits/stdc++.h>
using namespace std;
#define mkp make_pair
typedef pair<int,int> Position;
constexpr int SIZE = 15;
constexpr char EMPTY = '.';
constexpr char PLAYER = 'O';
constexpr char AI = 'X';
typedef long long ll;
template<typename T>
using V = vector<T>;
int dx[8] = {1,-1,1,-1,0,0,-1,1};
int dy[8] = {0,0,1,-1,1,-1,1,-1};
const int threadCount = 8;
struct Game {
V<V<char>> board;
V<V<V<char>>> boardCopy;
int turnCount;
ll callCount;
clock_t startTime;
int minX, maxX;
int minY, maxY;
int playerValue(char c) {
return (c == PLAYER ? 2 : -1);
}
ll evaluateSinglePosition(int x, int y, int id) {
auto &brd = (id == -1 ? board : boardCopy[id]);
assert(brd[x][y] != EMPTY);
auto isAdjacentEmpty = [&](int x, int y) {
return isWithinBoard(x, y) && brd[x][y] == EMPTY;
};
ll sum = 0;
ll K[5] = {(ll)1e7, (ll)1e5, (ll)1e3, 10, 1};
ll factor = playerValue(brd[x][y]);
for(int dir = 0; dir < 8; dir += 2) {
int cnt1 = countConsecutiveStones(brd[x][y], dir, x, y, id);
int cnt2 = countConsecutiveStones(brd[x][y], dir + 1, x, y, id);
int islive1 = isAdjacentEmpty(x + (cnt1 + 1) * dx[dir], y + (cnt1 + 1) * dy[dir]);
int islive2 = isAdjacentEmpty(x + (cnt2 + 1) * dx[dir + 1], y + (cnt2 + 1) * dy[dir + 1]);
int total = cnt1 + cnt2 + 1;
if(total == 5) sum += K[0];
if(total == 4 && islive1 && islive2)
sum += K[1];
else if(total == 4 && (islive1 || islive2))
sum += K[2];
if(total == 3 && islive1 && islive2)
sum += K[2];
else if(total == 3 && (islive1 || islive2))
sum += K[3];
if(total == 2 && islive1 && islive2)
sum += K[3];
else if(total == 2 && (islive1 || islive2))
sum += K[4];
if(total == 1 && islive1 && islive2)
sum += K[4];
}
return sum * factor;
}
ll evaluateBoard() {
ll sum = 0;
for(int x = 0; x < SIZE; ++x)
for(int y = 0; y < SIZE; ++y) {
if(board[x][y] == EMPTY) continue;
sum += evaluateSinglePosition(x, y, -1);
}
return sum;
}
void placeStone(char player, int x, int y, int id) {
assert(isValidMove(x, y, id));
boardCopy[id][x][y] = player;
};
void removeStone(int x, int y, int id) {
boardCopy[id][x][y] = EMPTY;
}
// 1 player, 0 AI
ll minimaxSearch(char player, int depth, ll val, int id, ll alpha=-1e18, ll beta=1e18) {
callCount++;
if(depth == 0) {
return val;
}
if(player == PLAYER) {
for(int x = minX; x <= maxX; ++x) {
for(int y = minY; y <= maxY; ++y) {
if(boardCopy[id][x][y] != EMPTY) continue;
placeStone(player, x, y, id);
alpha = max(alpha, minimaxSearch(AI, depth - 1, val + evaluateSinglePosition(x, y, id), id, alpha, beta));
removeStone(x, y, id);
if(alpha >= beta) break;
}
if(alpha >= beta) break;
}
return alpha;
}
if(player == AI) {
for(int x = minX; x <= maxX; ++x) {
for(int y = minY; y <= maxY; ++y) {
if(boardCopy[id][x][y] != EMPTY) continue;
placeStone(player, x, y, id);
beta = min(beta, minimaxSearch(PLAYER, depth - 1, val + evaluateSinglePosition(x, y, id), id, alpha, beta));
removeStone(x, y, id);
if(alpha >= beta) break;
}
if(alpha >= beta) break;
}
return beta;
}
return -1;
}
int countConsecutiveStones(char player, int dir, int x, int y, int id) {
auto& brd = (id == -1 ? board : boardCopy[id]);
int nx = x + dx[dir];
int ny = y + dy[dir];
int cnt = 0;
while(isWithinBoard(nx, ny) && brd[nx][ny] == player) {
++cnt;
nx += dx[dir];
ny += dy[dir];
}
return cnt;
}
bool isWithinBoard(int x, int y) {
return 0 <= x && x < SIZE && 0 <= y && y < SIZE;
}
bool isWinningMove(char player, int x, int y) {
auto _countConsecutive = [&](int dir, int x, int y) {
return countConsecutiveStones(player, dir, x, y, -1);
};
for(int dir = 0; dir < 8; dir += 2) {
if(_countConsecutive(dir, x, y) + _countConsecutive(dir + 1, x, y) + 1 >= 5) return true;
}
return false;
}
bool isValidMove(int x, int y) {
return board[x][y] == EMPTY;
}
bool isValidMove(int x, int y, int id) {
return boardCopy[id][x][y] == EMPTY;
}
int processMove(char player, int x, int y) {
if(!isValidMove(x, y)) { return -1; }
board[x][y] = player;
if(isWinningMove(player, x, y)) return 2;
return 1;
}
void printBoard() {
for(int x = 0; x <= SIZE; ++x)
cout << setw(3) << x;
cout << '\n';
for(int y = SIZE - 1; y >= 0; --y) {
cout << setw(3) << y + 1;
for(int x = 0; x < SIZE; ++x)
cout << setw(3) << board[x][y];
cout << '\n';
}
cout << '\n';
}
int playRound() {
turnCount++;
printBoard();
char player = (turnCount % 2 ? PLAYER : AI);
int x, y;
auto input = [&]() {
cout << "Player " << turnCount % 2 << ' ' << '\n';
cout << "Put at (x,y): ";
cin >> x >> y;
--x, --y;
};
input();
int res = processMove(player, x, y);
while(res == -1) {
cout << "Invalid move\n";
input();
res = processMove(player, x, y);
}
if(res == 2) {
cout << "Player " << turnCount % 2 << " wins!\n";
return 0;
}
turnCount++;
ll originalValue = evaluateBoard();
callCount = 0;
startTime = clock();
minX = minY = 1e9;
maxX = maxY = -1e9;
auto updateBoardBounds = [&]() {
for(int x = 0; x < SIZE; ++x) {
for(int y = 0; y < SIZE; ++y) {
if(board[x][y] == EMPTY) continue;
minX = min(minX, x);
minY = min(minY, y);
maxX = max(maxX, x);
maxY = max(maxY, y);
}
}
maxX = min(SIZE - 1, maxX + 3);
minX = max(0, minX - 3);
minY = max(0, minY - 3);
maxY = min(SIZE - 1, maxY + 3);
};
updateBoardBounds();
for(auto &i : boardCopy)
i = board;
vector<Position> validMoves;
auto identifyValidMoves = [&]() {
for(int x = minX; x <= maxX; ++x) {
for(int y = minY; y <= maxY; ++y) {
if(board[x][y] != EMPTY) continue;
validMoves.push_back(mkp(x, y));
}
}
};
identifyValidMoves();
vector<vector<Position>> dividedTasks(threadCount);
auto divideTasks = [&]() {
int taskSize = (validMoves.size() + threadCount - 1) / threadCount;
for(int i = 0; i < threadCount; ++i) {
dividedTasks[i] =
vector<Position>(validMoves.begin() + i * taskSize,
validMoves.begin() + min((i + 1) * taskSize, (int)validMoves.size()));
}
};
divideTasks();
vector<array<ll, 3>> results;
auto evaluateMoveOptions = [&](int id) {
ll minValue = 1e18;
int bestX, bestY;
for(auto &[x, y] : dividedTasks[id]) {
placeStone(AI, x, y, id);
ll val = minimaxSearch(PLAYER, 3, originalValue + evaluateSinglePosition(x, y, id), id);
if(val < minValue) {
minValue = val;
bestX = x;
bestY = y;
}
removeStone(x, y, id);
}
results.push_back(array<ll, 3>{minValue, bestX, bestY});
};
vector<thread> threads;
for(int i = 0; i < threadCount; ++i) {
threads.push_back(thread(evaluateMoveOptions, i));
}
for(auto &th : threads) {
th.join();
}
auto findBestMove = [&]() {
ll minValue = 1e18;
int bestX, bestY;
for(auto &[val, x, y] : results) {
if(val < minValue) {
minValue = val;
bestX = x;
bestY = y;
}
}
return mkp(bestX, bestY);
};
auto [bestX, bestY] = findBestMove();
system("cls");
cout << "Call Count: " << callCount << '\n';
cout << "Time Used: " << (1.0L * clock() - startTime) / threadCount / CLOCKS_PER_SEC << '\n';
res = processMove(AI, bestX, bestY);
if(res == 2) {
cout << "Player " << turnCount % 2 << " wins!\n";
return 0;
}
return 1;
}
Game() {
board.assign(SIZE, V<char>(SIZE, EMPTY));
boardCopy.assign(threadCount, V<V<char>>());
turnCount = 0;
}
};
int main() {
Game game;
while(game.playRound()) {};
game.printBoard();
}
```