从本节开始,我们废话少说,迅速进入代码编写阶段。对技术而言“做”永远是比“讲”更好的说,很多用语言讲不清楚的道理,看一下代码自然就明白了。我们要实现的围棋机器人必须做到以下几点:

1, 跟踪当前所下的每一步棋。
2, 跟踪当前的棋局进展。如果是机器人自我对弈,那么代码对棋局的跟踪与人和机器人对弈是对棋局的跟踪有所不同。
3, 根据当前棋盘局势,搜索多种可行的下法,并从中评估出最好的走法。
4, 将棋局转换为可以拥有训练网络的数据。

我们从易到难,先解决好小范围的问题,打好基础后才能处理更复杂的问题。首先我们要用代码编制好棋盘,player,落子等对象。首先我们用代码实现棋手:

import enumclass Player(enum.Enum):black = 1white = 2'''返回对方棋子颜色,如果本方是白棋,那就返回Player.black'''@propertydef  other(self):if self == Player.white:return  Player.blackelse:return Player.white

上一节我们讲过,围棋棋盘是由多条横线和竖线交织而成,棋子必须落在横线和竖线交叉点上,我们用以下代码表示交叉点:

from collections import namedtupleclass Point(namedtuple('Point', 'row col')):def  neghbors(self):'''返回当前点的相邻点,也就是相对于当前点的上下左右四个点'''return [Point(self.row - 1, self.col),Point(self.row + 1, self.col),Point(self.row, self.col - 1),Point(self.row, self.col + 1),]

这里我们使用python3的语言特性增加可读性,Point类其实包含两个整形成员,分别命名为row和col,我们可以使用point.row和point.col来访问两个成员,如果不使用nametuple,那么我们得通过point[0],piont[1]来访问两个成员,如此可读性就大大降低。

接下来我们需要用代码来表示“落子”:

import copyclass Move():def  __init__(self, point = None, is_pass = False, is_resign = False):assert(point is not None) ^is_pass ^is_resignself.point = point#是否轮到我下self.is_play (self.pint is not None)self.is_pass = is_passself.is_resign = is_resign@classmethoddef  play(cls, point):return Move(point = point)@classmethod#让对方继续下def  pass_turn(cls):return move(is_pass = True)@classmethod#投子认负def  resign(cls):return move(is_resign = True)

在围棋中,“落子”分三种情况,一种是把棋子放到某个点;一种是放弃下子,让对方继续下,类似于扑克中的“大”,“过”;第三是投子认负,我们上面代码中都对应了三种情况。

上面代码只是拥有表示下棋时的一下基本概念,并不包含逻辑,接下来我们要编写围棋的规则及逻辑代码。首先要做的是棋盘,棋盘在每次落子之后它要检测是否有对方棋子被吃,它要检测相邻棋子的所有自由点是否全部堵上,由于很可能有很多个棋子相邻在一起,因此这一步或许或比较耗时,我们先用代码表示相邻在一起的多个棋子:

class GoString():def __init__(self, color, stones, liberties):self.color = colorself.stones = set(stones)self.liberties = set(liberties)def  remove_liberty(self, point):self.liverties.remove(point)def  add_liberty(self, point):self.liberties.add(point)def  merged_with(self, go_string):#落子之后,两片相邻棋子可能会合成一片'''假设*代表黑棋,o代表白棋,x代表没有落子的棋盘点,当前棋盘如下:x  x  x  x  x  xx  *  x! *  o  *x  x  x  *  o  xx  x  *  x  o  xx  x  *  o  x  x注意看带!的x,如果我们把黑子下在那个地方,那么x!左边的黑棋和新下的黑棋会调用当前函数进行合并,同时x!上方的x和下面的x就会成为合并后相邻棋子共同具有的自由点。同时x!原来属于左边黑棋的自由点,现在被一个黑棋占据了,所以下面代码要把该点从原来的自由点集合中去掉'''assert go_string.color == self.colorcombined_stones = self.stones | go_string.stonesreturn GoString(self.color, combined_stones, (self.liberties | go_string.liberties) - combined_stones)@propertydef  num_liberties(self):return len(self.liberties)def __eq__(self, other):return isinstance(other, GoString) and self.color == other.color and self.stones == other.stones and self.liberties == other.liberties

上面代码中的merge_with函数不好理解,必须要仔细理解上面注释才好理解代码逻辑,同时我们可以借助下图来理解merge_with函数的逻辑:

试想在第二行两个分离的黑棋中落一个黑棋,那么左边单个黑棋和右边两个黑棋就会连成一片,左边黑棋与落在中间黑棋连接成片时,它的自由点集合要减去中间落入的黑棋,同理右边两个黑棋的自由点也要减去落在中间黑棋所占据的位置,这就是为何要执行语句(self.liberties | go_string.liberties) - combined_stones。

接下来我们使用代码实现棋盘:

class  Board():def  __init__(self, num_rows, num_cols):self.num_rows = num_rowsself.num_cols = num_colsself._grid = {}def  place_stone(self, player, point):#确保位置在棋盘内assert self.is_on_grid(point)#确定给定位置没有被占据assert self._grid.get(point) is Noneajecent_same_color = []adjacent_oppsite_color = []liberties = []for neighbor in point.neighbors():#判断落子点上下左右的邻接点情况if not self.is_on_grid(neighbor):continueneighbor_string = self._grid.get(neighbor)if neighbor_string is None:#如果邻接点没有被占据,那么就是当前落子点的自由点liberties.append(neighbor)elif neighbor_string.color == player:if neighbor_string not in adjacent_same_color:#记录与棋子同色的连接棋子adjacent_same_color.append(neighbor_string)else:if neighbor_string not in ajacent_opposite_color:#记录落点邻接点与棋子不同色的棋子adjacent_opposite_color.append(neighbor_string)#将当前落子与棋盘上相邻的棋子合并成一片     new_string = GoString(player, [point], liberties)for same_color_string in adjacent_same_color:new_string = new_string.merged_with(same_color_string)for new_string_point in new_string.stones:#访问棋盘某个点时返回与该点棋子相邻的所有棋子集合self._grid[new_string_point] = new_stringfor other_color_string in adjacent_opposite_color:#当该点被占据前,它属于反色棋子的自由点,占据后就不再属于反色棋子自由点other_color_string.remove_libertiy(point)for other_color_string in adjacent_opposite_color:#如果落子后,相邻反色棋子的所有自由点都被堵住,对方棋子被吃掉if other_color_string.num_liberties == 0:self._remove_string(other_color_string)def  is_on_grid(self, point):return 1 <= point.row <= self.num_rows and 1 <= point.col <= self.num_colsdef  get(self, point):string = self._grid.get(point)if string is None:return Nonereturn string.colordef  get_go_string(self, point):string = self._grid.get(point)if string is None:return Nonereturn stringdef  _remove_string(self, string):#从棋盘上删除一整片连接棋子for point in string.stones:for neighbor in point.neighbors():neighbor_string = self._grid.get(neighbor)if neighbor_string is None:continueif neigbor_string is not string:neighbor_string.add_liberty(point)self._grid[point] = None

这里我们需要解释_remove_string的逻辑。如下图:

当我们在像右边落入黑子后,中间被包围的白子被吃掉后需要从棋盘上拿开。此时我们需要把被拿走棋子所在的点设置成未被占据状态,同时查找改点上下左右四边的棋子片,为这些棋片增加一个自由点。

落子和棋盘都完成了,由于每次落子到棋盘上后,棋局的状态会发生变化,接下来我们完成棋盘状态的检测和落子法性检测,状态检测会让程序得知以下信息:各个棋子的摆放位置;轮到谁落子;落子前的棋盘状态,以及最后一次落子信息,以及落子后棋盘的状态变化:

class GameState():def  __init__(self, board, next_player, previous, move):self.board = boardself.next_player = next_playerself.previous_state = previousself.last_move = movedef apply_move(self, move):if move.is_play:next_board = copy.deecopy(self.board)next_board.place_stone(self.next_player, move.point)else:next_board = self.boardreturn GameState(next_board, self.next_player.other, self, move)@classmethoddef  new_game(cls, board_size):if isinstance(board_size, int):board_size = (board_size, board_size)board = Board(*board_size)return GameState(board, Player.black, None, None)def  is_over(self):if self.last_move is None:return Falseif self.last_move.is_resign:return Truesecond_last_move = self.previous_state.last_moveif second_last_move is None:return False#如果两个棋手同时放弃落子,棋局结束return self.last_move.is_pass and second_lass_move.is_pass

接下来我们需要确定,落子时是否合法。因此我们需要确定三个条件,落子的位置没有被占据;落子时不构成自己吃自己;落子不违反ko原则。第一个原则检测很简单,我们看看第二原则:

我们看上图,三个黑棋连片只有一个自由点,那就是小方块所在位置。但不管黑棋要不要堵住那个店,三个黑子终究要被吃掉,因此黑棋不能在小方块所在位置落点,因为落点后,四个黑棋连片,但却再也没有自由点,于是黑棋下在小方块位置,反而被对方吃的更多,这就叫自己吃自己,绝大多数围棋比赛都不允许这样的下法。

当时下面请看就不同了:

当黑棋下在小方块处,它能把中间两个白棋吃掉,因此就不算是自己吃自己,因为中间两个白棋拿掉后,黑棋就会有自由点。因此程序必须在落子结束,拿掉所有被吃棋子后,才能检查该步是否形成自己吃自己:

def  is_move_self_capture(self, player, move):if not move.is_play:return Falsenext_board = copy.deepcopy(self.board)#先落子,完成吃子后再判断是否是自己吃自己next_board.place_stone(player, move.point)new_string = next_board.get_go_string(move.point)return new_sting.num_liberties == 0

接下来我们完成ko的检测,也就是对方落子后,你的走棋方式不能把棋盘恢复到对方落子前的局面。由于我们上面实现的GameState类保留了落子前状态,因此当有新落子后,我们把当前状态跟以前状态比对,如果发现有比对上的,那表明当前落子属于ko,代码实现如下:

 @propertydef  situation(self):return (self.next_player, self.board)def does_move_violate_ko(self, player, move):if not move.is_play:return Falsenext_board = copy.deepcopy(self.board)next_board.place_stone(player, move.point)next_situation = (player.other, next_board)past_state = self.previous_state#判断Ko不仅仅看是否返回上一步的棋盘而是检测是否返回以前有过的棋盘状态while past_state is not None:if past_state.situtation == next_situtation:return Truereturn Falsedef  is_valid_move(self, move):if self.is_over():return Falseif move.is_pass or move.is_resign:return Truereturn (self.board.get(move.point) is None and not self.is_move_self_capture(self.next_player, move) andnot self.does_move_violate_ko(self.next_player, move))

我们上面实现的does_move_violate_ko效率比较差,因为每下一步棋,我们就得执行该函数,它会搜索过往所有棋盘状态进行比较,如果当前已经下了几百手,那么每下一步,它就得进行几百次比对,因此效率会非常慢,后面我们会有办法改进它的效率。

最后我们需要预防机器人下棋时,把自己的棋眼给堵死,例如下面棋局:

如果机器人下的是白棋,那么它不能自己把A,B点给堵上,因为堵上后,黑棋会把所有白棋吃掉,因此我们必须增加代码逻辑检测这种情况。我们队棋眼的定义是,所有的邻接点都被己方棋子占据的位置,并且该棋子四个对角线位置中至少有3个被己方棋子占据,如果棋子落子棋盘边缘,那么我们要求它所有对角线位置都被己方棋子占据,实现代码如下:

def  is_point_an_eye(board, point, color):if board.get(point) is not None:return Falsefor neighbor in point.neighbors():#检测邻接点全是己方棋子if board.is_on_grid(neighbor):neighbor_color = board.get(neighbor)if neighbor_color != color:return False#四个对角线位置至少有三个被己方棋子占据   friendly_corners = 0off_board_corners = 0corners = [Point(point.row - 1, point.col - 1),Point(point.row - 1, point.col + 1),Point(point.row + 1, point.col - 1),Point(point.row + 1, point.col + 1)]for corner in corners:if board.is_on_grid(corner):corner_color = board.get(corner)if corner_color == color:friedly_corner += 1else:off_board_corners += 1if off_board_corners > 0:return off_board_corners + friendly_corners == 4return friendly_corners >= 3

有了上面代码基础后,我们就可以实现自我博弈围棋机器人,那将是下一节的内容。

更详细的讲解和代码调试演示过程,请点击链接

更多技术信息,包括操作系统,编译器,面试算法,机器学习,人工智能,请关照我的公众号:

从零开始再造打爆李世石的AlphaGo:快速构建棋盘和围棋规则相关推荐

  1. 从零开始再造打爆李世石的AlphaGo:创造能下围棋的机器人

    我们在上节完成了围棋规则和棋盘状态监测功能,本节我们在基于上节的基础上,设计一个能自己下棋的围棋机器人.首先我们设计一个类叫Agent,它的初始化代码如下: class Agent:def __ini ...

  2. 从零开始再造打爆李世石的AlphaGo:围棋的基本规则和代码设计思路

    从本节开始,我们将从零开始,一行一行代码的编写,直到完整设计出当年击垮13次世界围棋冠军李世石的AlphaGo,幸运的是,在人工智能思维下,我们不需要成为围棋高手就能设计出AlphaGo,例如我对围棋 ...

  3. 一度智信:电商店铺爆款打造,新手快速上手!

    一度智信:电商店铺爆款打造,新手快速上手! 有爆款商品,对店铺来说非常重要,因为爆款是拼多多店铺流量转化的来源,可见店铺拥有爆款商品太重要了!想拥有爆款,那么店铺应该如何打造爆款呢?接下来一度智信电商 ...

  4. 【转】使用PowerApps快速构建基于主题的轻业务应用 —— 进阶篇

    在上一篇 使用PowerApps快速构建基于主题的轻业务应用 -- 入门篇 中,我用了三个实际的例子演示了如何快速开始使用PowerApps构建轻业务应用,你可能已经发现,我都是使用默认生成的设置,没 ...

  5. 1个通用工具平台+多个热点场景工具套件,助力开发者快速构建应用

    摘要:华为云为开发者提供了全流程的极简工具和模板,通过1个通用工具平台-"DevCloud"和多个热点场景的工具套件,支持Full Code和Low code/No code多种开 ...

  6. 百度正用谷歌AlphaGo,解决一个比围棋更难的问题 | 300块GPU在燃烧

    晓查 发自 凹非寺  量子位 报道 | 公众号 QbitAI 9102年,人类依然不断回想起围棋技艺被AlphaGo所碾压的恐怖. 却也有不以为然的声音:只会下棋的AI,再厉害也还是个运动员啊! 百度 ...

  7. 利用JMail组件快速构建邮件程序

    当你需要在VC程序中提供邮件支持功能的时候,你有许多种选择: 1)根据SMTP,POP3,MIME等协议从零开始实现.这要求熟悉RFC 821,RFC 822,RFC 1123, RFC 1652, ...

  8. 快速构建嵌入式全栈知识体系以及如何进阶

    快速构建嵌入式全栈知识体系以及如何进阶 嵌入式是一门交叉学科.一个嵌入式电子产品(比如手机)从底层到上层,一般会涉及半导体芯片.电子电路.计算机.操作系统.多媒体等不同专业领域的知识.很多从事嵌入式开 ...

  9. 快速构建Spring Cloud工程

    spring cloud简介 spring cloud为开发人员提供了快速构建分布式系统的一些工具,包括配置管理.服务发现.断路器.路由.微代理.事件总线.全局锁.决策竞选.分布式会话等等.它运行环境 ...

  10. 用Flutter + Dart快速构建一款绝美移动App

    作者 | Wojciech Kuroczycki 译者 | 弯月 来源 | CSDN(ID:CSDNnews) 如今,与前端或移动相关的新框架层出不穷.所有从事Web开发的人都应该熟悉各种目不暇接的新 ...

最新文章

  1. 一杯茶的时间,上手Zabbix
  2. 学习进度条(第三周)
  3. php 支付宝订单查询_5. PHP接入支付宝单笔订单查询接口
  4. Mapper 接口无法注入或Invalid bound statement (not found)
  5. ASP.NET MVC实践系列6-Grid实现(上)
  6. notepad++与ISE/Vivado关联
  7. Entity Framework With Oracle
  8. Mysql错误2003 -Can't connect toMySQL server on 'localhost'(10061)解决办法
  9. pycharm appiunm 公众号测试_知道答案公众号_知到APP笔尖上的艺术——书法基础与赏析单元测试答案_知道答...
  10. 第六季 流放之路教程
  11. iOS苹果内购(详细步骤)
  12. Kali Linux安装2019.2.28
  13. Java对List集合中的对象的某个中文字段按照拼音首字母进行排序
  14. 从0-1搭建一个服务器(以前不懂事,现在只想搞钱)
  15. 港股常见的宽基指数:恒生指数、H股指数和香港中小指数
  16. 最全的网站推广方案(SEO)
  17. 安卓Activity转场动画
  18. kata cantainer介绍及Ubuntu安装kata cantainer
  19. 与表达式p =0等价的c语言表达式是,2015年3月全国计算机二级C语言选择第1套
  20. final、finally与finalize三者的区别

热门文章

  1. 2021信息管理与信息系统专业保研(情报学|管理科学与工程)
  2. java火星坐标转百度坐标_各种地理坐标系的转换,火星坐标,百度坐标,wsg84等...
  3. 制度罚则-- 日志规范
  4. QListWidget自定义item的两种方式(二)——使用QWidget作为item
  5. C语言英尺和英寸换算米
  6. LuoguP4234_最小差值生成树_LCT
  7. ThinkPHP--initialize()方法
  8. rgb转换 css 字体
  9. PHP smarty
  10. MATLAB--特征值和特征向量 及具体应用