当前位置: 首页 > 工具软件 > 围棋学习 > 使用案例 >

AI围棋学习之路一----棋盘和落子的类构建

董元徽
2023-12-01

       我是一个围棋业余3段的人,学了三年,一直想拥有一个属于自己的AI,目前围棋基础还可以,就是苦于自己的编程实力不够,最近看到了一个非常棒的教程,有兴趣的去找tyler_download看他的文章,代码非常详细具体,适合小白,现将我的学习心得和要点一一陈述出来

要做围棋AI先要把架子打出来,先实现基本围棋游戏再考虑引入神经网络,今天就先完成棋盘和落子的类构建

如果你有一定围棋基础,理解下面的代码就很容易,如果没有,可以先去学习围棋的气、交叉点、胜负判断等知识,我在分析时也会穿插一些围棋基本知识

步骤

   1 棋手类

         分析:围棋有黑棋和白棋,这里的棋手代表黑白两方,属性就是颜色,方法是返回对方的颜色

import enum

#棋手(棋子颜色)
class Player(enum.Enum):
    black=1
    white=2
    #返回对方棋子颜色
    def other(self):
        if self == Player.black:
            return Player.white
        else:
            return Player.black

    2.  棋盘交叉点类

           分析: 围棋的棋子是下在交叉点上的,有9*9=81,13*13=169,19*19=361三种,而一个交叉点有上下左右四个方向的相邻点,而这个相邻点在我们之后算棋子的气(棋子的自由点)以及棋子块相连都有非常重要的用处,因此定义这个类的属性就是该点的行与列,方法就是返回该点的相邻点的集合

from collections import namedtuple

#棋盘交叉点
class Point(namedtuple("Point","row col")):#增加可读性,即可通过point.row,point.col得到行列
    #返回棋盘一个点的四个方向相邻点
    def neighbor(self):
        return[
            Point(self.row,self.col+1),
            Point(self.row,self.col-1),
            Point(self.row+1,self.col),
            Point(self.row-1,self.col)
            ]

 那位博主为了增代码可读性,引入了namedtuple

 3.落子类

     分析:落子的一个属性是所下的交叉点,然后落子前需要判断当前玩家有无pass,如果pass就让另一个玩家下,还要判断当前玩家有无投降,如果投降就不会做下面的事情

    

import copy
#落子
class Move():
    #初始化(落子为空,pass为假,投降为假)
    def __init__(self,point = None,is_pass = False,is_resign = False):
        assert (point is not None) ^is_pass ^is_resign   #断言,三种条件(落子为空,玩家pass了,玩家投降了)都不会执行下面语句,直接跳出
        self.point = point
        #是否下了
        self.is_play = (self.point is not None)
        self.is_pass = is_pass
        self.is_resign = is_resign

    #加上@classmethod可以直接用类名.方法名直接调用,而不用实例化对象
    @classmethod
    def play(cls,currentpoint):
        return Move(point = currentpoint)

    @classmethod
    #让对方去继续下
    def pass_turn(cls):
        return Move(is_pass = True)

    @classmethod
    #投降
    def resign(cls):
        return Move(is_resign = True)

  4.棋子块类

      分析:   一个棋子及棋子块当它的上下左右相邻点有同色的棋子的话,那么它们就可以相连成一个棋子块,相连的时候它们的气(未下子的相邻点个数)并不是等于两者各自的气相加,因为相连的时候,两个棋子或棋子块的 气 中有重合的气,要减掉,而重合的气就是相连后的棋子块中棋子的个数

#棋子块(相邻在一起的同色棋子,有棋子颜色,当前棋子集合,以及气(自由点集合的长度))
class GoBlock():
    def __init__(self,color,stones,liberties):
        self.color = color
        self.stones = set(stones)#棋子块里棋子的集合
        self.liberties = set(liberties)#棋子块的自由点集合

    #增加气(自由点)
    def add_liberty(self,point):
        self.liberties.add(point)

    #减少气(自由点)
    def remove_liberty(self,point):
        self.liberties.remove(point)

    @property
    #返回棋盘块的气(自由点的个数)
    def num_liberties(self):
        return len(self.liberties)

    #定义相等(类别相同,颜色相同,当前落子集合相同,当前自由点相同)
    def __eq__(self, other):
        return isinstance(other,GoBlock) and self.color == other.color and self.stones == other.stones and self.liberties == other.liberties

    #另一个与当前棋子块相同颜色的棋子块与之相连,要注意自由点的改变,
    def merge_with(self,current_goblock):
        assert self.color == current_goblock.color #保证棋子块颜色要相同
        combined_stones = self.stones| current_goblock.stones#合并两个集合
        combined_liberties = (self.liberties|current_goblock.liberties)-combined_stones #合并后原来的棋子块的自由点与当前棋子块的自由点会有重合的点,因此要减去重合的点
        return GoBlock(self.color,combined_stones,combined_liberties)

   5. 棋盘类 

          分析:棋盘类主要要做的是放置玩家落下的棋子,而玩家落子具随机性,可能会出现非法的落子

                  在非应氏规则中,以下落子是非法的(今天先解决前面两个简单的,关于劫、眼的概念大家也要去了解)

                       1.棋子没有下在棋盘内的交叉点上,即落在棋盘外了

                       2.棋子落下的地方已经有棋子了

                       3.在不是打劫且对方的气不是只有一口的时候,落在别人的眼里

                       4.当对方提劫后,在未找劫材的时候去提劫

                       5.棋子落下后使自己的棋子气为0,即自填(在应氏规则里是允许的)

          以下代码有几个要点:

                       1.same_color存当前落子点的上下左右相邻点中与之同色的棋子,opposite_color存当前落子点上下左右相邻点与            之不同色的棋子,而围棋并不是每个落子点都有上下左右四个有效的相邻点,因为棋盘的四个角只有两个方向的                              相邻点有效,另外两个方向的相邻点在棋盘外了无效;还有棋盘的四条边只有三个方向的相邻点是有效的,另                              一 个方向在棋盘外无效

                       2.在围棋中,如果一个棋子块的气为0,那么它的生命就结束了,就要从棋盘中拿掉,而棋子拿掉后会产生一些变                            化:(1)所拿掉棋子块所包含的棋子所在的交叉点应该是空,标明这些点可以重新落子

                                  (2)拿到棋子块后,其他棋子块的气就要发生变化,要加气,而找其他块,可以去找这个点的邻接点所在                                             棋子块

                       3.原博主在棋盘类中除了行和列以外还加了一个grid属性,通过分析代码我们可以发现这个集合存的是一个棋盘上

                         所有点所在的棋子块,通过grid.get(point)找到这个点所在的棋子块,如果棋子块不存在,表明该点未下过

#棋盘
class Board():
    #初始化棋盘(水平交叉点,垂直交叉点,主要有9*9,13*13,19*19)
    def _init_(self,row_nums,col_nums):
        self.row = row_nums
        self.col = col_nums
        self.grid = {}#棋盘上所有点所带棋子块的集合

    #放置棋子,要确保位置是在棋盘内
    def place(self,player,point):
        #确保是在棋盘内
        assert self.is_on_grid(point) #不在格子内会执行is_on_grid直接跳出
        #确保所放置的棋子位置没有其他棋子
        assert self.grid.get(point) #有的话就跳出

        same_color = []#相同棋子颜色组合
        opposite_color = [] #不同颜色棋子组合
        liberties = [] #该落子点的自由点集合(即它本身的气)


        for neighbor in point.neighbor:
            #判断改点的邻接情况(若在棋盘外则返回)
            if not self.is_on_grid(point):
                continue

            neighbor_string = self.grid.get(neighbor)
            if neighbor_string is None:#邻接点没有棋子,那该邻接点是落子点的自由点,即可在自由点集合里加入
                liberties.append(neighbor)
            elif neighbor_string.color == player:#如果相邻点与当前玩家的颜色相同,同时没有加入到相同棋子颜色集合中,在集合中加入
                if(neighbor_string not in same_color):
                    same_color.append(neighbor_string)
            else:#如果相邻点与当前玩家的颜色不同,同时没有加入到不同棋子颜色集合中,在集合中加入
                if(neighbor_string not in opposite_color):
                    opposite_color.append(neighbor_string)

        #将当前棋子与棋盘上相邻的相同颜色棋子连成一片
        new_block = GoBlock(player,[point],liberties)#当前落子的棋子块

        #遍历相同颜色棋子组合,去和他们合并
        for same_color_block in same_color:
            new_block = new_block.merge_with(same_color_block)

        for new_block_point in new_block.stones:#将每个交叉点都带上自己所在的棋子块
            self.grid[new_block_point] = new_block

        #遍历不同颜色棋子集合,减少他们的自由点
        for opposite_color_block in opposite_color:
            opposite_color_block.remove_liberty(point)

        #判断落子后,不同颜色棋子块有无自由点,即无空的气,要删除对方棋子
        for opposite_color_block in opposite_color:
            if(opposite_color_block.num_liberties == 0):
                self.remove_block(opposite_color_block)

    #判断有无在棋盘内
    def is_on_grid(self,point):
        return 1<= point.row <= self.row and 1<= point.col <= self.col


    #获取一个点所在的棋子块颜色,如果没有棋子块,就返回none
    def getColor(self,point):
        block = self.grid.get(point)
        if(block is None):
            return None
        else:
            return block.color

    #获取一个交叉点所在棋子块,如果没有就返回None
    def getBlock(self,point):
        block = self.grid.get(point)
        if(block is None):
            return None
        else:
            return block

    #删除气没了的棋子块
    def remove_block(self,block):
        #遍历该块的棋子,删除所有点所在棋子块集合,以及所有点相邻棋子所在棋子块的气要增加
        for point in block.stones:
            for neighbor in point.neighbor:
                neighbor_block = self.grid.get(neighbor)
                #判断一个点的邻接点的所在棋子块状态
                #1.没有,找下个点
                if(neighbor_block is None):
                    continue
                #2.有,但与当前落子点的棋子块是不同的棋子块,则要加气
                if(neighbor_block is not block):
                    neighbor_block.add_liberty(point)
            #使得当前点所在的棋子块为空
            self.grid[point] = None

       今天暂时写这么多,明天继续                

              

 

   

 类似资料: