扑克子

博客园 首页 新随笔 联系 订阅 管理

前言

——什么环节只要用算法判断一次,就能知道是否听牌立直、还差什么牌就可以荣和自摸?

——只要在缺一张手牌(如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就是对应的幺九牌,此外用shortageredundancy两个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:TileTypeblockTiles可以记录如何分为顺(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

规则参考

  1. 日麻百科

  2. 资深麻友

  3. 雀魂麻将

  4. 雀姬麻将

posted on 2024-01-14 20:59  扑克子  阅读(67)  评论(0编辑  收藏  举报