麻将胡牌算法的Java实现

最近一个棋牌游戏项目中涉及对麻将胡牌的判定,网上搜了搜虽然看到一些算法,但是感觉都不尽如人意,一般麻将的胡牌为1对和4组三张牌的连牌,所以在网上搜到的算法往往都死死的为了这个目的来实现,而且多数没有考虑到对百塔牌的支持,下面贴上代码:

/*
 * To change this license header, choose License Headers in Project Properties.
 * To change this template file, choose Tools | Templates
 * and open the template in the editor.
 */
package main;

import java.util.Arrays;

/**
 * 单张麻将牌
 *
 * @author 奔跑 QQ:361817468
 */
public class MahjongTile
{

    public static int MAHJONG_TILE_TYPE_TEN_THOUSAND = 1;
    public static int MAHJONG_TILE_TYPE_PIE = 2;
    public static int MAHJONG_TILE_TYPE_STRIP = 3;
    public static int MAHJONG_TILE_TYPE_WIND = 4;
    public static int MAHJONG_TILE_TYPE_MESS = 5;
    public static int MAHJONG_TILE_TYPE_FLOWER = 6;
    
    /**
     * 标准麻将的各种牌的名称,该名称为一个三维数组,第一维为各套独立的名称
     * 第二维为每套名称中的不同类别,例如万和桶九属于不同类型的牌
     * 第三维维具体的名称
     */
    public final static String[][][] STANDARD_MAHJONG_NAMES = {
    
        new String[][]{
            {"一万","二万","三万","四万","五万","六万","七万","八万","九万"},
            {"一桶","二桶","三桶","四桶","五桶","六桶","七桶","八桶","九桶"},
            {"一条","二条","三条","四条","五条","六条","七条","八条","九条"},
            {"东风","南风","西风","北风"},
            {"红中","发财","白板"},
            {"春","夏","秋","冬","梅","兰","竹","菊"}
        },
        new String[][]{
            {"一万","二万","三万","四万","五万","六万","七万","八万","九万"},
            {"一饼","二饼","三饼","四饼","五饼","六饼","七饼","八饼","九饼"},
            {"一条","二条","三条","四条","五条","六条","七条","八条","九条"},
            {"东风","南风","西风","北风"},
            {"红中","发财","白板"},
            {"春","夏","秋","冬","梅","兰","竹","菊"}
        }
    };

    private final int type;
    private final int typeId;
    private final int uniqueId;

    public MahjongTile(String name) throws MahjongTileInitWrongTypeAndTypeIdException, MahjongTileInitWrongNameException
    {
        for (String[][] standardMahjongName : STANDARD_MAHJONG_NAMES)
        {
            for (int j = 0; j < standardMahjongName.length; j++)
            {
                for (int k = 0; k < standardMahjongName[j].length; k++)
                {
                    if (standardMahjongName[j][k].equals(name))
                    {
                        this.type = j + 1;
                        this.typeId = k + 1;
                        this.uniqueId = computeUniqueId(type, typeId);
                        return;
                    }
                }
            }
        }
        throw new MahjongTileInitWrongNameException(name);
    }
    
    public MahjongTile(int type, int typeId) throws MahjongTileInitWrongTypeAndTypeIdException
    {
        this.uniqueId = computeUniqueId(type, typeId);
        this.type = type;
        this.typeId = typeId;
    }

    private void initCheck(int type, int typeId) throws MahjongTileInitWrongTypeAndTypeIdException
    {
        if (STANDARD_MAHJONG_NAMES[0].length < type || type < 1)
        {
            throw new MahjongTileInitWrongTypeAndTypeIdException(type, typeId, true);
        }
        else if (STANDARD_MAHJONG_NAMES[0][type - 1].length < typeId || typeId < 1)
        {
            throw new MahjongTileInitWrongTypeAndTypeIdException(type, typeId, false);
        }
    }

    private int computeUniqueId(int type, int typeId) throws MahjongTileInitWrongTypeAndTypeIdException
    {
        initCheck(type, typeId);
        if (type == MAHJONG_TILE_TYPE_TEN_THOUSAND)
        {
            return typeId;
        }
        else if (type == MAHJONG_TILE_TYPE_PIE)
        {
            return typeId + 9;
        }
        else if (type == MAHJONG_TILE_TYPE_STRIP)
        {
            return typeId + 18;
        }
        else if (type == MAHJONG_TILE_TYPE_WIND)
        {
            return typeId + 27;
        }
        else if (type == MAHJONG_TILE_TYPE_MESS)
        {
            return typeId + 31;
        }
        else
        {
            return typeId + 34;
        }
    }

    public int getType()
    {
        return type;
    }

    public int getTypeId()
    {
        return typeId;
    }

    public int getUniqueId()
    {
        return typeId;
    }

    public boolean isCanTwo(MahjongTile mahjongTile)
    {
        if (isCanAny() || mahjongTile.isCanAny())
        {
            return true;
        }
        else
        {
            return uniqueId == mahjongTile.uniqueId;
        }
    }

    private boolean isIdLink(int id1, int id2, int id3)
    {
        int[] ids =
        {
            id1, id2, id3
        };
        Arrays.sort(ids);
        if (ids[2] - ids[1] != 1)
        {
            return false;
        }
        else if (ids[1] - ids[0] != 1)
        {
            return false;
        }
        return true;
    }

    public boolean isCanThree(MahjongTile mahjongTileOne, MahjongTile mahjongTileTwo)
    {
        if (type == mahjongTileOne.type && type == mahjongTileTwo.type)
        {
            if (typeId == mahjongTileOne.typeId && typeId == mahjongTileTwo.typeId)
            {
                return true;
            }
            else if (isIdLink(typeId, mahjongTileOne.typeId, mahjongTileTwo.typeId) && type != MAHJONG_TILE_TYPE_WIND && type != MAHJONG_TILE_TYPE_MESS && type != MAHJONG_TILE_TYPE_FLOWER)
            {
                return true;
            }

        }
        if (isCanAny())
        {
            if (mahjongTileOne.isCanAny() || mahjongTileTwo.isCanAny())
            {
                return true;
            }
            else if (Math.abs(mahjongTileOne.typeId - mahjongTileTwo.typeId) <= 2 && mahjongTileOne.type == mahjongTileTwo.type)
            {
                return true;
            }
        }
        else if (mahjongTileOne.isCanAny())
        {
            if (isCanAny() || mahjongTileTwo.isCanAny())
            {
                return true;
            }
            else if (Math.abs(typeId - mahjongTileTwo.typeId) <= 2 && type == mahjongTileTwo.type)
            {
                return true;
            }
        }
        else if (mahjongTileTwo.isCanAny())
        {
            if (mahjongTileOne.isCanAny() || isCanAny())
            {
                return true;
            }
            else if ((Math.abs(typeId - mahjongTileOne.typeId) <= 2) && type == mahjongTileOne.type)
            {
                return true;
            }
        }

        return false;
    }

    public boolean isCanAny()
    {
        if (type == 1 && typeId == 9)
        {
            return true;
        }
        return false;
    }

    @Override
    public String toString()
    {
        String name = STANDARD_MAHJONG_NAMES[0][type - 1][typeId - 1];
        if (isCanAny())
        {
            name = name + "(百搭)";
        }
        return name;
    }

    
}

这是对单张麻将牌进行的一个简单封装,比较简单硬性的将麻将对象设为两个主要属性,一个是类型,一个是类型编号,比如万,比如桶就属于不同的类型,而1万,3万这样的同属于万类型下的不同类型编号,同时给了一个 STANDARD_MAHJONG_NAMES 三维数组常量,用来封装不同格式的麻将名称,比如有些地方二桶就二饼,再比如中文,英文的区别等等,就不多说了,这个类主要提供的核心功能为isCanTwo(MahjongTile mahjongTile),isCanThree(MahjongTile mahjongTileOne,
 MahjongTile mahjongTileTwo),isCanAny() 三个方法,顾名思义isCanTwo用来判断是否可以跟另外一张麻将牌结成对子,isCanThree 用来判断是否可以跟另外两张麻将结成趟,至于isCanAny()是用来判断该麻将是否具备百搭属性,我的代码中对于isCanAny() 的实现是随便写的,只是假设9万为百搭,至于为什么 type == 1 && typeId == 9 代表的是9万,自己看代码。


下面再贴两个无关紧要的异常类:

/*
 * To change this license header, choose License Headers in Project Properties.
 * To change this template file, choose Tools | Templates
 * and open the template in the editor.
 */
package main;

/**
 *
 * @author 奔跑 QQ:361817468
 */
public class MahjongTileInitWrongNameException extends Exception
{
    private final String wrongName;
    public MahjongTileInitWrongNameException(String wrongName)
    {
        this.wrongName = wrongName;
    }
    
    public String getWrongName()
    {
        return wrongName;
    }
    
    public String[][][] standardMahjongNames()
    {
        return MahjongTile.STANDARD_MAHJONG_NAMES;
    }
}
/*
 * To change this license header, choose License Headers in Project Properties.
 * To change this template file, choose Tools | Templates
 * and open the template in the editor.
 */
package main;

/**
 *
 * @author 奔跑 QQ:361817468
 */
public class MahjongTileInitWrongTypeAndTypeIdException extends Exception
{
    private final int type;
    private final int typeId;
    private final boolean isTypeWrong;
    
    public MahjongTileInitWrongTypeAndTypeIdException(int type,int typeId,boolean isTypeWrong)
    {
        this.type = type;
        this.typeId = typeId;
        this.isTypeWrong = isTypeWrong;
    }
    
    public int type()
    {
        return type;
    }
    
    public int typeId()
    {
        return typeId;
    }
    
    public boolean isTypeWrong()
    {
        return isTypeWrong;
    }
}
这两个类很简单,主要是当MahjongTile对象初始化的时候参数是否正确的判断,比如用十万初始化肯定要抛异常的.在这里就不多说了。


下面贴上核心工具类:

/*
 * To change this license header, choose License Headers in Project Properties.
 * To change this template file, choose Tools | Templates
 * and open the template in the editor.
 */
package main;

/**
 *
 * @author 奔跑 QQ:361817468
 */
public class MahjongStaticTool
{
private static MahjongTile[] removeSomeMahjongTiles(MahjongTile[] mahjongTiles, int[] indexs)
    {
        int lenNew = mahjongTiles.length - indexs.length;
        if (lenNew > 0)
        {
            MahjongTile[] mahjongTilesNew = new MahjongTile[lenNew];
            int index = 0;
            for (int i = 0; i < mahjongTiles.length; i++)
            {
                boolean isAppend = true;
                for (int j = 0; j < indexs.length; j++)
                {
                    if (i == indexs[j])
                    {
                        isAppend = false;
                        break;
                    }
                }
                if (isAppend)
                {
                    mahjongTilesNew[index] = mahjongTiles[i];
                    index++;
                }
            }
            return mahjongTilesNew;
        }
        return null;
    }

    //从数组长度为arrayLen的整形数组中任意抽取两个元素,把所有可能的组合的索引列成一个二位数组返回出来
    private static int[][] siphonTwoIndexs(int arrayLen)
    {
        int len = (arrayLen * (arrayLen - 1)) / 2;
        if (len > 0)
        {
            int[][] indexs = new int[len][2];
            int index = 0;
            for (int i = 0; i < arrayLen; i++)
            {
                for (int j = (i + 1); j < arrayLen; j++)
                {
                    indexs[index][0] = i;
                    indexs[index][1] = j;
                    index++;
                }
            }

            return indexs;
        }
        else
        {
            return null;
        }
    }

    //从数组长度为arrayLen的整形数组中任意抽取两个元素,把所有可能的组合的索引列成一个二位数组返回出来
    private static int[][] siphonThreeIndexs(int arrayLen)
    {
        int len = (arrayLen * (arrayLen - 1) * (arrayLen - 2)) / 6;
        if (len > 0)
        {
            int[][] indexs = new int[len][3];
            int index = 0;
            for (int i = 0; i < arrayLen; i++)
            {
                for (int j = (i + 1); j < arrayLen; j++)
                {
                    for (int k = (j + 1); k < arrayLen; k++)
                    {
                        indexs[index][0] = i;
                        indexs[index][1] = j;
                        indexs[index][2] = k;
                        index++;
                    }
                }
            }
            return indexs;
        }
        else
        {
            return null;
        }
    }

    private static MahjongTile[][] appendSomeMahjongTiles(MahjongTile[][] saveMahjongTileses, MahjongTile[] mahjongTiles)
    {
        if (saveMahjongTileses == null)
        {
            MahjongTile[][] mahjongTilesesReturn = new MahjongTile[1][];
            mahjongTilesesReturn[0] = mahjongTiles;
            return mahjongTilesesReturn;
        }
        else
        {
            MahjongTile[][] mahjongTilesesReturn = new MahjongTile[saveMahjongTileses.length + 1][];
            System.arraycopy(saveMahjongTileses, 0, mahjongTilesesReturn, 0, saveMahjongTileses.length);
            mahjongTilesesReturn[saveMahjongTileses.length] = mahjongTiles;
            return mahjongTilesesReturn;
        }
    }

    public static MahjongTile[][] tryCombination(MahjongTile[] mahjongTiles, int twoNum, int threeNum)
    {
        return MahjongStaticTool.tryCombination(mahjongTiles, twoNum, threeNum, null);
    }

    private static MahjongTile[][] tryCombination(MahjongTile[] mahjongTiles, int twoNum, int threeNum, MahjongTile[][] saveMahjongTileses)
    {
        if (mahjongTiles == null)
        {
            if (twoNum == 0 && threeNum == 0)
            {
                return saveMahjongTileses;
            }
            else
            {
                return null;
            }
        }
        if (mahjongTiles.length == ((twoNum * 2) + (threeNum * 3)))
        {
            if (threeNum > 0)
            {
                int[][] indexs = siphonThreeIndexs(mahjongTiles.length);
                if (indexs == null)
                {
                    return null;
                }
                
                for (int[] index : indexs)
                {
                    if (mahjongTiles[index[0]].isCanThree(mahjongTiles[index[1]], mahjongTiles[index[2]]))
                    {
                        MahjongTile[][] saveMahjongTilesesCache = appendSomeMahjongTiles(saveMahjongTileses, new MahjongTile[]{mahjongTiles[index[0]], mahjongTiles[index[1]], mahjongTiles[index[2]]});
                        MahjongTile[][] mahjongTilesesReturn = MahjongStaticTool.tryCombination(removeSomeMahjongTiles(mahjongTiles, new int[]{index[0], index[1], index[2]}), twoNum, threeNum - 1, saveMahjongTilesesCache);
                        if (mahjongTilesesReturn != null)
                        {
                            return mahjongTilesesReturn;
                        }
                    }
                }
            }
            else if (twoNum > 0)
            {
                int[][] indexs = siphonTwoIndexs(mahjongTiles.length);
                if (indexs == null)
                {

                    return null;
                }
                for (int[] index : indexs)
                {
                    if (mahjongTiles[index[0]].isCanTwo(mahjongTiles[index[1]]))
                    {
                        MahjongTile[][] saveMahjongTilesesCache = appendSomeMahjongTiles(saveMahjongTileses, new MahjongTile[]{mahjongTiles[index[0]], mahjongTiles[index[1]]});
                        MahjongTile[][] mahjongTilesesReturn = MahjongStaticTool.tryCombination(removeSomeMahjongTiles(mahjongTiles, new int[]{index[0], index[1]}), twoNum - 1, threeNum, saveMahjongTilesesCache);
                        if (mahjongTilesesReturn != null)
                        {
                            return mahjongTilesesReturn;
                        }
                    }
                }
            }
            else
            {
                return saveMahjongTileses;
            }
        }
        return null;
    }

    public static void main(String[] args) throws MahjongTileInitWrongTypeAndTypeIdException, MahjongTileInitWrongNameException
    {
        MahjongTile[] mahjongTiles = new MahjongTile[]
        {
            new MahjongTile(1, 9),
            new MahjongTile(1, 1),
            new MahjongTile(1, 1),
            new MahjongTile(1, 2),
            new MahjongTile(1, 3),
            
            new MahjongTile(1, 2),
            new MahjongTile(1, 3),
            new MahjongTile(1, 4),
            new MahjongTile(1, 2),
            new MahjongTile(1, 3),
            new MahjongTile(1, 4),
            new MahjongTile("九万"),
            new MahjongTile(2, 7),
            new MahjongTile(2, 8),

        };
        
        System.out.println("检查所有下列牌:");
        for (int i = 0; i < mahjongTiles.length; i++)
        {
            if (i != 0)
            {
                System.out.print(",");
            }
            System.out.print(mahjongTiles[i]);
        }
        System.out.println("");
        MahjongTile[][] mahjongTileses = tryCombination(mahjongTiles, 1, 4);
        if (mahjongTileses != null)
        {
            System.out.println("检查通过!");
            System.out.println("组合结果如下:");
            int twoIndex = 1;
            int threeIndex = 1;
            for (MahjongTile[] mahjongTilesRow : mahjongTileses)
            {
                if (mahjongTilesRow.length == 2)
                {
                    System.out.print("第"+twoIndex+"对组合:");
                    for (int j = 0; j < mahjongTilesRow.length; j++)
                    {
                        
                        if (j != 0)
                        {
                            System.out.print(",");
                        }
                        System.out.print(mahjongTilesRow[j]);
                    }
                    System.out.println("");
                    twoIndex ++;
                }
                else if (mahjongTilesRow.length == 3)
                {
                    System.out.print("第"+threeIndex+"趟组合:");
                    for (int j = 0; j < mahjongTilesRow.length; j++)
                    {
                        
                        if (j != 0)
                        {
                            System.out.print(",");
                        }
                        System.out.print(mahjongTilesRow[j]);
                    }
                    System.out.println("");
                    threeIndex ++;
                }
            }
        }
        else
        {
            System.out.println("检查未通过!");
        }
    }
}


这个类是用来判断胡牌的核心类,它用来判断胡牌的方法为 public static MahjongTile[][] tryCombination(MahjongTile[] mahjongTiles, int twoNum, int threeNum) 这个,
我们看这个方法的内部其实是对另外一个同名方法的 调用,我们来看这个方法的参数,mahjongTiles 这个不用说是带进去判断的麻将对象的数组,twoNum和threeNum的意思分别是需要判断参数mahjongTiles中是否有且仅有twoNum个对子和threeNum个趟,那么一般胡牌判断的话,这两个参数自然一个是1,一个是4了,就是判断麻将是否是1对+4趟,那么为什么这里要这么写呢,主要为了方便扩展,比如很多地方有7小对可以胡牌的,再比如打牌过程中,碰了,杠了的牌,可以直接不去考虑,只要考虑出去碰,杠外还缺几对和几趟就可以胡牌,这样一来,这个方法就显得很灵活,不拘泥于1对+4趟,那么这个方法返回的是一个MahjongTile对象的二维数组是什么意思呢,意思就是当返回为空时,说明判断不成立,当返回的是实打实的数组的时候就说明一定满足你带进去的参数twoNum个对子和threeNum个趟,同时这个实打实的数组就是按照参数要求的组合,比如您带进去twoNum为1threeNum为4的话,那么如果能胡牌,返回的一定是一个第一维长度为5的二维数组,同时第一个元素的长度又为2,对应的是对子,下面的4个元素的长度为3对应的是4趟。
 这个方法的核心思想就是递归,每一次执行就找出来一个对子,或者一组三张联牌,然后把还需要的组合递归下去,具体算法可以仔细看代码,本人写代码不太喜欢写太多注释,好在代码比较短,容易懂。

在该类的public static void main(String[] args)方法下有判断胡牌的实例代码。


原创文章,转载请注明原地址

---------------------
作者:奔跑猿 
来源:CSDN 
原文:https://blog.csdn.net/benpao2015y/article/details/53970565?utm_source=copy 
版权声明:本文为博主原创文章,转载请附上博文链接!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值