def test_second_one_suit_hand_dividing(self): hand = HandDivider() tiles_34 = self._string_to_34_array(sou="111123666789", honors="11") result = hand.divide_hand(tiles_34) self.assertEqual(len(result), 1) self.assertEqual(self._string(result[0]), ["111s", "123s", "666s", "789s", "11z"])
def test_second_one_suit_hand_dividing(self): hand = HandDivider() tiles_34 = self._string_to_34_array(sou='111123666789', honors='11') result = hand.divide_hand(tiles_34) self.assertEqual(len(result), 1) self.assertEqual(self._string(result[0]), ['111s', '123s', '666s', '789s', '11z'])
def test_simple_hand_dividing(self): hand = HandDivider() tiles_34 = self._string_to_34_array(man="234567", sou="23455", honors="777") result = hand.divide_hand(tiles_34) self.assertEqual(len(result), 1) self.assertEqual(self._string(result[0]), ["234m", "567m", "234s", "55s", "777z"])
def test_one_suit_hand_dividing(self): hand = HandDivider() tiles_34 = self._string_to_34_array(man='11122233388899') result = hand.divide_hand(tiles_34) self.assertEqual(len(result), 2) self.assertEqual(self._string(result[0]), ['111m', '222m', '333m', '888m', '99m']) self.assertEqual(self._string(result[1]), ['123m', '123m', '123m', '888m', '99m'])
def test_one_suit_hand_dividing(self): hand = HandDivider() tiles_34 = self._string_to_34_array(man="11122233388899") result = hand.divide_hand(tiles_34) self.assertEqual(len(result), 2) self.assertEqual(self._string(result[0]), ["111m", "222m", "333m", "888m", "99m"]) self.assertEqual(self._string(result[1]), ["123m", "123m", "123m", "888m", "99m"])
def test_second_simple_hand_dividing(self): hand = HandDivider() tiles_34 = self._string_to_34_array(man='123', pin='123', sou='123', honors='11222') result = hand.divide_hand(tiles_34) self.assertEqual(len(result), 1) self.assertEqual( self._string(result[0]), ['123m', '123p', '123s', '11z', '222z'] )
def test_simple_hand_dividing(self): hand = HandDivider() tiles_34 = self._string_to_34_array(man='234567', sou='23455', honors='777') result = hand.divide_hand(tiles_34) self.assertEqual(len(result), 1) self.assertEqual( self._string(result[0]), ['234m', '567m', '234s', '55s', '777z'] )
def test_hand_with_pairs_dividing(self): hand = HandDivider() tiles_34 = self._string_to_34_array(man="23444", pin="344556", sou="333") result = hand.divide_hand(tiles_34) self.assertEqual(len(result), 1) self.assertEqual(self._string(result[0]), ["234m", "44m", "345p", "456p", "333s"])
def test_hand_with_pairs_dividing(self): hand = HandDivider() tiles_34 = self._string_to_34_array(man='23444', pin='344556', sou='333') result = hand.divide_hand(tiles_34) self.assertEqual(len(result), 1) self.assertEqual( self._string(result[0]), ['234m', '44m', '345p', '456p', '333s'] )
def test_second_simple_hand_dividing(self): hand = HandDivider() tiles_34 = self._string_to_34_array(man="123", pin="123", sou="123", honors="11222") result = hand.divide_hand(tiles_34) self.assertEqual(len(result), 1) self.assertEqual(self._string(result[0]), ["123m", "123p", "123s", "11z", "222z"])
def test_chitoitsu_like_hand_dividing(self): hand = HandDivider() tiles_34 = self._string_to_34_array(man='112233', pin='99', sou='445566') result = hand.divide_hand(tiles_34) self.assertEqual(len(result), 2) self.assertEqual(self._string(result[0]), ['11m', '22m', '33m', '99p', '44s', '55s', '66s']) self.assertEqual(self._string(result[1]), ['123m', '123m', '99p', '456s', '456s'])
def test_third_one_suit_hand_dividing(self): hand = HandDivider() tiles_34 = self._string_to_34_array(pin='234777888999', honors='22') melds = [ self._make_meld(Meld.CHI, pin='789'), self._make_meld(Meld.CHI, pin='234'), ] result = hand.divide_hand(tiles_34, melds) self.assertEqual(len(result), 1) self.assertEqual(self._string(result[0]), ['234p', '789p', '789p', '789p', '22z'])
def test_chitoitsu_like_hand_dividing(self): hand = HandDivider() tiles_34 = self._string_to_34_array(man="112233", pin="99", sou="445566") result = hand.divide_hand(tiles_34) self.assertEqual(len(result), 2) self.assertEqual(self._string(result[0]), ["11m", "22m", "33m", "99p", "44s", "55s", "66s"]) self.assertEqual(self._string(result[1]), ["123m", "123m", "99p", "456s", "456s"])
def test_third_one_suit_hand_dividing(self): hand = HandDivider() tiles_34 = self._string_to_34_array(pin="234777888999", honors="22") melds = [ self._make_meld(Meld.CHI, pin="789"), self._make_meld(Meld.CHI, pin="234"), ] result = hand.divide_hand(tiles_34, melds) self.assertEqual(len(result), 1) self.assertEqual(self._string(result[0]), ["234p", "789p", "789p", "789p", "22z"])
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)
class HandCalculator: config = None ERR_NO_WINNING_TILE = "winning_tile_not_in_hand" ERR_OPEN_HAND_RIICHI = "open_hand_riichi_not_allowed" 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_NO_YAKU = "no_yaku" ERR_CHANKAN_WITH_TSUMO = "chankan_with_tsumo_not_allowed" ERR_RINSHAN_WITHOUT_TSUMO = "rinshan_without_tsumo_not_allowed" ERR_HAITEI_WITHOUT_TSUMO = "haitei_without_tsumo_not_allowed" ERR_HOUTEI_WITH_TSUMO = "houtei_with_tsumo_not_allowed" ERR_HAITEI_WITH_RINSHAN = "haitei_with_rinshan_not_allowed" ERR_HOUTEI_WITH_CHANKAN = "houtei_with_chankan_not_allowed" ERR_TENHOU_NOT_AS_DEALER = "tenhou_not_as_dealer_not_allowed" ERR_TENHOU_WITHOUT_TSUMO = "tenhou_without_tsumo_not_allowed" ERR_TENHOU_WITH_MELD = "tenhou_with_meld_not_allowed" ERR_CHIIHOU_AS_DEALER = "chiihou_as_dealer_not_allowed" ERR_CHIIHOU_WITHOUT_TSUMO = "chiihou_without_tsumo_not_allowed" ERR_CHIIHOU_WITH_MELD = "chiihou_with_meld_not_allowed" ERR_RENHOU_AS_DEALER = "renhou_as_dealer_not_allowed" ERR_RENHOU_WITH_TSUMO = "renhou_with_tsumo_not_allowed" ERR_RENHOU_WITH_MELD = "renhou_with_meld_not_allowed" # more possible errors, like tenhou and haitei can't be together (so complicated :<) def __init__(self): self.divider = HandDivider() 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 _find_win_groups(self, win_tile, hand, opened_melds): win_tile_34 = (win_tile or 0) // 4 # to detect win groups # we had to use only closed sets closed_set_items = [] for x in hand: if x not in opened_melds: closed_set_items.append(x) else: opened_melds.remove(x) # for forms like 45666 and ron on 6 # we can assume that ron was on 456 form and on 66 form # and depends on form we will have different hand cost # so, we had to check all possible win groups win_groups = [x for x in closed_set_items if win_tile_34 in x] unique_win_groups = [ list(x) for x in set(tuple(x) for x in win_groups) ] return unique_win_groups
def _hand(self, tiles, hand_index=0): hand_divider = HandDivider() return hand_divider.divide_hand(tiles)[hand_index]
class ImplementationAI(InterfaceAI): version = '0.0.1' agari = None shanten = None defence = None hand_divider = None finished_hand = None last_discard_option = None previous_shanten = 7 in_defence = False waiting = None current_strategy = None def __init__(self, player): super(ImplementationAI, self).__init__(player) self.agari = Agari() self.shanten = Shanten() self.defence = DefenceHandler(player) self.hand_divider = HandDivider() self.finished_hand = HandCalculator() self.previous_shanten = 7 self.current_strategy = None self.waiting = [] self.in_defence = False self.last_discard_option = None def init_hand(self): """ Let's decide what we will do with our hand (like open for tanyao and etc.) """ self.determine_strategy() def erase_state(self): self.current_strategy = None self.in_defence = False self.last_discard_option = None def draw_tile(self, tile): """ :param tile: 136 tile format :return: """ self.determine_strategy() def discard_tile(self, discard_tile): # we called meld and we had discard tile that we wanted to discard if discard_tile is not None: if not self.last_discard_option: return discard_tile return self.process_discard_option(self.last_discard_option, self.player.closed_hand, True) results, shanten = self.calculate_outs(self.player.tiles, self.player.closed_hand, self.player.open_hand_34_tiles) selected_tile = self.process_discard_options_and_select_tile_to_discard( results, shanten) # bot think that there is a threat on the table # and better to fold # if we can't find safe tiles, let's continue to build our hand if self.defence.should_go_to_defence_mode(selected_tile): if not self.in_defence: logger.info('We decided to fold against other players') self.in_defence = True defence_tile = self.defence.try_to_find_safe_tile_to_discard( results) if defence_tile: return self.process_discard_option(defence_tile, self.player.closed_hand) else: self.in_defence = False return self.process_discard_option(selected_tile, self.player.closed_hand) def process_discard_options_and_select_tile_to_discard( self, results, shanten, had_was_open=False): tiles_34 = TilesConverter.to_34_array(self.player.tiles) # we had to update tiles value there # because it is related with shanten number for result in results: result.tiles_count = self.count_tiles(result.waiting, tiles_34) result.calculate_value(shanten) # current strategy can affect on our discard options # so, don't use strategy specific choices for calling riichi if self.current_strategy: results = self.current_strategy.determine_what_to_discard( self.player.closed_hand, results, shanten, False, None, had_was_open) return self.chose_tile_to_discard(results) def calculate_outs(self, tiles, closed_hand, open_sets_34=None): """ :param tiles: array of tiles in 136 format :param closed_hand: array of tiles in 136 format :param open_sets_34: array of array with tiles in 34 format :return: """ tiles_34 = TilesConverter.to_34_array(tiles) closed_tiles_34 = TilesConverter.to_34_array(closed_hand) is_agari = self.agari.is_agari(tiles_34, self.player.open_hand_34_tiles) results = [] for hand_tile in range(0, 34): if not closed_tiles_34[hand_tile]: continue tiles_34[hand_tile] -= 1 shanten = self.shanten.calculate_shanten(tiles_34, open_sets_34) waiting = [] for j in range(0, 34): if hand_tile == j or tiles_34[j] == 4: continue tiles_34[j] += 1 if self.shanten.calculate_shanten(tiles_34, open_sets_34) == shanten - 1: waiting.append(j) tiles_34[j] -= 1 tiles_34[hand_tile] += 1 if waiting: results.append( DiscardOption(player=self.player, shanten=shanten, tile_to_discard=hand_tile, waiting=waiting, tiles_count=self.count_tiles( waiting, tiles_34))) if is_agari: shanten = Shanten.AGARI_STATE else: shanten = self.shanten.calculate_shanten(tiles_34, open_sets_34) return results, shanten def count_tiles(self, waiting, tiles_34): n = 0 for item in waiting: n += 4 - self.player.total_tiles(item, tiles_34) return n def try_to_call_meld(self, tile, is_kamicha_discard): if not self.current_strategy: return None, None if len(self.player.discards) <= 5 and tile // 4 <= 27: return None, None meld, discard_option = self.current_strategy.try_to_call_meld( tile, is_kamicha_discard) tile_to_discard = None if discard_option: self.last_discard_option = discard_option tile_to_discard = discard_option.tile_to_discard return meld, tile_to_discard def determine_strategy(self): # for already opened hand we don't need to give up on selected strategy if self.player.is_open_hand and self.current_strategy: return False return False old_strategy = self.current_strategy self.current_strategy = None # order is important, the first appropriate strtegy will be used strategies = [] if self.player.table.has_open_tanyao: strategies.append(TanyaoStrategy(BaseStrategy.TANYAO, self.player)) strategies.append(YakuhaiStrategy(BaseStrategy.YAKUHAI, self.player)) strategies.append(HonitsuStrategy(BaseStrategy.HONITSU, self.player)) for strategy in strategies: if strategy.should_activate_strategy(): self.current_strategy = strategy if self.current_strategy: if not old_strategy or self.current_strategy.type != old_strategy.type: message = '{} switched to {} strategy'.format( self.player.name, self.current_strategy) if old_strategy: message += ' from {}'.format(old_strategy) logger.debug(message) logger.debug('With hand: {}'.format( TilesConverter.to_one_line_string(self.player.tiles))) if not self.current_strategy and old_strategy: logger.debug('{} gave up on {}'.format(self.player.name, old_strategy)) return self.current_strategy and True or False def chose_tile_to_discard(self, results: [DiscardOption]) -> DiscardOption: """ Try to find best tile to discard, based on different valuations """ def sorting(x): # - is important for x.tiles_count # in that case we will discard tile that will give for us more tiles # to complete a hand return x.shanten, -x.tiles_count, x.valuation had_to_be_discarded_tiles = [ x for x in results if x.had_to_be_discarded ] if had_to_be_discarded_tiles: had_to_be_discarded_tiles = sorted(had_to_be_discarded_tiles, key=sorting) selected_tile = had_to_be_discarded_tiles[0] else: results = sorted(results, key=sorting) # remove needed tiles from discard options results = [x for x in results if not x.had_to_be_saved] # let's chose most valuable tile first temp_tile = results[0] # and let's find all tiles with same shanten results_with_same_shanten = [ x for x in results if x.shanten == temp_tile.shanten ] possible_options = [temp_tile] for discard_option in results_with_same_shanten: # there is no sense to check already chosen tile if discard_option.tile_to_discard == temp_tile.tile_to_discard: continue # we don't need to select tiles almost dead waits if discard_option.tiles_count <= 2: continue # let's check all other tiles with same shanten # maybe we can find tiles that have almost same tiles count number if temp_tile.tiles_count - 2 < discard_option.tiles_count < temp_tile.tiles_count + 2: possible_options.append(discard_option) # let's sort got tiles by value and let's chose less valuable tile to discard possible_options = sorted(possible_options, key=lambda x: x.valuation) selected_tile = possible_options[0] return selected_tile def process_discard_option(self, discard_option, closed_hand, force_discard=False): self.waiting = discard_option.waiting self.player.ai.previous_shanten = discard_option.shanten self.player.in_tempai = self.player.ai.previous_shanten == 0 # when we called meld we don't need "smart" discard if force_discard: return discard_option.find_tile_in_hand(closed_hand) last_draw_34 = self.player.last_draw and self.player.last_draw // 4 or None if self.player.last_draw not in AKA_DORA_LIST and last_draw_34 == discard_option.tile_to_discard: return self.player.last_draw else: return discard_option.find_tile_in_hand(closed_hand) def estimate_hand_value(self, win_tile, tiles=None, call_riichi=False): """ :param win_tile: 34 tile format :param tiles: :param call_riichi: :return: """ win_tile *= 4 # we don't need to think, that our waiting is aka dora if win_tile in AKA_DORA_LIST: win_tile += 1 if not tiles: tiles = self.player.tiles tiles += [win_tile] config = HandConfig(is_riichi=call_riichi, player_wind=self.player.player_wind, round_wind=self.player.table.round_wind, has_aka_dora=self.player.table.has_aka_dora, has_open_tanyao=self.player.table.has_open_tanyao) result = self.finished_hand.estimate_hand_value( tiles, win_tile, self.player.melds, self.player.table.dora_indicators, config) return result def should_call_riichi(self): print(self.player.discards) # empty waiting can be found in some cases if not self.waiting: return False if self.in_defence: return False #If we tenpai fast enough if len(self.player.discards) <= 8: return True if len(self.player.discards) >= 14: return False # we have a good wait, let's riichi if len(self.waiting) > 1: return True waiting = self.waiting[0] tiles = self.player.closed_hand + [waiting * 4] closed_melds = [x for x in self.player.melds if not x.opened] for meld in closed_melds: tiles.extend(meld.tiles[:3]) tiles_34 = TilesConverter.to_34_array(tiles) results = self.hand_divider.divide_hand(tiles_34) result = results[0] count_of_pairs = len([x for x in result if is_pair(x)]) # with chitoitsu we can call a riichi with pair wait if count_of_pairs == 7: return True for hand_set in result: # better to not call a riichi for a pair wait # it can be easily improved if is_pair(hand_set) and waiting in hand_set: return False return True def should_call_kan(self, tile, open_kan): """ Method will decide should we call a kan, or upgrade pon to kan :param tile: 136 tile format :param open_kan: boolean :return: kan type """ # we don't need to add dora for other players if self.player.ai.in_defence: return None if open_kan: # we don't want to start open our hand from called kan if not self.player.is_open_hand: return None # there is no sense to call open kan when we are not in tempai if not self.player.in_tempai: return None # we have a bad wait, rinshan chance is low if len(self.waiting) < 2: return None tile_34 = tile // 4 tiles_34 = TilesConverter.to_34_array(self.player.tiles) closed_hand_34 = TilesConverter.to_34_array(self.player.closed_hand) pon_melds = [x for x in self.player.open_hand_34_tiles if is_pon(x)] # let's check can we upgrade opened pon to the kan if pon_melds: for meld in pon_melds: # tile is equal to our already opened pon, # so let's call chankan! if tile_34 in meld: return Meld.CHANKAN count_of_needed_tiles = 4 # for open kan 3 tiles is enough to call a kan if open_kan: count_of_needed_tiles = 3 # we have 3 tiles in our hand, # so we can try to call closed meld if closed_hand_34[tile_34] == count_of_needed_tiles: if not open_kan: # to correctly count shanten in the hand # we had do subtract drown tile tiles_34[tile_34] -= 1 melds = self.player.open_hand_34_tiles previous_shanten = self.shanten.calculate_shanten(tiles_34, melds) melds += [[tile_34, tile_34, tile_34]] new_shanten = self.shanten.calculate_shanten(tiles_34, melds) # called kan will not ruin our hand if new_shanten <= previous_shanten: return Meld.KAN return None def should_call_win(self, tile, enemy_seat): return True def enemy_called_riichi(self, enemy_seat): """ After enemy riichi we had to check will we fold or not it is affect open hand decisions :return: """ if self.defence.should_go_to_defence_mode(): self.in_defence = True @property def enemy_players(self): """ Return list of players except our bot """ return self.player.table.players[1:]
class ImplementationAI(InterfaceAI): version = '0.3.2' agari = None shanten = None defence = None hand_divider = None finished_hand = None last_discard_option = None previous_shanten = 7 in_defence = False waiting = None current_strategy = None def __init__(self, player): super(ImplementationAI, self).__init__(player) self.agari = Agari() self.shanten = Shanten() self.defence = DefenceHandler(player) self.hand_divider = HandDivider() self.finished_hand = HandCalculator() self.previous_shanten = 7 self.current_strategy = None self.waiting = [] self.in_defence = False self.last_discard_option = None # Added for cowboy self.wanted_tiles_count = 0 self.pushing = False def init_hand(self): """ Let's decide what we will do with our hand (like open for tanyao and etc.) """ self.determine_strategy() def erase_state(self): self.current_strategy = None self.in_defence = False self.last_discard_option = None # Added for cowboy self.previous_shanten = 7 self.pushing = False def draw_tile(self, tile): """ :param tile: 136 tile format :return: """ self.determine_strategy() def discard_tile(self, discard_tile): # we called meld and we had discard tile that we wanted to discard if discard_tile is not None: if not self.last_discard_option: return discard_tile return self.process_discard_option(self.last_discard_option, self.player.closed_hand, True) results, shanten = self.calculate_outs(self.player.tiles, self.player.closed_hand, self.player.open_hand_34_tiles) if shanten < self.previous_shanten: logger.info("Shanten: {}".format(shanten)) self.previous_shanten = shanten selected_tile = self.process_discard_options_and_select_tile_to_discard(results, shanten) # bot think that there is a threat on the table # and better to fold # if we can't find safe tiles, let's continue to build our hand if self.defence.should_go_to_defence_mode(selected_tile): if not self.in_defence: logger.info('We decided to fold against other players') self.in_defence = True self.player.set_state("DEFENCE") else: #logger.info("Player is alreay in defence") pass defence_results, shanten = self.calculate_outs(self.player.tiles, self.player.closed_hand, self.player.open_hand_34_tiles) defence_tile = self.defence.try_to_find_safe_tile_to_discard(defence_results) if defence_tile: return self.process_discard_option(defence_tile, self.player.closed_hand) else: self.in_defence = False # Process the discard option before changing the state card2discard = self.process_discard_option(selected_tile, self.player.closed_hand) # After adjusting the defence, time to update the state if shanten == 0 and self.player.play_state == "PREPARING" and results: # and results for debugging if self.wanted_tiles_count > 4: self.player.set_state("PROACTIVE_GOODSHAPE") else: self.player.set_state("PROACTIVE_BADSHAPE") return card2discard def process_discard_options_and_select_tile_to_discard(self, results, shanten, had_was_open=False): tiles_34 = TilesConverter.to_34_array(self.player.tiles) # we had to update tiles value there # because it is related with shanten number for result in results: result.tiles_count = self.count_tiles(result.waiting, tiles_34) result.calculate_value(shanten) # current strategy can affect on our discard options # so, don't use strategy specific choices for calling riichi if self.current_strategy: results = self.current_strategy.determine_what_to_discard(self.player.closed_hand, results, shanten, False, None, had_was_open) return self.chose_tile_to_discard(results) def calculate_outs(self, tiles, closed_hand, open_sets_34=None): """ :param tiles: array of tiles in 136 format :param closed_hand: array of tiles in 136 format :param open_sets_34: array of array with tiles in 34 format :return: """ tiles_34 = TilesConverter.to_34_array(tiles) closed_tiles_34 = TilesConverter.to_34_array(closed_hand) is_agari = self.agari.is_agari(tiles_34, self.player.open_hand_34_tiles) results = [] for hand_tile in range(0, 34): if not closed_tiles_34[hand_tile]: continue tiles_34[hand_tile] -= 1 shanten = self.shanten.calculate_shanten(tiles_34, open_sets_34) waiting = [] for j in range(0, 34): if hand_tile == j or tiles_34[j] == 4: continue tiles_34[j] += 1 if self.shanten.calculate_shanten(tiles_34, open_sets_34) == shanten - 1: waiting.append(j) tiles_34[j] -= 1 tiles_34[hand_tile] += 1 if waiting: results.append(DiscardOption(player=self.player, shanten=shanten, tile_to_discard=hand_tile, waiting=waiting, tiles_count=self.count_tiles(waiting, tiles_34))) if is_agari: shanten = Shanten.AGARI_STATE else: shanten = self.shanten.calculate_shanten(tiles_34, open_sets_34) return results, shanten def count_tiles(self, waiting, tiles_34): n = 0 for item in waiting: n += 4 - self.player.total_tiles(item, tiles_34) return n def try_to_call_meld(self, tile, is_kamicha_discard): if not self.current_strategy: return None, None meld, discard_option = self.current_strategy.try_to_call_meld(tile, is_kamicha_discard) tile_to_discard = None if discard_option: self.last_discard_option = discard_option tile_to_discard = discard_option.tile_to_discard return meld, tile_to_discard def determine_strategy(self): # for already opened hand we don't need to give up on selected strategy if self.player.is_open_hand and self.current_strategy: return False old_strategy = self.current_strategy self.current_strategy = None # order is important strategies = [ YakuhaiStrategy(BaseStrategy.YAKUHAI, self.player), HonitsuStrategy(BaseStrategy.HONITSU, self.player), ] if self.player.table.has_open_tanyao: strategies.append(TanyaoStrategy(BaseStrategy.TANYAO, self.player)) for strategy in strategies: if strategy.should_activate_strategy(): self.current_strategy = strategy if self.current_strategy: if not old_strategy or self.current_strategy.type != old_strategy.type: message = '{} switched to {} strategy'.format(self.player.name, self.current_strategy) if old_strategy: message += ' from {}'.format(old_strategy) logger.info(message) logger.info('With such a hand: {}'.format(TilesConverter.to_one_line_string(self.player.tiles))) if not self.current_strategy and old_strategy: logger.debug('{} gave up on {}'.format(self.player.name, old_strategy)) return self.current_strategy and True or False def chose_tile_to_discard(self, results: [DiscardOption]) -> DiscardOption: """ Try to find best tile to discard, based on different valuations """ def sorting(x): # - is important for x.tiles_count # in that case we will discard tile that will give for us more tiles # to complete a hand return x.shanten, -x.tiles_count, x.valuation # util for drawing def get_order(t): # if it is honor if t // 9 >= 3: return 0 else: return min((t % 9), (8 - (t % 9))) + 1 def display_waiting(w): return TilesConverter.to_one_line_string([t * 4 for t in w]) had_to_be_discarded_tiles = [x for x in results if x.had_to_be_discarded] if had_to_be_discarded_tiles: had_to_be_discarded_tiles = sorted(had_to_be_discarded_tiles, key=sorting) selected_tile = had_to_be_discarded_tiles[0] else: results = sorted(results, key=sorting) #print("Len: ", len(results)) # init the temp_tile temp_tile = results[0] # remove needed tiles from discard options results = [x for x in results if not x.had_to_be_saved] # let's chose most valuable tile first if results: temp_tile = results[0] else: return temp_tile # and let's find all tiles with same shanten results_with_same_shanten = [x for x in results if x.shanten == temp_tile.shanten] # if there are 7 pairs tiles_34 = TilesConverter.to_34_array(self.player.tiles) paired_tiles = [x for x in range(0, 34) if tiles_34[x] == 2] num_pairs = len(paired_tiles) if num_pairs == 4 and temp_tile.shanten > 1 and not self.player.in_seven_pairs and not self.player.params.get("fool_in_pairs"): logger.info("There are 4 pairs!") if len(self.player.discards) > 6: logger.info("However it's too late for seven pairs.") for r in results: if r.tile_to_discard in paired_tiles: logger.info("With hand: {}".format(TilesConverter.to_one_line_string(self.player.tiles))) logger.info("Discard {}".format(display_waiting([r.tile_to_discard]))) return r else: logger.info("It's early, okay to go with seven pairs.") self.player.in_seven_pairs = True # TODO: a smart seven pairs strategy should be carried if self.player.in_seven_pairs and not self.player.params.get("fool_in_pairs"): single_tiles = [x for x in range(0,34) if tiles_34[x] in [1,3,4]] single_tiles.sort(key=lambda x: (self.count_tiles([x], tiles_34) >= 2, -get_order(x))) for s in single_tiles: # actually only #1 would be used most of the time for r in results: if r.tile_to_discard == s: logger.info("SevenPairsStrategy:") logger.info("Hand: {}".format(TilesConverter.to_one_line_string(self.player.tiles))) logger.info("Discard: {}".format(display_waiting([s]))) return r # if in drawing if temp_tile.shanten == 0: print("It's a drawing hand!") print("Hand: {}".format(TilesConverter.to_one_line_string(self.player.tiles))) # assume that temp tile got the biggest waiting if temp_tile.tiles_count > 4: print("It's a good shape, go for it.") else: logger.info("It's a bad shape drawing hand, need some calculation.") logger.info("Possible choices: {}".format(TilesConverter.to_one_line_string([x.tile_to_discard*4 for x in results_with_same_shanten]))) possible_choices = [(temp_tile, 99)] for r in results_with_same_shanten: print("\nCut:", display_waiting([r.tile_to_discard])) print("Waiting:", display_waiting(r.waiting)) print("Order:", [get_order(t) for t in r.waiting]) print("Outs:", r.tiles_count) if r.tiles_count == 0: print("It's an impossible drawing.") continue if len(r.waiting) == 1: print("It's an 1 out drawing.") possible_choices.append((r, get_order(r.waiting[0]))) else: print("It's a multiple out drawing.") r.waiting.sort(key=get_order) possible_choices.append((r, get_order(r.waiting[0]))) possible_choices.sort(key=lambda x: (x[1], -x[0].tiles_count)) final_choice = possible_choices[0][0] logger.info("Choice: {} {} {}".format(display_waiting([final_choice.tile_to_discard]), "with waiting", display_waiting(final_choice.waiting))) return final_choice # if not in drawing or in drawing with good shape possible_options = [temp_tile] for discard_option in results_with_same_shanten: # there is no sense to check already chosen tile if discard_option.tile_to_discard == temp_tile.tile_to_discard: continue # we don't need to select tiles almost dead waits if discard_option.tiles_count <= 2: continue # let's check all other tiles with same shanten # maybe we can find tiles that have almost same tiles count number # Cowboy: +-2 is a big difference, but +-1 is not diff = 1 if self.player.params.get("big_diff"): diff = 2 if temp_tile.tiles_count - diff < discard_option.tiles_count < temp_tile.tiles_count + diff: possible_options.append(discard_option) # let's sort got tiles by value and let's chose less valuable tile to discard possible_options = sorted(possible_options, key=lambda x: x.valuation) selected_tile = possible_options[0] if selected_tile.shanten == 0: print("\nChoice:", display_waiting([selected_tile.tile_to_discard]), "with waiting", display_waiting(selected_tile.waiting)) return selected_tile def process_discard_option(self, discard_option, closed_hand, force_discard=False): self.waiting = discard_option.waiting self.wanted_tiles_count = discard_option.tiles_count self.player.ai.previous_shanten = discard_option.shanten self.player.in_tempai = self.player.ai.previous_shanten == 0 # when we called meld we don't need "smart" discard if force_discard: return discard_option.find_tile_in_hand(closed_hand) last_draw_34 = self.player.last_draw and self.player.last_draw // 4 or None if self.player.last_draw not in AKA_DORA_LIST and last_draw_34 == discard_option.tile_to_discard: return self.player.last_draw else: return discard_option.find_tile_in_hand(closed_hand) def estimate_hand_value(self, win_tile, tiles=None, call_riichi=False): """ :param win_tile: 34 tile format :param tiles: :param call_riichi: :return: """ win_tile *= 4 # we don't need to think, that our waiting is aka dora if win_tile in AKA_DORA_LIST: win_tile += 1 if not tiles: tiles = self.player.tiles tiles += [win_tile] config = HandConfig( is_riichi=call_riichi, player_wind=self.player.player_wind, round_wind=self.player.table.round_wind, has_aka_dora=self.player.table.has_aka_dora, has_open_tanyao=self.player.table.has_open_tanyao ) result = self.finished_hand.estimate_hand_value(tiles, win_tile, self.player.melds, self.player.table.dora_indicators, config) return result def should_call_riichi(self): logger.info("Can call a reach!") # empty waiting can be found in some cases if not self.waiting: logger.info("However it is impossible to win.") return False # In pushing state, it's better to call it if self.pushing: logger.info("Go for it! The player is in pushing state.") return True # Get the rank EV after round 3 if self.table.round_number >= 5: # DEBUG: set this to 0 try: possible_hand_values = [self.estimate_hand_value(tile, call_riichi=True).cost["main"] for tile in self.waiting] except Exception as e: print(e) possible_hand_values = [2000] hand_value = sum(possible_hand_values) / len(possible_hand_values) hand_value += self.table.count_of_riichi_sticks * 1000 if self.player.is_dealer: hand_value += 700 # EV for dealer combo lose_estimation = 6000 if self.player.is_dealer else 7000 hand_shape = "pro_bad_shape" if self.wanted_tiles_count <= 4 else "pro_good_shape" rank_ev = self.defence.get_rank_ev(hand_value, lose_estimation, COUNTER_RATIO[hand_shape][len(self.player.discards)]) logger.info('''Cowboy: Proactive reach: Hand value: {} Hand shape: {} Is dealer: {} Current ranking: {} '''.format(hand_value, hand_shape, self.player.is_dealer, self.table.get_players_sorted_by_scores())) logger.info("Rank EV for proactive reach: {}".format(rank_ev)) if rank_ev < 0: logger.info("It's better to fold.") return False else: logger.info("Go for it!") return True should_attack = not self.defence.should_go_to_defence_mode() # For bad shape, at least 1 dora is required # Get count of dora dora_count = sum([plus_dora(x, self.player.table.dora_indicators) for x in self.player.tiles]) # aka dora dora_count += sum([1 for x in self.player.tiles if is_aka_dora(x, self.player.table.has_open_tanyao)]) if self.wanted_tiles_count <= 4 and dora_count == 0 and not self.player.is_dealer: should_attack = False logger.info("A bad shape with no dora, don't call it.") # # If player is on the top, no need to call reach # if self.player == self.player.table.get_players_sorted_by_scores()[0] and self.player.scores > 30000: # should_attack = False # logger.info("Player is in 1st position, no need to call reach.") if should_attack: # If we are proactive, let's set the state! logger.info("Go for it!") if self.player.play_state == "PREPARING": # If not changed in defense actions if self.wanted_tiles_count > 4: self.player.set_state("PROACTIVE_GOODSHAPE") else: self.player.set_state("PROACTIVE_BADSHAPE") return True else: logger.info("However it's better to fold.") return False # These codes are unreachable, it is fine. waiting = self.waiting[0] tiles = self.player.closed_hand + [waiting * 4] closed_melds = [x for x in self.player.melds if not x.opened] for meld in closed_melds: tiles.extend(meld.tiles[:3]) tiles_34 = TilesConverter.to_34_array(tiles) results = self.hand_divider.divide_hand(tiles_34) result = results[0] count_of_pairs = len([x for x in result if is_pair(x)]) # with chitoitsu we can call a riichi with pair wait if count_of_pairs == 7: return True for hand_set in result: # better to not call a riichi for a pair wait # it can be easily improved if is_pair(hand_set) and waiting in hand_set: return False return True def should_call_kan(self, tile, open_kan): """ Method will decide should we call a kan, or upgrade pon to kan :param tile: 136 tile format :param open_kan: boolean :return: kan type """ # we don't need to add dora for other players if self.player.ai.in_defence: return None if open_kan: # we don't want to start open our hand from called kan if not self.player.is_open_hand: return None # there is no sense to call open kan when we are not in tempai if not self.player.in_tempai: return None # we have a bad wait, rinshan chance is low if len(self.waiting) < 2: return None tile_34 = tile // 4 tiles_34 = TilesConverter.to_34_array(self.player.tiles) closed_hand_34 = TilesConverter.to_34_array(self.player.closed_hand) pon_melds = [x for x in self.player.open_hand_34_tiles if is_pon(x)] # let's check can we upgrade opened pon to the kan if pon_melds: for meld in pon_melds: # tile is equal to our already opened pon, # so let's call chankan! if tile_34 in meld: return Meld.CHANKAN count_of_needed_tiles = 4 # for open kan 3 tiles is enough to call a kan if open_kan: count_of_needed_tiles = 3 # we have 3 tiles in our hand, # so we can try to call closed meld if closed_hand_34[tile_34] == count_of_needed_tiles: if not open_kan: # to correctly count shanten in the hand # we had do subtract drown tile tiles_34[tile_34] -= 1 melds = self.player.open_hand_34_tiles previous_shanten = self.shanten.calculate_shanten(tiles_34, melds) melds += [[tile_34, tile_34, tile_34]] new_shanten = self.shanten.calculate_shanten(tiles_34, melds) # called kan will not ruin our hand if new_shanten <= previous_shanten: return Meld.KAN return None def should_call_win(self, tile, enemy_seat): return True def enemy_called_riichi(self, enemy_seat): """ After enemy riichi we had to check will we fold or not it is affect open hand decisions :return: """ #if self.defence.should_go_to_defence_mode(): # self.in_defence = True # No need to check it here pass @property def enemy_players(self): """ Return list of players except our bot """ return self.player.table.players[1:]