在“類與對象”實訓課上,有一道附加題讓我們用 OOP 做一個的井字棋模擬程序,要求中電腦是隨機落子的,這樣顯然不是很優雅。回憶起以前學的對抗搜索(這里叫 MaxMin 算法),我繼續給游戲中的電腦一方寫了個 AI。由於井字棋游戲運算規模很小,大部分的剪枝手段變得比較雞肋,但以此為引搜索了一些資料,了解一些有趣的計算機博弈論知識,有機會再繼續探究一下。
極大極小(Minimax)算法
Minimax算法 又名極小化極大算法,是一種找出失敗的最大可能性中的最小值的算法(即最小化對手的最大得益)。通常以遞歸形式來實現。
Minimax算法常用於棋類等由兩方較量的游戲和程序。該算法是一個零總和算法,即一方要在可選的選項中選擇將其優勢最大化的選擇,另一方則選擇令對手優勢最小化的一個,其輸贏的總和為0(有點像能量守恆,就像本身兩個玩家都有1點,最后輸家要將他的1點給贏家,但整體上還是總共有2點)。很多棋類游戲可以采取此算法,例如tic-tac-toe。
Alpha-beta 剪枝
Alpha-beta剪枝是一種搜索算法,用以減少極小化極大算法(Minimax算法)搜索樹的節點數。這是一種對抗性搜索算法,主要應用於機器游玩的二人游戲(如井字棋、象棋、圍棋)。當算法評估出某策略的后續走法比之前策略的還差時,就會停止計算該策略的后續發展。該算法和極小化極大算法所得結論相同,但剪去了不影響最終決定的分枝。
待改進的點:
-
判定勝利的方法比較蠢,可以用循環代替
-
沒有對搜索樹的預計深度進行評估,使 AI 做出最 “快” 的策略
-
代碼可讀性不強,不符合工程代碼規范
#include <bits/stdc++.h>
using namespace std;
class MyChess {
private:
char A[3][3];
int step;
public:
MyChess();
void DispChessboard();
void PlayerMove();
void ComputerMove();
bool isFull();
int MaxSearch();
int MinSearch();
int checkWin();
};
MyChess::MyChess() {
memset(A,0,sizeof(A));
step=0;
}
void MyChess::DispChessboard() {
cout<<"-------------------\n";
for (int i=0;i<3;i++) {
cout<<"| "<<A[i][0]<<" | "<<A[i][1]<<" | "<<A[i][2]<<" |\n";
cout<<"-------------------\n";
}
}
void MyChess::PlayerMove() {
int x,y;
cout<<"Step "<<++step<<" piece(x,y): ";
cin>>x>>y;
while (A[x][y] || x>2 || x<0 || y>2 || y<0) {
cout<<"ERROR!! piece(x,y): ";
cin>>x>>y;
}
A[x][y]='O';
}
void MyChess::ComputerMove() { // 模擬 AI 的選擇過程
int x,y,score=1;
for (int i=0;i<3;i++) {
for (int j=0;j<3;j++) {
if (!A[i][j]) {
A[i][j]='X';
int temp=MaxSearch();
if (score>temp) x=i,y=j,score=temp;
A[i][j]=0;
}
}
}
A[x][y]='X';
}
int MyChess::MaxSearch() { // 人類執子時,希望找到權值最大的子節點
int ret=-1,sta=checkWin();
if (sta) return sta;
if (isFull()) return 0;
for (int i=0;i<3;i++) for (int j=0;j<3;j++)
if (!A[i][j]) A[i][j]='O',ret=max(ret,MinSearch()),A[i][j]=0;
return ret;
}
int MyChess::MinSearch() { // 電腦執子時,希望找找到權值最小的子節點
int ret=1,sta=checkWin();
if (sta) return sta;
if (isFull()) return 0;
for (int i=0;i<3;i++) for (int j=0;j<3;j++)
if (!A[i][j]) A[i][j]='X',ret=min(ret,MaxSearch()),A[i][j]=0;
return ret;
}
bool MyChess::isFull() { // 平局判定
for (int i=0;i<3;i++)
for (int j=0;j<3;j++)
if (!A[i][j]) return false;
return true;
}
int MyChess::checkWin() {
for (int i=0;i<3;i++) if (A[i][0]=='O' && A[i][1]=='O' && A[i][2]=='O') return 1;
for (int i=0;i<3;i++) if (A[0][i]=='O' && A[1][i]=='O' && A[2][i]=='O') return 1;
for (int i=0;i<3;i++) if (A[i][0]=='X' && A[i][1]=='X' && A[i][2]=='X') return -1;
for (int i=0;i<3;i++) if (A[0][i]=='X' && A[1][i]=='X' && A[2][i]=='X') return -1;
if (A[0][0]=='O' && A[1][1]=='O' && A[2][2]=='O') return 1;
if (A[2][0]=='O' && A[1][1]=='O' && A[0][2]=='O') return 1;
if (A[0][0]=='X' && A[1][1]=='X' && A[2][2]=='X') return -1;
if (A[2][0]=='X' && A[1][1]=='X' && A[0][2]=='X') return -1;
return 0;
}
int main() {
MyChess Ch; int now=1;
Ch.DispChessboard();
while (!Ch.isFull()) {
if (now&1) {
Ch.PlayerMove();
if (Ch.checkWin()==1) { Ch.DispChessboard(),cout<<"You win!\n"; break; }
else if (Ch.isFull()) { Ch.DispChessboard(),cout<<"Gme tie!\n"; break; }
}
else {
Ch.ComputerMove();
Ch.DispChessboard();
if (Ch.checkWin()==-1) { cout<<"Computer win!\n"; break; }
}
now^=1;
}
return 0;
}