diff --git a/mahjong/hand_calculating/hand.py b/mahjong/hand_calculating/hand.py index e7a9166..23c63cf 100644 --- a/mahjong/hand_calculating/hand.py +++ b/mahjong/hand_calculating/hand.py @@ -1,7 +1,7 @@ from collections.abc import Collection from typing import TypedDict -from mahjong.constants import AKA_DORAS, CHUN, HAKU, HATSU +from mahjong.constants import AKA_DORAS, CHUN, HAKU, HATSU, NORTH from mahjong.hand_calculating.divider import HandDivider from mahjong.hand_calculating.fu import FuCalculator, FuDetail from mahjong.hand_calculating.hand_config import HandConfig @@ -101,6 +101,7 @@ def estimate_hand_value( config: HandConfig | None = None, scores_calculator_factory: type[ScoresCalculator] = ScoresCalculator, ura_dora_indicators: Collection[int] | None = None, + num_nuki_dora: int = 0, ) -> HandResponse: """ Estimate the point value of a winning hand. @@ -177,6 +178,7 @@ def estimate_hand_value( :class:`~mahjong.hand_calculating.scores.Aotenjou` for aotenjou (limitless) scoring :param ura_dora_indicators: ura dora indicator tile indices in 136-format (counted only when riichi or double riichi is declared) + :param num_nuki_dora: the number of nuki dora (north wind extraction) :return: :class:`~mahjong.hand_calculating.hand_response.HandResponse` with scoring details on success, or with :attr:`~mahjong.hand_calculating.hand_response.HandResponse.error` set on failure @@ -281,6 +283,8 @@ def estimate_hand_value( # precompute dora counts, invariant across all hand decompositions dora_count_map = build_dora_count_map(dora_indicators) precomputed_dora = count_dora_for_hand(tiles_34, dora_count_map) + if num_nuki_dora > 0: + precomputed_dora += dora_count_map.get(NORTH, 0) * num_nuki_dora + num_nuki_dora precomputed_aka_dora = 0 if config.options.has_aka_dora: @@ -290,6 +294,8 @@ def estimate_hand_value( if config.is_riichi or config.is_daburu_riichi: ura_count_map = build_dora_count_map(ura_dora_indicators) precomputed_ura_dora = count_dora_for_hand(tiles_34, ura_count_map) + if num_nuki_dora > 0: + precomputed_ura_dora += ura_count_map.get(NORTH, 0) * num_nuki_dora yakuhai_seat_wind_yaku = ( config.yaku.seat_wind_east, diff --git a/tests/hand_calculating/tests_yaku_calculation.py b/tests/hand_calculating/tests_yaku_calculation.py index 4debe52..375a068 100644 --- a/tests/hand_calculating/tests_yaku_calculation.py +++ b/tests/hand_calculating/tests_yaku_calculation.py @@ -1375,6 +1375,80 @@ def test_aka_dora() -> None: assert hand_calculation.han == 6 +@pytest.mark.parametrize( + ("num_nuki_dora", "num_dora_indicators", "han"), + [ + (0, 0, 1), + (1, 0, 2), + (2, 0, 3), + (3, 0, 4), + (4, 0, 5), + (-1, 0, 1), + (0, 1, 1), + (1, 1, 3), + (2, 1, 5), + (4, 1, 9), + (0, 2, 1), + (1, 2, 4), + (4, 4, 21), + (-1, 4, 1), + ], +) +def test_nuki_dora_with_dora_indicators(num_nuki_dora: int, num_dora_indicators: int, han: int) -> None: + tiles = TilesConverter.string_to_136_array(sou="345", pin="456", man="12355599", has_aka_dora=True) + win_tile = _string_to_136_tile(man="9") + + hand_config = HandConfig(is_tsumo=True, options=OptionalRules(has_aka_dora=True)) + dora_indicators = TilesConverter.string_to_136_array(honors="3" * num_dora_indicators) + + result = HandCalculator.estimate_hand_value( + tiles, + win_tile, + config=hand_config, + dora_indicators=dora_indicators, + num_nuki_dora=num_nuki_dora, + ) + assert result.error is None + assert result.han == han + + +@pytest.mark.parametrize( + ("num_nuki_dora", "num_ura_dora_indicators", "han"), + [ + (0, 0, 1), + (1, 0, 2), + (2, 0, 3), + (3, 0, 4), + (4, 0, 5), + (-1, 0, 1), + (0, 1, 1), + (1, 1, 3), + (2, 1, 5), + (4, 1, 9), + (0, 2, 1), + (1, 2, 4), + (4, 4, 21), + (-1, 4, 1), + ], +) +def test_nuki_dora_with_ura_dora_indicators(num_nuki_dora: int, num_ura_dora_indicators: int, han: int) -> None: + tiles = TilesConverter.string_to_136_array(sou="345", pin="456", man="12355599", has_aka_dora=True) + win_tile = _string_to_136_tile(man="9") + + hand_config = HandConfig(is_riichi=True, options=OptionalRules(has_aka_dora=True)) + ura_dora_indicators = TilesConverter.string_to_136_array(honors="3" * num_ura_dora_indicators) + + result = HandCalculator.estimate_hand_value( + tiles, + win_tile, + config=hand_config, + ura_dora_indicators=ura_dora_indicators, + num_nuki_dora=num_nuki_dora, + ) + assert result.error is None + assert result.han == han + + class TestYakuBaseClass: """ Test the abstract Yaku base class methods