ImageVerifierCode 换一换
格式:DOCX , 页数:47 ,大小:76.22KB ,
资源ID:8576869      下载积分:3 金币
快捷下载
登录下载
邮箱/手机:
温馨提示:
快捷下载时,用户名和密码都是您填写的邮箱或者手机号,方便查询和重复下载(系统自动生成)。 如填写123,账号就是123,密码也是123。
特别说明:
请自助下载,系统不会自动发送文件的哦; 如果您已付费,想二次下载,请登录后访问:我的下载记录
支付方式: 支付宝    微信支付   
验证码:   换一换

加入VIP,免费下载
 

温馨提示:由于个人手机设置不同,如果发现不能下载,请复制以下地址【https://www.bdocx.com/down/8576869.html】到电脑端继续下载(重复下载不扣费)。

已注册用户请登录:
账号:
密码:
验证码:   换一换
  忘记密码?
三方登录: 微信登录   QQ登录  

下载须知

1: 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。
2: 试题试卷类文档,如果标题没有明确说明有答案则都视为没有答案,请知晓。
3: 文件的所有权益归上传用户所有。
4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
5. 本站仅提供交流平台,并不能对任何下载内容负责。
6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

版权提示 | 免责声明

本文(关于棋软中国象棋程序设计探索.docx)为本站会员(b****5)主动上传,冰豆网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对上载内容本身不做任何修改或编辑。 若此文所含内容侵犯了您的版权或隐私,请立即通知冰豆网(发送邮件至service@bdocx.com或直接QQ联系客服),我们立即给予删除!

关于棋软中国象棋程序设计探索.docx

1、关于棋软中国象棋程序设计探索关于棋软中国象棋程序设计探索最专业的免费象棋软件网站 棋中论坛 (一) 引言 2005年2月我写出了象棋程序ElephantEye的第一个版本(0.90),本来它只是象棋界面ElephantBoard的调试引擎。在设计程序的过程中,我尝试性地加入了很多算法,发现每次改进都能让程序的棋力有大幅度的提高,因此便对象棋程序的算法产生了浓厚的兴趣。到现在我已经陆续对ElephantEye作了几十次加工(目前版本为1.05),使得它的棋力接近了顶尖商业软件的水平,在非商业的象棋程序中,ElephantEye无疑是最强的一个。 我希望能通过公开源代码的方式,推动中国象棋程序水平

2、的整体发展,然而根据很多网友的反馈意见,发现源代码中的很多部分并不是那么容易理解的,为此我花了大量的时间为源程序加了注释。尽管如此,很多思想还是有必要以文字的形式保留下来,因此我才打算以中国象棋程序设计探索为题,写几篇详细介绍ElephantEye算法的连载,希望能让源代码充分发挥它的作用。 总的来说,对弈程序是个系统工程,它是以下四个系统的有机结合:(1) 棋盘结构,(2) 局面评价,(3) 搜索技术,(4) 其他。以ElephantEye为例,这四个部分在程序中的比例各占25%,也就是说,每个方面都很重要。那么这四个部分应该以什么样的方式逐步建立呢?另一个公开源代码的程序VSCCP(Ver

3、y Simple Chinese Chess Program)给出了一个方向,这是本很好的对弈程序设计的入门教材。尽管VSCCP在棋力上还有很大的提升空间,但是它的结构体系是比较完整的,参考下面一组公式,找到有待提升的空间,只要稍作改进就能成为ElephantEye。 棋盘结构 = 局面表示 + 着法移动 + 着法生成 + 特殊局面判断 局面评价 = 知识 + 优化的局面表示 搜索技术 = 完全搜索 + 静态搜索 + 启发 + 裁剪 + 选择性延伸 + 置换表 + 残局库 + 并行技术 其他 = 开局库 + 时间控制 + 后台思考 + 引擎协议(二) 棋盘结构和着法生成器 最专业的免费象棋软件

4、网站 棋中论坛 在阅读本章前,建议读者先阅读象棋百科全书网站中对弈程序基本技术专题的以下几篇译文: (1) 数据结构简介(David Eppstein); (2) 数据结构位棋盘(James Swafford); (3) 数据结构旋转的位棋盘(James Swafford); (4) 数据结构着法生成器(James Swafford); (5) 数据结构0x88着法产生方法(Bruce Moreland); (6) 数据结构Zobrist键值(Bruce Moreland); 2.1 局面和着法的表示 局面是象棋程序的核心数据结构,除了要包括棋盘、棋子、哪方要走、着法生成的辅助结构、Zobri

5、st键值等,还要包含一些历史着法,来判断重复局面。ElephantEye的局面结构很庞大(见),其中大部分存储空间是用来记录历史局面的。 struct PositionStruct int nMoveNum; MoveStruct mvMoveListMAX_MOVE_NUM; / MAX_MOVE_NUM = 256 unsigned char ucRepHashREP_HASH_LEN; / REP_HASH_LEN = 1024 其中MoveStruct这个结构记录了四个信息:(1) 着法的起始格(Src),(2) 着法的目标格(Dst),(3) 着法吃掉的棋子(Cpt),(4) 着法是

6、否将军(Chk)。有意思的是,每个部分都只占一个字节,后两个部分(Cpt和Chk)与其说有特殊作用,不如说是为了凑一个32位整数。在MoveStruct出现的很多地方(置换表、杀手着法表、着法生成表)里,这两项都是没作用的,而只有在PositionStruct结构的记录历史着法的堆栈中才有意义。 Cpt一项主要用在撤消着法上,它可以用来还原被吃的棋子,而Chk一项则可以记录当前局面是否处于将军状态。ElephantEye是用MakeMove()函数来走棋的,每走完一步棋就做两次将军判断:第一次判断走完子的一方是否被将军,即Checked(sdPlayer),如果被将则立即撤消着法,并返回走子失

7、败的信息;第二次判断要走的一方是否被将军,由于交换了走子方(即执行了sdPlayer = 1 - sdPlayer),所以仍旧是Checked(sdPlayer),如果被将则Chk置为TRUE,这个着法被压入历史着法堆栈。因此LastMove().Chk就表示当前局面是否被将军。 2.2 循环着法的检测 Cpt和Chk的另一个作用就是判断循环着法:ElephantEye判断循环着法时,依次从堆栈顶往前读,读到吃过子的着法(Cpt不为零)就结束;而是否有单方面长将的情况,则是通过每个着法的Chk一项来判断的。 在循环着法的检测中,我们感兴趣的不是Cpt和Chk,而是RepHash结构,这是一个微

8、型的置换表,用来记录历史局面。ElephantEye在做循环着法的判断这之前,先去探测这个置换表,如果命中置换表,则说明可能存在重复局面(由于置换表可能有冲突,所以只是有这个可能),因而做循环检测;如果没有命中则肯定没有重复局面。 ElephantEye使用“归位检测法”来判断循环着法,即从最后一个着法开始,依次向前撤消着法,并记录每个移动过的棋子所在的格子。如果所有移动过的棋子同时归位,那么循环着法就出现了。因此中的IsRep()函数建立了两个归位数组,第一个记录了棋子的原始位置,第二个记录了新的位置,当两个位置重合时,说明棋子归位。 2.3 棋盘-棋子联系数组 众所周知,棋盘的表示有两种方

9、法。一是做一个棋盘数组(例如Squares109),每个元素记录棋子的类型(包括空格);二是做一个棋子数组(例如Pieces216),每个元素记录棋子的位置(包括被吃的状态)。如果一个程序同时使用这两个数组,那么着法生成的速度就可以快很多。这就是“棋盘-棋子联系数组”,它的技术要点是: (1) 同时用棋盘数组和棋子数组表示一个局面,棋盘数组和棋子数组之间可以互相转换。 (2) 随时保持这两个数组之间的联系,棋子移动时必须同时更新这两个数组。 根据这两个原则,棋盘-棋子联系数组可以定义为: struct PositionStruct int Squares90; int Pieces32; ;

10、在棋盘上删除一个棋子,需要做两个操作(分别修改棋盘数组和棋子数组)。同样,添加一个棋子时也需要两个操作。执行一个着法时有三个步骤: (1) 如果目标格上已经有棋子,就要先把它从棋盘上拿走(吃子的过程); (2) 把棋子从起始格上拿走; (3) 把棋子放在目标格上。 ElephantEye用一个函数MovePiece()来完成这项任务,它除了修改棋盘数组和棋子数组外,还修改Zobrist键值、位行和位列等信息。 “棋盘-棋子联系数组”最大的优势是:移动一步只需要有限的运算。对于着法产生过程,可以逐一查找棋子数组,如果该子没有被吃掉,就产生该子的所有合理着法,由于需要查找的棋子数组的数量(每方只有

11、16个棋子能走)比棋盘格子的数量(90个格子)少得多,因此联系数组的速度要比单纯的棋盘数组快很多。可以说,“棋盘-棋子联系数组”是所有着法生成器的基础,位行和位列、位棋盘等其他方法都只是辅助手段。 2.4 扩展的棋盘数组和棋子数组 如今,很少有程序使用Squares90和Pieces32这样的数组了,浪费一些存储空间以换取速度是流行的做法,例如ElephantEye就用了ucpcSquares256和ucsqPieces48。把棋盘做成16x16的大小,得到行号和列号就可以用16除,这要比用9或10除快得多。16x16的棋盘还有更大的好处,它可以防止棋子走出棋盘边界。 000102030405

12、060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b

13、9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff在中国象棋里,短程棋子(短兵器)指的是除车和炮以外的其他棋子,它们的着法都有固定的增量(行的增量,列的增量),因此处理起来非常简单,也是着法生成技术的基础。例如马有8个着法,增量分别是0x0e、0x1

14、2、0x1f和0x21,红方的过河兵有3个着法,增量分别是-0x10和0x01。 16x16的扩展棋盘如上图所示,底色是红色的格子都被标上“出界”的标记,目标格在这些格子上就说明着法无效。这样,马的着法产生就非常简单了: const int cnKnightMoveTab8 = -0x21, -0x1f, -0x12, -0x0e, +0x0e, +0x12, +0x1f, +0x21; const int cnHorseLegTab8 = -0x10, -0x10, -0x01, +0x01, -0x01, +0x01, +0x10, +0x10; for (i = MyFirstHorse

15、; i MyLastHorse; i +) / 在ElephantEye的Pieces48中,红方的MyFirstHorse为21,MyLastHorse为22。 SrcSq = ucsqPiecesi; if (SrcSq != 0) for (j = 0; j 8; j +) DstSq = SrcSq + cnKnightMoveTabj; LegSq = SrcSq + cnHorseLegTabj; if (cbcInBoardDstSq & (ucpcSquaresDstSq & MyPieceMask) = 0 & ucpcSquaresLegSq = 0) MoveListMo

16、veNum.Src = SrcSq; MoveListMoveNum.Dst = DstSq; MoveNum +; 上面的代码是着法生成器的典型写法,用了两层循环,第一层循环用来确定要走的棋子,第二层循环用来确定棋子走到的目标格。如果要加快程序的运行速度,第二个循环可以拆成顺序结构。这个代码还加入了蹩马腿的判断,马腿的位置增量由ccHorseLegTabj给出。 其它棋子的着法也同样处理,只要注意帅(将)和仕(士)把InBoardDstSq改为InFortDstSq就可以了。而对于兵和象等需要考虑是否能过河的棋子,判断是否过河的方法非常简单:红方是(SrcSq/DstSq & 0x80) !

17、= 0,黑方是(SrcSq/DstSq & 0x80) = 0。 Pieces48这个扩展的棋子数组比较难以理解,实际上用了“屏蔽位”的设计,即1位表示红子(16),1位表示黑子(32)。因此0到16没有作用,16到31代表红方棋子(16代表帅,17和18代表仕,依此类推,直到27到31代表兵),32到47代表黑方棋子(在红方基础上加16)。这样,棋盘数组Squares256中的每个元素的意义就明确了,0代表没有棋子,16到31代表红方棋子,32到47代表黑方棋子。这样表示的好处就是:它可以快速判断棋子的颜色,(Piece & 16)可以判断是否为红方棋子,(Piece & 32)可以判断是否

18、为黑方棋子。 “屏蔽位”的设计不仅仅限制在判断红方棋子还是黑方棋子,如果在棋子数组上再多加7个屏蔽位,就可以对每个兵种作快速判断,例如判断是否是红兵,不需要用(Piece = 27 & Piece = 31),而只要简单的(Piece & WhitePawnBitMask)即可。这样的话,棋子数组的大小就增加到212=4096个了,其中9个屏蔽位,还有3位表示同兵种棋子的编号(注意兵有5个,所以必须占据3位)。事实上,确实有象棋程序是使用Pieces4096的2.5 着法预生成数组 上面提到的着法生成技术,在速度上并不是最快的。我们仍旧以马的着法为例,在很多情况下,马会处于棋盘的边缘,所以往往

19、着法只有很少,而并不需要对每个马都作8次是否出界的判断。因此,对于每个短程子力,都给定一个2564到2569不等的数组,它们保存着棋子可以到达的绝对位置,这些数组称为“着法预生成数组”。例如,ElephantEye里用了ucsqKnightMoves25612和ucsqHorseLegs2568,前一个数组的第二个维度之所以大于8,是因为着法生成器依次读取数组中的值,读到0就表示不再有着法(12则是为了对齐地址)。程序基本上是这样的: for (i = MyFirstHorse; i = MyLastHorse; i +) SrcSq = ucsqPiecesi; if (SrcSq != 0

20、) j = 0; DstSq = ucsqKnightMovesSrcSqj; while (DstSq != 0) LegSq = ucsqHorseLegsSrcSqj; if (!(ucpcSquaresDstSq & MyPieceMask) & ucpcSquaresLegSq = 0) MoveListMoveNum.Src = SrcSq; MoveListMoveNum.Dst = DstSq; MoveNum +; j +; DstSq = ucsqHorseMovesSrcSqj; 和前一个程序一样,这个程序也同样用了两层循环,不同之处在于第二个循环读取的是着法预生成数组,

21、DstSq从ucsqHorseMoves25612中读出,LegSq从ucsqHorseLegs2568中读出。 2.6 位行和位列 车和炮的着法分为吃子和不吃子两种,这两种着法生成器原则上是分开的,因此分为车炮不吃子、车吃子和炮吃子三个部分。不吃子的着法可以沿着上下左右四条射线逐一生成(即并列做4个循环)。我们感兴趣的是吃子的着法,因为静态搜索只需要生成这种着法,能否不用循环就能做到?ElephantEye几乎就做到了。 “位行”和“位列”是目前比较流行的着法生成技术,但仅限于车和炮的着法,它是否有速度上的优势还很难说,但是设计程序时可以减少一层循环,这个思想就已经比较领先了。以“位”的形式

22、记录棋盘上某一行所有的格子的状态(仅仅指是否有子),就称为“位行”(BitRank),与之对应的是“位列”(BitFile),棋盘结构应该包含10个位行和9个位列,即: struct PositionStruct unsigned short wBitFiles16; unsigned short wBitRanks16; ; 值得注意的是,它仅仅是棋盘的附加信息,“棋盘-棋子联系数组”仍旧是必不可少的。它的运作方式有点和“棋盘-棋子联系数组”类似: (1) 同时用位行数组和位列数组表示棋盘上的棋子分布信息,位行数组和位列数组之间可以互相转换; (2) 随时保持这两个数组之间的联系,棋子移动时

23、必须同时更新这两个数组。 因此,移走或放入一颗棋子时,必须在位行和位列上置位: void AddPiece(int Square, int Piece) x = Square % 16; y = Square / 16; wBitFilesx = 1 (y - 3); wBitRanksy = 1 (x - 3); 车和炮是否能吃子(暂时不管吃到的是我方棋子还是敌方棋子),只取决于它所在的行和列上的每个格子上是否有棋子,而跟棋子的颜色和兵种无关,因此这些信息完全反映在位行和位列中。预置一个“能吃到的格子”的数组,以位行或位列为指标查找数组,就可以立即知道车或炮能吃哪个子了。预置数组到底有多大呢

24、? / 某列各个位置的车或炮(10)在各种棋子排列下(1024)能走到的最上边或最下边的格子 unsigned char ucsqFileMoveNonCap1010242;/ 不吃子 unsigned char ucsqFileMoveRookCap1010242; / 车吃子 unsigned char ucsqFileMoveCannonCap1010242; / 炮吃子 / 某列各个位置的车或炮(9)在各种棋子排列下(512)能走到的最左边或最右边的格子 unsigned char ucsqRankMoveNonCap95122; 数组中的值记录的是目标格子的偏移值,即相对于该行或列第

25、一个格子的编号。产生吃子着法很简单,以车吃子位例: for (i = MyFirstRook; i = MyLastRook; i +) SrcSq = ucsqPiecesi; if (SrcSq != -1) x = SrcSq % 16; y = SrcSq / 16; DstSq = ucsqFileMoveRookCapy - 3wBitFilesx0; / 得到向上吃子的目标格 if (DstSq != 0) DstSq += x * 16; / 注意:第x列的第一个格子总是x * 16。 MoveListMoveNum.Src = SrcSq; MoveListMoveNum.D

26、st = DstSq; MoveNum +; / 再把FileMoveRookCap.0替换成.1,得到向下吃子的着法 DstSq = ucsqRankMoveRookCapx - 3wBitRanksy0; / 得到向左吃子的目标格 if (DstSq != 0) DstSq += y; / 注意:第y行的第一个格子总是y。 MoveListMoveNum.Src = SrcSq; MoveListMoveNum.Dst = DstSq; MoveNum +; / 再把RankMoveRookCap.0替换成.1,得到向右吃子的着法 2.7 着法合理性的判断 ElephantEye搜索每个结

27、点时,着法都有四个来源:(1) 置换表,(2) 吃子着法生成器,(2) 杀手着法表,(3) 不吃子着法生成器。这四种来源分别代表了四种启发式算法:(1) 置换表启发,(2) 吃子启发,(3) 杀手着法启发,(4) 历史表启发,这会在以后的章节中介绍。 我们感兴趣的是杀手着法,它是以前搜索过的局面遗留下来的着法,当前的局面如果要使用这些着法,只需要做合理性的判断就可以了,如果杀手着法能产生截断,那么着法生成就没有必要了。因此,如何快速判断着法合理性,其意义可能比着法生成器还大。 ElephantEye判断着法合理性的程序包含在中,它分为三个步骤: (1) 判断棋子是否在棋盘上存在,如果不存在那么肯定不是合理着法; (2) 判断是否吃到自己一方的棋子,吃到自己棋子的着法肯定是不是合理着法; (3) 分兵种作额外的判断。 我们感兴趣的是相(象)马车炮四子的判断,其中相(象)的判断最简单,只需要满足3个条件: (1) 走成象步,ElephantEye里用了一个ccLegalMoveTab的数组; (2) 没有过河,即(SrcSq DstSq) & 0x80) = 0; (3) 没有被塞象眼,即ucpcSquares(SrcSq + Dst

copyright@ 2008-2022 冰豆网网站版权所有

经营许可证编号:鄂ICP备2022015515号-1