一、前言
前面我们讲述了如何进行绘制棋盘,以及悔棋的基本逻辑与算法实现,接下来我们就来讲讲大家最感兴趣的部分:如何实现人机对战。
二、获取权值
首先,我们来回想一下当我们在下棋时,是如何去决定下一步要下在哪里的?首先,我们要先纵观全局,看一看我们的棋子有没有已经成4个连子的情况,如果有的话肯定就下这个位置,这样我们这一回合就赢了。如果没有的话,那么接下来就要看对手的棋子有没有已经成4个连子的情况,如果有的话赶紧堵,否则对手这回合就赢了。接下来,我们就要判断我们有么有3连子且两头都没有被堵的情况,如果有的话肯定就下这些位置,这样下一回合我们就赢了。同理,如果我们没有这种情况,那么接下来就要判断对手有没有这种情况,如果有就赶紧堵,否则下回合对手就赢了。如果上述情况都不存在,那么我们接下来就需要尽可能的让局面出现上述的情况,且要防止对手出现这种情况。
那如何才能尽可能的让局面出现上述的情况,且要防止对手出现这种情况呢?我们可以进行一个类比,把规则想象成四子棋,上述局面变成了四子棋获胜的条件,既然上述情况没有出现,则表示如果这是一场四子棋,现在还没有决定胜负,为了防止对手获胜,让自己获胜,如何去决定下一步要下在哪里的思考过程和五子棋一模一样。
综上所述,我们可以得到这样的一个思想过程:
- 判断自己有没有4连子
- 判断对手有没有4连子
- 判断自己有没有3连子且没堵
- 判断对手有没有3连子且没堵
- 判断自己有没有3连子
- 判断对手有没有3连子
- 判断自己有没有2连子且没堵
- 判断对手有没有2连子且没堵
- 判断自己有没有2连子
- 判断对手有没有2连子
- 判断自己有没有1连子且没堵
- 判断对手有没有1连子且没堵
- 判断自己有没有1连子
- 判断对手有没有1连子
至于为什么同样的情况要先判断自己的再判断对手的,因为现在是我们下棋的回合,同样的情况先判断我们的,这样我们就能比对手更早一步到达下一阶段的情况。
其实上面的这套思想过程就可以形象的表现出这些情况的权重情况,越靠前的情况权重越大,越靠后的情况权重越小。这样我们就可以得到下面这张权重表(权值大小无所谓,只要靠前的情况一出现,后面的情况出现的再多也都不考虑了,因为每一格都有横向、纵向、左上到右下、右上到左下4种情况,所以每一层之间相差4倍以上即可,这里我选取了4倍)
编号 | 情况 | 权重 |
1 | 对手有且只有1子且被堵 | 4^0 |
2 | 自己有且只有1子且被堵 | 4^1 |
3 | 对手有且只有1子且没被堵 | 4^2 |
4 | 自己有且只有1子且没被堵 | 4^3 |
5 | 对手有且只有2子且被堵 | 4^4 |
6 | 自己有且只有2子且被堵 | 4^5 |
7 | 对手有且只有2子且没被堵 | 4^6 |
8 | 自己有且只有2子且没被堵 | 4^7 |
9 | 对手有且只有3子且被堵 | 4^8 |
10 | 自己有且只有3子且被堵 | 4^9 |
11 | 对手有且只有3子且没被堵 | 4^10 |
12 | 自己有且只有3子且没被堵 | 4^11 |
13 | 对手有4子 | 4^12 |
14 | 自己有4子 | 4^13 |
三、录入权值
有了权重表,那要如何将这些情况表示出来并录入呢?之前我们绘制棋盘时有说过,我们将整个棋盘看成一张二维数组,每一个元素只有0,1和-1三种情况,分别代表没棋子、1代表黑子,-1代表白子。这样就可以将棋盘转换成一个二维数组,每一种情况就可以表示成一串由数字组成的字符串。
有了表现方式,接下来我们便需要考虑情况了。因为我们下棋肯定是往没有棋子的位置下,这个位置肯定是数字0,那么所有的情况里肯定都会至少有一个0。接下来我们来仔细考虑所有的情况,首先规定以下规则:
- 遍历时遇到异色棋子停止遍历
- 遍历时遇到2格连续没有棋子停止遍历
- 遍历的第一格如果是空棋子位则停止遍历,且不录入情况字符串
- 遍历时遇到边界停止遍历
为了更直观地表现,我以一个例子作为解释(默认其余棋子与该位置相距很远(超过2格以上)):
这种情况的数字表达为:……01-10-1-110……
其中一共有三个空位(红色框),我们从左往右依次将这三个空位定为空位1、空位2和空位3。接下来我们对这三个空位进行情况分析
首先分析空位2,默认情况字符串为“0”,代表这个空位本身。
先往左遍历,往左一格是白子(-1),再一格是黑子(1),根据规则1,停止遍历,遍历结束后情况字符串为“1-10”;
再往右遍历,往右一格是白子(-1),再一格还是白子(-1),再一格是空位3(0),再一格还是空位(0),根据规则2,停止遍历,遍历结束后情况字符串为“1-10-1-100”。
于是,我们便得到空位2在横向维度上的情况字符串为“1-10-1-100”。
接下来我们分析空位1,默认情况字符串为“0”,代表这个空位本身。
先往左遍历,遍历的第一格为空棋子(0),根据规则3,停止遍历,且不录入情况字符串,遍历结束后情况字符串为“0”;
再往右遍历,往右一格是黑子(1),再一格是白子(-1),根据规则1,停止遍历,遍历结束后情况字符串为“01-1”;
于是,我们便得到空位1在横向维度上的情况字符串为“01-1”。
最后我们分析空位3,默认情况字符串为“0”,代表这个空位本身。
先往左遍历,往左一格是白子(-1),再一格还是是白子(-1),再一格是空位3(0),再一格是白子(-1),再一格是黑子(1),根据规则1,停止遍历,遍历结束后情况字符串为“1-10-1-10”;
再往右遍历,遍历的第一格为空棋子(0),根据规则3,停止遍历,且不录入情况字符串,遍历结束后情况字符串为“1-10-1-10”;
于是,我们便得到空位3在横向维度上的情况字符串为“1-10-1-10”。
接下来我们来分析权值。只有一个规则:存在多种情况时,只考虑权重最大的情况。
空位1的情况为“01-1”,属于情况2(自己有且只有1子且被堵),权重为4;
空位2的情况为“1-10-1-100”,往左看属于情况1(对手有且只有1子且被堵),往右看属于情况7(对手有且只有2子且没被堵),权重为4^6=4096;
空位3的情况为“1-10-1-10”,属于情况9(对手有且只有3子且被堵),权重为4^8=65536。
因为其余棋子相距很远,对这个位置的判断不造成影响,因此不存在竖直和斜向的情况(如果存在则需要分别考虑并相加),综上所述,权重最大的是空位3,接下来黑子会下在空位3。
有了情况字符串的获取规则,我们便可以在下棋前遍历棋盘中所有的空棋子位,计算他们四个方向上的权重并相加,最后计算出权重最大的点并在这个位置下棋子。
那么如何将这些所有可能出现的情况录入权值呢?我们可以使用java里的Collection里的哈希表(HashMap),它可以方便地定义两个对象的链接关系。这里我们需要链接的是情况字符串和权重值,因此我们可以这么定义:
HashMap<String, Integer> map = new HashMap<String, Integer>();
这样我们就实例化了一个哈希表的对象map,然后便可以往里面添加权重了。比如上面我举的例子,有三种情况,对应三个权重,我们就可以这样加进哈希表中:
map.put("01-1", 4);
map.put("1-10-1-100", 4096);
map.put("1-10-1-10", 65536);
最后,我们需要手动将所有可能出现的情况都考虑一遍,而且所有边界情况都要考虑到(也就是“0100”这种情况如果在边界也有可能出现“010”和“01”的情况,都需要录进去)。当我们将所有的情况都录入完成后,这个五子棋最人性化的AI便设计完毕了。录入的情况越多,AI越强大。可以根据这一点自定义五子棋AI的难度。
我几乎把所有可能出现的情况都考虑到了,最终整合到一个项目中。下载链接:https://download.csdn.net/download/thdgth/11945902