软工结队作业
结队编程作业:图片华容道
1.结队编程的具体分工
白霖(031804101) | 游戏代码实现和算法 |
熊崟(081800228) | 原型设计和博客编写 |
1、基本说明与设计思路
2.原型设计
通过HTML5,JavaScript,CSS的编写创建本地网页,通过网页的形式展现游戏内容和相关功能。
- 游戏中可以查看复原后的原图
- 游戏中可以查看复原后的原图
- 在游戏过程中可以重新开始。
- 游戏记录时间和步数。
- 可以按照时间和步数分别查询成绩排名以及对应的移动过程。
2.1.原型模型设计(Axure Rp)
原型模型和实际游戏界面在排版上不同,对应的功能键和功能相同
原型模型的构建应用的软件为Axure Rp 8
2.1 主页面(实际游戏页面)
排行榜:按照步数或者时间进行编排成功游戏者和对应的步数和时间
开始游戏:获取分割打乱后的图片,同时启动时间计时器(游戏进行时为“重新开始”,成功时为“再来一次”,都可以开始新一轮的游戏)
提示:获取并演示解过程
显示图片:可在左边显示原图,以供游戏参考(再点一次则隐藏)
3、困难与解决办法
在原型实现的过程中,运用动态面板进行操作和记录困难,多次出错。通过多次的查询和实例理解最终实现。
2.1AI与原型设计实现
1、代码实现思路
1.AI的原型实现
1)代码实现思路
网络接口的使用:
使用python的requests库来进行get和post请求
def getData(): url = "http://47.102.118.1:8089/api/problem?stuid=031804101" r = requests.get(url) data = json.loads(r.text) imgdata = base64.b64decode(data['img']) step = data['step'] swap = data['swap'] uuid = data['uuid'] # 将图片写到本地 path = "C:\\software\\char.jpg" file = open(path,'wb') file.write(imgdata) file.close() return step,swap,uuid def postData(uuid,ope,swap): url = "http://47.102.118.1:8089/api/answer" data = { "uuid":uuid, "answer":{ "operations": ope, "swap": swap } } r = requests.post(url,json=data) return r.text
代码组织与内部实现设计(类图)
算法流程图
我认为算法的关键是图片识别,如果图片都识别不了,后面就不用做了
采用KNN算法实现
# 训练模型,我练我自己? import os import numpy as np from PIL import Image from sklearn.neighbors import KNeighborsClassifier from sklearn.externals import joblib # subimg中每个文件夹都包含9张切割完的子图 path = 'C:\\software\\subimg\\' dirs = os.listdir(path) length = len(dirs) X_train=[] y_train=[] for j in range(length): dirpath = path + dirs[j] + "\\" names = os.listdir(dirpath) for i in range(len(names)): f = Image.open(dirpath+names[i],"r") # 判断是不是纯黑,纯黑就跳过 extrema = f.convert("L").getextrema() if extrema == (0, 0): continue # 图片有三个通道,通常是转为灰度图,在这我取其中一个通道 image = np.array(f) image = image[:,:,0] # 转换为2维的numpy数组 image = image.reshape(1,-1) # 有监督学习,还需要y标签 X_train.append(image) y_train.append(j*10+i) X_train = np.array(X_train) y_train = np.array(y_train) # 还需要再将X_train转为2维 X_train = X_train.reshape(X_train.shape[0],-1) # 预测的时候找和它最像的就行了 clf = KNeighborsClassifier(1) clf.fit(X_train,y_train) #保存模型,需要时直接加载使用 joblib.dump(clf, 'C:\\software\\clf.pkl')
# 识别图片,返回相应的序列及白块位置 # 将获取的图片转为np数组,并添加到列表 def imgToArr(): arrlist = [] f = Image.open('C:\\software\\char.jpg',"r") array = np.array(f) for i in range(3): for j in range(3): subarray = array[i*300:(i+1)*300,j*300:(j+1)*300] arrlist.append(subarray) return arrlist def getNum(): arrs = imgToArr() # load model clf = joblib.load('C:\\software\\clf.pkl') ans = [] dirindex = 0 # 对arrs中的每个np数组进行预测 for arr in arrs: # s1==0 白 s2==0 黑 s1 = np.count_nonzero(arr==0) s2 = np.count_nonzero(arr==255) if s1==0: ans.append("白") elif s2==0: ans.append("黑") else: arr = arr[:,:,0] arr = arr.reshape(1,-1) pre = int(clf.predict(arr)) dirindex=pre//10 ans.append(pre) path = 'C:\\software\\subimg\\' dirs = os.listdir(path) # 白块的位置 white = ans.index("白") # matchNums是已经确定的子图,noMatchNums还没确定的子图,有一白(白块恰好就是黑色的)和一黑一白两种情况 matchNums = [i%10 for i in ans if isinstance(i,int)] noMatchNums = [i for i in range(9) if i not in matchNums] # 对还未确定的子图进行判断 if "黑" not in ans: ans[ans.index("白")] = noMatchNums[0] else: img1path = path + dirs[dirindex] + "\\" files = os.listdir(img1path) img1path += files[noMatchNums[0]] arr = np.array(Image.open(img1path,"r")) if np.count_nonzero(arr==255) == 0: ans[ans.index("黑")] = noMatchNums[0] ans[ans.index("白")] = noMatchNums[1] else: ans[ans.index("黑")] = noMatchNums[1] ans[ans.index("白")] = noMatchNums[0] # ans是获取的图片,对应原图的序列(0-8) ans = [i%10 for i in ans] s = "" for l in ans: s+=str(l) return s,str(ans[white])
由序列,白块位置,step,swap得到结果,最开始使用遍历,遇到step=15+的20分钟还出不来
于是就对原来的方法稍微优化一下,不过只能得到大部分最优解
改进思路
# 交换字符串两个位置 def change(s,i,j): if i==j: return s a = min(i,j) b = max(i,j) return s[:a]+s[b]+s[a+1:b]+s[a]+s[b+1:] def getMethod(nums,white,step,swap): ''' 1.将部分数据写在文件里 white是指白块对应的数字 flag中的white.txt是判断当前序列在不在文件中 在的话有解,获取索引,去ans中的white.txt获得序列;否则无解 ''' ww = open("C:\\software\\flag\\"+white+".txt") qq = ww.read() pflag = qq.split("\n") ww.close() ww = open("C:\\software\\ans\\"+white+".txt") qq = ww.read() pans = qq.split("\n") ww.close() # 记录要交换的序列,交换的位置,以及最少步数 post_swap = None post_ope = None minsteps = 100 queue = [] flag = {} # 2.对强制交换后的序列也进行判重 flag2 = {} flag3 = {} lis = [[i,j] for i in range(9) for j in range(9) if i<j] flag[nums]=1 queue.append(nums+"0 ") while queue: p = queue[0] queue.remove(p) space = p.index(" ") # 步数 step2 = int(p[9:space]) # 空格+序列 me = p[space:] # 字符串 p=p[:9] if p=="012345678": if step2<=step: post_swap = [] post_ope = me[1:] break if step2 == step: s = change(p,swap[0]-1,swap[1]-1) if flag2.__contains__(s): continue else: flag2[s] = 1 if s in pflag: ins = pflag.index(s) method = pans[ins] if len(me[1:]+method)<minsteps: post_swap = [] post_ope = me[1:]+method minsteps = len(post_ope) # print("[]",post_ope,str(minsteps)) else: for li in lis: s2 = change(s,li[0],li[1]) if flag3.__contains__(s2): continue else: flag3[s2] = 1 if s2 in pflag: ins = pflag.index(s2) method = pans[ins] if len(me[1:]+method)<minsteps: post_swap = [li[0]+1,li[1]+1] post_ope = me[1:]+method minsteps = len(post_ope) # print(post_swap,post_ope,str(minsteps)) # 3.当前最短步数已经等于其它大佬的最短步数 #if minsteps == 20: # break if step2<step: pos = p.index(white) if(pos>=3):# 上 s = change(p,pos-3,pos) if(not flag.__contains__(s)): flag[s]=1 queue.append(s+str(step2+1)+me+"w") if(pos<=5):# 下 s = change(p,pos,pos+3) if(not flag.__contains__(s)): flag[s]=1 queue.append(s+str(step2+1)+me+"s") if(pos%3!=0):# 左 s = change(p,pos-1,pos) if(not flag.__contains__(s)): flag[s]=1 queue.append(s+str(step2+1)+me+"a") if(pos%3!=2):# 右 s = change(p,pos,pos+1) if(not flag.__contains__(s)): flag[s]=1 queue.append(s+str(step2+1)+me+"d") return post_ope,post_swap
因为涉及到图片的读写操作,以及文件的读取,所以io操作占了较大的比例
单元测试
from AI import getMethod import unittest from BeautifulReport import BeautifulReport as br class Test(unittest.TestCase): # 测试3组:强制交换完正好还原,一开始就是还原后的序列,正常情况 def test1(self): nums = "017345628" white = "2" step = 2 swap = [3,8] print(getMethod(nums,white,step,swap)) def test2(self): nums = "012345678" white = "2" step = 2 swap = [3,8] print(getMethod(nums,white,step,swap)) def test3(self): nums = "876543210" white = "2" step = 5 swap = [3,4] print(getMethod(nums,white,step,swap)) if __name__ == "__main__": ts = unittest.TestSuite() test = [Test('test1'),Test('test2'),Test('test3'),] ts.addTests(test) br(suite).report('result.html','report','.')
2.游戏实现
游戏分为前端和后端
前端采用了html、js、bootstrap、jquery、ajax等技术,后端用python flask库搭建了个本地服务器
1)生成几百种有解的序列,写入到文件
2)游戏页面点击开始游戏/重新开始,会通过ajax的get方法向后端获取数据,包括原图(随机),由原图生成的子图,白块,位置排列,白块位置,
前端根据获得的数据进行排列数据
3)交换相邻的块,js实现.点击白块周边的块,则两个img标签的src、value属性互换,标记白块位置的变量pos变为点击的块的id
function turn(btn) { white_btn = document.getElementById(pos); btn_id = btn.id; if (pos == btn_id) return; if ((btn_id == pos - 3) || (btn_id == parseInt(pos) + parseInt(3)) || (btn_id == pos - 1 && pos != 3 && pos != 6) || (btn_id == parseInt(pos) + parseInt(1) && pos != 2 && pos != 5)) { //两块交换,就交换他们的图片路径和img标签的value值, src = white_btn.src; white_btn.src = btn.src; btn.src = src; var tmp = $("#" + pos).attr("value"); $("#" + pos).attr("value", $("#" + btn_id).attr("value")); $("#" + btn_id).attr("value", tmp); //改变步数 pos = btn_id; k = document.getElementById("cnt").innerHTML; document.getElementById("cnt").innerHTML = parseInt(k) + 1; // 判断成功 var flag = 1; for (i = 0; i < 9; i++) { if ($("#" + i).attr("value") != i) { flag = 0; break; } } if (flag) { $("#succ").attr("class", "alert alert-success"); $("#succ").attr("role", "alert"); $("#succ").text("成功解出!"); //time stop clearTimeout(timer); //can't move $("img").attr("onClick", ""); document.getElementById("start").innerHTML = "再来一局"; // 有帮助 不记录 if(!isHelp) record(); } } }
4)自动演示还原.通过jquery获得每个img标签的value,得到一串序列,加上白块的位置,通过ajax的post方法提交给后端,后端调用解法函数返回走的序列,for循环+定时器,白块逐一移到序列的每一个位置
function help() { // 如果已经还原了 var arr = new Array(9); flag = 1; for (i = 0; i < 9; i++){ arr[i] = $("#" + i).attr("value"); if(arr[i]!=i) flag=0; } if(flag) document.getElementById("succ").innerHTML = '您已经成功啦'; else { isHelp = true; var jsonString = JSON.stringify(arr); $.ajax({ type: "POST", url: "http://127.0.0.1:5000/h2", data: { "pos": jsonString, "white": arr[pos] }, dataType: "json", contentType: "application/x-www-form-urlencoded;charset=UTF-8", async: false, success: function(data) { // 从后端返回的data是一串序列,第n步移动到哪一个位置,然后设个定时器不断调用移动函数即可 arr = data; var j = 0; function fn(){ turn(document.getElementById(arr[j])); j++; } for(var i = 0; i < arr.length; i++ ){ setTimeout(fn,i*700)//还原速度 } }, error: function(XMLHttpRequest, textStatus, errorThrown) { alert("请先开始游戏"); } }); } }
5)排行榜。分为时间榜和步数榜,使用bootstrap的按钮组件实现。
2、Github的代码签入记录
3、困难及解决方法
- 问题描述
对新知识不了解,经常出错,而且错误了没提示,只是该功能失效,不好检查bug
例如:jquery改不了标签的内容,只能用dom获取标签进行修改
本机作为服务器还得处理跨域问题... - 解决尝试
网上查阅相关资料 - 是否解决
是 - 有何收获
以前有看过相关视频,但只是纸上谈兵,自己从0到1实现一个小项目还是蛮有成就感。
4、队友互评
是个大佬般的人,编程设计等方面都很强。
5、学习进度和PSP
第N周 | 新增代码(行) | 累计代码(行) | 本周学习耗时(小时) | 累计学习耗时(小时) | 重要成长 |
1 | 200 | 200 | 14 | 14 | 解决图像识别问题 |
2 | 150 | 350 | 10 | 24 | 学习html,css,js,做出游戏雏形,AI算法初步 |
3 | 200 | 550 | 16 | 40 | 学习jquery,游戏功能添加 学习ajax,实现前后端交互 |
4 | 350 | 900 | 20 | 60 | 使用bootstrap美化页面, 添加排行榜功能,改进AI算法 |
PSP表格
PSP2.1 | Personal Software Process Stages |
预估耗时 (分钟) |
实际耗时 (分钟) |
Planning | 计划 | ||
· Estimate | · 估计这个任务需要多少 时间 |
180 | 200 |
Development | 开发 | ||
· Analysis | · 需求分析 (包括学习新 技术) |
450 | 480 |
· Design Spec | · 生成设计文档 | 200 | 180 |
· Design Review | · 设计复审 | 60 | 80 |
· Coding Standard | · 代码规范 (为目前的开 发制定合适的规范) |
60 | 60 |
· Design | · 具体设计 | 100 | 120 |
· Coding | · 具体编码 | 1500 | 1600 |
· Code Review | · 代码复审 | 90 | 90 |
· Test | · 测试(自我测试,修改 代码,提交修改) |
50 | 60 |
Reporting | 报告 | ||
· Test Repor | · 测试报告 | 60 | 70 |
· Size Measurement | · 计算工作量 | 50 | 75 |
· Postmortem & Process Improvement Plan |
· 事后总结, 并提出过程 改进计划 |
100 | 90 |
· 合计 | 2900 | 3105 |