SSDUT 小学期大作业,计划用 C++ 完成俄罗斯方块,用 QT 实现用户界面。实现基本功能后有多余时间,就加了 AI 的模块。目前的算法经测试,可以实现 25万 行左右的消除,在改进方块生成随机函数 (BAG7) 后,消除的行数会有大幅增加。

效果预览:

                   

以下是正文:

算法选取

实现 AI 的算法我考虑了两种,在其中权衡:

  • Pierre Dellacherie算法,求出一些参数,基于评估函数实现,每次只考虑当前情况的最优解
  • 搜索,基于DFS,用A*剪枝优化,结合多步的情况

DFS 需要优化的地方很多,假如计算的层数不够、没有高效的剪枝,一不小心容易写成人工智障,时间复杂度也不好。

Pierre Dellacherie算法更加清晰,复杂度更低。虽然不是很符合我的存储结构,但经过修改还是可以很好实现。

方块存储

我存储方块的模式比较简单暴力,直接放在数组矩阵里。共有 7 种方块,方块经过旋转形状不同,也分开存储。因此,共需要存 19 组数据来记录所有的方块。

以 “L” 形方块为例:

            "#3##",        //表示同一种方块的不同旋转
            "#3##",
            "#33#",
            "####"        
        
            "####",
            "333#",
            "3###",
            "####"
      
            "33##",
            "#3##",
            "#3##",
            "####"
           
            "##3#",
            "333#",
            "####",
            "####"

网上许多的方案是在确定一个中点(旋转点)之后,给出其他块的相对位置来表示形状,旋转时也是通过位置参数的变化实现。我这里虽然有些暴力但是可以减少很多计算。Pierre Dellacherie算法中的一个参数也需要用到中点这一概念,但很明显我的存储结构中没有维护中点这一概念。但其实也并不需要,我们可以简单修改此参数的获取规则,实现相同的作用。

Pierre Dellacherie算法

该算法只考虑当前,不对未来的情况进行计算。注重的是“不死性”,追求方块的“密集”,有时就算可以一次性消除 3 行,却会使全局方块更加“疏”,算法是不会在这种情况落子的。

每次生成一个方块,便穷举该方块所有旋转的所有落点。一种方块最多有 4 种旋转,并且由于游戏界面是 10 * 20 的,所以对于每个旋转形状,只需要考虑  10 种落点,故需要考虑的情况不多。加上剪枝后,穷举法也变得十分可观。

算法的核心是一个评估函数。对穷举出的每一种下落情况,计算 6 个参数值,用评估函数加权求和得到一个值,该值最大的情况便是目前方块的最优下落位置。

六个参数分别是:

  • Landing height 

表示方块下落后高度的参数 (相对于底部的高度) 。很明显,方块下落后越靠下越安全,使整体的方块堆越矮越好。传统的Pierre Dellacherie算法中,该参数是通过“中点”的位置给出,但是我的存储结构中没有这一概念。其实,不需要这样也可以得出。我的做法是求出当方块刚刚触碰到底部时,该方块的最高点和最低点高度的平均值,也可以表示该参数。注意,这里的 “刚刚触碰到底部” 是指方块无法再向下移动,但是还没有消除满行的中间时刻。之所以选这个时间点,是因为有可能在某种情况,方块固定到底部并且满行消除后,整个方块全都被消除,无法得到最高最低点。6 个参数中,只有这个参数选取的是这个时刻,其他 5 个都是指方块固定好,并且满行消除后的情况。如图,消除前,方块最高点和最低点高度分别为5和4,因此Landing height 值为 ((5 + 4)/ 25+4)/2 ;

  • Eroded piece cells

 表示当前方块下落后,消除的行数和此方块中参与到行消除的方块数目的乘积,体现的是方块对消除的贡献程度。如图,共消除 1 行,该方块贡献了 3 个单位块,因此Eroded piece cells 值为 1*3 ;

  • Row transitions

每一行中,方块有或无的变化次数之和。从有方块的格子到相邻的空白格计入一次变换。其中,边框算作有方块。注意,不仅仅是计算底部方块堆叠的区域,上方空白区域也要计算。如图,值为(16*2) + 4 + 2 + 6 + 6 ;

  • Colum transitions

列的变换次数,和 Row transitions 行的变换一个道理,类比一下便可。如图为(4*4) + (2*6) ;

  • Number of Holes

空洞的数量。空洞指的是,每列中某个方块下面没有方块的空白位置,该空白可能由 1 个单位或多个单位组成,但只要没有被方块隔断,都只算一个空洞。注意,空洞的计算以列为单位,若不同列的相邻空格连在一起,不可以将它们算作同一个空洞。如图,空洞有4个 ;

  • Well Sum

井的和。井指的是某一列中,两边都有方块的连续空格,边框算作有方块。若一个井由 1 个方块组成,则为 1 ;若由连续 3 个组成,则和为 1 + 2 + 3 的连加。如果一个井的顶部有方块的存在,则该“井”同时也是“空洞”。如图,“井”有 3 个,连加和为1 + (1+2) + (1+2) ;

图片示例 :

评估函数如下 (首字母简写):

F = -A_{1}*L + A_{2}* R - A_{3}*R- A_{4}* C - A_{5}*N -A_{6}* W

带入权重值后的评估函数详见代码

权重经验值:

A1:   -4.500158825082766 
A2:   3.4181268101392694 
A3 :  -3.2178882868487753 
A4 : -9.348695305445199 
A5 : -7.899265427351652 
A6 : -3.3855972247263626 

代码:

//比较赶时间,粗糙,希望别喷代码风格QAQ
#include <iostream>
using namespace std;

class AI{
public:
    AI(char MAP[100][100], int);
    int LandingHeight;
    int RowsEliminated;
    int RowTransitions();
    int ColumnTransitions();
    int NumberofHoles();
    int WellSums();
    int PierreDellacherie();
    
    bool collisionJudge(int, int);
    void addTomap(int, int);
    void AIgo(int &X, int &Y,int &shape);
 
private:
    int maxValue;
    char map[22][12];                        //用来操作
    char copyMap[22][12];                    //记录传入情况
    char cur[4][4];
    int type;//可以传出
};

AI :: AI(char MAP[100][100], int TYPE) {     //获得当前局势
    type = TYPE;//传入方块的值
    for(int i = 0;i < 22; i++)
        for(int j = 0;j < 12; j++) {
            if (MAP[i][j] != '#')
            copyMap[i][j] = MAP[i][j];
            else copyMap[i][j]=0;//空格赋值为0
        }
    maxValue = -0x3f3f3f;
}

 //碰撞检测只需要检测向下移动时的状态(由于变形是被枚举,左右移动不是用户操作的,一开始一定水平先移动到最优位置),用传入地图
bool AI :: collisionJudge(int x ,int y) { 
    for(int i=0; i<4; i++)
        for(int j=0; j<4; j++)
        {
            int tx = x+i;
            int ty = y+j;
            
            if(cur[i][j] && (tx <= 0 || tx >= 21 || ty <= 0 || ty >= 11))
                return false;
            if(cur[i][j] && copyMap[tx][ty])            //有方块重合
                return false;
        }
    return true;
}

void AI :: addTomap(int x, int y) {
    
    for(int i=0; i<4; i++)
        for(int j=0; j<4; j++) {
    
            int tx = x + i;
            int ty = y + j;
            
            if(tx<1 || tx > 20 || ty < 1 || ty > 10)    //界外不用考虑,剪枝
                continue;
            if(cur[i][j])
                map[tx][ty] = cur[i][j];
        }
    int maxHeight  = 21 - x;
    bool flag = true;
    for(int i=0; i<4 && flag; i++)
        for(int j=0; j<4; j++) {
            if (cur[i][j])
            {maxHeight -= i;flag = false; break;}
        }
    int minHeight = 18 - x;
    flag = true;
    for(int i=3; i>=0 && flag; i--)
        for(int j=0; j<4; j++) {
            if (cur[i][j])
            {minHeight += (3-i);flag = false; break;}
        }
    LandingHeight = (maxHeight + minHeight)/2;
    int cnt =0, sum=0;
    for(int i=20; i>=1; i--)
    {
        bool flag = false;
        for(int j=1; j<=10; j++)
            if(!map[i][j])
            {
                flag = true;
                break;
            }
        if(flag)
            continue;
        cnt++;                                    //行数
        
        for(int ii = 1; ii <= 10; ii ++)
            if (map[i][ii] == '#') sum ++;        //计算被消去个数
        
        for(int j=i; j>1; j--)
            for(int k=1; k<=10; k++)
                map[j][k] = map[j-1][k];
        i++;
    }
    
    RowsEliminated = cnt * sum;
}

int AI :: RowTransitions() {
    int cnt = 0;
    for(int i = 1; i <= 20; i ++)
        for(int j = 0; j <= 10; j++) {
            if (!(map[i][j]&&map[i][j+1]) && (map[i][j]||map[i][j+1]))
                    cnt ++;
        }
    return cnt;
}

int AI :: ColumnTransitions() {
    int cnt = 0;
    for(int j = 1; j <= 10; j ++)
        for(int i = 0; i <= 20; i++) {
            if (!(map[i][j]&&map[i+1][j]) && (map[i][j]||map[i+1][j]))
                cnt ++;
        }
    return cnt;
}

int AI :: NumberofHoles() {
    int cnt = 0;
    bool flag;
    for(int j = 1; j <= 10; j ++){
        flag = false;
        for(int i = 1; i <= 20; i++) {
            if (map[i][j]) flag = true;
            else
                if (flag == true) {
                    cnt ++;
                    flag = false;
                }
        }
    }
    return cnt;
}

int AI :: WellSums() {
    int sum = 0,cnt = 0;
    int well[21] = {0, 1, 3, 6, 10, 15, 21, 28, 36, 45, 55, 66, 78, 91, 105,
        120, 136, 153, 171, 190, 210};        //井的打表
    
    for(int j = 1; j <= 10; j ++)
        for(int i = 1; i <= 20; i++) {
            if(!map[i][j] && map[i][j-1] && map[i][j+1]) {
                cnt ++;
            }
            else {
                sum += well[cnt];
                cnt = 0;
            }
        }
    return sum;
}

void AI :: AIgo(int &X, int &Y,int &shape) {
    int Next[19]={1,0,3,2,5,6,7,4,9,10,11,8,13,14,15,12,17,16,18};
    int bag[7]={2,2,4,4,4,2,1};  //有几种变形
    int bagID[19]={0,0,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,6};//type对应
    char b[19][5][5]=
    {                            
        //存方块的数据,先不贴代码了,需要的话联系我的邮箱:originum@126.com   
    };
    
    for(int t = 0; t < bag[bagID[type]]; t++) {     //不同变形枚举
        type = Next[type];
        for (int i = 0; i < 4;i ++)                 //得到枚举的方块
            for (int j = 0; j < 4;j ++) {
                if (b[type][i][j] == '#')
                    cur[i][j] = 0;
                else
                    cur[i][j] = '#';                //特殊标记。此时形状无意义
            }    
        
        for (int i = 1; i <= 10; i++) {
            if (collisionJudge(1,i)) {              //当在第一行都判段为碰撞时,就是出界,不用考虑,理论上也不考虑方块逼近到最高处的可能性
                int j = 1;
                while (collisionJudge(++j,i));      //当没触底碰撞时,向下移动
                j --;
                
                for(int ii = 0;ii < 22; ii++)       //更新
                    for(int jj = 0;jj < 12; jj++)
                        map[ii][jj] = copyMap[ii][jj];  //把块加入map
              
                addTomap(j, i);
                int value = PierreDellacherie();
                if (value > maxValue) {
                    X = j;
                    Y = i;
                    shape = type;
                    maxValue = value;
                }
            }
        }
    }
}

int AI :: PierreDellacherie() {  
    
    int score =-45*LandingHeight + 34*RowsEliminated -
    32*RowTransitions() -
    93*ColumnTransitions() -
    79*NumberofHoles() -
    34*WellSums();
    
    return score;
}

请求源代码的人很多,我就放到GitHub上了,请见:https://github.com/Originum/AI-Tetris

这是我从前初学C++时的作业,欢迎完善。

Logo

CSDN联合极客时间,共同打造面向开发者的精品内容学习社区,助力成长!

更多推荐