最近需要做一個類似於鬥地主的遊戲——柬埔寨TienLen遊戲,規則方面和鬥地主相差不大,算法方面,也是大同小異,所以趁着這個機會,將這部分算法進行整理,文章中包含了牌模型的構建、初始化牌、洗牌、發牌、牌類型判斷、出牌校驗、提示等算法,AI算法暫時沒有整理:
1.定義牌對象
首先需要對牌對象進行定義,正常鬥地主玩法下,一張牌只有一個屬性,就是數字大小,而不管花色,而在我們的遊戲中,同樣數字的牌,不同花色之間還可以比較,因此,我們的牌一共有兩個基本屬性,分別爲花色和大小。
1.1 規則
對於花色,規則定義如下:黑桃>梅花>方片>紅桃
對於數字,規則定義如下:2最大,3最小
1.2 建模
我們將牌的牌面實際數字使用數字進行標記,使用數字3到15表示真實牌的3到2,其中11表示J,12表示Q,13表示K,14表示A,15表示2,其餘數字分別代表真實牌面數字。
將牌的牌面花色同樣使用數字進行標記:根據從大到小,分別標記爲:4——黑桃,3——梅花,2——方片,1——紅桃。
這樣,對於一張牌的數字模型,使用以下公式進行標記:
牌數字模型大小 = 牌面數字模型大小 * 10 + 牌面花色模型大小
// 牌號點數:如3~J~A~2,使用3~15數字
private int cardNumber;
// 牌色:如4紅桃, 3方片, 2梅花, 1黑桃
private int cardColor;
// 牌全稱:例34是紅桃3,152是梅花2,113是方片J
private String cardName;
// 牌描述:例紅桃3,梅花2
private String cardDesc;
1.3 總結
經過上邊的標記,我們很容易將一張牌的模型進行數字化,例如:
154——黑桃2
151——紅桃2
83——梅花8
這樣154>151,同樣黑桃2大於紅桃2
151>83,同樣紅桃2大於梅花8
2. 構建一副牌
構建一副牌可以從花色、牌面大小、牌的名稱、牌的描述四個方面進行構建,其中花色(4——黑桃,3——梅花,2——方片,1——紅桃),大小爲:使用數字3到15表示真實牌的3到2,牌名稱即牌大小和花色組成的數字大小,描述是通用的牌的叫法,比如:154——黑桃2 牌名稱爲154 描述爲 黑桃2
這裏需要主要的是:鬥地主中存在王,大小王,共54張牌,而在我們的TienLen遊戲中不存在大小王,只有52張牌
/**
* 初始化牌
* @return
*/
public List<CardInfo> initCard() {
List<CardInfo> cardList = new ArrayList<CardInfo>();
for (int i = 1; i < 5; i++) {
for (int j = 3; j < 16; j++) {
CardInfo cardInfo = new CardInfo();
cardInfo.setCardNumber(j);
cardInfo.setCardColor(i);
cardInfo.setCardName(j * 10 + i + "");
switch (i) {
case 1:
cardInfo.setCardDesc("紅桃" + j);
break;
case 2:
cardInfo.setCardDesc("方片" + j);
break;
case 3:
cardInfo.setCardDesc("梅花" + j);
break;
case 4:
cardInfo.setCardDesc("黑桃" + j);
break;
}
cardList.add(cardInfo);
}
}
return cardList;
}
3. 洗牌
洗牌只需要將牌進行打亂即可,這裏考慮使用隨機數進行交換,模擬洗牌,但是這樣的算法存在缺陷,即有可能洗完以後,牌仍然保持原樣
/**
* 洗牌
*
* @param cardList 初始化號的牌
* @return
*/
public List<CardInfo> washCard(List<CardInfo> cardList) {
List<CardInfo> randomCardList = cardList;
for (int i = 0; i < 100; i++) {
Random random = new Random();
// 找出52以內的隨機數,然後交換位置
int a = random.nextInt(52);
int b = random.nextInt(52);
CardInfo cardInfoTemp = randomCardList.get(a);
randomCardList.set(a, randomCardList.get(b));
randomCardList.set(b, cardInfoTemp);
}
return randomCardList;
}
4. 發牌
發牌算法很簡單,將已經洗好的52張牌,順序發給各個玩家。這裏我們與鬥地主區別在於,我們這裏一共有四個玩家,因此需要將牌分爲4份:
/**
* 發牌
*
* @param cardList 洗好的牌
* @return
*/
public List<CardInfo>[] handCard(List<CardInfo> cardList) {
List<CardInfo> playerCardList[] = new Vector[4];
for (int i = 0; i < 4; i++) {
playerCardList[i] = new Vector<CardInfo>();
}
for (int j = 0; j < 52; j++) {
switch (j % 4) {
case 0:
playerCardList[0].add(cardList.get(j));
break;
case 1:
playerCardList[1].add(cardList.get(j));
break;
case 2:
playerCardList[2].add(cardList.get(j));
break;
case 3:
playerCardList[3].add(cardList.get(j));
break;
default:
break;
}
}
return playerCardList;
}
5. 捋牌
也就是對牌進行排序,從大到小進行排序,這樣出來的牌,便於往後進行分類等運算。
/**
* 排序,按照從大到小的順序進行排
*
* @param cardList
* @return
*/
public List<CardInfo> sortCard(List<CardInfo> cardList) {
Collections.sort(cardList, new Comparator<CardInfo>() {
@Override
public int compare(CardInfo cardInfo1, CardInfo cardInfo2) {
int cardNum1 = Integer.valueOf(cardInfo1.getCardName());
int cardNum2 = Integer.valueOf(cardInfo2.getCardName());
if (cardNum1 > cardNum2) {
return -1;
} else if (cardNum1 == cardNum2) {
return 0;
} else {
return 1;
}
}
});
return cardList;
}
6. 出牌
出牌時,應該根據規則進行出牌,首先、判斷用戶所選擇的牌是否符合規則,即是否是單牌、對子、三張、鏈子、炸彈等
6.1 單張牌
獲取單張牌的算法很簡單,任意一張牌,都可以作爲單張牌使用,因此只需要將所有的牌都添加到單張牌的列表中即可。
/***
* 獲取單張牌
*
* @param mCardList
* @return
*/
public List<List<CardInfo>> get1(List<CardInfo> mCardList) {
List<List<CardInfo>> all1List = new ArrayList<>();
List<CardInfo> cardList;
sortCardAsc(mCardList);
for (int i = 0, length = mCardList.size(); i < length; i++) {
cardList = new ArrayList<>();
cardList.add(mCardList.get(i));
all1List.add(cardList);
}
return all1List;
}
6.2 對子
獲取對子時,需要注意,因爲我們TienLen遊戲的規則中,不僅需要比較牌面點數大小,還需要比較花色大小,所以,同樣4個2,可能組合成多種對子,且大小不一樣,比如黑桃2和梅花2,比如紅桃2和方片2
/**
* 獲取對子
* 這裏對i+1 i+2 i+3分別和第i張牌進行對比,
* 舉例:比如四個2,可以黑桃2和方片2一對,也可以是梅花2和紅桃2一對
*
* @param mCardList
* @return
*/
public List<List<CardInfo>> get11(List<CardInfo> mCardList) {
// 先對牌進行排序
sortCardAsc(mCardList);
List<List<CardInfo>> all11CardList = new ArrayList<>();
List<CardInfo> cardList;
for (int i = 0, length = mCardList.size(); i < length; i++) {
if (i + 1 < length
&& mCardList.get(i).getCardNumber() == mCardList.get(i + 1).getCardNumber()) {
cardList = new ArrayList<>();
cardList.add(mCardList.get(i));
cardList.add(mCardList.get(i + 1));
all11CardList.add(cardList);
}
if (i + 2 < length
&& mCardList.get(i).getCardNumber() == mCardList.get(i + 2).getCardNumber()) {
cardList = new ArrayList<>();
cardList.add(mCardList.get(i));
cardList.add(mCardList.get(i + 2));
all11CardList.add(cardList);
}
if (i + 3 < length
&& mCardList.get(i).getCardNumber() == mCardList.get(i + 3).getCardNumber()) {
cardList = new ArrayList<>();
cardList.add(mCardList.get(i));
cardList.add(mCardList.get(i + 3));
all11CardList.add(cardList);
}
}
return all11CardList;
}
6.3 三個
在鬥地主的規則中,好像也是三個也可以一起出,但是需要帶一個或者一對,我們TienLen遊戲中不需要帶,也不能帶,可以直接出,比如三個三,三個四,這樣的牌,獲取的算法和上邊對子的獲取算法一致
/***
* 獲取三個
* 算法個獲取對子的算法類似
*
* @param mCardList
* @return
*/
public List<List<CardInfo>> get111(List<CardInfo> mCardList) {
List<List<CardInfo>> all111List = new ArrayList<>();
List<CardInfo> cardList;
// 先對牌進行排序
sortCardAsc(mCardList);
for (int i = 0, length = mCardList.size(); i < length; i++) {
if (i + 2 < length
&& mCardList.get(i).getCardNumber() == mCardList.get(i + 2).getCardNumber()) {
cardList = new ArrayList<>();
cardList.add(mCardList.get(i));
cardList.add(mCardList.get(i + 1));
cardList.add(mCardList.get(i + 2));
all111List.add(cardList);
}
}
return all111List;
}
6.4 炸彈
炸彈,不論在鬥地主中還是我們現在做的TienLen中,都是一樣的作用,一樣的獲取方法,和獲取對子,三個的方法一致,這裏直接上代碼:
/***
* 獲取炸彈
*
* @param mCardList
* @return
*/
public List<List<CardInfo>> get1111(List<CardInfo> mCardList) {
List<List<CardInfo>> all1111List = new ArrayList<>();
List<CardInfo> cardList;
for (int i = 0, length = mCardList.size(); i < length; i++) {
if (i + 3 < length
&& mCardList.get(i).getCardNumber() == mCardList.get(i + 3).getCardNumber()) {
cardList = new ArrayList<>();
cardList.add(mCardList.get(i));
cardList.add(mCardList.get(i + 1));
cardList.add(mCardList.get(i + 2));
cardList.add(mCardList.get(i + 3));
all1111List.add(cardList);
}
}
return all1111List;
}
6.5 鏈子
終於說到了這個牌型——鏈子,鏈子在不同的玩法中,可以出不同的長度,在我們的TienLen中最少是三聯,這裏獲取時,先對手牌進行排序,排好序後,進行遍歷,找到能和當前牌連接起來的,且牌長度大於3的,均屬於鏈子:
/**
* 獲取鏈子
*
* @param mCardList
* @return
*/
public List<List<CardInfo>> get123(List<CardInfo> mCardList) {
// 鏈子長度必須大於3,即最少出3連
if (mCardList.size() < 3) {
return null;
}
// 構建返回數據
List<CardInfo> tempCardList = new ArrayList<>();
List<List<CardInfo>> all123List = new ArrayList<>();
// 先去掉2
for (int i = 0; i < mCardList.size(); i++) {
if (mCardList.get(i).getCardNumber() != 15) {
tempCardList.add(mCardList.get(i));
}
}
// 重新進行排序
sortCardAsc(tempCardList);
for (int i = 0; i < tempCardList.size(); i++) {
CardInfo tempCardInfo = tempCardList.get(i);
List<CardInfo> cardList = new ArrayList<>();
cardList.add(tempCardInfo);
List<CardInfo> cardListTempAfter = new ArrayList<>();
for (int j = i + 1; j < tempCardList.size(); j++) {
// 判斷當前牌是否個下一個牌能連起來(當前牌是5,當下一個是5+1=6時,即連起來了,當連起來大於3個牌時,即可以認爲是一連)
if ((tempCardInfo.getCardNumber() + 1) == tempCardList.get(j).getCardNumber()) {
cardListTempAfter.clear();
cardListTempAfter.addAll(cardList);
cardList.add(tempCardList.get(j));
tempCardInfo = tempCardList.get(j);
if (cardList.size() >= 3) {
List<CardInfo> cardListTemp = new ArrayList<>();
cardListTemp.addAll(cardList);
all123List.add(cardList);
cardList = new ArrayList<>();
cardList.addAll(cardListTemp);
}
} else if (tempCardInfo.getCardNumber() == tempCardList.get(j).getCardNumber()
&& tempCardInfo.getCardNumber() != tempCardList.get(i).getCardNumber()) {
List<CardInfo> cardListTemp = new ArrayList<>();
cardListTemp.addAll(cardListTempAfter);
if (cardListTemp.size() > 0
&& cardListTemp.get(cardListTemp.size() - 1).getCardNumber() != tempCardList
.get(j).getCardNumber()) {
cardListTempAfter.add(tempCardList.get(j));
if (cardListTempAfter.size() >= 3) {
all123List.add(cardListTempAfter);
cardListTempAfter = new ArrayList<>();
cardListTempAfter.addAll(cardListTemp);
}
}
}
}
}
return all123List;
}
6.6 雙鏈
雙鏈,也就是經常說的飛機帶翅膀,雙鏈的前提是對子,只有存在對子的情況下,才能找出來雙鏈,所以,其算法也是一樣,先找到所有的對子,然後去掉2,進行排序,再按照找鏈子的方法進行找,這樣返回的就是雙鏈。
/***
* 獲取飛機
*
* @param mCardInfoList
* @return
*/
public List<List<CardInfo>> get112233(List<CardInfo> mCardInfoList) {
int length = mCardInfoList.size();
// 雙鏈最少爲3連,所以最少六張牌
if (length < 6) {
return null;
}
// 保存所有的對子
List<CardInfo> tempList = new ArrayList<>();
// 保存所有不包含2的對子
List<CardInfo> apairTempList = new ArrayList<>();
// 防止重複添加
List<Integer> integerList = new Vector<>();
// 返回結果
List<List<CardInfo>> all112233List = new ArrayList<>();
// 存儲單個雙對鏈子
List<CardInfo> cardList;
// 先獲取所有的對子
for (int i = 0; i < length; i++) {
if (i + 1 < length
&& mCardInfoList.get(i).getCardNumber() == mCardInfoList.get(i + 1)
.getCardNumber()) {
tempList.add(mCardInfoList.get(i));
tempList.add(mCardInfoList.get(i + 1));
i = i + 1;
}
}
// 排序
sortCardAsc(tempList);
// 去除對2和相同的
for (int i = 0, tempLength = tempList.size(); i < tempLength; i++) {
if (!integerList.contains(Integer.valueOf(tempList.get(i).getCardNumber()))) {
apairTempList.add(tempList.get(i));
integerList.add(Integer.valueOf(tempList.get(i).getCardNumber()));
}
}
// 雙對的鏈子最少三聯
if (apairTempList.size() < 3) {
return null;
}
// 對之前拿到的對子List進行排序,正序
sortCardAsc(tempList);
// 到這裏已經拿到了所有對子中的某一個單牌,只需拿出所有的鏈子
List<List<CardInfo>> get123TempList = get123(apairTempList);
for (int j = 0; j < get123TempList.size(); j++) {
List<CardInfo> list123 = get123TempList.get(j);
sortCardAsc(list123);
for (int k = 0; k < tempList.size(); k++) {
if (tempList.get(k).getCardName().equals(list123.get(0).getCardName())) {
cardList = new ArrayList<>();
for (int l = k; l < list123.size() * 2 + k; l++) {
cardList.add(tempList.get(l));
}
all112233List.add(cardList);
}
}
}
return all112233List;
}
7 出牌
出牌有兩種情況,一種是手動選擇的,一種是通過提示,自動出牌的。對於手動選擇的,需要根據自己當前是否有首先出牌權,進行校驗,
- 如果當前是自己的局,也就是說,上輪出牌的過程中,自己最大,這局自己首先出,所以只需要校驗自己手動選擇的牌是否符合規則。
- 如果當前是別人的局,也就是說,自己當前跟着別人的局出牌,只能和別人的類型一致,且大於對方,所以需要校驗選擇的牌類型是否和別人的一致,再校驗是否比別人的大,才能出
對於通過提示出牌的,只適合第二種情況,也就是說,別人出牌,然後自己管,系統會進行提示
7.1 判斷所選擇的牌,是否符合已經定義的出牌類型,對應上邊所述的第一種情況,只要符合規則均可以出
/**
* 獲取出牌類型
*
* @param outCard
* @return
*/
public OutCardType getOutCardType(List<CardInfo> outCard) {
if (outCard != null) {
int cardLength = outCard.size();
if (outCard.get(0).getCardNumber() == outCard.get(cardLength - 1).getCardNumber()) {
switch (cardLength) {
case 1:
// 單牌
return OutCardType.type1;
case 2:
// 對子
return OutCardType.type11;
case 3:
// 三個
return OutCardType.type111;
case 4:
// 炸彈
return OutCardType.type1111;
}
}
// 判斷鏈子,最少三張
if (outCard.size() >= 3) {
List<CardInfo> tempCardList = new ArrayList<>();
// 先去掉2
for (int i = 0; i < outCard.size(); i++) {
if (outCard.get(i).getCardNumber() != 15) {
tempCardList.add(outCard.get(i));
}
}
// 重新進行排序
sortCardAsc(tempCardList);
// 判斷是否爲鏈子
List<List<CardInfo>> get123 = get123(outCard);
if (get123 != null && get123.size() > 0) {
for (List<CardInfo> list : get123) {
if (list.size() == outCard.size()) {
return OutCardType.type123;
}
}
}
// 雙對至少6張
if (outCard.size() >= 6) {
int length = outCard.size();
// 保存所有的對子
List<CardInfo> tempList = new ArrayList<>();
// 保存所有不包含2的對子
List<CardInfo> apairTempList = new ArrayList<>();
// 防止重複添加
List<Integer> integerList = new Vector<>();
// 先獲取所有的對子
for (int i = 0; i < length; i++) {
if (i + 1 < length
&& outCard.get(i).getCardNumber() == outCard.get(i + 1)
.getCardNumber()) {
tempList.add(outCard.get(i));
tempList.add(outCard.get(i + 1));
i = i + 1;
}
}
// 所有的牌均爲對子
if (tempList.size() == outCard.size()) {
// 去除對2
for (int i = 0, tempLength = tempList.size(); i < tempLength; i++) {
if (integerList.indexOf(outCard.get(i).getCardNumber()) < 0
&& tempList.get(i).getCardNumber() != 15) {
apairTempList.add(tempList.get(i));
integerList.add(tempList.get(i).getCardNumber());
}
i = i + 1;
}
// 到這裏已經拿到了所有對子中的某一個單牌,只需拿出所有的鏈子
List<List<CardInfo>> get123TempList = get123(apairTempList);
for (int i = 0; i < get123TempList.size(); i++) {
if (get123TempList.get(i).size() == length / 2) {
return OutCardType.type112233;
}
}
}
}
}
}
return OutCardType.type0;
}
只有當選中牌的類型是已知類型,才能第一步判斷出是否可以出牌,下一步則需要根據當前是不是自己輪,判斷需要不需要壓對方的牌
7.2 判斷當前所選擇的牌,是否符合規則,而且,是否比上一家出的牌大
/**
* 當上家出牌後,判斷自己是否可以出牌
*
* @param outCard
* @param mAllCard
* @param mSelectCard
* @return
*/
public boolean whetherCanPlay(List<CardInfo> outCard, List<CardInfo> mAllCard,
List<CardInfo> mSelectCard) {
boolean isCardCanPlay = false;
// 獲取對手牌型
OutCardType outCardType = getOutCardType(outCard);
OutCardType outCardTypeMy = getOutCardType(mSelectCard);
sortCard(outCard);
// 先對牌進行排序
sortCard(mSelectCard);
// 首先判斷牌的張數是否一樣
if (outCard.size() == mSelectCard.size() && outCardType == outCardTypeMy) {
int outCardName = Integer.valueOf(outCard.get(0).getCardName());
int mSelectCardName = Integer.valueOf(mSelectCard.get(0).getCardName());
// 相同,屬於同一級牌之間壓
switch (outCardType) {
case type1:
if (mSelectCardName > outCardName) {
isCardCanPlay = true;
}
break;
case type11:
if (mSelectCardName > outCardName) {
isCardCanPlay = true;
}
break;
case type111:
if (mSelectCardName > outCardName) {
isCardCanPlay = true;
}
break;
case type1111:
if (mSelectCardName > outCardName) {
isCardCanPlay = true;
}
break;
case type123:
if (mSelectCardName > outCardName) {
isCardCanPlay = true;
}
break;
case type112233:
if (mSelectCardName > outCardName) {
isCardCanPlay = true;
}
break;
default:
isCardCanPlay = false;
break;
}
} else {
// 當張數不一致時,有兩種情況,即炸彈壓2和連着的雙對壓對2
if (outCard.size() == 1 && mSelectCard.size() == 4) {
// 當別人爲單個2且自己的Type爲炸彈時
if (outCard.get(0).getCardNumber() == 15
&& getOutCardType(mSelectCard) == OutCardType.type1111) {
isCardCanPlay = true;
}
} else {
// 別人出牌爲一對2,自己應該用33-44-55-66或者55-66-77-88壓
if (outCard.size() == 2 && mSelectCard.size() >= 8) {
if (outCard.get(0).getCardNumber() == 15
&& getOutCardType(mSelectCard) == OutCardType.type112233) {
isCardCanPlay = true;
}
} else {
isCardCanPlay = false;
}
}
}
return isCardCanPlay;
}
這裏邊包含了部分規則,比如同樣的牌類型,比較大小,同時33445566可以壓對二這樣的規則
8. 提示
提示算法比較簡單,先獲取上家出牌的類型,再獲取自己手牌中對應類型的列表,逐個進行比較,直到找到合適的
9. AI
這裏除了上述洗牌、發牌、出牌等算法之外,還有單機模式的AI算法,回頭有空了整理下,我再發上來吧。
10. 總結
在這片文章中,只是寫了一個針對鬥地主類類遊戲的牌的算法,包含了牌模型構建、洗牌、發牌、出牌等算法的實現,雖然遊戲規則不同,但是思路大同小異,希望有需要的同學可以參考下。