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實現了邏輯嵌入前端中。
在前幾周存在着一定的負罪感(到最后也有),因為感覺都是隊友在寫前端游戲邏輯,自己好像什么都沒做,雖然也在學習,但是看不到成果就有些焦慮。沒想到最后還是選擇了擺爛,或者說是被迫擺爛,在這點上感覺還是很對不起隊友的。
最后還是要對隊友表示感謝,承擔了大部分代碼。