五子棋ai:极大极小搜索和α-β剪枝算法的思想和实现(qt和c++)(二)贪心算法和评估函数

我查找了大量的网上资料,结合自己编程实践,走了很多弯路,总结了一些关于五子棋ai的经验以供大家参考借鉴。

一、贪心算法(相当于ai只思考一步的情况)

对于五子棋ai,大部分人想到的是做一个评估函数。这里的评估函数网上大概有两种,但是很多人会弄混淆

第一种我称为K函数(只是一个代号,与第二种相区别),是对一个可走的空位子进行打分,如果ai白子落在这个空位置的分数越高,说明这个位置就越好,每次ai走棋就找到一个最好的空位置就行了。

第二种我称为F函数,是对现在的棋盘局面进行打分。 ai白子首先找所有可以走的空位置,模拟走了这个位置以后,用f函数进行局面评分,如果走了这样的一个空位置的得分越高,说明这个位置就越好,每次ai走棋就找这样一个分数最高的位置。

那么你可能就要问了,这两个评估函数有什么区别呢?区别非常大!如果你只是想实现一个只看一步的ai,那么你可以用K函数也可以用F函数。但是如果你想要实现基于博弈树的极大极小搜索和α-β剪枝算法的“聪明”ai,就只能用F函数因为博弈树必须要对局面打分,而不是对位置打分。如果你明白了这一点,那么接下来就好办了。

二、评估函数(对白子走了一步的局面评估)

怎么对局势有一个比较准确的估计呢?我查阅了大量资料。五子棋的局势无非就是对棋型个数和权重的统计。(对某一方而言的)

五子棋棋型

我分为8种:
1.连5或者长连
连5 长连
2.活4(有两个位置可以形成连5)
活4
3.冲4(有一个位置可以形成连5)
冲4(a)
冲4(b)
还有其他情况,比如边界,总之再下一步(只能是1步而不是2步)就能连5的就是冲4。
4.活3(走一步可以形成活4)
活3(a) 活3(b)
5.眠3(走一步可以形成冲4)
眠3(a)
眠3(b)
6.活2(走一步可以形成活3):形状自行脑补
7.眠2(走一步可以形成眠3)
8.活1(走一步可以形成活2)

具体评分规则

我用六元组一条直线上连续六个位置的状态)来表示棋型,通过检查棋盘上所有六元组来得分。最容易漏掉的地方是边界,比如如果一个斜线只有5个位置也有可能连5、但六元组没考虑进来等等问题,办法是用一个更大的、包括了边界的数组保存棋盘和边界信息

1.六元组所有棋型辨识

用一个棋型辨识数组保存所有棋型。

#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;
                                }                               
                            }
                        }
                    }
                }
            }
        }
    }
}

2.各棋型权重的设计(重点)

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。像这样可以分析出其他棋型权重。

3.进行棋型判别

之前说了,考虑到边界,需要一个更大的数组保存棋盘和边界信息。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++)(三)极大极小搜索和α-β剪枝算法

你可能感兴趣的:(c++,qt,游戏,ai)