|
用递归实现麻将胡牌的检测
数据表示
对于每一张牌的表示:
[
["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:
&#34;&#34;&#34;
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
&#34;&#34;&#34;
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:
&#34;&#34;&#34;check 4 mian zi hand winning&#34;&#34;&#34;
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:
&#34;&#34;&#34;check 7 doubles hand winning&#34;&#34;&#34;
double_num = 0 # 对子的数量
for counter in self.counters:
double_num += counter.count(2)
return double_num == 7
def _13h_hand(self) -> bool:
&#34;&#34;&#34;check 13 head hand winning&#34;&#34;&#34;
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__ == &#34;__main__&#34;:
string_hand = input(&#34;Input your hands >>&#34;).split(&#34; &#34;)
hand = Hand.create_hand_from_str(string_hand)
# print(hand.counters)
hand.is_win()一个关于检测麻将胜利的算法
代码:https://github.com/Cris-Murong/Mahjong |
|