github:https://github.com/1daimon/main.git
bilibili:https://www.bilibili.com/video/BV1m8411W7wS/
小程序名称:北冥有鱼plus
一、结对探索(4分)
1.1 队伍基本信息(1分)
结对编号:13 ;队伍名称:煤油忒涩
学号 | 姓名 | 作业博客链接 | 具体分工 |
---|---|---|---|
032002109 | 陈艺栋 | 文本 | 程序主体,前端,后端,ai算法 |
032002107 | 陈曦 | 文本 | ui原型,ai算法、介绍视频 |
1.2 描述结对的过程(1分)
很明显,看学号,同一宿舍,好沟通,意见不同了,可以直接线下solo,反正人跑不掉。
1.3 非摆拍的两人在讨论设计或结对编程过程的照片(2分)
二、原型设计(16分)
2.1 原型工具的选择(2分)
我们选择了Pixso作为原型工具,这款工具的优势在于操作界面简单、功能丰富;内建资源多且免费;实时自动云存储且支持多人线上协作。打开文件速度快,操作丝滑,且入门难度不大,因而我们选择了它。
2.2 遇到的困难与解决办法(3分)
困难1:现成的素材库的素材如果直接用在原型中,会显得很违和,比如:一些按钮看起来不像按钮,反倒像花纹;
困难2:在进行背景图、主题图和logo设计时,能够找到的素材总是达不到我们的预想;
对困难1和困难2的解决方法:利用iPad的绘图软件Procreate对按钮图标、背景图、Logo、棋盘图等进行加工绘制,结合PS软件对相关图片进行处理调整,再运用至原型中,视觉效果有所改良。
困难3:设计Logo时,鲲鲲很难画好看,设计棋盘时,棋盘中间分界的小篆书很难写好看;
对困难3的解决方法:一点一点慢慢改,总会好看的。
2.3 原型作品链接(5分)
https://pixso.cn/app/share/f/Beznd0zwclsWpTjfUTnNXwKBHS-OdrWM
2.4 原型界面图片展示(6分)
整体预览
菜单页
注册页
登录页
个人主页
对战页
游戏规则页
我们的创新点
1.独具特色的LOGO
2.(重要!)后端采用unicloud 开发
三、编程实现(14分)
3.1 网络接口的使用(2分)
通过unicloud云开发进行后端接口的开发实现登录注册登出等等基础接口,使用java语言完成websocket的开发实现在线联机对战。
3.2 代码组织与内部实现设计(类图)(2分)
前后端
AI部分
3.3 说明算法的关键与关键实现部分流程图(2分)
前端部分逻辑
比较复杂的点关键在于,如何在对战对己方与对方的操作进行限制,防止错误操作导致游戏的混乱,即判断本方是否投掷骰子,本方是否下子,对方是否投掷骰子,对方是否下子。通过对一些变量的判断更改来进行提示与限制。
websocket
websocket实现的关键就是什么时候接收值什么时候传送值,如何实现客户端与服务器的互通。在进行在线对战时,先进入房间等待,服务器储存在房间等待的用户信息,随机分配对手,进入对战后,监听每个玩家的操作,并将落子结果与棋盘结果传送到服务器并由服务器将之转发给另一方。
AI部分逻辑
简单模式算法流程图如下:关键在于搜索未填格子最多的行
困难模式算法流程图如下:关键在于通过模拟计算得到下在每一格的预估收益,并且根据已填格子情况进行修改。
3.4 贴出重要的/有价值的代码片段并解释(2分)
前端
step_in1(index){
if(this.please==0){
this.dialogToggle1('info')
return
}
if(this.click==1){
this.dialogToggle3('info')
return
}
if(this.use2==1){
this.dialogToggle1('info')
return
}
if(this.sum1[index]!=0){
this.dialogToggle4('info')
return
}
this.changenum1=1
this.ready1=1
this.click=1
this.ready2=true
console.log(index)
this.sum1[index]=this.result_num
this.ima_list1[index]=this.array_t[this.result_num-1]
console.log((this.ima_list1[index]))
this.ready=true
this.isShow= false// 更新dom
this.$nextTick(()=>{
this.isShow = true
})
this.use1=0
this.click=1
this.full=this.check(this.sum1)
if(0<=index &&index<3){
for (var i=0;i<3;i++){
if(this.sum2[i]==this.sum1[index]){
this.sum2[i]=0
this.ima_list2[i]=''
}
}
this.isShow= false// 更新dom
this.$nextTick(()=>{
this.isShow = true
})
}else if(3<=index &&index<6){
for (var i=3;i<6;i++){
if(this.sum2[i]==this.sum1[index]){
this.sum2[i]=0
this.ima_list2[i]=''
}
}
this.isShow= false// 更新dom
this.$nextTick(()=>{
this.isShow = true
})
}else{
for (var i=6;i<9;i++){
if(this.sum2[i]==this.sum1[index]){
this.sum2[i]=0
this.ima_list2[i]=''
}
}
this.isShow= false// 更新dom
this.$nextTick(()=>{
this.isShow = true
})
}
if(this.full==1){
console.log(this.sum1,this.sum2)
this.score1=this.calculate(this.sum1)
this.score2=this.calculate(this.sum2)
if(this.score1>this.score2){
this.winner='user1'
}else{
this.winner='user2'
}
this.message = `用户分数user1:${this.score1}user2:${this.score2}胜者:${this.winner}`
this.dialogToggle5('info')
return
}
},
step_in2(index){
if(this.please==0){
this.dialogToggle1('info')
return
}
if(this.click==1){
this.dialogToggle3('info')
return
}
if(this.use1==1){
this.dialogToggle1('info')
return
}
if(this.sum2[index]!=0){
this.dialogToggle4('info')
return
}
this.changenum2=1
this.ready2=1
this.click=1
console.log(index)
this.ready1=true
this.sum2[index]=this.result_num
this.ima_list2[index]=this.array_t[this.result_num-1]
console.log((this.ima_list2[index]))
this.isShow= false// 更新dom
this.$nextTick(()=>{
this.isShow = true
})
this.use2=0
this.click=1
this.full=this.check(this.sum2)
if(0<=index &&index<3){
for (var i=0;i<3;i++){
if(this.sum1[i]==this.sum2[index]){
this.sum1[i]=0
this.ima_list1[i]=''
}
}
this.isShow= false// 更新dom
this.$nextTick(()=>{
this.isShow = true
})
}else if(3<=index &&index<6){
for (var i=3;i<6;i++){
if(this.sum1[i]==this.sum2[index]){
this.sum1[i]=0
this.ima_list1[i]=''
}
}
this.isShow= false// 更新dom
this.$nextTick(()=>{
this.isShow = true
})
}else{
for (var i=6;i<9;i++){
if(this.sum1[i]==this.sum2[index]){
this.sum1[i]=0
this.ima_list1[i]=''
}
}
this.isShow= false// 更新dom
this.$nextTick(()=>{
this.isShow = true
})
}
if(this.full==1){
this.score1=this.calculate(this.sum1)
this.score2=this.calculate(this.sum2)
if(this.score1>this.score2){
this.winner='user1'
}else{
this.winner='user2'
}
this.message = `用户分数: user1:${this.score1} ; user2:${this.score2} ; 胜者 ${this.winner}`
this.dialogToggle5('info')
return
}
},
这一部分是前端本地对战时,棋盘的点击事件函数,通过一些变量来控制操作的正确性,如判断是否已经摇过骰子,是否已经点击下子,是否下在本方棋盘,是否下载空格子,以及一些游戏规则的实现,如判断同行是否消去,游戏是否结束等。
后端
@OnOpen
public void onOpen( Session session){
this.session = session;
index++;
try {
Result result = new Result();
if(index%2==0){
WebSocket socket1 = webSocketMap.get((index-1)+"");
if(socket1!=null){
result.setBout(true);
result.setMessage("系统:游戏开始,请您先掷骰!");
result.setColor("black");
JSONObject json1 = JSONObject.fromObject(result);
socket1.sendMessage(json1.toString());
//对先掷骰的对象发送数据结束
result.setMessage("系统:游戏开始,请等待对手掷骰!");
result.setBout(false);
result.setColor("white");
this.sendMessage(JSONObject.fromObject(result).toString());
//对后出手的发送消息结束
}else{
index--;
result.setMessage("系统:等待玩家匹配!");
this.sendMessage(JSONObject.fromObject(result).toString());
}
}else{
result.setMessage("系统:等待玩家匹配!");
this.sendMessage(JSONObject.fromObject(result).toString());
}
this.mykey = index;
webSocketMap.put(mykey+"", this); //加入map中
System.out.println(webSocketMap.size());
} catch (Exception e) {
e.printStackTrace();
}
}
@OnMessage
public void onMessage(String message) {
System.out.println(message);
JSONObject json = JSONObject.fromObject(message);
Result result = (Result) JSONObject.toBean(json,Result.class);
try {
WebSocket socket = null;
if(mykey%2==0){
socket = webSocketMap.get((mykey-1)+"");
}else{
socket = webSocketMap.get((mykey+1)+"");
}
if(socket!=null){
if(result.getXy()!=null&&!"".equals(result.getXy())){//有坐标表示为落子,反之则为发送信息
this.sendMessage(message);
result.setBout(true);//对手的bout改为true,表示接下来可以掷骰
result.setMessage("系统:对方已掷骰,正在等待您掷骰!");
socket.sendMessage(JSONObject.fromObject(result).toString());
}else{
Result newResult = new Result();
newResult.setMessage("自己:"+result.getMessage());
this.sendMessage(JSONObject.fromObject(newResult).toString());
newResult.setMessage("对方:"+result.getMessage());
socket.sendMessage(JSONObject.fromObject(newResult).toString());
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
这一部分代码就是websocket其中的一段逻辑,其中就是判断房间内是否有人,对手是否匹配完成,对战是否开始。以及对于对方落子与棋盘的信息的转发。使用websocket主要是为了减少网络资源的损耗,而不必像http那样用轮询的方式来进行信息的接收与发送。
AI
int nextStep(int ownBoard[],int otherBoard[],int figure){
float Block[3]={3,3,3};
for(int i=0;i<9;i++){//获取格子填充情况
if(ownBoard[i]!=0){
if(i>=0&&i<=2) Block[0]--;
else if(i>=3&&i<=5) Block[1]--;
else if(i>=6&&i<=8) Block[2]--;
}
}//
int ownScore=Getscore(ownBoard);
int otherScore=Getscore(otherBoard);
//for(int i=0;i<9;i++) cout<<ownBoard[i]<<" ";
float D_value[9]={0};//计算操作差值
for(int i=0;i<9;i++){
if(ownBoard[i]==0) {
int own1[9];
for(int ii=0;ii<9;ii++) {
own1[ii]=ownBoard[ii];
//cout<<own1[ii]<<" ";
}
//cout<<endl;
//复制一个ownboard
int other1[9];
for(int ii=0;ii<9;ii++) {
other1[ii]=otherBoard[ii];//复制一个otherboard
//cout<<other1[ii]<<" ";
}
//cout<<endl;
//对数组进行操作
own1[i]=figure;
if(i>=0&&i<=2){//同行消灭处理
for(int j=0;j<=2;j++){
if(other1[j]==figure) other1[j]=0;
}
}
else if(i>=3&&i<=5){
for(int j=3;j<=5;j++){
if(other1[j]==figure) other1[j]=0;
}
}
else if(i>=6&&i<=8){
for(int j=6;j<=8;j++){
if(other1[j]==figure) other1[j]=0;
}
}
int D_ownscore=Getscore(own1)-Getscore(ownBoard);
int D_otherscore=Getscore(otherBoard)-Getscore(other1);
int Differ=D_otherscore+D_ownscore;//获取操作后的差值
D_value[i]=Differ;
}
}//至此D_value的值已经初步形成
for(int i=0;i<3;i++){//根据已知格子权重赋值
if(Block[i]!=0)
switch (i)
{
case 0:
for(int j=0;j<3;j++){
if(D_value[j]!=0) {D_value[j]+=Block[0]*2.5;
if(Block[0]==3) D_value[j];
if(Block[0]==2) D_value[j]-=0;
if(Block[0]==1) D_value[j]-=3;
if(D_value[j]<0) D_value[j]=0.1;
}
}
break;
case 1:
for(int j=3;j<6;j++){
if(D_value[j]!=0) {D_value[j]+=Block[1]*2.5;
if(Block[1]==3) D_value[j];
if(Block[1]==2) D_value[j]-=0;
if(Block[1]==1) D_value[j]-=3;
if(D_value[j]<0) D_value[j]=0.1;
}
}
break;
case 2:
for(int j=6;j<9;j++){
if(D_value[j]!=0) {D_value[j]+=Block[2]*2.5;
if(Block[2]==3) D_value[j];
if(Block[2]==2) D_value[j]-=0;
if(Block[2]==1) D_value[j]-=3;
if(D_value[j]<0) D_value[j]=0.1;
}
}
break;
default:
break;
}
}
float max = 0;
float Di_value[9]={0.0};
int flag=0;
for(int i=0;i<9;i++) {
//cout<<D_value[i]<<" ";
if(D_value[i]>max){
max=D_value[i];
flag=i;
}
}
//cout<<endl;
return flag;
}
本函数中,先用Block数组记录已填格子情况,然后复制用于操作的数组,一个一个计算预计收益存入D_value数组中,再根据已填格子情况对预计收益进行调整,最后寻找预计收益最大的位置输出。
3.5 性能分析与改进(2分)
分析:存在一些代码冗杂,一些页面渲染的体验效果不是特别好,个别函数运行效率较为不理想。
改进:将复用多次的代码进行封装,将函数逻辑精简并减少循环。
3.6 单元测试(2分)
前端
前端采用console.log进行测试
后端
后端采用postman进行接口测试
3.7 贴出GitHub的代码签入记录,合理记录commit信息(2分)
四、总结反思(11分)
4.1 本次任务的PSP表格(2分)
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 30 | 60 |
Estimate | 估计这个任务需要多少时间 | 5 | 5 |
Development | 开发 | 900 | 1560 |
Analysis | 需求分析 (包括学习新技术) | 600 | 680 |
Design Spec | 生成设计文档 | 60 | 120 |
Design Review | 设计复审 | 30 | 30 |
Coding Standard | 代码规范 (为目前的开发制定合适的规范) | 15 | 10 |
Design | 具体设计 | 180 | 180 |
Coding | 具体编码 | 30 | 30 |
Code Review | 代码复审 | 180 | 180 |
Test | 测试(自我测试,修改代码,提交修改) | 300 | 360 |
Reporting | 报告 | 120 | 120 |
Test Report | 测试报告 | 60 | 45 |
Size Measurement | 计算工作量 | 30 | 20 |
Postmortem & Process Improvement Plan | 事后总结, 并提出过程改进计划 | 30 | 30 |
合计 | 2570 | 3430 |
4.2 学习进度条(每周追加)
陈艺栋
第N周 | 新增代码(行) | 累计代码(行) | 本周学习耗时(小时) | 累计学习耗时(小时) | 重要成长 |
---|---|---|---|---|---|
1 | 0 | 0 | 30 | 30 | 学习了一下AI理论,以及一点后端知识 |
2 | 3215 | 3215 | 31 | 61 | 学懂了websocket,可惜的是学废了AI,理论与实践有差距, 但是完成了大部分的前端逻辑以及渲染 |
3 | 4622 | 7837 | 30 | 91 | 学了一下unicloud,体验了一下云开发,用云开发解决了除了websocket外的所有后端接口,websocket用java的框架实现 |
陈曦
第N周 | 新增代码(行) | 累计代码(行) | 本周学习耗时(小时) | 累计学习耗时(小时) | 重要成长 |
---|---|---|---|---|---|
1 | 0 | 0 | 8 | 8 | 学会了原型软件pixso的用法,开始用Procreate、pixso、PS做UI设计 |
2 | 0 | 0 | 9 | 17 | 学习websocket,但没有投入实际使用,完成了原型设计 |
3 | 482 | 482 | 8 | 25 | 完成了三种难度模式的AI,完成了测试程序 |
4.3 最初想象中的产品形态、原型设计作品、软件开发成果三者的差距如何?
我们最初想象中的产品是界面简洁美观,带有一点中国古风的、满足账号登录登出、能够实现本地对战、人机对战、在线对战三大功能,其中人机对战适配有不同的难度的、带有在线排行榜单、个人成绩记录等功能的、人机交互友好,用户操作舒适的微信小程序。
原型设计作品做得差强人意,UI设计方面,由于原型设计者有些艺术细胞,但软件技术有待提高,小程序界面较为美观,但不够简约,在图标和按钮设计和原型交互设计上,原型设计者也遇到了各种问题,不得不将要求一再放低,在原型上没能实现模拟对战,是本次原型设计的一大遗憾(原型设计者真的很努力在做了QAQ)
最终开发出的软件是较为接近最初设定的产品形态的,除在线排行榜单功能,最初计划的功能均已实现。主要差距在用户体验不够尽善尽美,实际对战过程中,存在偶发的卡顿现象,是本软件的瑕疵。
4.4 评价你的队友
陈艺栋:我的队友陈曦值得学习的地方在于他的灵活运用能力比较强,能够想到各种方法、活用会用的工具应对遇到的问题,在美工设计上能够设计出符合一般审美的图形、图像,安排的任务也会尽心尽力去做;需要改进的地方在于他的工作和学习效率不够高,而且时间管理能力有待提高,经常出现时间安排不过来的情况,常导致作息不规律,精力不集中,这样效率就更不高了。
陈曦:我的队友陈艺栋给人一种很踏实的感觉,他学习能力强、编程能力强,在本次作业中完成了最为复杂、工作量和难度都非常大的前后端,让我有一种抱大腿的感觉;他值得学习的地方是优秀的时间管理能力和强大的自律性,这两点都是我欠缺的。需要改进的地方是为我安排任务时表达得不是很详细具体,稍稍有些缺少与我的沟通。
4.5 结对编程作业心得体会(3分)
-陈艺栋
作业难度:单说作业难度的话,最为困难的话对我而言是实现websocket和AI,因为之前没怎么碰过后端以及AI的训练.其它的总体而言难度还好.
完成后的感受:爽,很爽,非常爽,卸下了一个包袱,就是有些地方自己不是很满意.
代码模块异常:恶心我最久的就是websocket的实现,因为没碰过,要从0到1有点难度,还有就是unicloud的实现与流程也常报错.
结对困难与解决方法:困难就在于AI与后端,我俩都不太擅长.所以我俩就学,直接莽上去,撸起袖子加油熬夜.
启发:与队员的合作要经常交流,同时时间安排一定要合理,避免项目的拖延以及实现效果的不足.还有就是要有去碰壁去解决难题的勇气.困难在开发过程中一定是一个主旋律.
-陈曦
作业难度:对我来说这个作业是比较难的,因为我没有什么开发经验,很多东西都不懂,幸好有一个很好的队友.
完成后的感受:终于结束了,感觉自己又活过来了
代码模块异常:在我们训练学术ai失败后,选择用算法来模拟ai,这一块我的出错比较多,逻辑不够清晰.
结对困难与解决困难:困难就在于AI与后端,撸起袖子加油熬夜,可惜最后ai的训练训出了一个鬼东西,不如自己重写一个算法.
启发:学习要趁早,不要书到用时方恨少,就是自己的能力不足,导致此次项目完成的比较痛苦.