在这一节,将会给介绍算法模块的实现,即如何构造和分配牌、如何对牌的类型进行判断和比较。
我们将分配牌分成三个步骤,分别如下图所示:
众所众知,斗地主规则的牌数为54
张牌,其中1 ~ 2
各四张,大小王两张。因此我们需要循环54
构造出Card
对象:
for (int i = 0; i < 54; i++) {
int id = i + 1;
Card card = new Card(id);
card.setType(ConstructCard.getTypeById(id)); // 设置花色(type)
card.setNumber(ConstructCard.getNumberById(id)); // 设置牌的数值(number)
card.setGrade(ConstructCard.getGradeById(id)); // 设置牌的等级(grade)
allCardList.add(card); // 添加到用于存储所有牌的列表
}
并且根据id
分配不同的花色、数值和等级,具体的ConstructCard
逻辑可以看ConstructCard.java。这一步比较自定义,也可以自己实现,就不详细讲解了。
洗牌的逻辑非常简单,我们调用List
接口的shuffle
方法对所有牌列表进行乱序排序。为了保证牌的无序性,我们执行该方法1000
次:
private void shuffle() {
for (int i = 0; i < 1000; i++) {
Collections.shuffle(allCardList);
}
}
我们根据前面存放的allList
,将所有牌分成三份给三个玩家,并留三张底牌存入topCards
,等待分配给地主:
private void deal() {
/* 分派给1号玩家17张牌 */
for (int i = 0; i < 17; i++) {
Card card = allCardList.get(i * 3);
player1Cards.add(card);
}
/* 分派给2号玩家17张牌 */
for (int i = 0; i < 17; i++) {
Card card = allCardList.get(i * 3 + 1);
player2Cards.add(card);
}
/* 分派给3号玩家17张牌 */
for (int i = 0; i < 17; i++) {
Card card = allCardList.get(i * 3 + 2);
player3Cards.add(card);
}
/* 将剩余的三张牌添加到地主的牌当中 */
topCards = allCardList.subList(51, 54);
/* 将玩家的牌通过等级由小到大的排序 */
Collections.sort(player1Cards);
Collections.sort(player2Cards);
Collections.sort(player3Cards);
}
这里涉及到牌的排序问题,熟悉集合特性的大伙儿都知道:通过sort()
方法进行排序,底层会调用compareTo
方法来比较两个对象的排序大小,因此我们需要实现Comparable
接口,并重写Card
对象的该方法,让列表根据grade
等级进行排序。
这里我实现的排序方式是倒序,因为在玩家客户端展示的时候,牌会从左向右依次从小到大排序(根据通常情况下人的抓牌习惯),因此需要倒序排序:
public class Card implements Comparable<Card> {
@Override
public int compareTo(Card o) {
return - Integer.compare(this.getGradeValue(), o.getGradeValue());
}
}
除此之外,我们还提供了一个getCards
方法,可以根据playerId
来获取玩家分配到的牌:
public List<Card> getCards(int number) {
switch (number) {
case 1:
return player1Cards;
case 2:
return player2Cards;
case 3:
return player3Cards;
}
return null;
}
熟悉斗地主规则的大伙儿都知道,斗地主中所有出牌的类型有:单张、对子、三张、三带一、三带一对、四带二、顺子、连对、飞机、飞机带翅膀、炸弹、王炸。我们为这些类型创建一个枚举类TypeEnum
:
public enum TypeEnum {
SINGLE("单张"),
PAIR("对子"),
THREE("三张"),
THREE_WITH_ONE("三带一"),
THREE_WITH_PAIR("三带一对"),
FOUR_WITH_TWO("四带二");
STRAIGHT("顺子"),
STRAIGHT_PAIR("连对"),
AIRCRAFT_WITH_WINGS("飞机带翅膀"),
AIRCRAFT("飞机"),
BOMB("炸弹"),
JOKER_BOMB("王炸"),
}
首先我们找出类型比较相似的出牌类似,如:对子、三张、炸弹。因为它们有一个共同的特性:所有的牌的等级(或者数值)都是相同的。唯一不同的就是张数不同,我们先定义一个公共函数,用于判断牌序列中所有的牌是否等级相同,以及用于判空的函数:
private static boolean isAllGradeEqual(List<Card> cards) {
Card first = cards.get(0);
for (int i = 1; i < cards.size(); i++) {
if (!first.equalsByGrade(cards.get(i))) {
return false;
}
}
return true;
}
private static boolean isEmpty(List<Card> cards) {
if (cards == null) return true;
return cards.size() == 0;
}
只需满足牌序列张数为1
即可:
public static boolean isSingle(List<Card> cards) {
if (isEmpty(cards)) return false;
return cards.size() == 1;
}
三个类型满足所有牌等级相等,而对子、三张、炸弹分别还需要满足牌序列张数为:2
、3
、4
:
public static boolean isPair(List<Card> cards) {
if (isEmpty(cards) || cards.size() != 2) return false;
return isAllGradeEqual(cards);
}
public static boolean isThree(List<Card> cards) {
if (isEmpty(cards) || cards.size() != 3) return false;
return isAllGradeEqual(cards);
}
public static boolean isBomb(List<Card> cards) {
if (isEmpty(cards) || cards.size() != 4) return false;
return isAllGradeEqual(cards);
}
三单一有两种情况,一种是带的牌较小,只需要保证:第一张牌与第二张牌、第三章牌相等。如下图所示:
另一种情况是带的牌较大,只需要保证:第二张牌与第三章牌、第四张牌相等。如下图所示:
判断的算法逻辑为:
public static boolean isThreeWithOne(List<Card> cards) {
if (isEmpty(cards) || cards.size() != 4) return false;
// 防止该算法将炸弹判定为三带一
if (isAllGradeEqual(cards)) return false;
cards.sort(new CardSortComparable());
/* 是3带1,并且被带的牌在牌头 */
if (cards.get(0).equalsByGrade(cards.get(1)) && cards.get(0).equalsByGrade(cards.get(2)))
return true;
/* 是3带1,并且被带的牌在牌尾 */
else if (cards.get(3).equalsByGrade(cards.get(1)) && cards.get(3).equalsByGrade(cards.get(2)))
return true;
else
return false;
}
三带一对同样也有两种情况,一种也是被带的牌(对子)较小,在牌的末尾。这种情况下满足第一张牌等级与第二张牌和第三张牌等级相等,并且,第四张牌与第五张牌等级相同。如下图所示:
第二种情况是被带的牌(对子)较大,在牌的首部。这种情况下满足第三章牌的等级与第四张牌和第五张牌等级相同,并且第一张牌与第二张牌等级相同。如下图所示:
判断的算法逻辑为:
public static boolean isThreeWithPair(List<Card> cards) {
if (isEmpty(cards) || cards.size() != 5) return false;
cards.sort(new CardSortComparable());
if (cards.get(0).equalsByGrade(cards.get(1)) && cards.get(0).equalsByGrade(cards.get(2)))
return cards.get(3).equalsByGrade(cards.get(4));
else if (cards.get(2).equalsByGrade(cards.get(3)) && cards.get(2).equalsByGrade(cards.get(4))
&& cards.get(3).equalsByGrade(cards.get(4)))
return cards.get(0).equalsByGrade(cards.get(1));
else
return false;
}
判断四带二我们可以从0
遍历到2
,每次遍历都与后四张牌进行比较,如果这四张牌等级相同,那么即为四带二(因为可以忽略带的是两张单牌还是对子的情况):
public static boolean isFourWithTwo(List<Card> cards) {
if (isEmpty(cards) || cards.size() != 6) return false;
cards.sort(new CardSortComparable());
for (int i = 0; i < 3; i++) {
int grade1 = cards.get(i).getGradeValue();
int grade2 = cards.get(i + 1).getGradeValue();
int grade3 = cards.get(i + 2).getGradeValue();
int grade4 = cards.get(i + 3).getGradeValue();
if (grade2 == grade1 && grade3 == grade1 && grade4 == grade1) {
return true;
}
}
return false;
}
顺子的判断并不难,需要注意斗地主出顺子的约束有:顺子最少为5张,并且顺子中最大的牌只能是A。然后从0
遍历到cards.size - 1
(因为最后一张牌不用作比较),每次遍历都判断与下一张牌等级相比是否为递增:
public static boolean isStraight(List<Card> cards) {
if (isEmpty(cards) || cards.size() < 5) { // 顺子不能小于5个
return false;
}
cards.sort(new CardSortComparable());
Card last = cards.get(cards.size() - 1);
// 顺子最大的数只能是A
if (CardGradeEnum.getIllegalGradeOfStraight().contains(last.getGrade())) {
return false;
}
/* 判断卡片数组是不是递增的,如果是递增的,说明是顺子 */
for (int i = 0; i < cards.size() - 1; i++) {
/* 将每一张牌和它的后一张牌对比,是否相差1 */
Card cur = cards.get(i);
Card next = cards.get(i + 1);
if ((cur.getGradeValue() + 1) != next.getGradeValue()) {
return false;
}
}
return true;
}
连对的判断与顺子类似,但是连对(连续的对子)必须大于或等于3
个,也就是不能小于6
张牌,并且最大的连对数只能为A
。判断的主要逻辑为,遍历牌序列,判断第i
张牌是否与下一张牌相等,且是否与下一张牌的下一张牌等级递增1:
public static boolean isStraightPair(List<Card> cards) {
/* 连对的牌必须满足大于6张牌,而且必须是双数 */
if (isEmpty(cards) || cards.size() < 6 || cards.size() % 2 != 0) {
return false;
}
cards.sort(new CardSortComparable());
Card last = cards.get(cards.size() - 1);
// 连对最大的数只能是A
if (CardGradeEnum.getIllegalGradeOfStraight().contains(last.getGrade())) {
return false;
}
for (int i = 0; i < cards.size(); i += 2) {
Card current = cards.get(i); // 当前牌
Card next = cards.get(i + 1); // 下一张牌
if (i == cards.size() - 2) { // 判断牌尾的两张牌是否相等
if (!current.equalsByGrade(next)) {
return false;
}
break; // 跳出循环,因为不需要和下一个连对数进行比较
}
Card nextNext = cards.get(i + 2); // 下一张牌的下一张牌
if (!current.equalsByGrade(next)) { // 判断是否和下一牌的牌数相同
return false;
}
/* 判断当前的连对数是否和下一个连对数递增1,如果是,则满足连对的出牌规则 */
if ((current.getGradeValue() + 1) != nextNext.getGradeValue()) {
return false;
}
}
return true;
}
因为大小王等级和数值都不相同,因此我们只能通过相加牌的等级数来判断是否是王炸
private static int JOKER_BOMB_NUMBER_SUM = CardNumberEnum.BIG_JOKER.getValue() + CardNumberEnum.SMALL_JOKER.getValue();
public static boolean isJokerBomb(List<Card> cards) {
if (isEmpty(cards) || cards.size() != 2) return false;
int numberSum = 0;
for (Card card : cards) {
numberSum += card.getNumberValue();
}
return numberSum == JOKER_BOMB_NUMBER_SUM;
}
实现判断各个类型的方法之后,我们可以提供一个方法,传入一个牌序列并返回该序列的出牌类型。尽量将常出牌类型判断放在前面,提高命中率:
public static TypeEnum getCardType(List<Card> cards) {
TypeEnum type = null;
if (cards != null && cards.size() != 0) {
if (TypeJudgement.isSingle(cards)) {
type = TypeEnum.SINGLE;
} else if (TypeJudgement.isPair(cards)) {
type = TypeEnum.PAIR;
} else if (TypeJudgement.isThree(cards)) {
type = TypeEnum.THREE;
} else if (TypeJudgement.isThreeWithOne(cards)) {
type = TypeEnum.THREE_WITH_ONE;
} else if (TypeJudgement.isThreeWithPair(cards)) {
type = TypeEnum.THREE_WITH_PAIR;
} else if (TypeJudgement.isStraight(cards)) {
type = TypeEnum.STRAIGHT;
} else if (TypeJudgement.isStraightPair(cards)) {
type = TypeEnum.STRAIGHT_PAIR;
} else if (TypeJudgement.isFourWithTwo(cards)) {
type = TypeEnum.FOUR_WITH_TWO;
} else if (TypeJudgement.isBomb(cards)) {
type = TypeEnum.BOMB;
} else if (TypeJudgement.isJokerBomb(cards)) {
type = TypeEnum.JOKER_BOMB;
} else if (TypeJudgement.isAircraft(cards)) {
type = TypeEnum.AIRCRAFT;
} else if (TypeJudgement.isAircraftWithWing(cards)) {
type = TypeEnum.AIRCRAFT_WITH_WINGS;
}
}
return type;
}
前面我们实现了牌序列的类型判断,因此在已知两个牌序列的类型情况下,它们的大小比较将比较容易。比较特殊的是单张、对子、三张、炸弹等情况,都是只需要判断第一张牌大小即可。其他的出牌类型大小比较也不难:
该方法的代码有点长就不贴了,详细的逻辑可以看GradeComparison.java 中的方法。