胡牌
/// <summary>
/// 通用型字牌胡牌算法 by:黄敏
/// </summary>
public class huPaiSuanFa
{
/* 通用型字牌胡牌算法 解释文档 by:黄敏 2017/8/20
一:流程
假设 手牌为 2 7 10 2 7 10 贰 柒 拾
(这个手牌是已经将目标牌加入其中了,并且已经将手牌中3张以上的牌全部移除到明牌了
因为各地字牌游戏的胡息,胡牌规则不同,所以必须将它们剔除,本算法仅仅计算各种允许的吃牌规则后产生的吃牌的坎子。也是最难的部分)
首先是检查数量合法性,必须是3的倍数或者3的倍数+2才能胡牌。
1.1:首先将所有手牌可能出现的 有效组合(手牌能满足的) 保存下来(我们叫做 手牌的组合字典 把)。如2-有 22贰 和 2710 以及 22(将牌) 三种组合
1.2:根据他们各自组合的数量,选择最少的一个,这里是 贰 或者 柒 拾 ,我们选择 贰 吧
1.3: 贰有2种组合-一种是 22贰和贰 柒 拾,我们遍历它们。当然,我们先实行的还是22贰-等它结束再实行贰 柒 拾,这是第一层
1.4: 我们将 22贰 这个组合 保存起来并准备往下一层传递(如果有的话),然后从手牌列表(复制的,要保证第一层的参数不被修改)中删除
那么我们的手牌变成了 7 10 7 10 柒 拾。
1.5:OK,下面我们检查手牌,发现手牌还有6张牌,那么再次往下一层传递。这是 1-1层(第一个1代表第一层的第1个组合选择,第2个1代表第2层的第一个组合选择)
1.6:然后我们来到了1-1层,再次生成 手牌的组合字典,那么这个时候 7 只有1种组合了。其他的也只有一种组合了!
这样,我们就可以将他们加入 上一层传递下来的组合(22贰),形成一个列表,将其输出或者保存到某地,这样第2层结束了。那么我们就会返回第一层。
1.7:回到第一层,因为第一种组合我们已经结束,那么就是第2种组合了。这一次就是 2-0层了
1.8:…………按照以上套路走一遍就可以了。写说明好麻烦,那么我就懒得写了…………
1-0层 -》1-1层 -》1-1-1层 -》1-1-2层 -》1-2-0层 -》1-2-1层 -》1-2-2层 -》2-0层………………就是按照这个方式递归的。有点复杂,最好是按照我上面的文档走一遍会帮助理解
*/
#region @@@@@@@字牌胡牌算法接口
/// <summary>
/// 获得所有胡牌的组合列表,尚未做胡息计算,目标牌已经加入手牌或者明牌,需调用前手动定义
/// </summary>
/// <param name="handTileList">已经加入目标牌的手牌列表</param>
/// <param name="outVar">输出的所有胡牌组合的列表</param>
/// <returns></returns>
public bool GetAllHuPaiMeldList(List<PaoHuZi> handTileList, out List<List<List<PaoHuZi>>> outVar)
{
#region ***********数据准备
//胡牌的序列,仅手牌的序列
outVar = new List<List<List<PaoHuZi>>>();
//先复制一次,避免不小心修改到原始数据
List<PaoHuZi> handTileList_Temp = new List<PaoHuZi>(handTileList);
#endregion
#region *************手牌数量合法性检查
if (handTileList.Count <= 0)//没有牌被输入进来
{
log("错误:没有牌被输进来或者全部完结");
return false;
}
//先检查手牌的数量,如果是3的倍数或者3的倍数+2,那么就继续检查,否则返回
if (handTileList.Count % 3 != 0 && handTileList.Count % 3 != 2)
{
log("错误:牌的数量不能组成坎子-》" + handTileList.Count);
return false;
}
#endregion
#region *********将手牌转为字典格式,方便获取某牌的数量
//根据手牌列表生成手牌数量的字典
Dictionary<PaoHuZi, int> handTileCountDic = new Dictionary<PaoHuZi, int>();
foreach (var i in handTileList_Temp)
{
if (handTileCountDic.ContainsKey(i))
{
handTileCountDic[i]++;
}
else
{
handTileCountDic.Add(i, 1);
}
}
#endregion
#region ********极端情况快速返回
//极端情况,只剩下2张相同的牌了,那么就返回将牌坎子
if (handTileList_Temp.Count == 2)
{
if (handTileList_Temp[0] == handTileList_Temp[1])//这2张牌是一样的牌
{
log("正确,只剩下2张相同牌了-》" + handTileList_Temp[0]);
List<PaoHuZi> tileMeld = new List<PaoHuZi>();
tileMeld.Add(handTileList_Temp[0]);
tileMeld.Add(handTileList_Temp[1]);
List<List<PaoHuZi>> tempHuPaiMeldList = new List<List<PaoHuZi>>();
tempHuPaiMeldList.Add(tileMeld);
outVar.Add(tempHuPaiMeldList);
return true;
}
else//这2张牌不一样,所以无法构成牌组。
{
log("错误,只剩下2张牌了,但不一样-》" + handTileList_Temp[0] + " " + handTileList_Temp[1]);
outVar.Clear();
return false;
}
}
//极端情况,只剩下3张牌了
if (handTileList_Temp.Count == 3)
{
//查找这3张牌是否能够构成坎子牌
//先得到能够和第一张牌构成坎子的所有另外2张牌 的列表,
List<PaoHuZi[]> canChowList = GetChowRule(handTileList_Temp[0]);
bool canChow = false;
//检查手牌中的牌是否符合中间的某一规则
foreach (var i in canChowList)
{
int needTileCount = 1;
if (i[0] == i[1])//如果这2张牌是一样的那么就需要牌组里面有2张牌
{
needTileCount = 2;
}
if (handTileCountDic.ContainsKey(i[0]) && handTileCountDic[i[0]] >= needTileCount
&& handTileCountDic.ContainsKey(i[1]) && handTileCountDic[i[1]] >= needTileCount)//如果手牌中的牌数量和类型都符合,那么,他就能构成一坎牌
{
List<PaoHuZi> tileMeld = new List<PaoHuZi>();
tileMeld.Add(handTileList_Temp[0]);
tileMeld.Add(handTileList_Temp[1]);
tileMeld.Add(handTileList_Temp[2]);
List<List<PaoHuZi>> tempHuPaiMeldList = new List<List<PaoHuZi>>();
tempHuPaiMeldList.Add(tileMeld);
outVar.Add(tempHuPaiMeldList);
canChow = true;
}
}
//符合的话,就返回true
if (canChow)
{
log("正确,只剩下3张牌了,并且能吃牌");
return true;
}
else
{
log("错误,只剩下3张牌了,不吃牌");
outVar.Clear();
return false;
}
}
#endregion
#region ****检查普通情况
//开始排序剩下的手牌
if (handTileList_Temp.Count > 3)
{
#region ------生成字典,储存每一张手牌能够吃牌的组合,如果拥有为0的手牌,那么就立刻返回
//生成一个字典,用来储存每一张手牌能够吃牌的组合
Dictionary<PaoHuZi, List<PaoHuZi[]>> allTileCanChowDic = new Dictionary<PaoHuZi, List<PaoHuZi[]>>();
//获得每一张牌能够吃牌的组合
foreach (PaoHuZi i in handTileList_Temp)
{
if (!allTileCanChowDic.ContainsKey(i))
{//生成一个新的手牌列表,并且将要计算的目标牌从中移除
List<PaoHuZi> newHandTileList_Temp = new List<PaoHuZi>(handTileList_Temp);
newHandTileList_Temp.Remove(i);
List<PaoHuZi[]> allChowMeldForTarget = GetCanChowMeldOnHandTile(i, newHandTileList_Temp);
allTileCanChowDic.Add(i, allChowMeldForTarget);
}
}
//找找看有没有组合数为0的牌
int zeroCount = allTileCanChowDic.Where(x => x.Value.Count <= 0).Count();
//如果有就返回fasl,证明无法构成胡牌列表
if (zeroCount >= 1)
{
return false;
}
#endregion
#region --------开始调用递归函数进行排列
selectMeld(new List<List<PaoHuZi>>(), outVar, handTileList_Temp);
#endregion
#region ---获得排列的方法后,如果发现没有一个方法,那么就代表失败
if (outVar.Count >= 1)
{
return true;
}
#endregion
}
#endregion
return false;
}
#endregion
#region @@@@@需要手动改写的辅助方法
/// <summary>
/// 获得根据吃牌规则,获得能与目标牌构成坎子的另外2张牌的列表-可以根据种类不同进行修改
/// </summary>
/// <param name="targetIndex"></param>
/// <returns></returns>
private List<PaoHuZi[]> GetChowRule(PaoHuZi targetTile)
{//定义了哪些牌可以和目标牌构成 坎子。也就是吃牌的规则
//有规律的好定义。
//没有规律的参见最下面的 2 7 10来定义
//所有定义的规则,不需要考虑 另外2张牌是否合法,因为合法性检查会在后面做
List<PaoHuZi[]> returnVar = new List<PaoHuZi[]>();
//将牌
if (true)
{
PaoHuZi[] chowRuleElement = new PaoHuZi[1];
chowRuleElement[0] = targetTile;
returnVar.Add(chowRuleElement);
}
//如果-2 ,-1连续规则可用
if (true)
{
PaoHuZi[] chowRuleElement = new PaoHuZi[2];
if (GetChowRuleElement(targetTile - 2, targetTile - 1, out chowRuleElement))
{
returnVar.Add(chowRuleElement);
}
}
//如果-1 ,+1连续规则可用
if (true)
{
PaoHuZi[] chowRuleElement = new PaoHuZi[2];
if (GetChowRuleElement(targetTile + 1, targetTile - 1, out chowRuleElement))
{
returnVar.Add(chowRuleElement);
}
}
//如果+1 ,+2连续规则可用
if (true)
{
PaoHuZi[] chowRuleElement = new PaoHuZi[2];
if (GetChowRuleElement(targetTile + 1, targetTile + 2, out chowRuleElement))
{
returnVar.Add(chowRuleElement);
}
}
//如果+10 ,+10规则可用
if (true)
{
PaoHuZi[] chowRuleElement = new PaoHuZi[2];
if (GetChowRuleElement(targetTile + 10, targetTile + 10, out chowRuleElement))
{
returnVar.Add(chowRuleElement);
}
}
//如果+0 ,+10规则可用
if (true)
{
PaoHuZi[] chowRuleElement = new PaoHuZi[2];
if (GetChowRuleElement(targetTile, targetTile + 10, out chowRuleElement))
{
returnVar.Add(chowRuleElement);
}
}
//如果+0 ,-10规则可用
if (true)//如果+0 ,-10规则可用
{
PaoHuZi[] chowRuleElement = new PaoHuZi[2];
if (GetChowRuleElement(targetTile, targetTile - 10, out chowRuleElement))
{
returnVar.Add(chowRuleElement);
}
}
//如果-10 ,-10规则可用
if (true)
{
PaoHuZi[] chowRuleElement = new PaoHuZi[2];
if (GetChowRuleElement(targetTile - 10, targetTile - 10, out chowRuleElement))
{
returnVar.Add(chowRuleElement);
}
}
//特殊规则 2 7 10
if (true)
{
PaoHuZi[] chowRuleElement = new PaoHuZi[2];
switch (targetTile)
{
case (PaoHuZi)2:
if (GetChowRuleElement((PaoHuZi)7, (PaoHuZi)10, out chowRuleElement))
{
returnVar.Add(chowRuleElement);
};
break;
case (PaoHuZi)7:
if (GetChowRuleElement((PaoHuZi)2, (PaoHuZi)10, out chowRuleElement))
{
returnVar.Add(chowRuleElement);
};
break;
case (PaoHuZi)10:
if (GetChowRuleElement((PaoHuZi)2, (PaoHuZi)7, out chowRuleElement))
{
returnVar.Add(chowRuleElement);
};
break;
case (PaoHuZi)12:
if (GetChowRuleElement((PaoHuZi)17, (PaoHuZi)20, out chowRuleElement))
{
returnVar.Add(chowRuleElement);
};
break;
case (PaoHuZi)17:
if (GetChowRuleElement((PaoHuZi)12, (PaoHuZi)20, out chowRuleElement))
{
returnVar.Add(chowRuleElement);
};
break;
case (PaoHuZi)20:
if (GetChowRuleElement((PaoHuZi)12, (PaoHuZi)17, out chowRuleElement))
{
returnVar.Add(chowRuleElement);
};
break;
}
}
return returnVar;
}
/// <summary>
/// 本类的Log输出,记得改写
/// </summary>
/// <param name="logstring"></param>
private void log(string logstring)
{
Console.WriteLine(logstring);
}
#endregion
#region @@@@@@@@@@@@@@@字牌胡牌算法辅助方法
/// <summary>
/// 根据手牌选择能够组成胡牌 的组合。未做胡息判断,需提前移除3张以上相同牌再输入手牌
/// </summary>
/// <param name="allMeldCount">需要提取的坎子数量</param>
/// <param name="alreadySelectMeldList">递归中使用</param>
/// <param name="allMeldList">要输出的列表</param>
/// <param name="handTile">手牌,已经移除3张以上相同牌</param>
/// <param name="allTileCanChowDic">之前生成的吃牌组合</param>
private void selectMeld(List<List<PaoHuZi>> alreadySelectMeldList,List< List<List<PaoHuZi>>> allMeldList,List<PaoHuZi>handTile)
{
#region ****数据准备-复制手牌列表并排序
//复制手牌列表,避免修改到原始数据
List<PaoHuZi> handTileTemp = new List<PaoHuZi>(handTile);
handTileTemp.Sort();
#endregion
#region ******根据现有的手牌列表 获得 每张牌组合 的字典
//生成一个字典,用来储存每一张手牌能够吃牌的组合
Dictionary<PaoHuZi, List<PaoHuZi[]>> allTileCanChowDic = new Dictionary<PaoHuZi, List<PaoHuZi[]>>();
//获得每一张牌能够吃牌的组合
foreach (PaoHuZi i in handTileTemp)
{
if (!allTileCanChowDic.ContainsKey(i))
{//生成一个新的手牌列表,并且将要计算的目标牌从中移除
List<PaoHuZi> newHandTileList_Temp = new List<PaoHuZi>(handTileTemp);
newHandTileList_Temp.Remove(i);
List<PaoHuZi[]> allChowMeldForTarget = GetCanChowMeldOnHandTile(i, newHandTileList_Temp);
allTileCanChowDic.Add(i, allChowMeldForTarget);
}
}
#endregion
#region *******选择一张牌作为目标牌,从它的组合开始计算,可以有效的减少计算量
PaoHuZi targetTile = PaoHuZi.EPH_None;
//查找最少组合的数量
int minCount = allTileCanChowDic.Min(x => x.Value.Count);
//如果最少组合的数量为0,代表有手牌不能组合,那么就返回
if (minCount == 0)
{
return;
}
//找到最少组合的手牌,并以它为目标牌开始计算组合
List<PaoHuZi> minCoutTileList= allTileCanChowDic.Where(x => x.Value.Count == minCount).Select(x => x.Key).ToList();
if (minCoutTileList.Count >= 1)
{
targetTile = minCoutTileList[0];
}
else
{
return;
}
#endregion
#region ******遍历目标牌的组合的可能
//遍历目标牌的各种组合方式
foreach (var targetTileMaybeGroup in allTileCanChowDic[targetTile])
{
#region ————检查是否有将牌坎子-根据手牌的数量,并决定将牌坎子是否需要遍历
bool needJiang = handTileTemp.Count % 3 == 2 ? true : false;
//如果后面不需要将牌了,这个组合的可能就废弃,不进行计算
if (targetTileMaybeGroup.Length == 1 && !needJiang)
{
continue;
}
#endregion
//如果手牌能满足这个 吃牌方式 的需求,那么就形成一个坎子
if (AllElementHas(handTileTemp, targetTile, targetTileMaybeGroup))
{
#region -----将这个 坎子 加入到复制的 坎子列表
//将这个 坎子 加入 已经选择的坎子列表中
List<PaoHuZi> targetTileMeld = targetTileMaybeGroup.ToList();
targetTileMeld.Add(targetTile);
//复制一份 已经复制的坎子列表,并将这一轮选择的坎子加入它
//为何要复制一份呢?就是不要修改这一层级 递归函数的 参数,当函数返回这一层及的时候,它还是输入的状态,避免下层修改
//如果不复制,当下层返回的时候,因为做了修改,那么这一层的 这个列表就会错误,会将下层的数据也带入这一层来计算
List<List<PaoHuZi>> alreadySelectMeldList_New = new List<List<PaoHuZi>>();
foreach (var alreadySelectMeldListElement in alreadySelectMeldList)
{
List<PaoHuZi> newElement = new List<PaoHuZi>(alreadySelectMeldListElement);
alreadySelectMeldList_New.Add(newElement);
}
alreadySelectMeldList_New.Add(targetTileMeld);
#endregion
#region -----复制手牌 ,并从中删除这一轮选择的坎子 组成的牌
//复制一份手牌,并从 复制的手牌中 移除这轮选择的坎子的所有牌
List<PaoHuZi> handTileTemp_New = new List<PaoHuZi>(handTileTemp);
foreach (var targetTileMeld_Element in targetTileMeld)
{
handTileTemp_New.Remove(targetTileMeld_Element);
}
#endregion
#region ---------检查手牌的数量,如果全部完毕了,那么就返回选择的列表
//如果全部的手牌都放到列表里面了,那么收获的时候到了,
if (handTileTemp_New.Count == 0)
{
if (handTileTemp_New.Count == 0 || (handTileTemp_New.Count == 2 && handTileTemp_New[0] == handTileTemp_New[1]))
{
//将所有选择的列表 作为一个 胡牌列表,加入待返回的 所有胡牌列表 集合中去。
allMeldList.Add(alreadySelectMeldList_New);
return;
}
else
{
}
}
#endregion
#region ----检查手牌数量,如果还没有完毕,那么就继续递归下去
//如果手牌还有,就继续递归下去,但是手牌列表 要是删除已经选择了的牌的 新手牌列表。
//而且 已经选择的组合列表 要传递一个新复制的,避免下次尝试修改到 这个列表。等它返回的时候就会出现错误
else
{
selectMeld(alreadySelectMeldList_New, allMeldList, handTileTemp_New);
// alreadySelectMeldList = new List<List<PaoHuZi>>();
}
#endregion
}
}
#endregion
}
/// <summary>
/// 判断在手牌中是否包含目标牌和其他的所有牌
/// </summary>
/// <param name="handTile"></param>
/// <param name="tagetTile"></param>
/// <param name="chowRule"></param>
/// <returns></returns>
private bool AllElementHas(List<PaoHuZi> handTile,PaoHuZi tagetTile,PaoHuZi[] chowRule)
{
bool returnVar = true;
List<PaoHuZi> handTileTemp = new List<PaoHuZi>(handTile);
if (handTileTemp.Contains(tagetTile))
{
handTileTemp.Remove(tagetTile);
foreach (var tile in chowRule)
{
if (handTileTemp.Contains(tile))
{
handTileTemp.Remove(tile);
}
else
{
returnVar = false;
}
}
}
else
{
returnVar = false;
}
return returnVar;
}
/// <summary>
/// 根据手牌,确定能够吃的其目标牌的所有组合
/// </summary>
/// <param name="targetTile"></param>
/// <param name="handTileList"></param>
/// <returns></returns>
private List<PaoHuZi[]> GetCanChowMeldOnHandTile(PaoHuZi targetTile,List<PaoHuZi> handTileList)
{
//避免修改原始数据,先new一个
List<PaoHuZi> handTileListTemp = new List<PaoHuZi>(handTileList);
List<PaoHuZi[]> ChowRule = GetChowRule(targetTile);
for (int i = ChowRule.Count - 1; i >= 0; i--)
{
if (ChowRule[i].Length == 1)//将牌不需要验证,只要找到手牌里面有这个牌不
{
if (handTileListTemp.Contains(targetTile))
{
}
else
{
ChowRule.RemoveAt(i);
}
}
else//不是将牌
{
int needTileCout = 1;//每种牌需要的数量
if (ChowRule[i][0] == ChowRule[i][1])
{
needTileCout = 2;
}
//手牌中寻找这2张牌,并知道他们的数量
int tile1Cout = handTileListTemp.FindAll(x => x == ChowRule[i][0]).Count;
int tile2Cout = handTileListTemp.FindAll(x => x == ChowRule[i][1]).Count;
if (tile1Cout >= needTileCout && tile2Cout >= needTileCout)//代表这一组能够吃的起
{
}
else//吃不起就删除这个吃牌组合方式
{
ChowRule.RemoveAt(i);
}
}
}
如果手牌%3==2的情况,这个牌有2张的话(手牌中只需要再有一张即可,因为在进来前已经移除了一张),也可以构成将
//if (handTileList.Count % 3 == 2&& handTileList.Contains(targetTile))
//{
// PaoHuZi[] temp = new PaoHuZi[2] { targetTile, targetTile };
// ChowRule.Add(temp);
//}
return ChowRule;
}
/// <summary>
/// 获得符合吃牌规则的另外2张牌的牌组,同时判断牌是否合法
/// </summary>
/// <param name="chowTile1Index"></param>
/// <param name="chowTile2Index"></param>
/// <param name="chowRuleElement"></param>
/// <returns></returns>
private bool GetChowRuleElement(PaoHuZi chowTile1Index, PaoHuZi chowTile2Index,out PaoHuZi[] chowRuleElement)
{
chowRuleElement = new PaoHuZi[2];
bool returnVar = false;
if ((int)chowTile1Index > 0 &&(int) chowTile1Index <= 20 && (int)chowTile2Index > 0 && (int)chowTile2Index <= 20)
{
returnVar = true;
chowRuleElement[0] = chowTile1Index;
chowRuleElement[1] = chowTile2Index;
}
return returnVar;
}
#endregion
#region @@@@@@@@为算法准备的类或枚举
/// <summary>
/// 临时为 字牌算法 的跑胡子枚举,只在算法这个类中使用
/// </summary>
public enum PaoHuZi
{
EPH_None = 0,
EPH_SmallOne = 1,
EPH_SmallTwo = 2,
EPH_SmallThree = 3,
EPH_SmallFour = 4,
EPH_SmallFive = 5,
EPH_SmallSix = 6,
EPH_SmallSeven = 7,
EPH_SmallEight = 8,
EPH_SmallNine = 9,
EPH_SmallTen = 10,
EPH_BigOne = 11,
EPH_BigTwo = 12,
EPH_BigThree = 13,
EPH_BigFour = 14,
EPH_BigFive = 15,
EPH_BigSix = 16,
EPH_BigSeven = 17,
EPH_BigEight = 18,
EPH_BigNine = 19,
EPH_BigTen = 20,
}
#endregion
}