Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion mahjong/hand_calculating/divider.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,18 @@ def divide_hand(
:param melds: list of Meld objects
:return:
"""
if not HandDivider._validate_tiles(tiles_34):
return []

meld_blocks = HandDivider._melds_to_blocks(melds)
pure_hand = HandDivider._get_pure_hand(tiles_34, meld_blocks)
combinations = HandDivider._divide_hand_impl(pure_hand, meld_blocks)
return [[b.tiles_34 for b in blocks] for blocks in combinations]

@staticmethod
def _validate_tiles(tiles_34: Sequence[int]) -> bool:
return all(0 <= count <= 4 for count in tiles_34)

@staticmethod
def _melds_to_blocks(melds: Collection[Meld] | None = None) -> tuple[_Block, ...]:
if not melds:
Expand Down Expand Up @@ -98,6 +105,7 @@ def _divide_hand_impl(pure_hand: tuple[int, ...], melds: tuple[_Block, ...]) ->
for man in man_combinations:
for pin in pin_combinations:
for sou in sou_combinations:
# For invalid suits, *_combinations will be [] and the loop will not be executed.
all_blocks = [*man, *pin, *sou, *honors]

num_pair = sum(block.ty == _BlockType.PAIR for block in all_blocks)
Expand All @@ -116,6 +124,9 @@ def _divide_hand_impl(pure_hand: tuple[int, ...], melds: tuple[_Block, ...]) ->

@staticmethod
def _decompose_chiitoitsu(pure_hand: list[int]) -> list[_Block]:
if any(count not in {0, 2} for count in pure_hand):
return []

blocks = [_Block(i, _BlockType.PAIR) for i, count in enumerate(pure_hand) if count == 2]
return blocks if len(blocks) == 7 else []

Expand Down Expand Up @@ -156,6 +167,8 @@ def _decompose_single_color_hand_without_pair(
remaining: int,
) -> list[list[_Block]]:
if i == 9:
# If there is no tile of the target suits, returns [[]].
# If there are any tiles remaining, it returns [].
return [blocks] if remaining == 0 else []

if single_color_hand[i] == 0:
Expand Down Expand Up @@ -192,13 +205,15 @@ def _decompose_single_color_hand_without_pair(
new_combination = HandDivider._decompose_single_color_hand_without_pair(
single_color_hand,
new_blocks,
i + 1,
i + 1, # The triplet is extracted only once.
suit,
remaining - 3,
)
combinations.extend(new_combination)
single_color_hand[i] += 3

# If single_color_hand[i] is an invalid number (not empty, not a triplet, not a sequence),
# [] is returned immediately.
return combinations

@staticmethod
Expand Down
49 changes: 49 additions & 0 deletions tests/hand_calculating/tests_hand_dividing.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,43 @@ def test_block_from_meld_with_invalid_meld_type_raises_runtime_error() -> None:
_Block.from_meld(meld)


def test_divide_hand_skips_hand_with_negative_tiles() -> None:
tiles_34 = TilesConverter.string_to_34_array(man="123789", pin="123", sou="12", honors="11222")
tiles_34[_string_to_34_tile(sou="9")] = -2
result = HandDivider.divide_hand(tiles_34)
assert result == []


def test_divide_hand_skips_hand_with_5_identical_tiles() -> None:
tiles_34 = TilesConverter.string_to_34_array(pin="123", sou="123", honors="111")
tiles_34[_string_to_34_tile(man="1")] = 5
result = HandDivider.divide_hand(tiles_34)
assert result == []


def test_divide_hand_skips_hand_with_5_identical_tiles_with_meld() -> None:
tiles_34 = TilesConverter.string_to_34_array(pin="123", sou="123", honors="111")
tiles_34[_string_to_34_tile(man="1")] = 5
melds = [_make_meld(Meld.PON, man="111")]
result = HandDivider.divide_hand(tiles_34, melds)
assert result == []


def test_divide_hand_skips_hand_with_6_identical_tiles() -> None:
tiles_34 = TilesConverter.string_to_34_array(pin="123", sou="123", honors="11")
tiles_34[_string_to_34_tile(man="1")] = 6
result = HandDivider.divide_hand(tiles_34)
assert result == []


def test_divide_hand_skips_hand_with_6_identical_tiles_with_meld() -> None:
tiles_34 = TilesConverter.string_to_34_array(pin="123", sou="123", honors="11")
tiles_34[_string_to_34_tile(man="1")] = 6
melds = [_make_meld(Meld.PON, man="111")]
result = HandDivider.divide_hand(tiles_34, melds)
assert result == []


def test_divide_hand_skips_combinations_with_wrong_block_count() -> None:
# 5 melds + 1 pair = 6 blocks, which != 5
tiles_34 = TilesConverter.string_to_34_array(man="123789", pin="123789", sou="111", honors="11")
Expand All @@ -146,3 +183,15 @@ def test_decompose_chiitoitsu_rejects_hand_with_melds() -> None:
melds = [Meld(meld_type=Meld.PON, tiles=TilesConverter.string_to_136_array(man="111"))]
result = HandDivider.divide_hand(tiles_34, melds)
assert result == []


def test_decompose_chiitoitsu_rejects_hand_with_too_many_tiles() -> None:
tiles_34 = TilesConverter.string_to_34_array(man="2288", pin="2288", sou="228", honors="2244")
result = HandDivider.divide_hand(tiles_34)
assert result == []


def test_decompose_chiitoitsu_rejects_hand_with_too_many_pairs() -> None:
tiles_34 = TilesConverter.string_to_34_array(man="2288", pin="2288", sou="2288", honors="2244")
result = HandDivider.divide_hand(tiles_34)
assert result == []
Loading