python实现的扫雷游戏的AI解法(启发式算法)
相关:
本文中实现的《扫雷》游戏的AI解法的项目地址:
https://openi.pcl.ac.cn/devilmaycry812839668/AI_mine_game
该项目的解法效果:
之前介绍了网上的一些解决《扫雷》游戏的一些解法,包括DQN和启发式等AI算法,看着这些的实现个人有些手痒,于是就花了些时间自己用python代码实现了一个启发式方法求解《扫雷》游戏的算法。
求解《扫雷》游戏,很多人给出了很多启发式规则,但是实际上我个人认为就两条或者说三条,其他的那些规则属于在这两条或者说是三条规则基础上衍生的,实际上用这两条或三条规则就足够。
第一条:
如果一个格子揭开后其显示的周围有雷的数量和周边8个格子中未揭开的格子数量相同,那么说明这几个未被揭开的格子都是有雷的;
第二条:
如果一个格子揭开后其显示的周围有雷的数量和周边8个格子中标记的有雷的格子数量相同,那么说明其他几个未被揭开的格子都是无雷的。
第三条:
之所以第三条是独立出来的,因为这一条并不等同于前两条那么基础和必要,或者说没有前两条规则那么肯定不能行,但是没有第三条规则其实很多情况下也是可行的。但是有第三条的话会一定程度上提高我们的胜利比率。(注意:扫雷游戏在很多情况下是没有确定的胜利的情况的,也就是说在某种情况下能否胜利是要看概率的,而我们写的算法代码可以看作只是为了去尽可能接近这个概率而已)
第三条规则就是一个格子显示雷的数值在某些情况下是可以通过附近24个格子的数值进行优化的,比如一个格子的坐标为(x, y)那么在其-2,+2的范围下的其他标有数值的格子是可能和其进行化简的。
比如:一个格子坐标为(x, y)数值为3,其周边8个格子标有雷的数量为0,未被揭开的格子有四个,我们假设这4个未揭开的格子的真实有雷和无雷的情况分别用0或1表示,那么我们可以得到下面这个等式:
a+b+c+d=3
而坐标为(x+2, y+2)的一个格子可以得到下面的等式:
a+b+c=2
那么我们可以得到结论,那就是有无雷情况用d表示的格子肯定有雷,因为:
set(a,b,c,d)-set(a,b,c)=d
3-2=1
同理,如果假设(x-2, y-2)的一个格子的数值表示为a+b+c+d+e=3,那么我们可以判断出e这个表示的格子的情况一定为无雷的。
其实,第三条虽然是一种集合运算,但是其模拟的却是一种人类所用的数学推理的方法,在考虑使用这个方法之前曾经考虑过用python的线性代数运算library来实现这种数学推理的计算,但是感觉这么搞有些离谱,总觉得这个问题应该不至于到这个程度,然后盯着游戏画面好久,突然灵感一现,发现这个推理过程虽然看似是一个矩阵运算那种线性代数运算,但是如果只是小范围来看这个其实就是很简单的线性代数,因为每个变量的取值只能为0或1,并且可以完全使用for循环的方式加上set集合运算的方式实现消元化简,从而使用较为简单的编码方式就可以实现这种推理过程,而不需要搞进来一个线性数学的library,不过后来发现GitHub上的其他人实现的也都是大致用这种set结合加for循环的方式实现消元操作,看来这估计是正解。
在这三条的规则基础上我还加入了概率判断,这一点体现在随机进行格子选择时,我们可以根据一个未知格子周边(附近8个)已知格子的显示数值和这8个格子与其周边的另个格子中未知雷的数量计算出这个已知格子对这个目标的未知格子的又雷概率,我们可以取这个未知格子周边已知格子推断出的有雷概率取max,然后再计算出当前一共有多少未知格子和多少雷,然后计算出完全随机选格子的有雷概率,然后再在其中选择概率最小的,这样就能找到最小概率有雷的格子,以此实现最小概率触发雷。
再往下就是使用前面的前两个规则判断刚选的格子(假设此时为触发雷)是否可以判断出周边格子的情况。但是,这里我又加入一个规则,那就是一个格子被揭开显示数值后会影响其周边8个格子中未知格子的推理,导致这8个格子中的未知格子有可能被推理出来,因此我们将每个先揭开的格子或标记的格子其周边的格子保存起来,然后再对这些保存的各种进行判定,看其周边的格子是否可以被推理出来。
需要注意的是,一个格子被揭开显示数值或者被标记有雷后,其周边受影响可能被推断出的格子为附近8个,这时可以根据之前给出的前两条规则进行判断和推理,但是一个格子被揭开显示数值或者被标记有雷后其周边24个格子包括其自身,也就是共25个格子的set集合都有可能被推理化简,也就是之前所谓的for+set实现的消元操作。通过将这些规则结合在一起也就有了本文给出的代码实现。
最终的代码实现:
import numpy as np
import random
from typing import List
def belong_to(h, w, H, W):
near = []
for i in range(h-2, h+3):
for j in range(w-2, w+3):
if i>=0 and j>=0 and i<H and j<W and (i,j)!=(h,w):
near.append((i, j))
return near
def near_by(h, w, H, W):
near = []
for i in range(h-1, h+2):
for j in range(w-1, w+2):
if i>=0 and j>=0 and i<H and j<W and (i,j)!=(h,w):
near.append((i,j))
return near
def mine_count(h, w, real_state:np.array, H, W):
count = 0
for i, j in near_by(h, w, H=H, W=W):
if real_state[i][j]==1:
count += 1
return count
class Env():
def __init__(self, H, W, N):
self.H = H
self.W = W
self.N = N
# real state中0表示无雷,1表示有雷
self.real_state = np.zeros((H, W), dtype=np.int32)
self.mine = set()
while len(self.mine)!=N:
self.mine.add(random.randint(0, H*W-1))
for x in self.mine:
# print(x, self.H, self.W)
# print(self.real_state.shape)
self.real_state[x//self.W][x%self.W] = 1
# state_type中0表示无雷,1-8表示有雷, 用此来表示对附近雷的计数
self.state_type = np.zeros((H, W), dtype=np.int32)
for i in range(H):
for j in range(W):
self.state_type[i][j] = mine_count(h=i, w=j, H=H, W=W, real_state=self.real_state)
# obs为-100表示未翻开(未知),0-8表示翻开但无雷,数值大小表示翻开位置周边雷的数量
# agent的状态记录所用,也可以用来作为打印之用
self.obs = np.zeros((H, W), dtype=np.int32) -100
def act(self, i, j):
done = False
if self.obs[i][j]!=-100:
print("该位置已经被揭开过,重复翻开,error!!!")
return ValueError
if self.real_state[i][j] == 1:
# game over 触雷
done = True
return None, done
self.obs[i][j] = self.state_type[i][j]
return self.obs[i][j], done
def pp(self):
for i in range(self.H):
for j in range(self.W):
if self.obs[i][j]>=0:
print(self.obs[i][j], end=' ')
else:
print('*', end=' ')
print()
def input(self):
while True:
i, j = input('请输入坐标:').split()
_, done = self.act(int(i), int(j))
if done:
print('game over!!!')
print(self.real_state)
print(self.state_type)
break
self.pp()
# 测试用
# env=Env(5, 5, 5)
# env.input()
def play():
N = 99 # 雷的数量
H = 16
W = 30
env = Env(H=H, W=W, N=N) # H=36, W=64, N=100
known_count_dict = {} # (2,2):3, (3,3):2
known_set = set() # (2, 2)
unknown_set = set()
boom_set = set()
for i in range(H):
for j in range(W):
unknown_set.add((i,j))
new_nodes = []
new_relation_nodes_set = set()
while(len(unknown_set)>0):
probs_list = [] # ((1,1), 0.5, 3), ((2,2), 0.5, 2) # (node, prob, count) # count为node附近的unknown个数
for node in unknown_set:
p_list = []
n_c = 0 # node附近的unknown_node的个数
for _node in near_by(*node, H, W):
if _node in unknown_set:
n_c += 1
if _node in known_set:
count = known_count_dict[_node]
n = 0
for _node_node in near_by(*_node, H, W):
if _node_node in unknown_set:
n += 1
if _node_node in boom_set:
count -= 1
p_list.append(count/n) # 有雷的概率
p_list.append(N/len(unknown_set))
probs_list.append((node, max(p_list), n_c))
m_p = min(probs_list, key=lambda x:x[1])[1]
probs_list = [x for x in probs_list if x[1]==m_p]
node = min(probs_list, key=lambda x:x[2])[0]
count, done = env.act(*node)
if done == True:
print('游戏失败,触雷,game over!!!')
print(node)
raise Exception
print("成功完成一步!!! \n\n")
print("remove node:", node)
unknown_set.remove(node)
known_set.add(node)
known_count_dict[node] = count
env.pp() # 打印当前游戏环境的显示
new_nodes.append(node)
new_relation_nodes_set.add(node)
while new_nodes or new_relation_nodes_set:
# debug
# print(new_nodes)
# print(new_relation_nodes_set)
while new_nodes:
node = new_nodes.pop()
k = 0
b = 0
count = known_count_dict[node]
tmp_unk = set()
for _node in near_by(*node, H, W):
if _node in known_set:
new_relation_nodes_set.add(_node)
# k += 1
continue
if _node in boom_set:
new_relation_nodes_set.add(_node)
b += 1
continue
tmp_unk.add(_node) # 对unknown节点进行判断
count -= b
if count==len(tmp_unk):
# 全是雷
for _node in tmp_unk:
print("remove node:", _node)
unknown_set.remove(_node)
boom_set.add(_node)
new_relation_nodes_set.add(_node)
N -= 1
if count==0 and len(tmp_unk) > 0:
# 全都不是雷
for _node in tmp_unk:
c, done = env.act(*_node)
if done:
print("程序判断出错,把雷误触发了!!!")
raise Exception
print("remove node:", _node)
unknown_set.remove(_node)
known_set.add(_node)
known_count_dict[_node] = c
new_nodes.append(_node)
new_relation_nodes_set.add(_node)
while new_relation_nodes_set:
node = new_relation_nodes_set.pop()
tmp_set = set()
for i in range(-2, 3):
for j in range(-2, 3):
if node[0]+i>=0 and node[0]+i<H and node[1]+j>=0 and node[1]+j<W:
if (node[0]+i, node[1]+j) in known_set:
if known_count_dict[(node[0]+i, node[1]+j)]==0:
continue
tmp_set.add((node[0]+i, node[1]+j))
if len(tmp_set)==0:
continue
relations = []
for node in tmp_set: # node 为 known set
tmp_tmp_set = set()
c = known_count_dict[node]
for _node in near_by(*node, H, W):
if _node in boom_set:
c -= 1
continue
if _node in unknown_set:
tmp_tmp_set.add(_node)
continue
if len(tmp_tmp_set)==0:
continue
relations.append([tmp_tmp_set, c, node])
if len(relations)<2:
continue
for i in range(0, len(relations)):
for j in range(1, len(relations)):
if relations[i][0].issuperset(relations[j][0]):
relations[i][0] -= relations[j][0]
relations[i][1] -= relations[j][1]
if relations[i][1]==len(relations[i][0]) and relations[i][1]>0:
# 全是雷
for _node in relations[i][0]:
if _node in boom_set:
continue
print("remove node:", _node)
unknown_set.remove(_node)
boom_set.add(_node)
new_relation_nodes_set.add(relations[i][2])
N -= 1
if relations[i][1]==0 and len(relations[i][0]):
# 全都不是雷
for _node in relations[i][0]:
if _node in known_set:
continue
c, done = env.act(*_node)
if done:
print("程序判断出错,把雷误触发了!!!")
raise Exception
print("remove node:", _node)
unknown_set.remove(_node)
known_set.add(_node)
known_count_dict[_node] = c
new_nodes.append(_node)
new_relation_nodes_set.add(_node)
if relations[j][0].issuperset(relations[i][0]):
relations[j][0] -= relations[i][0]
relations[j][1] -= relations[i][1]
if relations[j][1]==len(relations[j][0]) and relations[j][1]>0:
# 全是雷
for _node in relations[j][0]:
if _node in boom_set:
continue
print("remove node:", _node)
unknown_set.remove(_node)
boom_set.add(_node)
new_relation_nodes_set.add(relations[j][2])
N -= 1
if relations[j][1]==0 and len(relations[j][0]):
# 全都不是雷
for _node in relations[j][0]:
if _node in known_set:
continue
c, done = env.act(*_node)
if done:
print("程序判断出错,把雷误触发了!!!")
raise Exception
print("remove node:", _node)
unknown_set.remove(_node)
known_set.add(_node)
known_count_dict[_node] = c
new_nodes.append(_node)
new_relation_nodes_set.add(_node)
print('游戏胜利,game over!!!')
return True
sss = []
for xyz in range(30000):
try:
sss.append(play())
print('第 %d 次游戏成功'%xyz)
except Exception:
print('第 %d 次游戏失败!!!'%xyz)
continue
print("成功次数: ", sum(sss))
print("成功比例: ", sum(sss)/30000)
个人github博客地址:
https://devilmaycry812839668.github.io/
python实现的扫雷游戏的AI解法(启发式算法)的更多相关文章
- [LeetCode] Minesweeper 扫雷游戏
Let's play the minesweeper game (Wikipedia, online game)! You are given a 2D char matrix representin ...
- 1秒内通关扫雷?他创造属于自己的世界记录!Python实现自动扫雷
五一劳动节假期,我们一起来玩扫雷吧.用Python+OpenCV实现了自动扫雷,突破世界记录,我们先来看一下效果吧. 中级 - 0.74秒 3BV/S=60.81 相信许多人很早就知道有扫雷这么一款经 ...
- 洛谷 P2670 扫雷游戏==Codevs 5129 扫雷游戏
题目描述 扫雷游戏是一款十分经典的单机小游戏.在n行m列的雷区中有一些格子含有地雷(称之为地雷格),其他格子不含地雷(称之为非地雷格).玩家翻开一个非地雷格时,该格将会出现一个数字——提示周围格子中有 ...
- 用Python设计第一个游戏 - 零基础入门学习Python002
用Python设计第一个游戏 让编程改变世界 Change the world by program 有些鱼油可能会说,哇,小甲鱼你开玩笑呐!这这这这就上游戏啦?你不打算给我们讲讲变量,分支,循环,条 ...
- 利用Python完成一个小游戏:随机挑选一个单词,并对其进行乱序,玩家要猜出原始单词
一 Python的概述以及游戏的内容 Python是一种功能强大且易于使用的编程语言,更接近人类语言,以至于人们都说它是“以思考的速度编程”:Python具备现代编程语言所应具备的一切功能:Pytho ...
- 原生javascript扫雷游戏
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/ ...
- Java练习(模拟扫雷游戏)
要为扫雷游戏布置地雷,扫雷游戏的扫雷面板可以用二维int数组表示.如某位置为地雷,则该位置用数字-1表示, 如该位置不是地雷,则暂时用数字0表示. 编写程序完成在该二维数组中随机布雷的操作,程序读入3 ...
- JAVA_扫雷游戏(布置地雷)
1.要为扫雷游戏布置地雷,扫雷游戏的扫雷面板可以用二维int数组表示.如某位置为地雷,则该位置用数字-1表示, 如该位置不是地雷,则暂时用数字0表示. 编写程序完成在该二维数组中随机布雷的操作,程序读 ...
- C语言二维数组实现扫雷游戏
#include<stdio.h> //使用二维数组实现 扫雷 int main() { char ui[8][8]={ '+','+','+','+','+','+','+','+', ...
- 【Android】自己动手做个扫雷游戏
1. 游戏规则 扫雷是玩法极其简单的小游戏,点击玩家认为不存在雷的区域,标记出全部地雷所在的区域,即可获得胜利.当点击不包含雷的块的时候,可能它底下存在一个数,也可能是一个空白块.当点击中有数字的块时 ...
随机推荐
- jenkins动态切换环境
一.代码层实现动态切换 1.首先在conftest.py下声明pytest_addoption钩子函数,写法如下 def pytest_addoption(parser): # 设置要接收的命令行参数 ...
- 使用 nuxi build-module 命令构建 Nuxt 模块
title: 使用 nuxi build-module 命令构建 Nuxt 模块 date: 2024/8/31 updated: 2024/8/31 author: cmdragon excerpt ...
- 怎么在Windows操作系统部署阿里开源版通义千问(Qwen2)
怎么在Windows操作系统部署阿里开源版通义千问(Qwen2) | 原创作者/编辑:凯哥Java | 分类:人工智能学习系列教程 GitHu ...
- c++学习笔记(一):内存分区模型
目录 内存分区模型 程序运行前 程序运行后 new操作符 内存分区模型 c++在执行时,将内存大方向划分为4个区域 代码区:存放函数体的二进制代码,由操作系统进行管理(编写的所有代码都会存放到该处) ...
- 2024 秋季PAT认证甲级(题解A1-A4)
2024 秋季PAT认证甲级(题解A-D) 写在前面 这一次PAT甲级应该是最近几次最简单的一次了,3个小时的比赛差不多30分钟就ak了(也是拿下了整场比赛的rk1),下面是题解报告,每个题目差不多都 ...
- DRBD - Distributed Replication Block Device
Ref https://computingforgeeks.com/install-and-configure-drbd-on-centos-rhel https://www.veritas.com/ ...
- 消息队列的对比测试与RocketMQ使用扩展
消息队列的对比测试与RocketMQ使用扩展 本文的主要内容包括以下几个方面: 原有的消息技术选型 RocketMQ与kafka 测试对比 如何构建自己的消息队列服务 RocketMQ扩展改造 ...
- 机器学习--决策树算法(CART)
CART分类树算法 特征选择 我们知道,在ID3算法中我们使用了信息增益来选择特征,信息增益大的优先选择.在C4.5算法中,采用了信息增益比来选择特征,以减少信息增益容易选择特征值多的特征的问题. ...
- Angular 18+ 高级教程 – Memory leak, unsubscribe, onDestroy
何谓 Memory Leak? Angular 是 SPA (Single-page application) 框架,用来开发 SPA. SPA 最大的特点就是它不刷新页面,不刷新就容易造成 memo ...
- 官方 | 征集 Flutter 桌面端应用程序的构建案例
亲爱的社区成员们,大家好! Google Flutter 团队希望了解开发者们使用 Flutter 构建的桌面端应用程序,以提高 Flutter 桌面端的测试覆盖率,邀请大家通过表单的形式提交征集和反 ...