结对编程作业

表格项 内容
队友博客地址 https://www.cnblogs.com/zxh2001/p/13841725.html
我的博客地址 https://www.cnblogs.com/wlululu/p/13841828.html
Github项目地址(小游戏) https://github.com/cath-cell/pairwork
Github项目地址(AI) https://github.com/bambilu32/031804127/tree/master/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签入记录

·评价我的队友
值得学习的地方

晓晗做事积极性很高,和她在一起更有学习的动力;遇到难题了她会积极思考学习,经常和我讨论问题的解决方法,帮我理清楚应该往哪个方向去做,她负责的部分完成的也很优秀。晓晗对学习的热情是我应该去学习的。

需要改进的地方

希望晓晗加强自己的编程能力,更上一层楼。

三、各类记录表格

学习进度条

第N周 新增代码 累计代码 本周学习耗时 累计学习耗时 重要成长
1 40 40 3 3 了解了json库,学会了对接口的使用
2 100 140 15 18 学习掌握了对Image库的使用
3 250 390 35 53 初步编写了A*算法,加深了理解
4 100 490 25 78 根据题目要求改进了算法
5 50 540 30 108 进行AI大比拼,修改了算法的缺陷

PSP表格:

**PSP2.1 ** **Personal Software Process Stages ** 预估耗时(分钟) 实际耗时(分钟)
Planning 计划 60 50
Estimate 估计这个任务需要多少时间 60 50
Development 开发 1830 2320
Analysis 需求分析 (包括学习新技术) 550 700
Design Spec 生成设计文档 200 150
Design Review 设计复审 160 150
Coding Standard 代码规范 (为目前的开发制定合适的规范) 300 450
Design 具体设计 200 300
Coding 具体编码 200 350
Code Review 代码复审 100 70
Test 测试(自我测试,修改代码,提交修改) 120 150
Reporting 报告 400 395
Test Report 测试报告 150 125
Size Measurement 计算工作量 200 210
Postmortem & Process Improvement Plan 事后总结, 并提出过程改进计划 50 60
合计 2290 2430
posted @ 2020-10-19 18:40  wlululu  阅读(221)  评论(0编辑  收藏  举报