diff --git a/mahjong/agari.py b/mahjong/agari.py index 526185f..43622bb 100644 --- a/mahjong/agari.py +++ b/mahjong/agari.py @@ -5,7 +5,8 @@ class Agari: - def is_agari(self, tiles_34: Sequence[int], open_sets_34: Optional[Collection[Sequence[int]]] = None) -> bool: + @staticmethod + def is_agari(tiles_34: Sequence[int], open_sets_34: Optional[Collection[Sequence[int]]] = None) -> bool: """ Determine was it win or not :param tiles_34: 34 tiles format array @@ -102,47 +103,48 @@ def is_agari(self, tiles_34: Sequence[int], open_sets_34: Optional[Collection[Se return False nn0 = (n00 * 1 + n01 * 2) % 3 - m0 = self._to_meld(tiles, 0) + m0 = Agari._to_meld(tiles, 0) nn1 = (n10 * 1 + n11 * 2) % 3 - m1 = self._to_meld(tiles, 9) + m1 = Agari._to_meld(tiles, 9) nn2 = (n20 * 1 + n21 * 2) % 3 - m2 = self._to_meld(tiles, 18) + m2 = Agari._to_meld(tiles, 18) if j & 4: return ( not (n0 | nn0 | n1 | nn1 | n2 | nn2) - and self._is_mentsu(m0) - and self._is_mentsu(m1) - and self._is_mentsu(m2) + and Agari._is_mentsu(m0) + and Agari._is_mentsu(m1) + and Agari._is_mentsu(m2) ) if n0 == 2: return ( not (n1 | nn1 | n2 | nn2) - and self._is_mentsu(m1) - and self._is_mentsu(m2) - and self._is_atama_mentsu(nn0, m0) + and Agari._is_mentsu(m1) + and Agari._is_mentsu(m2) + and Agari._is_atama_mentsu(nn0, m0) ) if n1 == 2: return ( not (n2 | nn2 | n0 | nn0) - and self._is_mentsu(m2) - and self._is_mentsu(m0) - and self._is_atama_mentsu(nn1, m1) + and Agari._is_mentsu(m2) + and Agari._is_mentsu(m0) + and Agari._is_atama_mentsu(nn1, m1) ) if n2 == 2: return ( not (n0 | nn0 | n1 | nn1) - and self._is_mentsu(m0) - and self._is_mentsu(m1) - and self._is_atama_mentsu(nn2, m2) + and Agari._is_mentsu(m0) + and Agari._is_mentsu(m1) + and Agari._is_atama_mentsu(nn2, m2) ) return False - def _is_mentsu(self, m: int) -> bool: + @staticmethod + def _is_mentsu(m: int) -> bool: a = m & 7 b = 0 c = 0 @@ -180,31 +182,33 @@ def _is_mentsu(self, m: int) -> bool: return a == 0 or a == 3 - def _is_atama_mentsu(self, nn: int, m: int) -> bool: + @staticmethod + def _is_atama_mentsu(nn: int, m: int) -> bool: if nn == 0: - if (m & (7 << 6)) >= (2 << 6) and self._is_mentsu(m - (2 << 6)): + if (m & (7 << 6)) >= (2 << 6) and Agari._is_mentsu(m - (2 << 6)): return True - if (m & (7 << 15)) >= (2 << 15) and self._is_mentsu(m - (2 << 15)): + if (m & (7 << 15)) >= (2 << 15) and Agari._is_mentsu(m - (2 << 15)): return True - if (m & (7 << 24)) >= (2 << 24) and self._is_mentsu(m - (2 << 24)): + if (m & (7 << 24)) >= (2 << 24) and Agari._is_mentsu(m - (2 << 24)): return True elif nn == 1: - if (m & (7 << 3)) >= (2 << 3) and self._is_mentsu(m - (2 << 3)): + if (m & (7 << 3)) >= (2 << 3) and Agari._is_mentsu(m - (2 << 3)): return True - if (m & (7 << 12)) >= (2 << 12) and self._is_mentsu(m - (2 << 12)): + if (m & (7 << 12)) >= (2 << 12) and Agari._is_mentsu(m - (2 << 12)): return True - if (m & (7 << 21)) >= (2 << 21) and self._is_mentsu(m - (2 << 21)): + if (m & (7 << 21)) >= (2 << 21) and Agari._is_mentsu(m - (2 << 21)): return True elif nn == 2: - if (m & (7 << 0)) >= (2 << 0) and self._is_mentsu(m - (2 << 0)): + if (m & (7 << 0)) >= (2 << 0) and Agari._is_mentsu(m - (2 << 0)): return True - if (m & (7 << 9)) >= (2 << 9) and self._is_mentsu(m - (2 << 9)): + if (m & (7 << 9)) >= (2 << 9) and Agari._is_mentsu(m - (2 << 9)): return True - if (m & (7 << 18)) >= (2 << 18) and self._is_mentsu(m - (2 << 18)): + if (m & (7 << 18)) >= (2 << 18) and Agari._is_mentsu(m - (2 << 18)): return True return False - def _to_meld(self, tiles: list[int], d: int) -> int: + @staticmethod + def _to_meld(tiles: list[int], d: int) -> int: result = 0 for i in range(0, 9): result |= tiles[d + i] << i * 3 diff --git a/mahjong/hand_calculating/divider.py b/mahjong/hand_calculating/divider.py index fbd8624..717aa67 100644 --- a/mahjong/hand_calculating/divider.py +++ b/mahjong/hand_calculating/divider.py @@ -1,6 +1,4 @@ -import hashlib import itertools -import marshal from collections.abc import Collection, Sequence from functools import reduce from typing import Optional @@ -11,14 +9,8 @@ class HandDivider: - divider_cache = None - cache_key = None - - def __init__(self) -> None: - self.divider_cache = {} - + @staticmethod def divide_hand( - self, tiles_34: Sequence[int], melds: Optional[Collection[Meld]] = None, use_cache: bool = False, @@ -32,11 +24,6 @@ def divide_hand( if not melds: melds = [] - if use_cache: - self.cache_key = self._build_divider_cache_key(tiles_34, melds) - if self.cache_key in self.divider_cache: - return self.divider_cache[self.cache_key] - closed_hand_tiles_34 = list(tiles_34) # small optimization, we can't have a pair in open part of the hand, @@ -45,7 +32,7 @@ def divide_hand( for open_item in open_tile_indices: closed_hand_tiles_34[open_item] -= 1 - pair_indices = self.find_pairs(closed_hand_tiles_34) + pair_indices = HandDivider.find_pairs(closed_hand_tiles_34) # let's try to find all possible hand options hands: list[list[list[int]]] = [] @@ -59,13 +46,13 @@ def divide_hand( local_tiles_34[pair_index] -= 2 # 0 - 8 man tiles - man = self.find_valid_combinations(local_tiles_34, 0, 8) + man = HandDivider.find_valid_combinations(local_tiles_34, 0, 8) # 9 - 17 pin tiles - pin = self.find_valid_combinations(local_tiles_34, 9, 17) + pin = HandDivider.find_valid_combinations(local_tiles_34, 9, 17) # 18 - 26 sou tiles - sou = self.find_valid_combinations(local_tiles_34, 18, 26) + sou = HandDivider.find_valid_combinations(local_tiles_34, 18, 26) honor: list = [] for x in HONOR_INDICES: @@ -119,12 +106,10 @@ def divide_hand( result = sorted(hands) - if use_cache: - self.divider_cache[self.cache_key] = result - return result - def find_pairs(self, tiles_34: Sequence[int], first_index: int = 0, second_index: int = 33) -> list[int]: + @staticmethod + def find_pairs(tiles_34: Sequence[int], first_index: int = 0, second_index: int = 33) -> list[int]: """ Find all possible pairs in the hand and return their indices :return: array of pair indices @@ -140,8 +125,8 @@ def find_pairs(self, tiles_34: Sequence[int], first_index: int = 0, second_index return pair_indices + @staticmethod def find_valid_combinations( - self, tiles_34: Sequence[int], first_index: int, second_index: int, @@ -248,11 +233,3 @@ def is_valid_combination(possible_set: tuple[int, int, int]) -> bool: combinations_results.append(results) return combinations_results - - def clear_cache(self) -> None: - self.divider_cache = {} - self.cache_key = None - - def _build_divider_cache_key(self, tiles_34: Sequence[int], melds: Collection[Meld]) -> str: - prepared_array = list(tiles_34) + [x.tiles for x in melds] if melds else list(tiles_34) - return hashlib.md5(marshal.dumps(prepared_array)).hexdigest() diff --git a/mahjong/hand_calculating/fu.py b/mahjong/hand_calculating/fu.py index 3a8d381..9ce9160 100644 --- a/mahjong/hand_calculating/fu.py +++ b/mahjong/hand_calculating/fu.py @@ -29,8 +29,8 @@ class FuCalculator: CLOSED_TERMINAL_KAN = "closed_terminal_kan" OPEN_TERMINAL_KAN = "open_terminal_kan" + @staticmethod def calculate_fu( - self, hand: Collection[Sequence[int]], win_tile: int, win_group: Sequence[int], @@ -154,9 +154,10 @@ def calculate_fu( else: fu_details.append({"fu": 30, "reason": FuCalculator.BASE}) - return fu_details, self.round_fu(fu_details) + return fu_details, FuCalculator.round_fu(fu_details) - def round_fu(self, fu_details: Collection[dict[str, Any]]) -> int: + @staticmethod + def round_fu(fu_details: Collection[dict[str, Any]]) -> int: # 22 -> 30 and etc. fu = sum([x["fu"] for x in fu_details]) return (fu + 9) // 10 * 10 diff --git a/mahjong/hand_calculating/hand.py b/mahjong/hand_calculating/hand.py index efa9b4f..cdb6182 100644 --- a/mahjong/hand_calculating/hand.py +++ b/mahjong/hand_calculating/hand.py @@ -12,10 +12,10 @@ from mahjong.tile import TilesConverter from mahjong.utils import is_aka_dora, is_chi, is_kan, is_pon, plus_dora +_DEFAULT_CONFIG = HandConfig() -class HandCalculator: - config = None +class HandCalculator: ERR_NO_WINNING_TILE = "winning_tile_not_in_hand" ERR_OPEN_HAND_RIICHI = "open_hand_riichi_not_allowed" ERR_OPEN_HAND_DABURI = "open_hand_daburi_not_allowed" @@ -41,11 +41,8 @@ class HandCalculator: # more possible errors, like tenhou and haitei can't be together (so complicated :<) - def __init__(self) -> None: - self.divider = HandDivider() - + @staticmethod def estimate_hand_value( - self, tiles: Collection[int], win_tile: int, melds: Optional[Collection[Meld]] = None, @@ -70,14 +67,12 @@ def estimate_hand_value( if not dora_indicators: dora_indicators = [] - self.config = config or HandConfig() + config = config or _DEFAULT_CONFIG - agari = Agari() hand_yaku = [] scores_calculator = scores_calculator_factory() tiles_34 = TilesConverter.to_34_array(tiles) - fu_calculator = FuCalculator() is_aotenjou = isinstance(scores_calculator, Aotenjou) opened_melds = [x.tiles_34 for x in melds if x.opened] @@ -85,98 +80,98 @@ def estimate_hand_value( is_open_hand = len(opened_melds) > 0 # special situation - if self.config.is_nagashi_mangan: - hand_yaku.append(self.config.yaku.nagashi_mangan) + if config.is_nagashi_mangan: + hand_yaku.append(config.yaku.nagashi_mangan) fu = 30 - han = self.config.yaku.nagashi_mangan.han_closed - cost = scores_calculator.calculate_scores(han, fu, self.config, False) + han = config.yaku.nagashi_mangan.han_closed + cost = scores_calculator.calculate_scores(han, fu, config, False) return HandResponse(cost, han, fu, hand_yaku) if win_tile not in tiles: return HandResponse(error=HandCalculator.ERR_NO_WINNING_TILE) - if self.config.is_riichi and not self.config.is_daburu_riichi and is_open_hand: + if config.is_riichi and not config.is_daburu_riichi and is_open_hand: return HandResponse(error=HandCalculator.ERR_OPEN_HAND_RIICHI) - if self.config.is_daburu_riichi and is_open_hand: + if config.is_daburu_riichi and is_open_hand: return HandResponse(error=HandCalculator.ERR_OPEN_HAND_DABURI) - if self.config.is_ippatsu and not self.config.is_riichi and not self.config.is_daburu_riichi: + if config.is_ippatsu and not config.is_riichi and not config.is_daburu_riichi: return HandResponse(error=HandCalculator.ERR_IPPATSU_WITHOUT_RIICHI) - if self.config.is_chankan and self.config.is_tsumo: + if config.is_chankan and config.is_tsumo: return HandResponse(error=HandCalculator.ERR_CHANKAN_WITH_TSUMO) - if self.config.is_rinshan and not self.config.is_tsumo: + if config.is_rinshan and not config.is_tsumo: return HandResponse(error=HandCalculator.ERR_RINSHAN_WITHOUT_TSUMO) - if self.config.is_haitei and not self.config.is_tsumo: + if config.is_haitei and not config.is_tsumo: return HandResponse(error=HandCalculator.ERR_HAITEI_WITHOUT_TSUMO) - if self.config.is_houtei and self.config.is_tsumo: + if config.is_houtei and config.is_tsumo: return HandResponse(error=HandCalculator.ERR_HOUTEI_WITH_TSUMO) - if self.config.is_haitei and self.config.is_rinshan: + if config.is_haitei and config.is_rinshan: return HandResponse(error=HandCalculator.ERR_HAITEI_WITH_RINSHAN) - if self.config.is_houtei and self.config.is_chankan: + if config.is_houtei and config.is_chankan: return HandResponse(error=HandCalculator.ERR_HOUTEI_WITH_CHANKAN) # raise error only when player wind is defined (and is *not* EAST) - if self.config.is_tenhou and self.config.player_wind and not self.config.is_dealer: + if config.is_tenhou and config.player_wind and not config.is_dealer: return HandResponse(error=HandCalculator.ERR_TENHOU_NOT_AS_DEALER) - if self.config.is_tenhou and not self.config.is_tsumo: + if config.is_tenhou and not config.is_tsumo: return HandResponse(error=HandCalculator.ERR_TENHOU_WITHOUT_TSUMO) - if self.config.is_tenhou and melds: + if config.is_tenhou and melds: return HandResponse(error=HandCalculator.ERR_TENHOU_WITH_MELD) # raise error only when player wind is defined (and is EAST) - if self.config.is_chiihou and self.config.player_wind and self.config.is_dealer: + if config.is_chiihou and config.player_wind and config.is_dealer: return HandResponse(error=HandCalculator.ERR_CHIIHOU_AS_DEALER) - if self.config.is_chiihou and not self.config.is_tsumo: + if config.is_chiihou and not config.is_tsumo: return HandResponse(error=HandCalculator.ERR_CHIIHOU_WITHOUT_TSUMO) - if self.config.is_chiihou and melds: + if config.is_chiihou and melds: return HandResponse(error=HandCalculator.ERR_CHIIHOU_WITH_MELD) # raise error only when player wind is defined (and is EAST) - if self.config.is_renhou and self.config.player_wind and self.config.is_dealer: + if config.is_renhou and config.player_wind and config.is_dealer: return HandResponse(error=HandCalculator.ERR_RENHOU_AS_DEALER) - if self.config.is_renhou and self.config.is_tsumo: + if config.is_renhou and config.is_tsumo: return HandResponse(error=HandCalculator.ERR_RENHOU_WITH_TSUMO) - if self.config.is_renhou and melds: + if config.is_renhou and melds: return HandResponse(error=HandCalculator.ERR_RENHOU_WITH_MELD) - if not agari.is_agari(tiles_34, all_melds): + if not Agari.is_agari(tiles_34, all_melds): return HandResponse(error=HandCalculator.ERR_HAND_NOT_WINNING) - if not self.config.options.has_double_yakuman: - self.config.yaku.daburu_kokushi.han_closed = 13 - self.config.yaku.suuankou_tanki.han_closed = 13 - self.config.yaku.daburu_chuuren_poutou.han_closed = 13 - self.config.yaku.daisuushi.han_closed = 13 - self.config.yaku.daisuushi.han_open = 13 + if not config.options.has_double_yakuman: + config.yaku.daburu_kokushi.han_closed = 13 + config.yaku.suuankou_tanki.han_closed = 13 + config.yaku.daburu_chuuren_poutou.han_closed = 13 + config.yaku.daisuushi.han_closed = 13 + config.yaku.daisuushi.han_open = 13 - hand_options = self.divider.divide_hand(tiles_34, melds, use_cache=use_hand_divider_cache) + hand_options = HandDivider.divide_hand(tiles_34, melds, use_cache=use_hand_divider_cache) calculated_hands = [] for hand in hand_options: - is_chiitoitsu = self.config.yaku.chiitoitsu.is_condition_met(hand) - valued_tiles = [HAKU, HATSU, CHUN, self.config.player_wind, self.config.round_wind] + is_chiitoitsu = config.yaku.chiitoitsu.is_condition_met(hand) + valued_tiles = [HAKU, HATSU, CHUN, config.player_wind, config.round_wind] - win_groups = self._find_win_groups(win_tile, hand, opened_melds) + win_groups = HandCalculator._find_win_groups(win_tile, hand, opened_melds) for win_group in win_groups: cost = None error = None hand_yaku = [] han = 0 - fu_details, fu = fu_calculator.calculate_fu(hand, win_tile, win_group, self.config, valued_tiles, melds) + fu_details, fu = FuCalculator.calculate_fu(hand, win_tile, win_group, config, valued_tiles, melds) is_pinfu = len(fu_details) == 1 and not is_chiitoitsu and not is_open_hand @@ -184,214 +179,207 @@ def estimate_hand_value( kan_sets = [x for x in hand if is_kan(x)] chi_sets = [x for x in hand if is_chi(x)] - if self.config.is_tsumo: + if config.is_tsumo: if not is_open_hand: - hand_yaku.append(self.config.yaku.tsumo) + hand_yaku.append(config.yaku.tsumo) if is_pinfu: - hand_yaku.append(self.config.yaku.pinfu) + hand_yaku.append(config.yaku.pinfu) # let's skip hand that looks like chitoitsu, but it contains open sets if is_chiitoitsu and is_open_hand: continue if is_chiitoitsu: - hand_yaku.append(self.config.yaku.chiitoitsu) + hand_yaku.append(config.yaku.chiitoitsu) - is_daisharin = self.config.yaku.daisharin.is_condition_met( - hand, self.config.options.has_daisharin_other_suits - ) - if self.config.options.has_daisharin and is_daisharin: - self.config.yaku.daisharin.rename(hand) - hand_yaku.append(self.config.yaku.daisharin) + is_daisharin = config.yaku.daisharin.is_condition_met(hand, config.options.has_daisharin_other_suits) + if config.options.has_daisharin and is_daisharin: + config.yaku.daisharin.rename(hand) + hand_yaku.append(config.yaku.daisharin) - if self.config.options.has_daichisei and self.config.yaku.daichisei.is_condition_met(hand): - hand_yaku.append(self.config.yaku.daichisei) + if config.options.has_daichisei and config.yaku.daichisei.is_condition_met(hand): + hand_yaku.append(config.yaku.daichisei) - is_tanyao = self.config.yaku.tanyao.is_condition_met(hand) - if is_open_hand and not self.config.options.has_open_tanyao: + is_tanyao = config.yaku.tanyao.is_condition_met(hand) + if is_open_hand and not config.options.has_open_tanyao: is_tanyao = False if is_tanyao: - hand_yaku.append(self.config.yaku.tanyao) + hand_yaku.append(config.yaku.tanyao) - if self.config.is_riichi and not self.config.is_daburu_riichi: - if self.config.is_open_riichi: - hand_yaku.append(self.config.yaku.open_riichi) + if config.is_riichi and not config.is_daburu_riichi: + if config.is_open_riichi: + hand_yaku.append(config.yaku.open_riichi) else: - hand_yaku.append(self.config.yaku.riichi) + hand_yaku.append(config.yaku.riichi) - if self.config.is_daburu_riichi: - if self.config.is_open_riichi: - hand_yaku.append(self.config.yaku.daburu_open_riichi) + if config.is_daburu_riichi: + if config.is_open_riichi: + hand_yaku.append(config.yaku.daburu_open_riichi) else: - hand_yaku.append(self.config.yaku.daburu_riichi) + hand_yaku.append(config.yaku.daburu_riichi) if ( - not self.config.is_tsumo - and self.config.options.has_sashikomi_yakuman - and ( - (self.config.yaku.daburu_open_riichi in hand_yaku) - or (self.config.yaku.open_riichi in hand_yaku) - ) + not config.is_tsumo + and config.options.has_sashikomi_yakuman + and ((config.yaku.daburu_open_riichi in hand_yaku) or (config.yaku.open_riichi in hand_yaku)) ): - hand_yaku.append(self.config.yaku.sashikomi) + hand_yaku.append(config.yaku.sashikomi) - if self.config.is_ippatsu: - hand_yaku.append(self.config.yaku.ippatsu) + if config.is_ippatsu: + hand_yaku.append(config.yaku.ippatsu) - if self.config.is_rinshan: - hand_yaku.append(self.config.yaku.rinshan) + if config.is_rinshan: + hand_yaku.append(config.yaku.rinshan) - if self.config.is_chankan: - hand_yaku.append(self.config.yaku.chankan) + if config.is_chankan: + hand_yaku.append(config.yaku.chankan) - if self.config.is_haitei: - hand_yaku.append(self.config.yaku.haitei) + if config.is_haitei: + hand_yaku.append(config.yaku.haitei) - if self.config.is_houtei: - hand_yaku.append(self.config.yaku.houtei) + if config.is_houtei: + hand_yaku.append(config.yaku.houtei) - if self.config.is_renhou: - if self.config.options.renhou_as_yakuman: - hand_yaku.append(self.config.yaku.renhou_yakuman) + if config.is_renhou: + if config.options.renhou_as_yakuman: + hand_yaku.append(config.yaku.renhou_yakuman) else: - hand_yaku.append(self.config.yaku.renhou) + hand_yaku.append(config.yaku.renhou) - if self.config.is_tenhou: - hand_yaku.append(self.config.yaku.tenhou) + if config.is_tenhou: + hand_yaku.append(config.yaku.tenhou) - if self.config.is_chiihou: - hand_yaku.append(self.config.yaku.chiihou) + if config.is_chiihou: + hand_yaku.append(config.yaku.chiihou) - if self.config.yaku.honitsu.is_condition_met(hand): - hand_yaku.append(self.config.yaku.honitsu) + if config.yaku.honitsu.is_condition_met(hand): + hand_yaku.append(config.yaku.honitsu) - if self.config.yaku.chinitsu.is_condition_met(hand): - hand_yaku.append(self.config.yaku.chinitsu) + if config.yaku.chinitsu.is_condition_met(hand): + hand_yaku.append(config.yaku.chinitsu) - if self.config.yaku.tsuisou.is_condition_met(hand): - hand_yaku.append(self.config.yaku.tsuisou) + if config.yaku.tsuisou.is_condition_met(hand): + hand_yaku.append(config.yaku.tsuisou) - if self.config.yaku.honroto.is_condition_met(hand): - hand_yaku.append(self.config.yaku.honroto) + if config.yaku.honroto.is_condition_met(hand): + hand_yaku.append(config.yaku.honroto) - if self.config.yaku.chinroto.is_condition_met(hand): - hand_yaku.append(self.config.yaku.chinroto) + if config.yaku.chinroto.is_condition_met(hand): + hand_yaku.append(config.yaku.chinroto) - if self.config.yaku.ryuisou.is_condition_met(hand): - hand_yaku.append(self.config.yaku.ryuisou) + if config.yaku.ryuisou.is_condition_met(hand): + hand_yaku.append(config.yaku.ryuisou) - if self.config.paarenchan > 0 and not self.config.options.paarenchan_needs_yaku: + if config.paarenchan > 0 and not config.options.paarenchan_needs_yaku: # if no yaku is even needed to win on paarenchan and it is paarenchan condition, just add paarenchan - self.config.yaku.paarenchan.set_paarenchan_count(self.config.paarenchan) - hand_yaku.append(self.config.yaku.paarenchan) + config.yaku.paarenchan.set_paarenchan_count(config.paarenchan) + hand_yaku.append(config.yaku.paarenchan) # small optimization, try to detect yaku with chi required sets only if we have chi sets in hand if len(chi_sets): - if self.config.yaku.chantai.is_condition_met(hand): - hand_yaku.append(self.config.yaku.chantai) + if config.yaku.chantai.is_condition_met(hand): + hand_yaku.append(config.yaku.chantai) - if self.config.yaku.junchan.is_condition_met(hand): - hand_yaku.append(self.config.yaku.junchan) + if config.yaku.junchan.is_condition_met(hand): + hand_yaku.append(config.yaku.junchan) - if self.config.yaku.ittsu.is_condition_met(hand): - hand_yaku.append(self.config.yaku.ittsu) + if config.yaku.ittsu.is_condition_met(hand): + hand_yaku.append(config.yaku.ittsu) if not is_open_hand: - if self.config.yaku.ryanpeiko.is_condition_met(hand): - hand_yaku.append(self.config.yaku.ryanpeiko) - elif self.config.yaku.iipeiko.is_condition_met(hand): - hand_yaku.append(self.config.yaku.iipeiko) + if config.yaku.ryanpeiko.is_condition_met(hand): + hand_yaku.append(config.yaku.ryanpeiko) + elif config.yaku.iipeiko.is_condition_met(hand): + hand_yaku.append(config.yaku.iipeiko) - if self.config.yaku.sanshoku.is_condition_met(hand): - hand_yaku.append(self.config.yaku.sanshoku) + if config.yaku.sanshoku.is_condition_met(hand): + hand_yaku.append(config.yaku.sanshoku) # small optimization, try to detect yaku with pon required sets only if we have pon sets in hand if len(pon_sets) or len(kan_sets): - if self.config.yaku.toitoi.is_condition_met(hand): - hand_yaku.append(self.config.yaku.toitoi) + if config.yaku.toitoi.is_condition_met(hand): + hand_yaku.append(config.yaku.toitoi) - if self.config.yaku.sanankou.is_condition_met(hand, win_tile, melds, self.config.is_tsumo): - hand_yaku.append(self.config.yaku.sanankou) + if config.yaku.sanankou.is_condition_met(hand, win_tile, melds, config.is_tsumo): + hand_yaku.append(config.yaku.sanankou) - if self.config.yaku.sanshoku_douko.is_condition_met(hand): - hand_yaku.append(self.config.yaku.sanshoku_douko) + if config.yaku.sanshoku_douko.is_condition_met(hand): + hand_yaku.append(config.yaku.sanshoku_douko) - if self.config.yaku.shosangen.is_condition_met(hand): - hand_yaku.append(self.config.yaku.shosangen) + if config.yaku.shosangen.is_condition_met(hand): + hand_yaku.append(config.yaku.shosangen) - if self.config.yaku.haku.is_condition_met(hand): - hand_yaku.append(self.config.yaku.haku) + if config.yaku.haku.is_condition_met(hand): + hand_yaku.append(config.yaku.haku) - if self.config.yaku.hatsu.is_condition_met(hand): - hand_yaku.append(self.config.yaku.hatsu) + if config.yaku.hatsu.is_condition_met(hand): + hand_yaku.append(config.yaku.hatsu) - if self.config.yaku.chun.is_condition_met(hand): - hand_yaku.append(self.config.yaku.chun) + if config.yaku.chun.is_condition_met(hand): + hand_yaku.append(config.yaku.chun) - if self.config.yaku.east.is_condition_met(hand, self.config.player_wind, self.config.round_wind): - if self.config.player_wind == EAST: - hand_yaku.append(self.config.yaku.yakuhai_place) + if config.yaku.east.is_condition_met(hand, config.player_wind, config.round_wind): + if config.player_wind == EAST: + hand_yaku.append(config.yaku.yakuhai_place) - if self.config.round_wind == EAST: - hand_yaku.append(self.config.yaku.yakuhai_round) + if config.round_wind == EAST: + hand_yaku.append(config.yaku.yakuhai_round) - if self.config.yaku.south.is_condition_met(hand, self.config.player_wind, self.config.round_wind): - if self.config.player_wind == SOUTH: - hand_yaku.append(self.config.yaku.yakuhai_place) + if config.yaku.south.is_condition_met(hand, config.player_wind, config.round_wind): + if config.player_wind == SOUTH: + hand_yaku.append(config.yaku.yakuhai_place) - if self.config.round_wind == SOUTH: - hand_yaku.append(self.config.yaku.yakuhai_round) + if config.round_wind == SOUTH: + hand_yaku.append(config.yaku.yakuhai_round) - if self.config.yaku.west.is_condition_met(hand, self.config.player_wind, self.config.round_wind): - if self.config.player_wind == WEST: - hand_yaku.append(self.config.yaku.yakuhai_place) + if config.yaku.west.is_condition_met(hand, config.player_wind, config.round_wind): + if config.player_wind == WEST: + hand_yaku.append(config.yaku.yakuhai_place) - if self.config.round_wind == WEST: - hand_yaku.append(self.config.yaku.yakuhai_round) + if config.round_wind == WEST: + hand_yaku.append(config.yaku.yakuhai_round) - if self.config.yaku.north.is_condition_met(hand, self.config.player_wind, self.config.round_wind): - if self.config.player_wind == NORTH: - hand_yaku.append(self.config.yaku.yakuhai_place) + if config.yaku.north.is_condition_met(hand, config.player_wind, config.round_wind): + if config.player_wind == NORTH: + hand_yaku.append(config.yaku.yakuhai_place) - if self.config.round_wind == NORTH: - hand_yaku.append(self.config.yaku.yakuhai_round) + if config.round_wind == NORTH: + hand_yaku.append(config.yaku.yakuhai_round) - if self.config.yaku.daisangen.is_condition_met(hand): - hand_yaku.append(self.config.yaku.daisangen) + if config.yaku.daisangen.is_condition_met(hand): + hand_yaku.append(config.yaku.daisangen) - if self.config.yaku.shosuushi.is_condition_met(hand): - hand_yaku.append(self.config.yaku.shosuushi) + if config.yaku.shosuushi.is_condition_met(hand): + hand_yaku.append(config.yaku.shosuushi) - if self.config.yaku.daisuushi.is_condition_met(hand): - hand_yaku.append(self.config.yaku.daisuushi) + if config.yaku.daisuushi.is_condition_met(hand): + hand_yaku.append(config.yaku.daisuushi) # closed kan can't be used in chuuren_poutou - if not len(melds) and self.config.yaku.chuuren_poutou.is_condition_met(hand): + if not len(melds) and config.yaku.chuuren_poutou.is_condition_met(hand): if tiles_34[win_tile // 4] == 2 or tiles_34[win_tile // 4] == 4: - hand_yaku.append(self.config.yaku.daburu_chuuren_poutou) + hand_yaku.append(config.yaku.daburu_chuuren_poutou) else: - hand_yaku.append(self.config.yaku.chuuren_poutou) + hand_yaku.append(config.yaku.chuuren_poutou) - if not is_open_hand and self.config.yaku.suuankou.is_condition_met( - hand, win_tile, self.config.is_tsumo - ): + if not is_open_hand and config.yaku.suuankou.is_condition_met(hand, win_tile, config.is_tsumo): if tiles_34[win_tile // 4] == 2: - hand_yaku.append(self.config.yaku.suuankou_tanki) + hand_yaku.append(config.yaku.suuankou_tanki) else: - hand_yaku.append(self.config.yaku.suuankou) + hand_yaku.append(config.yaku.suuankou) - if self.config.yaku.sankantsu.is_condition_met(hand, melds): - hand_yaku.append(self.config.yaku.sankantsu) + if config.yaku.sankantsu.is_condition_met(hand, melds): + hand_yaku.append(config.yaku.sankantsu) - if self.config.yaku.suukantsu.is_condition_met(hand, melds): - hand_yaku.append(self.config.yaku.suukantsu) + if config.yaku.suukantsu.is_condition_met(hand, melds): + hand_yaku.append(config.yaku.suukantsu) - if self.config.paarenchan > 0 and self.config.options.paarenchan_needs_yaku and len(hand_yaku) > 0: + if config.paarenchan > 0 and config.options.paarenchan_needs_yaku and len(hand_yaku) > 0: # we waited until here to add paarenchan yakuman only if there is any other yaku - self.config.yaku.paarenchan.set_paarenchan_count(self.config.paarenchan) - hand_yaku.append(self.config.yaku.paarenchan) + config.yaku.paarenchan.set_paarenchan_count(config.paarenchan) + hand_yaku.append(config.yaku.paarenchan) # yakuman is not connected with other yaku yakuman_list = [x for x in hand_yaku if x.is_yakuman] @@ -399,7 +387,7 @@ def estimate_hand_value( if not is_aotenjou: hand_yaku = yakuman_list else: - scores_calculator.aotenjou_filter_yaku(hand_yaku, self.config) + scores_calculator.aotenjou_filter_yaku(hand_yaku, config) yakuman_list = [] # calculate han @@ -424,29 +412,29 @@ def estimate_hand_value( count_of_dora += plus_dora(tile, dora_indicators) for tile in tiles_for_dora: - if is_aka_dora(tile, self.config.options.has_aka_dora): + if is_aka_dora(tile, config.options.has_aka_dora): count_of_aka_dora += 1 if count_of_dora: - self.config.yaku.dora.han_open = count_of_dora - self.config.yaku.dora.han_closed = count_of_dora - hand_yaku.append(self.config.yaku.dora) + config.yaku.dora.han_open = count_of_dora + config.yaku.dora.han_closed = count_of_dora + hand_yaku.append(config.yaku.dora) han += count_of_dora if count_of_aka_dora: - self.config.yaku.aka_dora.han_open = count_of_aka_dora - self.config.yaku.aka_dora.han_closed = count_of_aka_dora - hand_yaku.append(self.config.yaku.aka_dora) + config.yaku.aka_dora.han_open = count_of_aka_dora + config.yaku.aka_dora.han_closed = count_of_aka_dora + hand_yaku.append(config.yaku.aka_dora) han += count_of_aka_dora - if not is_aotenjou and (self.config.options.limit_to_sextuple_yakuman and han > 78): + if not is_aotenjou and (config.options.limit_to_sextuple_yakuman and han > 78): han = 78 if fu == 0 and is_aotenjou: fu = 40 if not error: - cost = scores_calculator.calculate_scores(han, fu, self.config, len(yakuman_list) > 0) + cost = scores_calculator.calculate_scores(han, fu, config, len(yakuman_list) > 0) calculated_hand = { "cost": cost, @@ -460,33 +448,33 @@ def estimate_hand_value( calculated_hands.append(calculated_hand) # exception hand - if not is_open_hand and self.config.yaku.kokushi.is_condition_met(None, tiles_34): + if not is_open_hand and config.yaku.kokushi.is_condition_met(None, tiles_34): if tiles_34[win_tile // 4] == 2: - hand_yaku.append(self.config.yaku.daburu_kokushi) + hand_yaku.append(config.yaku.daburu_kokushi) else: - hand_yaku.append(self.config.yaku.kokushi) + hand_yaku.append(config.yaku.kokushi) - if not self.config.is_tsumo and self.config.options.has_sashikomi_yakuman: - if self.config.is_riichi and not self.config.is_daburu_riichi: - if self.config.is_open_riichi: - hand_yaku.append(self.config.yaku.sashikomi) + if not config.is_tsumo and config.options.has_sashikomi_yakuman: + if config.is_riichi and not config.is_daburu_riichi: + if config.is_open_riichi: + hand_yaku.append(config.yaku.sashikomi) - if self.config.is_daburu_riichi: - if self.config.is_open_riichi: - hand_yaku.append(self.config.yaku.sashikomi) + if config.is_daburu_riichi: + if config.is_open_riichi: + hand_yaku.append(config.yaku.sashikomi) - if self.config.is_renhou and self.config.options.renhou_as_yakuman: - hand_yaku.append(self.config.yaku.renhou_yakuman) + if config.is_renhou and config.options.renhou_as_yakuman: + hand_yaku.append(config.yaku.renhou_yakuman) - if self.config.is_tenhou: - hand_yaku.append(self.config.yaku.tenhou) + if config.is_tenhou: + hand_yaku.append(config.yaku.tenhou) - if self.config.is_chiihou: - hand_yaku.append(self.config.yaku.chiihou) + if config.is_chiihou: + hand_yaku.append(config.yaku.chiihou) - if self.config.paarenchan > 0: - self.config.yaku.paarenchan.set_paarenchan_count(self.config.paarenchan) - hand_yaku.append(self.config.yaku.paarenchan) + if config.paarenchan > 0: + config.yaku.paarenchan.set_paarenchan_count(config.paarenchan) + hand_yaku.append(config.yaku.paarenchan) # calculate han han = 0 @@ -498,7 +486,7 @@ def estimate_hand_value( fu = 0 if is_aotenjou: - if self.config.is_tsumo: + if config.is_tsumo: fu = 30 else: fu = 40 @@ -512,22 +500,22 @@ def estimate_hand_value( count_of_dora += plus_dora(tile, dora_indicators) for tile in tiles_for_dora: - if is_aka_dora(tile, self.config.options.has_aka_dora): + if is_aka_dora(tile, config.options.has_aka_dora): count_of_aka_dora += 1 if count_of_dora: - self.config.yaku.dora.han_open = count_of_dora - self.config.yaku.dora.han_closed = count_of_dora - hand_yaku.append(self.config.yaku.dora) + config.yaku.dora.han_open = count_of_dora + config.yaku.dora.han_closed = count_of_dora + hand_yaku.append(config.yaku.dora) han += count_of_dora if count_of_aka_dora: - self.config.yaku.aka_dora.han_open = count_of_aka_dora - self.config.yaku.aka_dora.han_closed = count_of_aka_dora - hand_yaku.append(self.config.yaku.aka_dora) + config.yaku.aka_dora.han_open = count_of_aka_dora + config.yaku.aka_dora.han_closed = count_of_aka_dora + hand_yaku.append(config.yaku.aka_dora) han += count_of_aka_dora - cost = scores_calculator.calculate_scores(han, fu, self.config, len(hand_yaku) > 0) + cost = scores_calculator.calculate_scores(han, fu, config, len(hand_yaku) > 0) calculated_hands.append( {"cost": cost, "error": None, "hand_yaku": hand_yaku, "han": han, "fu": fu, "fu_details": []} ) @@ -555,7 +543,8 @@ def estimate_hand_value( return HandResponse(cost, han, fu, hand_yaku, error, fu_details, is_open_hand) - def _find_win_groups(self, win_tile: int, hand: list[list[int]], opened_melds: list[list[int]]) -> list[list[int]]: + @staticmethod + def _find_win_groups(win_tile: int, hand: list[list[int]], opened_melds: list[list[int]]) -> list[list[int]]: win_tile_34 = (win_tile or 0) // 4 _opened_melds = opened_melds[:] diff --git a/mahjong/hand_calculating/scores.py b/mahjong/hand_calculating/scores.py index 38174a9..7b12067 100644 --- a/mahjong/hand_calculating/scores.py +++ b/mahjong/hand_calculating/scores.py @@ -6,7 +6,8 @@ class ScoresCalculator: - def calculate_scores(self, han: int, fu: int, config: HandConfig, is_yakuman: bool = False) -> dict[str, Any]: + @staticmethod + def calculate_scores(han: int, fu: int, config: HandConfig, is_yakuman: bool = False) -> dict[str, Any]: """ Calculate how much scores cost a hand with given han and fu :param han: int @@ -166,7 +167,8 @@ def calculate_scores(self, han: int, fu: int, config: HandConfig, is_yakuman: bo class Aotenjou(ScoresCalculator): - def calculate_scores(self, han: int, fu: int, config: HandConfig, is_yakuman: bool = False) -> dict[str, int]: + @staticmethod + def calculate_scores(han: int, fu: int, config: HandConfig, is_yakuman: bool = False) -> dict[str, int]: base_points: int = fu * pow(2, 2 + han) rounded = (base_points + 99) // 100 * 100 double_rounded = (2 * base_points + 99) // 100 * 100 @@ -178,11 +180,8 @@ def calculate_scores(self, han: int, fu: int, config: HandConfig, is_yakuman: bo else: return {"main": config.is_dealer and six_rounded or four_rounded, "additional": 0} - def aotenjou_filter_yaku( - self, - hand_yaku: Union[MutableSequence[Yaku], MutableSet[Yaku]], - config: HandConfig, - ) -> None: + @staticmethod + def aotenjou_filter_yaku(hand_yaku: Union[MutableSequence[Yaku], MutableSet[Yaku]], config: HandConfig) -> None: # in aotenjou yakumans are normal yaku # but we need to filter lower yaku that are precursors to yakumans if config.yaku.daisangen in hand_yaku: diff --git a/mahjong/shanten.py b/mahjong/shanten.py index d9598ea..d26b954 100644 --- a/mahjong/shanten.py +++ b/mahjong/shanten.py @@ -1,4 +1,3 @@ -import warnings from collections.abc import Sequence from mahjong.constants import HONOR_INDICES, TERMINAL_INDICES @@ -7,174 +6,23 @@ class Shanten: AGARI_STATE = -1 - _tiles: list[int] = [] - _number_melds = 0 - _number_tatsu = 0 - _number_pairs = 0 - _number_jidahai = 0 - _flag_four_copies = 0 - _flag_isolated_tiles = 0 - _min_shanten = 0 - - @property - def tiles(self) -> list[int]: - warnings.warn( - "`tiles` is deprecated. This attribute reflects internal state and should not be used.", - DeprecationWarning, - stacklevel=2, - ) - return self._tiles - - @tiles.setter - def tiles(self, value: list[int]) -> None: - warnings.warn( - "`tiles` is deprecated. This attribute reflects internal state and should not be used.", - DeprecationWarning, - stacklevel=2, - ) - self._tiles = value - - @property - def number_melds(self) -> int: - warnings.warn( - "`number_melds` is deprecated. This attribute reflects internal state and should not be used.", - DeprecationWarning, - stacklevel=2, - ) - return self._number_melds - - @number_melds.setter - def number_melds(self, value: int) -> None: - warnings.warn( - "`number_melds` is deprecated. This attribute reflects internal state and should not be used.", - DeprecationWarning, - stacklevel=2, - ) - self._number_melds = value - - @property - def number_tatsu(self) -> int: - warnings.warn( - "`number_tatsu` is deprecated. This attribute reflects internal state and should not be used.", - DeprecationWarning, - stacklevel=2, - ) - return self._number_tatsu - - @number_tatsu.setter - def number_tatsu(self, value: int) -> None: - warnings.warn( - "`number_tatsu` is deprecated. This attribute reflects internal state and should not be used.", - DeprecationWarning, - stacklevel=2, - ) - self._number_tatsu = value - - @property - def number_pairs(self) -> int: - warnings.warn( - "`number_pairs` is deprecated. This attribute reflects internal state and should not be used.", - DeprecationWarning, - stacklevel=2, - ) - return self._number_pairs - - @number_pairs.setter - def number_pairs(self, value: int) -> None: - warnings.warn( - "`number_pairs` is deprecated. This attribute reflects internal state and should not be used.", - DeprecationWarning, - stacklevel=2, - ) - self._number_pairs = value - - @property - def number_jidahai(self) -> int: - warnings.warn( - "`number_jidahai` is deprecated. This attribute reflects internal state and should not be used.", - DeprecationWarning, - stacklevel=2, - ) - return self._number_jidahai - - @number_jidahai.setter - def number_jidahai(self, value: int) -> None: - warnings.warn( - "`number_jidahai` is deprecated. This attribute reflects internal state and should not be used.", - DeprecationWarning, - stacklevel=2, - ) - self._number_jidahai = value - - @property - def number_characters(self) -> int: - warnings.warn( - "`number_characters` is deprecated. This attribute reflects internal state and should not be used.", - DeprecationWarning, - stacklevel=2, - ) - return self._flag_four_copies - - @number_characters.setter - def number_characters(self, value: int) -> None: - warnings.warn( - "`number_characters` is deprecated. This attribute reflects internal state and should not be used.", - DeprecationWarning, - stacklevel=2, - ) - self._flag_four_copies = value - - @property - def number_isolated_tiles(self) -> int: - warnings.warn( - "`number_isolated_tiles` is deprecated. This attribute reflects internal state and should not be used.", - DeprecationWarning, - stacklevel=2, - ) - return self._flag_isolated_tiles - - @number_isolated_tiles.setter - def number_isolated_tiles(self, value: int) -> None: - warnings.warn( - "`number_isolated_tiles` is deprecated. This attribute reflects internal state and should not be used.", - DeprecationWarning, - stacklevel=2, - ) - self._flag_isolated_tiles = value - - @property - def min_shanten(self) -> int: - warnings.warn( - "`min_shanten` is deprecated. This attribute reflects internal state and should not be used.", - DeprecationWarning, - stacklevel=2, - ) - return self._min_shanten - - @min_shanten.setter - def min_shanten(self, value: int) -> None: - warnings.warn( - "`min_shanten` is deprecated. This attribute reflects internal state and should not be used.", - DeprecationWarning, - stacklevel=2, - ) - self._min_shanten = value - - def calculate_shanten(self, tiles_34: Sequence[int], use_chiitoitsu: bool = True, use_kokushi: bool = True) -> int: + @staticmethod + def calculate_shanten(tiles_34: Sequence[int], use_chiitoitsu: bool = True, use_kokushi: bool = True) -> int: """ Return the minimum shanten for provided hand, it will consider chiitoitsu and kokushi options if possible. """ - shanten_results = [self.calculate_shanten_for_regular_hand(tiles_34)] + shanten_results = [Shanten.calculate_shanten_for_regular_hand(tiles_34)] if use_chiitoitsu: - shanten_results.append(self.calculate_shanten_for_chiitoitsu_hand(tiles_34)) + shanten_results.append(Shanten.calculate_shanten_for_chiitoitsu_hand(tiles_34)) if use_kokushi: - shanten_results.append(self.calculate_shanten_for_kokushi_hand(tiles_34)) + shanten_results.append(Shanten.calculate_shanten_for_kokushi_hand(tiles_34)) return min(shanten_results) - def calculate_shanten_for_chiitoitsu_hand(self, tiles_34: Sequence[int]) -> int: + @staticmethod + def calculate_shanten_for_chiitoitsu_hand(tiles_34: Sequence[int]) -> int: """ Calculate the number of shanten for chiitoitsu hand """ @@ -185,7 +33,8 @@ def calculate_shanten_for_chiitoitsu_hand(self, tiles_34: Sequence[int]) -> int: kinds = len([x for x in tiles_34 if x >= 1]) return 6 - pairs + (7 - kinds if kinds < 7 else 0) - def calculate_shanten_for_kokushi_hand(self, tiles_34: Sequence[int]) -> int: + @staticmethod + def calculate_shanten_for_kokushi_hand(tiles_34: Sequence[int]) -> int: """ Calculate the number of shanten for kokushi musou hand """ @@ -199,16 +48,28 @@ def calculate_shanten_for_kokushi_hand(self, tiles_34: Sequence[int]) -> int: return 13 - terminals - (completed_terminals and 1 or 0) - def calculate_shanten_for_regular_hand(self, tiles_34: Sequence[int]) -> int: + @staticmethod + def calculate_shanten_for_regular_hand(tiles_34: Sequence[int]) -> int: """ Calculate the number of shanten for regular hand """ - # we will modify tiles array later, so we need to use a copy - tiles_34 = list(tiles_34) + return _RegularShanten(tiles_34).calculate() - self._init(tiles_34) - count_of_tiles = sum(tiles_34) +class _RegularShanten: + def __init__(self, tiles_34: Sequence[int]) -> None: + # we will modify tiles array later, so we need to use a copy + self._tiles = list(tiles_34) + self._number_melds = 0 + self._number_tatsu = 0 + self._number_pairs = 0 + self._number_jidahai = 0 + self._flag_four_copies = 0 + self._flag_isolated_tiles = 0 + self._min_shanten = 8 + + def calculate(self) -> int: + count_of_tiles = sum(self._tiles) assert count_of_tiles <= 14, f"Too many tiles = {count_of_tiles}" self._remove_character_tiles(count_of_tiles) @@ -218,16 +79,6 @@ def calculate_shanten_for_regular_hand(self, tiles_34: Sequence[int]) -> int: return self._min_shanten - def _init(self, tiles: list[int]) -> None: - self._tiles = tiles - self._number_melds = 0 - self._number_tatsu = 0 - self._number_pairs = 0 - self._number_jidahai = 0 - self._flag_four_copies = 0 - self._flag_isolated_tiles = 0 - self._min_shanten = 8 - def _scan(self, init_mentsu: int) -> None: for i in range(0, 27): self._flag_four_copies |= (self._tiles[i] == 4) << i diff --git a/tests/hand_calculating/tests_fu_calculation.py b/tests/hand_calculating/tests_fu_calculation.py index 2cac408..2b3091c 100644 --- a/tests/hand_calculating/tests_fu_calculation.py +++ b/tests/hand_calculating/tests_fu_calculation.py @@ -421,3 +421,16 @@ def test_incorrect_fu_calculation_test_case_2() -> None: result = calculator.estimate_hand_value(tiles, win_tile, melds=melds, config=HandConfig(is_houtei=True)) assert result.fu == 30 + + +def test_calculate_fu_can_call_as_static_method() -> None: + config = HandConfig() + + tiles = TilesConverter.string_to_136_array(sou="112244", man="115599", pin="6") + win_tile = _string_to_136_tile(pin="6") + hand = _hand(TilesConverter.to_34_array(tiles + [win_tile])) + + fu_details, fu = FuCalculator.calculate_fu(hand, win_tile, _get_win_group(hand, win_tile), config) + assert 1 == len(fu_details) + assert {"fu": 25, "reason": FuCalculator.BASE} in fu_details + assert fu == 25 diff --git a/tests/hand_calculating/tests_hand_dividing.py b/tests/hand_calculating/tests_hand_dividing.py index e3c2b82..eb543d3 100644 --- a/tests/hand_calculating/tests_hand_dividing.py +++ b/tests/hand_calculating/tests_hand_dividing.py @@ -95,3 +95,10 @@ def test_fix_not_correct_kan_handling() -> None: ] hand.estimate_hand_value(tiles, win_tile, melds=melds) + + +def test_divide_hand_can_call_as_static_method() -> None: + tiles_34 = TilesConverter.string_to_34_array(man="234567", sou="23455", honors="777") + result = HandDivider.divide_hand(tiles_34) + assert len(result) == 1 + assert _string(result[0]) == ["234m", "567m", "234s", "55s", "777z"] diff --git a/tests/hand_calculating/tests_scores_calculation.py b/tests/hand_calculating/tests_scores_calculation.py index 3d50c61..bd993cc 100644 --- a/tests/hand_calculating/tests_scores_calculation.py +++ b/tests/hand_calculating/tests_scores_calculation.py @@ -279,3 +279,10 @@ def test_kiriage_mangan() -> None: result = hand.calculate_scores(han=3, fu=60, config=config) assert result["main"] == 12000 + + +def test_calculate_scores_can_call_as_static_method() -> None: + config = HandConfig(options=OptionalRules(kazoe_limit=HandConfig.KAZOE_NO_LIMIT)) + + result = ScoresCalculator.calculate_scores(han=1, fu=30, config=config) + assert result["main"] == 1000 diff --git a/tests/tests_agari.py b/tests/tests_agari.py index ba6a44f..23a6ce4 100644 --- a/tests/tests_agari.py +++ b/tests/tests_agari.py @@ -79,3 +79,8 @@ def test_is_agari_and_open_hand() -> None: _string_to_open_34_set(sou="555"), ] assert not agari.is_agari(tiles, melds) + + +def test_is_agari_can_call_as_static_method() -> None: + tiles = TilesConverter.string_to_34_array(sou="123456789", pin="123", man="33") + assert Agari.is_agari(tiles) diff --git a/tests/tests_shanten.py b/tests/tests_shanten.py index 36a3bf8..5636c3e 100644 --- a/tests/tests_shanten.py +++ b/tests/tests_shanten.py @@ -173,3 +173,23 @@ def test_shanten_number_and_open_sets() -> None: tiles = TilesConverter.string_to_34_array(sou="88") assert shanten.calculate_shanten(tiles) == Shanten.AGARI_STATE + + +def test_calculate_shanten_can_call_as_static_method() -> None: + tiles = TilesConverter.string_to_34_array(sou="4566677", pin="1367", man="8", honors="12") + assert Shanten.calculate_shanten(tiles) == 2 + + +def test_calculate_shanten_for_regular_hand_can_call_as_static_method() -> None: + tiles = TilesConverter.string_to_34_array(sou="111234567", pin="11", man="567") + assert Shanten.calculate_shanten_for_regular_hand(tiles) == Shanten.AGARI_STATE + + +def test_calculate_shanten_for_chiitoitsu_hand_can_call_as_static_method() -> None: + tiles = TilesConverter.string_to_34_array(sou="114477", pin="114477", man="77") + assert Shanten.calculate_shanten_for_chiitoitsu_hand(tiles) == Shanten.AGARI_STATE + + +def test_calculate_shanten_for_kokushi_hand_can_call_as_static_method() -> None: + tiles = TilesConverter.string_to_34_array(sou="19", pin="19", man="19", honors="12345677") + assert Shanten.calculate_shanten_for_kokushi_hand(tiles) == Shanten.AGARI_STATE