五子棋人机对战
差不多是我一年前做的项目了,今天翻回来再回顾回顾,总结下。
1. 项目流程
UI界面部分
利用Java的图形界面工具swing和awt来绘制棋盘的框架,绘制了15X15的网格。然后在画布上增加监听器来监听鼠标点击的部分,然后在邻近的网格交点处绘制棋子,这样就实现了下棋的效果。
逻辑处理部分
19X19的棋盘,就用19X19的二维数组来存储棋盘的信息。每当玩家新增加一个棋子,后台部分就会遍历一遍数组,对每一个槽进行8个方向深度为5的DFS搜索,判断是否有五子相连的情况,这样就实现了输赢的判断。
人机对战原理
简而言之就是统计棋型累计评分,例如4个我方或对方的棋子相连,那么权重就是一个非常大的数,用来表示这个位置非常重要;如果是一个3个棋子相连也赋予一个对应权值。我把这种棋局用字符串表示,权重用整型数表示,以键值对的方式存储在一个HashMap中,那么这个哈希表就是电脑的判断棋局的依据。接着在电脑出手的时候,就遍历一遍二维数组,同样是对每一个槽进行8个方向的DFS搜索,之后查询哈希表累计该槽的权值。遍历完之后,权值最大的位置就是电脑下棋的位置。
人机对战进阶
极大值极小值搜索
初始版本的AI只能根据当前的局势判断下一步,而极大极小值算法可以自己先多走几步,预判未来的局势再决定当前的局势。过程是这样的:
先设定好一个搜索树层数 Depth
,即要模拟 Depth
步;因为每一方都想赢,所以每一步的模拟都是模拟当前角色的最优下法,那么从AI方计算的视角来看,AI下棋的时候应该要使得全局的权值最大,对方下棋的时候就应要使得全局的权值最小;其实双方的最优下法逻辑是一样的,只不过从我方来看对方要_加个负号_,也可以说是我要使我的分数尽量大,对方要使我的分数尽量小。最后,综合模拟的若干步累计权值,取其中权值最大的下法下棋。
但是这个算法的开销很大,因为假设棋盘有N个空位,那么这棵搜索树的第一层就有N个节点,第二层就有N-1个节点……这样下去时间复杂度和空间复杂度都是指数级的增长,所以为了优化我们需要剪枝来舍去不必要的搜索。
剪枝
这个算法就是给搜索树上的每一个节点维护一个下界为上界为
的窗口。这个窗口从父亲到儿子直接传递,从儿子回溯到父亲就要进行相应变化,因为搜索树结果信息是自底向上传递的,所以更新也是自底向上更新。如果当前为对方节点,那么对方会尽量使可取值小,故下拉上界
;若当前使我方,则会尽量使可取值大,故上推下界
。
如果发现那么就可以剪枝了,为什么呢?首先一个前提是,当前的每一步都是由AI来模拟对于每一个下棋这的最优走法。假设当前为MAX方,那么由某个子节点的信息MAX方更新了
发现
,说明当前MAX节点可获得一个值大于由对方约束的最大值,那么对方是肯定不会走这里的,所以本节点之后的其他子节点就没有继续搜索的必要了;同理,假设当前为MIN方,那么由某个子节点的信息MIN方更新了
,若发现
,说明当前的MIN节点可以获得一个值小于对方约束的最小值,那么对方是肯定不会走这里的,所以本节点的其他子节点可以剪掉。
项目中遇到的问题
2. 知识点涉及
HashMap
哈希表本质上就是一个数组,只不过是通过哈希函数映射某个关键码来获取数组的下标。正是利用了数组空间连续查询高效,所以哈希函数如果设计得好,能够在的复杂度下实现查找。
但是容易出现冲突,就是不同的关键码通过哈希函数映射出同一个值,这个时候就需要解决冲突的策略。大体上分为两种:
DFS
本质上就是状态的转移。它从某一个初始状态开始,一直转移到状态无法转移为止。如果把搜索的过程看作一棵树,那么信息的传递就会是自下而上或者是自上而下,然后对应的最终的完整信息状态就会出现在根或者是叶子。对于我这个五子棋,信息的传递就是自上而下的,也即最终某个方向上棋局的状态是搜索到叶子处获得。
3. 亮点
4. 可改进之处
- 剪枝后的效率还是不太高,3层的延时在2s左右
-
5. 我的收获