Java 五子棋AI博弈树算法实现

实际上现在的棋类AI都是采用了效率更高的算法(如蒙特卡洛树搜索)+Deep Learning实现。今天我们只探讨较简单的五子棋AI,大致有两种算法:五元组和博弈树。

Java学习笔记

第一节 Java 类与对象以及继承
第二节 Java 对象的保存和传递
第三节 Java 数组和列表的使用
第四节 Java 五子棋AI博弈树算法

目录

    • Java学习笔记
  • 前言
  • 一、HashMap的使用
  • 二、获取落子状态
  • 三、构建权值表并获取分数
  • 四、AI遍历与递归
  • 总结


前言

五子棋UI界面设计和博弈树的概念在这里不做介绍(参考博客:五子棋人机对战(Java项目)),博主的UI具体效果如下:
Java 五子棋AI博弈树算法实现_第1张图片

我们先来梳理一下五子棋AI的设计流程:二者博弈,就是一场利益争夺战,那么最终结果就看博弈双方谁能够获得最大的价值。那么下棋前计算机需要遍历能够落子的点,然后找到价值最大的落子点。
那么现在又诞生一个问题:如何定义价值最大
实际上不管是五元组还是博弈树,都需要有一个相应的“价值最大”的评分制度,我们把这个叫做权值表
例如(只列举部分情况):

活三 活四 连五
500 10000 50000

实际上,这就是建立了一种棋子状态与得分的映射关系,我们当然可以用 if - else if 语句来进行加分,也可以利用HashMap来建立这种映射关系。


一、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;
	}

四、AI遍历与递归

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

你可能感兴趣的:(java,eclipse)