1 2 3 4

可可爱爱秃头程序圆

导航

结对编程作业

我们的小队——青春喂了软工队

话不多说赶紧放上我们作业的github链接

GitHub:GITHUB

分工

在查找完相关资料后,我们将主要任务拆分成为了两小趴~

分工 曾丽莉 郭畅
原型设计(微信小程序) AI算法
我们的照片哈哈哈
真棒

两个憨憨在讨论原型设计

原型设计

原型设计

坦白来说,“原型设计”是我第一次接触的词语,在查了大量资料过后,才发现原型设计在一个软件的诞生之前有着重要的角色,
一个好的原型设计足以确定整个软件的方向,相当于一个流程图,此次的作业中,我使用了Mockplus来做此次的原型设计(就新手必备!太好操作了也)

先放上一张总的各个界面的图(在两个人讨论过后,这次的图片华容道我们想做成一个微信小程序,原型设计是基于手机屏幕实现的)

1.游戏的主页面

一个游戏最基础的是什么!头脑里面马上想起来的就是游戏的界面,我们总共分成三个难度等级,分别是33和44和5*5
直接在主面板上面点击即可转到相应的界面,每个界面先划分了5个小模块,分别是返回主页游戏规则原图提示排行榜,以及不能过关的时候的AI演示.
(由于技术原因,在原型设计的时候,我们先用一张照片来表示随机生成的棋盘形状)


2.导入和登陆注册页面

在设计完成基本游戏界面后,接下来扩展的是登陆注册的功能,原型图如下:
由于登陆涉及网站服务器的绑定,实在不会操作,在后来的实现中没有完成该功能。。。

3.各个模块小功能

有了整个大致的框架,接下来要做的就是细化每个小模块的功能,计划总共有三个小模块,分别是游戏规则说明,排行榜,AI演示

4.遇到的困难以及解决方案

一开始使用的原型设计是AXURE,有“亿”点点过于专业了,在尝试之后,只会拉一个框,剩下的只会乱按,在不断的百度之后,发现了Mockplus这个神仙软件!非常好上手,无论是建立页面之间的连接,或者是在搭建整个页面的时候,小组件很清晰的让你知道怎么使用,不足的是,在实现插入能够活动的方块这个操作,实现起来有点复杂,到了最后也没有将其实现(十分遗憾)
最后微信小程序实现主页面的设计见GitHub

AI算法

代码实现

1)网络接口

  • 因为之前从来没有接触过json还有base64编码,刚开始还是一脸懵的
  • 对base64解码,直接使用python的base64里的函数进行解析就好啦。
import base64
with open("D:\\pycharm\\三阶数字华容道最优解(更新版)\\三阶数字华容道最优解(更新版)\\q_table\\test.txt","r") as f:
    imgdata = base64.b64decode(f.read())
    file = open('D:\\pycharm\\三阶数字华容道最优解(更新版)\\三阶数字华容道最优解(更新版)\\img2\\img\\test.jpg','wb')
    file.write(imgdata)
    file.close()

这个解码还是比较简单的,直接把图片保存下来

2)代码组织与内部设计实现

MyFrame.py

Generator.py

Qipan.py

Prediction.py

3)算法的关键与关键实现部分流程图

1.前期对解码出来的图片进行分割,并求出对应图片及乱序矩阵
分割:直接可对所有图片,将其转化为正方形,不够的地方使用白色填充,进行切割
求解图片:将图片转化为像素矩阵,进行一一对比
2.解题代码:使用BFS进行对数据进行预处理后,求解
定义

  • 状态(S):每个棋盘的布局称为一个状态,其中状态 [[1,2,3],[4,5,6],[7,8,9]] 称为零状态
  • 代价(C):从当前状态到零状态所需的最小步数
  • 预处理:从零状态开始,使用BFS遍历每个状态,在遍历的同时,给每个状态标记遍历的层号(零状态为第0层),即代价。
    为防止重复遍历某些状态,使用一个字典保存遍历过的状态及其对应层号,即 q_tab={s1:c1, s2:c2, ..., s181440:c181440}。
  • 归位:在q_tab 中查找与当前状态相连的状态的代价,选择代价最小的状态作为下一步决策。

解题——流程图

4)重要代码片段

数据预处理:

  • 通过 BFS 构建 q_tab 表,即“状态->代价”映射表
import numpy as np

class Generator:
    def __init__(self):
        self.n=3  #棋盘阶数
        self.N=self.n*self.n  #棋盘中棋子个数(包含空格)
        self.dict={}  #用于判重
        self.que_qi=[]  #用于广度优先搜索中,辅助队列(存储棋盘)
        self.que_bk=[]  #用于广度优先搜索中,辅助队列(存储空格)
        self.que_lv=[]  #用于广度优先搜索中,辅助队列(存储层数)
        self.deep=31  #遍历深度
        self.X=[-1,-self.n,1,self.n]  #棋子移动方位
        self.qi_init="123456789"  #初始棋盘布局

    
    def move(self,qi,blank,x,level):  #将空格 blank 移向 x 位置
        if x<0 or x>=self.N or blank==x:
            return
        if abs(blank%self.n-x%self.n)>1:
            return
        if blank<x:
            temp=qi[:blank]+qi[x]+qi[blank+1:x]+qi[blank]+qi[x+1:]
        else:
            temp=qi[:x]+qi[blank]+qi[x+1:blank]+qi[x]+qi[blank+1:]
        if temp in self.dict:
            return
        self.dict[temp]=level+1
        self.que_qi=[temp]+self.que_qi
        self.que_bk=[x]+self.que_bk
        self.que_lv=[level+1]+self.que_lv
        
    def bfs(self):  #广度优先搜索
        self.que_qi=[self.qi_init]+self.que_qi
        self.que_bk=[9-1]+self.que_bk   # 根据题目中空格所在位置对应的值进行修改
        self.que_lv=[0]+self.que_lv
        self.dict[self.qi_init]=0
        lv=0
        while lv<self.deep and self.que_qi!=[]:
            qi=self.que_qi.pop()
            bk=self.que_bk.pop()
            lv=self.que_lv.pop()
            direction=np.random.permutation(4) #生成0~3的随机排列
            for i in direction:
                x=bk+self.X[i]
                self.move(qi,bk,x,lv)
                
    def save_jie3(self):  #保存样本数据
        num=len(self.dict)
        data=np.zeros((num,self.N))
        label=np.zeros(num)
        i=0
        for k in self.dict:
            label[i]=self.dict[k]
            for j in range(self.N):
                data[i][j]=float(k[j])
            i+=1
        np.savez("jie3.npz",data=data,label=label)
        
    def save_q_tab(self):  #保存Query表
        k=list(self.dict.keys())
        v=list(self.dict.values())
        np.savez("q_tab.npz",k=k,v=v)
        
    def save_txt(self):  #保存为TXT文档
        file=open("jie3.txt",'w')
        s=""
        for k in self.dict:
            v=self.dict[k]
            for i in range(self.N):
                s+=k[i]+' '
            s+=str(v)+'\n'
        file.write(s)
        file.flush()
        file.close()
                
gen=Generator()
gen.bfs()
gen.save_jie3()
gen.save_q_tab()
gen.save_txt()

运行完以后,q_tab.npz 保存了“状态 -> 代价”的映射,jie3.txt 的部分内容如下:

可得到代价分布图

归位:
最重要的是预测下一步走向:
调用代价函数得到的“状态--代价”表,根据随机序列,寻找代价最小的序列为下一步对应的状态矩阵

class Prediction:
    def __init__(self):
        self.n=3
        self.N=self.n*self.n
        q_tab=np.load('q_tab.npz')
        k=q_tab['k']
        v=q_tab['v']
        self.q_tab=dict(zip(k,v))  #构建Q表
        self.X=[-1,0,1,0] #方向
        self.Y=[0,-1,0,1]
        
    def pre_step(self,x):  #预测状态 x 对应的步数
        x=np.array(x).reshape(1,-1)
        k = ""
        for i in range(self.N):
            k+=str(x[0,i])
        v=self.q_tab.get(k,-1)
        return v

    def pre_next(self,sta,bk_x,bk_y,bk_x_p,bk_y_p):  #预测下一步往哪个方向走
        step=[10000,10000,10000,10000]
        direction=np.random.permutation(4) #生成0~3的随机排列
        for i in direction:
            x=bk_x+self.X[i]
            y=bk_y+self.Y[i]
            if x<0 or x>=self.n or y<0 or y>=self.n or x==bk_x_p and y==bk_y_p:
                continue
            t=sta[x][y]
            sta[x][y]=7     # 修改空格位置的值
            sta[bk_x][bk_y]=t
            step[i]=self.pre_step(sta)
            sta[x][y]=t
            sta[bk_x][bk_y]=7   # 根据移动格修改
        return np.argmin(step)

最后定义棋盘,采用多线程实现:主线程负责监听用户动作(点击“开始/暂停”按钮),子线程负责棋盘更新。
主线程

子线程

class Qipan:
    def __init__(self):
        self.n=3
        self.N=self.n*self.n
        self.init=np.arange(1,self.N+1).reshape(self.n,self.n)
        self.qipan = self.init.copy()
        self.bk_x=1
        self.bk_y=0 # 打乱后的空格位置
        self.bk_x_p=-1
        self.bk_y_p=-1
        self.pre=Prediction()
        self.started=False  #标记是否开始
        self.X=[-1,0,1,0]
        self.Y=[0,-1,0,1]
        self.dir = []

    def make_qipan(self):  #生成棋盘
        print("请输入待解数组")
        string = sys.stdin.readline().strip()
        arr = []
        count = 0
        for i in string:
            try:
                arr.append(int(i))
            except:
                if i == '[':
                    count += 1
                pass
        cols = int(len(arr) / count)
        grid = []
        cur = []
        for i in arr:
            if len(cur) <= cols:
                cur.append(i)
            else:
                grid.append(cur)
                cur = []
                cur.append(i)
        grid.append(cur)
        self.qipan=grid
        self.step=0  #提示计步
        self.started=True  #标记是否开始

    def move(self,x,y):  #移动棋子
        if x<0 or x>=self.n or y<0 or y>=self.n:
            return
        self.qipan[self.bk_x][self.bk_y]=self.qipan[x][y]       # 交换值
        self.qipan[x][y]=1      # 空格位置的值
        self.bk_x_p=self.bk_x   #交换位置
        self.bk_y_p=self.bk_y
        self.bk_x=x
        self.bk_y=y

    def change_move(self,a,b,c,d):  # 交换棋子
        t = self.qipan[a][b]
        self.qipan[a][b] = self.qipan[c][d]
        self.qipan[c][d] = t

    def is_finish(self):  #判断游戏是否结束
        for i in range(self.n):
            for j in range(self.n):
                if self.qipan[i][j]!=self.init[i][j]:
                    return False
        print("".join(self.dir))
        return True

    def show(self):  #打印当前棋盘状态
        s=""
        for i in range(self.n):
            for j in range(self.n):
                if self.qipan[i][j]==1:     # 当位置是空格处的值时,打印空格
                    s+="  "
                else:
                    s+=str(self.qipan[i][j])+" "
            s+="\n"
        print(s)

    def direction(self,i):
        if i == 0:
            print("w")
            dire = 'w'
        if i == 1:
            print("a")
            dire = 'a'
        if i == 2:
            print("s")
            dire = 's'
        if i == 3:
            print("d")
            dire = 'd'
        self.dir.append(dire)

    def tips(self):
        if self.step != 13-1: # 不在强制交换的步数中
            i = self.pre.pre_next(self.qipan, self.bk_x, self.bk_y, self.bk_x_p, self.bk_y_p)
            x = self.bk_x + self.X[i]
            y = self.bk_y + self.Y[i]
            self.move(x, y)
            self.direction(i)
            self.step += 1
            print("step", self.step)
            print("进行交换的棋子:", "(", x, ",", y, ")")
            self.show()
        else: # 在强制交换的步数中
            self.show()
            print("请输入要交换的值的坐标:两个")
            print("第一个")
            a, b = map(eval, input('两个值:').split())
            print("第二个")
            c, d = map(eval, input('两个值:').split())
            self.change_move(a,b,c,d)      # 移动强制交换的两个值
            s = self.pre.pre_step(self.qipan) # 移动后确定是否有解
            if s == -1: #强制交换后无解
                print("无解,需要进行手动交换")
                # 打印棋盘
                self.show()
                print("请选择需要交换的数值")
                # 输入两个值
                p = -1 # 因为无解返回-1
                while(p == -1): # 当为-1即为无解时,进入循环
                    print("请输入要交换的值的坐标:两个")
                    print("第一个")
                    g, h = map(eval, input('两个值:').split())
                    print("第二个")
                    z, c = map(eval, input('两个值:').split())
                    self.change_move(g,h,z,c)
                    p = self.pre.pre_step(self.qipan)    # 如果有解那p将不为-1,退出循环
                    self.change_move(z,c,g,h)
                # 退出循环说明有解了
                print("开始正常运行")
                self.change_move(g,h,z,c)
                i = self.pre.pre_next(self.qipan, self.bk_x, self.bk_y, self.bk_x_p, self.bk_y_p)
                x = self.bk_x + self.X[i]
                y = self.bk_y + self.Y[i]
                self.move(x, y)
                self.direction(i)
                self.step += 1
                print("step", self.step)
                print("进行交换的棋子:", "(", x, ",", y, ")")
                self.show()
            else: #强制交换后有解
                i = self.pre.pre_next(self.qipan, self.bk_x, self.bk_y, self.bk_x_p, self.bk_y_p)
                x = self.bk_x + self.X[i]
                y = self.bk_y + self.Y[i]
                self.move(x, y)
                self.direction(i)
                self.step += 1
                print("step", self.step)
                print("进行交换的棋子:", "(", x, ",", y, ")")
                self.show()

5)性能分析与改进

性能分析:(使用pycharm里的分析)没错又是它,嘻嘻嘻
主函数的性能分析:

生成“状态—代价映射”表的性能分析:

改进:
由于刚开始的算法只是随机生成棋盘进行求解,所以要做的改进就是自定义棋盘进行求解
自定义输入棋盘,并将自定义棋盘赋值给self.qipan使得可以对棋盘进行调用

    def make_qipan(self):  #生成棋盘
        print("请输入待解数组")
        string = sys.stdin.readline().strip()
        arr = []
        count = 0
        for i in string:
            try:
                arr.append(int(i))
            except:
                if i == '[':
                    count += 1
                pass
        cols = int(len(arr) / count)
        grid = []
        cur = []
        for i in arr:
            if len(cur) <= cols:
                cur.append(i)
            else:
                grid.append(cur)
                cur = []
                cur.append(i)
        grid.append(cur)
        self.qipan=grid
        self.step=0  #提示计步
        self.started=True  #标记是否开始

强制转换,增加新函数change_move,以及强制交换无解做法,满足强制交换要求

    def change_move(self,a,b,c,d):  # 交换棋子
        t = self.qipan[a][b]
        self.qipan[a][b] = self.qipan[c][d]
        self.qipan[c][d] = t
    def tips(self):
        if self.step != 13-1: # 不在强制交换的步数中....................................................................
            i = self.pre.pre_next(self.qipan, self.bk_x, self.bk_y, self.bk_x_p, self.bk_y_p)
            x = self.bk_x + self.X[i]
            y = self.bk_y + self.Y[i]
            self.move(x, y)
            self.direction(i)
            self.step += 1
            print("step", self.step)
            print("进行交换的棋子:", "(", x, ",", y, ")")
            self.show()
        else: # 在强制交换的步数中
            self.show()
            print("请输入要交换的值的坐标:两个")
            print("第一个")
            a, b = map(eval, input('两个值:').split())
            print("第二个")
            c, d = map(eval, input('两个值:').split())
            self.change_move(a,b,c,d)      # 移动强制交换的两个值
            s = self.pre.pre_step(self.qipan) # 移动后确定是否有解
            if s == -1: #强制交换后无解
                print("无解,需要进行手动交换")
                # 打印棋盘
                self.show()
                print("请选择需要交换的数值")
                # 输入两个值
                p = -1 # 因为无解返回-1
                while(p == -1): # 当为-1即为无解时,进入循环
                    print("请输入要交换的值的坐标:两个")
                    print("第一个")
                    g, h = map(eval, input('两个值:').split())
                    print("第二个")
                    z, c = map(eval, input('两个值:').split())
                    self.change_move(g,h,z,c)
                    p = self.pre.pre_step(self.qipan)    # 如果有解那p将不为-1,退出循环
                    self.change_move(z,c,g,h)
                # 退出循环说明有解了
                print("开始正常运行")
                self.change_move(g,h,z,c)
                i = self.pre.pre_next(self.qipan, self.bk_x, self.bk_y, self.bk_x_p, self.bk_y_p)
                x = self.bk_x + self.X[i]
                y = self.bk_y + self.Y[i]
                self.move(x, y)
                self.direction(i)
                self.step += 1
                print("step", self.step)
                print("进行交换的棋子:", "(", x, ",", y, ")")
                self.show()
            else: #强制交换后有解
                i = self.pre.pre_next(self.qipan, self.bk_x, self.bk_y, self.bk_x_p, self.bk_y_p)
                x = self.bk_x + self.X[i]
                y = self.bk_y + self.Y[i]
                self.move(x, y)
                self.direction(i)
                self.step += 1
                print("step", self.step)
                print("进行交换的棋子:", "(", x, ",", y, ")")
                self.show()

还有一个失败的算法5555

使用神经网络映射 q_tab 表。基于神经网络的方法和使用 q_tab 表差不多,主要差别是:在预处理阶段,加了神经网络训练;在归位时,不在q_tab 表中查找代价,而是在神经网络中映射代价。神经网络的网络参数空间小于q_tab 表的空间,这样节省了不少内存空间。(不过是借鉴的,看不懂,没法继续了)

6)性能分析图和程序中消耗最大的函数

消耗最大的以及耗时最长的应该是Generator.py了,毕竟是肉眼可见的慢hhhhh

Github签入记录


遇到的异常

碰到最不好解决的异常,就是在修改代码,从随机生成棋盘到最后自定义棋盘的过程,每个类之间互相联系,修改代码要更加谨慎
由于使用了“代价——映射”表,所以程序其他程序只能等待该程序运行完,才能运行,由于表中生成的是所有可达排列,所以很浪费时间
在进行强制交换时,想要用“状态——代价”表判断有无解,怎样利用好这个表,是个问题
最后的解决

因为映射表中的内容,是可达状态到零状态的所有排列,所以,只要判断每一步生成的序列是否在表中,get()即可,不在则返回-1,
则当不为-1时,说明有解,为-1时,无解
无解时再进行一次自己交换,为了使交换会有解,每次交换都要进行判断是否为-1,再决定是否进行交换
但是最后又出现了一个问题,当需要交换的位置中有空格时,会发生混乱,还没有解决
只能运行两次代码,衔接起来求解,非常慢了可以说
有何收获

评价队友

值得学习的地方
这次AI的算法非常复杂,搭档能在短短几星期将其读通,并且改出来!非常非常棒!

需要改进的地方
希望我们能够多沟通,多交流想法

PSP和学习进度条

PSP

PSP Personal Software Process Stages 预估耗时(分钟) 实际耗时(分钟)
Planning 计划 30 25
· Estimate · 估计这个任务需要多少时间 30 25
Development 开发 1790 2084
· Analysis · 需求分析 (包括学习新技术) 620 642
· Design Spec · 生成设计文档 60 55
· Design Review · 设计复审 30 32
· Coding Standard · 代码规范 (为目前的开发制定合适的规范) 30 30
· Design · 具体设计 300 310
· Coding · 具体编码 500 570
· Code Review · 代码复审 50 45
· Test · 测试(自我测试,修改代码,提交修改) 200 400
Reporting 报告 120 160
· Test Report · 测试报告 60 70
· Size Measurement · 计算工作量 30 50
· Postmortem & Process Improvement Plan · 事后总结, 并提出过程改进计划 30 40
· 合计 1940 2269

学习进度条

第N周 新增代码(行) 本周学习耗时(小时) 累计学习耗时(小时) 重要成长
1 256 20 20 初步掌握BFS以及代价映射思路
2 185 10 30 尝试使用DNN,理解思路(但未采用)
3 56 6 36 用映射表的思路进行强制转换
4 185 6 42 图片像素化,矩阵相似

posted on 2020-10-19 22:36  可可爱爱秃头程序圆  阅读(86)  评论(0编辑  收藏  举报