结对编程作业
- 首先是github链接:
| |github |
| ---- | ---- | ---- |
| 吕铭飞 | 点我 |
| 王涵永 | 点我 |
-
分工:
分工 | 原型设计和游戏素材 | 原型实现 | AI | 接口测试和游戏BUG测试 |
---|---|---|---|---|
吕铭飞 | ||||
王涵永 |
[1]原型设计
[1.1]此次结对作业的设计说明
(a)按键选择与尺寸
(b)各按键功能
play_button:
exit_button:
retreat_button:
renovate_button:
next_button:
(c)游戏模拟
[1.2]原型模型采用的原型模型设计工具
原型开发工具:墨刀 MOCKINGBOT
[1.3]两人在讨论、细化和使用专用原型模型工具时的结对照片
[1.4]遇到的困难及解决方法:
(1)困难描述
(a)第一次接触原型设计,从0开始
(b)图片选择困难
(c)各个游戏按钮间对应的逻辑关系
(2)解决尝试
(a)在b站进行了视频学习:墨刀原型设计基础
(b)在操作时摸索原型设计更多“隐藏”内容
(c)花费了相当大的时间在各种图库中寻找图片并进行图片处理
(d)游戏按钮间的逻辑关系通过实践逐步完善
(3)是否解决
所遇到的问题在实践中均得到解决
(4)有何收获
通过初步原型设计的学习与实践,让我对于一个项目前期的开端有了很棒的体验(至少能先拿到原型设计的分数)。在拿到项目需求后,需要对需求先进行分析并构思框架然后进行原型设计,并在视频学习与实践中逐步完善原型设计。这样的经历让我对之后的团队项目的开发和自己个人项目的开发有了更加清晰的思路,这样的进步可以说是比学习了原型设计有更有意义的收获;今后肯定还会接触更多的原型设计,希望可以更熟练运用原型设计工具。
[2]AI算法实现:
[2.1]网络接口的使用
接口函数定义如下
####使用get函数调用api接口获取数据
def get(url_):
response = requests.get(url_)
response_dict = response.json()
head_img = response_dict['img']
head_step = response_dict['step']
head_swap = response_dict['swap']
head_uuid = response_dict['uuid']
head = base64.b64decode(head_img)#
f=open("question/picture_q.png",'wb')#将图片保存在本地
f.write(head)
f.close()
return head_step,head_swap,head_uuid
def post(url_, head_uuid, operations, head_swap):
#提交答案
data = {"uuid":head_uuid,
"answer":{
"operations":operations,
"swap":head_swap
}}
res = requests.post(url=url_, json=data)
print(res.text)
[2.2]算法的关键与关键实现部分流程图
1.AI算法关键与流程图
题目实际上可以转化为八数码问题,对于八数码问题的解决,首先要考虑是否有答案。每一个状态可认为是一个3×3的矩阵,问题即通过矩阵的变换,是否可以变换为目标状态对应的矩阵?由数学知识百度结果可知,可计算这两个有序数列的逆序值,如果两者奇偶性相同,则可通过某些变换到达,否则,这两个状态不可达。这样,就可以在具体解决问题之前判断出问题是否可解,从而可以避免不必要的搜索。
如果初始状态可以到达目标状态,那应该采取什么样的方法呢?
常用的状态空间搜索有深度优先和广度优先。广度优先是从初始状态一层一层向下找,直到找到目标为止。深度优先是按照一定的顺序前查找完一个分支,再查找另一个分支,以至找到目标为止。广度和深度优先搜索有一个很大的缺陷就是他们都是在一个给定的状态空间中穷举。这在状态空间不大的情况下是很合适的算法,可是当状态空间十分大,且不预测的情况下就不可取了。他的效率实在太低,甚至不可完成。由于八数码问题状态空间共有9!个状态,对于八数码问题如果选定了初始状态和目标状态,有9!/2个状态要搜索,考虑到时间和空间的限制,在这里采用A*算法作为搜索策略。在这里就要用到启发式搜索。
启发式搜索就是在状态空间中的搜索对每一个搜索的位置进行评估,得到最好的位置,再从这个位置进行搜索直到目标。这样可以省略大量无畏的搜索路径,提到了效率。在启发式搜索中,对位置的估价是十分重要的。采用了不同的估价可以有不同的效果。
启发中的估价是用估价函数表示的,如:f(n) = g(n) +h(n)其中f(n) 是节点n的估价函数,g(n)是在状态空间中从初始节点到n节点的实际代价,h(n)是从n到目标节点最佳路径的估计代价。 在此八数码问题中,显然g(n)就是从初始状态变换到当前状态所移动的步数,估计函数f(n)采用的是曼哈顿距离,即d(i,j)=|X1-X2|+|Y1-Y2|。
2.图像匹配关键与流程图
图像匹配我选用了感知哈希算法
哈希算法可以用来判断两个图片的相似度,通常可以用来进行图像检索。哈希算法对每一张图片生成一个“指纹”,通过比较两张图片的指纹,来判断他们的相似度,是否属于同一张图片。
哈希算法是一类算法的总称,通常包括aHash、pHash、dHash。
- aHash:平均值哈希。速度最快,但是常常不太精确。
- pHash:感知哈希。精确度高,但是速度方面较差一些。
- dHash:差异值哈希。精确度较高,且速度也较快。
他们的步骤都类似,大致可分为:
- 缩小尺寸,建议 8*8
- 灰度化
- 计算平均值
- 比较像素的灰度
- 计算哈希值
起初我们选择的是平均值hash,但在比对图片时发现无法正确比对全黑和全白的图片。和队友讨论后发现,ahash是通过判断每个像素的灰度值和平均值的关系,若大于等于平均值为1,相反为0以此生成图片的hash值,而黑白图片里的每个像素一定是大于等于平均值的,即生成的hash值相同,通过查看输出hash值也验证了这个讨论结果。也就是说两张完全黑白的图片是无法通过ahash判定出不同的!而dhash则是根据每行前一个像素大于后一个像素为1,相反为0,生成hash值,自然也不行。因此最后我们选用的是phash,即感知哈希算法,通过离散余弦变换(DCT)降低图片频率,相比aHash有更高的精度,也不会出现上述问题。
[2.3]重要的/有价值的代码片段
比较题目图片的函数定义如下:
使用phash比较切割后的图块函数定义如下:
def compare_image_with_hash(image_file1, image_file2, max_dif=0):
"""
max_dif: 允许最大hash差值, 越小越精确,最小为0
"""
ImageFile.LOAD_TRUNCATED_IMAGES = True
hash_1 = None
hash_2 = None
with open(image_file1, 'rb') as fp:
hash_1 = imagehash.phash(Image.open(fp))
#print(hash_1)
with open(image_file2, 'rb') as fp:
hash_2 = imagehash.phash(Image.open(fp))
#print(hash_2)
dif = hash_1 - hash_2
#print(dif)
if dif < 0:
dif = -dif
if dif <= max_dif:
return True
else:
return False
调用接口进行图像匹配的结果如下:
A*算法中的重要函数和类的定义:
结点类定义如下:
# 边缘队列中的节点类
class Node:
state = None # 状态
value = -1 # 启发值
step = 0 # 初始状态到当前状态的距离(步数)
action = Start # 到达此节点所进行的操作
parent = None, # 父节点
# 用状态和步数构造节点对象
def __init__(self, state, step, action, parent):
self.state = state
self.step = step
self.action = action
self.parent = parent
# 计算估计距离 = 当前状态与目标态的启发距离 + 移动当前状态花费的代价(步数)
self.value = GetDistance(state, goal_state) + step
Astar算法实现如下:
# A*算法寻找初始状态到目标状态的路径
def AStar(init, goal, head_step):
"""A*算法寻找初始状态到目标状态的路径"""
# 边缘队列初始已有源状态节点
queue = [Node(init, 0, Start, None)]
visit = {} # 访问过的状态表
count = 0 # 循环次数
# 队列没有元素则查找失败
while queue:
# GetMinIndex(queue)函数用于获取拥有最小估计距离的节点索引
index = GetMinIndex(queue)
node = queue[index]
#访问过的结点放在visit列表中
visit[toInt(node.state)] = True
count += 1
#找到目标状态,返回此结点
if node.state == goal:
return node, count
del queue[index]
# 扩展当前节点
for act in GetActions(node.state):
# 获取此操作下到达的状态节点并将其加入边缘队列中
near = Node(act(node.state), node.step + 1, act, node)
#print(near.step)
#不在visit表中的结点加入到边缘队列中
if toInt(near.state) not in visit:
queue.append(near)
return None, count
强制交换功能实现如下:
def forced_xchg(image_sequence, head_swap):
"""强制交换列表中的两个元素"""
#先转换为列表下标
head_swap[0] -= 1
head_swap[1] -= 1
a = [head_swap[0]//3, head_swap[0] % 3]
b = [head_swap[1]//3, head_swap[1] % 3]
#交换二维列表中的a,b位置的值
image_sequence[a[0]][a[1]], image_sequence[b[0]][b[1]] = image_sequence[b[0]][b[1]], image_sequence[a[0]][a[1]]
#若可解,直接返回列表;若不可解,寻找最优的交换方式,即曼哈顿距离最短的状态
if judge(image_sequence):
return image_sequence, a, b
else:
min_cost = 9999
for i in range(8):
for j in range(i+1, 9):
#先获得一份列表的深拷贝
temp = copy.deepcopy(image_sequence)
a = [i//3, i % 3]
b = [j//3, j % 3]
temp[a[0]][a[1]], temp[b[0]][b[1]] = temp[b[0]][b[1]], temp[a[0]][a[1]]
#若此状态可解,则计算曼哈顿距离
if judge(temp):
cost = GetDistance(temp, goal_state)
if min_cost > cost:
min_cost = cost
min_a = list(a)
min_b = list(b)
#根据最小代价交换列表下标
image_sequence[min_a[0]][min_a[1]], image_sequence[min_b[0]][min_b[1]] = image_sequence[min_b[0]][min_b[1]], image_sequence[min_a[0]][min_a[1]]
#返回交换后的列表和自由交换的列表下标
return image_sequence,min_a,min_b
曼哈顿距离的计算、判断是否可解(逆序对的奇偶性)等函数实现较为简单,也能在网上找到很多方法和代码,故不贴出,可在github查看。
[2.4]性能分析与改进
原本想当然的使用了BFS,发现盲目式搜索确实太慢了,而且要是用简单的广搜也不符合这次设计算法这个任务的本意嘛,人工智能课里也学习过启发式搜索,所以去学习了一下A算法的实现。A采用广度优先搜索策略,在搜索过程中使用启发函数,即有大致方向的向前进虽然目标有时候不是很明确。A改成A后时间看起来好了不少,但是还是有些慢,后来发现舍友用的是IDA,迭代加深搜索算法,会在搜索过程中采用估值函数剪枝,以减少不必要的搜索。但是离DDL差不多了也已经来不及修改了。(下次一定早早问大佬)
[2.5]性能分析图和程序中消耗最大的函数
可以看出图像匹配和A算法各占一半,当然,图像匹配使用的是Phash时间确实要长一些,A这里的确是我写的算法性能不好,因为我先冲着把题都做对去的,结果大比拼排名真的好惨。
[2.6]项目部分单元测试代码,测试函数,构造测试数据的思路
单元测试主要使用的是题目提供的网络接口进行测试。
测试代码如下:
def test_my(self):
url_ = "http://47.102.118.1:8089/api/problem?stuid=031802230"
head_step,head_swap,head_uuid = api.get(url_)
operations = ""
x,y = head_swap[0],head_swap[1]
free_swap, operations = main(head_step,head_swap,head_uuid,x,y)
url_ = "http://47.102.118.1:8089/api/answer"
api.post(url_, head_uuid, operations, free_swap)
[3]原型实现:
[3.1]代码组织与内部实现设计(类图)
[3.2]游戏截图
[3.3]实现思路
大佬都写小程序,我只能用用pygame之类的。pygame学起来还是比较容易的。
主程序:
import sys
import os
import pygame
import game_function as gf
import random
from PIL import Image
from pygame.sprite import Group
from settings import Settings
from block import Block
from stepboard import Stepboard
from game_stat import GameStats
from button import Button
from logo import Logo
from background import Background
os.chdir(sys.path[0])
def run_game():
# 初始化背景设置
image_list = gf.init_images()
pygame.init()
ai_settings = Settings()
screen = pygame.display.set_mode((ai_settings.screen_width, ai_settings.screen_height))
background = Background(ai_settings, screen)
stats = GameStats(ai_settings)
sb = Stepboard(ai_settings, screen, stats)
pygame.display.set_caption("图片华容道")
logo = Logo(ai_settings, screen)
# 创建开始游戏按钮
play_button = Button(ai_settings, screen, stats, "", "images_material/play_button.png")
replay_button = Button(ai_settings, screen, stats, "", "images_material/again.png")
exit_button = Button(ai_settings, screen, stats, "", "images_material/exit_button.png")
back_button = Button(ai_settings, screen, stats, "", "images_material/back.png")
reset_button = Button(ai_settings, screen, stats, "", "images_material/reset.png")
# 创建滑块列表
blocks = list()
# 填充滑块列表
gf.create_all_blocks(ai_settings, screen, blocks, image_list) # 把切割好的图像列表传进来
BLOCKS_ORI = list(blocks)
reset_blocks = list()
# 开始游戏主循环
while True:
# 监视键盘和鼠标事件
gf.check_events(ai_settings, screen, blocks, BLOCKS_ORI, reset_blocks, stats, sb, play_button, replay_button, reset_button, exit_button, back_button, image_list)
if stats.game_menu:
gf.update_screen_menu(ai_settings, screen, blocks, stats, sb, play_button, exit_button, logo, background)
else:
gf.update_screen_playing(ai_settings, screen, blocks, stats, sb, replay_button, back_button, reset_button, background)
run_game()
主要的游戏功能都封装在game_function中,各种类也都是单独放在一个py文件里,代码太长就放个截图
[4]贴出Github的代码签入记录,合理记录commit信息。
做之前没有认真审题,不知道要有签入记录,初版有一些bug所以当时就没上传,github上只上传了目前最新的代码
[5]遇到的代码模块异常或结对困难及解决方法。
[5.1]问题描述
原型设计刚开始无从下手,毕竟是一个从没了解过的东西,也不知该如何实现游戏功能,如果做小程序还要考虑学习成本,ai算法反而是最好入手的东西(虽然做出来都不咋地)。
做的过程中由于时间安排不太合理,再者对python也不够熟练,很多地方走了弯路,比如小游戏刚开始无法重置,算法的强制交换有问题导致只能正确解出3/4左右的测试样例。
[5.2]解决尝试
花了一段时间去学习pygame,因为我知道我们做不了前后端,小程序基本没可能实现,果断选择制作电脑版免得浪费时间,然后就是原型工具的使用,主要靠队友,我基本上就在旁边看看。算法猛肝了几天终于有点了起色。虽说性能不行,但至少功能无误。
[5.3]是否解决
已解决,亟待优化
[5.4]有何收获
学会了制作简单的小游戏,加强了编写代码的能力,学会了网络接口的使用,一步步爬坑,改BUG,接口测试终于全是True的时候还是很有成就感的。
[6]评价你的队友。
[6.1]值得学习的地方:
王涵永做事积极性很高,沉着冷静,不慌不忙,还经常鼓励我,请我喝AD钙和奶茶,让我更有写代码的动力。
[6.2]需要改进的地方:
希望涵永加强自己买AD钙的能力的编程能力。
[7]PSP表格和学习进度条
| PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
| ---- | ---- | ---- | ---- | ---- | ---- |
| Planning | 计划 | | |
| Estimate | 估计这个任务需要多少时间 | 20 | 20 |
| Development | 开发 | | |
| Analysis | 需求分析 (包括学习新技术) | 600 | 840 |
| Design Spec | 生成设计文档 | 30 | 120 |
| Design Review | 设计复审 | 20 | 20 |
| Coding Standard | 代码规范 (为目前的开发制定合适的规范) | 30 | 50 |
| Design | 具体设计 | 120 | 400 |
| Coding | 具体编码 | 800 | 1460 |
| Code Review | 代码复审 | 60 | 90 |
| Test | 测试(自我测试,修改代码,提交修改) | 200 | 200 |
| Reporting | 报告 | | |
| Test Repor | 测试报告 | 30 | 30 |
| Size Measurement | 计算工作量 | 15 | 25 |
| Postmortem & Process Improvement Plan | 事后总结, 并提出过程改进计划 | 45 | 60 |
| | 合计 | 1970 | 3315 |
学习进度条
第N周 | 新增代码(行) | 累计代码(行) | 本周学习耗时(小时) | 累计学习耗时(小时) | 重要成长 |
---|---|---|---|---|---|
1 | 0 | 0 | 8 | 8 | 熟悉Pygame模块的特性并学习制作了简单的游戏样例 |
2 | 524 | 524 | 12 | 20 | 完成了图像匹配和BFS,使用pygame制作了基本的游戏界面 |
3 | 797 | 1321 | 18 | 38 | 修改BFS算法为A*,完成了游戏的基本功能,完善了游戏界面 |
4 | 300 | 1621 | 8 | 46 | 完成AI大比拼的接口,修复了游戏的一些BUG |