基于Pierre Dellacherie算法实现俄罗斯方块的人工智能(python实现)《三》

基于Pierre Dellacherie算法实现俄罗斯方块的人工智能(python实现)《三》

本章主要讲述人工智能的实现。先讲解Pierre Dellacherie算法的基本知识,再讲解我是怎么实现的。

一些有趣的背景知识

  • 为啥叫俄罗斯方块?:首先俄罗斯方块是一个俄罗斯人做的,这个无聊的人就是前苏联科学家阿列克谢·帕基特诺夫(铭记前辈)
  • 游戏规则就不说了,不懂的人都是没有童年的
  • 俄罗斯方块会结束吗?在1997年Heidi Burgiel根据极大极小值算法证明了完全随机的俄罗斯方块游戏最终一定会结束。于是大家的热情转向了怎样才能让俄罗斯方块得到更高的分数,在2009年的时候,世界最优的俄罗斯方块的智能算法(by Thiery & Scherrer)是可以平均消除3500万行的(很牛嘟有没有!)
  • 俄罗斯方块的智能算法的类型,基本上有one-piecetwo-piece两种,就是仅考虑当前方块和把下一个方块也纳入考虑范围两种算法。但实际上,one-piece算法相对简单一些,而且不比two-piece差,甚至还要强。

Pierre Dellacherie算法原理

  • 所谓让机器自己去玩俄罗斯方块,就是让机器计算当前方块的所有形态可放置的所有位置,然后根据统一的评价标准,计算出最优的位置进行放置。
  • 但是评价结果的因素是多方面,对于这些因素需要一个统一的考虑,选择一个合理的评估策略。
  • 根据整理大概有一下几个相关的参数

    • 当一块板块摆放之后,与这个板块接触的小方块的数量是一个需要考虑的参数。很显然,与值接触的小方块越多,说明这个板块摆放再改位置后产生的“空洞”的数量越少,如果一个“棋盘”局面中空的小方块或者“空洞”数量少则说明这个局面对玩家有利。
    • 当一个板块摆放在某个位置之后,这个板块的最高点的高度是一个需要考虑的参数。这个高度会影响整体的高度,当有两个位置可选择摆放位置时,应该优先放置再板块最高点的高度比较低的位置上。
    • 当一个板块摆放在某个位置之后能消除的行数是一个重要参数。毫无疑问,消除的行越多越好。
    • 游戏区域中已经被下落板块填充的区域中空的小方格的数量也是评价游戏局面的一个重要参数。很显然,每一行中空的小方格数量越多,局面对玩家越不利。
    • 游戏区域中已经下落板块填充的区域中“空洞”的数量也是一个重要参数。如果一个空的小方格上方被其他板块的小方格挡住,则这个小方格就形成了“空洞”,“空洞”是俄罗斯方块游戏中最难处理的情况,必须等上层的小方块都消除之后才有可能填充“空洞”,很显然,这是一个能恶化局面的参数。
  • 简单地理解,摆放一个板块的策略是:板块放置的位置越靠下越好,方块之间越紧密越好,自身对消除行的方块贡献数量越多越好。

  • 这里要注意的是不可为了追求消除行数,而去造成过多的空洞,这样也是不合理的。

  • Pierre Dellacherie算法将上述抽象的参数转化为6种具体的属性。如下:

    • landingHeight:指当前板块放置之后,板块重心距离游戏区域底部的距离。(也就是小方块的海拔高度)
    • erodedPieceCellsMetric:这是消除参数的体现,他代表的是消除的行数与当前摆放的板块中被消除的小方块的格数的成绩。
      • 举个例子:下面这个例子就是说明红色的小方块下落之后会消除2行,而且自身贡献的小方格数是3个,所以返回值是3*2=6
        这里写图片描述
    • boardRowTransitions:对于每一行小方格,从左往右看,从无小方格到有小方格是一种“变换”,从有小方格到无小方格也是一种“变换”,这个属性是各行中“变换”之和

      • 何谓“变换”举个例子:
      • 这里写图片描述
      • 上面这张图片中用红色边框标注的即为一次“变换”,第一行为7次变换,第二行为6次变换
    • boardColTransitions:这是每一列的变换次数之和

    • boardBuriedHoles:各列中的“空洞的小方格数之和”
      • 举例说明:
      • 这里写图片描述
      • 如图所示,空洞数为6
    • boardWells:各列中“井”的深度的连加和
      • “井”的定义是,两边(包括边界)都有方块填充的空列。
      • 举例说明:
        这里写图片描述
      • 如图所示,以两边的最低边为“井”的开始,图中一共有两个“井”,深度分别为2和3,
      • (ps:不要怀疑为什么有些既是洞又是“井”)
      • 所以这个图返回的boardWells值应该是(1+2)+(1+2+3)= 9
  • 接下来介绍Pierre Dellacherie算法的评估函数了。

    • value = -landingHeight + erodedPieceCellsMetric - boardRowTransitions - boardColTransitions - (4 * boardBuriedHoles) - boardWells
    • 根据各指标的权重的经验值修改评估函数为:value = -45 × landingHeight + 34 × erodedPieceCellsMetric - 32 × boardRowTransitions - 93 × boardColTransitions - (79 × boardBuriedHoles) - 34 × boardWells
  • value值大的为最优位置,你没看错就是大的是最优,即使所有数都会是负数
  • 有点同学可能会问,要出现两个局面评分相同那怎么办呢?问得非常好,这个时候需要加入一个计算优先度的函数,这个也很简单。公式如下:
    • priority=100 * 板块需要水平移动移动的次数 + 板块需要选择的次数
  • (ps:可能PD算法的设计是 如果板块摆放再游戏区域的左侧优先度要加上10,那是因为他的那个游戏横向的小方格数量是10个,是一个偶数,而他的中心点在6这个位置。)
  • priority值小的为最优位置
  • 以上就是Pierre Dellacherie算法的全部内容,建议各位老铁根据理解自己写代码,这样不会有桎梏。下面是我的代码的解释

代码解释:

根据上面的理解我直接写了一个robotWorker类,看懂了上面的概念写代码并不难,难在第一步,获取某方块的所有形态可以放置的所有位置这个才是难点。
基本的实现步骤是:
- 1. 获取某方块的所有形态可以放置的所有位置
- 2. 计算所有位置的value值和priority值
- 3. 比较值的大小找到最优位置

  • 下面代码可能有很多可以优化的地方,我写得比较急(一天实现),没有优化,各位轻喷
  • 新建一个robotWorker对象时,需要把center(中心点),shape(方块名),station(状态 or 形态),color(颜色,其实没什么用),matrix(游戏界面中存储颜色的矩阵)
class RobotWorker():
    SHAPES = ['I', 'J', 'L', 'O', 'S', 'T', 'Z']
    I = [[(0, -1), (0, 0), (0, 1), (0, 2)],
         [(-1, 0), (0, 0), (1, 0), (2, 0)]]
    J = [[(-2, 0), (-1, 0), (0, 0), (0, -1)],
         [(-1, 0), (0, 0), (0, 1), (0, 2)],
         [(0, 1), (0, 0), (1, 0), (2, 0)],
         [(0, -2), (0, -1), (0, 0), (1, 0)]]
    L = [[(-2, 0), (-1, 0), (0, 0), (0, 1)],
         [(1, 0), (0, 0), (0, 1), (0, 2)],
         [(0, -1), (0, 0), (1, 0), (2, 0)],
         [(0, -2), (0, -1), (0, 0), (-1, 0)]]
    O = [[(0, 0), (0, 1), (1, 0), (1, 1)]]
    S = [[(-1, 0), (0, 0), (0, 1), (1, 1)],
         [(1, -1), (1, 0), (0, 0), (0, 1)]]
    T = [[(0, -1), (0, 0), (0, 1), (-1, 0)],
         [(-1, 0), (0, 0), (1, 0), (0, 1)],
         [(0, -1), (0, 0), (0, 1), (1, 0)],
         [(-1, 0), (0, 0), (1, 0), (0, -1)]]
    Z = [[(0, -1), (0, 0), (1, 0), (1, 1)],
         [(-1, 0), (0, 0), (0, -1), (1, -1)]]

    SHAPES_WITH_DIR = {
        'I': I, 'J': J, 'L': L, 'O': O, 'S': S, 'T': T, 'Z': Z
    }
    def __init__(self,center,shape,station,color,matrix):
        self.center = center
        self.shape = shape
        self.station = station
        self.color = color
        self.matrix = matrix
  • 根据中心点和形状和形态获取其他点
    def get_all_gridpos(self, center,shape,dir):
        curr_shape = self.SHAPES_WITH_DIR[shape][dir]

        return [(cube[0] + center[0], cube[1] + center[1])
                for cube in curr_shape]
    # 碰撞检测
    def conflict(self, center,matrix,shape,dir):
        for cube in self.get_all_gridpos(center,shape,dir):
            # 超出屏幕之外,说明不合法
            if cube[0] < 0 or cube[1] < 0 or cube[0] >= GRID_NUM_HEIGHT or  cube[1] >= GRID_NUM_WIDTH:
                return True

            screen_color_matrix = self.copyTheMatrix( matrix )
            # 不为None,说明之前已经有小方块存在了,也不合法
            if screen_color_matrix[cube[0]][cube[1]] is not None:
                return True

        return False
  • 复制矩阵,因为我怕修改矩阵会影响到原来的颜色矩阵
    def copyTheMatrix(self,screen_color_matrix):
        newMatrix = [[None] * GRID_NUM_WIDTH for i in range(GRID_NUM_HEIGHT)]
        for i in range( len( screen_color_matrix ) ):
            for j in range( len( screen_color_matrix[i] ) ):
                newMatrix[i][j] = screen_color_matrix[i][j]

        return newMatrix
  • 获取所有可能的位置,基本思路是,按照每一列,从上到下检测每一个位置,如果当前位置是可以放置的,而且下一个位置是不可以放置的,那么当前位置就是一个可能放置方块的位置。for循环每种形态。
    def getAllPossiblePos(self,thisShape = 'Z'):
        theMatrix = self.matrix
        theStationNum = len(self.SHAPES_WITH_DIR[thisShape])
        theResult = []
        for k in range(theStationNum):
            for j in range(len(theMatrix[1])):
                for i in range(len(theMatrix) - 1):
                    if self.conflict([i + 1, j], theMatrix, thisShape, k) == True and self.conflict([i, j], theMatrix,
                                                                                                thisShape, k) == False:
                        if {"center": [i, j], "station": k} not in theResult:
                            theResult.append({"center": [i, j], "station": k})

        return theResult

    # 获取方块海拔。
    def getLandingHeight(self,center):
        return GRID_NUM_HEIGHT-1-center[0]
  • 获取消除的行数和自身贡献的方格数。思路是满一行之后,检测这一行的每一个坐标是否在方块的所有点的坐标矩阵之内。从下网上开始搜索,一但一行里面没有一个实心方格则跳出循环,这也算是一种优化

    def getErodedPieceCellsMetric(self,center,station):
        theNewMatrix = self.getNewMatrix(center,station)
        lines = 0
        usefulBlocks = 0
        theAllPos = self.get_all_gridpos(center,self.shape,station)
        for i in range(len(theNewMatrix)-1,0,-1):
            count = 0
            for j in range(len(theNewMatrix[1])):
                if theNewMatrix[i][j] is not None:
                    count += 1
            # 满一行
            if count == 15:
                lines +=1
                for k in range(len(theNewMatrix[1])):
                    if [i,k] in theAllPos:
                        usefulBlocks +=1
            # 整行未填充,则跳出循环
            if count == 0:
                break
        return lines*usefulBlocks

    # 把可能的坐标位置放进去颜色矩阵,形成新的颜色矩阵。
    def getNewMatrix(self,center,station):
        theNewMatrix = self.copyTheMatrix(self.matrix)
        theAllPos = self.get_all_gridpos(center,self.shape,station)
        for cube in theAllPos:
            theNewMatrix[cube[0]][cube[1]] = self.color
        return theNewMatrix

    # 获取行变换数
    def getBoardRowTransitions(self,theNewmatrix):
        transition = 0
        for i in range( len(theNewmatrix)-1 , 0 , -1 ):
            count = 0
            for j in range( len(theNewmatrix[1])-1 ):
                if theNewmatrix[i][j] is not None :
                    count += 1
                if theNewmatrix[i][j] == None and theNewmatrix[i][j+1] != None:
                    transition += 1
                if theNewmatrix[i][j] != None and theNewmatrix[i][j+1] == None:
                    transition += 1
        return transition

    # 获取列变换数
    def getBoardColTransitions(self,theNewmatrix):
        transition = 0
        for j in range( len(theNewmatrix[1]) ):
            for i in range( len(theNewmatrix)-1,1,-1 ):
                if theNewmatrix[i][j] == None and theNewmatrix[i-1][j] != None:
                    transition += 1
                if theNewmatrix[i][j] != None and theNewmatrix[i-1][j] == None:
                    transition += 1
        return transition
  • 获取空洞,思路是按照列开始检测,从上往下,碰到有实心方格把colHoles设为0,继续往下,碰到空心方格则+1。每一列循环后加入到总的空洞数里。
    def getBoardBuriedHoles(self,theNewmatrix):
        holes = 0
        for j in range(len( theNewmatrix[1] )):
            colHoles = None
            for i in range( len( theNewmatrix ) ):
                if colHoles == None and theNewmatrix[i][j] != None:
                    colHoles = 0

                if colHoles != None and theNewmatrix[i][j] == None:
                    colHoles += 1
            if colHoles is not None:
                holes += colHoles
        return holes
  • 获取“井”的数量以及返回“井”数的连加数。根据《算法的乐趣》这本书的建议,用空间换时间,直接把各个连加数用数组存储起来。
  • 获取“井”的数量的思路是按照每一列从上往下开始检测,碰到空心方格而且两边是墙或者实心方格的时候,“井”深加1,继续往下检测,再次碰到空心方格则“井”深加1,若碰到实心方格则重新开始统计“井”深,并把当前“井”加入到总数。
    def getBoardWells(self,theNewmatrix):
        sum_n = [0,1,3,6,10,15,21,28,36,45,55]
        wells = 0
        sum = 0

        for j in range( len(theNewmatrix[1]) ):
            for i in range( len(theNewmatrix) ):
                if theNewmatrix[i][j] == None:
                    if (j-1<0 or theNewmatrix[i][j-1] != None) and (j+1 >= 15 or theNewmatrix[i][j+1] != None):
                        wells += 1
                    else:
                        sum += sum_n[wells]
                        wells = 0
        return sum
        # 计算优先度的函数
    def getPrioritySelection(self,point):
        tarStation = point['station']
        nowStation = self.station
        #
        colNum = abs(7 - point['center'][1] )
        if tarStation >= nowStation:
            changeTimes = tarStation - nowStation
        else :
            changeTimes = len(self.SHAPES_WITH_DIR[self.shape]) - nowStation + tarStation

        result = colNum*100 + changeTimes
        if point['center'][1] <=7 :
            result += 10
        return result

    # 根据点的中心位置计算分数
    def evaluateFunction(self,point):
        newMatrix = self.getNewMatrix( point['center'],point['station'] )
        lh = self.getLandingHeight( point['center'] )
        epcm = self.getErodedPieceCellsMetric(point['center'],point['station'])
        brt = self.getBoardRowTransitions(newMatrix)
        bct = self.getBoardColTransitions(newMatrix)
        bbh = self.getBoardBuriedHoles(newMatrix)
        bw = self.getBoardWells(newMatrix)

        # 两个计算分数的式子,前者更优,后者是PD算法的原始设计
        score = -45*lh + 34*epcm - 32*brt - 98*bct - 79* bbh -34*bw
        # score = -1*lh + epcm - brt - bct - 4*bbh - bw
        return score
  • robotWorker的主要执行逻辑。获取所有的可能的位置,遍历计算所有可能位置的分数value,大者为优,碰到相同分数的则计算优先度,小者为优。最后返回最优点对象,包括中心坐标和形态
    def mainProcess(self):
        pos = self.getAllPossiblePos(self.shape)
        bestScore = -999999
        bestPoint = None
        for point in pos:
            theScore = self.evaluateFunction( point)
            if theScore > bestScore:
                bestScore = theScore
                bestPoint = point
            elif theScore == bestScore:
                if self.getPrioritySelection( point ) < self.getPrioritySelection(bestPoint):
                    bestScore = theScore
                    bestPoint = point

        return bestPoint    
  • 程序运行如下:
    这里写图片描述
  • 至此基于Pierre Dellacherie算法实现俄罗斯方块的人工智能(python实现)系列文章全部讲述完毕,谢谢观看,下面是参考论文和博客以及书籍。

参考文献:

  1. 俄罗斯方块AI设计文档
  2. H5版俄罗斯方块(3)—游戏的AI算法
  3. El-Tetris – An Improvement on Pierre Dellacherie’s Algorithm
  4. 书籍《算法的乐趣》,作者:王晓华
  5. 论文《基于 Pierre Dellacherie 算法的俄罗斯方块游戏的研究和实现》作者:杨新年

猜你喜欢

转载自blog.csdn.net/qq_41882147/article/details/80005763
今日推荐