最近學習使用了一款HTML5游戲引擎(青瓷引擎),並用它嘗試做了一個斗地主的游戲,簡單實現了單機對戰和網絡對戰,代碼可已放到github上,在此談談自己如何通過引擎來開發這款游戲的。
(點擊圖片進入游戲體驗)
前文鏈接:
javascript開發HTML5游戲--斗地主(單機模式part1)
本文章為第二部分內容,主要包括發牌、搶地主流程。主要內容如下:
- 發牌
- 搶地主流程
- 確定地主
- 手牌布局問題
一、發牌
發牌,就是取3張底牌,然后3個玩家各發17張牌,之前我把一副牌的信息都放置在了Scripts/logic/clone/Card.js中,也就是qc.landlord.Card類。存放在這個類的屬性data中,這是一個數組,數組中的對象有3個屬性,如下:
- icon : 牌圖片文件名,用於顯示這張牌;
- val : 牌值,也就是大小,由於在斗地主規則中除大小王外,2最大,A其次,所以這里並不完全按照數值排大小,A是14,2是15,小王16,大王17。其它牌自己的數值就是大小;
- type : 花色,雖然說在斗地主中沒有花色大小問題,但是為了手牌美觀,一般都會把同等大小的牌按照一定的順序(黑桃-紅心-草花-方塊)來排列。
javascript的數組有許多強大的方法,在發牌這塊上算是很多都派上用場了,整個發牌流程的思路如下:
- 使用數組slice方法復制一副牌來發牌,保證原牌組不會變動;
- 使用數組splice方法結合隨機數,抽取一張牌,這里利用splice做刪除時會以一個數組形式返回被刪除的幾個元素,得到返回的對象后加入到對應玩家的手牌的數組中。這樣就不用去做一個洗牌的代碼,好比如買了一副新牌經常我們都要洗一下再玩,但是也可以不洗,三個人每人隨機從中抽一張,直到17張為止,大概就是這個思路。
- 使用數組sort方法對每個玩家手牌排序,這里需要我們自己寫一個數組元素的比較方法。因為按照上面的方法,每個玩家的手牌都是亂序的,我們要求是從大到小並且等值的按照花色排序,排序不僅是為了展示美觀,后面AI分析牌也用於判斷順子、連對之類的牌型,這里給出牌比較的代碼,如下:
1 /** 2 * 卡牌排序 3 * @method cardSort 4 * @param {Object} a [description] 5 * @param {Object} b [description] 6 * @return 1 : a < b ,-1 a : > b [description] 7 */
8 GameRule.prototype.cardSort = function (a, b){ 9 var va = parseInt(a.val); 10 var vb = parseInt(b.val); 11 if(va === vb){ 12 return a.type > b.type ? 1 : -1; 13 } else if(va > vb){ 14 return -1; 15 } else { 16 return 1; 17 } 18 };
發牌的時候,利用定時器每0.2秒給每個玩家都發一張牌,共發17張,這樣玩家就可以看到一個發牌的動畫。左右邊的AI玩家不需要顯示牌,只需要顯示背面,所以每次只需要在各自的手牌容器中加一個牌的圖片就可以;但是玩家自己的牌要按順序顯示,所以每次取牌,都要根據大小判斷位置再放進去。代碼在Scripts/ui/PlayUI.js中,這里給主要的代碼,如下:
1 //發牌
2 PlayUI.prototype.dealCards = function (){ 3 var self = this, 4 cards = G.cardMgr.getNewCards(); 5 //抽三張底牌
6 for (var i = 0; i < 3; i++) { 7 G.hiddenCards.push(self.getOneCard(cards)); 8 } 9 //總牌數
10 var total = 17; 11 var deal = function (){ 12 //左邊電腦玩家發牌
13 card = self.getOneCard(cards); 14 G.leftPlayer.cardList.push(card); 15 var c = self.game.add.clone(self.cardPrefab, self.leftPlayerArea.getScript('qc.engine.PlayerUI').cardContainer); 16 c.visible = true; 17 c.interactive = false; 18 //右邊電腦玩家發牌
19 card = self.getOneCard(cards); 20 G.rightPlayer.cardList.push(card); 21 c = self.game.add.clone(self.cardPrefab, self.rightPlayerArea.getScript('qc.engine.PlayerUI').cardContainer); 22 c.visible = true; 23 c.interactive = false; 24 //左邊電腦玩家發牌
25 //玩家的牌
26 card = self.getOneCard(cards); 27 G.ownPlayer.cardList.push(card); 28 self.insertOneCard(card); 29 if ( --total > 0) { 30 self.dealTimer = self.game.timer.add(200, deal); 31 } else { 32 G.leftPlayer.cardList.sort(G.gameRule.cardSort); 33 G.rightPlayer.cardList.sort(G.gameRule.cardSort); 34 G.ownPlayer.cardList.sort(G.gameRule.cardSort); 35 for (i = 0; i < G.currentCards.length; i++) { 36 G.currentCards[i].getScript('qc.engine.CardUI').isSelected = false; 37 } 38 //進入搶地主階段
39 self.robLandlord(); 40 } 41 }; 42 deal(); 43
44 };
二、搶地主流程
1、流程介紹
搶地主,就是玩家輪換叫分的過程,代碼的流程如下:
2、AI手牌評分
這里實現的AI搶地主,先根據手牌對AI玩家進行手牌評分,如果評分大於上家的叫分,就叫分,否則不叫。整體思路是看了以下這篇文章來寫的,包括后面的AI出牌之類都是從這邊看的,文章鏈接:斗地主ai設計。
叫牌原則分析
因為在斗地主中,火箭、炸彈、王和2可以認為是大牌,所以叫牌需要按照這些牌的多少來判斷。下面是一個簡單的原則,來自於上面這篇文章:
假定火箭為8分,炸~彈為6分,大王4分,小王3分,一個2為2分,則當分數
大於等於7分時叫3分;
大於等於5分時叫2分;
大於等於3分時叫1分;
小於三分不叫
我在Scripts/logci/AILogic.js下創建了AILogic類,在創建對象時需要傳入一個玩家對象,該類會對玩家手牌進行分析歸類,這些在AI出牌中再詳細闡述,這里我們先看看AI手牌評分的代碼吧。如下:
1 /** 2 * 手牌評分,用於AI根據自己手牌來叫分 3 * @method function 4 * @return {[nmber]} 所評得分 5 */
6 AILogic.prototype.judgeScore = function() { 7 var self = this, 8 score = 0; 9 score += self._bomb.length * 6;//有炸彈加六分
10 if(self._kingBomb.length > 0 ){//王炸8分
11 score += 8; 12 } else { 13 if(self.cards[0].val === 17){ 14 score += 4; 15 } else if(self.cards[0].val === 16){ 16 score += 3; 17 } 18 } 19 for (var i = 0; i < self.cards.length; i++) { 20 if(self.cards[i].val === 15){ 21 score += 2; 22 } 23 } 24 console.info(self.player.name + "手牌評分:" + score); 25 if(score >= 7){ 26 return 3; 27 } else if(score >= 5){ 28 return 2; 29 } else if(score >= 3){ 30 return 1; 31 } else {//4相當於不叫
32 return 4; 33 } 34 };
3、輪換搶地主
繼發牌完成之后,就進入到了搶地主階段,發完牌后隨機選取一個玩家開始叫分。由於進入單機模式便給每一個玩家添加了一個nextPlayer指向自己下一家,形成一個循環的引用,所以很容易找到自己下一家。如果是玩家則給玩家顯示叫分按鈕,AI則給出分數,主要代碼如下:

1 /** 2 * 搶地主階段 3 * @method robLandlord 4 */
5 PlayUI.prototype.robLandlord = function (){ 6 var self = this; 7 //隨機獲取從哪一家開始
8 var fb = G.gameRule.random(1,3); 9 var firstPlayer = fb === 1 ? G.ownPlayer : (fb == 2 ? G.rightPlayer : G.leftPlayer); 10 self.provideScore(firstPlayer); 11 }; 12
13 /** 14 * 輪換叫分 15 * @method robLandlord 16 */
17 PlayUI.prototype.provideScore = function(player){ 18 var self = this; 19 if(player.isAI){//AI玩家隨機出分
20 self.scoreThree.visible = false; 21 self.scoreTwo.visible = false; 22 self.scoreOne.visible = false; 23 self.scoreZero.visible = false; 24 self.game.timer.add(1000, function (){ 25 var s = (new AILogic(player)).judgeScore(); 26 var area = player.nextPlayer.isAI ? window.landlordUI.rightCards : window.landlordUI.leftCards; 27 if(s < 4 && s > self.currentScore){//小於3分
28 console.info(player.name + ":叫" + s); 29 self.currentScore = s; 30 self.scorePanel.text = s + ''; 31 self.currentLandlord = player; 32 //根據下家是否是AI判斷他的出牌區
33 for (var i = 0; i < area.children.length; i++) {//清空
34 area.children[i].destroy(); 35 } 36 var mesg = self.game.add.clone(self.msgPrefab, area); 37 mesg.text = s + '分'; 38 if(s === 3){//三分,得地主
39 self.setLandlord(player); 40 return; 41 } 42 } else { 43 var mesg = self.game.add.clone(self.msgPrefab, area); 44 mesg.text = '不叫'; 45 console.info(player.name + "沒有叫分搶地主"); 46 } 47 if(++self.round === 3){//已經三次不再進行
48 if(self.currentLandlord){//有叫分的得地主
49 self.setLandlord(self.currentLandlord); 50 } else {//沒有叫分,重新發牌
51 self.showRestartMesg(); 52 self.startGame(); 53 } 54 } else { 55 self.provideScore(player.nextPlayer); 56 } 57 }); 58 } else { 59 self.scoreZero.visible = true; 60 self.scoreThree.visible = true; 61 if(self.currentScore < 2) 62 self.scoreTwo.visible = true; 63 if(self.currentScore < 1) 64 self.scoreOne.visible = true; 65 } 66 }; 67
68 /** 69 * 玩家給分(搶地主) 70 * @method function 71 * @return {[type]} [description] 72 */
73 PlayUI.prototype.playerProvideScore = function(score){ 74 var self = this; 75 if(score < 4){//小於3分
76 self.currentScore = score; 77 self.scorePanel.text = score + ''; 78 self.currentLandlord = G.ownPlayer; 79 var mesg = self.game.add.clone(self.msgPrefab, window.landlordUI.ownCards); 80 mesg.text = score + '分'; 81 if(score === 3){//三分,得地主
82 self.setLandlord(G.ownPlayer); 83 return; 84 } 85 } else { 86 var mesg = self.game.add.clone(self.msgPrefab, window.landlordUI.ownCards); 87 mesg.text = '不叫'; 88 } 89 if(++self.round === 3){//已經三次不再進行
90 if(self.currentLandlord){//有叫分的得地主
91 self.setLandlord(self.currentLandlord); 92 } else {//沒有叫分,重新發牌
93 self.showRestartMesg(); 94 self.startGame(); 95 } 96 } else { 97 self.provideScore(G.ownPlayer.nextPlayer); 98 } 99 };
這里的playerProvideScore方法是玩家叫分,玩家有四個叫分按鈕:1分、2分、3分、不叫,每個按鈕事件都是調用這個方法,只是傳入不同的分數。詳細完整代碼可以在github上查看,去玩玩這個游戲結合代碼應該更好理解。
三、確定地主
完成搶地主后,確定地主的環節也是有不少事情要做,主要是以下幾點:
- 將底牌給地主,這里AI玩家只要修改牌數量,玩家的則需要將3張底牌插入對應位置,保證順序
- 顯示出底牌
- 界面上標明各個玩家身份
- 保存本局的分數,也就是地主叫的分數
- 讓地主開始出牌
代碼如下:
1 //設置地主
2 PlayUI.prototype.setLandlord = function(player){ 3 var self = this; 4 self.scorePanel.text = self.currentScore + ''; 5 self.scoreThree.visible = false; 6 self.scoreTwo.visible = false; 7 self.scoreOne.visible = false; 8 self.scoreZero.visible = false; 9 //顯示底牌
10 var oldHiddenCard = self.hiddenContainer.children; 11 for (var i = 0; i < self.hiddenContainer.children.length; i++) { 12 self.hiddenContainer.children[i].frame = G.hiddenCards[i].icon; 13 } 14 //self.startBtn.visible = false;
15 //設置地主及農民信息
16 G.ownPlayer.isLandlord = false; 17 G.leftPlayer.isLandlord = false; 18 G.rightPlayer.isLandlord = false; 19 player.isLandlord = true; 20 self.setAIStation(self.leftPlayerArea, G.leftPlayer.isLandlord); 21 self.setAIStation(self.rightPlayerArea, G.rightPlayer.isLandlord); 22 self.ownPlayerArea.getScript('qc.engine.PlayerUI').headPic.frame = G.ownPlayer.isLandlord ? 'landlord.png' : 'peasant.png'; 23 self.ownPlayerArea.getScript('qc.engine.PlayerUI').headPic.visible = true; 24 //把底牌給地主
25 player.cardList = player.cardList.concat(G.hiddenCards); 26 player.cardList.sort(G.gameRule.cardSort); 27 self.reDraw(); 28 if(!player.isAI){//不是AI需要重新渲染牌組
29 for (i = 0; i < G.hiddenCards.length; i++) { 30 self.insertOneCard(G.hiddenCards[i]); 31 } 32 } 33 for (i = 0; i < G.currentCards.length; i++) { 34 G.currentCards[i].getScript('qc.engine.CardUI').isSelected = false; 35 } 36 console.info('本輪地主是' + player.name); 37 //由地主開始出牌
38 window.landlordUI.cleanAllPlayArea(); 39 window.landlordUI.playCard(player); 40 };
四、手牌布局問題
完成上面的步驟后,游戲也進入可以愉快打牌的階段了,這里分享下我在用青瓷引擎做手牌布局顯示的時候遇到的些問題。如上圖,每個區域的手牌,左右兩邊玩家顯示倒是問題不大,因為其只是顯示相應數量的牌,都以背面顯示,並不需要真正顯示牌。主要還是在玩家手牌的問題,如果每張牌都我們去控制布局,會很繁瑣,我一開始就走了一個錯誤的路線,用這樣的方法:每張牌都放在手牌區域底下,每張牌設置不同的AnchoredX屬性值,來達到每張牌錯開的效果,但是會導致一些問題:
- 每當玩家出牌后,需要對所有牌重新布局
- 每當玩家出牌后,剩下牌難以居中顯示
- 要加入底牌時,由於要找在對應位置,難以實現
后面才發現青瓷引擎為我們提供了一個很好的布局組件:表格布局組件(點擊我看文檔),很好幫我實現了這個功能。真是一開始沒看全文檔,浪費了不少時間。實現的話,在牌的容器節點加入TableLayout組件,屬性設置如下圖,然后就只要往如圖cardList加子節點(卡牌圖片),刪除子節點(卡牌圖片),所有牌整體居中顯示,而且每張牌固定錯開30像素,不用做其他任何事情,就達到了我想要的布局效果。當然,左右兩邊玩家的手牌我也用了同樣的方式,只是用的是豎直的排列方式。
確定完了地主,就可以進入玩牌了,我會在下一篇文章分享單機模式斗地主剩下的流程。