人工智能导论第一次A*算法实现走迷宫
本文介绍了一个基于A*算法的迷宫求解可视化程序实现。主要内容包括: 环境搭建:使用Anaconda配置Python3.8环境,安装PyQt5等依赖包 核心算法实现: 配置类定义迷宫参数 点类确保坐标唯一性 A*算法类实现路径搜索,包括代价计算、启发函数和邻居节点获取 可视化界面: 从文本数据解析迷宫 使用PyQt5构建图形界面 实现搜索过程和结果的动态展示 使用方法: 运行程序文件启动可视化界面
搭建环境:
首先下载Anaconda,这一步是大前提!!!!!!
https://www.bilibili.com/video/BV1tNK3z3EeE/?vd_source=4f1a7ab31a6e7a590a8a8a5088e74b3c
之后可以再base里面,也可以搭建新的环境
这里我conda create -n keshe python=3.8搭建了一个叫做keshe的环境
之后我们再VScode里面使用这个环境,如图右下角是环境选取,右上角三角是开始运行

会看到有下面的报错,

这个时候我们在环境中增加对应的包
pip install PyQt5,再次运行即可成功。。
如果没有Vscode的话,你可以桌面建立一个文本,然后把代码复制进去
import time, sys
from PyQt5.QtWidgets import QDialogButtonBox, QDialog, QMainWindow, QGridLayout, QTextEdit, QLineEdit, QWidget, \
QMessageBox, QApplication, QLabel, QPushButton, QHBoxLayout, QVBoxLayout
from PyQt5.QtCore import Qt, QTimer, QObject, pyqtSignal, QBasicTimer
from PyQt5.QtGui import QPainter, QColor, QFont, QPen
import json
class config:
WIDTH = 67 # 地图列数 - 根据迷宫数据确定
HEIGHT = 41 # 地图行数 - 根据迷宫数据确定
blockLength = 12 # 绘制画面时每一个节点方块的边长 - 改小一点以适应更大的迷宫
class point: # 点类(每一个唯一坐标只有对应的一个实例)
_list = [] # 储存所有的point类实例
_tag = True # 标记最新创建的实例是否为_list中的已有的实例,True表示不是已有实例
def __new__(cls, x, y): # 重写new方法实现对于同样的坐标只有唯一的一个实例
for i in point._list:
if i.x == x and i.y == y:
point._tag = False
return i
nt = super(point, cls).__new__(cls)
point._list.append(nt)
return nt
def __init__(self, x, y):
if point._tag:
self.x = x
self.y = y
self.father = None
self.F = 0 # 当前点的评分 F=G+H
self.G = 0 # 起点到当前节点所花费的消耗
self.cost = 0 # 父节点到此节点的消耗
else:
point._tag = True
@classmethod
def clear(cls): # clear方法,每次搜索结束后,将所有点数据清除,以便进行下一次搜索的时候点数据不会冲突。
point._list = []
def __eq__(self, T): # 重写==运算以便实现point类的in运算
if type(self) == type(T):
return (self.x, self.y) == (T.x, T.y)
else:
return False
def __str__(self):
return '(%d,%d)[F=%d,G=%d,cost=%d][father:(%s)]' % (self.x, self.y, self.F, self.G, self.cost, str(
(self.father.x, self.father.y)) if self.father != None else 'null')
class A_Search: # 核心部分,寻路类
def __init__(self, arg_start, arg_end, arg_map):
self.start = arg_start # 储存此次搜索的开始点
self.end = arg_end # 储存此次搜索的目的点
self.Map = arg_map # 一个二维数组,为此次搜索的地图引用
self.open = [] # 开放列表:储存即将被搜索的节点
self.close = [] # 关闭列表:储存已经搜索过的节点
self.result = [] # 当计算完成后,将最终得到的路径写入到此属性中
self.count = 0 # 记录此次搜索所搜索过的节点数
self.useTime = 0 # 记录此次搜索花费的时间--在此演示中无意义,因为process方法变成了一个逐步处理的生成器,统计时间无意义。
# 开始进行初始数据处理
self.open.append(arg_start)
def cal_F(self, loc):
print('计算值:', loc)
G = loc.father.G + loc.cost
H = self.getEstimate(loc)
F = G + H
print("F=%d G=%d H=%d" % (F, G, H))
return {'G': G, 'H': H, 'F': F}
def F_Min(self): # 搜索open列表中F值最小的点并将其返回,同时判断open列表是否为空,为空则代表搜索失败
if len(self.open) <= 0:
return None
t = self.open[0]
for i in self.open:
if i.F < t.F:
t = i
return t
def getAroundPoint(self, loc): # 获取指定点周围所有可通行的点,并将其对应的移动消耗进行赋值。
l = [(loc.x, loc.y + 1, 10), (loc.x + 1, loc.y + 1, 14), (loc.x + 1, loc.y, 10), (loc.x + 1, loc.y - 1, 14),
(loc.x, loc.y - 1, 10), (loc.x - 1, loc.y - 1, 14), (loc.x - 1, loc.y, 10), (loc.x - 1, loc.y + 1, 14)]
for i in l[::-1]:
if i[0] < 0 or i[0] >= config.HEIGHT or i[1] < 0 or i[1] >= config.WIDTH:
l.remove(i)
nl = []
for i in l:
if self.Map[i[0]][i[1]] == 0:
nt = point(i[0], i[1])
nt.cost = i[2]
nl.append(nt)
return nl
def addToOpen(self, l,
father): # 此次判断的点周围的可通行点加入到open列表中,如此点已经在open列表中则对其进行判断,如果此次路径得到的F值较之之前的F值更小,则将其父节点更新为此次判断的点,同时更新F、G值。
for i in l:
if i not in self.open:
if i not in self.close:
i.father = father
self.open.append(i)
r = self.cal_F(i)
i.G = r['G']
i.F = r['F']
else:
tf = i.father
i.father = father
r = self.cal_F(i)
if i.F > r['F']:
i.G = r['G']
i.F = r['F']
# i.father=father
else:
i.father = tf
def getEstimate(self, loc): # H :从点loc移动到终点的预估花费
return (abs(loc.x - self.end.x) + abs(loc.y - self.end.y)) * 10
def DisplayPath(self): # 在此演示中无意义
print('搜索花费的时间:%.2fs.迭代次数%d,路径长度:%d' % (self.useTime, self.count, len(self.result)))
if self.result != None:
for i in self.result:
self.Map[i.x][i.y] = 8
for i in self.Map:
for j in i:
if j == 0:
print('%s' % '□', end='')
elif j == 1:
print('%s' % '▽', end='')
elif j == 8:
print('%s' % '★', end='')
print('')
else:
print('搜索失败,无可通行路径')
def process(self): # 使用yield将process方法变成一个生成器,可以逐步的对搜索过程进行处理并返回关键数据
while True:
self.count += 1
tar = self.F_Min() # 先获取open列表中F值最低的点tar
if tar == None:
self.result = None
self.count = -1
break
else:
aroundP = self.getAroundPoint(tar) # 获取tar周围的可用点列表aroundP
self.addToOpen(aroundP, tar) # 把aroundP加入到open列表中并更新F值以及设定父节点
self.open.remove(tar) # 将tar从open列表中移除
self.close.append(tar) # 已经迭代过的节点tar放入close列表中
if self.end in self.open: # 判断终点是否已经处于open列表中
e = self.end
self.result.append(e)
while True:
e = e.father
if e == None:
break
self.result.append(e)
yield (tar, self.open, self.close)
break
# self.repaint()
# print('返回')
yield (tar, self.open, self.close)
time.sleep(0.05) # 暂停时间改短一点,加快动画速度
# self.useTime = time2 - time1
class GameBoard(QMainWindow): # 可视化类,pyqt5进行编写。
def __init__(self):
print('初始化地图...')
self.Map = []
self.startPoint = None
self.endPoint = None
self.createMazeFromText() # 从文本数据创建迷宫
self.search = None
self.centerTimer = None
self.yi = None
self.special = None
self.displayFlush = False
super().__init__()
print('初始化UI...')
self.initUI()
def createMazeFromText(self):
"""从文本数据创建迷宫地图"""
maze_text = """#################################################################################
#.#...#....$....#...................#...#.........#.......#.............#.......#
#.#.#.#.###.###.#########.#########.#.#####.#####.#####.#.#.#######.###.#.#####.#
#...#.....#...#.#.........#.#.....#.#...#...#...#.......#.#.#.......#.#.#.#...#.#
#############.#.#.#########.#.###.#.###.#.###.#.#.#######.###.#######.#.#.#.#.#.#
#...........#.#...#.#.....#...#...#...#.#.#.#.#...#...#.......#.......#.#.#.#.#.#
#.#########.#.#####.#.#.#.#.###.#####.#.#.#.#.#####.#.#########.###.###.###.#.#.#
#.#.........#...#...#.#.#.#...#.....#.#.#.#...#.#...#.......#.....#.#...#...#...#
#.#########.#.#.###.#.#.#####.###.#.#.#.#.#.###.#.#########.#####.#.#.###.#####.#
#.#.......#.#.#...#...#.#.....#.#.#.#...#.#.....#.#.....#.#...#...#.......#...#.#
#.#.#####.#.#.###.#####.#.#####.#.#.###.#.#######.###.#.#.###.#.###########.#.#.#
#...#...#.#.#...#.....#.#.......#.#.#...#.....#...#...#.....#.#.#...#...#...#...#
#####.#.#.#.#########.#.#######.#.###.#######.#.###.#########.###.#.#.#.#.#######
#.....#...#.#.........#.......#.#...#.#.#.....#.#.....#.......#...#.#.#.#.#.....#
#.#########.#.#########.###.###.###.#.#.#.###.#.#.###.#.#######.###.#.###.#.###.#
#...#.#.....#...#.....#.#.#...#.#.#.....#...#.#.#...#.#...#...#...#.#.#...#...#.#
###.#.#.#####.#.#.#.###.#.###.#.#.#####.###.###.#####.###.#.#.#.###.#.#.#####.#.#
#...#...#.....#.#.#.#...#...#.....#...#.#...#...........#.#.#...#...#.......#.#.#
#.###.#########.#.#.#.###.#.#####.#.#.###.###.###########.#.#####.#########.###.#
#.#.............#.#.......#.#...#.#.#...#.#...#.#.......#.......#.#...#.....#...#
#.#.#############.#########.#.#.###.###.#.#.###.#.#####.#.#######.#.#.#.#####.#.#
#.#.#...........#.#.#.#.....#.#.....#...#.#.....#...#.#.#.#.#...#.#.#.#.#.....#.#
#.###.#########.#.#.#.#######.#######.###.#####.###.#.#.#.#.###.#.#.#.#.#####.#.#
#.....#...#.....#...#.........#.....#...#.....#...#...#.#.....#.#...#.#.#.....#.#
#.#####.#.#.#######.###########.#######.#.#######.###.#.###.###.#####.#.#.#####.#
#.....#.#.#...#...#.#.......#.........#.#...#.......#.#.#...#...#.....#.#.#...#.#
#######.#.###.#.###.#.#####.#.#####.###.#.#.#.#######.#.#####.###.#####.#.###.#.#
#.......#.#...#.....#.#...#.#...#.#.....#.#.#.#.#.....#...#...#...#.....#...#.#.#
#.#######.#.#.#####.#.###.#.###.#.#######.#.#.#.#.#######.#.###.#.###.#####.#.#.#
#.#.#.....#.#.#...#.#...#.#...#...#.#...#.#...#.#.....#.#...#...#...#.......#...#
#.#.#.#####.#.#.#.#####.#.###.###.#.#.#.#.#####.#####.#.#####.#####.#########.###
#.#...#.....#.#.#.#...#...#.#.#...#...#.#.#...#.....#...#.#...#...#.....#...#.#.#
#.###.###.#.###.#.#.#.###.#.#.#.#######.#.#.#.#####.###.#.#.###.#.#####.###.#.#.#
#...#...#.#.#...#.#.#...#.#.#.#.#.......#...#.........#.#...#...#.#...#...#.#...#
#.#.###.#.#.#.###.#.###.#.#.#.#.###.###.###########.###.#.###.###.###.###.#.###.#
#.#...#.#.#.#...#...#...#.#.#.#.....#...#...#.....#.#...#.....#.....#.#...#...#.#
#.###.#.#.#####.#####.#.#.#.#.#######.###.#.#####.#.#.#############.#.#.###.#.#.#
#...#.#...#...#.....#.#.#.#.#.#...#...#.#.#.......#.#.#...#...#...#...#.#.#.#...#
###.#.#####.#.#####.#.###.#.#.#.#.#.###.#.#########.#.#.#.#.#.#.#.#####.#.#.#####
#...#.......#.......#.......#...#.......@...........#...#...#...#.......#.......#
#################################################################################"""
lines = maze_text.strip().split('\n')
if not lines:
print("迷宫数据为空")
return
# 设置地图尺寸
config.HEIGHT = len(lines)
config.WIDTH = len(lines[0])
# 初始化地图
self.Map = []
for i in range(config.HEIGHT):
row = []
for j in range(config.WIDTH):
char = lines[i][j]
if char == '#': # 墙壁
row.append(1)
elif char == '$': # 起点
row.append(0)
self.startPoint = (j, i)
print(f"找到起点: ({j}, {i})")
elif char == '@': # 终点
row.append(0)
self.endPoint = (j, i)
print(f"找到终点: ({j}, {i})")
else: # 可通行的路径
row.append(0)
self.Map.append(row)
print(f"迷宫创建成功: {config.WIDTH}x{config.HEIGHT}")
print(f"起点: {self.startPoint}, 终点: {self.endPoint}")
def initUI(self):
# 开始初始化UI部分
# 创建UI控件
self.label_tips = QLabel(
"<p style='color:green'>迷宫求解器</p>基于A*算法的迷宫求解可视化程序\n<p style='color:green'>颜色说明:</p>\n黄色代表起点,绿色代表终点,黑色代表墙壁,红色代表待搜索的open列表,灰色代表已搜索过的close列表,蓝色代表当前搜索到的路径",
self)
self.label_display = QLabel("", self)
self.button_start = QPushButton("开始搜索", self)
self.button_reset = QPushButton("重置迷宫", self)
# 设置控件属性
self.label_tips.setWordWrap(True)
self.label_display.setWordWrap(True)
# 设置控件样式
self.label_display.setStyleSheet("border:1px solid black")
self.label_display.setAlignment(Qt.AlignLeft)
self.label_display.setAlignment(Qt.AlignTop)
# 设置控件的尺寸和位置
self.label_tips.resize(200, 150)
self.button_start.resize(80, 30)
self.button_reset.resize(80, 30) # 修复这里的笔误
self.label_tips.move(50 + (config.WIDTH) * config.blockLength, 20)
self.label_display.move(50 + (config.WIDTH) * config.blockLength, 200)
self.button_start.move(50 + (config.WIDTH) * config.blockLength, 170)
self.button_reset.move(150 + (config.WIDTH) * config.blockLength, 170)
# 给控件绑定事件
self.button_start.clicked.connect(self.button_StartEvent)
self.button_reset.clicked.connect(self.button_Reset)
# UI初始化完成
window_width = 100 + (config.WIDTH * config.blockLength) + 250
window_height = 100 + (config.HEIGHT * config.blockLength)
self.setGeometry(100, 100, window_width, window_height)
self.setMinimumSize(window_width, window_height)
self.setMaximumSize(window_width, window_height)
self.setWindowTitle('迷宫求解器 - A*算法')
self.show()
def addDisplayText(self, text):
if self.displayFlush:
self.label_display.setText(text + '\n')
self.displayFlush = False
else:
self.label_display.setText(self.label_display.text() + text + '\n')
def button_StartEvent(self):
if self.startPoint != None and self.endPoint != None:
if self.centerTimer == None:
self.centerTimer = QBasicTimer()
self.button_start.setEnabled(False)
self.centerTimer.start(30, self) # 加快动画速度
self.search = A_Search(point(self.startPoint[1], self.startPoint[0]),
point(self.endPoint[1], self.endPoint[0]), self.Map)
self.yi = self.search.process()
self.addDisplayText('开始进行搜索')
def button_Reset(self):
"""重置迷宫"""
if self.centerTimer and self.centerTimer.isActive():
self.centerTimer.stop()
self.search = None
self.yi = None
self.special = None
point.clear()
self.button_start.setEnabled(True)
self.displayFlush = True
self.addDisplayText('迷宫已重置')
self.repaint()
def paintEvent(self, event):
qp = QPainter()
qp.begin(self)
self.drawBoard(event, qp)
qp.end()
def drawBoard(self, event, qp):
self.drawMap(qp)
def drawMap(self, qp): # 画面绘制方法,每次地图有所改动都将重绘
if self.search != None:
if self.special != None:
e = self.special[0]
path = [e]
while True:
e = e.father
if e != None:
path.append(e)
else:
break
else:
path = None
pen = QPen(QColor(0, 0, 0), 1, Qt.SolidLine)
qp.setPen(pen)
for i in range(len(self.Map)):
for j in range(len(self.Map[i])):
wordTag = False
if i == self.search.start.x and j == self.search.start.y:
qp.setBrush(QColor(255, 255, 0)) # 起点 - 黄色
elif i == self.search.end.x and j == self.search.end.y:
qp.setBrush(QColor(100, 200, 50)) # 终点 - 绿色
else:
if self.Map[i][j] == 0: # 可通行路径
tagx = True
if path:
for k in path:
if k.x == i and k.y == j:
tagx = False
qp.setBrush(QColor(0, 100, 255)) # 路径 - 蓝色
if tagx:
if self.special != None:
if i == self.special[0].x and j == self.special[0].y:
qp.setBrush(QColor(0, 255, 0)) # 当前搜索点 - 绿色
else:
tag = True
for k in self.special[1]: # open列表
if k.x == i and k.y == j:
tag = False
wordTag = True
word = str(k.F)
qp.setBrush(QColor(150, 0, 0)) # open列表 - 红色
break
if tag:
for k in self.special[2]: # close列表
if k.x == i and k.y == j:
qp.setBrush(QColor(150, 150, 150)) # close列表 - 灰色
break
else:
qp.setBrush(QColor(220, 220, 220)) # 空白路径 - 浅灰色
else:
qp.setBrush(QColor(220, 220, 220)) # 空白路径 - 浅灰色
elif self.Map[i][j] == 1: # 墙壁
qp.setBrush(QColor(0, 0, 0)) # 墙壁 - 黑色
else:
qp.setBrush(QColor(255, 0, 0))
qp.drawRect(50 + j * config.blockLength, 50 + i * config.blockLength, config.blockLength,
config.blockLength)
else:
# 没有搜索时的绘制
for i in range(len(self.Map)):
for j in range(len(self.Map[i])):
if (j, i) == self.startPoint:
qp.setBrush(QColor(255, 255, 0)) # 起点 - 黄色
elif (j, i) == self.endPoint:
qp.setBrush(QColor(100, 200, 50)) # 终点 - 绿色
else:
if self.Map[i][j] == 0:
qp.setBrush(QColor(220, 220, 220)) # 可通行路径 - 浅灰色
elif self.Map[i][j] == 1:
qp.setBrush(QColor(0, 0, 0)) # 墙壁 - 黑色
else:
qp.setBrush(QColor(255, 0, 0))
qp.drawRect(50 + j * config.blockLength, 50 + i * config.blockLength, config.blockLength,
config.blockLength)
def timerEvent(self, e):
try:
data = next(self.yi)
except Exception as e:
self.addDisplayText('搜索结束:')
print('搜索结束!')
if self.search.result == None:
self.addDisplayText('未找到可行路径')
print('搜索结束!')
else:
self.addDisplayText('总计搜索节点数:%d' % self.search.count)
self.addDisplayText('最终路径长度:%d' % len(self.search.result))
self.centerTimer.stop()
self.button_start.setEnabled(True)
self.displayFlush = True
else:
self.special = data
self.repaint()
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = GameBoard()
sys.exit(app.exec_())
首先复制你的.py的文件路径
然后终端运行一下,如果你是base没有新建的话,那就直接在这个里面
python +文件地址,注意把双引号“”去掉


由此就成功了。
下面讲解一下代码部分:
1. 配置类 (config)
class config:
WIDTH = 67 # 地图列数
HEIGHT = 41 # 地图行数
blockLength = 12 # 每个方块的绘制边长
2. 点类 (point)
def __new__(cls, x, y): # 重写new方法
for i in point._list:
if i.x == x and i.y == y:
point._tag = False
return i # 返回已存在的实例
nt = super(point, cls).__new__(cls)
point._list.append(nt)
return nt # 创建新实例
-
为了确保每个坐标只有唯一实例,避免重复创建相同坐标的点
3. A*搜索算法类 (A_Search)
初始化列表
def __init__(self, arg_start, arg_end, arg_map):
self.open = [] # 待探索节点
self.close = [] # 已探索节点
self.result = [] # 最终路径
核心算法流程
1. 代价计算
loc.cost就是邻居节点间的代价
def cal_F(self, loc):
G = loc.father.G + loc.cost # 实际代价
H = self.getEstimate(loc) # 预估代价
F = G + H # 总代价
return {'G': G, 'H': H, 'F': F}
2. 启发函数
用曼哈顿距离作为启发函数,哈佛曼就是两点间的距离D=|x1-x2|+|y1-y2|
并且由于我们的实际代价是一个格子是10,我们这里给我们的哈佛曼预测也乘以10,这样可以和移动代价保持一致
def getEstimate(self, loc):
return (abs(loc.x - self.end.x) + abs(loc.y - self.end.y)) * 10
3. 邻居节点获取
def getAroundPoint(self, loc):
l = [(loc.x, loc.y + 1, 10), (loc.x + 1, loc.y + 1, 14), ...]
4. 生成器实现逐步搜索
def process(self):
while True:
tar = self.F_Min() # 获取F值最小的节点
aroundP = self.getAroundPoint(tar)
self.addToOpen(aroundP, tar)
# ... 搜索逻辑
yield (tar, self.open, self.close) # 返回当前状态
4. 游戏界面类 (GameBoard)
迷宫数据解析
def createMazeFromText(self):
UI初始化
def initUI(self):
绘制系统
def drawMap(self, qp):
动画控制
def timerEvent(self, e):
剩下如果再加功能的话的功能就很简单了,你自己问ai加就行了
更多推荐

所有评论(0)