一字棋指的是:在一個九宮格內率先連成三個字的取勝
首先,基於前面決策樹的講解 博弈的棋類遊戲等等 只要找到合適的估值函數都可以使用博弈樹來實現 下面我們來使用博弈樹完成一字棋的算法。
根據前面的算法思想我們算法大致分爲幾步:
1.對棋局落子有正確的估值
2.通過遍歷建立博弈樹
3.對博弈樹進行α-β剪枝增快查找速度(這裏由於數據量較小 放在最後一起講解)
4.根據極大值 極小值搜索獲取博弈樹產生的結果
首先在我們假設電腦先走 這時通過
在博弈樹中通過這段代碼在open表中將所有棋局儲存 並且打分
for(int i=0;i<3;i++)
for(int j=0;j<3;j++)
if(closed[closedtop]->all[i][j]==N) //在firstox中已經將棋局傳入
{
opentop++;//新節點於open表入隊
open[opentop]=new OXchess;//開闢空間
open[opentop]->should(closed[closedtop],closedtop,i,j,step);//對新節點進行合適的操作
}
在經歷完一輪循環後 將所有的open的元素 入closed表 (此時closed中的所有元素 flag = 1)
也就是記錄了計算機所有的落子位置 下面進行第二次循環 同理完成人類落子 。。。(可以一直模擬下去 但是隨着模擬的回合增加 由於估值函數不可能完全擬合 會產生過擬合的現象)
並且如果完全模擬完所有的情況有9! = 362 880種情況
此時編譯器也會報錯 這裏我們將博弈樹深度設爲5
會發現 AI出現了問題 由於擬合層數過多 但是估值函數較爲粗糙 導致隨着擬合次數的增加 會使估值的誤差也快速上漲 導致出現過擬合的問題
我們可以通過調整估值代碼的參數來完成更多層數的擬合 通過觀察可以發現這個問題是 對於我馬上要形成三個棋子並且下一步是我下棋的估值過低 導致在不斷的迭代過程中 自己形成二子的得分甚至高於防止對手形成三子 對於這種情況就要提高估值 在這個遊戲中往後看兩步足以取勝 我們選擇迭代次數爲2 也就是flagmax = 2
void toscore()//得到當前局面score值的函數
{
score=0;//score復位爲0
int i,j,o,x,n;//i.j循環用,o,x,n分別代表某一路(連續三子爲一路)上oxn棋子的數目
for(i=0;i<3;i++)//橫向
{
o=0;x=0;n=0;//每一路之前要復位置零
for(j=0;j<3;j++)
{
if(all[i][j]==O)//o計數
o++;
if(all[i][j]==X)//x計數
x++;
if(all[i][j]==N)//n計數
n++;
if(o+n==3)//當這一路上只有O棋子與空棋子時
{
if(o==3)//O有3子
score=score+999999;//這種棋面已贏,評估值無限大
if(o==2)//O有2子
{
if(flag%2==1)//如果這種棋面的層數是奇數,說明下一步是計算機下O棋,當某一路上已有2個O子時,已經必勝
//評估值很大,但要小於對方已贏棋面評估值的絕對值,否則會產生不去圍堵對方的勝招,而自顧自做2連子的棋面
score=score+20000;
else//如果下一步是人類下棋,這種局面的評估值不是很大
score=score+1000;
}
if(o==1)//O有1子
score=score+100;//加一點評估值
}
if(x+n==3)//當這一路上只有X棋子與空棋子時
{
if(x==3)//X有3子
score=score-99999;//人類已經贏的棋面,評估值無限小
//但絕對值要小於計算機已贏棋面的絕對值,否則會產生明明自己走在某處就可以直接勝利還偏偏去圍堵對方的勝招的情況
if(x==2)//X有2子
{
if(flag%2==0)//如果下一步是人類下棋,評估值很小
score=score-10000;
else//如果下一步是計算機下棋,評估值不是很小
score=score-1000;
}
if(x==1)//X有1子
score=score-100;//減一點評估值
}
//此處沒有寫oxn都有的情況 因爲這種情況 這一條路 誰也贏不了
}
}
for(i=0;i<3;i++)//豎向,下面同上
{
o=0;x=0;n=0;
for(j=0;j<3;j++)
{
if(all[j][i]==O)
o++;
if(all[j][i]==X)
x++;
if(all[j][i]==N)
n++;
if(o+n==3)
{
if(o==3)
score=score+999999;
if(o==2)
{
if(flag%2==1)
score=score+20000;
else
score=score+1000;
}
if(o==1)
score=score+100;
}
if(x+n==3)
{
if(x==3)
score=score-99999;
if(x==2)
{
if(flag%2==0)
score=score-10000;
else
score=score-1000;
}
if(x==1)
score=score-100;
}
}
}
o=0;x=0;n=0;
for(i=0;i<3;i++)//左上——右下,下面同上
{
if(all[i][i]==O)
o++;
if(all[i][i]==X)
x++;
if(all[i][i]==N)
n++;
if(o+n==3)
{
if(o==3)
score=score+999999;
if(o==2)
{
if(flag%2==1)
score=score+20000;
else
score=score+1000;
}
if(o==1)
score=score+100;
}
if(x+n==3)
{
if(x==3)
score=score-99999;
if(x==2)
{
if(flag%2==0)
score=score-10000;
else
score=score-1000;
}
if(x==1)
score=score-100;
}
}
o=0;x=0;n=0;
for(i=0;i<3;i++)//右上——左下,下面同上
{
if(all[i][2-i]==O)
o++;
if(all[i][2-i]==X)
x++;
if(all[i][2-i]==N)
n++;
if(o+n==3)
{
if(o==3)
score=score+999999;
if(o==2)
{
if(flag%2==1)
score=score+20000;
else
score=score+1000;
}
if(o==1)
score=score+100;
}
if(x+n==3)
{
if(x==3)
score=score-99999;
if(x==2)
{
if(flag%2==0)
score=score-10000;
else
score=score-1000;
}
if(x==1)
score=score-100;
}
}
}
};
下面完成極大值極小值搜索 但是這裏的樹由於使用線性表實現 因此是一棵線性的樹
上圖是對比圖注意 不同的顏色代表不同的深度 於是下面使用極大極小值搜索 一個回合(人一次 計算機一次)完成一次替換
for(int lag=closedtop;lag>0;lag--)//從closed表棧頂處向下掃描,通過葉子節點的score值給整個博弈樹各節點的score賦值
{
if(closed[lag]->flag%2==0)//或節點 人類下棋時
{//或節點的父節點是與節點,與節點的score值取其子節點中最小的score值
if(closed[lag]->score<=closed[closed[lag]->parent]->score)//如果該節點的score值比其父節點的score值低
{
closed[closed[lag]->parent]->score=closed[lag]->score;//就把該節點的score值賦給父節點
}
}
else//與節點 電腦下棋時
{//與節點的父節點是或節點,或節點的score值取其子節點中最大的score值
if(closed[lag]->score>=closed[closed[lag]->parent]->score)//如果該節點的score值比其父節點的score值高
{
closed[closed[lag]->parent]->score=closed[lag]->score;//就把該節點的score值賦給父節點
if(closed[lag]->flag==1)//如果該節點是第一層節點,說明此時博弈樹選擇的第一層節點就是該節點
tag=lag;//記錄下該節點在closed表中的數組下標
}
}
}
最後附上所有代碼以供參考:
#include<iostream>
using namespace std;
enum Chess{O,X,N};//棋子的類型:O代表計算機,X代表人類,N代表空
enum Node{and,or};//棋盤狀態節點的類型:and代表與節點 我方下棋,or代表或節點 對手下棋
#define maxsize 9999//open和closed表的最大容量
#define flagmax 2//分析最高層數(也就是最多分析的回合),設爲2時AI出錯率爲0,設爲更高值時,會出現過擬合現象,導致擬合效果下降
class OXchess//棋盤狀態節點類,用於博弈樹搜索
{
public:
Chess all[3][3];//當前棋盤狀態
int play[2];//當前落子位置 play[0]爲x play[1]爲y
int parent;//當前父節點指針
int score;//當前棋盤狀態分數
int flag;//當前擴展層數
Node node;//當前節點類型
OXchess()//構造函數,用來初始化棋盤狀態節點
{
for(int i=0;i<3;i++)
for(int j=0;j<3;j++)
all[i][j]=N;//棋盤置空
play[0]=-1;play[1]=-1;//落子爲空
parent=-2;//父節點指針爲空
score=9999;//score初始值
flag=0;//flag初始值
node=or;//節點類型初始值
}
~OXchess(){}//析構函數
void should(OXchess *a,int closedtop,int x,int y,int step)//對每個新產生的節點應該做的操作的函數(落子、棋局打分)
{
for(int i=0;i<3;i++)//首先複製父節點的棋盤
for(int j=0;j<3;j++)
{
all[i][j]=a->all[i][j];
}
play[0]=x;play[1]=y;//然後根據參數存儲即將的落子
parent=closedtop;//確定父節點指針
flag=a->flag+1;//當前層數爲父節點+1
if(flag%2==0)//當該節點層數爲偶數時
{
score=-999999999;//score設爲無限小,方便與其子節點中score較大的值比較,並獲取其值
node=or;//節點類型爲或節點
playchess(x,y,X);//在落子座標放上人類的棋子
}
else//當該節點層數爲奇數時
{
score=999999999;//score設爲無限大,方便與其子節點中score較小的值比較,並獲取其值
node=and;//節點類型爲與節點
playchess(x,y,O);//在落子處座標放上計算機的棋子
}
if(flag==flagmax || step==8)//當該節點爲無法再擴展的節點時,掃描棋盤通過局面得到score值
{
toscore();//得到當前局面score值
}
}
void playchess(int x,int y,Chess z)//落子的函數
{
all[x][y]=z;//在座標x,y處落子z
}
void copyOX(OXchess *a)//複製同類對象的函數
{
for(int i=0;i<3;i++)
for(int j=0;j<3;j++)
all[i][j]=a->all[i][j];//複製棋盤
play[0]=a->play[0];play[1]=a->play[1];//複製落子位置
parent=a->parent;//複製父節點
score=a->score;//複製score
flag=a->flag;//複製層數
node=a->node;//複製節點類型
}
void toscore()//得到當前局面score值的函數
{
score=0;//score復位爲0
int i,j,o,x,n;//i.j循環用,o,x,n分別代表某一路(連續三子爲一路)上oxn棋子的數目
for(i=0;i<3;i++)//橫向
{
o=0;x=0;n=0;//每一路之前要復位置零
for(j=0;j<3;j++)
{
if(all[i][j]==O)//o計數
o++;
if(all[i][j]==X)//x計數
x++;
if(all[i][j]==N)//n計數
n++;
if(o+n==3)//當這一路上只有O棋子與空棋子時
{
if(o==3)//O有3子
score=score+999999;//這種棋面已贏,評估值無限大
if(o==2)//O有2子
{
if(flag%2==1)//如果這種棋面的層數是奇數,說明下一步是計算機下O棋,當某一路上已有2個O子時,已經必勝
//評估值很大,但要小於對方已贏棋面評估值的絕對值,否則會產生不去圍堵對方的勝招,而自顧自做2連子的棋面
score=score+20000;
else//如果下一步是人類下棋,這種局面的評估值不是很大
score=score+1000;
}
if(o==1)//O有1子
score=score+100;//加一點評估值
}
if(x+n==3)//當這一路上只有X棋子與空棋子時
{
if(x==3)//X有3子
score=score-99999;//人類已經贏的棋面,評估值無限小
//但絕對值要小於計算機已贏棋面的絕對值,否則會產生明明自己走在某處就可以直接勝利還偏偏去圍堵對方的勝招的情況
if(x==2)//X有2子
{
if(flag%2==0)//如果下一步是人類下棋,評估值很小
score=score-10000;
else//如果下一步是計算機下棋,評估值不是很小
score=score-1000;
}
if(x==1)//X有1子
score=score-100;//減一點評估值
}
//此處沒有寫oxn都有的情況 因爲這種情況 這一條路 誰也贏不了
}
}
for(i=0;i<3;i++)//豎向,下面同上
{
o=0;x=0;n=0;
for(j=0;j<3;j++)
{
if(all[j][i]==O)
o++;
if(all[j][i]==X)
x++;
if(all[j][i]==N)
n++;
if(o+n==3)
{
if(o==3)
score=score+999999;
if(o==2)
{
if(flag%2==1)
score=score+20000;
else
score=score+1000;
}
if(o==1)
score=score+100;
}
if(x+n==3)
{
if(x==3)
score=score-99999;
if(x==2)
{
if(flag%2==0)
score=score-10000;
else
score=score-1000;
}
if(x==1)
score=score-100;
}
}
}
o=0;x=0;n=0;
for(i=0;i<3;i++)//左上——右下,下面同上
{
if(all[i][i]==O)
o++;
if(all[i][i]==X)
x++;
if(all[i][i]==N)
n++;
if(o+n==3)
{
if(o==3)
score=score+999999;
if(o==2)
{
if(flag%2==1)
score=score+20000;
else
score=score+1000;
}
if(o==1)
score=score+100;
}
if(x+n==3)
{
if(x==3)
score=score-99999;
if(x==2)
{
if(flag%2==0)
score=score-10000;
else
score=score-1000;
}
if(x==1)
score=score-100;
}
}
o=0;x=0;n=0;
for(i=0;i<3;i++)//右上——左下,下面同上
{
if(all[i][2-i]==O)
o++;
if(all[i][2-i]==X)
x++;
if(all[i][2-i]==N)
n++;
if(o+n==3)
{
if(o==3)
score=score+999999;
if(o==2)
{
if(flag%2==1)
score=score+20000;
else
score=score+1000;
}
if(o==1)
score=score+100;
}
if(x+n==3)
{
if(x==3)
score=score-99999;
if(x==2)
{
if(flag%2==0)
score=score-10000;
else
score=score-1000;
}
if(x==1)
score=score-100;
}
}
}
};
class OX//遊戲類實體
{
public:
Chess chess[3][3];//記錄當前棋盤狀態
int step;//記錄當前已走步數
Chess wholast;//記錄上一步走棋的是誰
bool ifend;//記錄遊戲是否結束
bool winner;//記錄遊戲是否有勝者產生
bool humanfirst;//記錄是人先走還是計算機先走
OX()//構造函數,初始化遊戲
{
char command,del;//command用來接收指令 a.人先走棋 b.電腦先走 。 del用來吸收換行符。
for(int i=0;i<3;i++)
for(int j=0;j<3;j++)
chess[i][j]=N;//初始化棋盤
wholast=N;//初始化wholast
step=0;//初始化step
ifend=false;//初始化ifend
winner=false;//初始化winner
cout<<"一字棋啓動ing......"<<endl<<"請選擇(a.人先走棋 b.電腦先走):"<<endl<<"->";
command=getchar();//獲取指令
del=getchar();//吸收換行符
while(command!='a' && command!='b')//指令輸錯的時候要求玩家重新輸
{
cout<<"小老弟走點心,按着我說的來,請重新選擇(a.人先走棋 b.電腦先走):"<<endl<<"->";
command=getchar();
del=getchar();
}
if(command=='a')//初始化humanfirst
humanfirst=true;
else
humanfirst=false;
};
~OX()//析構函數,在遊戲結束時 輸出棋盤 然後 輸出遊戲結果
{
display();//輸出棋盤
if(winner)//存在勝者時輸出誰贏了
{
if(wholast==O)
cout<<"我贏了!人類被我打敗了 啊哈哈哈"<<endl;
if(wholast==X)
cout<<"人類竟然能打敗我 這這這不可能..."<<endl;
}
else//不存在勝者時輸出平局
cout<<"咋們水平差不多,人類也就如此"<<endl;
}
void dis(Chess x)//輸出棋子的函數
{//保證棋盤中先走的人的棋子顯示的是O,後走的人的棋子顯示的是X(當計算機先走時,輸出結果與棋盤存儲結果相同,人先走時則相反)
if(x==O)
{
if(humanfirst==true)
cout<<"X";
else
cout<<"O";
}
if(x==X)
{
if(humanfirst==true)
cout<<"O";
else
cout<<"X";
}
if(x==N)//棋子爲空時輸出空格
cout<<" ";
cout<<" ";
}
void display()//輸出棋盤的函數
{
cout<<endl
<<" 1 2 3"<<endl
<<" ┏━┳━┳━┓"<<endl
<<"1┃";dis(chess[0][0]);cout<<"┃";dis(chess[0][1]);cout<<"┃";dis(chess[0][2]);cout<<"┃"<<endl
<<" ┣━╋━╋━┫"<<endl
<<"2┃";dis(chess[1][0]);cout<<"┃";dis(chess[1][1]);cout<<"┃";dis(chess[1][2]);cout<<"┃"<<endl
<<" ┣━╋━╋━┫"<<endl
<<"3┃";dis(chess[2][0]);cout<<"┃";dis(chess[2][1]);cout<<"┃";dis(chess[2][2]);cout<<"┃"<<endl
<<" ┗━┻━┻━┛"<<endl<<endl;
}
void play(int x,int y,Chess z)//下棋的函數,於棋盤chess[x][y]處下子z
{
chess[x][y]=z;
}
bool havewinner()//判斷wholast是否獲得了勝利
{
int i,j,z;//i,j用來循環,z用來記錄一條線上wholast棋子的數目
for(i=0;i<3;i++)//橫向
{
z=0;
for(j=0;j<3;j++)
{
if(chess[i][j]==wholast)
z++;
if(z==3)
return true;
}
}
for(i=0;i<3;i++)//豎向
{
z=0;
for(j=0;j<3;j++)
{
if(chess[j][i]==wholast)
z++;
if(z==3)
return true;
}
}
z=0;
for(i=0;i<3;i++)//左上——右下
{
if(chess[i][i]==wholast)
z++;
if(z==3)
return true;
}
z=0;
for(i=0;i<3;i++)//右上——左下
{
if(chess[i][2-i]==wholast)
z++;
if(z==3)
return true;
}
return false;
}
void getend()//每次有計算機或玩家下子後即判斷當前局勢
{
bool x=havewinner();//判斷下子的人是否贏了
if(x)//如果贏了
{
ifend=true;//ifend置true,代表遊戲已結束
winner=true;//winner置true,代表產生勝者
}
if(step==9)//棋盤已滿
ifend=true;//ifend置true,代表遊戲已結束,此時若winner保持初始值false,則代表遊戲是平局
}
void humanplay()//人類下棋的函數
{
int x,y;//用來記錄步子,x爲縱座標,y爲橫座標
display();//輸出當前棋盤狀態給玩家看
cout<<"小老弟讓我看看你的高招:(例:若要下在棋盤中橫座標爲1,縱座標爲3的位置,請輸入:1+空格+3即:1 3)"<<endl<<"->";
cin>>y>>x;//輸入指令
while(!(y>0 && x>0 && y<4 && x<4 && chess[x-1][y-1]==N) )//指令輸錯的時候要求玩家重新輸
{
cout<<"你怎麼不按規則和我下棋啊!:(例:若要下在棋盤中橫座標爲1,縱座標爲3的位置,請輸入:1+空格+3即:1 3)"<<endl<<"->";
cin>>y>>x;
}
x--;y--;//棋盤座標與數組座標的變換
play(x,y,X);//人類下棋
wholast=X;//wholast置X,代表最近一步棋是人類下的
step++;//已走步數+1
}
void computerplay()//計算機下棋的函數
{
int x[2]={-1,-1};//保存棋招
minmax(x);//極大極小法獲取棋招 傳入的是數組首地址
play(x[0],x[1],O);//按棋招下棋
cout<<endl<<"計算機下在了位置:"<<x[1]+1<<" "<<x[0]+1<<endl;//輸出下子的位置
wholast=O;//wholast置O,代表最近一步棋是計算機下的
step++;//已走步數+1
}
void minmax(int a[])//構建博弈樹 並利用極大極小值算法獲取結果
{
OXchess *open[maxsize],*closed[maxsize];//建立open表closed表
int opentop=-1,openrear=-1,closedtop=-1,tag=-1;//open表爲隊列,closed表爲棧,tag用於記錄博弈樹最終所選擇的第一層節點
opentop++;//當前局面入隊,作爲open表的第一個節點
open[opentop]=new OXchess;//開闢空間
firstOX(open[opentop]);//open表生成這次博弈樹的第一個節點
while(opentop!=openrear)//當open表不爲空的時候
{
closedtop++;openrear++;//open表隊尾元素出隊,然後進入closed表入棧
closed[closedtop]=new OXchess;//新節點開闢空間
closed[closedtop]->copyOX(open[openrear]);//把元素複製過去
free(open[openrear]);//再回收舊空間
//只執行一次將所有可以落子的位置 遍歷打分
if(closed[closedtop]->flag<flagmax && step<9)//當棋盤未滿,並且被考察節點的層數未到達最大層數限制時,生成新節點
{
for(int i=0;i<3;i++)
for(int j=0;j<3;j++)
if(closed[closedtop]->all[i][j]==N) //在firstox中已經將棋局傳入
{
opentop++;//新節點於open表入隊
open[opentop]=new OXchess;//開闢空間
open[opentop]->should(closed[closedtop],closedtop,i,j,step);//對新節點進行合適的操作
}
}
}
//極大極小搜索
for(int lag=closedtop;lag>0;lag--)//從closed表棧頂處向下掃描,通過葉子節點的score值給整個博弈樹各節點的score賦值
{
if(closed[lag]->flag%2==0)//或節點 人類下棋時
{//或節點的父節點是與節點,與節點的score值取其子節點中最小的score值
if(closed[lag]->score<=closed[closed[lag]->parent]->score)//如果該節點的score值比其父節點的score值低
{
closed[closed[lag]->parent]->score=closed[lag]->score;//就把該節點的score值賦給父節點
}
}
else//與節點 電腦下棋時
{//與節點的父節點是或節點,或節點的score值取其子節點中最大的score值
if(closed[lag]->score>=closed[closed[lag]->parent]->score)//如果該節點的score值比其父節點的score值高
{
closed[closed[lag]->parent]->score=closed[lag]->score;//就把該節點的score值賦給父節點
if(closed[lag]->flag==1)//如果該節點是第一層節點,說明此時博弈樹選擇的第一層節點就是該節點
tag=lag;//記錄下該節點在closed表中的數組下標
}
}
}
a[0]=closed[tag]->play[0];//獲取最終博弈樹選擇的那個節點所存儲的棋招縱座標
a[1]=closed[tag]->play[1];//獲取最終博弈樹選擇的那個節點所存儲的棋招橫座標
}
void firstOX(OXchess *a)//open表生成第一個節點的函數
{
a->parent=-1;//parent置-1
for(int i=0;i<3;i++)
for(int j=0;j<3;j++)
a->all[i][j]=chess[i][j];// 從遊戲實例獲取棋盤局面
a->score=-999999999;//score置無限小
}
};
int main()//主函數
{
OX OX1;//定義遊戲實體,自動調用構造函數OX();初始化遊戲
if(OX1.humanfirst)//如果人先走
OX1.humanplay();//那就人走
else//如果計算機先走
OX1.computerplay();//那就計算機走
OX1.getend();//判斷局勢,判斷ifend和winner的值是否需要改變
while(!OX1.ifend)//如果遊戲沒結束
{
if(OX1.wholast==O)//如果上一步走棋的是計算機
OX1.humanplay();//那麼輪到人類走棋
else//如果上一步走棋的是人類
OX1.computerplay();//那麼輪到計算機走棋
OX1.getend();//判斷局勢,判斷ifend和winner的值是否需要改變
}//如果遊戲結束,退出程序時自動調用OX類的析構函數~OX();,輸出遊戲結果。
return 0;
}