结对编程作业
表格项 | 内容 |
---|---|
我的博客地址 | https://www.cnblogs.com/zxh2001/p/13841725.html |
队友博客地址 | https://www.cnblogs.com/wlululu/p/13841828.html |
Github项目地址(小游戏) | https://github.com/cath-cell/pairwork |
分工内容(我的部分) | 负责小游戏的原型设计以及实现 |
分工内容(队友的部分) | 负责AI的算法设计以及实现 |
一、原型设计
一开始拿到题目完全不知道该怎么做小游戏,查资料之后了解到可以用PyQt5库或者Pygame库来进行小游戏的制作,最后我选择的是PyQt5库。使用的原型设计工具是Qt Designer,这是一个和PyQt5库相配套的原型设计工具,使用起来非常方便,可以直接将.ui结尾的文件转成.py的代码框架文件。
最开始的原型图如下:
可以看到Qt Designer的左边是控件区,可以直接拖进工作区使用,右边是控件的设置区,可以查看控制是继承于什么类,也可设置控件的各种Style(通过Style Sheet,这里按钮的紫色与背景的粉紫色就是通过css代码设置Style Sheet完成的样式),也可为控件添加槽函数(内置的),最开始设计了六个功能:选择图片,保存进度,读取进度,Ai提示,打乱图片,以及关闭整个窗口。最后发现Ai功能实在做不好,无奈之下只能去掉了这个按钮,最后成品只有五个按钮。
成品图如下:
上方我放置了一个showView的控件作为图片显示的区域,下方五个按钮用的是自带的Layout布局(啊,实在是做的很简陋),其中close是添加自带槽函数的按钮(剩余按钮功能是自己编写的)
以下是我们讨论作业时的照片:
二、AI和小游戏的原型实现
1.AI的原型实现
代码实现思路:
● 网络接口的使用
测试组给了GET和POST两个接口,刚开始看有点懵逼,没懂给这两个接口是什么意思orz。。后来才知道是用来获取题目和提交题目的。这就刚好用到了这学期学到的有关网络爬虫的知识,所以这部分代码的实现没什么困难,主要就是用到requests库get和post题目,json库对数据的规范。需要注意的就是对图片的保存了,一开始我是直接爬取图片地址并保存,结果图片没办法查看,仔细审题,发现图片是经过bs64编码的图片,需要先解码,用到了base64库,这里贴上解码的代码:
import base64
img = base64.b64decode(dic["data"]["img"])
● 代码组织与内部实现设计(类图)
由于赛题需要获取,而且测试组给的测试接口是实时的,因此我将实现AI、获取题目、提交题目的代码分不同的.py文件写了(主要是变量名太多,怕写在一个.py文件里弄混)需要的时候直接import调用就好。下面展示类图:
● 算法的关键与关键部分流程图
最开始题目发布下来,就了解到原理是八数码问题 ,但是初始状态是图片,而且目标状态是未知的,需要自己在给定的图片集里面找到正确的字母,并且还要对切割后的图片正确标号,将初始状态和目标状态转换成一串数字,然后再用自己设计的算法解题。当时头都大了,和搭档讨论后理清了思路,先展示一下流程图:
切割图片
这个函数实现的主要功能就是将图片切成九张并存入列表,image.crop(area)是根据area切割原图的相应部分。
def splitImage(src, c_image):
image_o = Image.open(src)
dim = 3
x = image_o.size[0] / dim
y = image_o.size[1] / dim
count = 0
for j in range(dim):
for i in range(dim):
area = (i * x, j * y, i * x + x, j * y + y)
image = image_o
im = image.crop(area)
c_image.append(im)
count = count + 1
找到正确的字母
c_image = [] # 原图切片后的顺序
c_image1 = [] # 题目切片后的顺序
src1 = 'test/1_.png' # 题目保存地址
splitImage(src1, c_image1)
for item in geturlPath(): # geturlPath()是遍历图像集的函数
flag = 0 # 用于记录匹配到的图片个数
c_image2 = []
splitImage(item, c_image2)
for i in range(len(c_image1)):
for j in range(len(c_image2)):
if c_image1[i] == c_image2[j]:
flag += 1
break
if flag == 8: # 题目是随机扣了一张做空白格,因此有八张是一样的就是找到了原图
print("找到了原图:" + item)
c_image = c_image2 # 最后用C_image保存原图切割后的图片
break
得到初始状态和目标状态
这部分就是将问题从图片形式转换成数字形式。
map_1 = [] # 目标状态
map_2 = [] # 初始状态
for k in range(9):
map_1.append(k + 1)
for i in range(len(c_image1)):
temp = 0 # 用于辨别是否匹配到图片
for j in range(len(c_image)):
if c_image1[i] == c_image[j]:
temp = 1
map_2.append(j + 1)
# 没有匹配到的就是被扣掉的那一块,置为0
if temp == 0:
map_2.append(0)
# 将原图中被扣掉的那一块的位置赋值为0
for i in range(len(map_1)):
temp2 = 0
for j in range(len(map_2)):
if map_1[i] == map_2[j]:
temp2 = 1
break
if temp2 == 0:
map_1[i] = 0
break
AI我用到的是A算法,A算法的介绍:https://blog.csdn.net/qq_36946274/article/details/81982691
总的来说,就是将每次空白格移动后的状态看成一个节点,初始状态和目标状态分别对应初始结点和目标节点,我要做的就是运用A*算法找到从初始节点到目标节点的最优路径。
描述A*算法中的节点数据
class Node:
def __init__(self, array2d, g=0, h=0):
self.array2d = array2d # 二维数组
self.father = None # 父节点
self.g = g # g值
self.h = h # h值
A*算法
class A:
"""
A*算法
"""
def __init__(self, startNode, endNode):
"""
startNode: 寻路起点
endNode: 寻路终点
"""
global count
# 开放列表
self.openList = []
# 封闭列表
self.closeList = []
# 起点
self.startNode = startNode
# 终点
self.endNode = endNode
# 当前处理的节点
self.currentNode = startNode
# 最后生成的路径
self.pathlist = []
# step步
self.step = 0
count = 0
return
def getMinFNode(self):
"""
获得openlist中F值最小的节点
"""
nodeTemp = self.openList[0]
for node in self.openList:
if node.g + node.h < nodeTemp.g + nodeTemp.h:
nodeTemp = node
return nodeTemp
def nodeInOpenlist(self, node):
for nodeTmp in self.openList:
if nodeTmp.array2d == node.array2d:
return True
return False
def nodeInCloselist(self, node):
for nodeTmp in self.closeList:
if nodeTmp.array2d == node.array2d:
return True
return False
def endNodeInOpenList(self):
for nodeTmp in self.openList:
if nodeTmp.array2d == self.endNode.array2d:
return True
return False
def getNodeFromOpenList(self, node):
for nodeTmp in self.openList:
if nodeTmp.array2d == node.array2d:
return nodeTmp
return None
def searchOneNode(self, node):
"""
搜索一个节点
"""
# 忽略封闭列表
if self.nodeInCloselist(node):
return
# G值计算
gTemp = self.step
# 如果不再openList中,就加入openlist
if self.nodeInOpenlist(node) is False:
node.setG(gTemp)
# H值计算
node.setH(self.endNode)
self.openList.append(node)
node.father = self.currentNode
# 如果在openList中,判断currentNode到当前点的G是否更小
# 如果更小,就重新计算g值,并且改变father
else:
nodeTmp = self.getNodeFromOpenList(node)
if self.currentNode.g + gTemp < nodeTmp.g:
nodeTmp.g = self.currentNode.g + gTemp
nodeTmp.father = self.currentNode
return
def searchNear(self):
"""
搜索下一个可以动作的数码
找到0所在的位置并以此进行交换
"""
flag = False
for x in xrange(0, 3):
for y in xrange(0, 3):
if self.currentNode.array2d[x][y] == 0:
flag = True
break
if flag is True:
break
self.step += 1
if x - 1 >= 0:
arrayTemp = move(copy.deepcopy(self.currentNode.array2d), x, y, x - 1, y)
self.searchOneNode(Node(arrayTemp))
if x + 1 < 3:
arrayTemp = move(copy.deepcopy(self.currentNode.array2d), x, y, x + 1, y)
self.searchOneNode(Node(arrayTemp))
if y - 1 >= 0:
arrayTemp = move(copy.deepcopy(self.currentNode.array2d), x, y, x, y - 1)
self.searchOneNode(Node(arrayTemp))
if y + 1 < 3:
arrayTemp = move(copy.deepcopy(self.currentNode.array2d), x, y, x, y + 1)
self.searchOneNode(Node(arrayTemp))
return
def start(self):
"""
开始寻路
"""
# 将初始节点加入开放列表
self.startNode.setH(self.endNode)
self.startNode.setG(self.step)
self.openList.append(self.startNode)
global key
while True:
# 获取当前开放列表里F值最小的节点
# 并把它添加到封闭列表,从开发列表删除它
self.currentNode = self.getMinFNode()
self.closeList.append(self.currentNode)
self.openList.remove(self.currentNode)
self.step = self.currentNode.getG()
self.searchNear()
# 判断是否进行强制交换
if key == 0 and self.step == Step + 1:
nodeTmp = self.currentNode
while True:
self.pathlist.append(nodeTmp)
if nodeTmp.father is not None:
nodeTmp = nodeTmp.father
else:
key = 1
return True
# 检验是否结束
if self.endNodeInOpenList():
nodeTmp = self.getNodeFromOpenList(self.endNode)
while True:
self.pathlist.append(nodeTmp)
if nodeTmp.father is not None:
nodeTmp = nodeTmp.father
else:
return True
elif len(self.openList) == 0:
return False
return True
题目有要求在执行了一定步数之后,会强制调换两个方块的位置,强制调换后,在没有解的情况下,允许自由调换任意两个方块的位置,因此我在A算法中加入了判断是否需要强制调换的代码,然后用key标记它,代表后续再次执行A算法不需要再执行强制调换。
# 判断是否进行强制交换
if key == 0 and self.step == Step + 1:
nodeTmp = self.currentNode
while True:
self.pathlist.append(nodeTmp)
if nodeTmp.father is not None:
nodeTmp = nodeTmp.father
else:
key = 1
return True
判断八数码问题的可解性,可以通过判断其逆序数的奇偶来确定。
# 计算是奇数列还是偶数列
def getStatus(array2d):
y = 0
for i in range(len(array2d)):
if array2d[i] != 0:
for j in range(i):
if array2d[j] > array2d[i]:
y += 1
return y
# 根据奇数列和偶数列判断是否有解
def pd(start, end):
startY = getStatus(start)
endY = getStatus(end)
# print(startY)
# print(endY)
if startY % 2 != endY % 2:
return False
else:
return True
自由交换我就是用random随机选取位置交换的方块的位置,然后判断交换后有没有解,有解则继续执行A*算法。
while pd(ls_2, g) is False:
ct = 1
ls_2 = copy.deepcopy(ls_3) # ls_3存储了强制交换后的状态
# 进行自由交换
swap(ls_2)
def swap(arr):
t = arr
x = random.randint(0, 2)
y = random.randint(0, 2)
k = random.randint(0, 2)
v = random.randint(0, 2)
temp = t[x][y]
t[x][y] = t[k][v]
t[k][v] = temp
print("自由交换的方块为:", x * 3 + y + 1, k * 3 + v + 1)
● 展示性能分析图和程序中消耗最大的函数
● 展示出项目部分单元测试代码,并说明测试的函数,构造测试数据的思路
之前提到判断八数码问题的可解性,可以通过判断其逆序数的奇偶来确定,这里我构造了三个初始状态,两个无解,一个有解,来测试判断函数。
end = [[1, 2, 3], [4, 0, 5], [7, 8, 9]]
start_1 = [[1, 4, 3], [0, 6, 7], [5, 8, 9]]
start_2 = [[3, 1, 0], [8, 9, 2], [5, 6, 7]]
start_3 = [[1, 2, 3], [9, 8, 0], [7, 5, 6]]
if pd(start_1, end):
print("有解")
else:
print("无解")
if pd(start_2, end):
print("有解")
else:
print("无解")
if pd(start_3, end):
print("有解")
else:
print("无解")
无解
有解
无解
贴出Github的代码签入记录,合理记录commit信息
遇到的代码模块异常或结对困难及解决方法
最困难的一部分应该就是对图片的处理了,不知道怎么将图片问题转换成数字问题。后来通过和搭档的讨论和自己在网上搜索方法,发现可以用Image库实现对图片的切割以及存入列表等操作,这样问题的实现就简单多了。通过解决这个困难,也熟悉了Image库的使用。
2.小游戏的原型实现
既然原型文件已经生成,用命令行将其转为同一个名字的py文件,之后每次修改原型文件都要执行一次命令行,所以我写了一个小小的bat文件,每次修改原型后点击运行就好,不用每次都打开cmd。
接下来是各个功能函数的介绍:
·第一个功能是选择图片,即点击按钮后弹出同目录下pics文件夹里的可选文件
代码如下:
#读取图片文件的方法(choose功能对应的函数)
def openfile(self):
#如果已经有图片被打开了,那么清空画布
if self.if_opened is True:
self.scene.clear()
#设此时没有图片被打乱
self.if_opened = True
self.scene.if_make_upset = False
#获得图片的名字和类型,这几种图片类型都可以打开
image_name, image_type = QFileDialog.getOpenFileName(self, "选择图片", "./pics","*.jpg;;*.png;;*.bmp;;All Files (*)")
#打开对应的文件
self.image = Image.open(image_name)
#将图片赋给合并的图片(
self.combine_image = self.image
self.image_name = image_name
#按照大小缩放图片
pix = QtGui.QPixmap(image_name)
#生成Graphics的实例
item = QtWidgets.QGraphicsPixmapItem(pix)
#将实例添加到Scene的实例scene上
self.scene.addItem(item)
#显示
self.View.setScene(self.scene)
·第二个功能是将打开的图片切成九块并随机打乱
#打乱图片的方法(mess功能对应的函数)
def Upset(self):
# 判断原图是否为空
if self.image is None:
return
flag = False
#将打乱置为True
self.if_upset = True
#记录图片被打乱,未选择图片,x,y赋初值
self.scene.if_make_upset = True
self.scene.x = -1
self.scene.y = -1
self.scene.now_selected = -1
dim = 3
self.x = self.image.size[0] / 3
self.y = self.image.size[1] / 3
#计算出XY在此维度下应有的大小
x = self.x
y = self.y
#清空图片碎的列表
self.image_cuts.clear()
#按照XY的大小切割图片
count = 0
for j in range(3):
for i in range(3):
#将最后一块置为白色
if count == 8:
a_image = Image.new('RGB', (self.image.size[0], self.image.size[1]), color='#FFFFFF')
area = (0, 0, x, y)
im = a_image.crop(area)
self.image_cuts.append(im)
count = count + 1
break
#area表示的是像素块,四个坐标控制一个像素块的位置
area = (i * x, j * y, i * x + x, j * y + y)
image = self.image
im = image.crop(area)
self.image_cuts.append(im)
count = count + 1
# 数组形态的图赋初值
self.map.clear()
for k in range(count - 1):
self.map.append(k + 1)
#将空白块赋值为0
self.map.append(0)
self.number = count
while flag is False:
# 打乱图片碎
for i in range(int((count / 3) * (count / 3)) + 1):
n = random.randint(0, count - 1)
m = n
while n == m:
m = random.randint(0, count - 1)
o = m
while o == m or o == n:
o = random.randint(0, count - 1)
if n > m:
n, m = m, n
if m > o:
m, o = o, m
self.map[n], self.map[m] = self.map[m], self.map[n]
self.map[m], self.map[o] = self.map[o], self.map[m]
self.image_cuts[n], self.image_cuts[m] = self.image_cuts[m], self.image_cuts[n]
self.image_cuts[m], self.image_cuts[o] = self.image_cuts[o], self.image_cuts[m]
#判断是否有解
flag = self.whether_reduction()
#将图片碎拼回图片(方法在下面)
self.combine()
#显示完整图片(方法在下面)
self.show_image()
#将白块赋为0
self.white = self.map.index(0)
·第三个功能是保存当前进度,思路是在当前目录下新建一个文件夹用来存该进度时的图片,读取进度就是读取这张图片,再显示出来即可
代码如下:
#保存进度的方法(Save功能对应的函数)
def progress_save(self):
#如果被打乱
if self.if_upset:
#将图片碎拼成图片
self.combine()
#将当时状态的拼接图片保存到文件夹中并命名待用
self.combine_image.save("./pics/combine_image.bmp")
#将原本的图片保存在文件夹中,这里我也不知道为什么只能存为bmp,如果是jpg就打不开
self.image.save("./pics/save.bmp")
#打开text文件夹中的文件list
file = open('./text/list.txt', 'w')
#将映射存入list.txt
for i in range(len(self.map)):
file.write(str(self.map[i]))
file.write('\t')
return
·第四个功能和第三个功能相对,读取第三个功能保存的进度,代码如下:
def progress_read(self):
#清空画布
if self.if_opened is True:
self.scene.clear()
self.if_opened = True
self.scene.if_make_upset = True
self.if_upset = True
self.scene.now_selected = -1
#读取保存进度时的图片
image_name = "./pics/combine_image.bmp"
self.combine_image = Image.open(image_name)
self.image = Image.open("./pics/save.bmp")
self.image_name = image_name
# 选择维度
# self.dim = self.selectHard.value()
#读取维度文件
# file = open('./text/dim.txt', 'r')
# data = file.read()
# self.dim = int(data)
# file.close()
dim=3
# dim = self.dim
#根据维度获得XY
self.x = self.combine_image.size[0] / dim
self.y = self.combine_image.size[1] / dim
x = self.x
y = self.y
# 分块信息清空
self.image_cuts.clear()
self.map.clear()
#再次切割图片
count = 0
for j in range(dim):
for i in range(dim):
area = (i * x, j * y, i * x + x, j * y + y)
image = Image.open(self.image_name)
im = image.crop(area)
self.image_cuts.append(im)
count = count + 1
# 将保存的数组形态的List赋值给List
file = open('./text/list.txt', 'r')
#按行读取内容
data = file.readlines()
#临时存储
map1 = []
for i in data:
k = i.strip()
j = k.split('\t')
map1.append(j)
file.close()
for i in range(len(map1[0])):
self.map.append(int(map1[0][i]))
self.number = count
#将图片碎拼起来并显示
self.combine()
self.show_image()
self.image_name = "./pics/save.bmp"
·第五个功能就是关闭窗口,这里用的是内置的方法,就没有代码展示了。
·小游戏实现部分展示:
·展示性能分析图和程序中消耗最大的函数
·github签入记录
·评价我的队友
璐姐真的超级棒,她做事效率高,学习新知识很迅速,也很有自己的想法,也将自己那部分完成的非常好,她的效率和热情是我应该学习的。
遇到的困难及改进:
第一次用原型设计软件,也是第一次在这样的框架下打代码,搞清楚怎么用槽函数将按钮与功能连接起来就不是很难。但是有一个要注意的点是为了让代码的独立性更好,为View视图单独写了一个py文件盛装View的类,这样在Windows主类直接调用就可以。
还有就是后来的ai大比拼需要出题,也需要按要求把题目传上去,也遇到了一些困难,最后都csdn解决了。
三、各类记录表格
学习进度条:
第N周 | 新增代码(行) | 累计代码(行) | 本周学习耗时(小时) | 累计学习耗时(小时) | 重要成长 |
---|---|---|---|---|---|
1 | 100 | 100 | 9 | 9 | 熟悉Qt Designer的用法 |
2 | 300 | 400 | 20 | 29 | 用python实现小游戏 |
3 | 200 | 600 | 20 | 20 | 缝缝补补,修修改改 |
PSP表格:
Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|
Planning | 60 | 60 |
Estimate | 120 | 100 |
Development | 180 | 120 |
Analysis | 360 | 480 |
Design Spec | 300 | 240 |
Design Review | 200 | 300 |
Coding Standard | 200 | 260 |
Design | 180 | 150 |
Coding | 600 | 700 |
Code Review | 400 | 200 |
Test | 60 | 60 |
Reporting | 60 | 50 |
Test Report | 30 | 30 |
Size Measurement | 60 | 80 |
Postmortem & Process Improvement Plan | 180 | 120 |
Total | 2990 | 2950 |