最近一個棋牌遊戲項目中涉及對麻将胡牌的判定,網上搜了搜雖然看到一些算法,但是感覺都不盡如人意,一般麻将的胡牌為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)方法下有判斷胡牌的執行個體代碼。
原創文章,轉載請注明原位址