https://github.com/zlplease/pigtails
姓名 | 分工 | 博客连接 |
---|---|---|
陈宇扬 | 原型设计,AI算法 | https://www.cnblogs.com/Enuang/p/15442938.html |
张乐芃 | 前端界面,游戏逻辑 | https://www.cnblogs.com/zlplease/p/15441529.html |
一、原型设计
1.1 设计说明
pigtails参考经典卡牌游戏猪尾巴,利用原型设计工具墨刀,设计了登录、首页、对战以及广场四大模块,具体模块信息如下:
-
登录模块:用户通过输入学号和密码进行登录操作,并跳转至首页。针对于网络,学号,密码错误,设计了相应的提示弹窗。
-
首页模块:首页模块显示玩家基本信息,并提供本地对战、在线对战以及人机对战三大模式。针对于他人公开的比赛,设计了广场模块。
-
对战模块:对战模块分为本地对战、在线对战以及人机对战。
1.本地对战:
本地对战游戏双方在同一台设备上操作,根据头像外边框确定回合,并在对方回合时禁用点击事件。由于pigtails的卡牌堆为堆叠状,玩家只能选择并打出卡牌堆的顶部卡牌,为模拟玩家随意选择卡牌,设计了换牌按钮,换牌按钮点击会随机排列当前卡牌,贴心地为喜欢“玄学”的玩家提供帮助。
2.在线对战:
在线对战玩家通过不同设备进行对战。如下为在线对战创建对局的等候室页面,玩家可以选择公开方式或私人方式创建对局,并获取对局id,点击开始游戏后进入在线对战游戏页面。
相对于本地对战,为方便玩家更好知道对方的手牌信息,玩家二的卡牌组相关信息不再颠倒。针对于房间人数,设置了uuid对局号复制组件供玩家分享对局。
当房间人数达到上限时,uuid对局号复制组件会自动消亡。
此外还设置了托管按钮,玩家点击托管按钮即可触发代理事件,我方会自动从卡牌堆翻开一张牌。
3.人机对战:
玩家二设置为pigtails设计的AI,在我方做出相应操作后,AI会计算当前操作并打出相应卡牌。
4.相应弹窗提示:
包含玩家输赢提示(玩家一胜利、玩家二胜利以及评分秋色)、房间人数不够提示(人还没齐,快叫上你的小伙伴)、玩家先手提示(比赛刚开始,X方先手)、牌堆更新提示(已更新牌堆)、代理提示(已开启代理)
5.等候室:
用户选择在线对战进入,选择创建私人房间或者公开房间,用户可选择创建新对局并加入;用户还可输入他人分享的对句号加入他人创建的对局。
-
广场模块:广场模块显示所有用户创建的公开的对局,对局类型分为已开始和未开始。已开始状态仅显示双方对局ID,而未开始状态,玩家可根据房间已有玩家选择进入相应对局。广场模块按4个对局一页分布。
1.2 困难及解决方法
困难:接口返回的参数有限,对于不同模式的相同属性难以综合统筹。其次,考虑到玩家使用的便捷性,选择了微信小程序,但微信小程序不支持横屏布置,在设计竖屏布局时有些痛苦,既要考虑到美观又得考虑信息的全面性,难以二者兼顾。
解决过程:对于设计上遇到的困难,通过浏览zcool以及dribbble等设计网站,参考了以上网站竖屏游戏的设计模式与设计风格。
收获:素材+++,收获巨多素材。在浏览上述网站时被各大设计师的产品设计吸引,审美能力有所提升。大概熟悉了产品的设计模式,以及众多设计师的设计规范与设计步骤。
二、原型设计实现
先贴出小程序体验码,暂时存在较多bug(修不完了,以后再也不写这么多动态类和布尔值了)。注:由于小程序严格要求https,在使用体验版时,需打开调试模式。希望大家多提意见!
2.1 代码实现思路
2.1.1 网络接口的使用
利用微信小程序自带api-->wx.request接入接口,token通过wx.setStorage存入本地缓存,全局数据(例如用户名,请求url)通过app.globalData接收并在onload生命周期上赋值。针对于除了登录外的其他接口,均需要鉴权,则在请求头header中添加token。
header: {
'content-type': 'application/json',
'Authorization': wx.getStorageSync('token')
},
接口方面根据接口返回的状态码对程序状态做不同处理,以登录接口为例。请求成功且状态码为200时,完成数据存储和页面跳转;当请求成功且状态码为其他时,弹出提示弹出,并显示提示信息(缺少学会,密码错误等参数错误);当请求失败返回网络问题提示。以此类推其他接口。
login: function() {
var data = {
'student_id': this.data.stuId,
'password': this.data.password,
}
wx.request({
url: 'http://172.17.173.97:8080/api/user/login',
data: data,
header: {'content-type':'application/x-www-form-urlencoded'},
method: 'POST',
success: (res)=>{
console.log(res)
console.log(res.data.status)
if (res.data.status == 200) {
console.log(res.data.data.detail.name)
app.globalData.name = res.data.data.detail.name
//将token存入本地缓存
wx.setStorage({
key: 'token',
data: res.data.data.token,
});
wx.navigateTo({
url: '/pages/home/index',
});
} else {
wx.showToast({
title: res.data.data.error_msg,
icon: 'none'
})
}
},
fail: e => {
wx.showToast({
title: '网络出错啦',
icon: 'none'
})
}
});
},
2.1.2 代码组织与内部实现设计
pigtails基本页面处理函数如下:
生命周期函数:
- 监听页面加载onLoad:在页面加载时,接收其他页面传送
onLoad: function (options) {
console.log(options.uuid)
this.setData({
uuid: options.uuid
})
var listen = setInterval(this.listen, 2000);
var over = setInterval(this.over, 5000)
this.setData({
listen: listen,
over: over
})
},
- 监听页面初次渲染完成OnReady:初始化游戏
onReady: function () {
this.initGame()
},
- 监听页面卸载onUnload: 在用户离开或游戏结束时清除定时器
onUnload: function () {
clearInterval(this.data.listen);
clearInterval(this.data.over)
},
对局函数:
- selectCard:卡组牌选择
- selectHandCards:手牌类型选择
- confirm:确定出牌
- cancel:取消出牌
- over:监听对局结束以及提示
- listen:设置定时器访问服务器并获取上步操作,并根据操作完成本地牌堆的嵌入与设置。
- trusteeship:开启托管(内有大量bug,望各位手下留情)
- getOperation:获取AI返回值,以便下一步操作
- copy:复制uuid号
- initGame:初始化牌堆
由于使用小程序,没有用到面向对象的思想,自然也没有类图了。
2.1.3算法的关键与关键实现部分流程图
前端页面流程:
AI方面,前面几周一直在纠结于利用搜索树来获取最佳的下一步操作,但无奈水平不足,没能实现,有待后续更深入地学习,重写优化AI算法。于是采用最简单的笨方法,把我人脑能想到的情况罗列,并采用我认为的最佳策略。
主要策略分为:
1.AI手牌比对面手牌多时,可以选择出牌将判定区的牌收回(前提是自己的手牌+判定区手牌在一个安全的数值内),方便中后期控牌,逼迫对方翻出概率最高的的牌。如果收回判定区手牌后,会让自己的牌数过多,AI就选择翻牌。
2.AI手牌比对方手牌多时,最大限度打出让对手翻牌概率最大的牌,即打出牌堆中数量最多的花色,让对手收走判定区内的牌。
3.在和其他小伙伴玩猪尾巴的过程中,找到了一种必胜局面:我方手牌+3*牌堆+判定区牌数-2<敌方手牌,所以以上策略要尽量让对手在后期多收牌。
2.1.4 代码片段
本地对战的data字段,为每局对战初始化game对象,根据其他数据对于卡牌状态、回合等进行处理并由wx-if或动态class在页面进行状态显示。
data: {
//0代表自己,1代表对手
turn: 0,
//是否有选中的牌
selectedCard: false,
//是否有选择的手牌
selectedHandCard: false,
//选择的花色
flower: '',
//游戏是否结束
gameOver: true,
//放置区是否为空
placeEmpty: true,
game: {
player1: {
spade: [],
heart: [],
club: [],
diamond: [],
totalCount: 0
},
player2: {
spade: [],
heart: [],
club: [],
diamond: [],
totalCount: 0
},
deck: [],
placement: {
nowCards: [],
topCard: ''
}
},
}
css部分,采用nth-child(x)子元素选择器,z-index设置层级关系,transform改变元素偏转角度实现卡牌堆叠效果
.deckCards, .placeCards {
position: absolute;
width: 100%;
height: 100%;
border: 1rpx solid #ededed;
z-index: 999;
}
.deckCards:nth-child(1) {
z-index: 4;
}
.deckCards:nth-child(2) {
z-index: 3;
transform: translateX(1%) translateY(1%);
}
.deckCards:nth-child(3) {
z-index: 2;
transform: translateX(2%) translateY(3%);
}
.deckCards:nth-child(4) {
z-index: 1;
transform: translateX(3%) translateY(5%);
}
动态class类设定,通过mustache语法判断并赋予动态类名,下例为不在自己回合将自己的点击事件全部禁用
<view class="player player1 {{turn == 1 ? 'disable' : ''}}"></view>
.disable {
pointer-events: none;
}
2.1.5性能分析与改进
先贴出小程序性能分析图


从图可知,由于小程序本地代码限制,难以在本地存储较高质量图片,故选择使用外引图片,此外,校园网的访问速度也导致耗时加长。针对于这一点只能通过分包,故未进行改进。
由于listen函数未进行解耦,且在页面加载时设置两秒的间隔监听,造成资源消耗较大(调试起来痛苦不堪,时间太长请求耗时过长,用户体验不佳;时间太短,服务器以及本地请求频繁,资源浪费较严重)
改进思路:将服务器挂载在外,不要用校园网。采用分包进行小程序资源分配,增加本地资源量
现给出巨大耗时的函数(求放过):
listen: function () {
var url = app.globalData.baseUrl + '/game/' + this.data.uuid + '/last'
// console.log(url)
wx.request({
url: url,
header: {
'content-type': 'application/json',
'Authorization': wx.getStorageSync('token')
},
method: 'GET',
success: (res) => {
console.log(res.data)
if (res.data.code == 403) {
wx.showToast({
title: '人还没齐,快叫上你的小伙伴',
icon: 'none',
duration: 1500,
});
}
else if (res.data.code == 200) {
console.log(res.data.data.last_msg)
var msg = res.data.data.last_msg
var turn1 = res.data.data.your_turn
if (msg == '对局刚开始') {
var text = ''
var turn = -1
if (turn1) {
text = '对局刚开始,我方先手'
turn = 0
}
else {
text = '对局刚开始,对方先手'
turn = 1
}
wx.showToast({
title: text,
icon: 'none',
duration: 1500,
});
this.setData({
turn: turn,
isBegin: 1
})
}
else {
var info = res.data.data.last_code
var player = info[0]
var type = info[2]
var flower = info[4]
var card = info.substring(4, 6)
var game = this.data.game
console.log(player)
console.log(typeof (player))
console.log(typeof (this.data.recordPlayer))
console.log(this.data.recordPlayer)
console.log((player == this.data.recordPlayer))
if (player != this.data.recordPlayer) {
if (type == 0) {
game.deck.pop()
}
else {
console.log(player)
if (!turn1) {
switch (flower) {
case 'S': game.player1.spade.pop()
break;
case 'H': game.player1.heart.pop()
break;
case 'C': game.player1.club.pop()
break;
case 'D': game.player1.diamond.pop()
break;
}
game.player1.totalCount -= 1
}
else {
switch (flower) {
case 'S': game.player2.spade.pop()
break;
case 'H': game.player2.heart.pop()
break;
case 'C': game.player2.club.pop()
break;
case 'D': game.player2.diamond.pop()
break;
}
game.player2.totalCount -= 1
}
}
console.log(game.placement)
console.log(card)
//判断是否要收牌,不要则放置右侧牌顶
if (game.placement.topCard[0] != card[0]) {
game.placement.nowCards.push(card)
game.placement.topCard = card
}
else {
console.log('收牌')
game.placement.nowCards.push(card)
var chargeCards = game.placement.nowCards
var len = game.placement.nowCards.length
chargeCards.sort()
console.log(chargeCards)
for (var i = 0; i < len; i++) {
var flower1 = chargeCards[i][0]
var card1 = chargeCards[i]
if (!turn1) {
switch (flower1) {
case 'S': game.player1.spade.push(card1)
break;
case 'H': game.player1.heart.push(card1)
break;
case 'C': game.player1.club.push(card1)
break;
case 'D': game.player1.diamond.push(card1)
break;
}
game.player1.totalCount += 1
}
else {
switch (flower1) {
case 'S': game.player2.spade.push(card1)
break;
case 'H': game.player2.heart.push(card1)
break;
case 'C': game.player2.club.push(card1)
break;
case 'D': game.player2.diamond.push(card1)
break;
}
game.player2.totalCount += 1
}
}
game.placement.nowCards = []
game.placement.topCard = ''
}
this.setData({
game: game,
flower: '',
turn: (this.data.turn + 1) % 2,
isBegin: 1,
recordPlayer: player
})
}
}
// console.log(this.data.game)
}
else if (res.data.code == 400) {
wx.showToast({
title: res.data.data.error_msg,
icon: 'none',
duration: 1500,
});
}
},
});
},
2.2 Github的代码签入记录

2.3 遇到的代码模块异常或结对困难及解决方案
张乐芃:
困难:起初的困难在于对于游戏规则的理解,编写代码时设置的参数较多,在理清楚状态变化时问题较大,结对时沟通较少,没能互相push。
解决过程:游戏规则的理解先是百度无果,后在B站上发现这原来是switch上的游戏,通过观看视频以及跟队友真实卡牌模拟操作,分析探讨可能存在的规律。代码模块异常时采用debugger和打印输出的方式,通过小程序调试器自带的network、appdata等模块,一步一步观察比对数据变化解决。
收获:收获了一切看淡的心,写代码不痛快,改代码最痛苦。合作时需要多沟通多push。
陈宇扬:
困难:困难在于前期了解规则后,想AI算法想了好久,从最开始的简单搜索树,到博弈树,再到蒙特卡洛树、卷积神经网络,学习上花费了不少时间,但是结合到具体,无奈本人水平不足,没能实现;到最后实在ddl要到了,人都要emo了,只能摆烂了,其他又不会,就只能用if else混混日子这样的;
解决过程:和其他组的伙伴一起打牌,讨论,不断从网上学习算法。
收获:收获了一堆堆算法文档的收藏,可以在短短时间内没能实现,但是对这些算法有了一定的了解;此外,结对还是需要多沟通,相互push。
2.4 评价你的队友
张乐芃:
- 队友值得学习的地方:
深夜战神,每次都是凌晨两点的问候;算法大佬,算法薄弱er真是十分的羡慕;做事效率巨高。好耶!抱到大腿了。 - 队友需要改进的地方:
PUSH!PUSH!PUSH!
陈宇扬:
- 队友值得学习的地方:
用一句过世主播的话来说就是:“卧槽!芃!我滴超人!”。本人没有怎么接触过前端技术,因此小程序上代码逻辑的实现都是队友完成的,实在是太感谢队友的付出了。 - 队友需要改进的地方:
最后结对编程和队友的比赛撞上了,时间管理上还存在不足,其他是真的完美。
2.5 PSP和学习进度条
PSP表格
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | ||
· Estimate | · 估计这个任务需要多少时间 | 30 | 25 |
Development | 开发 | ||
· Analysis | · 需求分析 (包括学习新技术) | 720 | 960 |
· Design Spec | · 生成设计文档 | 120 | 150 |
· Design Review | · 设计复审 | 30 | 30 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 20 | 12 |
· Design | · 具体设计 | 300 | 360 |
· Coding | · 具体编码 | 1200 | 1800 |
· Code Review | · 代码复审 | 100 | 120 |
· Test | · 测试(自我测试,修改代码,提交修改) | 60 | 60 |
Reporting | 报告 | ||
· Test Repor | · 测试报告 | 120 | 100 |
· Size Measurement | · 计算工作量 | 30 | 24 |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 30 | 45 |
合计 | 2760 | 3686 |
学习进度条:
张乐芃:
第N周 | 新增代码(行) | 累计代码(行) | 本周学习耗时(小时) | 累计学习耗时(小时) | 重要成长 |
---|---|---|---|---|---|
1 | 0 | 0 | 8 | 8 | 梳理游戏逻辑,分工分配任务 |
2 | 1800 | 1800 | 23 | 31 | 完成首页、登录、广场的样式与逻辑,本地对战的部分 |
3 | 800 | 2600 | 15 | 46 | 完成pvp模式的代码编写 |
4 | 600 | 3200 | 10 | 56 | AI功能嵌入,细节调节 |
陈宇扬:
第N周 | 新增代码(行) | 累计代码(行) | 本周学习耗时(小时) | 累计学习耗时(小时) | 重要成长 |
---|---|---|---|---|---|
1 | 0 | 0 | 8 | 8 | 梳理游戏逻辑,分工分配任务 |
2 | 150 | 150 | 19 | 27 | 熟悉墨刀使用,查找素材,完成原型设计。学习使用博弈树,写了模板后发现写不出评价函数。 |
3 | 230 | 380 | 20 | 47 | 学习使用蒙特卡洛树和简单卷积神经网络。 |
4 | 420 | 800 | 16 | 63 | 最后一周了,没时间只能写个摆烂算法了,后续学习后再重写吧。 |
三、心得
张乐芃:
星期六一杯茶,一支烟(吸烟有害健康,不要吸烟),一个bug改一天。之前有前端基础,也做过微信小程序,代码部分问题不大。在数次打扰评测组后端负责人后慢慢清晰逻辑,但不得不吐槽一句,把后端要求校园网的需求真是“丧心病狂”,校园网实在是太卡了。接接口时,不是在卡,就是在卡的路上。看着体验版转圈圈(加载)的速度,看着校园网下打不开的百度,属实是泪了。
代码部分的话,这次尝试了较多的动态类已实现不同情况下的样式显示,也算一次小小的尝试。由于没有对部分函数进行封装,造成出现较多冗余代码,只希望bug少点。这次作业的话学会了用调试模式去查看各种日志,第一次写小游戏还是挺有意思的。作业前端部分不是很难,改bug很痛苦,几百行代码里理逻辑。本次作业没有采用切图的方式,所有的样式都是代码实现的,也算复习了一遍css。遗憾的是,没有加入音效相关和更有趣的动画,最近太忙了,希望以后能有机会。
陈宇扬:
这次结对编程,只能说万幸找了个极其靠谱的队友,在心得这再次感谢队友对本次结对编程做出的贡献。因为游戏逻辑和前端是捆绑的,我实在帮不上什么忙(好像以队友的水平也不用我帮忙)。所以我就负责了前期的原型设计和AI算法,前期原型设计上也要谢谢队友给的相关设计网站,最后花了大概5个小时设计了原型。
对于AI算法,我的评价是——寄!
从结对一开始,有空闲时间总会和舍友打几把,试图找找算法思路。但很可惜的是,玩了这么多把,感觉到的内涵只有两个字:随机!在算法方面首先是想到了简单的搜索,先罗列出所有情况,在对结果胜利的情况里找到获胜概率最高的下一步操作,执行,但是这样做每次执行时都要进行一次搜索,运行起来会占用太多的资源,所以也就抛弃了。第二周在人工智能课上刚好就讲到了博弈树于是就想试试能不能实现,先是实现了一个模板,然后想了很久没有想到评价函数到底应该怎么写,于是乎,这个方案也被抛弃了。第三周在找可参考可copy的代码时,发现了蒙特卡洛树实现斗地主AI的一篇文章,信心满满去写,然后之后在某一天偶然之间发现了一篇博文中说到蒙特卡洛树需要后续结构都已知,但是我们的猪尾巴在摸牌方面存在随机性(还有就是蒙特卡洛树确实不好实现),因此这个方案也被抛弃了。还有一点遗憾是本来想搭建服务器实现算法逻辑的,可惜到最后也放弃了,只用JavaScript实现了逻辑嵌入前端中。
在前几周存在着一定的负罪感(到最后也有),因为感觉都是队友在写前端游戏逻辑,自己好像什么都没做,虽然也在学习,但是看不到成果就有些焦虑。没想到最后还是选择了摆烂,或者说是被迫摆烂,在这点上感觉还是很对不起队友的。
最后还是要对队友表示感谢,承担了大部分代码。