最近學習使用了一款HTML5遊戲引擎(青瓷引擎),並用它嘗試作了一個鬥地主的遊戲,簡單實現了單機對戰和網絡對戰,代碼可已放到github上,在此談談本身如何經過引擎來開發這款遊戲的。javascript
客戶端代碼html
服務端代碼前端
(點擊圖片進入遊戲體驗)java
前文連接:git
javascript開發HTML5遊戲--鬥地主(單機模式part1)github
javascript開發HTML5遊戲--鬥地主(單機模式part2)web
本文章爲第三部份內容,主要AI相關邏輯實現,參考文章鬥地主ai設計。主要內容以下:算法
鬥地主ai設計文章中將牌型分爲了11種,我對其中的三帶1、飛機帶翅膀、四帶二這種類型又細分爲帶單牌或者帶對兩種,因此一共是14種類型:編程
單牌、對子、三根、三帶單、三帶對、順子、連對、三順(飛機不帶牌)、飛機帶單、飛機帶對、王炸、炸彈、四帶2、四帶兩對;api
主要是用於玩家選中牌是否合法的檢測,根據牌數量和一些邏輯要判斷是比較容易的。好比一張只會是單牌,確定是對的,兩張牌若是兩張大小同樣是對子,不同就是錯誤牌型,這樣一直日後判斷,順子子話就是判斷遞減,由於牌是有序的,因此若是是順子確定都是每張都比下一張大1,可是還要五張牌以上,基本都是長度加邏輯的判斷,咱們很容易得出傳入的一組牌是什麼牌型。我把牌型判斷的代碼都寫在了Scirpts/logic/GameRule.js下,簡單用常量列舉出以上幾種牌型,主要看typeJudge這個方法,傳入一組牌,返回判斷結果對象,錯誤牌型返回null,結果對象有三個屬性分別爲:
除了炸彈王炸之外,其餘牌必須是同牌型並且數量相等才能比較,炸彈能夠大過王炸之外的牌型,同是炸彈仍是比大小,王炸大任何牌型。這個邏輯就用在跟牌的時候,判斷玩家要出的牌必要大過上家才能出牌。
鬥地主AI最爲複雜就是出牌拆牌問題,若是AI是有什麼出什麼,那AI很難獲勝,後面牌就零碎的出不完了。鬥地主不只是本身要儘快出完牌,在對手快贏時也要儘可能阻止對手贏。在鬥地主ai設計中手牌分析一塊給咱們理清了思路,剩下的就靠咱們本身去用代碼實現。我在遊戲中也沒有很完美實現做者所描述的,但也是能夠進行遊戲了,牌很差的話仍是會輸給AI的。一開始看我也是一頭霧水,可是這些邏輯有思路了一步步來都是能夠實現的。
按照文章的邏輯,AILogic對象構造的時候將玩家的手牌進行分析(見該類的analyse方法),將各個牌型存進對象的幾個屬性中,跟文章順序有點不一樣,我分析的順序如圖:
一手牌找出王炸,剩下的再去找出炸彈,而後再去找出三順(飛機不帶牌),以此類推。在AILogic類中,用了八個屬性(都是數組)來存放這些牌型,分析以後咱們很容易獲得這個玩家的手牌的狀況。若是你看過代碼,嘗試在單機遊戲發牌完後,在瀏覽器開發者工具的控制檯中輸入如下代碼:
var ai = new qc.landlord.AILogic(G.ownPlayer); ai.log();
我拿着左邊圖的手牌,分析會獲得右邊的日誌信息。會看到打印出如下內容:
固然之後我能夠很便利的使用這個來完成一個託管功能,其實也就把玩家也當成一個AI處理。若是你把上面代碼的ownPlayer改爲leftPlayer就能夠偷偷看到AI的手牌狀況啦。
可能會有人有疑問那些三帶一之類的牌型怎麼沒了,這裏分析其實只是要知道玩家有哪些基本的牌型,合理的分配開,讓AI儘量快出完。至於相似三帶1、四帶二之類的就稱之爲組合牌型,能夠在要出牌的時候進行組合,好比在上家出3334這樣的牌的時候,AI通常是先找符合條件的三根,好比有777,再去找哪一個合適的單根來帶,找不到單牌,能夠考慮去拆對,想一想咱們玩鬥地主的時候也是如此吧。
AI出牌,按照文章中的出牌原則來走,總的來講就是對手牌大於2張從小往大出,對手牌小於等2,從大往小出,儘可能不出單。經過上面的手牌分析,大概思路是這樣的:好比咱們知道手上最小的那張是黑桃3(因爲手牌被排序過,最後一張確定是最小),而後拿着這張牌去找在咱們分析的哪一個牌型裏,找到了好比是一對3,出這個對子;這裏我還加了三帶一出法,好比找到是單牌黑桃3,能夠在判斷下有沒有三根能夠出,有就組成一個三帶一打出去。
先給個圖讓你們看看吧:
每當一個玩家打出牌後,咱們都要把出的牌型記錄下來,還有最後是哪一個玩家出的牌,這些都是AI用於判斷出牌的信息,也就是在AILogic.follow方法的三個參數:
跟牌就是出跟上家出牌同樣牌型的牌,把傳進方法的牌型,用switch判斷對號入座,這裏有14種牌型,就能夠分爲14個case塊,剩下的就是一塊一塊按照跟牌的原則去完善就ok了。像王炸這樣直接返回null了,這樣就是這個不出。固然炸彈算是特殊狀況,通常是不出的,咱們能夠判斷AI在無牌可跟狀況下,給出一些特殊狀況觸發出炸彈,好比本身只剩兩手牌,當手上就一個炸彈一個順子的時候,相信要是咱們本身玩的話確定就很爽的炸下去,而後贏了,雖然存在被炸的風險。這裏我就很少貼代碼了,有興趣能夠到個人github上看看,這部分寫的比較雜亂。
在完成牌型判斷和AI出牌跟牌算法後,咱們就能夠繼續完善整個出牌的流程。繼搶地主流程完成以後,會通知地主開始出牌,所謂出牌也個輪換的過程,跟搶地主是相似的。在Scripts/ui/landlordUI.js中寫了個playCard方法來實現玩家出牌,傳入的是一個player,這裏看下這段代碼吧:
1 /** 2 * 輪換出牌 3 * @param {Player} player 玩家 4 */
5 LandlordUI.prototype.playCard = function (player){ 6 var self = this; 7 if(player.isAI){ 8 console.info(player.name + '出牌中'); 9 var ai = new qc.landlord.AILogic(player); 10 //ai.info();
11 //根據下家是不是AI判斷他的出牌區
12 var area = player.nextPlayer.isAI ? self.rightCards : self.leftCards; 13 for (var i = 0; i < area.children.length; i++) {//清空
14 area.children[i].destroy(); 15 } 16 //AI出牌
17 self.game.timer.add(1000, function(){ 18 var result = null; 19 if(!self.roundWinner || self.roundWinner.name == player.name){//若是本輪出牌贏牌是本身:出牌
20 self.cleanAllPlayArea(); 21 result = ai.play(window.playUI.currentLandlord.cardList.length); 22 } else { //跟牌
23 result = ai.follow(self.winCard, self.roundWinner.isLandlord, self.roundWinner.cardList.length); 24 } 25 if(result){ 26 for (i = 0; i < result.cardList.length ; i ++) {//將牌顯示到出牌區域上
27 var c = self.game.add.clone(self.cardPrefab, area); 28 c.getScript('qc.engine.CardUI').show(result.cardList[i], false); 29 c.interactive = false; 30 for (var j = 0; j < player.cardList.length; j ++) {//刪除手牌信息
31 if(player.cardList[j].val === result.cardList[i].val 32 && player.cardList[j].type === result.cardList[i].type){ 33 player.cardList.splice(j, 1); 34 break; 35 } 36 } 37 } 38 if(result.cardKind === G.gameRule.BOMB || result.cardKind === G.gameRule.KING_BOMB){//出炸彈翻倍
39 var rate = parseInt(window.playUI.ratePanel.text); 40 window.playUI.ratePanel.text = (rate * 2) + ''; 41 } 42 self.roundWinner = player; 43 delete result.cardList; 44 self.winCard = result; 45 window.playUI.reDraw(); 46 } else { 47 self.game.add.clone(self.msgPrefab, area); 48 } 49 if(player.cardList.length === 0){ 50 self.judgeWinner(player); 51 return; 52 } 53 //繼續下家出牌
54 self.playCard(player.nextPlayer); 55 }); 56 } 57 else { 58 console.info('該你出了'); 59 if(self.getReadyCardsKind()){ 60 self.playBtn.state = qc.UIState.NORMAL; 61 } else { 62 self.playBtn.state = qc.UIState.DISABLED; 63 } 64 self.playBtn.visible = true; 65 self.warnBtn.visible = true; 66 self.cleanPlayArea(); 67 if(!self.roundWinner || self.roundWinner.name == player.name){//若是本輪出牌贏牌是本身:出牌,不顯示不出按鈕
68 self.winCard = null; 69 } else { 70 self.noCardBtn.visible = true; 71 } 72 self.promptTimes = 0; 73 //準備要提示的牌
74 var ai = new qc.landlord.AILogic(G.ownPlayer); 75 self.promptList = ai.prompt(self.winCard); 76 } 77 }
簡單解釋下
出牌流程就是這樣子一直輪換,直到有一家把牌出完,這樣就能夠進入最後的勝利斷定。
勝利斷定也是作一些操做,在一局遊戲結束後,要作的事情邏輯實現並不複雜,我這裏就概括下代碼中作了些什麼:
這裏提下分數的保存問題,引擎爲咱們也提供了個便利的緩存方法。在遊戲對象game下能夠獲取到能夠storage(點擊看文檔)對象,該對象能夠幫咱們將信息存到瀏覽器的緩存中,用的是咱們熟知的key/value形式,簡單易用。在Player類中能夠看到如下代碼:
Object.defineProperties(Player.prototype, { score: { get: function(){ return this._score; }, set: function(v){ this._score = v; var storage = G.game.storage; storage.set(this.name, v); storage.save(); } } });
這樣就能夠很便利的保存分數,你們也能夠在進入單機模式的按鈕時間中發現用改方法提取緩存中的分數信息,固然找不到就給玩家默認500的分數。
寫到這裏整個單機模式的鬥地主就算是完成了,小弟第一次作遊戲,也是第一次發博客,不足之處各位讀者多包涵。最後說兩個遊戲中增長體驗的內容:
通常我在玩紙牌類的遊戲的話,很喜歡這種拖動的方式,刷的過去一排牌選上來了,我在遊戲中也添加了這個功能,具體怎麼實現的,繼續往下看吧。
青瓷引擎爲咱們提供了一種面向組件式編程(點擊看文檔),咱們能夠將一個腳本掛載在任意的一個遊戲節點下,在整個遊戲開發中不少地方都用到了這種方式。
我編寫了一個CardlistUI.js掛載在玩家手牌的父節點上,根據拖放的開始和結束座標,計算中間有哪些紙牌,是的話就選中上來。這裏值得一提的是,咱們須要把掛載腳本的節點的屬性
Interactive打上勾,不然該節點只是個普通節點是沒法進行拖放、點擊等操做的,如圖:
在PC上玩鬥地主的時候呢,常常選完牌直接右擊就出牌啦,不用再去找那個【出牌】按鈕點着出,確實也是個不錯的體驗。
首先在瀏覽器上右擊就會出現右擊菜單,得先屏蔽掉它,引擎在這事上好像尚未支持,本身找了份代碼,以下:
1 /** 屏蔽瀏覽器右擊菜單*/ 2 if (window.Event) 3 window.document.captureEvents(Event.MOUSEUP); 4 function nocontextmenu(event){ 5 event.cancelBubble = true; 6 event.returnValue = false; 7 return false; 8 } 9 function norightclick(event){ 10 if (window.Event){ 11 if (event.which == 2 || event.which == 3) 12 return false; 13 } 14 else 15 if (event.button == 2 || event.button == 3){ 16 event.cancelBubble = true; 17 event.returnValue = false; 18 return false; 19 } 20 } 21 window.document.oncontextmenu = nocontextmenu; // for IE5+ 22 window.document.onmousedown = norightclick; // for all others 23 /** 屏蔽瀏覽器右擊菜單 end*/
在右鍵點擊事件上,輸入交互(點擊看文檔)仍是有不錯的支持,這裏利用出牌按鈕是否顯示判斷已經輪到本身出牌了,代碼以下:
1 var self = this, input = self.game.input; 2 this.addListener(input.onPointerDown, function(id, x, y){ 3 input = self.game.input; 4 var pointer = input.getPointer(id); 5 if (pointer.isMouse) { 6 if (pointer.id === qc.Mouse.BUTTON_RIGHT) { 7 if(self.playBtn.visible){ 8 self.playEvent(); 9 } 10 } 11 } 12 }, self);
一開始我用AI出牌/跟牌算法來作提示,發現很很差用,由於AI跟牌並非任何狀況都會出牌的,點着提示不跳出提示牌愣在那也是很不合理的。我以前也玩過qq的鬥地主,跟牌提示的 話不只僅是提示一種牌,而是將全部符合條件的牌依次顯示,好比當前最大是張K,我手上有張2和一對A,沒有大小王,點一下【提示】2被選上來,再點一下第一張A被選上來,原來的2 又回去了,再次點擊又變回2了。提示是根據匹配度來的,並不考慮如今需不須要出或者會不會拆牌的問題,好比單根,就從能大過上家的單牌從小到大開始找,而後再去對子裏從小到大 找,而後是三根,而後是拆炸彈,最後是出炸彈,沒有了以後從頭開始,若是玩家沒有任何牌能夠出,直接幫作【不出】操做。
在上面出牌的代碼中,輪到玩家的代碼最後有一段這樣的代碼:
self.promptTimes = 0; //準備要提示的牌 var ai = new qc.landlord.AILogic(G.ownPlayer); self.promptList = ai.prompt(self.winCard);
這個也是依賴於AI手牌分析的,具體方法各位應該看代碼會更清楚些。每次輪到玩家就將這個提示牌的數組promptList保存起來,每次玩家點擊【提示】,先用promptList的長度對 promptTimes取模,用這個結果去找promptList中的元素,完了以後要把promptTimes 加 1。把對應的牌選出來。這樣就達到循環提示出牌的效果,跟牌依然是根據牌型對號入座,一 個個去寫,出牌的話我採用了比較偷懶的作法,直接從小到大提示,好比有牌是這樣228885,先提示一張5,而後888,而後22,再點又一張5了,如此循環。
小弟以前都是作Java web開發的,前端也會作,對於就是js還算是比較熟悉的。畢業一年多吧,也不算工做經驗豐富,一開始要作遊戲還真沒什麼概念,把界面佈局作好後,想着這個AI設計也是很迷茫的,不過網上不少大神寫的文章都給了我不少思路,跟着他們講解的思路走,一步步來,把複雜的分紅一小塊一小塊的完成了,最後問題總會解決的。作這個鬥地主單機版了花了一週左右吧,這其中除了引擎提供了很好的支持外,青瓷引擎中的文檔也給了諸多幫助。學習新的東西,我都是把內容都過一遍,大概知道能作啥,有個印象,有demo的話也去看看,在用的時候就知道我大概什麼事情,再去翻文檔,或者去請教別人,用多了天然也就熟悉了。雖說最後遊戲是能夠玩了,代碼仍是很雜亂的,存在許多不合理的,固然也有很多的bug,圖片都是網絡上找的,總體遊戲界面也比較粗糙,本身能力仍是須要許多提高的,這個遊戲能夠優化的地方還有不少。