文档章节

原创干货!麻将平胡算法

kakai
 kakai
发布于 2019/05/07 10:15
字数 2859
阅读 1.2W
收藏 86

此算法基本可以通用于所有麻将的平胡规则,即满足m * ABC + n * AAA + AA(其中m、n可为0)的胡牌公式,红黑字牌也可由此算法演变。

首先,我们要约定每张麻将都可以由一个数字表示,比如11表示一万,12表示二万,21表示一条,22表示二条,31表示一筒,32表示二筒……

即所有牌用两位数表示,表示万条筒的两位数个位为牌点,十位为牌类型,其它表示非字牌的两位数与牌类型相同,以下用一个枚举类定义:

import java.util.HashMap;
import java.util.Map;

/**
 * 麻将类型枚举
 *
 * @author zkpursuit
 */
public enum CardType {

    wan(1, "万"), tiao(2, "条"), tong(3, "筒"),
    dong(40, "东风"), nan(41, "南风"), xi(42, "西风"),
    bei(43, "北风"), zhong(44, "中"), fa(45, "发"), ban(46, "白板");

    //类型
    private final int value;
    //牌名
    private final String name;

    private CardType(int value, String name) {
        this.value = value;
        this.name = name;
    }

    public int getValue() {
        return value;
    }

    public String getName() {
        return name;
    }

    private static final Map<Integer, String> numMap = new HashMap<>();
    private static final Map<Integer, CardType> types = new HashMap<>();
    private static final Map<Integer, String> typeNames = new HashMap<>();

    static {
        numMap.put(1, "一");
        numMap.put(2, "二");
        numMap.put(3, "三");
        numMap.put(4, "四");
        numMap.put(5, "五");
        numMap.put(6, "六");
        numMap.put(7, "七");
        numMap.put(8, "八");
        numMap.put(9, "九");
        CardType[] enums = CardType.values();
        for (CardType cardType : enums) {
            types.put(cardType.getValue(), cardType);
            typeNames.put(cardType.getValue(), cardType.getName());
        }
    }

    /**
     * 获取牌类型枚举
     *
     * @param typeValue 牌类型值
     * @return 牌类型枚举
     */
    public static final CardType getCardType(int typeValue) {
        return types.get(typeValue);
    }

    /**
     * 获取牌的类型名
     *
     * @param typeValue 牌类型
     * @return 牌类型名
     */
    public static final String getCardTypeName(int typeValue) {
        return typeNames.get(typeValue);
    }

    /**
     * 获取牌类型数值表示
     *
     * @param card 牌号
     * @return 牌类型数值表示
     */
    public static final int getCardTypeValue(int card) {
        if (card < 40) {
            return HandCards.getCardLeftValue(card);
        }
        return card;
    }

    /**
     * 将牌数据转换为现实中可读的牌
     *
     * @param card 牌数据
     * @return 现实中可读的牌
     */
    public static final String getCardName(int card) {
        if (card < 40) {
            int type = HandCards.getCardLeftValue(card);
            int point = HandCards.getCardRightValue(card);
            StringBuilder sb = new StringBuilder();
            sb.append(numMap.get(point));
            sb.append(getCardTypeName(type));
            return sb.toString();
        }
        return getCardTypeName(card);
    }

}

以上定义了各张牌的数字表示,接下来我们分析手牌的存储结构,手牌可以用一个数组表示,数组下标号能除尽10的数组元素为保留位,不用于存储任何数据。举例解释此数组存储牌的数据结构:

0号下标保留位
1~9号下标为万字牌牌点,其对应的数组元素为牌的张数
10号下标保留位
11~19号下标为条字牌牌点,其对应的数组元素为牌的张数
20号下标为保留位
21~29号下标为筒字牌牌点,其对应的数组元素为牌的张数
40~46号下标分别表示东、南、西、北、中、发、白的存储位。

根据以上的定义,则可以根据数组下标获得万条筒字牌的类型和牌点,(下标/10 + 1) 则为字牌类型,(下标%10) 则为字牌点数。

具体定义一个手牌类,里面定义了各种静态的换算函数,可参看注释。

/**
 * 手牌
 *
 * @author zkpursuit
 */
public class HandCards {

    /**
     * 获取牌号最左边的一位数,如果牌为筒、条、万,则返回值为牌类型数值
     *
     * @param card 牌号
     * @return 牌号从左至右第一位数(十位数)
     */
    public final static int getCardLeftValue(int card) {
        return card / 10;
    }

    /**
     * 获取牌号最右边的一位数,如果牌为筒、条、万,则返回值为牌点数
     *
     * @param card 牌号
     * @return 牌号从右至左第一位数(个位数)
     */
    public final static int getCardRightValue(int card) {
        return card % 10;
    }

    /**
     * 获取牌号最左边的一位数,如果牌为筒、条、万,则返回值为牌类型数值
     *
     * @param idx 牌在归类数组中的索引位置
     * @return 牌号从左至右第一位数(十位数)
     */
    public final static int getCardLeftValueByClusterIndex(int idx) {
        return idx / 10 + 1;
    }

    /**
     * 获取牌号最右边的一位数,如果牌为筒、条、万,则返回值为牌点数
     *
     * @param idx 牌在归类数组中的索引位置
     * @return 牌号从右至左第一位数(个位数)
     */
    public final static int getCardRightValueByClusterIndex(int idx) {
        return idx % 10;
    }

    /**
     * 根据牌号取得其所在的牌归类数组中的索引
     *
     * @param card 牌号
     * @return 牌归类数组中的索引
     */
    public final static int getClusterIndexByCard(int card) {
        int left = getCardLeftValue(card);
        int right = getCardRightValue(card);
        int idx = (left - 1) * 10 + right;
        return idx;
    }

    /**
     * 根据十位数和个位数确定牌在聚合数组中的索引位置
     *
     * @param leftValue 十位数
     * @param rightValue 个位数
     * @return 聚合数组中的索引位置
     */
    public final static int getClusterIndex(int leftValue, int rightValue) {
        return (leftValue - 1) * 10 + rightValue;
    }

    /**
     * 归类牌<br>
     * 数组索引 / 10 + 1 表示牌类型<br>
     * 数组索引 % 10 表示牌点数<br>
     * 数组索引位置的值表示牌数量
     */
    private int[] cardClusterArray;
    /**
     * 起始有效的索引位置<br>
     * 第一个值不为0的索引位置<br>
     */
    private int startIndex;
    /**
     * 归类牌数组的有效索引位置,因为有可能后面的位置全是0<br>
     * 此索引的后续索引位置的值全部为0,即最后一个值不为0的索引位置<br>
     */
    private int lastIndex;
    /**
     * 所有的牌数量
     */
    private int cardTotals;

    /**
     * 构造方法
     */
    public HandCards() {
        cardClusterArray = new int[40];
        startIndex = 1000;
        lastIndex = -1;
        cardTotals = 0;
    }

    /**
     * 构造方法
     *
     * @param cards 未归类的牌数组
     */
    public HandCards(int[] cards) {
        this();
        if (cards != null) {
            setCards(cards);
        }
    }

    /**
     * 重置数据
     */
    public void reset() {
        if (cardTotals != 0) {
            int len = getClusterValidLength();
            for (int i = 0; i < len; i++) {
                cardClusterArray[i] = 0;
            }
        }
        startIndex = 1000;
        lastIndex = -1;
        cardTotals = 0;
    }

    /**
     * 清除数据
     */
    public void clear() {
        reset();
    }

    /**
     * 重置数据并以传入的牌数据再次初始化数据
     *
     * @param cards 牌数据
     */
    public final void setCards(int[] cards) {
        reset();
        for (int card : cards) {
            addCard(card);
        }
    }

    /**
     * 添加num张牌
     *
     * @param card 添加的牌号
     * @param num 添加的数量
     * @return true添加成功;false添加失败
     */
    public boolean addCard(int card, int num) {
        int idx = getClusterIndexByCard(card);
        int lastNum = cardClusterArray[idx] + num;
        if (lastNum > 4) {
            return false;
        }
        cardClusterArray[idx] = lastNum;
        if (idx > lastIndex) {
            lastIndex = idx;
        }
        if (idx < startIndex) {
            startIndex = idx;
        }
        cardTotals += num;
        return true;
    }

    /**
     * 添加一张牌
     *
     * @param card 牌号
     * @return true添加成功;false添加失败
     */
    public boolean addCard(int card) {
        return addCard(card, 1);
    }

    /**
     * 添加牌集合
     *
     * @param cards 牌集合,比如 [11, 23, 33, 33, 33, 34]
     * @return true添加成功,只要有一张添加失败则全部失败
     */
    public boolean addCards(int... cards) {
        for (int card : cards) {
            int idx = getClusterIndexByCard(card);
            int lastNum = cardClusterArray[idx] + 1;
            if (lastNum > 4) {
                return false;
            }
        }
        for (int card : cards) {
            addCard(card);
        }
        return true;
    }

    /**
     * 移除num张牌
     *
     * @param card 移除的牌号
     * @param num 移除的数量
     * @return true移除成功;false移除失败
     */
    public boolean removeCard(int card, int num) {
        int idx = getClusterIndexByCard(card);
        if (cardClusterArray[idx] < num) {
            return false;
        }
        cardClusterArray[idx] -= num;
        if (cardClusterArray[idx] == 0) {
            if (idx == startIndex) {
                startIndex = 1000;
                for (int i = idx; i < cardClusterArray.length; i++) {
                    if (cardClusterArray[i] > 0) {
                        startIndex = i;
                        break;
                    }
                }
            }
            if (lastIndex == idx) {
                int start = startIndex;
                if (start >= cardClusterArray.length) {
                    start = 0;
                }
                lastIndex = -1;
                for (int i = idx; i >= start; i--) {
                    if (cardClusterArray[i] > 0) {
                        lastIndex = i;
                        break;
                    }
                }
            }
        }
        cardTotals -= num;
        return true;
    }

    /**
     * 移除一张牌
     *
     * @param card 牌号
     * @return true移除成功;false移除失败
     */
    public boolean removeCard(int card) {
        return removeCard(card, 1);
    }

    /**
     * 移除牌号对应的所有牌
     *
     * @param card 牌号
     * @return true移除成功;false移除失败
     */
    public boolean removeCardOfAll(int card) {
        int num = getCardNum(card);
        if (num >= 0) {
            return removeCard(card, num);
        }
        return true;
    }

    /**
     * 移除牌
     *
     * @param cards 需要移除的牌
     * @return true表示移除成功,只要有一张牌移除失败则整个失败
     */
    public boolean removeCards(int... cards) {
        for (int card : cards) {
            int idx = getClusterIndexByCard(card);
            if (cardClusterArray[idx] < 1) {
                return false;
            }
        }
        for (int card : cards) {
            removeCard(card);
        }
        return true;
    }

    /**
     * 是否有指定的牌
     *
     * @param card 牌号
     * @return true表示存在
     */
    public boolean hasCard(int card) {
        return getCardNum(card) > 0;
    }

    /**
     * 获取牌号对应的数量
     *
     * @param card 牌号
     * @return 牌号对应的数量
     */
    public int getCardNum(int card) {
        int idx = getClusterIndexByCard(card);
        return cardClusterArray[idx];
    }

    /**
     * 获取归类的牌数据,整除10的索引位置为保留位,不参与任何实际运算<br>
     * 数组索引从0开始,有效长度(后面全部为0)结束<br>
     * 此数组为数据副本,其中的任何数据变动都不会改变原数组<br>
     * 数组索引 / 10 + 1 表示牌类型<br>
     * 数组索引 % 10 表示牌点数<br>
     *
     * @return 归类的牌数据
     */
    public int[] getCardClusterArray() {
        int[] array = new int[getClusterValidLength()];
        System.arraycopy(cardClusterArray, 0, array, 0, array.length);
        return array;
    }

    /**
     * 根据提供的索引位置获取牌数量
     *
     * @param idx 牌归类数组中的索引位置
     * @return 牌数量
     */
    public int getCardNumByClusterIndex(int idx) {
        return cardClusterArray[idx];
    }

    /**
     * 根据索引位置定位对应的牌
     *
     * @param idx 归类牌数组中的索引位置
     * @return -1表示找不到对应的牌,否则返回牌号
     */
    public int getCardByClusterIndex(int idx) {
        if (cardClusterArray[idx] <= 0) {
            return -1;
        }
        int left = getCardLeftValueByClusterIndex(idx);
        int right = getCardRightValueByClusterIndex(idx);
        return left * 10 + right;
    }

    /**
     * 归类牌数组中起始有效索引
     *
     * @return 起始有效索引,第一个值不为0的索引位置
     */
    public int getClusterValidStartIndex() {
        if (cardTotals == 0) {
            return 1;
        }
        return startIndex;
    }

    /**
     * 归类牌数组中最终的有效索引
     *
     * @return 最终有效索引,其后的值全为0
     */
    public int getClusterValidEndIndex() {
        return lastIndex;
    }

    /**
     * 归类牌数组的有效长度<br>
     * 有效的起始索引到有效的最后索引之前的长度<br>
     *
     * @return 有效长度,因为归类数组中后面可能有很多无效的0
     */
    public int getClusterValidLength() {
        return lastIndex + 1;
    }

    /**
     * 所有牌的张数
     *
     * @return 总张数
     */
    public int getCardTotals() {
        return cardTotals;
    }

    /**
     * 获取所有的牌数据,未归类
     *
     * @return 未归类的牌数据,两位数的牌号数组
     */
    public int[] getCards() {
        if (cardTotals <= 0) {
            return null;
        }
        int len = getClusterValidLength();
        int[] cards = new int[cardTotals];
        int idx = 0;
        for (int i = getClusterValidStartIndex(); i < len; i++) {
            int left = getCardLeftValueByClusterIndex(i);
            int right = getCardRightValueByClusterIndex(i);
            int count = cardClusterArray[i];
            int card = left * 10 + right;
            for (int j = 0; j < count; j++) {
                cards[idx] = card;
                idx++;
            }
        }
        return cards;
    }

    @Override
    public HandCards clone() {
        HandCards copy = new HandCards();
        copy.cardTotals = this.cardTotals;
        copy.lastIndex = this.lastIndex;
        copy.startIndex = this.startIndex;
        if (cardClusterArray != null) {
            int[] copyCardClusterArray = new int[cardClusterArray.length];
            System.arraycopy(cardClusterArray, 0, copyCardClusterArray, 0, cardClusterArray.length);
            copy.cardClusterArray = copyCardClusterArray;
        }
        return copy;
    }

}

准备工作都做好了,怎么使用上面定义的数据结构实现平胡算法呢?平胡满足m * ABC + n * AAA + AA(其中m、n可为0)的胡牌公式,分析此公式,AA表示一对牌,则算法必然需要分析手牌中是否含有一对牌,ABC表示三张相同类型且连续的牌,AAA表示三张相同类型且牌点也相同的牌。

依据公式,我们用递归思路编写一个平胡胡牌算法(其中包含简单的测试用例):

import java.util.Arrays;


/**
 *
 * @author zkpursuit
 */
public final class AI {

    /**
     * 递归方式判断平胡
     *
     * @param cardClusterArray 牌号和牌数量的簇集对象集合
     * @param len 所有牌数量
     * @return true表示可以胡牌
     */
    private static boolean isPingHu(int[] cardClusterArray, int startIndex, int len) {
        if (len == 0) {
            return true;
        }
        int i;
        if (len % 3 == 2) {
            //移除一对(两张牌),胡牌中必须包含一对
            for (i = startIndex; i < cardClusterArray.length; i++) {
                if (cardClusterArray[i] >= 2) {
                    cardClusterArray[i] -= 2;
                    if (AI.isPingHu(cardClusterArray, startIndex, len - 2)) {
                        return true;
                    }
                    cardClusterArray[i] += 2;
                }
            }
        } else {
            //是否是顺子
            int loopCount = cardClusterArray.length - 2;
            for (i = startIndex; i < loopCount; i++) {
                int idx1 = i + 1;
                int idx2 = i + 2;
                int type1 = HandCards.getCardLeftValueByClusterIndex(i);
                int type2 = HandCards.getCardLeftValueByClusterIndex(idx1);
                int type3 = HandCards.getCardLeftValueByClusterIndex(idx2);
                if (cardClusterArray[i] > 0 && cardClusterArray[idx1] > 0 && cardClusterArray[idx2] > 0 && type1 < 4 && type2 < 4 && type3 < 4) {
                    cardClusterArray[i] -= 1;
                    cardClusterArray[idx1] -= 1;
                    cardClusterArray[idx2] -= 1;
                    if (AI.isPingHu(cardClusterArray, startIndex, len - 3)) {
                        return true;
                    }
                    cardClusterArray[i] += 1;
                    cardClusterArray[idx1] += 1;
                    cardClusterArray[idx2] += 1;
                }
            }
            //三个一样的牌(暗刻)
            for (i = startIndex; i < cardClusterArray.length; i++) {
                if (cardClusterArray[i] >= 3) {
                    cardClusterArray[i] -= 3;
                    if (AI.isPingHu(cardClusterArray, startIndex, len - 3)) {
                        return true;
                    }
                    cardClusterArray[i] += 3;
                }
            }
        }
        return false;
    }

    /**
     * 递归方式判断平胡
     *
     * @param mycards 手牌
     * @return true表示可以胡牌
     */
    public static boolean isPingHu(HandCards mycards) {
        int[] cardClusterArray = mycards.getCardClusterArray();
        int totals = mycards.getCardTotals();
        if (totals % 3 != 2) {
            return false;
        }
        return AI.isPingHu(cardClusterArray, mycards.getClusterValidStartIndex(), totals);
    }

    public static void main(String[] args) {
        HandCards handCards = new HandCards(new int[]{11, 12, 13, 22, 23, 24, 33, 33, 33, 36, 36});
        System.out.println(Arrays.toString(handCards.getCardClusterArray()));
        System.out.println(Arrays.toString(handCards.getCards()));
        for (int i = handCards.getClusterValidStartIndex(); i <= handCards.getClusterValidEndIndex(); i++) {
            int card = handCards.getCardByClusterIndex(i);
            if (card > 0) {
                int num = handCards.getCardNum(card);
                System.out.println(num + "张  " + CardType.getCardName(card));
            }
        }
        boolean bool = isPingHu(handCards);
        System.out.println("是否胡牌:" + bool);
    }

}

 

© 著作权归作者所有

kakai

kakai

粉丝 97
博文 13
码字总数 12842
作品 1
长沙
后端工程师
私信 提问
加载中

评论(21)

kakai
kakai 博主

引用来自“Qyee1666”的评论

😓 这也拿出来?
123345

还减对??

引用来自“kakai”的评论

如果123345就是表示六张牌,我想你根本就没认真看博文,是两位数表示一张牌,不是一位数,谢谢评论!如果你在非认真的情况下就下否定结论,请移驾别处,谢谢!

引用来自“Qyee1666”的评论

ABCCDE FF 胡?
请在满足平胡公式的情况下自行运行AI类中的main样例,我是部署到生产环境才发上来的(此处算法经过了稍许修改),如果的确是算法本身有问题,我会尽快修复或者删除博文,以免误人,谢谢!
kakai
kakai 博主

引用来自“Qyee1666”的评论

😓 这也拿出来?
123345

还减对??

引用来自“kakai”的评论

如果123345就是表示六张牌,我想你根本就没认真看博文,是两位数表示一张牌,不是一位数,谢谢评论!如果你在非认真的情况下就下否定结论,请移驾别处,谢谢!

引用来自“Qyee1666”的评论

ABCCDE FF 胡?
ABCCDE FF满足2 * ABC + AA,这个是肯定判胡的
Qyee1666
Qyee1666

引用来自“Qyee1666”的评论

😓 这也拿出来?
123345

还减对??

引用来自“kakai”的评论

如果123345就是表示六张牌,我想你根本就没认真看博文,是两位数表示一张牌,不是一位数,谢谢评论!如果你在非认真的情况下就下否定结论,请移驾别处,谢谢!
ABCCDE FF 胡?
Qyee1666
Qyee1666

引用来自“Qyee1666”的评论

😓 这也拿出来?
123345

还减对??

引用来自“kakai”的评论

如果123345就是表示六张牌,我想你根本就没认真看博文,是两位数表示一张牌,不是一位数,谢谢评论!如果你在非认真的情况下就下否定结论,请移驾别处,谢谢!
ABCCDE FF
kakai
kakai 博主

引用来自“javaxiaoz”的评论

发个表情打破零回复
哈哈,谢谢!谢谢!
javaxiaoz
javaxiaoz
发个表情打破零回复
kakai
kakai 博主

引用来自“风华神使”的评论

有个小错误,胡->和
哈哈哈,一般游戏用户界面中用“胡”的比较多。
风华神使
风华神使
有个小错误,胡->和
kakai
kakai 博主

引用来自“喵小强”的评论

这种规则不应该用规则引擎写的么
规则引擎没这么智能吧,还能写算法?这仅仅是平胡算法(是除红中赖子外最复杂的胡牌算法),还有很多玩法的胡牌规则,比如七小对、门清等,多种算法规则的组合用规则引擎还是可以的。
我姓杨啊
我姓杨啊

引用来自“我姓杨啊”的评论

自从玩了雀姬,都害怕这种胡法了

引用来自“kakai”的评论

不了解雀姬,呵
日本麻将,里边得有役
棋牌的胡牌算法 - QiPai

棋牌的胡牌算法,特殊牌型判断算法 1、跑胡子算法 实现: 有C++版(基于递归,回溯)。 lua版(基于查表)。 2、高效的麻将胡牌算法,能处理任意张赖子: 速度:1S左右处理100万次每次四个赖子...

袁峰云
2017/12/12
4.5K
3
运城麻将游戏开发_运城麻将规则

  运城麻将起源于山西省西南部,深受运城人的喜爱,当地也有人将之称为贴金麻将。运城麻将具有独特的金牌,上金和包牌等游戏规则,玩法更复杂,也更刺激。想进行运城麻将游戏开发,就需要了...

网狐棋牌开发
2017/12/22
0
0
血战麻将算法

算法简介 本文的麻将算法不是按人工智能的方式进行讲解,本人从事游戏服务器开发。所以该算法主要用于一般的麻将游戏机器人,主要是让机器人具有正常操作选择。算法无法与现实生活中的麻将高...

shezjl
2016/03/25
1.6K
2
中国麻将:世界上最早的区块链项目

中国麻将:世界上最早的区块链项目 最近区块链这个玩意又被市场搞的很是火热,相信大部分人都不太清楚这玩意到底是怎么样的一个概念,它来了,它来了,它到底是啥~ 国家都开始发文支持了,下...

Devinxtw
03/31
0
0
《数据科学入门》,一桌麻将我就数据科学入了门

在说这本《数据科学入门》之前,我们先来讨论一下啥叫入门?我觉得入门其实就是做两件事:一是带你看看这物件是个什么鬼,也就是进门前带你在门口瞅一瞅。二是给你指条该怎么整地路,也就是告...

近远0607
2018/11/27
0
0

没有更多内容

加载失败,请刷新页面

加载更多

广州哪里有开餐饮费发票

广州开餐饮费发票发票电薇13564998196陈晨100 % 真。从主业来看,2019年众诚保险围绕车险业务采取增设分支机构、加强合作、优化用户体验等动作,但综合成本率仍有所上行,业内指出,车险的价...

枅票微fp2090
26分钟前
19
0
深圳哪里有开餐饮费发票

深圳开餐饮费发票发票电薇13564998196陈晨100 % 真。从主业来看,2019年众诚保险围绕车险业务采取增设分支机构、加强合作、优化用户体验等动作,但综合成本率仍有所上行,业内指出,车险的价...

枅票嶶fp2090
29分钟前
25
0
略谈分布式系统中的容器设计模式

本文作者:zytan_cocoa 略谈分布式系统中的容器设计模式 谭中意 2020/3/5 前言:云原生(Cloud Native)不仅仅是趋势,更是现在进行时,它是构建现代的,可弹性伸缩的,快速迭代的计算网络服...

百度开发者中心
03/11
21
0
OSChina 周三乱弹 —— 小姐姐的领带有点带歪了,请帮忙正一下

Osc乱弹歌单(2020)请戳(这里) 【今日歌曲】 @薛定谔的兄弟 :分享洛神有语创建的歌单「我喜欢的音乐」: 《アイタクテ -voice & piano-》- 和紗 手机党少年们想听歌,请使劲儿戳(这里) ...

小小编辑
今天
25
0
对象名称前的单下划线和双下划线是什么意思?

问题: Can someone please explain the exact meaning of having leading underscores before an object's name in Python? 有人可以解释一下在Python中对象名称前加下划线的确切含义吗? ......

技术盛宴
今天
29
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部