diff --git a/CHANGELOG.md b/CHANGELOG.md index 1dbafbf..8f9b958 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -94,6 +94,7 @@ The following methods are now available as static methods: - Fixed an issue where `KokushiMusou.is_condition_met()` would return `None` if the condition was not met. It now consistently returns a `bool` value. Remove any `None` checks in the code that relied on the previous behavior. - `Shanten.calculate_shanten()` and `Shanten.calculate_shanten_for_regular_hand()` now raises `ValueError` instead of `assert` when the number of tiles is 15 or more. - `HandDivider.divide_hand()` now determines block type from `Meld.type` instead of inferring it from `Meld.tiles`. Behavior may differ for invalid `Meld.tiles` or inconsistent `Meld.type` and `Meld.tiles` combinations. +- Removed `HandCalculator.ERR_HAND_NOT_CORRECT`. Hands that previously returned `ERR_HAND_NOT_CORRECT` now return `ERR_HAND_NOT_WINNING` instead. ## What's Changed - Placeholder. It would be filled on release automatically diff --git a/mahjong/hand_calculating/hand.py b/mahjong/hand_calculating/hand.py index a3f310f..0eb90d0 100644 --- a/mahjong/hand_calculating/hand.py +++ b/mahjong/hand_calculating/hand.py @@ -1,6 +1,5 @@ from collections.abc import Collection -from mahjong.agari import Agari from mahjong.constants import AKA_DORA_LIST, CHUN, HAKU, HATSU from mahjong.hand_calculating.divider import HandDivider from mahjong.hand_calculating.fu import FuCalculator @@ -23,7 +22,6 @@ class HandCalculator: ERR_OPEN_HAND_DABURI = "open_hand_daburi_not_allowed" ERR_IPPATSU_WITHOUT_RIICHI = "ippatsu_without_riichi_not_allowed" ERR_HAND_NOT_WINNING = "hand_not_winning" - ERR_HAND_NOT_CORRECT = "hand_not_correct" ERR_NO_YAKU = "no_yaku" ERR_CHANKAN_WITH_TSUMO = "chankan_with_tsumo_not_allowed" ERR_RINSHAN_WITHOUT_TSUMO = "rinshan_without_tsumo_not_allowed" @@ -82,7 +80,6 @@ def estimate_hand_value( is_aotenjou = isinstance(scores_calculator, Aotenjou) opened_melds = [x.tiles_34 for x in melds if x.opened] - all_melds = [x.tiles_34 for x in melds] is_open_hand = len(opened_melds) > 0 # special situation @@ -153,9 +150,6 @@ def estimate_hand_value( if config.is_renhou and melds: return HandResponse(error=HandCalculator.ERR_RENHOU_WITH_MELD) - if not Agari.is_agari(tiles_34, all_melds): - return HandResponse(error=HandCalculator.ERR_HAND_NOT_WINNING) - if not config.options.has_double_yakuman: config.yaku.daburu_kokushi.han_closed = 13 config.yaku.suuankou_tanki.han_closed = 13 @@ -533,7 +527,7 @@ def estimate_hand_value( ) if not calculated_hands: - return HandResponse(error=HandCalculator.ERR_HAND_NOT_CORRECT) + return HandResponse(error=HandCalculator.ERR_HAND_NOT_WINNING) # find most expensive hand calculated_hands = sorted(calculated_hands, key=lambda x: (x["han"], x["fu"]), reverse=True) diff --git a/tests/hand_calculating/tests_hand_response_error.py b/tests/hand_calculating/tests_hand_response_error.py index ef68733..7383765 100644 --- a/tests/hand_calculating/tests_hand_response_error.py +++ b/tests/hand_calculating/tests_hand_response_error.py @@ -15,7 +15,7 @@ def test_no_winning_tile() -> None: win_tile = _string_to_136_tile(sou="9") result = hand.estimate_hand_value(tiles, win_tile, config=_make_hand_config(is_riichi=True)) - assert result.error == "winning_tile_not_in_hand" + assert result.error == HandCalculator.ERR_NO_WINNING_TILE def test_open_hand_riichi() -> None: @@ -26,7 +26,7 @@ def test_open_hand_riichi() -> None: melds = [_make_meld(Meld.CHI, sou="123")] result = hand.estimate_hand_value(tiles, win_tile, melds=melds, config=_make_hand_config(is_riichi=True)) - assert result.error == "open_hand_riichi_not_allowed" + assert result.error == HandCalculator.ERR_OPEN_HAND_RIICHI def test_open_hand_daburi() -> None: @@ -42,7 +42,7 @@ def test_open_hand_daburi() -> None: melds=melds, config=_make_hand_config(is_riichi=True, is_daburu_riichi=True), ) - assert result.error == "open_hand_daburi_not_allowed" + assert result.error == HandCalculator.ERR_OPEN_HAND_DABURI def test_ippatsu_without_riichi() -> None: @@ -52,7 +52,7 @@ def test_ippatsu_without_riichi() -> None: win_tile = _string_to_136_tile(sou="4") result = hand.estimate_hand_value(tiles, win_tile, config=_make_hand_config(is_ippatsu=True)) - assert result.error == "ippatsu_without_riichi_not_allowed" + assert result.error == HandCalculator.ERR_IPPATSU_WITHOUT_RIICHI def test_hand_not_winning() -> None: @@ -62,7 +62,7 @@ def test_hand_not_winning() -> None: win_tile = _string_to_136_tile(sou="4") result = hand.estimate_hand_value(tiles, win_tile) - assert result.error == "hand_not_winning" + assert result.error == HandCalculator.ERR_HAND_NOT_WINNING def test_no_yaku() -> None: @@ -73,7 +73,7 @@ def test_no_yaku() -> None: melds = [_make_meld(Meld.CHI, sou="123")] result = hand.estimate_hand_value(tiles, win_tile, melds=melds) - assert result.error == "no_yaku" + assert result.error == HandCalculator.ERR_NO_YAKU def test_chankan_with_tsumo() -> None: @@ -83,7 +83,7 @@ def test_chankan_with_tsumo() -> None: win_tile = _string_to_136_tile(sou="1") result = hand.estimate_hand_value(tiles, win_tile, config=_make_hand_config(is_tsumo=True, is_chankan=True)) - assert result.error == "chankan_with_tsumo_not_allowed" + assert result.error == HandCalculator.ERR_CHANKAN_WITH_TSUMO def test_rinshan_without_tsumo() -> None: @@ -93,7 +93,7 @@ def test_rinshan_without_tsumo() -> None: win_tile = _string_to_136_tile(sou="4") result = hand.estimate_hand_value(tiles, win_tile, config=_make_hand_config(is_tsumo=False, is_rinshan=True)) - assert result.error == "rinshan_without_tsumo_not_allowed" + assert result.error == HandCalculator.ERR_RINSHAN_WITHOUT_TSUMO def test_haitei_without_tsumo() -> None: @@ -103,7 +103,7 @@ def test_haitei_without_tsumo() -> None: win_tile = _string_to_136_tile(sou="4") result = hand.estimate_hand_value(tiles, win_tile, config=_make_hand_config(is_tsumo=False, is_haitei=True)) - assert result.error == "haitei_without_tsumo_not_allowed" + assert result.error == HandCalculator.ERR_HAITEI_WITHOUT_TSUMO def test_houtei_with_tsumo() -> None: @@ -113,7 +113,7 @@ def test_houtei_with_tsumo() -> None: win_tile = _string_to_136_tile(sou="4") result = hand.estimate_hand_value(tiles, win_tile, config=_make_hand_config(is_tsumo=True, is_houtei=True)) - assert result.error == "houtei_with_tsumo_not_allowed" + assert result.error == HandCalculator.ERR_HOUTEI_WITH_TSUMO def test_haitei_with_rinshan() -> None: @@ -127,7 +127,7 @@ def test_haitei_with_rinshan() -> None: win_tile, config=_make_hand_config(is_tsumo=True, is_rinshan=True, is_haitei=True), ) - assert result.error == "haitei_with_rinshan_not_allowed" + assert result.error == HandCalculator.ERR_HAITEI_WITH_RINSHAN def test_houtei_with_chankan() -> None: @@ -141,7 +141,7 @@ def test_houtei_with_chankan() -> None: win_tile, config=_make_hand_config(is_tsumo=False, is_chankan=True, is_houtei=True), ) - assert result.error == "houtei_with_chankan_not_allowed" + assert result.error == HandCalculator.ERR_HOUTEI_WITH_CHANKAN def test_tenhou_not_as_dealer() -> None: @@ -160,7 +160,7 @@ def test_tenhou_not_as_dealer() -> None: win_tile, config=_make_hand_config(is_tsumo=True, is_tenhou=True, player_wind=SOUTH), ) - assert result.error == "tenhou_not_as_dealer_not_allowed" + assert result.error == HandCalculator.ERR_TENHOU_NOT_AS_DEALER def test_tenhou_without_tsumo() -> None: @@ -170,7 +170,7 @@ def test_tenhou_without_tsumo() -> None: win_tile = _string_to_136_tile(sou="4") result = hand.estimate_hand_value(tiles, win_tile, config=_make_hand_config(is_tsumo=False, is_tenhou=True)) - assert result.error == "tenhou_without_tsumo_not_allowed" + assert result.error == HandCalculator.ERR_TENHOU_WITHOUT_TSUMO def test_tenhou_with_meld() -> None: @@ -186,7 +186,7 @@ def test_tenhou_with_meld() -> None: melds=melds, config=_make_hand_config(is_tsumo=True, is_rinshan=True, is_tenhou=True), ) - assert result.error == "tenhou_with_meld_not_allowed" + assert result.error == HandCalculator.ERR_TENHOU_WITH_MELD def test_chiihou_as_dealer() -> None: @@ -205,7 +205,7 @@ def test_chiihou_as_dealer() -> None: win_tile, config=_make_hand_config(is_tsumo=True, is_chiihou=True, player_wind=EAST), ) - assert result.error == "chiihou_as_dealer_not_allowed" + assert result.error == HandCalculator.ERR_CHIIHOU_AS_DEALER def test_chiihou_without_tsumo() -> None: @@ -215,7 +215,7 @@ def test_chiihou_without_tsumo() -> None: win_tile = _string_to_136_tile(sou="4") result = hand.estimate_hand_value(tiles, win_tile, config=_make_hand_config(is_tsumo=False, is_chiihou=True)) - assert result.error == "chiihou_without_tsumo_not_allowed" + assert result.error == HandCalculator.ERR_CHIIHOU_WITHOUT_TSUMO def test_chiihou_with_meld() -> None: @@ -231,7 +231,7 @@ def test_chiihou_with_meld() -> None: melds=melds, config=_make_hand_config(is_tsumo=True, is_rinshan=True, is_chiihou=True), ) - assert result.error == "chiihou_with_meld_not_allowed" + assert result.error == HandCalculator.ERR_CHIIHOU_WITH_MELD def test_renhou_as_dealer() -> None: @@ -250,7 +250,7 @@ def test_renhou_as_dealer() -> None: win_tile, config=_make_hand_config(is_tsumo=False, is_renhou=True, player_wind=EAST), ) - assert result.error == "renhou_as_dealer_not_allowed" + assert result.error == HandCalculator.ERR_RENHOU_AS_DEALER def test_renhou_with_tsumo() -> None: @@ -260,7 +260,7 @@ def test_renhou_with_tsumo() -> None: win_tile = _string_to_136_tile(sou="4") result = hand.estimate_hand_value(tiles, win_tile, config=_make_hand_config(is_tsumo=True, is_renhou=True)) - assert result.error == "renhou_with_tsumo_not_allowed" + assert result.error == HandCalculator.ERR_RENHOU_WITH_TSUMO def test_renhou_with_meld() -> None: @@ -276,7 +276,7 @@ def test_renhou_with_meld() -> None: melds=melds, config=_make_hand_config(is_tsumo=False, is_renhou=True), ) - assert result.error == "renhou_with_meld_not_allowed" + assert result.error == HandCalculator.ERR_RENHOU_WITH_MELD def test_win_tile_only_in_opened_meld() -> None: @@ -291,15 +291,15 @@ def test_win_tile_only_in_opened_meld() -> None: meld = Meld(meld_type=Meld.PON, tiles=tiles[0:3], opened=True, called_tile=tiles[0], who=0) result = hand.estimate_hand_value(tiles, win_tile, melds=[meld]) - assert result.error == "hand_not_correct" + assert result.error == HandCalculator.ERR_HAND_NOT_WINNING @pytest.mark.parametrize( "error_string", [ - "hand_not_winning", - "no_yaku", - "winning_tile_not_in_hand", + HandCalculator.ERR_HAND_NOT_WINNING, + HandCalculator.ERR_NO_YAKU, + HandCalculator.ERR_NO_WINNING_TILE, ], ) def test_str_returns_error_when_error_is_set(error_string: str) -> None: diff --git a/tests/tests_agari.py b/tests/tests_agari.py index d6b50ca..e91b6bf 100644 --- a/tests/tests_agari.py +++ b/tests/tests_agari.py @@ -107,15 +107,17 @@ def test_honor_tile_overflow_returns_false(tiles_34: list[int]) -> None: assert Agari.is_agari(tiles_34) is False -def test_pin_suit_mod3_equals_1_returns_false() -> None: - # 1 tile in pin suit: sum = 1, 1 % 3 = 1 - tiles = TilesConverter.string_to_34_array(pin="1") - assert Agari.is_agari(tiles) is False - - -def test_sou_suit_mod3_equals_1_returns_false() -> None: - # 1 tile in sou suit: sum = 1, 1 % 3 = 1 - tiles = TilesConverter.string_to_34_array(sou="1") +@pytest.mark.parametrize( + ("man", "pin", "sou"), + [ + pytest.param("1", "", "", id="man"), + pytest.param("", "1", "", id="pin"), + pytest.param("", "", "1", id="sou"), + ], +) +def test_single_tile_in_suit_returns_false(man: str, pin: str, sou: str) -> None: + # a single tile in any suit can't form a valid group (tile count % 3 == 1) + tiles = TilesConverter.string_to_34_array(man=man, pin=pin, sou=sou) assert Agari.is_agari(tiles) is False @@ -148,3 +150,27 @@ def test_is_mentsu_negative_a_returns_false() -> None: # 1m count=2 implies 2 sequences, but 2m count=0 gives a = 0 - 2 = -2 < 0 tiles = TilesConverter.string_to_34_array(man="114", honors="11") assert Agari.is_agari(tiles) is False + + +def test_open_hand_with_kan_meld() -> None: + # 4-tile kan meld exercises the len(meld) > 3 branch + tiles = TilesConverter.string_to_34_array(man="1111", pin="123456789", sou="22") + kan_of_1m = [0, 0, 0, 0] + assert Agari.is_agari(tiles, [kan_of_1m]) is True + + +@pytest.mark.parametrize( + ("man", "pin"), + [ + pytest.param("66", "111222333444"), + pytest.param("99", "111222333444"), + pytest.param("22", "111222333444"), + pytest.param("44", "111222333444"), + pytest.param("77", "111222333444"), + ], +) +def test_atama_mentsu_pair_positions(man: str, pin: str) -> None: + # each case places the pair at a different position within the man suit, + # exercising distinct branches in _is_atama_mentsu + tiles = TilesConverter.string_to_34_array(man=man, pin=pin) + assert Agari.is_agari(tiles) is True