MahjongRepository / mahjong

Implementation of riichi mahjong related stuff (hand cost, shanten, agari end, etc.)
MIT License
377 stars 38 forks source link

IndexError with certain invalid hands (five of the same tile) #36

Closed jsettlem closed 2 years ago

jsettlem commented 3 years ago

Background

While playing around with this library, I was randomly generating and evaluating hands and accidentally made some hands with five of the same tile. In some cases, this leads to an IndexError

Code

from mahjong.hand_calculating.hand import *
from mahjong.hand_calculating.hand_config import *
from mahjong.meld import *
import traceback

calculator = HandCalculator()

config = HandConfig(is_tsumo=True, is_riichi=False, player_wind=SOUTH, round_wind=EAST,
                    options=OptionalRules(has_open_tanyao=True, has_aka_dora=True))

draws = [
    [36, 96, 12, 12, 44, 16, 16, 100, 16, 16, 16, 92, 28, 40, 0],
    [40, 40, 32, 0, 24, 28, 64, 40, 40, 0, 12, 8, 16, 40, 80],
    [88, 28, 76, 32, 40, 44, 76, 24, 76, 76, 100, 48, 88, 76, 20],
    [16, 80, 16, 16, 8, 28, 80, 16, 80, 16, 88, 12, 88, 4, 60],
    [84, 84, 76, 76, 4, 4, 28, 4, 4, 20, 4, 16, 76, 24, 128],
    [52, 124, 52, 52, 56, 80, 124, 76, 52, 84, 52, 56, 64, 124, 80],
    [4, 80, 16, 16, 4, 28, 100, 84, 96, 4, 104, 4, 88, 4, 100],
    [12, 20, 116, 120, 4, 16, 120, 4, 28, 120, 4, 116, 4, 4, 12],
    [76, 48, 76, 88, 76, 48, 12, 100, 16, 76, 8, 48, 88, 76, 4]

]
for draw in draws:
    print("hand", TilesConverter.to_one_line_string(draw[:14], print_aka_dora=True))
    print("winning tile", TilesConverter.to_one_line_string([draw[0]], print_aka_dora=True))
    print("dora", TilesConverter.to_one_line_string(draw[-1:], print_aka_dora=True))

    try:
        result = calculator.estimate_hand_value(draw[:14], win_tile=draw[0], dora_indicators=draw[-1:], config=config)
        print(result)
    except:
        print(traceback.format_exc())

Result

A whole bunch of stack traces ``` "C:\Program Files\Python39\python.exe" Z:/Dropbox/PyCharm/mahjong-master/mahjong-master/doc/reproduce_the_mahjong.py hand 44000008m123p678s winning tile 1p dora 1m Traceback (most recent call last): File "Z:\Dropbox\PyCharm\mahjong-master\mahjong-master\doc\reproduce_the_mahjong.py", line 29, in result = calculator.estimate_hand_value(draw[:14], win_tile=draw[0], dora_indicators=draw[-1:], config=config) File "Z:\Dropbox\PyCharm\mahjong-master\mahjong-master\mahjong\hand_calculating\hand.py", line 533, in estimate_hand_value calculated_hand = calculated_hands[0] IndexError: list index out of range hand 11340789m222228p winning tile 2p dora 3s Traceback (most recent call last): File "Z:\Dropbox\PyCharm\mahjong-master\mahjong-master\doc\reproduce_the_mahjong.py", line 29, in result = calculator.estimate_hand_value(draw[:14], win_tile=draw[0], dora_indicators=draw[-1:], config=config) File "Z:\Dropbox\PyCharm\mahjong-master\mahjong-master\mahjong\hand_calculating\hand.py", line 533, in estimate_hand_value calculated_hand = calculated_hands[0] IndexError: list index out of range hand 789m234p22222008s winning tile 0s dora 6m Traceback (most recent call last): File "Z:\Dropbox\PyCharm\mahjong-master\mahjong-master\doc\reproduce_the_mahjong.py", line 29, in result = calculator.estimate_hand_value(draw[:14], win_tile=draw[0], dora_indicators=draw[-1:], config=config) File "Z:\Dropbox\PyCharm\mahjong-master\mahjong-master\mahjong\hand_calculating\hand.py", line 533, in estimate_hand_value calculated_hand = calculated_hands[0] IndexError: list index out of range hand 234000008m33300s winning tile 0m dora 7p Traceback (most recent call last): File "Z:\Dropbox\PyCharm\mahjong-master\mahjong-master\doc\reproduce_the_mahjong.py", line 29, in result = calculator.estimate_hand_value(draw[:14], win_tile=draw[0], dora_indicators=draw[-1:], config=config) File "Z:\Dropbox\PyCharm\mahjong-master\mahjong-master\mahjong\hand_calculating\hand.py", line 533, in estimate_hand_value calculated_hand = calculated_hands[0] IndexError: list index out of range hand 222220678m22244s winning tile 4s dora 6z Traceback (most recent call last): File "Z:\Dropbox\PyCharm\mahjong-master\mahjong-master\doc\reproduce_the_mahjong.py", line 29, in result = calculator.estimate_hand_value(draw[:14], win_tile=draw[0], dora_indicators=draw[-1:], config=config) File "Z:\Dropbox\PyCharm\mahjong-master\mahjong-master\mahjong\hand_calculating\hand.py", line 533, in estimate_hand_value calculated_hand = calculated_hands[0] IndexError: list index out of range hand 00000668p234s555z winning tile 0p dora 3s Traceback (most recent call last): File "Z:\Dropbox\PyCharm\mahjong-master\mahjong-master\doc\reproduce_the_mahjong.py", line 29, in result = calculator.estimate_hand_value(draw[:14], win_tile=draw[0], dora_indicators=draw[-1:], config=config) File "Z:\Dropbox\PyCharm\mahjong-master\mahjong-master\mahjong\hand_calculating\hand.py", line 533, in estimate_hand_value calculated_hand = calculated_hands[0] IndexError: list index out of range hand 22222008m340789s winning tile 2m dora 8s Traceback (most recent call last): File "Z:\Dropbox\PyCharm\mahjong-master\mahjong-master\doc\reproduce_the_mahjong.py", line 29, in result = calculator.estimate_hand_value(draw[:14], win_tile=draw[0], dora_indicators=draw[-1:], config=config) File "Z:\Dropbox\PyCharm\mahjong-master\mahjong-master\mahjong\hand_calculating\hand.py", line 533, in estimate_hand_value calculated_hand = calculated_hands[0] IndexError: list index out of range hand 222224068m33444z winning tile 4m dora 4m Traceback (most recent call last): File "Z:\Dropbox\PyCharm\mahjong-master\mahjong-master\doc\reproduce_the_mahjong.py", line 29, in result = calculator.estimate_hand_value(draw[:14], win_tile=draw[0], dora_indicators=draw[-1:], config=config) File "Z:\Dropbox\PyCharm\mahjong-master\mahjong-master\mahjong\hand_calculating\hand.py", line 533, in estimate_hand_value calculated_hand = calculated_hands[0] IndexError: list index out of range hand 340m444p22222008s winning tile 2s dora 2m Traceback (most recent call last): File "Z:\Dropbox\PyCharm\mahjong-master\mahjong-master\doc\reproduce_the_mahjong.py", line 29, in result = calculator.estimate_hand_value(draw[:14], win_tile=draw[0], dora_indicators=draw[-1:], config=config) File "Z:\Dropbox\PyCharm\mahjong-master\mahjong-master\mahjong\hand_calculating\hand.py", line 533, in estimate_hand_value calculated_hand = calculated_hands[0] IndexError: list index out of range Process finished with exit code 0 ```

Expected

I'm not sure? Obviously this is an exceptional case, but the library gracefully handles many other exceptional cases so I would expect it to handle this one as well.

Environment

Python 3.9.0 (tags/v3.9.0:9cf6752, Oct 5 2020, 15:34:40) [MSC v.1927 64 bit (AMD64)] on win32

Reproduced with both 1.1.11 from PyPI and the latest master commit. The stack traces are based off c3e64e5f04a49153564be58a2dd30eb495553a84

Nihisil commented 3 years ago

Thanks for the report. Please check if it still reproducible on latest master version

jsettlem commented 3 years ago

I can confirm that all the test cases I provided now result in "hand_not_correct." I generated ~9 billion random hands and encountered no further exceptions.

However, this does lead to a slight inconsistency. Some invalid hands result in "hand_not_correct" and some result in "hand_not_winning." Invalid winning hands do seem to score correctly (at least the couple I tested):

[[12, 13, 4, 5, 6, 7, 7, 28, 36, 40, 44, 92, 96, 100, 0],
 [12, 13, 16, 17, 18, 19, 19, 28, 36, 40, 44, 92, 96, 100, 0],
 [12, 13, 20, 21, 22, 23, 23, 28, 36, 40, 44, 92, 96, 100, 0],
 [12, 13, 17, 17, 17, 17, 17, 17, 36, 40, 44, 92, 96, 100, 0],
 [12, 13, 14, 16, 17, 18, 19, 19, 36, 40, 44, 92, 96, 100, 0]]

hand: 22222448m123p678s
winning tile: 4m
dora: 1m
Result: hand_not_correct

hand: 44055558m123p678s
winning tile: 4m
dora: 1m
Result: hand_not_correct

hand: 44666668m123p678s
winning tile: 4m
dora: 1m
Result: hand_not_winning

hand: 44555555m123p678s
winning tile: 4m
dora: 1m
Result: 1 han, 40 fu

hand: 44405555m123p678s
winning tile: 4m
dora: 1m
Result: 2 han, 30 fu

While testing this, I also realized one_line_string_to_136_array() (understandably) breaks these invalid hands:

hand='22222448m123p678s'
TilesConverter.one_line_string_to_136_array(hand, has_aka_dora=True)=[4, 5, 6, 7, 8, 12, 13, 28, 36, 40, 44, 92, 96, 100]
TilesConverter.to_one_line_string(TilesConverter.one_line_string_to_136_array(hand, has_aka_dora=True), print_aka_dora=True)='22223448m123p678s'

hand='44055558m123p678s'
TilesConverter.one_line_string_to_136_array(hand, has_aka_dora=True)=[12, 13, 16, 17, 18, 19, 20, 28, 36, 40, 44, 92, 96, 100]
TilesConverter.to_one_line_string(TilesConverter.one_line_string_to_136_array(hand, has_aka_dora=True), print_aka_dora=True)='44055568m123p678s'

hand='44666668m123p678s'
TilesConverter.one_line_string_to_136_array(hand, has_aka_dora=True)=[12, 13, 20, 21, 22, 23, 24, 28, 36, 40, 44, 92, 96, 100]
TilesConverter.to_one_line_string(TilesConverter.one_line_string_to_136_array(hand, has_aka_dora=True), print_aka_dora=True)='44666678m123p678s'

hand='44555555m123p678s'
TilesConverter.one_line_string_to_136_array(hand, has_aka_dora=True)=[12, 13, 17, 18, 19, 20, 21, 22, 36, 40, 44, 92, 96, 100]
TilesConverter.to_one_line_string(TilesConverter.one_line_string_to_136_array(hand, has_aka_dora=True), print_aka_dora=True)='44555666m123p678s'

hand='22222244m123p678s'
TilesConverter.one_line_string_to_136_array(hand, has_aka_dora=True)=[4, 5, 6, 7, 8, 9, 12, 13, 36, 40, 44, 92, 96, 100]
TilesConverter.to_one_line_string(TilesConverter.one_line_string_to_136_array(hand, has_aka_dora=True), print_aka_dora=True)='22223344m123p678s'

In each case a five of a kind becomes four of a kind plus the next tile up when passed through one_line_string_to_136_array (so 22222m becomes 22223m).

Obviously these are very exceptional cases and I don't actually need support for them for my use case, so I understand if you want to consider them "out of scope," but I did want to at least mention them.