结对编程作业

https://github.com/pat-chou-li/pigtail

姓名 分工 博客链接
吴振溢 负责前端界面和游戏逻辑 https://www.cnblogs.com/pat-chou-li/p/15422158.html
周伟杰 负责AI算法和原型设计 https://www.cnblogs.com/lovtt/p/15426527.html

一、原型设计

[2.2.1]提供此次结对作业的设计说明 (11分)
https://modao.cc/app/d69f2bbb008057d1a3c9d2cd9edc417f607879e5
本次作业我选择采用墨刀进行原型设计。
之前没接触过原型设计,选择用墨刀并看网课一网上速成。由于没有ui设计的基础,再加上不熟练,所以比较丑。下面给出原型设计的展示:

  • 游戏主页
    由于前端队友说卡面太掉san了后面换成了百变小樱的卡背,以及原本打算做出卡牌翻面的效果,但碍于技术与时间原因没有做出来,这是比较遗憾的地方。
  • 登录页面

    添加了键盘事件
  • 游戏模式选择
  • 如果选择在线对战进入游戏大厅
  • 选择创建房间

    有人加入对局,等待3秒后开始游戏

    如果选择加入房间,则显示房间列表,也可以通过输入uid加入对局
  • 对局效果,有实现简单的交互

[2.2.2]遇到的困难及解决方法:(4分) **

  • 遇到的困难:
    主要还是原型设计工具使用不够熟练,又很多想实现的效果,但由于技术问题没有实现。
    由于之前没学过ui设计,缺乏经验,只能做做便宜ui,好看的素材也比较难找。
  • 解决方法
    墨刀使用不熟练就只能一边看相关文档一边操作,遇到不会实现的功能就利用网络搜索教程。
    素材问题最后也找到了几个能用的素材网站,但审美确实很难在短时间内练就。

二、原型设计实现

[2.3.1]代码实现思路: (11分)

  • 网络接口的使用:

    主要是获取上步操作的网络接口,在创建对局后、游戏进行中均频繁的用到了。

    在创建对局后,要通过每隔一段时间请求此接口,来得到是否有人加入这个对局,并进入游戏对局界面。

    在游戏中,也需要每隔一段时间请求此接口,为了得到对方的操作,并即时反映到UI界面上,问题是来到最后一张牌翻出后,此时游戏已经结束了,不能通过这个接口来获取上步操作,只能通过另一个获取对局信息的接口,从中拿到最后一步的数据,反映到UI界面上再结束游戏。

    为了保证模块的解耦和复用,无论我方操作还是对方操作,都通过获取上步操作拿到再进行渲染,而不是在本地处理我方操作。

  • 代码组织与内部实现设计(类图)

    小程序各页面的事件处理函数主要分为3个部分:

    • 生命周期函数:

      1.onLoad():在页面打开时挂载定时器,初始化牌堆,初始化AI。

      2.onUnload():卸载监听器,卸载AI。

    • 对局详情类函数:

      initPile():打乱牌堆

      flop():翻牌操作

      run():出牌操作

      clear():清空判定区,分发手牌

      over():结束游戏

      listen():监听上步操作(online限定)

      change():根据上步操作改变UI界面(online限定)

    • AI类函数:

      AIinterface():返回AI决策出的操作。

      changeRobot():打开或关闭托管开关。

      p1AI()、p2AI():挂载AI。

  • 说明算法的关键与关键实现部分流程图
    吴振溢:
    写页面没用到什么算法捏,总之就是

    监听上步操作->反映到UI界面

    用户做出操作
    两者并行

    周伟杰:

    分为牌比对面对和牌比对面少的情况
    如果牌比对面少,以翻牌为主,同时如果我吃牌后不会比对面多并且牌数小于16张的话(一个相对安全的数值),我会选择吃牌。
    如果牌比对面多,以出牌为主,并且如果对面吃下牌会导致我打到必胜的局面的话,我会选择出牌堆里概率最大的牌。

  • 贴出你认为重要的/有价值的代码片段,并解释(2分)
    吴振溢:
    比较有价值的应该是监听中,怎么检测到返回过来的是新的报文,来确定有人做出了操作。

//发起对上步接口的请求
success(res){
    if (res.data.code == 400){
        /*游戏结束判断,这里略过*/
    }
    //先存下返回的value
    newValue = res.data
    //先更新turn(是否是自己回合的标记),有的时候turn更新了并不一定是有了新操作,这时候不能执行change()函数改变UI界面。
    if (newValue.data.your_turn != that.data.turn) {
						that.setData({
							turn: newValue.data.your_turn,
						})
						that.data.lastTurn.data.your_turn =
							newValue.data.your_turn
					}
    //更新完turn后,再通过浅拷贝来对比新旧报文,如果有更新了,就执行change()函数更新游戏界面。
					if (
						JSON.stringify(that.data.lastTurn) !=
						JSON.stringify(newValue)
					) {
						that.setData({
							lastTurn: newValue,
						})
						that.change(newValue)
					}
	}

周伟杰:

比较有价值的代码是我计算出了当场上的局面到达某种程度时,只要一直翻牌就能获胜。
首先是对方手牌到达总牌数的3/4的时候,和我方手牌+3*牌堆+判定去牌数-2仍然小于敌方手牌的时候。
即this.data.enermyTotal > this.data.pileTotal + 25 或者this.data.enermyTotal > 39
代码块:

if (
			this.data.meTotal == 0 ||
			this.data.enermyTotal > this.data.pileTotal + 25 ||
			this.data.enermyTotal > 39
		) {
			res.currentTarget.dataset.type = 0
			console.log(375)
			return res //没牌和翻牌必胜的情况,直接翻牌
		}

后面的很多判断都是基于这点进行
比如当我判断对面吃下牌后会导致我的必胜局面,那我就想尽办法然后对面吃牌。对面只有选择翻牌才会吃牌,而如果手里有其他三种花色的牌对方其实是不会选择去赌翻牌的,所以我只有在对方其他三种花色的牌加起来都比我特定花色的牌数低时,才会出那张牌。

//当我方能逼迫对面翻牌且对方吃牌概率大于0.75时,出最有可能被翻出的牌
					if (meMsg[maxFlower] == 0) {
						var myMaxFlower
						var myMaxNumber = -1
						for (var key in meMsg) {
							if (key == TopFlower) {
								continue
							}
							if (myMaxNumber <= meMsg[key]) {
								//获取我方卡组中最多的花色
								myMaxFlower = key
								myMaxNumber = meMsg[key]
							}
						}
						if (myMaxNumber == -1 || myMaxNumber == 0) {
							//如果手里只有一种花色且恰好是牌顶的牌,只能翻牌
							res.currentTarget.dataset.type = 0
							console.log(422)
							return res
						}
						res.currentTarget.dataset.type = 1
						res.currentTarget.dataset.flower = myMaxFlower
						console.log(427)
						return res //打出这个花色
						//当对面即使吃下牌也无法赢时,出手里最多并且不是判定区顶的牌
					}
  • 性能分析与改进

    • 要论性能消耗最大的函数,那肯定是监听获取上步操作的函数,由于不停的发送请求,造成了严重的耗时。而ai的算法只用到普通的if-else判断,没有什么很复杂的逻辑,所以消耗不大。
  • 描述你改进的思路(2分)

    改进思路:

    1.后端架设服务器进行监听,有新的操作再返回给我,可惜我不会后端。

    2.减少请求次数,但是这样会导致页面反馈不及时,比如用户点击一秒后才出现页面反馈,这样对用户交互体验很不好,请求次数过多则会导致小程序卡顿甚至闪退。所以我在多次测试下找到了两者的平衡点,大约是500ms。

最后起码得到了Audit的认可(

  • 展示性能分析图和程序中消耗最大的函数

    丑丑的listen(),因为没用Promise,嵌套then很难看。

    listen() {
    		if (this.pileTotal <= 0) {
    			clearInterval(this.data.inid)
    			this.over()
    		} else {
    			let that = this
    			var newValue = {}
    			wx.request({
    				url:
    					'http://172.17.173.97:9000/api/game/' +
    					that.data.uuid +
    					'/last',
    				header: {
    					Authorization: that.data.token,
    				},
    				success(res) {
    					if (res.data.code == 400) {
    						clearInterval(that.data.inid)
    						if (that.data.pileTotal <= 1) {
    							wx.request({
    								url:
    									'http://172.17.173.97:9000/api/game/' +
    									that.data.uuid,
    								header: {
    									Authorization: that.data.token,
    								},
    								success(res) {
    									let value = {
    										msg: '操作成功',
    										data: {
    											your_turn: true,
    											last_code: res.data.data.last,
    										},
    									}
    									that.change(value)
    									that.over()
    									return
    								},
    							})
    						} else {
    							wx.showToast({
    								title: '对局已结束!',
    								icon: 'error',
    							})
    							setTimeout(() => {
    								wx.reLaunch({
    									url: '/pages/chooseMode/chooseMode',
    								})
    							}, 2000)
    							return
    						}
    					}
    					newValue = res.data
    					if (newValue.data.your_turn != that.data.turn) {
    						that.setData({
    							turn: newValue.data.your_turn,
    						})
    						that.data.lastTurn.data.your_turn =
    							newValue.data.your_turn
    					}
    					if (
    						JSON.stringify(that.data.lastTurn) !=
    						JSON.stringify(newValue)
    					) {
    						that.setData({
    							lastTurn: newValue,
    						})
    						that.change(newValue)
    					}
    				},
    				fail(res) {
    					wx.showToast({
    						title: '网络异常',
    					})
    				},
    			})
    		}
    	},
    
  • 展示出项目部分单元测试代码,并说明测试的函数,构造测试数据的思路 (2分)

    小程序上没有dom节点树,运行环境比较特殊,比较麻烦,我就把需要测试的函数单独移植出来在node环境下进行测试了。

    var assert = require('assert')
    
    var CheckArrayElement = function (array) {
    	array.sort() //数组排序
    	var reNum = 0 //返回结果
    	//遍历整个数组对象
    	for (var i = 0; i < array.length; i++) {
    		//跳过最后一个元素的比较
    		if (i + 1 == array.length) {
    			continue
    		}
    		//判断相邻的元素是否相同
    		if (array[i] == array[i + 1]) {
    			reNum += 1
    		}
    	}
    	return reNum
    }
    
    function initPile(){
    	/*初始化牌堆函数*/
    }
    
    let pile = initPile()
    let flag = !CheckArrayElement(pile)
    assert(flag, 'error:牌组中有重复元素')
    flag = pile.length
    assert(flag, 'error:牌组不足52张')
    

    简单测试一下打乱牌组函数,如果有重复元素或者不足52张,则说明函数有问题。

[2.3.2]贴出Github的代码签入记录,合理记录commit信息。(1分)

[2.3.3]遇到的代码模块异常或结对困难及解决方法。(4分)

  • 困难描述(1分)
    吴振溢:
    游戏逻辑是这样进行的:

    不停的请求上一步操作,直到与data中存储的操作不一样,就更新data,并进行一次视图更新。

    与此同时,用户如果进行了翻牌、出牌,就向网络接口请求一次操作,但并不进行视图更新,而是让监听函数拿到新的操作后,再一起更新。

    产生问题的地方是当把最后一张牌打出后,对局成为结束状态,获取上步操作的接口就不再返回操作了,这套逻辑不再适用。
    周伟杰:
    1.算法是真的难想。
    一开始想写博弈树,但找不到一个合适的打分函数。因为我至始至终都没看透这个游戏的窍门,不知道什么情况对自己是有利的(真的不是纯运气游戏吗)。
    手牌的优势数量无法确定,可能也就最后在越到最后博弈越有价值。
    2.由于是前后端分离,一开始打算搭个服务器提供ai的接口,但我并没有部署服务器的经验。并且我的算法也并不复杂,专门搭个服务器好像并不值得。于是想直接写进前端里,但是我也没学过js。

  • 解决过程(2分)
    吴振溢:
    尽管获取上步操作的接口不再返回操作,但是listen()监听器此时并没有卸载,并且接口仍然会返回400以表示对局结束,那么就在监听器得到400的返回值时,将监听器卸载,并访问获取对局信息的接口,来得到上局游戏的最后一步操作,并且执行change()函数更新页面,再执行over()进行游戏结束的判断,显示胜利信息或者失败信息两秒后退出页面,并销毁所有监听器。
    周伟杰:
    1.最后写了个相对摆烂的算法,只通过if-else判断出当前可能比较好的选择(也不能说多好,毕竟我自己都不太清楚怎样才算好。
    2.再三权衡后直接速成js写算法,然后和队友de了一晚上bug。

  • 有何收获(1分)

    算是对返回值的使用有了一个新的理解吧,不仅是res.data,状态量也有所意义,并且能够利用他很好地完成逻辑。

[2.3.4]评价你的队友。(2分)
吴振溢:

  • 值得学习的地方

    虽然完全不会设计,但是讨论任务分配的时候还是非常乐意的去承担了这份任务,(虽然设计稿最终丑丑的),但是这种愿意学习的精神还是非常值得认可的。我自己就有些躲在舒适圈的感觉,用惯了vue,写惯了web页面,一开始发现可能要写小程序就十分抗拒。

  • 需要改进的地方

    AI算法摆烂到最后一周配合着坦克大战和团队项目任务肝爆,ddl战士了属于是。
    周伟杰:

  • 队友值得学习的地方
    由于这个作业网络接口都已经给好了,游戏逻辑又和前端绑定,本人又没有接触过前端的内容,所以基本上大部分的工作都由队友国庆爆肝完成,只能说感恩了。
    并且选择了自己并不熟悉的微信小程序作为平台开发,这种敢于挑战自己的精神值得学习。

  • 队友需要改进的地方
    最后实现界面有点丑,并且卡牌拖动之类的动画效果也摆烂没做。

[2.3.5]提供此次结对作业的PSP和学习进度条(每周追加),示例如下(2分)

PSP2.1 Personal Software Process Stages 预估耗时(分钟) 实际耗时(分钟)
Planning 计划
· Estimate · 估计这个任务需要多少时间 10 10
Development 开发
· Analysis · 需求分析 (包括学习新技术) 600 500
· Design Spec · 生成设计文档 120 80
· Design Review · 设计复审 20 20
· Coding Standard · 代码规范 (为目前的开发制定合适的规范) 30 20
· Design · 具体设计 300 250
· Coding · 具体编码 1800 2200
· Code Review · 代码复审 260 300
· Test · 测试(自我测试,修改代码,提交修改) 300 350
Reporting 报告
· Test Repor · 测试报告 60 60
· Size Measurement · 计算工作量 20 20
· Postmortem & Process Improvement Plan · 事后总结, 并提出过程改进计划 60 60
· 合计 3580 3870

吴振溢

N周 新增代码(行) 累计代码(行) 本周学习耗时(小时) 累计学习耗时(小时) 重要成长
1 50 50 1 1 看了看小程序开发文档,进行了pigtail = new miniProgram()
2 2000 2050 30 30 国庆3天放假,开冲!完成除AI和动画外所有基本功能。
3 150 2200 3 3 修修小bug。
4 300 2500 5 5 模拟AI接口,加个托管按钮。
5 200 2700 5 5 修bug,优化交互。

周伟杰:

N周 新增代码(行) 累计代码(行) 本周学习耗时(小时) 累计学习耗时(小时) 重要成长
1 0 0 6 6 构思算法
2 0 0 10 16 学习原型设计工具的使用并完成原型设计
3 100 100 10 26 尝试用博弈树但是失败了捏
4 0 100 6 32 虽然每天都在思考,但确实没什么好想法
5 428 528 6 38 ddl爆肝写了能润的算法,然后de了一晚上bug

三、心得

吴振溢:
说实话学前端以来第一次做出这么丑的玩意,也是第一次在项目里写这么多奇怪的js,之前都是在写业务逻辑,游戏逻辑完全没有接触过,比想象中的复杂得多,接下来还要做另一门课的坦克大战,实时刷新的前提下想必会比现在还麻烦。

本来我是很喜欢做动画、写交互的,但是遇到了许多问题。

首先是监听请求已经浪费了过多的运存,导致游戏有些卡卡的了,在此基础上加上动画可能会导致运存过大强制结束等等。

然后是由于本人实在不熟悉微信小程序的开发环境,没有dom,更没有jQuery,只能调用微信小程序的方法animation或者用CSS@keyframe动画,经过3天国庆30小时爆肝,现在已经看见这个界面就有些难受,再加上其他课程的压力,完全不想去学习新的动画方法然后继续写动画了——

然后就是后悔,十分的后悔,为什么不用组件库。

祈祷不要出什么奇怪的bug吧。(本人加入自己的对局有已知的bug和原因,但是我觉得这不符合现实逻辑所以没去管,想自己和自己玩可以玩本地对战捏)
周伟杰:
由于游戏逻辑和前端绑定所以我没什么事干所以就去负责算法和原型设计了。
原型设计去看网课花了6小时速成,并加一晚上设计出了原型设计稿。由于之前没有做过设计感觉还蛮新鲜的,虽然由于经验不足做出来有点小丑,但确实是一次不错的经历。
但这算法是人想的吗?
结队开始以来每天有空就在想算法的事,但就是想不到。而且这游戏也太烂了,玩的时候没有任何有趣的要素,也很枯燥,而且会出现永远无法结束对局的情况。和同学打了几盘也没悟出其中的奥秘。一开始想写博弈树,但不知道这个游戏什么局面是有利的,牌多牌少的各种分支非常多,写博弈树也就无从谈起。在网络上搜索也没有类似的样例供参考,感觉沾点人工智能了,总之不是我这个水平能想出的东西。最后迫于时间和精力,写了一个相对还行的算法,至少不会做出那种明显会导致游戏输掉的铸币操作。大概我本人什么水平我的ai就什么水平
比较遗憾的地方是原本想部署服务器搭个算法的服务器的,但是后来感觉我的算法似乎不太配,而且课程也很多,实在没有精力去实现了,希望做团队项目的时候有机会吧。
最后感谢一下我的队友,做了大部分的代码工作。

posted @ 2021-10-18 20:25  szly  阅读(185)  评论(0编辑  收藏  举报