我查找了大量的网上资料,结合自己编程实践,走了很多弯路,总结了一些关于五子棋ai的经验以供大家参考借鉴。
对于五子棋ai,大部分人想到的是做一个评估函数。这里的评估函数网上大概有两种,但是很多人会弄混淆。
第一种我称为K函数(只是一个代号,与第二种相区别),是对一个可走的空位子进行打分,如果ai白子落在这个空位置的分数越高,说明这个位置就越好,每次ai走棋就找到一个最好的空位置就行了。
第二种我称为F函数,是对现在的棋盘局面进行打分。 ai白子首先找所有可以走的空位置,模拟走了这个位置以后,用f函数进行局面评分,如果走了这样的一个空位置的得分越高,说明这个位置就越好,每次ai走棋就找这样一个分数最高的位置。
那么你可能就要问了,这两个评估函数有什么区别呢?区别非常大!如果你只是想实现一个只看一步的ai,那么你可以用K函数也可以用F函数。但是如果你想要实现基于博弈树的极大极小搜索和α-β剪枝算法的“聪明”ai,就只能用F函数,因为博弈树必须要对局面打分,而不是对位置打分。如果你明白了这一点,那么接下来就好办了。
怎么对局势有一个比较准确的估计呢?我查阅了大量资料。五子棋的局势无非就是对棋型个数和权重的统计。(对某一方而言的)
我分为8种:
1.连5或者长连
2.活4(有两个位置可以形成连5)
3.冲4(有一个位置可以形成连5)
还有其他情况,比如边界,总之再下一步(只能是1步而不是2步)就能连5的就是冲4。
4.活3(走一步可以形成活4)
5.眠3(走一步可以形成冲4)
6.活2(走一步可以形成活3):形状自行脑补
7.眠2(走一步可以形成眠3)
8.活1(走一步可以形成活2)
我用六元组(一条直线上连续六个位置的状态)来表示棋型,通过检查棋盘上所有六元组来得分。最容易漏掉的地方是边界,比如如果一个斜线只有5个位置也有可能连5、但六元组没考虑进来等等问题,办法是用一个更大的、包括了边界的数组保存棋盘和边界信息。
用一个棋型辨识数组保存所有棋型。
#define C_NONE 0//棋子:黑子,白子,无子
#define C_BLACK 1
#define C_WHITE 2
//棋型代号 下标 权重
#define OTHER 0//0,其他棋型不考虑
#define WIN 1//100000,白赢
#define LOSE 2//-10000000
#define FLEX4 3//50000,白活4
#define flex4 4//-80000
#define BLOCK4 5//400
#define block4 6//-80000
#define FLEX3 7//400
#define flex3 8//-8000
#define BLOCK3 9//20
#define block3 10//-40
#define FLEX2 11//20
#define flex2 12//-40
#define BLOCK2 13//1
#define block2 14//-2
#define FLEX1 15//1
#define flex1 16//-2
int tuple6type[4][4][4][4][4][4];//棋型辨识数组,0无子,1黑子,2白子,3边界
需要对这个数组进行初始化。
void chessAi::init_tuple6type(){
memset(tuple6type,0,sizeof (tuple6type));//全部设为0
//白连5,ai赢
tuple6type[2][2][2][2][2][2]=WIN;
tuple6type[2][2][2][2][2][0]=WIN;
tuple6type[0][2][2][2][2][2]=WIN;
tuple6type[2][2][2][2][2][1]=WIN;
tuple6type[1][2][2][2][2][2]=WIN;
tuple6type[3][2][2][2][2][2]=WIN;//边界考虑
tuple6type[2][2][2][2][2][3]=WIN;
//黑连5,ai输
tuple6type[1][1][1][1][1][1]=LOSE;
tuple6type[1][1][1][1][1][0]=LOSE;
tuple6type[0][1][1][1][1][1]=LOSE;
tuple6type[1][1][1][1][1][2]=LOSE;
tuple6type[2][1][1][1][1][1]=LOSE;
tuple6type[3][1][1][1][1][1]=LOSE;
tuple6type[1][1][1][1][1][3]=LOSE;
//白活4
tuple6type[0][2][2][2][2][0]=FLEX4;
//黑活4
tuple6type[0][1][1][1][1][0]=flex4;
//白活3
tuple6type[0][2][2][2][0][0]=FLEX3;
tuple6type[0][0][2][2][2][0]=FLEX3;
tuple6type[0][2][0][2][2][0]=FLEX3;
tuple6type[0][2][2][0][2][0]=FLEX3;
//黑活3
tuple6type[0][1][1][1][0][0]=flex3;
tuple6type[0][0][1][1][1][0]=flex3;
tuple6type[0][1][0][1][1][0]=flex3;
tuple6type[0][1][1][0][1][0]=flex3;
//白活2
tuple6type[0][2][2][0][0][0]=FLEX2;
tuple6type[0][2][0][2][0][0]=FLEX2;
tuple6type[0][2][0][0][2][0]=FLEX2;
tuple6type[0][0][2][2][0][0]=FLEX2;
tuple6type[0][0][2][0][2][0]=FLEX2;
tuple6type[0][0][0][2][2][0]=FLEX2;
//黑活2
tuple6type[0][1][1][0][0][0]=flex2;
tuple6type[0][1][0][1][0][0]=flex2;
tuple6type[0][1][0][0][1][0]=flex2;
tuple6type[0][0][1][1][0][0]=flex2;
tuple6type[0][0][1][0][1][0]=flex2;
tuple6type[0][0][0][1][1][0]=flex2;
//白活1
tuple6type[0][2][0][0][0][0]=FLEX1;
tuple6type[0][0][2][0][0][0]=FLEX1;
tuple6type[0][0][0][2][0][0]=FLEX1;
tuple6type[0][0][0][0][2][0]=FLEX1;
//黑活1
tuple6type[0][1][0][0][0][0]=flex1;
tuple6type[0][0][1][0][0][0]=flex1;
tuple6type[0][0][0][1][0][0]=flex1;
tuple6type[0][0][0][0][1][0]=flex1;
int p1,p2,p3,p4,p5,p6,x,y,ix,iy;//x:左5中黑个数,y:左5中白个数,ix:右5中黑个数,iy:右5中白个数
for(p1=0;p1<4;++p1){
for(p2=0;p2<3;++p2){
for(p3=0;p3<3;++p3){
for(p4=0;p4<3;++p4){
for(p5=0;p5<3;++p5){
for(p6=0;p6<4;++p6){
x=y=ix=iy=0;
if(p1==1)x++;
else if(p1==2)y++;
if(p2==1){x++;ix++;}
else if(p2==2){y++;iy++;}
if(p3==1){x++;ix++;}
else if(p3==2){y++;iy++;}
if(p4==1){x++;ix++;}
else if(p4==2){y++;iy++;}
if(p5==1){x++;ix++;}
else if(p5==2){y++;iy++;}
if(p6==1)ix++;
else if(p6==2)iy++;
if(p1==3||p6==3){//有边界
if(p1==3&&p6!=3){//左边界
//白冲4
if(ix==0&&iy==4){//若右边有空位是活4也没关系,因为活4权重远大于冲4,再加上冲4权重变化可以不计
if(tuple6type[p1][p2][p3][p4][p5][p6]==0)
tuple6type[p1][p2][p3][p4][p5][p6]=BLOCK4;
}
//黑冲4
if(ix==4&&iy==0){
if(tuple6type[p1][p2][p3][p4][p5][p6]==0)
tuple6type[p1][p2][p3][p4][p5][p6]=block4;
}
//白眠3
if(ix==0&&iy==3){
if(tuple6type[p1][p2][p3][p4][p5][p6]==0)
tuple6type[p1][p2][p3][p4][p5][p6]=BLOCK3;
}
//黑眠3
if(ix==3&&iy==0){
if(tuple6type[p1][p2][p3][p4][p5][p6]==0)
tuple6type[p1][p2][p3][p4][p5][p6]=block3;
}
//白眠2
if(ix==0&&iy==2){
if(tuple6type[p1][p2][p3][p4][p5][p6]==0)
tuple6type[p1][p2][p3][p4][p5][p6]=BLOCK2;
}
//黑眠2
if(ix==2&&iy==0){
if(tuple6type[p1][p2][p3][p4][p5][p6]==0)
tuple6type[p1][p2][p3][p4][p5][p6]=block2;
}
}else if(p6==3&&p1!=3){//右边界
//白冲4
if(x==0&&y==4){
if(tuple6type[p1][p2][p3][p4][p5][p6]==0)
tuple6type[p1][p2][p3][p4][p5][p6]=BLOCK4;
}
//黑冲4
if(x==4&&y==0){
if(tuple6type[p1][p2][p3][p4][p5][p6]==0)
tuple6type[p1][p2][p3][p4][p5][p6]=block4;
}
//黑眠3
if(x==3&&y==0){
if(tuple6type[p1][p2][p3][p4][p5][p6]==0)
tuple6type[p1][p2][p3][p4][p5][p6]=BLOCK3;
}
//白眠3
if(x==0&&y==3){
if(tuple6type[p1][p2][p3][p4][p5][p6]==0)
tuple6type[p1][p2][p3][p4][p5][p6]=block3;
}
//黑眠2
if(x==2&&y==0){
if(tuple6type[p1][p2][p3][p4][p5][p6]==0)
tuple6type[p1][p2][p3][p4][p5][p6]=BLOCK2;
}
//白眠2
if(x==0&&y==2){
if(tuple6type[p1][p2][p3][p4][p5][p6]==0)
tuple6type[p1][p2][p3][p4][p5][p6]=block2;
}
}
}else{//无边界
//白冲4
if((x==0&&y==4)||(ix==0&&iy==4)){
if(tuple6type[p1][p2][p3][p4][p5][p6]==0)
tuple6type[p1][p2][p3][p4][p5][p6]=BLOCK4;
}
//黑冲4
if((x==4&&y==0)||(ix==4&&iy==0)){
if(tuple6type[p1][p2][p3][p4][p5][p6]==0)
tuple6type[p1][p2][p3][p4][p5][p6]=block4;
}
//白眠3
if((x==0&&y==3)||(ix==0&&iy==3)){
if(tuple6type[p1][p2][p3][p4][p5][p6]==0)
tuple6type[p1][p2][p3][p4][p5][p6]=BLOCK3;
}
//黑眠3
if((x==3&&y==0)||(ix==3&&iy==0)){
if(tuple6type[p1][p2][p3][p4][p5][p6]==0)
tuple6type[p1][p2][p3][p4][p5][p6]=block3;
}
//白眠2
if((x==0&&y==2)||(ix==0&&iy==2)){
if(tuple6type[p1][p2][p3][p4][p5][p6]==0)
tuple6type[p1][p2][p3][p4][p5][p6]=BLOCK2;
}
//黑眠2
if((x==2&&y==0)||(ix==2&&iy==0)){
if(tuple6type[p1][p2][p3][p4][p5][p6]==0)
tuple6type[p1][p2][p3][p4][p5][p6]=block2;
}
}
}
}
}
}
}
}
}
F函数是对ai(白子)走了一步之后的局面进行评分,棋型的权重的设计是有技巧的。
棋型代号 | 棋型说明 | 权重 | 棋型代号 | 棋型说明 | 权重 |
---|---|---|---|---|---|
WIN | 白赢 | 1000000 | LOSE | 黑赢 | -10000000 |
FLEX4 | 白活4 | 50000 | flex4 | 黑活4 | -100000 |
BLOCK4 | 白冲4 | 400 | block4 | 黑冲4 | -100000 |
FLEX3 | 白活3 | 400 | flex3 | 黑活3 | -8000 |
BLOCK3 | 白眠3 | 20 | block3 | 黑眠3 | -50 |
FLEX2 | 白活2 | 20 | flex2 | 黑活2 | -50 |
BLOCK2 | 白眠2 | 1 | block2 | 黑眠2 | -3 |
FLEX1 | 白活1 | 1 | flex1 | 黑活1 | -3 |
规则一:己方棋型权重为正,对方棋型权重为负,且相同棋型时,对方权重的绝对值要大于己方(可以设置为2倍或者3倍关系)。这是因为要考虑到进攻和防守,现在是己方(ai白子)下,例如:如果己方走了一步棋形成了活2,而对方已经有一个活2,那么显然是对方占优一些,因为下一步是对方走,对方是可以形成更高等级的活3的,所以己方活2就没有对方活2等级高。
规则2:等级:连5>活4>冲4=活3>眠3=活2>眠2=活1,相邻等级的权重设置为相差20倍(也可以30,40倍)。这是因为会重复计算等级比较低的棋型,为了不影响总体判断,比如一开始放一个子,活1的权重会计算16次,我设置为20倍,那么活2的权重刚好比16倍活1还要大一些。
规则3:对方连5、对方活4、对方冲4、对方活3的绝对值要设置大一点,这一点非常重要!!如果此时对方已经连五,说明己方已经输了。如果此时对方有活4和冲4,那么如果己方没有连5的话,己方必须要去阻止对方的活4和冲4。像这样可以分析出其他棋型权重。
之前说了,考虑到边界,需要一个更大的数组保存棋盘和边界信息。evaluate()不仅可以对当前局势进行评分,还可以判断游戏输赢的情况。
struct EVALUATION{
int score;
gameResult result;
int STAT[8];//储存部分棋形的个数(杀棋棋型),下标WIN=1为白连5,LOSE=2为黑连5,FLEX4=3为白活4,BLOCK4=5为白冲4,FLEX3=7为白活3
};
EVALUATION chessAi::evaluate(int board[15][15]){
//各棋型权重
int weight[17]={0,1000000,-10000000,50000,-100000,400,-100000,400,-8000,20,-50,20,-50,1,-3,1,-3};
int i,j,type;
int stat[4][17];//统计4个方向上每种棋型的个数
memset(stat,0,sizeof(stat));
int STAT[17];//存在这种棋型的方向的个数
memset(STAT,0,sizeof(STAT));
int A[17][17];//包括边界的虚拟大棋盘,board[i][j]=A[i-1][j-1],3表示边界
for(int i=0;i<17;++i)A[i][0]=3;
for(int i=0;i<17;++i)A[i][16]=3;
for(int j=0;j<17;++j)A[0][j]=3;
for(int j=0;j<17;++j)A[16][j]=3;
for(int i=0;i<15;++i)
for(int j=0;j<15;++j)
A[i+1][j+1]=board[i][j];
//判断横向棋型
for(i=1;i<=15;++i){
for(j=0;j<12;++j){
type=tuple6type[A[i][j]][A[i][j+1]][A[i][j+2]][A[i][j+3]][A[i][j+4]][A[i][j+5]];
stat[0][type]++;
}
}
//判断竖向棋型
for(j=1;j<=15;++j){
for(i=0;i<12;++i){
type=tuple6type[A[i][j]][A[i+1][j]][A[i+2][j]][A[i+3][j]][A[i+4][j]][A[i+5][j]];
stat[1][type]++;
}
}
//判断左上至右下棋型
for(i=0;i<12;++i){
for(j=0;j<12;++j){
type=tuple6type[A[i][j]][A[i+1][j+1]][A[i+2][j+2]][A[i+3][j+3]][A[i+4][j+4]][A[i+5][j+5]];
stat[2][type]++;
}
}
//判断右上至左下棋型
for(i=0;i<12;++i){
for(j=5;j<17;++j){
type=tuple6type[A[i][j]][A[i+1][j-1]][A[i+2][j-2]][A[i+3][j-3]][A[i+4][j-4]][A[i+5][j-5]];
stat[3][type]++;
}
}
EVALUATION eval;
memset(eval.STAT,0,sizeof (eval.STAT));
int score=0;
for(i=1;i<17;++i){
score+=(stat[0][i]+stat[1][i]+stat[2][i]+stat[3][i])*weight[i];//初步计分
int count=stat[0][i]+stat[1][i]+stat[2][i]+stat[3][i];//统计所有方向上部分棋型的个数
if(i==WIN)eval.STAT[WIN]=count;
else if(i==LOSE)eval.STAT[LOSE]=count;
else if(i==FLEX4)eval.STAT[FLEX4]=count;
else if(i==BLOCK4)eval.STAT[BLOCK4]=count;
else if(i==FLEX3)eval.STAT[FLEX3]=count;
}
eval.result=R_DRAW;
//白赢
if(eval.STAT[WIN]>0)eval.result=R_WHITE;
//黑赢
else if(eval.STAT[LOSE]>0)eval.result=R_BLACK;
eval.score=score;
return eval;
}
本节我们实现了最重要的评估函数,这个函数写了我很久,但现在回想起来实现并不困难,主要是权重设计太恼人了。
下一篇:五子棋ai:极大极小搜索和α-β剪枝算法的思想和实现(qt和c++)(三)极大极小搜索和α-β剪枝算法