用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用户管理系统
Mar 13 Python
Python cookbook(字符串与文本)针对任意多的分隔符拆分字符串操作示例
Apr 19 Python
PyQt5实现无边框窗口的标题拖动和窗口缩放
Apr 19 Python
用Python下载一个网页保存为本地的HTML文件实例
May 21 Python
根据DataFrame某一列的值来选择具体的某一行方法
Jul 03 Python
详解Python中的type和object
Aug 15 Python
Python定时任务随机时间执行的实现方法
Aug 14 Python
Python算法的时间复杂度和空间复杂度(实例解析)
Nov 19 Python
python numpy数组中的复制知识解析
Feb 03 Python
在python中求分布函数相关的包实例
Apr 15 Python
python脚本监控logstash进程并邮件告警实例
Apr 28 Python
python中entry用法讲解
Dec 04 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
解析使用ThinkPHP应该掌握的调试手段
2013/06/20 PHP
PHP实现动态创建XML文档的方法
2018/03/30 PHP
使用laravel的migrate创建数据表的方法
2019/09/30 PHP
php libevent 功能与使用方法详解
2020/03/04 PHP
ext实现完整的登录代码
2008/08/08 Javascript
理解Javascript_10_对象模型
2010/10/16 Javascript
node.js中的buffer.length方法使用说明
2014/12/14 Javascript
Jquery中find与each方法用法实例
2015/02/04 Javascript
JS右下角广告窗口代码(可收缩、展开及关闭)
2015/09/04 Javascript
分享两款带遮罩的jQuery弹出框
2015/12/30 Javascript
js表单处理中单选、多选、选择框值的获取及表单的序列化
2016/03/08 Javascript
Javascript之BOM(window对象)详解
2016/05/25 Javascript
使用JavaScript判断用户输入的是否为正整数(两种方法)
2017/02/05 Javascript
JavaScript实现的数字与字符串转换功能示例
2017/08/23 Javascript
Node.js中Bootstrap-table的两种分页的实现方法
2017/09/18 Javascript
基于JavaScript实现每日签到打卡轨迹功能
2018/11/29 Javascript
[01:32]DOTA2次级联赛——首支职业女子战队选拔赛全记录
2014/10/23 DOTA
python中ConfigParse模块的用法
2014/09/29 Python
Python装饰器decorator用法实例
2014/11/10 Python
浅析Python多线程下的变量问题
2015/04/28 Python
探究python中open函数的使用
2016/03/01 Python
Python内置数据结构与操作符的练习题集锦
2016/07/01 Python
详解如何使用Python编写vim插件
2017/11/28 Python
PyQt5每天必学之弹出消息框
2018/04/19 Python
Python 读取图片文件为矩阵和保存矩阵为图片的方法
2018/04/27 Python
python2 与 pyhton3的输入语句写法小结
2018/09/10 Python
Python3爬虫使用Fidder实现APP爬取示例
2018/11/27 Python
Python调用百度根据经纬度查询地址的示例代码
2019/07/07 Python
小班幼儿评语大全
2014/04/30 职场文书
初中学习计划书范文
2014/09/15 职场文书
小学秋季运动会报道稿
2014/09/30 职场文书
捐款感谢信
2015/01/20 职场文书
圣诞晚会主持词开场白
2015/05/28 职场文书
九年级历史教学反思
2016/02/19 职场文书
解决Navicat for Mysql连接报错1251的问题(连接失败)
2021/05/27 MySQL
解析python中的jsonpath 提取器
2022/01/18 Python