`
javatgo
  • 浏览: 1179126 次
  • 性别: Icon_minigender_2
  • 来自: 北京
文章分类
社区版块
存档分类
最新评论

围棋AI之路(二)棋盘的实现

阅读更多
代码先公布:http://download.csdn.net/source/891878

到现在为止,我只实现了一个棋盘,确切的说是在棋盘上随机走棋的速度测试程序,我借鉴了lib-ego,在上面做了一些改进,现在这个棋盘可以使用围棋规则或者五子棋规则。我的目标是让我的AI程序用同样的算法来对待围棋、五子棋甚至小时候玩过的黑白棋,它不需要任何棋类知识,你只要告诉它下棋的规则。我们的脑细胞可曾了解究竟什么是围棋?它们只是机械的执行自己的职能,而亿万个细胞堆叠在一起就使人类会下棋了。

上面说的三种棋的棋盘有一些共同的特点:棋盘是由n行n列的平行线段交叉组成的格子,棋子分黑白两种颜色,棋手分为两方,分执一种颜色的棋子。双方轮流下子,每次下一个子,棋子要下在空的交叉点上(黑白棋似乎是下在格子里,但是应该没有本质区别)。

根据这些特点我们开始设计棋盘的结构。

一、比特棋盘

很想在围棋中使用比特棋盘,就像国际象棋中那样,用一个64bit的数就描述了棋盘上的一种棋子。围棋上尽管也可以做到,例如用一个361bit的数来描述棋盘上的黑棋,另一个361bit数描述白棋,但是没见过谁这么做。

一般还是用传统的数组来描述棋盘,数组的每个元素有三个状态:黑(black)、白(white)、空(empty)。

为何计算机不是三进制的?我以前曾经这么想过,如果计算机是三进制的,会不会能更好的描述围棋?

后来我发现,其实棋盘上的点不只三个状态,还漏掉了一个off_board,也就是棋盘外的点。因此棋盘其实是4进制的,和2进制的计算机还是契合的不错的。

如何理解off_board也是一种状态?我们可以观察一下棋盘的边界,边界再往外就是off_board了,对围棋来说,通常的一颗子有4口气,但是到边界上就变成三口气或者两口气了,就仿佛边界外有敌人的子一样。对于五子棋,如果对方冲四冲到边界上,就不用挡了,就好像棋盘外有自己的棋子给它挡住了一样。

我按这种物理意义来为这些状态指派2进制数:

empty 00
black 01
white 10
off_board 11

这里empty就是没有棋子,black和white分别有一个棋子,而off_board则是同时有两个棋子,哪方的棋子靠近它,它就表现为另一方。

这样做的好处是,我可以用一个8bit的数来描述一个棋子的邻点,8bit总共256种情况,非常适合查表,通过查表,我就能得知任何情况下交叉点的“气”了。

关于计算交叉点的“气”,lib-ego中采用的另一种方法,它仅仅只增量计算交叉点周围黑、白、空三种情况的数量(off_board就分摊到黑白两种情况上了),而不管具体分布情况。目前我还没有发现我的方法表现出来的优势,但是我坚信我的方法比lib-ego中的好,因为它合乎道。

看起来,可以用一个8bit的数来存4个位置的状态,那么整个棋盘总共需要56个64bit数,比国际象棋没多太多,然而最终我没有贯彻比特棋盘的思想,因为我觉得那样不自然,我仍然选用传统的数组方式。

二、代码优化

许多人都指出优化应该晚做。但是对一份已经优化过的代码,如果不了解其优化手段,很难明白一些代码的意义。

1 使用编译期常量来代替变量。
例如棋盘的尺寸这个量,棋子的坐标计算依赖于它,为一些结构分配多大空间也与这个量相关。为了避免运行期再去计算这些东西,我们可以用宏或者const int来定义它:
  1. constuintboard_size=9;
但是我们希望程序可以运行在9路,13路,19路棋盘上,而且运行中可以改变棋盘,因此我采用了template。基本棋盘结构类似下面这样:
  1. template<uintT>
  2. classVertex{
  3. uintidx;
  4. public:
  5. conststaticuintcnt=(T+2)*(T+2);
  6. };
  7. template<uintT>
  8. classBoard{
  9. public:
  10. staticconstuintboard_size=T;
  11. Colorcolor_at[Vertex<T>::cnt];
  12. };

这里Vertex表示棋盘的交叉点,Vertex的内部实现不用类似class CPoint{int x;int y;};这样的方式实现,而只用一个整数来表示坐标,因为许多时候处理一维数组时的速度要快过二维数组,尽管理论上它们是一样的。

2 控制循环
如果在代码中看到这样的宏定义
  1. #definecolor_for_each(col)\
  2. for(Colorcol=0;color::in_range(col);++col)

而充斥在代码中的大片的vertex_for_each_all、vertex_for_each_nbr的使用,C++的死忠们不要急于排斥它,(我知道C++中有“优雅”的不依赖宏的方式来实现for_each,我也知道这样带来了一种方言),请先考虑一下为何需要for_each。

首先我们不希望在代码中出现大量for(;;)这样的语句,因为它会让代码行变的难看,并且以后修改困难。其次,我们有根据情况选择是否循环展开的需求。

  1. //所谓循环展开就是,正常代码这样:
  2. for(inti=0;i<4;i++){code;}
  3. //循环展开的代码是:
  4. i=0;code;
  5. i=1;code;
  6. i=2;code;
  7. i=3;code;
循环展开的效率提升不能一概而论,它与代码块的长度和循环次数都有关系,但是宏赋予了我们控制的能力。
这两个要求我不知道除了宏还有什么简单的方法可以做到。

3 避免条件语句
因为条件语句会影响CPU的指令缓存的命中率。为人熟知的一个用位运算来取代条件语句的例子是:
  1. Playerother(Playerpl){
  2. if(pl==black)returnwhite;
  3. elsereturnblack;
  4. }
改为位运算就是这样:
  1. Playerother(Playerpl){
  2. returnPlayer(pl^3);
  3. }
这里要假定black为1,white为2才能成立。如果black为0,white为1,则代码要改成(pl ^ 1)。
不过就这个例子来看,在我的CPU上没发现有什么效率的变化。在没有什么有说服力的例子出来之前,姑且存疑。

4 控制inline
需要清楚一点,inline不一定能提高运行速度。作为一个例子,请将代码中play_eye函数前面的no_inline修饰换成all_inline(表示总是内联),再编译运行一次看看,消耗的时间居然翻倍,为什么会这样?
这个函数的调用场景是:
  1. if(...){
  2. returnplay_eye(player,v);
  3. }else...
实际运行中,play_eye的调用频度不太高,如果内联的话,那么前面的if判断如果走的不是play_eye的这个分支,就会导致指令指针跳过很长一段代码到达下面的分支,因此指令缓存会失效。

你也许会说现代编译器能把这些做的很好,不用你操心这些细节了。那好吧,其实我只是建议,在瓶颈的地方手工指定一下是否内联,也许会有意想不到的性能提升。(注意inline这个关键字只是建议编译器内联,编译器不做保证,但是编译器通常都提供了额外的指令让你精确控制要不要内联。)

5 查表代替运算
不要迷信查表,因为表通常存在内存中,而你的指令放在CPU的指令缓存中,如果一两条指令能算出来的东西你去查表有可能得不偿失。


三、类的设计

一般来说,表示规则和表示棋盘的类会实现为一个类,如果把规则和棋盘分开来的话,那么应用代码可以创建一个棋盘类,再根据要求附加不同的规则类,类似下面这样写:
  1. Board<T>board;
  2. board.attach(newGoRule<T>());
  3. board.play(...);

看起来很优雅对不对?
但是在最终决定如何设计类结构之前,先看两点性能上的要求:

1) 不使用虚函数
原因是,除了虚函数表的空间开销,以及调用时多出来的几条机器指令外,虚函数使得编译器难以实现inline,因为虚函数是迟绑定,运行时才决定调用的函数是哪个,而C++编译器一般只能进行编译期的inline。

2) 棋盘可以快速被拷贝
记得我们的目的是让棋盘可以模拟很多盘随机对局,每一次随机对局都应该在原有棋盘的一个拷贝上进行,如果拷贝一次棋盘的代价很高的话,模拟的效率会很低。

现在,我们要否决上面的代码了,因为我们不能new一个规则类,这会破坏棋盘的快速拷贝能力,我所能想到的最快的棋盘拷贝代码是用memcpy,如果棋盘的数据成员含有指针,memcpy出来的棋盘会有问题。

继承怎么样呢?我们定义一个Board接口,也就是纯虚类,然后从这个接口继承,这是很通用的优雅解决方案,但是用到了虚函数。而且单继承会导致类数量过多,例如,我有一个基础的BasicBoard类,现在我希望能实现邻点计数功能,那么我写了一个NbrCounterBoard从BasicBoard类继承,我们的GoBoard可以从NbrCounterBoard继承。围棋还需要计算每一步棋的hash值,用以判定局面重复,那么我要实现一个ZorbistBoard,它从NbrCounterBoard继承,最终的GoBoard就从ZorbistBoard继承。黑白棋不需要计算hash,它可以直接从NbrCounterBoard继承,五子棋两个特性都不需要,那么它直接就从BasicBoard继承。一切听起来很完美,但这只是运气好而已,如果有一种棋需要hash但不需要邻点计算,这样的设计就over了。

组合可以吗?当然可以。看下面:

  1. classGoBoard{
  2. private:
  3. ZorbistBoardzb;
  4. public:
  5. BasicBoardbb;
  6. };
  7. //如果把组合类设为私有,许多功能你还需要中转一下
  8. voidGoBoard::foo(){returnzb.foo();}
  9. //如果设成公有,那么需要很啰嗦的调用形式
  10. GoBoardboard;
  11. board.bb.bar();
  12. //更要命的是,
  13. //如果ZorbistBoard::foo()需要调用BasicBoard::bar(),你会这样写代码吗?
  14. voidZorbistBoard::foo(GoBoard*pGB){
  15. pGB->bb.bar();
  16. }
  17. voidGoBoard::foo(){returnzb.foo(this);}

我们看到,这样子代码显得很罗嗦。这把我由组合引向了多继承:
  1. classGoBoard:
  2. publicBasicBoard<GoBoard>,
  3. publicZobristBoard<GoBoard>
  4. {
  5. };

我借鉴了ATL库的做法,把GoBoard当做模板参数传进去,这样,当ZobristBoard需要调用BasicBoard方法时,可以这样做:
  1. template<typenameDerive>
  2. classZobristBoard{
  3. public:
  4. voidfoo(){
  5. Derive*p=static_cast<Derive*>(this);
  6. p->bar();
  7. }
  8. };


四、模拟对局

我们这样来进行一场模拟对局:双方在规则的允许下,轮流下棋,当一方没有棋可下时,就pass,而双方接连pass时对局终止。对围棋和黑白棋来说,这样的过程是适应的,对于五子棋,我们需要加上中盘获胜的判断,实际上围棋中也可以用中盘胜来加快模拟速度,即一方已经明显优势的情况下,就不需要进行到双pass终局了。

首先我们看看围棋规则如何实现,围棋的三大规则,即提子(气尽棋亡)、打劫、禁同,造就了围棋的复杂性。如果没有提子,双方无论怎么下结果都是一样,如果没有打劫,双方互不相让也使得没有终局的可能。而禁同,也就是禁止全局同形,则可以看成是打劫的一般情况,也是为了防止对局无法终止。

还有一种情况也会导致对局无法终止,那就是双方都自填眼位,虽然这种情况理论上可以被禁同规则所限制,但是我是等不到对局结束的那一天了,何况这种求败并且寄希望于对方也求败的下法,在博弈程序中是不必考虑的。因此,在我们的随机模拟中,还要加上一条不填眼的规则。

在提子中还有一个分支,就是提自己的子,也即是自杀,一般比赛中是不允许自杀的,但是应氏规则中好像是允许的。模拟中肯定要禁止单个棋子的自杀行为,因为这也会导致无法终局(同上面一样,这种情况可以被禁同所限制,后面再说禁同的问题),但是多子的自杀究竟要不要在模拟中禁止?lib-ego中没有禁止,但是我发现禁止或不禁止导致的模拟胜率是有差异的,为了让模拟对局更贴近实际对局规则,我选择禁止多子自杀,尽管这需要更多的计算。

这样,在模拟中需要实现提子、打劫、不填眼、不自杀、禁同5个规则。而理论上我们只需要实现提子和禁同两个规则。

1 禁同
如果要实现禁同,我们需要为每一步棋形成的局面记录一个hash值,为了减少冲突的可能,一般使用64bit的hash值,然后如果这个hash值与以前的hash重复,则把这一步棋撤销。平均一局棋大概不超过1000步,那么进行二分查找是能够快速的判断hash重复的,但是如何撤销一步棋呢?要知道围棋是有提子的,如果这步棋出现提子,则撤销时还要将提去的子也放回来。每次提子记住那些被移走的棋子的位置,这是一个办法。lib-ego中采用了一种简单的、低效的的手段:无论是判断是否重复还是进行撤销,都根据历史棋步,把整个棋局重新下一遍。这种方法我初看时也觉得效率太低了,但是后来想通了,因为这样做,只额外存储了历史棋步,额外计算了hash。

其实这就是在表明,放弃在模拟对局中实现禁同,禁同只用到真正下棋的判断中。甚至我觉得更进一步,在模拟棋盘中,历史棋步与hash计算都不需要。因为现实对局中的全局同型是少之又少的,而检测全局同型的开销又太大,我们在模拟中设定一个棋局最大步数,凡是超过这个步数的模拟对局都弃掉不用,这样就绕开了禁同的问题。

2 提子
为了高效的判断棋子的气,这里用到了“伪气”的技巧。只要有一个空的交叉点,那么这个交叉点周围的每个棋子都能得到一口气,这就叫伪气。举图为例
├┼┼┼┼
├┼┼┼┼
○○○┼┼
●●○┼┼
└●○┴┴

上图黑棋真实的气只有一口,但是按伪气来说,就有两口,因为那个空点连着两个黑子,每个黑子都算有一口气。

按照伪气的计算法,每下一子,就减掉上下左右共4口气,每提走一子,则加上4口气。有了伪气这个工具,再来计算提子就简单多了,伪气为0的棋串就从棋盘上移走。

那么棋串怎么弄呢?我们把棋串实现为一个循环链表。一开始单个棋子就自己和自己首尾相连,并且拥有一个棋串id(就取它的位置作为id值),如果两个棋子相邻了,而棋串id不同,那么把它们合并为一个棋串,由于它们都是循环链表,合并的过程就相当于两个环扭断再对接成一个更大的环,于是合并的结果依然是循环链表。

3 打劫
打劫用了一个简单的方式来判定:如果能够在对方眼的位置下子,并且刚好只提了一个子,那么提去的那个子的位置被记录为劫争位,劫争位每次下子前被清除,也就是说只要不下在劫争位,pass也好,劫争位就被清除,下次那个位置就被允许下子。

4 填眼
下围棋的应该知道如何判断真眼和假眼,当在棋盘中间被对方占据两个“肩”或者边角处被对方占据一个“肩”,眼就是假眼了,我们随机模拟时,只要这个眼还没有确诊为假眼,我们就不往眼里下子。这里会存在误判,例如下图,白棋两个眼按照我们的规则判断是假眼,但白棋是活棋:
├┼┼┼┼┼┼┼
●●●●●●┼┼
○○○○○●●┼
├○●●○○●┼
○●●┼●○●┼
○●┼●●○●┼
○●●○○○●┼
└○○○●●●┴

不过没有关系,我们禁止填眼的目的是让大多数情况都能终局,而不是防止电脑把活棋走死。

5 自杀
单子自杀的判定是,当在对方眼中下棋时,将上下左右的棋串的气依次减1,如果没有棋串的气等于0,那么这就是一次自杀行为,我们把气加回去,然后禁止它下这一手。如下图,白棋下A点是自杀,下B点不是自杀。
├┼┼┼┼
○┼┼┼┼
●○○┼┼
B●○┼┼
●●○┼┼
A●○┴┴

多子自杀的判定是,当在一个没有气的交叉点上下子时,先把上下左右的棋串的气减1,然后判断,如果既没有让对方棋串的气为0,也没有使自己的至少一个棋串的气不为0,那么这就是一次自杀,我们再把气加回去。如下图,黑棋下A点是自杀,下B点或者c点不是自杀。
├○┼┼┼┼┼
○┼○○┼┼┼
●●B●○┼┼
○○●○○┼┼
●○○●○○┼
A●○C●○┴

这里的要点是在合并棋串之前做判断,因为棋串一旦合并后就不方便拆开了。

五子棋规则的实现相比围棋要容易很多,只用仿照围棋棋串的合并算法,在4个方向上分别建立棋串,合并棋串后,判断一下4个方向上是否有棋串的长度大于等于5。对于五子棋的职业规则,如禁手和三手交换五手两打,我暂时就不考虑了。毕竟有黑石那么牛的程序在那里。

五、下一步

自然是引入UCT算法了,也有可能是UCG,也就是UCB for Graph。
分享到:
评论

相关推荐

    grid+svg+js实现简单的围棋棋盘

    在本文中,我们将深入探讨如何使用CSS Grid布局、SVG、JavaScript以及HTML来创建一个...通过对这些文件的详细研究和理解,你可以进一步完善和扩展这个围棋棋盘项目,使其具备更多的功能,如AI对战、在线多人游戏等。

    围棋 AI 硕士论文

    总之,《围棋AI硕士论文》深入剖析了围棋AI的设计原理和实现方法,展示了人工智能在棋类游戏领域的巨大潜力,同时也为其他复杂问题的解决提供了有益的启示。随着技术的进步,我们期待看到更加智能、更具人性化的围棋...

    Python-Tensorflow仿AlphaGo框架实现的AI围棋程序

    总之,通过这个项目,你不仅可以学习到Python编程和TensorFlow的基本用法,还可以深入理解人工智能在复杂问题解决中的应用,特别是如何将深度学习与强化学习方法相结合,创造出能够与顶尖人类玩家竞争的围棋AI。

    围棋与人工智能

    尽管围棋具有极高的复杂性,现代人工智能的研究和应用却使其成为重要的研究领域之一。围棋的人工智能研究不仅关乎游戏本身,还对人工智能学科的发展具有深远的影响。 人工智能(AI)是模拟和延伸人类智能的科学技术...

    [纯C语言 + Win32 API]一步一步写个围棋程序之八:棋盘来了

    然而,实现完整的围棋游戏还需要更多的工作,比如AI对战、悔棋、保存和加载棋局等功能。在实践中,可能还需要考虑性能优化,如使用双缓冲技术提高画面更新效率。 总结起来,本篇介绍的是如何使用纯C语言和Win32 API...

    围棋AI,基于蒙特卡洛树搜索(MCTS)算法 Java版.zip

    围棋AI是一种人工智能应用,它利用复杂的算法来模拟和学习围棋策略。在这个项目中,我们重点关注的是基于蒙特卡洛树搜索(MCTS)算法的实现,这是一种在围棋和其他棋类游戏中广泛应用的高效搜索方法。MCTS是概率搜索...

    人工智能实验围棋博弈

    在人工智能领域,围棋因其复杂性被誉为棋类智能的巅峰挑战之一。在2016年,Google的AlphaGo击败了世界冠军李世石,标志着人工智能在围棋博弈上的重大突破。 在Java语言实现围棋博弈的过程中,我们需要关注以下几个...

    围棋棋盘3D模型

    5. **互动功能**:在3D模型的基础上,可以添加交互功能,比如棋子的放置、移动,甚至可以设计出智能AI对弈。这需要编程技术,如C++、Unity脚本或者Unreal Engine蓝图系统来实现。 6. **导出与导入**:3D模型可以...

    Unity开发围棋源码(04是围棋)_unity围棋_围棋_Unity围棋

    在Unity游戏引擎中进行围棋应用的开发是一项技术性较强的工作,涉及到编程语言C#、Unity引擎的使用以及人工智能算法的应用。本项目是一个基于Unity的围棋游戏源码,主要亮点在于实现了围棋的提子算法,这对于游戏...

    51单片机实现围棋 单片机围棋实验

    实现围棋游戏,需要编写程序来模拟棋盘状态和棋子的移动规则。在编程时,通常会用到C语言或汇编语言,创建数据结构来表示棋盘状态,如二维数组。每个元素代表棋盘上的一个格子,可以存放黑白双方棋子的状态。程序...

    人工智能围棋

    《人工智能围棋:探索AI在棋盘游戏中的应用与学习机制》 人工智能围棋,以其独特的自我学习能力和对弈策略,已经成为现代围棋技术发展的重要里程碑。本文将深入探讨这一领域的核心概念,以及“Leela”这款人工智能...

    cgo.zip_visual c_人工智能_围棋_围棋 智能_围棋点目算法

    在实际的围棋AI中,点目算法通常是计算棋盘上双方占有地盘大小的一种方式。它涉及到复杂的边界判断和空点归属,有时还会用到特定的规则来处理特殊情况。初级的围棋AI可能采用简单的策略,如模拟有限步数的随机落子,...

    棋盘算法的实现C++

    棋盘算法的实现是计算机科学中的一个重要概念,尤其在游戏开发、图形学和人工智能领域有着广泛应用。本项目提供了一个基于C++的棋盘算法实现,适用于算法初学者进行学习和实践。C++是一种通用的、面向对象的编程语言...

    实施AlphaGoZero论文的围棋AI程序_C++_Python_下载.zip

    《AlphaGoZero围棋AI程序实现:C++与Python解析》 AlphaGoZero是谷歌DeepMind公司于2017年发布的围棋人工智能系统,它在无需任何人类棋谱的情况下,通过自我对弈学习,仅用三天时间就达到了超越顶尖人类棋手的水平...

    人工智能围棋的游戏界面

    源代码可能分为几个部分:用户界面的实现(如使用GUI库如PyQt或Tkinter)、围棋逻辑的处理(如棋盘状态的更新和合法性检查)、以及人工智能算法的实现(如基于深度学习的蒙特卡洛树搜索)。资源文件则用于构建游戏的...

    人工智能围棋5*5python代码

    通过这个项目,你可以逐步提升到更复杂的AI策略,如神经网络模型,如蒙特卡洛树搜索(MCTS),这对于实现更强大的围棋AI至关重要。 总之,"人工智能围棋5x5python代码"项目为学习者提供了一个有趣的平台,让他们...

    围棋-Python源码

    创建棋盘:使用二维数组或矩阵表示围棋的棋盘。可以根据游戏规则确定棋盘的大小,通常是19x19个交叉点。 定义玩家和空点:使用常量或枚举类型来定义两个玩家(黑棋和白棋)以及空点。 初始化棋盘:将棋盘上的所有...

    各类棋盘游戏合集的Unity源码(中国象棋、围棋、五子棋等)

    Unity的各类棋盘游戏合集源码,有单人AI,也有联机功能,可二次开发。源码内有中国象棋、围棋、五子棋、国际象棋、 泰国象棋、日本将棋、黑白棋、韩国将棋、空当接龙、扫雷、数独、九子棋法老激光棋。支持Unity编辑...

    [纯C语言 + Win32 API]一步一步写个围棋程序之十二:实现对弈

    6. **人工智能(AI)**:为了让程序能与人类玩家对弈,我们需要实现一个简单的AI算法。最基础的方法是基于启发式搜索,例如Minimax算法配合Alpha-Beta剪枝。这需要评估函数来判断棋局的好坏,并在有限的深度内寻找...

Global site tag (gtag.js) - Google Analytics