资源下载地址:https://download.csdn.net/download/sheziqiong/85677012
资源下载地址:https://download.csdn.net/download/sheziqiong/85677012
五子棋小游戏-tkinter版
一、实现内容
- 图形界面
- 局域网联机
- 人机对战
- 悔棋
- 先后手
- 重新开始
- 导出/导入棋盘
游戏规则
假设俩个人轮流报数,可以报 1、2、3 这三个数,然后积分榜累加这俩个人报的数,最先加到 6 的人输
这个游戏存在先手优势,即谁最先报数,就有必胜的方案www.biyezuopin.vip
博弈树
博弈树的树叶表示游戏的结局
下图中方块表示乙报完数后的局面(此时甲要开始报数了),圆圈表示甲报完数后的局面,由图可知甲先报数
对于甲来说,第一次不能报 2 和 3,因为这样乙总有办法让甲输,即图中红色路线
如果甲报数 1,那么无论第二次乙报什么数,甲总有路线让乙输,即图中蓝色路线
极大极小搜索
将报数游戏中,将甲(自己)获胜用 1 代替,将乙(对手)获胜用 -1 代替
以根节点(树的最顶端)作为第一个极大层(Max),极小层(Min)和极大层交替出现
极小层中选择子节点最小的数,极大层选择子节点最大的数
先手开始选择总是在偶数层(0、2、4…),而第二个选手开始选择总是在奇数层(1、3、5…),对应于先手位于极大层,第二个选手位于极小层,也就意味着,位于极大层的选手需要将自身利益最大化,会选择子节点中较大的那个,而位于极小层的选手会将对手的利益最小化,而选择子节点中最小的那个
根据上图可知,抽离博弈树的一部分后,这个子树的根节点是 -1,也就是说轮到乙选择了,局势变成这样的话,甲是不可能获胜的,因为乙肯定会选择对甲不利的 -1 这条路线
博弈树的最后结果
整个博弈树根节点的值所代表的就是先手在这场博弈中的结果(如果比赛双方都遵循最大最小值原则)
那些 -1 都是对甲不利的局面,也就代表了本场比赛的决胜权被对手掌握
井字游戏
打分函数
乙会尽力让评分降低,甲需要让评分更高,因此通过深度优先遍历,选择一条分值高的路径
打分函数根据日常经验来制定
令 α 为 -∞ 是为了让接下来任意一个比这个大的数可以替换掉 -∞
-
根据给定深度进行遍历(图中仅仅遍历 5 层)
首先将父节点的 α、β 值传递到叶子节点
negamax(board, candidates, role, i, MIN, MAX)
-
进行回溯,节点处于 Max 层,因此 α 变成 5
if v["score"] > best["score"]: best = v # best在遍历子节点时选取分数最高f
-
继续遍历兄弟树
从子节点中 7、4、5 选一个最小的 4 作为 β 的值
由于该兄弟节点的评分较小,回溯时不会改变 Max 层的 α 值
-
继续回溯至第 1 层,由于是最小层,因此 β 改为 5(目前子节点最小只有 5)
-
继续遍历第一层第一个节点的右子树
-
以此类推,获得的最优选择是评分为 6 的路径
这是全部遍历的情况,需要继续优化,参考α-β剪枝
代码实现
# minimax.py
def r(board, deep, alpha, beta, role, step, steps):
# ...
# 获取当前j棋盘分数
_e = board.evaluate(role)
leaf = {"score": _e, "step": step, "steps": steps}
# 搜索到底(搜索范围:给定 depth ~ 0)或者已经胜利, 返回叶子节点
if (deep <= 0 or func.greatOrEqualThan(_e, S["FIVE"])
or func.littleOrEqualThan(_e, -S["FIVE"])):
return leaf
best = {"score": MIN, "step": step, "steps": steps}
onlyThree = step > 1 if len(board.allSteps) > 10 else step > 3
points = board.gen(role, onlyThree) # 启发式评估, 获取整个棋盘下一步可能获胜的节点
# 如果没有节点, 即当前节点是树叶, 直接返回
if len(points) == 0:
return leaf
# 对可能获胜节点进行遍历
for item in points:
board.AIput(item["p"], role) # 在可能获胜的节点上模拟落子
_deep = deep - 1 # 深度减一
_steps = steps.copy() # 复制一下之前的步骤
_steps.append(item) # 步骤增加当前遍历的节点
# 进行递归, 总步数加一
v = r(board, _deep, -beta, -alpha, func.reverse(role), step + 1, _steps)
# 下一步是对手, 对手分数越高, 代表自己分数越低, 所以分数取反
v["score"] = - v["score"]
board.AIremove(item["p"]) # 在棋盘上删除这个子(恢复原来的棋盘)
if v["score"] > best["score"]:
best = v
# ...
return best
# minimax.py
def negmax(..., alpha, beta, ...):
# 当前处于极大层(根节点), 对 candidates 里的落子点进行遍历
# 找到最优解(alpha最大的)
for item in candidates:
# 在棋盘上落子
board.AIput(item["p"], role)
# 注意, alpha/beta 交换了, 因为每一层的 alpha 或 beta 只有一个会变
# 但是递归时需要传那个不变的进去
v = r(board, deep - 1, -beta, -alpha, func.reverse(role), 1, [item])
v["score"] = -1 * v["score"]
alpha = max(alpha, v["score"])
# 从棋盘上移除这个子
board.AIremove(item["p"])
item["v"] = v
return alpha
二、工作量
/(ㄒoㄒ)/~~ 左右互博和局域网联机做了我快一个星期, 一开始用的 pygame, 感觉按钮啊提示框啥的都要自己实现, 有点儿麻烦, 所以改用 tkinter了, 没想到这个也挺麻烦的, 网上的教程也很少
基本原理
根据评分表对某个位置进行评分
图中白子位置上
- —:+++AO+++
- |:+++AO+++
- \:+++O+++
- /:+++AO+++
A 代表敌方棋子,O 代表我方棋子
然后遍历棋盘的每一个位置,找到评分最高的位置落子,缺点是只顾眼前利益,电脑只能预测一步
Alpha Beta 剪枝
核心是固定深度
剪去 MAX 层叫 Alpha 剪枝
剪去 MIN 层叫 Beta 剪枝
触发剪枝的条件
-
当极小层某节点的 α 大于等于 β 时不需要继续遍历其子节点
下图中 α=5,说明我们存在一个使我们得分至少为 5 的情况,如果在遍历子节点的过程中,发现 β 小于 α 了,不会继续遍历后面的节点,因为后面的分数如果更大,对手不可能会选,如果后面的分数更小,对手肯定会选,那我们更加不能选这条路,因此不需要继续考虑了
对于极大极小值搜索一章的博弈树进行剪枝可得
-
当极大层某节点的 α 小于等于 β 时不需要继续遍历
因为如果后面的分数更低,我们没必要选,如果后面的分数高,会导致这条路分数更高,对手不会选这条路,没必要继续考虑
代码实现
# minimax.
def r(..., alpha, ...):
# ...
# 将 alpha 值与子节点分数做比较, 选出最大的分数给 alpha
alpha = max(best["score"], alpha)
# alpha-beta 剪枝
if func.greatOrEqualThan(a, beta):
ABcut += 1 # 剪枝数加一
v["score"] = MAX - 1 # 被剪枝的用极大值来记录, 但是必须比 MAX 小
v["abcut"] = 1 # 剪枝标记
return v
参考资料
Zobrist 散列算法
基本过程
不同的走法最终达到的局势相同, 则可以重复利用缓存中原来计算过的结果
根据 ABC = ACB 可知, 不同步骤只要进行异步运算的值相同, 则最终值相同, 利用 code 作字典的键值可以快速找到缓存中的数据
评分表
特征 | 分数 |
---|---|
活一 | 10 |
活二 | 100 |
活三 | 1000 |
活四 | 100000 |
连五 | 10000000 |
眠一 | 1 |
眠二 | 10 |
眠三 | 100 |
眠四 | 10000 |
# 三、结果
1. 首页
[![](https://img-blog.csdnimg.cn/img_convert/e308cadce5daff23b28ed54de3dbc2bf.png)](https://imgtu.com/i/IOnmWQ)
2. 本地开局
![](https://img-blog.csdnimg.cn/img_convert/3ae8f21a467aa7a88b9f4cd56100e1ba.png)
3. 获胜界面
![](https://img-blog.csdnimg.cn/img_convert/cf2320cd47c975deb1864953de693dc8.png)
4. 网络联机
> 需要先运行 server.py
![](https://img-blog.csdnimg.cn/img_convert/b01454699e9922194f1ad69b866742ef.png)
询问是否接受对战邀请
![](https://img-blog.csdnimg.cn/img_convert/a3fcf914f618c7c01e2e5b2e8e28200c.png)
可边下棋边聊天
![](https://img-blog.csdnimg.cn/img_convert/0aa02686073b6d79ea7ad7bee533e43a.png)
可拒绝/接受对方悔棋
![](https://img-blog.csdnimg.cn/img_convert/7d3f36ceaed84a653907ae13e490600b.png)
5. 人机模式
![](https://img-blog.csdnimg.cn/img_convert/92483faaa416b3e826c3f01ab2d2097c.png)
[资源下载地址](https://download.csdn.net/download/sheziqiong/85677012):https://download.csdn.net/download/sheziqiong/85677012
[资源下载地址](https://download.csdn.net/download/sheziqiong/85677012):https://download.csdn.net/download/sheziqiong/85677012