赞
踩
实际上现在的棋类AI都是采用了效率更高的算法(如蒙特卡洛树搜索)+Deep Learning实现。今天我们只探讨较简单的五子棋AI,大致有两种算法:五元组和博弈树。
第一节 Java 类与对象以及继承
第二节 Java 对象的保存和传递
第三节 Java 数组和列表的使用
第四节 Java 五子棋AI博弈树算法
五子棋UI界面设计和博弈树的概念在这里不做介绍(参考博客:五子棋人机对战(Java项目)),博主的UI具体效果如下:
我们先来梳理一下五子棋AI的设计流程:二者博弈,就是一场利益争夺战,那么最终结果就看博弈双方谁能够获得最大的价值。那么下棋前计算机需要遍历能够落子的点,然后找到价值最大的落子点。
那么现在又诞生一个问题:如何定义价值最大?
实际上不管是五元组还是博弈树,都需要有一个相应的“价值最大”的评分制度,我们把这个叫做权值表。
例如(只列举部分情况):
活三 | 活四 | 连五 |
---|---|---|
500 | 10000 | 50000 |
实际上,这就是建立了一种棋子状态与得分的映射关系,我们当然可以用 if - else if 语句来进行加分,也可以利用HashMap来建立这种映射关系。
HashMap是Java中最常用的集合类框架,也是Java语言中非常典型的数据结构。它的底层实现逻辑以及其特点我们不在本文介绍,现在我们只介绍一下HashMap的使用方法:
首先我们需要创建一个HashMap对象:
HashMap<K, V> hm = new HashMap<>();
显然HashMap创建时有使用到我们上一节介绍的“泛型”。(第三节 Java 数组和列表的使用)
显然,五子棋的权指表需要将其中的 K 定为String,V 定为Integer:
HashMap<String, Integer> hm = new HashMap<>();
之后就是添加权值对。与ArrayList类似,我们只需要用put方法即可添加需要的权值对:
public static void main(String[] args) {
HashMap<String, Integer> hm = new HashMap<>();
hm.put("yes",1);
hm.put("no",0);
}
之后使用get方法便可调取到映射:
public static void main(String[] args) {
HashMap<String, Integer> hm = new HashMap<>();
hm.put("yes",1);
hm.put("no",0);
String s = "no";
int a = hm.get("yes");
int b = hm.get(s);
System.out.println("a "+a);
System.out.println("b "+b);
}
a 1
b 0
得益于自动拆箱机制,a和b成功提取到对应Integer值。
学会使用HashMap后,现在我们可以着手获取落子状态了。首先我们可以把棋盘看做是15*15的二维数组,其中每个位置上黑棋为1,白棋为2,无棋为0。
对于博弈树算法,我们只需要计算每次落子时带来的收益,我们可以先假设计算机在一处落子,五子棋的规则表明,一个子可以在四个方向上影响周围子:上——下、左——右、左上——右下、右上——左下。因此我们需要获取的落子状态为落子后在四个方向上棋子的顺序。
这里介绍博主的思路:以水平方向(左——右)为例
public String[] search(String[] SearchArr, byte[][] seat, int X, int Y) { String Evel = ""; //保存水平方向排序 String Vertical = ""; //保存竖直方向排序 String LeftCross = ""; //保存正斜向排序 String RightCross = ""; //保存反斜向排序 String Evel = ""; byte color = 0; int i,j; color = seat[X][Y]; //seat数组保存当前棋局状态,类型为byte Evel += seat[X][Y]; //保存当前位置棋子颜色 //水平方向 //向左遍历 for(i=X-1,j=Y;i>=0;i--) { if(0 == seat[i][j]) { //判断相邻是否落子 break; }else if(seat[i][j] != color) { //判断相邻是否异色 Evel = seat[i][j] + Evel; //int、byte等类型会自动转为String类型保存 break; }else if(seat[i][j] == color) { Evel = seat[i][j] + Evel; } } //向右遍历 for(i=X+1,j=Y;i<NUM;i++) { if(0 == seat[i][j]) { //判断相邻是否落子 break; }else if(seat[i][j] != color) { //判断相邻是否落子 Evel += seat[i][j]; break; }else if(seat[i][j] == color) { Evel += seat[i][j]; } //竖直方向 略 //左上——右下方向 略 //右上——左下方向 略 SearchArr[0] = Evel; SearchArr[1] = Vertical; SearchArr[2] = LeftCross; SearchArr[3] = RightCross; return SearchArr; }
之后按照相同的思路,把四个方向的String全部保存即可。
博主的思路比较简单,没有考虑将“0”也保存进棋局,因为这样会使得棋子排序种类特别繁多,构建权值对将成为难题,当然如果考虑更多更全面的情况,AI的水平也会更高。
public void setHashMap() { hm.put("1", 0); hm.put("2", 0); hm.put("12",2); hm.put("21", 2); hm.put("212",1); hm.put("121", 5); hm.put("11",20); hm.put("22", 20); hm.put("112", 10); hm.put("211", 10); hm.put("122", 10); hm.put("221", 10); hm.put("2112", 5); hm.put("1221", 5); hm.put("111",200); hm.put("222", 200); hm.put("1112",12); hm.put("2221", 12); hm.put("2111",12); hm.put("1222", 12); hm.put("21112",5); hm.put("12221", 5); hm.put("1111",15000); hm.put("2222", 15000); hm.put("21111",120); hm.put("12222", 120); hm.put("11112",120); hm.put("22221", 120); hm.put("211112",5); hm.put("122221", 5); hm.put("11111",50000); hm.put("22222",50000); hm.put("211111",50000); hm.put("122222",50000); hm.put("111112",50000); hm.put("222221",50000); hm.put("2111112",50000); hm.put("1222221",50000); }
权值的设置博主没有仔细研究,权值设定仅供参考。
之后便是撰写getValue函数获取落子价值。
private int getValue(int x, int y) { int value = 0; String[] SearchArr = new String[4]; SearchArr = search(SearchArr, seat, x, y); //如果有五连则直接加50000(防止六连或更多连等极端情况) if(SearchArr[0].contains("11111") || SearchArr[0].contains("22222")) value += 50000; else if(SearchArr[1].contains("11111") || SearchArr[1].contains("22222")) value += 50000; else if(SearchArr[2].contains("11111") || SearchArr[2].contains("22222")) value += 50000; else if(SearchArr[3].contains("11111") || SearchArr[3].contains("22222")) value += 50000; else { value += hm.get(SearchArr[0]); value += hm.get(SearchArr[1]); value += hm.get(SearchArr[2]); value += hm.get(SearchArr[3]); } return value; }
public int[] doAI(int AIcolor) { int seatValue[][] = new int[NUM][NUM]; for(int n=0; n<NUM; n++) { for(int m=0; m<NUM; m++) { if(seat[m][n] != 0) { //检测是否有棋子 continue; } seat[m][n] = (byte)AIcolor;//如果没有棋子,假设下棋,并获取价值 seatValue[m][n] = getValue(m, n); seat[m][n] = 0; //还原假设的落子 } } int maxValue = 0; int X=0,Y=0; for(int n=0;n<NUM;n++) { for(int m=0;m<NUM;m++) { if(maxValue<seatValue[m][n]) { maxValue = seatValue[m][n]; X = m; Y = n; } } } int[] bestSeat = {X,Y}; return bestSeat; }
显然,这里AI只进行了1步预测,因此获得的落子得分只是估值,可能在几步之内有更大价值的落子点,我们可以在落子之后继续预测对方落子点,以此来获得更加精确的落子估值。这就需要通过递归算法实现,理论上,只要遍历深度足够,我们便能够得到所以落子的准确得分。但实际上遍历次数是随着深度成指数性增长的,我们必须优化算法来舍弃那些一眼就无需遍历的点:
public int[] doAI(int AIcolor, int deep) { int[][] seatValue = new int[NUM][NUM]; for(int n=0; n<NUM; n++) { for(int m=0; m<NUM; m++) { if(seat[m][n] != 0) { //检测是否有棋子 continue; } int num = 0; //检测周围2格内是否有棋子,若没有则不考虑该落子情况 for(int i = (n-2>0? n-2:0);i<NUM && i<n+2;i++) for(int j= (m-2>0? m-2:0);j<NUM && j<m+2;j++) num += seat[i][j]; if(0==num) { continue; } seat[m][n] = (byte)AIcolor; //如果没有棋子,假设下棋 seatValue[m][n] += getValue(m,n); //获取假设落子后的分数 int[] fore = new int[2]; if(BLACK==AIcolor) { if(deep>=0) fore = doAI(WHITE, deep-1); //deep不为负则继续预测 else fore = doAI(WHITE); //deep为负便不再预测 seat[fore[0]][fore[1]] = (byte)WHITE; seatValue[fore[0]][fore[1]] += getValue(fore[0], fore[1]); seat[fore[0]][fore[1]] = 0; } else if(WHITE==AIcolor) { if(deep>=0) fore = doAI(BLACK, deep-1); else fore = doAI(BLACK); seat[fore[0]][fore[1]] = (byte)BLACK; seatValue[fore[0]][fore[1]] += getValue(fore[0], fore[1]); seat[fore[0]][fore[1]] = 0; } seat[m][n] = 0; //还原假设的落子 } } int Value = 0; int X=0,Y=0; for(int n=0;n<NUM;n++) { for(int m=0;m<NUM;m++) { if(Value<seatValue[m][n]) { Value = seatValue[m][n]; X = m; Y = n; } } } int[] bestSeat = {X,Y}; return bestSeat; }
到这里,我们基于博弈树的五子棋AI算法基本完成。
以上就是今天要讲的内容。
实际上,当deep=1时(往后思考3步),每一步计算的时间已经有好几秒了,甚至复杂的情况需要十几秒,这时电脑已经具备了和常人下棋的能力。
当deep=2 时需要的时间已经长得离谱了。读者可以在此基础上继续优化,提高电脑的计算效率。
最后放上源代码以及打包完的exe文件(复盘功能无效):读者可以自行游玩。
链接:https://pan.baidu.com/s/1yxRKxMCmaLhXZ7P0MEAQOg
提取码:fqwu
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。