前言
——什么环节只要用算法判断一次,就能知道是否听牌立直、还差什么牌就可以荣和自摸?
——只要在缺一张手牌(如1、4、7、10、13张时)的情况下判断是否听牌、听哪些牌,就可以为上面的复杂判断提供基础。
但网上大部分方法会用大量遍历、查表等方法,解决效率问题这也就是我探索新方法的初衷
分类
分类思路
为了简明地探讨这个问题,我先举一个已经和牌的例子:
🀇🀈🀉 🀜🀝🀞 🀖🀗🀘 🀆🀆🀆 🀃🀃如果未立直被点北风,那就是个很惨的役牌1番40符233
为了更好看清,我分为了4个面子和1个雀头,这时如果拿走一张牌就能,让它变成听牌的形式,共有3种情况:
首先下一个定义:几张连续或相同的牌,我称为1块(Block
),下面例子中会用空格分开各块
- 拿走刻子的一张,还剩5块
- 拿走顺子的边张后,还剩下5块
- 拿走顺子的坎(嵌)张,变成6块(这是听牌情况下,块数最多的情况)
- 拿走雀头的一张,还剩5块
此外,和牌时还有更复杂的复合形式,即有一块里既有雀头又有面子,但归根结底还是上面这些形式的复合,这里举个简单的例子:
🀇🀈🀉🀊🀊 🀜🀝🀞 🀖🀗🀘 🀆🀆🀆- 它如果缺一张四万时,会形成一块不完整型,既含有雀头也含有面子:
- 它如果缺一张二万时,会形成两块不完整型,既含有雀头也含有面子:
- 这些便是所有情况的基本形式,我把它分为4大类,6小类,下面我将依次介绍:
完整型判断Lv.1 | 完整型判断Lv.2 | ~指“不完整型” | ||
---|---|---|---|---|
牌数 | IntegrityType | 类型名 | 判断听牌方法 | 备注 |
3n | Type0 | 完整型 | 直接判断(IntegrityJudge()) | |
TypeEx | 雀半不完整型 | 去对+取坎张 | 半~与雀头~合并而成 | |
3n+1 | Type1 | 半不完整型 | 取坎张(会成对出现) | |
雀面不完整型 | 遍历+去对 | 雀头~与面子~合并而成 | ||
3n+2 | Type2 | 雀头不完整型 | 去对(去掉一个对子) | |
面子不完整型 | 遍历(3-9次)+与前后块连接 |
- 完整型(3n):顾名思义,只含有面子(刻子或顺子)的块,牌数是3的倍数,可以直接判断。但可能和半不完整型一同出现:
- 雀头不完整型(3n+2):包含一个雀头,虽然牌数不是3的倍数,但较完整,去对(去掉一个对子)就可以判断出缺(听)的牌:
- 面子不完整型(3n+2):即完整型缺一张牌(但不会形成两块),听牌时会和雀头不完整型一起出现,形成多面听或者双碰,用遍历(在该块的范围内遍历,遍历的次数不多)的方法可以找出:
(根据不同牌型,遍历次数比不同牌的数量多0~2次(至少3次、至多9次)就可以)
- 雀面不完整型(3n+1):即雀头不完整型缺一张牌,因为不知道缺在雀头还是在面子上,所以只能用遍历(但遍历次数不多)后再去对来处理:
- 半不完整型(3n+1):即听坎张,所以在听牌时都会成对出现,所以取坎张(两块中间的那张)就可以了:
- 雀半不完整型(3n):也听坎张,所以会和半不完整型一同出现,所以先在去对后取坎张就可以了(图同上)
综上,所有牌型都可以分为如上6种情况处理,可以算是一种归类或者剪枝(?)
接下来便可以写代码了,首先得先按数量分成几块(Block
),才能进行更深层的操作:
分类代码实现
首先,这个算法是针对每一家手牌进行判断的,所以针对Opponent
类编写常用的判断关系和手牌进张的方法:
public static class OpponentHelper
{
/// <summary>
/// 两张手牌间关系
/// </summary>
/// <param name="hands">手牌</param>
/// <param name="num">前张牌序号</param>
public static int GetRelation(this List<Tile> hands, int num)
{
try
{
return hands[num + 1].Val - hands[num].Val;
}
catch (Exception)
{
// (尽量大的数)
return int.MaxValue;
}
}
/// <summary>
/// 摸牌
/// </summary>
/// <param name="hands">手牌</param>
/// <param name="tile">进张</param>
/// <returns>插入牌的位置</returns>
public static int TileIn(this List<Tile>hands, Tile tile)
{
var ru = 0;
// 找到进张插入的位置
while (ru < hands.Count && tile.Val > hands[ru].Val)
++ru;
hands.Insert(ru, tile);
return ru;
}
}
开始写Opponent
类里ReadyHandJudge()
函数里的内容:
首先声明一个readyHands
铳牌列表,用于储存听的牌
public class Opponent
{
...
/// <summary>
/// 听牌判断(在摸牌前判断)
/// </summary>
/// <returns>听的牌</returns>
public List<Tile> ReadyHandJudge()
{
var readyHands = new List<Tile>();
...
}
...
}
然后是特殊牌型的判断(国士无双和七对子):
由于算法很简单也很多样,我就不做详细介绍,只有大致介绍:
这里我的牌对应数字的定义有一些优势:
牌 | 值 |
---|---|
一萬 ~ 九萬 | 0 ~ 8 |
一筒 ~ 九筒 | 16 ~ 24 |
一索 ~ 九索 | 32 ~ 40 |
東 | 48 |
南 | 56 |
西 | 64 |
北 | 72 |
白 | 80 |
發 | 88 |
中 | 96 |
牌的序号*8就是对应的幺九牌,此外用shortage
和redundancy
两个bool
型变量便可以轻松实现
public class Opponent
{
...
/// <summary>
/// 国士牌型判断
/// </summary>
/// <returns>听牌</returns>
private IEnumerable<Tile> ThirteenOrphansJudge()
{
// 是否缺了某张幺九牌(0或1)
var shortage = false;
// 是否多了某张幺九牌(0或1)
var redundancy = false;
var shortTile = 0; // 缺的幺九牌
// 判断十三张幺九牌的拥有情况
for (var i = 0; i < 13; ++i)
{
var temp = (shortage ? 1 : 0) - (redundancy ? 1 : 0);
// 如果和上张映射幺九牌一样
if (Hands[i].Val == (i + temp - 1) * 8)
{
// 如果之前已经有一个多的牌
if (redundancy)
yield break;
redundancy = true; // 记录有多牌
} // 如果和下张映射幺九牌一样
else if (Hands[i].Val == (i + temp + 1) * 8)
{
// 如果之前已经有一个缺牌则不是国士,否则记录缺牌
if (shortage)
yield break;
shortage = true;
shortTile = i * 8;
} // 有不是幺九牌即不符合国士
else if (Hands[i].Val != (i + temp) * 8)
yield break;
}
// 若有多张,记听一面或记听一面(红中)(因为红中在最后不会被redundancy记录)
if (redundancy)
yield return new(shortage ? shortTile : 96);
// 若不缺张则记听十三面
else for (var i = 0; i < 13; ++i)
yield return new(i * 8);
}
...
}
由于日麻没有龙七对的役种,只好逐张判断,一般情况下偶数序号牌和下一张是相同的,而奇数的和下张不是相同:
public class Opponent
{
...
/// <summary>
/// 七对牌型判断
/// </summary>
/// <returns>听的牌</returns>
private Tile? SevenPairsJudge()
{
// 多出来的单张
var single = false;
// 该单张牌位置
var singleTile = 0;
// 判断相同或连续的关系
for (var i = 0; i < 12; ++i)
// 如果偶数位关系对应不是相同,或奇数位不是其他关系(出现单张)
if (((i + (single ? 1 : 0)) % 2 ^ (Hands.GetRelation(i) > 0 ? 1 : 0)) > 0)
{
// 直接异或运算无法排除龙七对
// 如果这个错误关系是相同,则是龙七对;如果之前已经有单牌了,则不是七对子
if (Hands.GetRelation(i) is 0 || single)
return null;
single = true;
singleTile = Hands[i].Val;
}
// 如果没查到单张
if (!single)
// 那单张就是最后一个
singleTile = Hands[12].Val;
// 记听一面
return new(singleTile);
}
...
}
接下来是判断完整型:
首先写Block
类,其成员字段拥有3个,在代码片中有注释;通常在创建新Block
时就已经确定了其中FirstLoc
的值,所以为只读:
public class Block
{
/// <summary>
/// 块内牌数(至少一张)
/// </summary>
public int Len { get; set; } = 1;
/// <summary>
/// 类型(真(3n)为完整型(由整数个面子组成),假为不完整型(含有雀头、不完整的面子))
/// </summary>
public IntegrityType Integrity { get; set; } = IntegrityType.Type0;
/// <summary>
/// 块内首张牌的序号
/// </summary>
public int FirstLoc { get; }
public int LastLoc => FirstLoc + Len - 1;
/// <summary>
/// 完整类型
/// </summary>
public enum IntegrityType
{
/// <summary>
/// 完整型(3n)
/// </summary>
Type0,
/// <summary>
/// 雀面不完整型或半不完整型(3n+1)
/// </summary>
Type1,
/// <summary>
/// 雀头不完整型或面子不完整型(3n+2)
/// </summary>
Type2,
/// <summary>
/// 雀半不完整型(3n)
/// </summary>
TypeEx
}
public Block(int loc) => FirstLoc = loc;
...
}
然后写Block
的判断:
每块按牌数初步被判断为4大类,由IntegrityType
枚举记录。不难发现,在如下这种听牌情况时,块数达到了最多的6块,不完整型最多3块:
而且在找到下一块的开头时,也会得到上一块的总长度,所以把上一块收尾和下一块的开头写在同一个循环体内
由于判断听牌时,我们只关心不完整块,所以只返回不完整块(其中判断雀不完整型所用方法IntegrityJudge()
在下一节介绍):
public class Opponent
{
...
/// <summary>
/// 获取分块
/// </summary>
/// <returns>不完整的块数(最多3个)</returns>
private List<Block> GetBlocks(out List<Block> blocks)
{
var errBlocks = new List<Block>(4);
blocks = new(6) { new(0) };
for (var i = 0; i < Hands.Count - 1; ++i)
// 当关系不是相同或连续
if (Hands.GetRelation(i) > 1)
{
// 记录上一块的长度
blocks[^1].Len = i - blocks[^1].FirstLoc + 1;
// 筛选完整型Lv.1
blocks[^1].Integrity = (blocks[^1].Len % 3) switch
{
0 => Block.IntegrityType.Type0,
1 => Block.IntegrityType.Type1,
2 => Block.IntegrityType.Type2,
_ => throw new ArgumentOutOfRangeException()
};
// 如果类型是不完整则记录
if (blocks[^1].Integrity is not Block.IntegrityType.Type0)
errBlocks.Add(blocks[^1]);
// 若块序号达到(6 - 副露数)或有4个不完整型则无听
if (blocks.Count + Melds.Count is 6 || errBlocks.Count is 4)
return new();
// 下一块,括号里是块内首张牌的序号
blocks.Add(new(i + 1));
}
// 最后一块的记录无法写进循环
{
blocks[^1].Len = Hands.Count - blocks[^1].FirstLoc;
blocks[^1].Integrity = (blocks[^1].Len % 3) switch
{
0 => Block.IntegrityType.Type0,
1 => Block.IntegrityType.Type1,
2 => Block.IntegrityType.Type2,
_ => throw new ArgumentOutOfRangeException()
};
if (blocks[^1].Integrity is not Block.IntegrityType.Type0)
errBlocks.Add(blocks[^1]);
if (errBlocks.Count is 4)
return new();
}
// 通过完整型Lv.1的块,筛选完整型Lv.2发现有一块不完整,则为不完整型加半不完整型,多于一块则无听
foreach (var block in blocks.Where(block => block.Integrity is Block.IntegrityType.Type0
&& !block.IntegrityJudge(Hands)))
if (errBlocks.Count is not 4)
{
block.Integrity = Block.IntegrityType.TypeEx;
errBlocks.Add(block);
// 特殊标记
errBlocks.Add(new(0));
errBlocks.Add(new(0));
}
else return new();
return errBlocks;
}
}
下面就要写重要的判断完整型方法(IntegrityJudge()
):
完整型判断
完整型判断思路
为了更好看清每块的内部结构,我们需要继续细分:
定义:块(Block
)内所有相同的牌分为1组(Group
)
如此,例如:
示意图:整张图都是属于一个块的,每一列都是一个组
🀇🀇🀇🀈🀉🀉🀊🀊🀊🀋🀋🀌然后想象自己是程序,用自动机式的思维,从最左边的第0组开始,一组一组地判断:
-
先杠刻子:如果遇到3个没杠掉的圈,3个一起杠掉;
-
再杠顺子:剩下的没杠掉的如果不满3个,在本组每杠掉1个,下组和下下组也杠掉1个(也是共杠3个);
如果这组要杠掉1个,而下组或下下组不够的杠了,说明不是完整型,反之如果刚好杠完就是完整型。
拿上图举例:
第一次 ——
第二次 ——
第三次 ——
第四次 ——
判断出这是完整型了,很简单吧?
如果是如下的牌型呢?
🀇🀇🀇🀇🀈🀉🀉🀊🀊🀊🀋🀋第一次 ——
第二次 ——
第三次 ——
杠到第四次时,发现四万有2张,理应杠掉五万和六万各2张,但是不够了,所以这不是完整型
这种方法可以正确分离所有的类型,除了三连刻无法识别成3条顺子:
🀇🀇🀇🀈🀈🀈🀉🀉🀉但是就听牌来说,这并不会影响到是否听牌、听哪些牌的判断,而且之后改进也十分容易
完整型判断代码实现
根据原理,实现这个并不难(写在Block
类下):
-
注1:70-76行是为了以后对接“去对”的操作,现在并没有什么用╮(╯▽╰)╭
-
注2:
TileType
和blockTiles
可以记录如何分为顺(Sequence
)和刻(Triplet
),以后算符判断牌型时会用到,现在没有用
public class Block
{
...
private enum TileType { Sequence, Triplet };
/// <summary>
/// 筛选完整型Lv.2
/// </summary>
/// <param name="hands">判断的牌组</param>
/// <param name="eyesLoc">雀头的序号(-1为没有雀头)</param>
public bool IntegrityJudge(List<Tile> hands, int eyesLoc = -1)
{
var groups = GetGroups(hands);
// 在此时没用,但在和牌算符时会用到
var blockTiles = new TileType[Len];
for (var i = 0; i < blockTiles.Length; ++i)
blockTiles[i] = TileType.Sequence;
// 若有雀头,则将雀头认为是刻
if (eyesLoc is not -1)
{
++groups[eyesLoc].Confirmed;
++groups[eyesLoc].Confirmed;
blockTiles[groups[eyesLoc].Loc - FirstLoc] = TileType.Triplet;
blockTiles[groups[eyesLoc].Loc - FirstLoc + 1] = TileType.Triplet;
}
// 每次循环记录一个组
for (var i = 0; i < groups.Count; ++i)
{
// 该组牌数
switch (groups[i].Len - groups[i].Confirmed)
{
// 刚好全部确定
case 0:
continue;
// 都是顺,确定后面2组分别有1张是顺
case 1:
if (groups.Count > i + 2)
{
++groups[i + 1].Confirmed;
++groups[i + 2].Confirmed;
continue;
}
break;
// 都是顺,确定后面2组分别有2张是顺
case 2:
if (groups.Count > i + 2)
{
++groups[i + 1].Confirmed;
++groups[i + 1].Confirmed;
++groups[i + 2].Confirmed;
++groups[i + 2].Confirmed;
continue;
}
break;
// 3刻1顺,确定后面2组分别有1张是顺
case 4:
if (groups.Count > i + 2)
{
++groups[i + 1].Confirmed;
++groups[i + 2].Confirmed;
blockTiles[groups[i].Loc - FirstLoc] = TileType.Triplet;
blockTiles[groups[i].Loc - FirstLoc + 1] = TileType.Triplet;
blockTiles[groups[i].Loc - FirstLoc + 2] = TileType.Triplet;
continue;
}
break;
// 3张是刻
case 3:
blockTiles[groups[i].Loc - FirstLoc] = TileType.Triplet;
blockTiles[groups[i].Loc - FirstLoc + 1] = TileType.Triplet;
blockTiles[groups[i].Loc - FirstLoc + 2] = TileType.Triplet;
continue;
// 可能是负数
default:
break;
}
Integrity = eyesLoc is -1 ? IntegrityType.TypeEx : IntegrityType.Type2;
return false;
}
return true;
}
}
其他不完整型判断
不完整型判断思路
有了IntegrityJudge()
函数,剩下的一切都很明朗了:只要想办法往完整型上凑就好了。之前说了如果是听牌的牌型,不完整型(errBlock
)最多只能有3个,那分别有1、2、3个时,会有特征吗?
答案是有,而且有较为明显的区别:
- 有1个时:该不完整型一定是雀面不完整型,例:
- 有2个时:会有一个雀头完整型和一个面子不完整型,例:
- 有3个时:会有一个雀头完整型和两个半不完整型,如:
- 特殊:在完整型判断Lv.1时只有一个半不完整型,而完整型判断Lv.2时会发现一个牌数为3n的雀半不完整型,例如:
所以可以用一个switch
语句,来讨论这4种情况:
注:七对子可能复合二杯口,在复合的时候应该删除七对子的听牌,以防重复听牌
注:遍历有两种模式,一种遍历后直接判断是否完整(面子不完整型),一种遍历后还要去对(雀面不完整型),所以参数列表里还有个bool
类型表示是否要去对
public class Opponent
{
...
/// <summary>
/// 听牌判断(在摸牌前判断)
/// </summary>
/// <returns>听的牌</returns>
public List<Tile> ReadyHandJudge()
{
var readyHands = new List<Tile>();
var sevenPairsFlag = false;
// 如果没有副露(特殊牌型判断)
if (Melds.Count is 0)
{
if (ThirteenOrphansJudge().ToList() is { Count: not 0 } readyHandsList)
return readyHandsList;
if (SevenPairsJudge() is { } tile)
{
readyHands.Add(tile);
sevenPairsFlag = true;
}
// 有可能复合二杯口,故听牌后不退出(会进入case 1或2)
}
var errBlocks = GetBlocks(out var blocks);
// 不完整型块数
switch (errBlocks.Count)
{
// 有一块不完整型(一块雀面不完整型(3n+1))
// 二杯口缺雀头会在这里出现
case 1:
{
// 将此不完整型遍历
readyHands.AddRange(errBlocks[0].Traversal(Hands, true));
var index = blocks.IndexOf(errBlocks[0]);
// 与前块连接
if (index is not 0)
{
var joint = JointBlocks(blocks[index - 1], blocks[index]);
// 如果该牌组完整,则记听一面
if (joint?.JointedBlock.IgnoreEyesJudge(joint.Value.JointedHands) is true)
readyHands.Add(joint.Value.MiddleTile);
}
// 与后块连接
if (index != blocks.Count - 1)
{
var joint = JointBlocks(blocks[index], blocks[index + 1]);
// 如果该牌组是雀头完整型,则记听一面
if (joint?.JointedBlock.IgnoreEyesJudge(joint.Value.JointedHands) is true)
readyHands.Add(joint.Value.MiddleTile);
}
break;
}
// 有两块不完整型(一块面子不完整型(3n+2),一块雀头完整型(3n+2))
// 二杯口缺面子会在这里出现
case 2:
{
if (errBlocks[1].IgnoreEyesJudge(Hands))
readyHands.AddRange(errBlocks[0].Traversal(Hands, false));
if (errBlocks[0].IgnoreEyesJudge(Hands))
readyHands.AddRange(errBlocks[1].Traversal(Hands, false));
break;
}
// 有三块不完整型(两块半不完整型(3n+1),一块雀头完整型(3n+2))
case 3:
{
// 如果3n+2的不完整型夹在中间或不是雀头完整型,则无听
var eyesIndex = errBlocks
.FindIndex(eyesBlock => eyesBlock.Integrity is Block.IntegrityType.Type2);
if (eyesIndex is 1 || !errBlocks[eyesIndex].IgnoreEyesJudge(Hands))
break;
var joint = eyesIndex is 0
? JointBlocks(errBlocks[1], errBlocks[2])
: JointBlocks(errBlocks[0], errBlocks[1]);
if (joint is null)
break;
// 如果该牌组完整,则记听一面
if (joint.Value.JointedBlock.IntegrityJudge(joint.Value.JointedHands))
readyHands.Add(joint.Value.MiddleTile);
break;
}
// 有两块不完整型(一块雀半完整型(3n),一块半不完整型(3n+1))
case 4:
{
var joint = errBlocks[0].FirstLoc < errBlocks[1].FirstLoc ?
JointBlocks(errBlocks[0], errBlocks[1]) :
JointBlocks(errBlocks[1], errBlocks[0]);
if (joint is null)
break;
// 如果该牌组是雀头完整型,则记听一面
if (joint.Value.JointedBlock.IgnoreEyesJudge(joint.Value.JointedHands))
readyHands.Add(joint.Value.MiddleTile);
break;
}
}
// 如果有听(七对子),则为二杯口,删除七对子的听牌,否则会重复
if (sevenPairsFlag && readyHands.Count > 1)
readyHands.RemoveAt(0);
return readyHands;
}
...
}
有了上面的解释,这段代码应该不难理解,现在该写取坎张、遍历和去对的算法了:
-
取坎张:把半不完整型和半(雀半)不完整型连接起来,中间补上一张牌(即听的牌);如果补上一张牌后仍然不能形成一个完整型块,则说明无听
-
遍历:就是在面子(雀面)不完整型块上,加上任意一张,在不形成新块的情况下使它成为完整型,例如:
所以说遍历次数比不同牌的数量多0~2次(至少3次、至多9次)就可以,在上图情况下需要遍历不同的牌数2+2次,而如果块中含有幺九牌或字牌,遍历次数能减少1~2次
去对:找到所有的对子,每次去掉一个,查看它是否完整;这里直接传递给IntegrityJudge()
,让它直接认为那个对子是刻子的一部分,就可以排除对子了
不完整型判断代码实现
public class Opponent
{
...
/// <summary>
/// 连接两块
/// </summary>
/// <param name="frontBlock">前块</param>
/// <param name="followBlock">后块</param>
/// <returns>连接后的牌、连接后的块、用来连接的牌</returns>
private (List<Tile> JointedHands, Block JointedBlock, Tile MiddleTile)? JointBlocks(Block frontBlock, Block followBlock)
{
// 判断连接的两块是否连续
if (followBlock.FirstLoc - frontBlock.LastLoc is not 1)
return null;
// 如果原来这两张牌中间不是隔一张,则无听
if (Hands.GetRelation(frontBlock.LastLoc) is not 2)
return null;
// 临时记录中间隔的牌(可能是铳牌)
var tempReadyHands = new Tile(Hands[frontBlock.LastLoc].Val + 1);
// 临时用来判断的牌组
var jointedHands = new List<Tile>();
// 这两块不完整型总张数
var jointedBlock = new Block(0) { Len = frontBlock.Len + 1 + followBlock.Len };
// 复制该不完整型所有牌
jointedHands.AddRange(Hands.GetRange(frontBlock.FirstLoc, jointedBlock.Len - 1));
// 插入一张中间隔的牌
jointedHands.Insert(frontBlock.Len, tempReadyHands);
return (jointedHands, jointedBlock, tempReadyHands);
}
...
}
public class Block
{
...
/// <summary>
/// 遍历
/// </summary>
/// <param name="hands">判断的牌组</param>
/// <param name="mode">是否要去对(真为雀面不完整型,假为面子不完整型)</param>
/// <returns>听的牌,可能本来它就不为空,不过在这里不影响(将来算符时可能改动)</returns>
public IEnumerable<Tile> Traversal(List<Tile> hands, bool mode)
{
// 可能的首张牌
var first = hands[FirstLoc].Val - 1;
// 如果首张是一万、筒、索或字牌,则first没有前一张,加回hands[loc]
if ((hands[FirstLoc].Val & 15) is 0 || hands[FirstLoc].Val / 8 > 5)
++first;
// 可能的末张牌
var last = hands[FirstLoc + Len - 1].Val + 1;
// 如果末张是九万、筒、索或字牌,则得last没有后一张,减回hands[loc]
if ((hands[FirstLoc + Len - 1].Val & 15) is 8 || hands[FirstLoc + Len - 1].Val / 8 > 5)
--last;
var tempBlock = new Block(0) { Len = Len + 1 };
var tempTile = first;
// 每张牌都插入尝试一次(遍历)
for (var i = 0; i < last - first + 1; ++i, ++tempTile)
{
var tempHands = new List<Tile>();
// 重新复制所有牌
for (var j = FirstLoc; j < FirstLoc + Len; ++j)
tempHands.Add(new(hands[j].Val));
// 插入尝试的牌
tempHands.TileIn(new(tempTile));
if (mode switch
{
// 雀面不完整型且遍历、去对后完整,则听牌
true => tempBlock.IgnoreEyesJudge(tempHands),
// 面子不完整型且遍历后完整,则听牌
false => tempBlock.IntegrityJudge(tempHands)
})
yield return new(tempTile);
}
}
/// <summary>
/// 去对后完整(雀头完整型)
/// </summary>
/// <param name="hands">判断的牌组</param>
/// <returns>是否完整</returns>
public bool IgnoreEyesJudge(List<Tile> hands)
{
for (int i = FirstLoc, tempGroupNum = 0; i < FirstLoc + Len - 1; ++i)
{
// 当关系是连续,则组数加一
if (hands.GetRelation(i) is 1)
++tempGroupNum;
// 当关系是相同,若是雀头完整型,则听牌
else if (IntegrityJudge(hands, tempGroupNum))
return true;
}
return false;
}
}
以上就是全部的听牌算法,代码并不长,理论上可以判断出日麻里所有听的牌(不考虑振听、空听情况下),大家可以自己实验一下23333
完整代码
C#:https://github.com/Poker-sang/Mahjong/tree/master/c#
C++(C++/CLI):https://github.com/Poker-sang/Mahjong/tree/master/MahjongHelper
C++(采用C++20标准):https://github.com/Poker-sang/Mahjong/tree/master/Cpp
规则参考
-
资深麻友
-
雀魂麻将
-
雀姬麻将