def test_valued_pair_fu(self): fu_calculator = FuCalculator() config = HandConfig() tiles = self._string_to_136_array(sou='12378', man='123456', honors='11') win_tile = self._string_to_136_tile(sou='6') hand = self._hand(self._to_34_array(tiles + [win_tile])) valued_tiles = [EAST] fu_details, fu = fu_calculator.calculate_fu(hand, win_tile, self._get_win_group(hand, win_tile), config, valued_tiles=valued_tiles,) self.assertEqual(2, len(fu_details)) self.assertTrue({'fu': 30, 'reason': FuCalculator.BASE} in fu_details) self.assertTrue({'fu': 2, 'reason': FuCalculator.VALUED_PAIR} in fu_details) self.assertEqual(fu, 40) # double valued pair valued_tiles = [EAST, EAST] fu_details, fu = fu_calculator.calculate_fu(hand, win_tile, self._get_win_group(hand, win_tile), config, valued_tiles=valued_tiles) self.assertEqual(3, len(fu_details)) self.assertTrue({'fu': 30, 'reason': FuCalculator.BASE} in fu_details) self.assertTrue({'fu': 2, 'reason': FuCalculator.VALUED_PAIR} in fu_details) self.assertEqual(fu, 40)
def test_closed_terminal_pon_fu(self): fu_calculator = FuCalculator() config = HandConfig() tiles = self._string_to_136_array(sou="11178", man="123456", pin="11") win_tile = self._string_to_136_tile(sou="6") hand = self._hand(self._to_34_array(tiles + [win_tile])) fu_details, fu = fu_calculator.calculate_fu( hand, win_tile, self._get_win_group(hand, win_tile), config) self.assertEqual(2, len(fu_details)) self.assertTrue({"fu": 30, "reason": FuCalculator.BASE} in fu_details) self.assertTrue({ "fu": 8, "reason": FuCalculator.CLOSED_TERMINAL_PON } in fu_details) self.assertEqual(fu, 40) # when we ron on the third pon tile we consider pon as open tiles = self._string_to_136_array(sou="11678", man="123456", pin="11") win_tile = self._string_to_136_tile(sou="1") hand = self._hand(self._to_34_array(tiles + [win_tile])) fu_details, fu = fu_calculator.calculate_fu( hand, win_tile, self._get_win_group(hand, win_tile), config) self.assertEqual(2, len(fu_details)) self.assertTrue({"fu": 30, "reason": FuCalculator.BASE} in fu_details) self.assertTrue({ "fu": 4, "reason": FuCalculator.OPEN_TERMINAL_PON } in fu_details) self.assertEqual(fu, 40)
def test_penchan_fu(self): fu_calculator = FuCalculator() config = HandConfig() # 1-2-... wait tiles = self._string_to_136_array(sou='12456', man='123456', pin='55') win_tile = self._string_to_136_tile(sou='3') hand = self._hand(self._to_34_array(tiles + [win_tile])) fu_details, fu = fu_calculator.calculate_fu(hand, win_tile, self._get_win_group(hand, win_tile), config) self.assertEqual(2, len(fu_details)) self.assertTrue({'fu': 30, 'reason': FuCalculator.BASE} in fu_details) self.assertTrue({'fu': 2, 'reason': FuCalculator.PENCHAN} in fu_details) self.assertEqual(fu, 40) # ...-8-9 wait tiles = self._string_to_136_array(sou='34589', man='123456', pin='55') win_tile = self._string_to_136_tile(sou='7') hand = self._hand(self._to_34_array(tiles + [win_tile])) fu_details, fu = fu_calculator.calculate_fu(hand, win_tile, self._get_win_group(hand, win_tile), config) self.assertEqual(2, len(fu_details)) self.assertTrue({'fu': 30, 'reason': FuCalculator.BASE} in fu_details) self.assertTrue({'fu': 2, 'reason': FuCalculator.PENCHAN} in fu_details) self.assertEqual(fu, 40)
def test_penchan_fu(self): fu_calculator = FuCalculator() config = HandConfig() # 1-2-... wait tiles = self._string_to_136_array(sou="12456", man="123456", pin="55") win_tile = self._string_to_136_tile(sou="3") hand = self._hand(self._to_34_array(tiles + [win_tile])) fu_details, fu = fu_calculator.calculate_fu( hand, win_tile, self._get_win_group(hand, win_tile), config) self.assertEqual(2, len(fu_details)) self.assertTrue({"fu": 30, "reason": FuCalculator.BASE} in fu_details) self.assertTrue({ "fu": 2, "reason": FuCalculator.PENCHAN } in fu_details) self.assertEqual(fu, 40) # ...-8-9 wait tiles = self._string_to_136_array(sou="34589", man="123456", pin="55") win_tile = self._string_to_136_tile(sou="7") hand = self._hand(self._to_34_array(tiles + [win_tile])) fu_details, fu = fu_calculator.calculate_fu( hand, win_tile, self._get_win_group(hand, win_tile), config) self.assertEqual(2, len(fu_details)) self.assertTrue({"fu": 30, "reason": FuCalculator.BASE} in fu_details) self.assertTrue({ "fu": 2, "reason": FuCalculator.PENCHAN } in fu_details) self.assertEqual(fu, 40)
def test_closed_pon_fu(self): fu_calculator = FuCalculator() config = HandConfig() tiles = self._string_to_136_array(sou='22278', man='123456', pin='11') win_tile = self._string_to_136_tile(sou='6') hand = self._hand(self._to_34_array(tiles + [win_tile])) fu_details, fu = fu_calculator.calculate_fu( hand, win_tile, self._get_win_group(hand, win_tile), config) self.assertEqual(2, len(fu_details)) self.assertTrue({'fu': 30, 'reason': FuCalculator.BASE} in fu_details) self.assertTrue({ 'fu': 4, 'reason': FuCalculator.CLOSED_PON } in fu_details) self.assertEqual(fu, 40) # when we ron on the third pon tile we consider pon as open tiles = self._string_to_136_array(sou='22678', man='123456', pin='11') win_tile = self._string_to_136_tile(sou='2') hand = self._hand(self._to_34_array(tiles + [win_tile])) fu_details, fu = fu_calculator.calculate_fu( hand, win_tile, self._get_win_group(hand, win_tile), config) self.assertEqual(2, len(fu_details)) self.assertTrue({'fu': 30, 'reason': FuCalculator.BASE} in fu_details) self.assertTrue({ 'fu': 2, 'reason': FuCalculator.OPEN_PON } in fu_details) self.assertEqual(fu, 40)
def test_fu_based_on_win_group(self): fu_calculator = FuCalculator() config = HandConfig() tiles = self._string_to_136_array(man='234789', pin='1234566') win_tile = self._string_to_136_tile(pin='6') hand = self._hand(self._to_34_array(tiles + [win_tile])) win_groups = [x for x in hand if win_tile // 4 in x] # pinfu wait 4-5-6 fu_details, fu = fu_calculator.calculate_fu(hand, win_tile, win_groups[0], config) self.assertEqual(fu, 30) # pair wait 66 fu_details, fu = fu_calculator.calculate_fu(hand, win_tile, win_groups[1], config) self.assertEqual(fu, 40)
def test_chitoitsu_fu(self): fu_calculator = FuCalculator() config = HandConfig() tiles = self._string_to_136_array(sou='112244', man='115599', pin='6') win_tile = self._string_to_136_tile(pin='6') hand = self._hand(self._to_34_array(tiles + [win_tile])) fu_details, fu = fu_calculator.calculate_fu(hand, win_tile, self._get_win_group(hand, win_tile), config) self.assertEqual(1, len(fu_details)) self.assertTrue({'fu': 25, 'reason': FuCalculator.BASE} in fu_details) self.assertEqual(fu, 25) fu_details, fu = fu_calculator.calculate_fu(hand, win_tile, self._get_win_group(hand, win_tile), config) self.assertEqual(1, len(fu_details)) self.assertTrue({'fu': 25, 'reason': FuCalculator.BASE} in fu_details) self.assertEqual(fu, 25)
def test_tsumo_hand_base(self): fu_calculator = FuCalculator() config = HandConfig(is_tsumo=True) tiles = self._string_to_136_array(sou='22278', man='123456', pin='11') win_tile = self._string_to_136_tile(sou='6') hand = self._hand(self._to_34_array(tiles + [win_tile])) fu_details, _ = fu_calculator.calculate_fu(hand, win_tile, self._get_win_group(hand, win_tile), config) self.assertTrue({'fu': 20, 'reason': FuCalculator.BASE} in fu_details)
def test_tsumo_hand_base(self): fu_calculator = FuCalculator() config = HandConfig(is_tsumo=True) tiles = self._string_to_136_array(sou="22278", man="123456", pin="11") win_tile = self._string_to_136_tile(sou="6") hand = self._hand(self._to_34_array(tiles + [win_tile])) fu_details, _ = fu_calculator.calculate_fu( hand, win_tile, self._get_win_group(hand, win_tile), config) self.assertTrue({"fu": 20, "reason": FuCalculator.BASE} in fu_details)
def test_valued_pair_fu(self): fu_calculator = FuCalculator() config = HandConfig() tiles = self._string_to_136_array(sou="12378", man="123456", honors="11") win_tile = self._string_to_136_tile(sou="6") hand = self._hand(self._to_34_array(tiles + [win_tile])) valued_tiles = [EAST] fu_details, fu = fu_calculator.calculate_fu( hand, win_tile, self._get_win_group(hand, win_tile), config, valued_tiles=valued_tiles, ) self.assertEqual(2, len(fu_details)) self.assertTrue({"fu": 30, "reason": FuCalculator.BASE} in fu_details) self.assertTrue({ "fu": 2, "reason": FuCalculator.VALUED_PAIR } in fu_details) self.assertEqual(fu, 40) # double valued pair valued_tiles = [EAST, EAST] fu_details, fu = fu_calculator.calculate_fu(hand, win_tile, self._get_win_group( hand, win_tile), config, valued_tiles=valued_tiles) self.assertEqual(2, len(fu_details)) self.assertTrue({"fu": 30, "reason": FuCalculator.BASE} in fu_details) self.assertTrue({ "fu": 4, "reason": FuCalculator.DOUBLE_VALUED_PAIR } in fu_details) self.assertEqual(fu, 40)
def test_pair_wait_fu(self): fu_calculator = FuCalculator() config = HandConfig() tiles = self._string_to_136_array(sou='123678', man='123456', pin='1') win_tile = self._string_to_136_tile(pin='1') hand = self._hand(self._to_34_array(tiles + [win_tile])) fu_details, fu = fu_calculator.calculate_fu(hand, win_tile, self._get_win_group(hand, win_tile), config) self.assertEqual(2, len(fu_details)) self.assertTrue({'fu': 30, 'reason': FuCalculator.BASE} in fu_details) self.assertTrue({'fu': 2, 'reason': FuCalculator.PAIR_WAIT} in fu_details) self.assertEqual(fu, 40)
def test_tsumo_hand_and_disabled_pinfu(self): fu_calculator = FuCalculator() config = HandConfig(is_tsumo=True, options=OptionalRules(fu_for_pinfu_tsumo=True)) tiles = self._string_to_136_array(sou='2278', man='123456', pin='123') win_tile = self._string_to_136_tile(sou='6') hand = self._hand(self._to_34_array(tiles + [win_tile])) fu_details, fu = fu_calculator.calculate_fu(hand, win_tile, self._get_win_group(hand, win_tile), config) self.assertEqual(2, len(fu_details)) self.assertTrue({'fu': 20, 'reason': FuCalculator.BASE} in fu_details) self.assertTrue({'fu': 2, 'reason': FuCalculator.TSUMO} in fu_details) self.assertEqual(fu, 30)
def test_open_terminal_kan_fu(self): fu_calculator = FuCalculator() config = HandConfig() tiles = self._string_to_136_array(sou='2278', man='123456', pin='111') win_tile = self._string_to_136_tile(sou='6') hand = self._hand(self._to_34_array(tiles + [win_tile])) melds = [self._make_meld(Meld.KAN, pin='111', is_open=True)] fu_details, fu = fu_calculator.calculate_fu(hand, win_tile, self._get_win_group(hand, win_tile), config, melds=melds) self.assertEqual(2, len(fu_details)) self.assertTrue({'fu': 20, 'reason': FuCalculator.BASE} in fu_details) self.assertTrue({'fu': 16, 'reason': FuCalculator.OPEN_TERMINAL_KAN} in fu_details) self.assertEqual(fu, 40)
def test_open_hand_base(self): fu_calculator = FuCalculator() config = HandConfig() tiles = self._string_to_136_array(sou='22278', man='123456', pin='11') win_tile = self._string_to_136_tile(sou='6') hand = self._hand(self._to_34_array(tiles + [win_tile])) melds = [self._make_meld(Meld.PON, sou='222')] fu_details, fu = fu_calculator.calculate_fu(hand, win_tile, self._get_win_group(hand, win_tile), config, melds=melds) self.assertEqual(2, len(fu_details)) self.assertTrue({'fu': 20, 'reason': FuCalculator.BASE} in fu_details) self.assertTrue({'fu': 2, 'reason': FuCalculator.OPEN_PON} in fu_details) self.assertEqual(fu, 30)
def test_open_hand_withou_additional_fu(self): fu_calculator = FuCalculator() config = HandConfig() tiles = self._string_to_136_array(sou='23478', man='234567', pin='22') win_tile = self._string_to_136_tile(sou='6') hand = self._hand(self._to_34_array(tiles + [win_tile])) melds = [self._make_meld(Meld.CHI, sou='234')] config = HandConfig(options=OptionalRules(fu_for_open_pinfu=False)) fu_details, fu = fu_calculator.calculate_fu(hand, win_tile, self._get_win_group(hand, win_tile), config, melds=melds) self.assertEqual(1, len(fu_details)) self.assertTrue({'fu': 20, 'reason': FuCalculator.BASE} in fu_details) self.assertEqual(fu, 20)
def test_open_hand_without_additional_fu(self): fu_calculator = FuCalculator() config = HandConfig() tiles = self._string_to_136_array(sou="23478", man="234567", pin="22") win_tile = self._string_to_136_tile(sou="6") hand = self._hand(self._to_34_array(tiles + [win_tile])) melds = [self._make_meld(Meld.CHI, sou="234")] fu_details, fu = fu_calculator.calculate_fu(hand, win_tile, self._get_win_group( hand, win_tile), config, melds=melds) self.assertEqual(2, len(fu_details)) self.assertTrue({"fu": 20, "reason": FuCalculator.BASE} in fu_details) self.assertTrue({ "fu": 2, "reason": FuCalculator.HAND_WITHOUT_FU } in fu_details) self.assertEqual(fu, 30)
def test_closed_terminal_kan_fu(self): fu_calculator = FuCalculator() config = HandConfig() tiles = self._string_to_136_array(sou="2278", man="123456", pin="111") win_tile = self._string_to_136_tile(sou="6") hand = self._hand(self._to_34_array(tiles + [win_tile])) melds = [self._make_meld(Meld.KAN, pin="111", is_open=False)] fu_details, fu = fu_calculator.calculate_fu(hand, win_tile, self._get_win_group( hand, win_tile), config, melds=melds) self.assertEqual(2, len(fu_details)) self.assertTrue({"fu": 30, "reason": FuCalculator.BASE} in fu_details) self.assertTrue({ "fu": 32, "reason": FuCalculator.CLOSED_TERMINAL_KAN } in fu_details) self.assertEqual(fu, 70)
def test_open_kan_fu(self): fu_calculator = FuCalculator() config = HandConfig() tiles = self._string_to_136_array(sou="22278", man="123456", pin="11") win_tile = self._string_to_136_tile(sou="6") hand = self._hand(self._to_34_array(tiles + [win_tile])) melds = [self._make_meld(Meld.KAN, sou="222", is_open=True)] fu_details, fu = fu_calculator.calculate_fu(hand, win_tile, self._get_win_group( hand, win_tile), config, melds=melds) self.assertEqual(2, len(fu_details)) self.assertTrue({"fu": 20, "reason": FuCalculator.BASE} in fu_details) self.assertTrue({ "fu": 8, "reason": FuCalculator.OPEN_KAN } in fu_details) self.assertEqual(fu, 30)
def test_open_terminal_pon_fu(self): fu_calculator = FuCalculator() config = HandConfig() tiles = self._string_to_136_array(sou="2278", man="123456", honors="111") win_tile = self._string_to_136_tile(sou="6") hand = self._hand(self._to_34_array(tiles + [win_tile])) melds = [self._make_meld(Meld.PON, honors="111")] fu_details, fu = fu_calculator.calculate_fu(hand, win_tile, self._get_win_group( hand, win_tile), config, melds=melds) self.assertEqual(2, len(fu_details)) self.assertTrue({"fu": 20, "reason": FuCalculator.BASE} in fu_details) self.assertTrue({ "fu": 4, "reason": FuCalculator.OPEN_TERMINAL_PON } in fu_details) self.assertEqual(fu, 30)
def estimate_hand_value( self, tiles, win_tile, melds=None, dora_indicators=None, config=None, scores_calculator_factory=ScoresCalculator, use_hand_divider_cache=False, ): """ :param tiles: array with 14 tiles in 136-tile format :param win_tile: 136 format tile that caused win (ron or tsumo) :param melds: array with Meld objects :param dora_indicators: array of tiles in 136-tile format :param config: HandConfig object :param use_hand_divider_cache: could be useful if you are calculating a lot of menchin hands :return: HandResponse object """ if not melds: melds = [] if not dora_indicators: dora_indicators = [] self.config = config or HandConfig() 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] all_melds = [x.tiles_34 for x in melds] is_open_hand = len(opened_melds) > 0 # special situation if self.config.is_nagashi_mangan: hand_yaku.append(self.config.yaku.nagashi_mangan) fu = 30 han = self.config.yaku.nagashi_mangan.han_closed cost = scores_calculator.calculate_scores(han, fu, self.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: return HandResponse(error=HandCalculator.ERR_OPEN_HAND_RIICHI) if self.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: return HandResponse( error=HandCalculator.ERR_IPPATSU_WITHOUT_RIICHI) if self.config.is_chankan and self.config.is_tsumo: return HandResponse(error=HandCalculator.ERR_CHANKAN_WITH_TSUMO) if self.config.is_rinshan and not self.config.is_tsumo: return HandResponse(error=HandCalculator.ERR_RINSHAN_WITHOUT_TSUMO) if self.config.is_haitei and not self.config.is_tsumo: return HandResponse(error=HandCalculator.ERR_HAITEI_WITHOUT_TSUMO) if self.config.is_houtei and self.config.is_tsumo: return HandResponse(error=HandCalculator.ERR_HOUTEI_WITH_TSUMO) if self.config.is_haitei and self.config.is_rinshan: return HandResponse(error=HandCalculator.ERR_HAITEI_WITH_RINSHAN) if self.config.is_houtei and self.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: return HandResponse(error=HandCalculator.ERR_TENHOU_NOT_AS_DEALER) if self.config.is_tenhou and not self.config.is_tsumo: return HandResponse(error=HandCalculator.ERR_TENHOU_WITHOUT_TSUMO) if self.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: return HandResponse(error=HandCalculator.ERR_CHIIHOU_AS_DEALER) if self.config.is_chiihou and not self.config.is_tsumo: return HandResponse(error=HandCalculator.ERR_CHIIHOU_WITHOUT_TSUMO) if self.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: return HandResponse(error=HandCalculator.ERR_RENHOU_AS_DEALER) if self.config.is_renhou and self.config.is_tsumo: return HandResponse(error=HandCalculator.ERR_RENHOU_WITH_TSUMO) if self.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 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 hand_options = self.divider.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 ] win_groups = self._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) is_pinfu = len( fu_details) == 1 and not is_chiitoitsu and not is_open_hand pon_sets = [x for x in hand if is_pon(x)] 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 not is_open_hand: hand_yaku.append(self.config.yaku.tsumo) if is_pinfu: hand_yaku.append(self.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) 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) if self.config.options.has_daichisei and self.config.yaku.daichisei.is_condition_met( hand): hand_yaku.append(self.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 = False if is_tanyao: hand_yaku.append(self.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) else: hand_yaku.append(self.config.yaku.riichi) if self.config.is_daburu_riichi: if self.config.is_open_riichi: hand_yaku.append(self.config.yaku.daburu_open_riichi) else: hand_yaku.append(self.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))): hand_yaku.append(self.config.yaku.sashikomi) if self.config.is_ippatsu: hand_yaku.append(self.config.yaku.ippatsu) if self.config.is_rinshan: hand_yaku.append(self.config.yaku.rinshan) if self.config.is_chankan: hand_yaku.append(self.config.yaku.chankan) if self.config.is_haitei: hand_yaku.append(self.config.yaku.haitei) if self.config.is_houtei: hand_yaku.append(self.config.yaku.houtei) if self.config.is_renhou: if self.config.options.renhou_as_yakuman: hand_yaku.append(self.config.yaku.renhou_yakuman) else: hand_yaku.append(self.config.yaku.renhou) if self.config.is_tenhou: hand_yaku.append(self.config.yaku.tenhou) if self.config.is_chiihou: hand_yaku.append(self.config.yaku.chiihou) if self.config.yaku.honitsu.is_condition_met(hand): hand_yaku.append(self.config.yaku.honitsu) if self.config.yaku.chinitsu.is_condition_met(hand): hand_yaku.append(self.config.yaku.chinitsu) if self.config.yaku.tsuisou.is_condition_met(hand): hand_yaku.append(self.config.yaku.tsuisou) if self.config.yaku.honroto.is_condition_met(hand): hand_yaku.append(self.config.yaku.honroto) if self.config.yaku.chinroto.is_condition_met(hand): hand_yaku.append(self.config.yaku.chinroto) if self.config.yaku.ryuisou.is_condition_met(hand): hand_yaku.append(self.config.yaku.ryuisou) if self.config.paarenchan > 0 and not self.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) # 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 self.config.yaku.junchan.is_condition_met(hand): hand_yaku.append(self.config.yaku.junchan) if self.config.yaku.ittsu.is_condition_met(hand): hand_yaku.append(self.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 self.config.yaku.sanshoku.is_condition_met(hand): hand_yaku.append(self.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 self.config.yaku.sanankou.is_condition_met( hand, win_tile, melds, self.config.is_tsumo): hand_yaku.append(self.config.yaku.sanankou) if self.config.yaku.sanshoku_douko.is_condition_met(hand): hand_yaku.append(self.config.yaku.sanshoku_douko) if self.config.yaku.shosangen.is_condition_met(hand): hand_yaku.append(self.config.yaku.shosangen) if self.config.yaku.haku.is_condition_met(hand): hand_yaku.append(self.config.yaku.haku) if self.config.yaku.hatsu.is_condition_met(hand): hand_yaku.append(self.config.yaku.hatsu) if self.config.yaku.chun.is_condition_met(hand): hand_yaku.append(self.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 self.config.round_wind == EAST: hand_yaku.append(self.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 self.config.round_wind == SOUTH: hand_yaku.append(self.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 self.config.round_wind == WEST: hand_yaku.append(self.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 self.config.round_wind == NORTH: hand_yaku.append(self.config.yaku.yakuhai_round) if self.config.yaku.daisangen.is_condition_met(hand): hand_yaku.append(self.config.yaku.daisangen) if self.config.yaku.shosuushi.is_condition_met(hand): hand_yaku.append(self.config.yaku.shosuushi) if self.config.yaku.daisuushi.is_condition_met(hand): hand_yaku.append(self.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 tiles_34[win_tile // 4] == 2 or tiles_34[win_tile // 4] == 4: hand_yaku.append( self.config.yaku.daburu_chuuren_poutou) else: hand_yaku.append(self.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 tiles_34[win_tile // 4] == 2: hand_yaku.append(self.config.yaku.suuankou_tanki) else: hand_yaku.append(self.config.yaku.suuankou) if self.config.yaku.sankantsu.is_condition_met( hand, melds): hand_yaku.append(self.config.yaku.sankantsu) if self.config.yaku.suukantsu.is_condition_met( hand, melds): hand_yaku.append(self.config.yaku.suukantsu) if self.config.paarenchan > 0 and self.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) # yakuman is not connected with other yaku yakuman_list = [x for x in hand_yaku if x.is_yakuman] if yakuman_list: if not is_aotenjou: hand_yaku = yakuman_list else: scores_calculator.aotenjou_filter_yaku( hand_yaku, self.config) yakuman_list = [] # calculate han for item in hand_yaku: if is_open_hand and item.han_open: han += item.han_open else: han += item.han_closed if han == 0: error = HandCalculator.ERR_NO_YAKU cost = None # we don't need to add dora to yakuman if not yakuman_list: tiles_for_dora = tiles[:] count_of_dora = 0 count_of_aka_dora = 0 for tile in tiles_for_dora: 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): 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) 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) han += count_of_aka_dora if not is_aotenjou and ( self.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) calculated_hand = { "cost": cost, "error": error, "hand_yaku": hand_yaku, "han": han, "fu": fu, "fu_details": fu_details, } calculated_hands.append(calculated_hand) # exception hand if not is_open_hand and self.config.yaku.kokushi.is_condition_met( None, tiles_34): if tiles_34[win_tile // 4] == 2: hand_yaku.append(self.config.yaku.daburu_kokushi) else: hand_yaku.append(self.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 self.config.is_daburu_riichi: if self.config.is_open_riichi: hand_yaku.append(self.config.yaku.sashikomi) if self.config.is_renhou and self.config.options.renhou_as_yakuman: hand_yaku.append(self.config.yaku.renhou_yakuman) if self.config.is_tenhou: hand_yaku.append(self.config.yaku.tenhou) if self.config.is_chiihou: hand_yaku.append(self.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) # calculate han han = 0 for item in hand_yaku: if is_open_hand and item.han_open: han += item.han_open else: han += item.han_closed fu = 0 if is_aotenjou: if self.config.is_tsumo: fu = 30 else: fu = 40 tiles_for_dora = tiles[:] count_of_dora = 0 count_of_aka_dora = 0 for tile in tiles_for_dora: 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): 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) 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) han += count_of_aka_dora cost = scores_calculator.calculate_scores(han, fu, self.config, len(hand_yaku) > 0) calculated_hands.append({ "cost": cost, "error": None, "hand_yaku": hand_yaku, "han": han, "fu": fu, "fu_details": [] }) # let's use cost for most expensive hand calculated_hands = sorted(calculated_hands, key=lambda x: (x["han"], x["fu"]), reverse=True) calculated_hand = calculated_hands[0] cost = calculated_hand["cost"] error = calculated_hand["error"] hand_yaku = calculated_hand["hand_yaku"] han = calculated_hand["han"] fu = calculated_hand["fu"] fu_details = calculated_hand["fu_details"] return HandResponse(cost, han, fu, hand_yaku, error, fu_details, is_open_hand)
def estimate_hand_value(self, tiles, win_tile, melds=None, dora_indicators=None, config=None): """ :param tiles: array with 14 tiles in 136-tile format :param win_tile: 136 format tile that caused win (ron or tsumo) :param melds: array with Meld objects :param dora_indicators: array of tiles in 136-tile format :param config: HandConfig object :return: HandResponse object """ if not melds: melds = [] if not dora_indicators: dora_indicators = [] self.config = config or HandConfig() agari = Agari() hand_yaku = [] scores_calculator = ScoresCalculator() tiles_34 = TilesConverter.to_34_array(tiles) divider = HandDivider() fu_calculator = FuCalculator() 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 if self.config.is_nagashi_mangan: hand_yaku.append(self.config.yaku.nagashi_mangan) fu = 30 han = self.config.yaku.nagashi_mangan.han_closed cost = scores_calculator.calculate_scores(han, fu, self.config, False) return HandResponse(cost, han, fu, hand_yaku) if win_tile not in tiles: return HandResponse(error="Win tile not in the hand") if self.config.is_riichi and is_open_hand: return HandResponse( error="Riichi can't be declared with open hand") if self.config.is_ippatsu and is_open_hand: return HandResponse( error="Ippatsu can't be declared with open hand") if self.config.is_ippatsu and not self.config.is_riichi and not self.config.is_daburu_riichi: return HandResponse( error="Ippatsu can't be declared without riichi") if not agari.is_agari(tiles_34, all_melds): return HandResponse(error='Hand is 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 hand_options = divider.divide_hand(tiles_34, melds) 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 ] win_groups = self._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) is_pinfu = len( fu_details) == 1 and not is_chiitoitsu and not is_open_hand pon_sets = [x for x in hand if is_pon(x)] chi_sets = [x for x in hand if is_chi(x)] if self.config.is_tsumo: if not is_open_hand: hand_yaku.append(self.config.yaku.tsumo) if is_pinfu: hand_yaku.append(self.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) 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_tanyao = self.config.yaku.tanyao.is_condition_met(hand) if is_open_hand and not self.config.options.has_open_tanyao: is_tanyao = False if is_tanyao: hand_yaku.append(self.config.yaku.tanyao) if self.config.is_riichi and not self.config.is_daburu_riichi: hand_yaku.append(self.config.yaku.riichi) if self.config.is_daburu_riichi: hand_yaku.append(self.config.yaku.daburu_riichi) if self.config.is_ippatsu: hand_yaku.append(self.config.yaku.ippatsu) if self.config.is_rinshan: hand_yaku.append(self.config.yaku.rinshan) if self.config.is_chankan: hand_yaku.append(self.config.yaku.chankan) if self.config.is_haitei: hand_yaku.append(self.config.yaku.haitei) if self.config.is_houtei: hand_yaku.append(self.config.yaku.houtei) if self.config.is_renhou: if self.config.options.renhou_as_yakuman: hand_yaku.append(self.config.yaku.renhou_yakuman) else: hand_yaku.append(self.config.yaku.renhou) if self.config.is_tenhou: hand_yaku.append(self.config.yaku.tenhou) if self.config.is_chiihou: hand_yaku.append(self.config.yaku.chiihou) if self.config.yaku.honitsu.is_condition_met(hand): hand_yaku.append(self.config.yaku.honitsu) if self.config.yaku.chinitsu.is_condition_met(hand): hand_yaku.append(self.config.yaku.chinitsu) if self.config.yaku.tsuisou.is_condition_met(hand): hand_yaku.append(self.config.yaku.tsuisou) if self.config.yaku.honroto.is_condition_met(hand): hand_yaku.append(self.config.yaku.honroto) if self.config.yaku.chinroto.is_condition_met(hand): hand_yaku.append(self.config.yaku.chinroto) # 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.chanta.is_condition_met(hand): hand_yaku.append(self.config.yaku.chanta) if self.config.yaku.junchan.is_condition_met(hand): hand_yaku.append(self.config.yaku.junchan) if self.config.yaku.ittsu.is_condition_met(hand): hand_yaku.append(self.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 self.config.yaku.sanshoku.is_condition_met(hand): hand_yaku.append(self.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): if self.config.yaku.toitoi.is_condition_met(hand): hand_yaku.append(self.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 self.config.yaku.sanshoku_douko.is_condition_met(hand): hand_yaku.append(self.config.yaku.sanshoku_douko) if self.config.yaku.shosangen.is_condition_met(hand): hand_yaku.append(self.config.yaku.shosangen) if self.config.yaku.haku.is_condition_met(hand): hand_yaku.append(self.config.yaku.haku) if self.config.yaku.hatsu.is_condition_met(hand): hand_yaku.append(self.config.yaku.hatsu) if self.config.yaku.chun.is_condition_met(hand): hand_yaku.append(self.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 self.config.round_wind == EAST: hand_yaku.append(self.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 self.config.round_wind == SOUTH: hand_yaku.append(self.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 self.config.round_wind == WEST: hand_yaku.append(self.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 self.config.round_wind == NORTH: hand_yaku.append(self.config.yaku.yakuhai_round) if self.config.yaku.daisangen.is_condition_met(hand): hand_yaku.append(self.config.yaku.daisangen) if self.config.yaku.shosuushi.is_condition_met(hand): hand_yaku.append(self.config.yaku.shosuushi) if self.config.yaku.daisuushi.is_condition_met(hand): hand_yaku.append(self.config.yaku.daisuushi) if self.config.yaku.ryuisou.is_condition_met(hand): hand_yaku.append(self.config.yaku.ryuisou) # closed kan can't be used in chuuren_poutou if not len( melds ) and self.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) else: hand_yaku.append(self.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 tiles_34[win_tile // 4] == 2: hand_yaku.append(self.config.yaku.suuankou_tanki) else: hand_yaku.append(self.config.yaku.suuankou) if self.config.yaku.sankantsu.is_condition_met( hand, melds): hand_yaku.append(self.config.yaku.sankantsu) if self.config.yaku.suukantsu.is_condition_met( hand, melds): hand_yaku.append(self.config.yaku.suukantsu) # yakuman is not connected with other yaku yakuman_list = [x for x in hand_yaku if x.is_yakuman] if yakuman_list: hand_yaku = yakuman_list # calculate han for item in hand_yaku: if is_open_hand and item.han_open: han += item.han_open else: han += item.han_closed if han == 0: error = 'There are no yaku in the hand' cost = None # we don't need to add dora to yakuman if not yakuman_list: tiles_for_dora = tiles[:] # we had to search for dora in kan fourth tiles as well for meld in melds: if meld.type == Meld.KAN or meld.type == Meld.CHANKAN: tiles_for_dora.append(meld.tiles[3]) count_of_dora = 0 count_of_aka_dora = 0 for tile in tiles_for_dora: 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): 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) 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) han += count_of_aka_dora if not error: cost = scores_calculator.calculate_scores( han, fu, self.config, len(yakuman_list) > 0) calculated_hand = { 'cost': cost, 'error': error, 'hand_yaku': hand_yaku, 'han': han, 'fu': fu, 'fu_details': fu_details } calculated_hands.append(calculated_hand) # exception hand if not is_open_hand and self.config.yaku.kokushi.is_condition_met( None, tiles_34): if tiles_34[win_tile // 4] == 2: hand_yaku.append(self.config.yaku.daburu_kokushi) else: hand_yaku.append(self.config.yaku.kokushi) if self.config.is_renhou and self.config.options.renhou_as_yakuman: hand_yaku.append(self.config.yaku.renhou_yakuman) if self.config.is_tenhou: hand_yaku.append(self.config.yaku.tenhou) if self.config.is_chiihou: hand_yaku.append(self.config.yaku.chiihou) # calculate han han = 0 for item in hand_yaku: if is_open_hand and item.han_open: han += item.han_open else: han += item.han_closed fu = 0 cost = scores_calculator.calculate_scores(han, fu, self.config, len(hand_yaku) > 0) calculated_hands.append({ 'cost': cost, 'error': None, 'hand_yaku': hand_yaku, 'han': han, 'fu': fu, 'fu_details': [] }) # let's use cost for most expensive hand calculated_hands = sorted(calculated_hands, key=lambda x: (x['han'], x['fu']), reverse=True) calculated_hand = calculated_hands[0] cost = calculated_hand['cost'] error = calculated_hand['error'] hand_yaku = calculated_hand['hand_yaku'] han = calculated_hand['han'] fu = calculated_hand['fu'] fu_details = calculated_hand['fu_details'] return HandResponse(cost, han, fu, hand_yaku, error, fu_details)