用Python编写一个国际象棋AI程序


Posted in Python onNovember 28, 2014

最近我用Python做了一个国际象棋程序并把代码发布在Github上了。这个代码不到1000行,大概20%用来实现AI。在这篇文章中我会介绍这个AI如何工作,每一个部分做什么,它为什么能那样工作起来。你可以直接通读本文,或者去下载代码,边读边看代码。虽然去看看其他文件中有什么AI依赖的类也可能有帮助,但是AI部分全都在AI.py文件中。

AI 部分总述

AI在做出决策前经过三个不同的步骤。首先,他找到所有规则允许的棋步(通常在开局时会有20-30种,随后会降低到几种)。其次,它生成一个棋步树用来随后决定最佳决策。虽然树的大小随深度指数增长,但是树的深度可以是任意的。假设每次决策有平均20个可选的棋步,那深度为1对应20棋步,深度为2对应400棋步,深度为3对应8000棋步。最后,它遍历这个树,采取x步后结果最佳的那个棋步,x是我们选择的树的深度。后面的文章为了简单起见,我会假设树深为2。

生成棋步树

棋步树是这个AI的核心。构成这个树的类是MoveNode.py文件中的MoveNode。他的初始化方法如下:

def __init__(self, move, children, parent) :
  self.move = move
  self.children = children
  self.parent = parent
  pointAdvantage = None
  depth = 1

这个类有五个属性。首先是move,即它包含的棋步,它是个Move类,在这不是很重要,只需要知道它是一个告诉一个起子往哪走的棋步,可以吃什么子,等等。然后是children,它也是个MoveNode类。第三个属性是parent,所以通过它可以知道上一层有哪些MoveNode。pointAdvantage属性是AI用来决定这一棋步是好是坏用的。depth属性指明这一结点在第几层,也就是说该节点上面有多少节点。生成棋步树的代码如下:

def generateMoveTree(self) :
  moveTree = []
  for move in self.board.getAllMovesLegal(self.side) :
    moveTree.append(MoveNode(move, [], None))
 
  for node in moveTree :
    self.board.makeMove(node.move)
    self.populateNodeChildren(node)
    self.board.undoLastMove()
  return moveTree

变量moveTree一开始是个空list,随后它装入MoveNode类的实例。第一个循环后,它只是一个拥有没有父结点、子结点的MoveNode的数组,也就是一些根节点。第二个循环遍历moveTree,用populateNodeChildren函数给每个节点添加子节点:

def populateNodeChildren(self, node) :
  node.pointAdvantage = self.board.getPointAdvantageOfSide(self.side)
  node.depth = node.getDepth()
  if node.depth == self.depth :
    return
 
  side = self.board.currentSide
 
  legalMoves = self.board.getAllMovesLegal(side)
  if not legalMoves :
    if self.board.isCheckmate() :
      node.move.checkmate = True
      return
    elif self.board.isStalemate() :
      node.move.stalemate = True
      node.pointAdvantage = 0
      return
 
  for move in legalMoves :
    node.children.append(MoveNode(move, [], node))
    self.board.makeMove(move)
    self.populateNodeChildren(node.children[-1])
    self.board.undoLastMove()

这个函数是递归的,并且它有点难用图像表达出来。一开始给它传递了个MoveNode对象。这个MoveNode对象会有为1的深度,因为它没有父节点。我们还是假设这个AI被设定为深度为2。因此率先传给这个函数的结点会跳过第一个if语句。

然后,决定出所有规则允许的棋步。不过这在这篇文章讨论的范围之外,如果你想看的话代码都在Github上。下一个if语句检查是否有符合规则的棋步。如果一个都没有,要么被将死了,要么和棋了。如果是被将死了,由于没有其他可以走的棋步,把node.move.checkmate属性设为True并return。和棋也是相似的,不过由于哪一方都没有优势,我们把node.pointAdvantage设为0。

如果不是将死或者和棋,那么legalMoves变量中的所有棋步都被加入当前结点的子节点中作为MoveNode,然后函数被调用来给这些子节点添加他们自己的MoveNode。

当结点的深度等于self.depth(这个例子中是2)时,什么也不做,当前节点的子节点保留为空数组。

遍历树

假设/我们有了一个MoveNode的树,我们需要遍历他,找到最佳棋步。这个逻辑有些微妙,需要花一点时间想明白它(在明白这是个很好的算法之前,我应该更多地去用Google)。所以我会尽可能充分解释它。比方说这是我们的棋步树:

如果这个AI很笨,只有深度1,他会选择拿“象”吃“车”,导致它得到5分并且总优势为+7。然后下一步“兵”会吃掉它的“后”,现在优势从+7变为-2,因为它没有提前想到下一步。

用Python编写一个国际象棋AI程序

在假设它的深度为2。将会看到它用“后”吃“马”导致分数-4,移动“后”导致分数+1,“象”吃“车”导致分数-2。因此,他选择移动后。这是设计AI时的通用技巧,你可以在这找到更多资料(极小化极大算法)。

所以我们轮到AI时让它选择最佳棋步,并且假设AI的对手会选择对AI来说最不利的棋步。下面展示这一点是如何实现的:

def getOptimalPointAdvantageForNode(self, node) :
  if node.children:
    for child in node.children :
      child.pointAdvantage = self.getOptimalPointAdvantageForNode(child)
 
    #If the depth is divisible by 2, it's a move for the AI's side, so return max
    if node.children[0].depth % 2 == 1 :
      return(max(node.children).pointAdvantage)
    else :
      return(min(node.children).pointAdvantage)
  else :
    return node.pointAdvantage

这也是个递归函数,所以一眼很难看出它在干什么。有两种情况:当前结点有子节点或者没有子节点。假设棋步树正好是前面图中的样子(实际中每个树枝上会有更多结点)。

第一种情况中,当前节点有子节点。拿第一步举例,Q吃掉N。它子节点的深度为2,所以2除2取余不是1。这意味着子节点包含对手的一步棋,所以返回最小步数(假设对手会走出对AI最不利的棋步)。

该节点的子节点不会有他们自己的节点,因为我们假设深度为2。因此,他们但会他们真实的分值(-4和+5)。他们中最小的是-4,所以第一步,Q吃N,被给为分值-4。

其他两步也重复这个步骤,移动“后”的分数给为+1,“象”吃“车”的分数给为-2。

选择最佳棋步

最难的部分已经完成了,现在这个AI要做的事就是从最高分值的棋步中做选择。

def bestMovesWithMoveTree(self, moveTree) :
  bestMoveNodes = []
  for moveNode in moveTree :
    moveNode.pointAdvantage = self.getOptimalPointAdvantageForNode(moveNode)
    if not bestMoveNodes :
      bestMoveNodes.append(moveNode)
    elif moveNode > bestMoveNodes[0] :
      bestMoveNodes = []
      bestMoveNodes.append(moveNode)
    elif moveNode == bestMoveNodes[0] :
      bestMoveNodes.append(moveNode)
 
  return [node.move for node in bestMoveNodes]

此时有三种情况。如果变量bestMoveNodes为空,那么moveNode的值是多少,都添加到这个list中。如果moveNode的值高于bestMoveNodes的第一个元素,清空这个list然后添加该moveNode。如果moveNode的值是一样的,那么添加到list中。

最后一步是从最佳棋步中随机选择一个(AI能被预测是很糟糕的)

bestMoves = self.bestMovesWithMoveTree(moveTree)
randomBestMove = random.choice(bestMoves)

这就是所有的内容。AI生成一个树,用子节点填充到任意深度,遍历这个树找到每个棋步的分值,然后随机选择最好的。这有各种可以优化的地方,剪枝,剃刀,静止搜索等等,但是希望这篇文章很好地解释了基础的暴力算法的象棋AI是如何工作的。

本文由 伯乐在线 - 许世豪 翻译自 mbuffett

Python 相关文章推荐
跟老齐学Python之玩转字符串(2)更新篇
Sep 28 Python
详解python时间模块中的datetime模块
Jan 13 Python
python套接字流重定向实例汇总
Mar 03 Python
python http接口自动化脚本详解
Jan 02 Python
Python中enumerate()函数编写更Pythonic的循环
Mar 06 Python
用TensorFlow实现lasso回归和岭回归算法的示例
May 02 Python
Python爬虫框架scrapy实现downloader_middleware设置proxy代理功能示例
Aug 04 Python
Python实现按逗号分隔列表的方法
Oct 23 Python
python-itchat 获取微信群用户信息的实例
Feb 21 Python
python中使用ctypes调用so传参设置遇到的问题及解决方法
Jun 19 Python
Python实现FLV视频拼接功能
Jan 21 Python
python名片管理系统开发
Jun 18 Python
Python中给List添加元素的4种方法分享
Nov 28 #Python
Python列表(list)、字典(dict)、字符串(string)基本操作小结
Nov 28 #Python
跟老齐学Python之使用Python查询更新数据库
Nov 25 #Python
跟老齐学Python之使用Python操作数据库(1)
Nov 25 #Python
Python标准库os.path包、glob包使用实例
Nov 25 #Python
PHP魔术方法__ISSET、__UNSET使用实例
Nov 25 #Python
Python标准库之多进程(multiprocessing包)介绍
Nov 25 #Python
You might like
Javascript 获取链接(url)参数的方法[正则与截取字符串]
2010/02/09 Javascript
JavaScript学习笔记之获取当前目录的实现代码
2010/12/14 Javascript
jquery查找tr td 示例模拟
2014/05/08 Javascript
7个有用的jQuery代码片段分享
2015/05/19 Javascript
jQuery使用$.ajax进行即时验证实例详解
2015/12/11 Javascript
JS实现设置ff与ie元素绝对位置的方法
2016/03/08 Javascript
javascript实现可键盘控制的抽奖系统
2016/03/10 Javascript
Node.js 使用流实现读写同步边读边写功能
2017/09/11 Javascript
解决jquery有正确返回值但不执行success函数的问题
2018/08/20 jQuery
es6基础学习之解构赋值
2018/12/10 Javascript
Vue侦测相关api的实现方法
2019/05/22 Javascript
jQuery实现视频展示效果
2020/05/30 jQuery
vue将data恢复到初始状态 && 重新渲染组件实例
2020/09/04 Javascript
基于ajax实现上传图片代码示例解析
2020/12/03 Javascript
[46:49]完美世界DOTA2联赛PWL S3 access vs Rebirth 第二场 12.19
2020/12/24 DOTA
Python中利用函数装饰器实现备忘功能
2015/03/30 Python
详解Python爬虫的基本写法
2016/01/08 Python
Python使用pip安装报错:is not a supported wheel on this platform的解决方法
2018/01/23 Python
Python实现获取邮箱内容并解析的方法示例
2018/06/16 Python
python多线程+代理池爬取天天基金网、股票数据过程解析
2019/08/13 Python
在pycharm中配置Anaconda以及pip源配置详解
2019/09/09 Python
Python django搭建layui提交表单,表格,图标的实例
2019/11/18 Python
10款最佳Python开发工具推荐,每一款都是神器
2020/10/15 Python
python中_del_还原数据的方法
2020/12/09 Python
python批量生成身份证号到Excel的两种方法实例
2021/01/14 Python
S’well Bottle保温杯官网:绝缘不锈钢水瓶
2018/05/09 全球购物
美国家居装饰和豪华家具购物网站:One Kings Lane
2018/12/24 全球购物
Java基础面试题
2014/07/19 面试题
项目资料员岗位职责
2013/12/10 职场文书
专升本个人自我评价
2013/12/22 职场文书
质量承诺书怎么写
2014/05/24 职场文书
心理咨询专业自荐信
2014/07/07 职场文书
校园元旦活动总结
2014/07/09 职场文书
考试作弊检讨书范文
2015/01/27 职场文书
springboot利用redis、Redisson处理并发问题的操作
2021/06/18 Java/Android
浅谈sql_@SelectProvider及使用注意说明
2021/08/04 Java/Android