VV游戏

 找回密码
 立即注册
查看: 200|回复: 0

麻将胡牌检测算法

[复制链接]

2

主题

5

帖子

9

积分

新手上路

Rank: 1

积分
9
发表于 2023-4-24 17:12:09 | 显示全部楼层 |阅读模式
用递归实现麻将胡牌的检测

数据表示

对于每一张牌的表示:
[
    ["1m", "2m", "3m", "4m", "5m", "6m", "7m", "8m", "9m"], # 万牌
    ["1s", "2s", "3s", "4s", "5s", "6s", "7s", "8s", "9s"], # 条/索牌
    ["1p", "2p", "3p", "4p", "5p", "6p", "7p", "8p", "9p"], # 饼牌
    ["do", "na", "xi", "be", "zh", "fa", "ba"], # 字牌 东南西北中发白 (其实字牌顺序对于算法影响不大)
]创建一个counters,一一对应上面每一种类型的牌在手牌中出现的次数。
例:1m 2m 3m 3s 4s 5s 1p 1p 5p 6p 7p fa fa fa例如,以上这一副这样的牌应该被转换为一下的格式输入:
counters = [
    [1, 1, 1, 0, 0, 0, 0, 0, 0],
    [0, 0, 1, 1, 1, 0, 0, 0, 0],
    [2, 0, 0, 0, 1, 1, 1, 0, 0],
    [0, 0, 0, 0, 0, 3, 0],
]这样的格式可以忽略万索饼字牌的不同点,而只去注重其相同的性质。
面子手 分类讨论

对于面子手的判断是最难的。面子手的结构可以理解为4个面子加上1个雀头(一个对子)。而面子可以是顺子(三张数字连续的牌)或者是刻子(三张一样的牌)。因此,面子手中,每一张牌肯定只能是一下组合中的一种: 1. 这个牌是一个顺子的一部分 2. 这个牌是一个刻子的一部分 3. 这个牌是一个对子的一部分
counters表中每一个非0的牌,必须是上面三种可能中的一种。将counters拆分成为4个独立的列表,命名为counter。根据上面的counters例子,万牌的counter应该是:
counter = [1, 1, 1, 0, 0, 0, 0, 0, 0]从左往右遍历counter,找到第一个非0的值: - 如果值为1,这张牌一定是1个顺子的第一张牌。 - 如果值为2,这张牌一定是2个一模一样的顺子的第一张牌,或者是1个对子。 - 如果值为3,这张牌一定是3个一模一样的顺子的第一张牌,或者是1个顺子的第一张牌加上1个对子,或者是1个刻子。 - 如果值为4,这张牌一定是4个一模一样的顺子的第一张牌,或者是2个一模一样的顺子的第一张牌加上一个对子,或者是1个顺子的第一张牌加上1个刻子。
如果第一个非0值不能满足上面的一个条件,这一副手牌一定没有胡。
同时,一些牌是不能考虑带有顺子的组合(例如:字牌);一些牌不能考虑对子的组合(例如:这幅手牌已经在之前有了一个对子了)。
在考虑多种组合的可能时(例如值为2,3,4的情况),使用递归,依次尝试每一种组合。
面子手 检测组合

将以上的每一个组合总结成为一个规则(rule)。rule是一个长度为3的tuple,分别记录所需要的(顺子数量,对子数量,刻子数量)。所有的规则总结是这个样子的:
rules = {
    1: [(1, 0, 0)],
    2: [(2, 0, 0), (0, 1, 0)],
    3: [(3, 0, 0), (1, 1, 0), (0, 0, 1)],
    4: [(4, 0, 0), (2, 1, 0), (1, 0, 1)],
}每一个带有顺子的组合需要满足以下条件才可以算成功匹配: - 当前counter不是字牌counter。 - 当前的index小于等于6。(顺子会往后延续两位) - 后面的两位的值必须大于等于顺子的数量。
每一个带有对子的组合需要满足以下条件才可以算成功匹配: - 当前的手牌没有成功匹配过对子。
每一个带有刻子的组合可以直接算成功匹配:
每次成功匹配后,创建一个深复制。并且,在counter中减去当前匹配到的组合中所需要的牌。
如果一个counter的值全是0,这一个counter的检测将会传回真(True)。
对子手 13幺手 概要

除了普通的面子手胡牌,7对子和13幺也是两种另外的胡牌方式。这两种相对简单,便统一说明。
日麻中,7对子的胡牌不能包含重复的对子,其他麻将规则有的允许重复的对子。下文代码是按照日麻规则编写的。
十三幺指的是包含以下每一种牌至少一种(其中一种需要两张,其余都是一张):
1m 9m 1s 9s 1p 9p do na xi be zh fa ba代码

from copy import deepcopy


class Hand:
    def __init__(self, counters: list[list[int]]) -> None:
        self.counters = counters

    @classmethod
    def create_hand_from_str(cls, hand: list[str]) -> "Hand":
        """return a `Hand` based on the given list of string"""
        counters_rep = [
            [f"{i}{z}" for i in range(1, 10 if z != "z" else 8)]
            for z in ("m", "s", "p", "z")
        ]
        counters = [[0 for _ in range(9)] for _ in range(3)] + [[0 for i in range(7)]]

        for counter_i in range(4):
            for rep_i in range(len(counters_rep[counter_i])):
                counters[counter_i][rep_i] = hand.count(counters_rep[counter_i][rep_i])

        return cls(counters)

    def is_win(self) -> bool:
        """check three different kind of hand to determine the winning"""
        if self._4mz_hand():
            print("Win: mian zi hand")
            return True

        if self._7d_hand():
            print("Win: 7 double hand")
            return True

        if self._13h_hand():
            print("Win: 13 head hand")
            return True

        print("Not win")
        return False

    def _rule_checker(
        self,
        counter: list[int],
        tile_i: int,
        rule: tuple[int],
        allow_d: bool,
        allow_s: bool,
    ) -> bool:
        """
        check the given `rule` with the `counter`

        if passed - continue to call `_seb_4mz_hand`

        else - return False

        Parameters:
            - counter - a list of frequency for each tile
            - tile_i - index of this tile in the `counter`
            - rule - a tuple of three integer, marking the numbers of (sequence, double, triple)
            - allow_d - allows to use rules contain double (Y/N)
            - allow_s - allows to use rules contain sequence (Y/N)

        Returns:
            - bool - results of the checking
        """

        s, d, t = rule  # unpack the rule (sequence, double, triple)

        if not allow_d and d > 0 or not allow_s and s > 0:
            return False

        if s > 0:
            if tile_i > 6:
                return False
            elif counter[tile_i + 1] < s or counter[tile_i + 2] < s:
                return False

        temp_counter = deepcopy(counter)

        temp_counter[tile_i] -= s + d * 2 + t * 3
        temp_counter[tile_i + 1] -= s
        temp_counter[tile_i + 2] -= s

        if d:
            allow_d = False

        return self._sub_4mz_hand(temp_counter, allow_d, allow_s)

    def _sub_4mz_hand(
        self, counter: list[int], allow_d: bool = True, allow_s: bool = True
    ) -> bool:
        """
        check the `counter` by calling `rule_check`

        Parameters:
            - counter - a list of frequency for each tile
            - allow_d - allows to use rules contain double (Y/N)
            - allow_s - allows to use rules contain sequence (Y/N)

        Returns:
            - result of the checking
        """
        if sum(counter) == 0:
            return True, allow_d

        for i, num in enumerate(counter):
            if num != 0:
                first_tile_freq, first_tile_i = num, i
                break

        rules = {
            1: [(1, 0, 0)],
            2: [(2, 0, 0), (0, 1, 0)],
            3: [(3, 0, 0), (1, 1, 0), (0, 0, 1)],
            4: [(4, 0, 0), (2, 1, 0), 1, 0, 1],
        }

        for num in range(1, 5):
            if first_tile_freq == num:
                for rule_i in range(len(rules[num])):
                    if self._rule_checker(
                        counter, first_tile_i, rules[num][rule_i], allow_d, allow_s
                    ):
                        return True
        return False

    def _4mz_hand(self) -> bool:
        """check 4 mian zi hand winning"""
        res = [False for _ in range(4)]

        for i, counter in enumerate(self.counters):
            allow_d = sum(counter) % 3 != 0
            res = self._sub_4mz_hand(counter, allow_d, allow_s=i != 3)

        return all(res)

    def _7d_hand(self) -> bool:
        """check 7 doubles hand winning"""
        double_num = 0  # 对子的数量
        for counter in self.counters:
            double_num += counter.count(2)

        return double_num == 7

    def _13h_hand(self) -> bool:
        """check 13 head hand winning"""
        for i, counter in enumerate(self.counters):
            if i == 3:
                # 字牌
                if not all([n not in (1, 2) for n in counter]):
                    return False
            else:
                # 19牌
                if counter[0] not in (1, 2) and counter[-1] not in (1, 2):
                    return False

        return True


if __name__ == "__main__":
    string_hand = input("Input your hands >>").split(" ")
    hand = Hand.create_hand_from_str(string_hand)
    # print(hand.counters)
    hand.is_win()一个关于检测麻将胜利的算法
代码:https://github.com/Cris-Murong/Mahjong
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

Archiver|手机版|小黑屋|VV游戏

GMT+8, 2025-4-8 06:00 , Processed in 0.351052 second(s), 30 queries .

Powered by Discuz! X3.4

Copyright © 2001-2021, Tencent Cloud.

快速回复 返回顶部 返回列表