前言
——什么环节只要用算法判断一次,就能知道是否听牌立直、还差什么牌就可以荣和自摸?
——只要在缺一张手牌(如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
规则参考
-
资深麻友
-
雀魂麻将
-
雀姬麻将
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· 清华大学推出第四讲使用 DeepSeek + DeepResearch 让科研像聊天一样简单!
· 实操Deepseek接入个人知识库
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· 易语言 —— 开山篇