Java第十课——封装性和哈希表实现五子棋AI

一.Java封装性

Emm…为什么在这里讲封装性呢?主要是看到某一小伙伴的代码,是份五子棋的代码,感觉写的非常有条理,而我们码龄差不多,到现在我才意识到最开始学代码时的那句话——自己的类做自己的事。而码了一年才注意到这件事也是惭愧!
先讲讲什么是封装性,正如字面理解,就是把一个类要做的事情封起来,像一个小盒子,一般来说,类外调用时就只能调用方法或是利用get()来取值,内部的变量一般是不能直接获取的。
同时,封装性在内也被处理的很好。之前写代码时不喜欢在函数里加形参,不喜欢用构造方法来获取参数,就容易导致变量名不匹配而出错,其实也是内部封装没有做好的结果。另外内部部的参数也不习惯设置访问权限,也很容易引起数值被东改西改,回头找不到错误原因。
最后,封装性还是要回到那句话:自己的类做自己的事。拿之前写的五子棋来说,监听器类做了很多事情,但从封装性的角度来看,其实监听器只要做一件事情,判断事件源,然后选择性可以加上少量运算,再调用对应方法,这其实才是监听器应该做的事情。至于调用什么方法,就看事件源的需求了。
那么实现封装性的方法就多种多样了,通俗来讲就是依据访问性来加入修饰符。例如只在类内使用的变量一般加private,同包下用protected,常量用static final,只在方法里使用的变量放进形参,多用构造方法实现赋值可以避免破坏封装性…等等
拿五子棋举个例子,建立一个类,这个类实现了五子棋的所有方法(如:绘制,下棋,悔棋,AI等等)
里面的变量有

private static final int ROW = 18;// 行数
private static final int COL = 18;// 列数

private static final int Height = 50;// 格子和落子区间的高度
private static final int Width = 50;// 格子的和落子区间宽度

private static final int PHeight = 1000;// 棋盘的总高度
private static final int PWidth = 1000;// 棋盘的总宽度

private static final int Length = 75;// 棋盘外围到边界的长度

private static final int Diameter = 30;// 棋子的直径
private static final int Radius = Diameter / 2;// 棋子的半径

protected int type[][] = new int[ROW][COL];// 保存棋子的类型
protected int row[] = new int[Record];// 在悔棋中记录行位置
protected int col[] = new int[Record];// 在悔棋中记录列位置

最后我把五子棋的代码修正了,分为4类ChessPanel封装了和棋盘有关的所有方法;ToolPanel封装了工具栏(包括悔棋按钮,重新开始按钮和切换模式的拉下框);Frame窗体,包含前两个类的对象;Listener监听器。具体代码这里就不放了。

哈希表实现AI算法

1.什么是哈希表?
先讲讲哈希表,哈希表是一种数据结构,通过关键码值(Key)而访问对应权值(Value)。举个例子:哈希表像是一个仓库,仓库里堆满了上了锁的箱子,每个锁对应一把钥匙,当我们像得到箱子里的东西(Value)时就需要对应的钥匙(Key)来访问。从数学上来讲就是一种映射关系,比如定义映射f为:"A"表示100,"B"表示1000,那么自变量输入"A"就能得到100。
2.AI算法实现思路
大概了解机制之后,即使没完全弄清楚,那从利用哈希表实现AI来再分析分析。
想要实现电脑下棋,实质就是电脑扫描棋盘,找到最适合下棋的那个点落子就可以了,那么怎么选出这个点?那就要一个方法,使得电脑找到下棋位置最佳,或者说获得收益最大的点,于是我们需要定义一个权值(Value)来表示在某一点的收益大小,Value越大表示收益越高。接下来会有四个问题
1、权值怎么计算得到的?
2、怎么通过场上的局势来获取到对应的权值?比如玩家已经连成三子了,那如何让电脑知道并拿到这个三个子所对应的权值?
3、要判断多少种情况?每种情况都要判断吗?
4、权值的大小如何决定?比如连成三个子和连成两个子的权值肯定不一样,或是可能会出现两个点权值大小相同,如何权衡?

一个一个解决:
1、不难理解,在某一个点的权值应该要综合考量,从这个点出发,八个方向都要判断权值大小,最后在权值的和为这一点的权值大小。
2、这里便要用到哈希表,先定义一个String Key 来获取场上的情况,比如如果1表示黑子,2表示白子的话,"111"表示三个黑子连在一起,"121"表示黑白黑…而这个String便是哈希表里的Key,通过这个Key去访问权值Value而得到在当前方向的权值,哈希表的具体使用方法和创建会在后面介绍。
3、某个方向上的情况比较多,当然各种情况都判断理论上能更确切的贴近场上情况,但实际操作时可以忽略掉一部分意义不太大的。比如电脑作为白子: “1”,“11”,“111”,"1111"这四种情况是肯定不能忽略的,而如果黑子旁已经连了一个白子像: “12”,“112”,“1112”,“11112"也是需要判断的,但像"212”,“2212”这类的就可以忽略
4、权值大小的话,没有什么特别的数字和比例,可以自己摸索或是在网上摘录都可以,可以自己先思考一些决定性的大小:比如已经电脑自己连成四个了,那就给一个较大的权值
3.哈希表的使用方法
找到思路和实现方法之后就可以开始了,先了解哈希表的使用
创建:HashMap<K,V> 哈希表名 = new HashMap<K,V>();
这里的K,V 是变量类型,拿等下使用哈希表的创建举例子

HashMap<String, Integer> hm = new HashMap<String, Integer>();

使用前要把对应关系放进去,用put方法

hm.put("1", 10);
hm.put("11", 100);
hm.put("111", 1000);
hm.put("1111", 10000);
hm.put("", 0);

使用时用get方法:

String key = "";
...//得到对应Key
Integer value = hm.get(key);

4.具体实现
1、放进对应关系
这份代码的权值是没怎么思考过的,可以自己修改权值,而判断情况的种类可以当作参考

private void putinit() {
	hm.put("1", 10);
	hm.put("11", 100);
	hm.put("111", 1000);
	hm.put("1111", 10000);

	hm.put("2", 10);
	hm.put("22", 100);
	hm.put("222", 1000);
	hm.put("2222", 10000);
  
	hm.put("12", 20);
	hm.put("112", 200);
	hm.put("1112", 2000);
	hm.put("11112", 20000);
  
	hm.put("21", 20);
	hm.put("221", 200);
	hm.put("2221", 2000);
	hm.put("22221", 20000);

	hm.put("", 0);
 }

2、把各个位置权值放进权值数组
创建一个权值数组,专门用来放每个位置的权值,玩家每走一次便要清零,于是写一个方法AI()用来给权值数组赋值

private void AI() {
	for (int i = 0; i < ROW; i++) {
		for (int j = 0; j < COL; j++) {
			if (type[i][j] == 0) {// 当前位置是空的
				String dir = "";// direction也就是求权值方向
				// 对八个方向同求权值
				// 向上
				dir = "up";
				getvalue(i, j, dir);
				
				// 向下
				dir = "down";
				getvalue(i, j, dir);
				
				// 向左 ...
				// 向右 ...
				// 左上 ...
				// 右上
				// 左下
				// 右下
			}
		}
	}
 }

在这段代码里,我用另一个函数getvalue通过哈希表来获取对应位置的权值,因为方向不同对应的 i 和 j 的加减方式不同,所以要String direction来帮助判断方向
3、从哈希表里获取权值

// 与AI()配套的方法,用于求权值
private void getvalue(int i, int j, String dir) {
	String number = "";
	int color = 0;
	// 对八个方向同求权值
	// 以向上举例
	if (dir.equals("up")) {// 向上:k=j-1
		for (int k = j - 1; k >= 0; k--) {// 记住每个方向的终止条件不同
			if (type[i][k] == 0) {// 上面那个也是空的
				break;// 两个空的就退出
			} else {// 上面那个不是空的
				if (color == 0) {// color还没被赋值时,获取这个位置(也就是第二个位置)的棋子颜色
					color = type[i][k];
					number += type[i][k];
				} else if (color == type[i][k]) {// 从第三个位置之后,如果颜色与第二个位置相同则记录
					number += type[i][k];
				} else {// 颜色出现不同
					number += type[i][k];
					break;// 可直接退出
				}
			}
		}
	} else if (dir.equals("down")) {// 向下:k=j+1
		for (int k = j + 1; k < type[j].length; k++) {
			if (type[i][k] == 0) {
				break;
			} else {
				if (color == 0) {
					color = type[i][k];
					number += type[i][k];
				} else if (color == type[i][k]) {
					number += type[i][k];
				} else {
					number += type[i][k];
					break;
				}
			}
		}
	} else if () { // 向左:k=i-1 }
	...//八个方向都获取
	
	// 根据code取出hm对应的权值
	Integer v = hm.get(number);
	if (v != null) {
		value[i][j] += v;
	}
}

最后paint方法和悔棋的操作就不细说了,到这实现电脑下棋的操作算是完成了