之前我們實現了叫地主、玩家和電腦自動出牌主要功能,但是還有個問題,出牌的時候,沒有有效性檢查和比較牌力大小。比如說,出牌3,4,5,目前是可以出牌的,然后下家可以出任何牌如3,6,9。
- 問題1:出牌檢查有效性,就是出牌類型判斷,像單張、對子、順子、炸彈等等類型;
- 問題2:上家出牌后,下家再出牌的時候,要判斷當前牌力是否大於上家的牌力;
那本篇我們主要解決以上2個問題。
卡牌信息類重構
首先,原先的卡牌類,已經實現了單張卡牌牌力的比較,但是有些復雜,我們先對這個比較邏輯進行優化。思路是卡牌的cardIndex就表示在此類型卡牌中的大小權重,所有,在初始化卡牌的過程中,對cardIndex進行特殊的處理:
cardIndex=(卡牌的原始索引+10)%13
比如:普通牌3--cardIndex=(3+10)%13=0,轉換后排最小
普通牌J--cardIndex=(11+10)%13=8,轉換后排第8張,如此,我們就可以輕松比較cardIndex,來判斷單牌的實際大小了。
再看看優化后的代碼,是不是比以前簡潔很多?更主要的是,為了方便后面的復雜組合牌力的判斷。
public class CardInfo : IComparable { public string cardName; //卡牌圖片名 public CardTypes cardType; //牌的類型 public int cardIndex; //牌在所在類型的索引3-10,J,Q,K,A,2(0-12) public bool isSelected; //是否選中 public CardInfo(string cardName) { this.cardName = cardName; var splits = cardName.Split('_'); switch (splits[1]) { case "1": cardType = CardTypes.Hearts; cardIndex = (int.Parse(splits[2]) + 10) % 13; break; case "2": cardType = CardTypes.Spades; cardIndex = (int.Parse(splits[2]) + 10) % 13; break; case "3": cardType = CardTypes.Diamonds; cardIndex = (int.Parse(splits[2]) + 10) % 13; break; case "4": cardType = CardTypes.Clubs; cardIndex = (int.Parse(splits[2]) + 10) % 13; break; case "joker": cardType = CardTypes.Joker; cardIndex = (int.Parse(splits[2]) + 10) % 13; break; default: throw new Exception(string.Format("卡牌文件名{0}非法!", cardName)); } } //卡牌大小比較 public int CompareTo(object obj) { CardInfo other = obj as CardInfo; if (other == null) throw new Exception("比較對象類型非法!"); //如果當前是大小王 if (cardType == CardTypes.Joker) { //對方也是大小王 if (other.cardType == CardTypes.Joker) { return cardIndex.CompareTo(other.cardIndex); } //對方不是大小王 return 1; } //如果是一般的牌 else { //對方是大小王 if (other.cardType == CardTypes.Joker) { return -1; } //如果對方也是一般的牌 else { //計算牌力 if (cardIndex == other.cardIndex) { return -cardType.CompareTo(other.cardType); } return cardIndex.CompareTo(other.cardIndex); } } } }
出牌類型基類
接下來,那怎么判斷出牌有效性和出牌的牌力大小判斷呢?
我們這樣想,每次出牌都是一組牌堆,那我們首先要判斷這一組牌堆的類型,比如3帶1,單張、炸彈等等;
其次,確定牌堆類型后,我們需要判斷這個牌堆是否是合法的,其實就遍歷驗證是否符合上述的牌堆類型,如果滿足,就認為合法,如果所有類型都不滿足,則出牌無效,不允許出牌;
再者,怎么判斷下家出牌的牌力要大於上家呢,這里我們還得有一個方法,判斷相同類型的2個牌堆,牌堆1是否比牌堆2牌力大;
最后,為了實現電腦出牌的AI,還得有自動查找1個跟上家出牌類型一樣的牌堆,而且比上家的牌堆大,如果有這種牌,則可以壓住上家,否則自動過牌;
有了整體思路,我們這樣設計,先定義一個虛基類--出牌類型基類,這里定義了所有需要子類牌堆類型實現的方法
/// <summary> /// 出牌類型基類 /// </summary> public abstract class FollowCardsBase { /// <summary> /// 驗證類型 /// </summary> /// <returns></returns> public abstract bool Validate(List<CardInfo> cardInfos); /// <summary> /// 找到最小滿足的牌組 /// </summary> /// <returns></returns> public abstract List<CardInfo> FindBigger(List<CardInfo> handCardInfos, List<CardInfo> cardInfos); /// <summary> /// 判斷是否牌大過要比較的牌組 /// </summary> /// <param name="handCardInfos"></param> /// <param name="cardInfos"></param> /// <returns></returns> public abstract bool IsBigger(List<CardInfo> handCardInfos, List<CardInfo> cardInfos); } }
Validate方法:子類實現驗證出牌是否滿足此類型;
IsBigger方法:給定2個出牌的牌堆,判斷此類型牌堆1是否滿足牌力大於牌堆2;
FindBigger方法:在給定的手牌中,找出符合類型中滿足牌力大於給定牌堆的組合;
這樣,我們定義好基類,再利用子類去實現各自的方法,比如對子牌類型的子類,我們判斷如果是對子,出牌的時候Validate判斷是否也是對子,如果出牌是對子,而且IsBigger
,就允許出牌;當然AI出牌的時候,通過FindBigger,找到滿足對子類型,且比給定的對子牌力大的牌組,進行后續出牌操作。
定義各個類型牌型
因為斗地主涉及的牌型有很多,我們可以簡單歸納一下。
我這里把單張和順子作為一種牌組類型來實現,因為考慮順子其實就是一種單張的特殊情景,只是約束條件是大於等於連續5張的單張牌。所以對子和連對、3帶1或3帶1的飛機等等,就都可以歸為一類,個人感覺實現會簡單些。
這里還是以單張和順子舉例:
/// <summary> /// 驗證類型 /// </summary> /// <returns></returns> public override bool Validate(List<CardInfo> cardInfos) { cardInfos.Sort(); if (cardInfos.Count == 1) //單張 { return true; } else if (cardInfos.Count >= 5) //順子 { //如果最大的牌是王或者2,則不是順子 if (cardInfos.Last().cardType == CardTypes.Joker || cardInfos.Last().cardIndex == 12) return false; for (int i = 0; i < cardInfos.Count - 2; i++) { if (cardInfos[i].cardIndex + 1 != cardInfos[i + 1].cardIndex) return false; } return true; } return false; }
按照上一節所述,我們牌組類型子類,首先需要實現Validate方法,來驗證是否屬於單張或順子。
- 如果是一張牌,毫無疑問,是單張
- 如果是大於等於5張牌,最大的牌不是王或者2,而且是連續的牌,則是順子
- 其他情況肯定不是單張或順子
/// <summary> /// 判斷是否牌大過要比較的牌組 /// </summary> /// <param name="handCardInfos"></param> /// <param name="cardInfos"></param> /// <returns></returns> public override bool IsBigger(List<CardInfo> handCardInfos, List<CardInfo> cardInfos) { cardInfos.Sort(); handCardInfos.Sort(); //牌數一樣且最小牌比要比較的牌組的最小牌大 if (handCardInfos.Count == cardInfos.Count && Validate(handCardInfos) && Validate(cardInfos)) { if (handCardInfos[0].CompareTo(cardInfos[0]) > 0) return true; } return false; }
接着,怎么判斷2組牌組的牌力大小呢?
- 第一步,將2牌組按照從小到大排序
- 判斷2牌組的牌數是否一樣
- 判斷2牌組的類型是否都是單張或順子
- 如果滿足以上2個條件,再判斷2牌組的第一張大小
- 如果牌組1的第一張大於牌組2的第一張,則可以認為牌組1牌力大於牌組2
- 反之也成立
- 否則,如果相等,則牌力一樣
/// <summary> /// 找到最小滿足的牌組 /// </summary> /// <returns></returns> public override List<CardInfo> FindBigger(List<CardInfo> handCardInfos, List<CardInfo> cardInfos) { cardInfos.Sort(); handCardInfos.Sort(); if (cardInfos.Count == 1) //單張 { var cardInfo = handCardInfos.FirstOrDefault(s => s.CompareTo(cardInfos[0]) > 0 && s.cardIndex != cardInfos[0].cardIndex); if (cardInfo != null) { var result = new List<CardInfo>(); result.Add(cardInfo); return result; } return null; } else if (cardInfos.Count >= 5) //順子 { var count = handCardInfos.Count - cardInfos.Count; if (count >= 0) { //手牌比牌組多count,則有count + 1可能滿足牌組 for (int i = 0; i < count + 1; i++) { var mayBiggerCardInfos = handCardInfos.Skip(i).Take(cardInfos.Count).ToList(); //是順子,且最小的牌比要比較的牌組最小牌要大 if (Validate(mayBiggerCardInfos) && mayBiggerCardInfos[0].CompareTo(cardInfos[0]) > 0 && mayBiggerCardInfos[0].cardIndex != cardInfos[0].cardIndex) { return mayBiggerCardInfos; } } return null; } return null; } return null; }
再來看,我們怎么根據給定的單張或順子,在手牌中找到對應牌力大於上家的牌組;
這里提供了一個簡單的實現方式,找到最小滿足的牌組,當然,如果要實現智能的電腦出牌的AI,或者智能提示出牌,肯定要復雜很多,就不在探討范圍內了。
- 第一步,將2牌組按照從小到大排序
- 如果上家牌是單張,那簡單,從手牌中找到第1張滿足牌力大於上家牌的單張即可;
- 如果上家牌是順子,判斷手牌比上家牌的牌數只差,比如手牌17張,上家牌是5張的順子,那就是17-5=12,理論上有12+1最多可能滿足的牌組組合;
- 那從第一張手牌開始,遍歷12+1次,每次取當前手牌后的5張牌,判斷是否比上家牌牌力大,如果不滿足,則繼續下輪遍歷;
- 如果找到,則返回該牌組,可以認為這牌組牌力大於上家牌,且類型為單張或順子,牌數跟上家牌一樣;
- 如果遍歷完還沒找到,認為手牌中沒有大於上家牌的牌組,游戲玩家可以跳過次回合;
至此,我們的單張和順子類型的出牌檢驗邏輯,大體上就完成了。接下來,我們只需要稍微修改下之前的出牌邏輯~
出牌邏輯調整
這里需要調整2處,一方面,在玩家出牌時,要增加判斷,如果玩家選擇的牌堆,牌力不夠,需要提示玩家,很簡單,不在復述;
再來看電腦出牌的邏輯,我們在PlayOthre類中,增加以下這段代碼:
if (CardManager._instance.cardManagerState == CardManagerStates.Playing) { if (Input.GetKeyDown(KeyCode.Q)) //出牌 { var singleCards = new SingleCards(); var cardInfos = singleCards.FindBigger(this.cardInfos, CardManager._instance.currentCardInfos); if (cardInfos != null) { cardInfos.ForEach(s => s.isSelected = true); ForFollow(); } else { NotFollow(); } } }
電腦AI出牌的時候,通過FindBigger,去匹配手牌中有沒有滿足牌力大於上家牌的牌組類型,如果有,將返回的牌組出出去,如果沒有,則自動過牌。
那現在玩家出牌、電腦AI出牌就可以正常處理了,我們來看一下效果:

寫在最后
我們的斗地主游戲開發有段時間了,本文就作為階段性的結束篇~當然,遠算不上是一個成品游戲,我只是簡單的實現了部分功能,有些代碼現在看來還是偷工減料。要完成一個成品游戲,需要大量的精力和時間,條件也不允許,以后有機會的話,可能會對這個項目完善和優化。
這是我第一次寫一個系列的東西,寫得不好請大家多多包涵~推出這個系列,我的主旨是想和大家分享下Unity3D開發一個斗地主游戲的整體架構和設計模式,雖然是個半成品,但是有些代碼我是真的用心去寫的,盡量的把我實現的思路和對游戲的理解展現給大家。就像一千個人眼中有一千個哈姆雷特,由於我們對游戲理解的不同,同樣的斗地主游戲,不同的開發者,實現的方法可能就是不同的。我能做的,我希望做的,就是把個人的思想剖析給你們,如果你們能理解,或者對你們有所幫助,也是我的幸事。
最后,謝謝各位的一路陪伴。也歡迎大家關注,我們一同學習,共同進步,以后會堅持出新的系列!
