我是一个围棋业余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
今天暂时写这么多,明天继续