写在前面
1.我的博客地址
2.队友的博客地址
3.github项目地址
4.具体分工
姓名分工 | AI算法 | 原型设计 | 游戏实现 | 博客 |
白霖 | √ | √ | ||
熊崟 | √ | √ |
1.设计说明
通过HTML5,JavaScript,CSS的编写创建本地网页,通过网页的形式展现游戏内容和相关功能:
1)可以查看原图,或者不看(记忆大佬)。
2)在游戏过程中可以重新开始。
3)记录时间和步数。
4)根据时间和步数分别查看排名。
5)自动演示走法。
2.原型模型设计(Axure Rp)
原型模型的构建应用的软件为Axure Rp 8
主页面(实际游戏页面)
排行榜:按照步数或者时间进行编排成功游戏者和对应的步数和时间
开始游戏:获取分割打乱后的图片,同时启动时间计时器(游戏进行时为“重新开始”,成功时为“再来一次”,都可以开始新一轮的游戏)
提示:获取并演示解过程
显示图片:可在左边显示原图,以供游戏参考(再点一次则隐藏)
3.结对交流过程
4.困难与解决办法
在原型实现的过程中,运用动态面板进行操作和记录困难,多次出错。通过多次的查询和实例理解最终实现。
二、AI与原型设计实现
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("
")
ww.close()
ww = open("C:\software\ans\"+white+".txt")
qq = ww.read()
pans = qq.split("
")
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.评价你的队友。
- 值得学习的地方
实力很强,提出的idea很好,画图很漂亮 - 需要改进的地方
得早点开始准备
5.学习进度条
第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算法 |
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 |