【cocos2D-x学习】12.第三个小游戏——五子棋

【目标】使用cocos2d编写一个简单的五子棋程序


【参考】

1、五子棋基本棋型及其特点

2、TestCpp工程

3、五子棋AI循序渐进(虽然是VB,但是写的很好)

4、人工智能 (这书其实很一般)

5、cococs2d-x 多线程加载plist


        9月份颓废了好一阵子,下旬的时候才重新上阵,看到周围的同事在玩五子棋,就忍不住自己也写了一个,写了半个多月。现在算是一个还算拿得出手的小作品。这里总结一下。


一、五子棋游戏框架

       作为一个只有一个棋盘和两种棋子的游戏来说,如果单纯只是一个人类选手对战的五子棋平台,那么整个架构比起扫雷来说复杂不了多少。

       但是这次却是我写过最复杂的一个程序了,其复杂性主要在于引入了AI。整体的架构如下:

【cocos2D-x学习】12.第三个小游戏——五子棋_第1张图片


        对这张图做一些解释:

1、界面部分

      抽象分层使用cocos的scene——layer——sprite机制,出于对于逻辑管理与图像处理混搭的恐惧症状,在本部分只集中管理UI,将逻辑处理部分抽象到了辅助层中。

2、辅助工具部分

      主要包括两个工具:

      一是回合管理。五子棋是两个棋手轮流下,途中可能发生走棋、悔棋、投降、胜利等等情况。另外开始的时候还要设置两个棋手的身份:是不是AI?总之,将这一部分的逻辑处理集中到了 playerController中,而 Gamelayer 继承了这个类,是在实际游戏过程中的回合管理者。

      二是触屏管理。在“8.自己的触屏管理——添加ClickManager”里面我就提到过这个东西。这次总体变化不大,只是优化了一下代码。需要接受触屏事件的类,只需要持有一个ClickDelegate变量,然后自己继承一下ClickListener就可以。总体机制类似于android中的onClickListener。

3、AI部分

      这是本次架构比较失败的一块,只要是写单实例的时候,没有考虑游戏重启的情况,后面改BUG的时候偷懒就没有调整接口。

      ComputerPlayer是供外部调用的接口部分,他主要承担两个功能:一是扮演电脑玩家,和玩家对弈;二是充当裁判,判定是不是已经胜利。

      而这些功能,算法实际上是由computerIntellegence(以下简记为CI)实现的。CI会调用Evaluator来评估场上局势,从而获取最佳的落子点,或者判断是不是已经获胜。

      至于评估的依据,则是来源于MapHelper中记录的数据,这里记录的数据主要有:用一个二维数组记录了地图上所有点的情况,另外用两个vector记录了两个玩家的落子记录。点和方向的数据结构则是由Point 和  Direction 两个类提供。

     另外在评估局势的时候,需要根据棋型打分。棋型的信息,由shape类提供



二、cocos2D-x中的多线程

    由于AI计算最佳落子点耗时比较久,不能放在UI线程来做,所以需要使用多线程。这里主要参照《cococs2d-x 多线程加载plist》一文实现,简单说就是

    1、添加pthread头文件依赖

    2、添加pthread库依赖

    3、使用pthread即可

    在移植到android的时候,倒是不需要做任何特殊的处理,毕竟linux一般都是带pthread的。


三、在界面处理中,在本程序过程中学习到的技术

      界面使用CCTMXTiledMap来实现,没有太多的难点。这里主要记录一些可能会遇到的问题。

1、label文字的刷新

      由于一般的CCLabelTTF在setSttring的时候,实际上需要重新创建一个新的 CCLabelTTF,代价太大,所以一般来说,如果可以预见到这个label需要改变内容,是不推荐使用CCLabelTTF的。根据实际情况,有以下两种选择。

     1)UI线程上的刷新:如果是只是需要在UI线程上刷新文字,那么可以参考cocos中帧率显示的机制,使用CCLabelAtlas,初始化的时候,指定一个字体文件,在需要改变的时候,使用setString即可,不过我在实际使用的时候,发现如果初始化的时候设置字符串为空字符串的话,后续可能不能显示,所以开始最好还是先把位置占好。

     2)非UI线程上的刷新:上面的方式基本可以满足大多数的需求,但是在某些机器上(谨慎怀疑和显卡有关),如果在非UI线程上,使用上面的更新方式,可能会在运行的时候,报出“This application has requested the Runtime to terminate it in an unusual way”的告警信息,并导致程序崩溃。可能是由于openGL绘制是不能在非UI线程上进行的。

     我这里给出一种规避方案。以我这里的需求为例,label字样内容需要在BLACK和WHITE两种中进行切换,那么事先就干脆创建两个label,一个是WHITE,一个是BLACK,两个重合放置,通过setVisible隐藏其中的一个来达到需求。由于setVisible只是设置一个标志位,并通知UI线程界面数据脏,所以并不会引发问题。当然代价就是需要多创建部件。


2、界面中,子布局获取父节点的引用

     我以前写的代码中,在需要获取父节点的引用时,通常都是在构造函数中传入,一来是交叉引用,二来不能使用默认的CREATE_FUNC宏,也比较麻烦。实际上,可以通过getParent来获取父节点的引用,不过代价是需要用cast进行类型转换,过多的类型转换当然也不是好的编码风格。


3、menu的元素间隔设置

alignItemsVerticallyWithPadding(float padding)


4、cocos中输出LOG

     cocos2d::CCLog,要在win32下输出还需要在main中打开调试宏,这个LOG是平台兼容的,比我自己写的好多啦。


5、几个很有用的类

    1)CCLayerMultiplex:实现layer切换,Test程序中大量用到,这样不需要切换Scene也可以进行布景切换

    2)CCMenuItemToggle:类似于combo-button,可以从多个选项中选取一个,当前的值,可以通过

dynamic_cast<CCMenuItemToggle*>(pSender)->getSelectedIndex();
    来获取


四、简单的AI设计

     总体来说,有了前面的扫雷和音乐炫台的设计,五子棋的UI设计还是很简单的,这次的重心是AI的设计。

1、AI的总体流程

    当轮到AI行动时,AI首先获得所有可以下的点。然后对其中的每一点,进行试探和评估,最终从中选出得分最大的点,作为自己的落子点。

    这里有两个最核心的点:试探、评估。下面分开来评述。


2、评估函数

    评估解决的问题是,给定两个棋局,判定哪一个形势更好。

    这个评估函数需要满足几个条件,好适应后面的MIN-MAX算法:首先要只和当前形势有关,和未来局势无关,这样就不需要附加信息。其次要同时和双方相关,这样无论是MIN还是MAX来调用,都可以获得同样的结果。

    这里采用了一个简单的方法:对一个指定的棋子来说,他的得分是和他所在的棋型有关的。比如说,如果他在一个五连当中,那立刻就赢了,就给他50000分;他如果横向冲四,纵向也冲四,那构成一个双四,给他10000分;依次类推。

     而对于一方势力来说,其得分就是他最高的那个棋子得分。而对于整个局势来说,得分就是双方得分的差值。

     需要注意的一点,由于一个棋子需要同时观察四个方向:横、竖、左上到右下、左下到右上。那么有质变性的棋型得分,就应该至少相差4倍。例如五连和活四,由于五连是立刻取胜,所以比活四高一个档次,那么得分至少要是活四的四倍。这样才能保证当有点A,在四个方向同时构成活四,另一个点B直接构成五连的时候,点B的评分更高。


3、min-max

    有了估值,下面来研究试探。简单来说,就是走几步,然后来进行一次评估,看这几步走的怎么样,最终选出一种最好的下法。

    MIN-MAX是基于深度搜索的一种简单博弈算法,他的思想是,双方都想会选最好的落子点。比如两个棋手A和B,局面估分是用A的得分减去B的得分,即

V = A - B
   那么,当轮到A下的时候,他就会选使得得分最高的那个点来下:

A'(1) : max {V(1, 0) = A(1) - B(0)}
   这个符号表达可能不规范,意思是最终选取的 A 的第一步(记为 A'(1)),是使得 A(1) - B(0) 得分最高的那个点。

    但是这个选择可能是不好的:因为没有考虑B的应对。如果考虑下面的场景(O是A的子,X是B的子,_是空格,左上角为(1, 1)点)

    _OO__

    _XXX_

   当前A的得分是活二 100分,B的得分是活三415分,如果A选择自己连成活三的话,那么局面估值就是0分,如果选择堵B的话,那么得分就是活二减去冲三,在我的程序中得分是 100 - 210 = -110分,所以A应该选择自己连成活三。

    不过显然,这是不正确的,因为无视了B的应对:如果不堵B的话,那么下一步B就会形成活四,那么此时的局面得分是  415 - 10000 = -9585,而如果堵B的话,那么得分可能会是 活二减去冲四,得分是 100 - 450 = -350。所以如果考虑B的应对的话,应该堵B,这也和我们人自己的认识是一样的。

   那么如何去考虑B的应对呢,可以认为,无论A选择了什么棋,B都会在A下过之后,选择对B最有利的那个点来下,就是

B'(1) : min { V(1, 1) = A(1) - B(1) }

   所以如果考虑B的应对,A的策略应该是:

A'(1) : max { V(1, 1) = A(1) - B'(1) }
   两者一综合,就是

A'(1) : max {  min{ A(1) - B(1) } }
   即取最大的那个最小值。

   这个循环可以一直不停地继续下去,B的行动也需要考虑A的应对。这里的B就是MIN选手,A就是MAX选手。

   用状态空间树也可以表征这个过程。如果把双方的选择路径用树来表示,那么可能是这样的


   这里考虑3步棋,那么如果A选择了第一步落在 A-1 点,B会落在哪一点呢?如果B落在 B-1 点,那么相应的A就会选择 A-12点,得分是100分,如果落在 B-2 点,A只能下在 A-13点,得分也是100分。也就是说,B下在B-1的预期得分是100分,下在B-2的预期得分也是100分,那么B随便在B-1和B-2间选一个就好了。这样一来,A-1点的预期得分就是100分。

    类似的,可以知道,如果下在A-2点,那么B就会选择下在 B-4点,A-2的预期得分是 -40分。两者一对比,显然A-1 的预期得分更高,A应该选择落在 A-1点。

   这个流程,我们可以在这张图上标示出来,点B-1标示B落完子,那么轮到A行动了,A是MAX选手,他会选择子节点中得分最高的进行上溯。而在A-1节点,是B在行动,他会选择子节点中得分最低的进行上溯,这个过程如下:

【cocos2D-x学习】12.第三个小游戏——五子棋_第2张图片

    如果AI按照这个方法选择最优点,那么就是所谓的MIN-MAX博弈算法。


4、alpha-beta剪枝

     对上面的算法来说,其复杂度是呈指数增长的,一般来说,一个棋手可以下的落子位有几百个,那么即使只考虑2~3步棋,也是不小的开销,更不要说更高的深度了。实际上,我们不需要遍历树的每一条路径,就可以获得最优解,有些路径一看就没必要走,这就是剪枝的思想。

     下面具体说alpha-beta剪枝

     1)先说alpha,alpha是记录一个max节点(即图中的MAX行动节点)当前可选的最大值,如上图中的现状点,首先评估A-1点的得分,遍历其子树最终的达到得分是100,那么此时“现状节点”可选的最大值alpha就是100。

     然后试探A-2节点。当获取到B-3节点的得分80之后,还有必要继续评估A-2的其他儿子吗?不需要了,因为B会选择最小得分点,那么A-2的最终得分,不会超过B-3节点的得分80分,所以剩下的B-4和B-5完全不用再看了,这样的剪枝,我们就称之为alpha-剪枝。

    2)然后说beta,beta是记录一个MIN节点当前可选的最小值,以A-2点为例,当前是B行动,他要选取最小得分点。评估完B-3点后,当前的beta值是80,(顺便一提,此时ALPHA是100,应该发生alpha剪枝),然后继续评估B-4点,发现比原来的最小值还要小,那么更新beta到-40分。

    然后试探B-5点,落子B-5后,先看看如果A下在A-17会怎样?结果局面得分是1000,超过了当前的beta值,这意味着,只要B下在B-5,那么A下过之后,局面的得分不会低于1000分(因为A会选择最优点),所以后面的A-18和A-19也完全不用再看了,我们称之为beta剪枝。


    那么,将上面的两种情况同时使用起来,就是所谓的alpha-beta剪枝。在我实际的代码使用中,在MIN-MAX搜索深度为2的情况下,使用alpha-beta剪枝,大约能减少50%以上的计算量,还是非常有效的。


   最后附上 源代码: http://download.csdn.net/detail/ronintao/6380249

   其实AI还有很大的优化空间,不过最近估计也没有什么时间去做优化了,以后掌握了新的算法再系统的改进吧。

你可能感兴趣的:(【cocos2D-x学习】12.第三个小游戏——五子棋)