def __init__(self): self.shanten = Shanten() self.agari = Agari() self.finished_hand = HandCalculator() self.data_to_save = [] self.csv_exporter = CSVExporter()
def test_is_agari_and_open_hand(self): agari = Agari() tiles = self._string_to_34_array(sou="23455567", pin="222", man="345") melds = [ self._string_to_open_34_set(man="345"), self._string_to_open_34_set(sou="555"), ] self.assertFalse(agari.is_agari(tiles, melds))
def test_is_agari_and_open_hand(self): agari = Agari() tiles = self._string_to_34_array(sou='23455567', pin='222', man='345') melds = [ self._string_to_open_34_set(man='345'), self._string_to_open_34_set(sou='555'), ] self.assertFalse(agari.is_agari(tiles, melds))
def __init__(self, clients, replays_directory, replay_name): self.tiles = [] self.dead_wall = [] self.dora_indicators = [] self.discards = [] self.clients = clients self.agari = Agari() self.finished_hand = HandCalculator() self.replays_directory = replays_directory self.replay_name = replay_name
def test_is_not_agari(self): agari = Agari() tiles = self._string_to_34_array(sou='123456789', pin='12345') self.assertFalse(agari.is_agari(tiles)) tiles = self._string_to_34_array(sou='111222444', pin='11145') self.assertFalse(agari.is_agari(tiles)) tiles = self._string_to_34_array(sou='11122233356888') self.assertFalse(agari.is_agari(tiles))
def test_is_not_agari(self): agari = Agari() tiles = self._string_to_34_array(sou="123456789", pin="12345") self.assertFalse(agari.is_agari(tiles)) tiles = self._string_to_34_array(sou="111222444", pin="11145") self.assertFalse(agari.is_agari(tiles)) tiles = self._string_to_34_array(sou="11122233356888") self.assertFalse(agari.is_agari(tiles))
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 test_is_chitoitsu_agari(self): agari = Agari() tiles = self._string_to_34_array(sou='1133557799', pin='1199') self.assertTrue(agari.is_agari(tiles)) tiles = self._string_to_34_array(sou='2244', pin='1199', man='11', honors='2277') self.assertTrue(agari.is_agari(tiles)) tiles = self._string_to_34_array(man='11223344556677') self.assertTrue(agari.is_agari(tiles))
def test_is_chitoitsu_agari(self): agari = Agari() tiles = self._string_to_34_array(sou="1133557799", pin="1199") self.assertTrue(agari.is_agari(tiles)) tiles = self._string_to_34_array(sou="2244", pin="1199", man="11", honors="2277") self.assertTrue(agari.is_agari(tiles)) tiles = self._string_to_34_array(man="11223344556677") self.assertTrue(agari.is_agari(tiles))
def test_is_kokushi_musou_agari(self): agari = Agari() tiles = self._string_to_34_array(sou='19', pin='19', man='199', honors='1234567') self.assertTrue(agari.is_agari(tiles)) tiles = self._string_to_34_array(sou='19', pin='19', man='19', honors='11234567') self.assertTrue(agari.is_agari(tiles)) tiles = self._string_to_34_array(sou='19', pin='19', man='19', honors='12345677') self.assertTrue(agari.is_agari(tiles)) tiles = self._string_to_34_array(sou='129', pin='19', man='19', honors='1234567') self.assertFalse(agari.is_agari(tiles)) tiles = self._string_to_34_array(sou='19', pin='19', man='19', honors='11134567') self.assertFalse(agari.is_agari(tiles))
def test_is_kokushi_musou_agari(self): agari = Agari() tiles = self._string_to_34_array(sou="19", pin="19", man="199", honors="1234567") self.assertTrue(agari.is_agari(tiles)) tiles = self._string_to_34_array(sou="19", pin="19", man="19", honors="11234567") self.assertTrue(agari.is_agari(tiles)) tiles = self._string_to_34_array(sou="19", pin="19", man="19", honors="12345677") self.assertTrue(agari.is_agari(tiles)) tiles = self._string_to_34_array(sou="129", pin="19", man="19", honors="1234567") self.assertFalse(agari.is_agari(tiles)) tiles = self._string_to_34_array(sou="19", pin="19", man="19", honors="11134567") self.assertFalse(agari.is_agari(tiles))
def __init__(self, player): super(ImplementationAI, self).__init__(player) self.agari = Agari() self.shanten_calculator = Shanten() self.defence = DefenceHandler(player) self.riichi = Riichi(player) self.hand_divider = HandDivider() self.finished_hand = HandCalculator() self.hand_builder = HandBuilder(player, self) self.erase_state()
def __init__(self, player): self.player = player self.table = player.table self.kan = Kan(player) self.agari = Agari() self.shanten_calculator = Shanten() self.defence = TileDangerHandler(player) self.riichi = Riichi(player) self.hand_divider = HandDivider() self.finished_hand = HandCalculator() self.hand_builder = HandBuilder(player, self) self.placement = player.config.PLACEMENT_HANDLER_CLASS(player) self.suji = Suji(player) self.kabe = Kabe(player) self.erase_state()
def test_is_agari(self): agari = Agari() tiles = self._string_to_34_array(sou="123456789", pin="123", man="33") self.assertTrue(agari.is_agari(tiles)) tiles = self._string_to_34_array(sou="123456789", pin="11123") self.assertTrue(agari.is_agari(tiles)) tiles = self._string_to_34_array(sou="123456789", honors="11777") self.assertTrue(agari.is_agari(tiles)) tiles = self._string_to_34_array(sou="12345556778899") self.assertTrue(agari.is_agari(tiles)) tiles = self._string_to_34_array(sou="11123456788999") self.assertTrue(agari.is_agari(tiles)) tiles = self._string_to_34_array(sou="233334", pin="789", man="345", honors="55") self.assertTrue(agari.is_agari(tiles))
def test_is_agari(self): agari = Agari() tiles = self._string_to_34_array(sou='123456789', pin='123', man='33') self.assertTrue(agari.is_agari(tiles)) tiles = self._string_to_34_array(sou='123456789', pin='11123') self.assertTrue(agari.is_agari(tiles)) tiles = self._string_to_34_array(sou='123456789', honors='11777') self.assertTrue(agari.is_agari(tiles)) tiles = self._string_to_34_array(sou='12345556778899') self.assertTrue(agari.is_agari(tiles)) tiles = self._string_to_34_array(sou='11123456788999') self.assertTrue(agari.is_agari(tiles)) tiles = self._string_to_34_array(sou='233334', pin='789', man='345', honors='55') self.assertTrue(agari.is_agari(tiles))
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)
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)
import time from game import Hand from game import Wall from mahjong.hand_calculating.hand import HandCalculator, HandConfig from mahjong.shanten import Shanten from mahjong.tile import TilesConverter from mahjong.agari import Agari from mahjong.meld import Meld import numpy as np c = HandCalculator() st = Shanten() ag = Agari() ops = 1e7 # Control Code k = 0 cstart = time.time_ns() while k < ops: k += 1 w = Wall() hand = Hand(w) hand.draw(w) cend = time.time_ns() # Test Code k = 0 start = time.time_ns() while k < ops:
#!/usr/bin/env python # -*- coding: utf-8 -*- from mahjong.shanten import Shanten from mahjong.agari import Agari from mahjong.hand_calculating.hand import HandCalculator from mahjong.hand_calculating.hand_config import HandConfig from mahjong.tile import TilesConverter SHANTEN = Shanten() CALCULATOR = HandCalculator() AGARI = Agari() TO_GRAPH_LIST = [ "🀇", "🀈", "🀉", "🀊", "🀋", "🀌", "🀍", "🀎", "🀏", "🀙", "🀚", "🀛", "🀜", "🀝", "🀞", "🀟", "🀠", "🀡", "🀐", "🀑", "🀒", "🀓", "🀔", "🀕", "🀖", "🀗", "🀘", "🀀", "🀁", "🀂", "🀃", "🀆", "🀅", "🀄" ] NUM_HAIS = 34 def get_total_score(hand34, wintile34): result = CALCULATOR.estimate_hand_value(TilesConverter.to_136_array(hand34), wintile34 * 4, config=HandConfig(is_tsumo=True)) return result.cost['main'] + 2 * result.cost['additional'] def is_agari(hand34): return AGARI.is_agari(hand34) def tiles34_to_list(tiles): result = [] for i in xrange(34):
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:]
class GameManager: """ Allow to play bots between each other To have a metrics how new version plays against old versions """ replay_name = "" tiles = None dead_wall = None clients = None dora_indicators = None players_with_open_hands = None discards = None replay = None dealer = None current_client_seat = None round_number = 0 honba_sticks = 0 riichi_sticks = 0 _unique_dealers = 0 _need_to_check_same_winds = None def __init__(self, clients, replays_directory, replay_name): self.tiles = [] self.dead_wall = [] self.dora_indicators = [] self.discards = [] self.clients = clients self.agari = Agari() self.finished_hand = HandCalculator() self.replays_directory = replays_directory self.replay_name = replay_name @staticmethod def generate_replay_name(): return f"{datetime.datetime.now().strftime('%Y-%m-%d_%H_%M_%S')}_{randint(0, 99999):03}.txt" def init_game(self): """ Beginning of the game. Clients random placement and dealer selection. """ logger.info("Replay name: {}".format(self.replay_name)) self.replay = TenhouReplay(self.replay_name, self.clients, self.replays_directory) seed(shuffle_seed()) self.clients = self._randomly_shuffle_array(self.clients) for i in range(0, len(self.clients)): self.clients[i].seat = i # oya should be always first player # to have compatibility with tenhou format self.set_dealer(0) for client in self.clients: client.player.scores = 25000 self._unique_dealers = 0 self.round_number = 0 def play_game(self): logger.info("The start of the game") is_game_end = False self.init_game() self.replay.init_game(shuffle_seed()) while not is_game_end: self.init_round() results = self.play_round() dealer_won = False was_retake = False for result in results: # we want to increase honba in that case and don't move dealer seat if result["is_abortive_retake"]: dealer_won = True if not result["winner"]: was_retake = True continue if result["winner"].player.is_dealer: dealer_won = True old_dealer = self.dealer # if dealer won we need to increment honba sticks if dealer_won: self.honba_sticks += 1 # otherwise let's move dealer seat else: # retake and dealer is noten if was_retake: self.honba_sticks += 1 else: self.honba_sticks = 0 new_dealer = self._move_position(self.dealer) self.set_dealer(new_dealer) is_game_end = self._check_the_end_of_game(old_dealer) # important increment, we are building wall seed based on the round number self.round_number += 1 winner = self.recalculate_players_position() # winner takes riichi sticks winner.player.scores += self.riichi_sticks * 1000 self.replay.end_game() logger.info("Final Scores: {0}".format( self.players_sorted_by_scores())) total_scores = sum([x.player.scores for x in self.clients]) assert total_scores == 100000, total_scores def init_round(self): """ Generate players hands, dead wall and dora indicators """ self._need_to_check_same_winds = True self.players_with_open_hands = [] self.dora_indicators = [] self.tiles = self._generate_wall() for client in self.clients: client.erase_state() self.dead_wall = self._cut_tiles(14) self.add_new_dora_indicator() for x in range(0, len(self.clients)): client = self.clients[x] # each client think that he is a player with position = 0 # so, we need to move dealer position for each client # and shift scores array client_dealer = self._enemy_position(self.dealer, x) player_scores = deque( [i.player.scores / 100 for i in self.clients]) player_scores.rotate(x * -1) player_scores = list(player_scores) client.table.init_round( self._unique_dealers, self.honba_sticks, self.riichi_sticks, self.dora_indicators[0], client_dealer, player_scores, ) # each player by rotation draw 4 tiles until they have 12 # after this each player draw one more tile # and this is will be their initial hand # we do it to make the tiles allocation in hands # more random for _ in range(0, 3): for client in self.clients: client.player.tiles += self._cut_tiles(4) for client in self.clients: client.player.tiles += self._cut_tiles(1) client.player.tiles = sorted(client.player.tiles) client.player.init_hand(client.player.tiles) logger.info("Seed: {}".format(shuffle_seed())) logger.info("Dealer: {}, {}".format( self.dealer, self.clients[self.dealer].player.name)) logger.info("Wind: {}. Riichi sticks: {}. Honba sticks: {}".format( self._unique_dealers, self.riichi_sticks, self.honba_sticks)) logger.info("Round number: {}".format(self.round_number)) logger.info("Players: {0}".format(self.players_sorted_by_scores())) self.replay.init_round(self.dealer, self._unique_dealers, self.honba_sticks, self.riichi_sticks, self.dora_indicators[0]) def play_round(self) -> []: continue_to_play = True number_of_kan_sets_per_player = {0: 0, 1: 0, 2: 0, 3: 0} while continue_to_play: current_client = self._get_current_client() in_tempai = current_client.player.in_tempai drawn_tile = self._cut_tiles(1)[0] drawn_tile_34 = drawn_tile // 4 current_client.table.count_of_remaining_tiles -= 1 self.replay.draw(current_client.seat, drawn_tile) current_client.player.draw_tile(drawn_tile) tiles = current_client.player.tiles if (self.player_can_call_kyuushu_kyuuhai(current_client.player) and current_client.player.should_call_kyuushu_kyuuhai()): return [self.abortive_retake(AbortiveDraw.NINE_DIFFERENT)] # win by tsumo after tile draw is_win = self.agari.is_agari(TilesConverter.to_34_array(tiles), current_client.player.meld_34_tiles) if is_win: tiles.remove(drawn_tile) can_win = True # with open hand it can be situation when we in the tempai # but our hand doesn't contain any yaku # in that case we can't call ron if not current_client.player.in_riichi: result = current_client.player.ai.estimate_hand_value_or_get_from_cache( drawn_tile_34, is_tsumo=True, is_rinshan=current_client.is_rinshan) can_win = result.error is None if can_win: result = self.process_the_end_of_the_round( tiles=tiles, win_tile=drawn_tile, winner=current_client, loser=None, is_tsumo=True) return [result] else: # we can't win # so let's add tile back to hand # and discard it later tiles.append(drawn_tile) # checks if we can call closed kan or shouminkan current_client_tiles_34 = TilesConverter.to_34_array( current_client.player.tiles) if current_client_tiles_34[drawn_tile_34] == 4 and len( self.tiles) > 1: kan_type = current_client.player.should_call_kan( drawn_tile, open_kan=False, from_riichi=current_client.player.in_riichi) if kan_type: tiles = [ (drawn_tile_34 * 4), (drawn_tile_34 * 4) + 1, (drawn_tile_34 * 4) + 2, (drawn_tile_34 * 4) + 3, ] opened = False if kan_type == MeldPrint.SHOUMINKAN: opened = True meld = MeldPrint( kan_type, tiles, opened=opened, called_tile=drawn_tile, who=current_client.seat, from_who=current_client.seat, ) logger.info("Called meld: {} by {}".format( meld, current_client.player.name)) self.replay.open_meld(meld) if opened: for client in self.clients: client.is_ippatsu = False result = self.check_clients_possible_ron( current_client, drawn_tile, is_tsumogiri=False, is_chankan=True) # the end of the round if result: return result number_of_kan_sets_per_player[current_client.seat] += 1 if (sum(number_of_kan_sets_per_player.values()) == 4 and len(number_of_kan_sets_per_player.values()) != 1): return [self.abortive_retake(AbortiveDraw.FOUR_KANS)] # we need to notify each client about called meld for _client in self.clients: _client.table.add_called_meld( self._enemy_position(current_client.seat, _client.seat), meld) self.add_new_dora_indicator() current_client.is_rinshan = True # after that we will return to the current client next draw continue # we had to clear state after tile draw current_client.is_ippatsu = False current_client.is_rinshan = False # if not in riichi, let's decide what tile to discard if not current_client.player.in_riichi: tile = current_client.player.discard_tile() in_tempai = current_client.player.in_tempai else: tile = current_client.player.discard_tile(drawn_tile, force_tsumogiri=True) who_called_riichi_seat = None if in_tempai and not current_client.player.is_open_hand and current_client.player.can_call_riichi( ): who_called_riichi_seat = current_client.seat for client in self.clients: client.table.add_called_riichi_step_one( self._enemy_position(who_called_riichi_seat, client.seat)) self.replay.riichi(current_client.seat, 1) self.replay.discard(current_client.seat, tile) is_tsumogiri = drawn_tile == tile result = self.check_clients_possible_ron(current_client, tile, is_tsumogiri) # the end of the round if result: # check_clients_possible_ron already returns array return result # if there is no challenger to ron, let's check can we call riichi with tile discard or not if who_called_riichi_seat is not None: self.call_riichi(current_client) for client in self.clients: client.table.add_called_riichi_step_two( self._enemy_position(who_called_riichi_seat, client.seat)) self.replay.riichi(current_client.seat, 2) count_of_riichi_players = 0 for client in self.clients: if client.player.in_riichi: count_of_riichi_players += 1 if count_of_riichi_players == 4: return [self.abortive_retake(AbortiveDraw.FOUR_RIICHI)] # abortive retake result = self._check_same_winds() if result: return [result] # let's check other players hand to possibility open sets possible_melds = [] tile_34 = tile // 4 for other_client in self.clients: # there is no need to check the current client # or check client in riichi if other_client == current_client or other_client.player.in_riichi: continue # was a tile discarded by the left player or not if other_client.seat == 0: is_kamicha_discard = current_client.seat == 3 else: is_kamicha_discard = other_client.seat - current_client.seat == 1 other_client.table.revealed_tiles[tile_34] -= 1 # opened kan other_client_closed_hand_34 = TilesConverter.to_34_array( other_client.player.closed_hand) if (other_client_closed_hand_34[tile_34] == 3 and len(self.tiles) > 1 and other_client.player.should_call_kan( tile, open_kan=True)): tiles = [ (tile_34 * 4), (tile_34 * 4) + 1, (tile_34 * 4) + 2, (tile_34 * 4) + 3, ] meld = MeldPrint( MeldPrint.KAN, tiles, opened=True, called_tile=tile, who=other_client.seat, from_who=current_client.seat, ) logger.info("Called meld: {} by {}".format( meld, other_client.player.name)) self.replay.open_meld(meld) # we changed current client seat self.players_with_open_hands.append(other_client.seat) number_of_kan_sets_per_player[other_client.seat] += 1 if (sum(number_of_kan_sets_per_player.values()) == 4 and len(number_of_kan_sets_per_player.values()) != 1): return [self.abortive_retake(AbortiveDraw.FOUR_KANS)] # we need to notify each client about called meld for _client in self.clients: _client.table.add_called_meld( self._enemy_position(other_client.seat, _client.seat), meld) self.add_new_dora_indicator() # move to draw tile action other_client.is_rinshan = True self.current_client_seat = self._move_position( other_client.seat, shift=-1) continue meld, discard_option = other_client.player.try_to_call_meld( tile, is_kamicha_discard) other_client.table.revealed_tiles[tile_34] += 1 if meld: meld.from_who = current_client.seat meld.who = other_client.seat meld.called_tile = tile possible_melds.append({ "meld": meld, "discard_option": discard_option }) if possible_melds: # pon is more important than chi possible_melds = sorted( possible_melds, key=lambda x: x["meld"].type == Meld.PON) meld = possible_melds[0]["meld"] discard_option = possible_melds[0]["discard_option"] # clear ippatsu after called meld for client_item in self.clients: client_item.is_ippatsu = False # we changed current client with called open set self.current_client_seat = meld.who current_client = self._get_current_client() self.players_with_open_hands.append(self.current_client_seat) logger.info("Called meld: {} by {}".format( meld, current_client.player.name)) # we need to notify each client about called meld for _client in self.clients: _client.table.add_called_meld( self._enemy_position(current_client.seat, _client.seat), meld) self.replay.open_meld(meld) current_client.player.tiles.append(tile) discarded_tile = current_client.player.discard_tile( discard_option) self.replay.discard(current_client.seat, discarded_tile) # the end of the round result = self.check_clients_possible_ron( current_client, discarded_tile, False) if result: # check_clients_possible_ron already returns array return result self.current_client_seat = self._move_position( self.current_client_seat) # retake if not len(self.tiles): continue_to_play = False result = self.process_the_end_of_the_round([], 0, None, None, False) return [result] def check_clients_possible_ron(self, current_client, tile, is_tsumogiri, is_chankan=False) -> []: """ After tile discard let's check all other players can they win or not at this tile """ possible_win_client = [] for other_client in self.clients: # there is no need to check the current client if other_client == current_client: continue # let's store other players discards if not is_chankan: other_client.table.add_discarded_tile( self._enemy_position(current_client.seat, other_client.seat), tile, is_tsumogiri) if self.can_call_ron( other_client, tile, self._enemy_position(current_client.seat, other_client.seat), is_chankan): possible_win_client.append(other_client) if len(possible_win_client) == 3: return [self.abortive_retake(AbortiveDraw.TRIPLE_RON)] # check multiple ron results = [] for client in possible_win_client: result = self.process_the_end_of_the_round( tiles=client.player.tiles, win_tile=tile, winner=client, loser=current_client, is_tsumo=False, is_chankan=is_chankan, ) results.append(result) return results def recalculate_players_position(self): """ For players with same count of scores we need to set position based on their initial seat on the table """ temp_clients = sorted(self.clients, key=lambda x: x.player.scores, reverse=True) for i in range(0, len(temp_clients)): temp_client = temp_clients[i] for client in self.clients: if client.id == temp_client.id: client.player.position = i + 1 # return winner of the game return sorted([x for x in self.clients], key=lambda x: x.player.position)[0] def can_call_ron(self, client, win_tile, shifted_enemy_seat, is_chankan): if not client.player.in_tempai: return False tiles = client.player.tiles is_agari = self.agari.is_agari( TilesConverter.to_34_array(tiles + [win_tile]), client.player.meld_34_tiles) if not is_agari: return False # check for furiten for item in client.player.discards: discarded_tile = item.value // 4 if discarded_tile in client.player.ai.waiting: return False # it can be situation when we are in agari state # but our hand doesn't contain any yaku # in that case we can't call ron if not client.player.in_riichi: result = client.player.ai.estimate_hand_value_or_get_from_cache( win_tile // 4, is_tsumo=False, is_chankan=is_chankan, ) if result.error: return False # bot decided to not call ron if not client.player.should_call_win(win_tile, False, shifted_enemy_seat, is_chankan): return False return True def call_riichi(self, client): client.player.in_riichi = True # -1000 we will deduct in the bot logic self.riichi_sticks += 1 if len(client.player.discards ) == 1 and not self.players_with_open_hands: client.is_daburi = True # we will set it to False after next draw # or called meld client.is_ippatsu = True def set_dealer(self, dealer): self.dealer = dealer self._unique_dealers += 1 for x in range(0, len(self.clients)): client = self.clients[x] # each client think that he is a player with position = 0 # so, we need to move dealer position for each client # and shift scores array client.player.dealer_seat = self._enemy_position(self.dealer, x) # first move should be dealer's move self.current_client_seat = dealer def process_the_end_of_the_round(self, tiles, win_tile, winner, loser, is_tsumo, is_chankan=False): """ Increment a round number and do a scores calculations """ if winner: return self.agari_result(winner, loser, is_tsumo, tiles, win_tile, is_chankan) else: return self.retake() def agari_result(self, winner, loser, is_tsumo, tiles, win_tile, is_chankan): logger.info( "{}: {} + {}".format( is_tsumo and "Tsumo" or "Ron", TilesConverter.to_one_line_string(tiles, print_aka_dora=True), TilesConverter.to_one_line_string([win_tile], print_aka_dora=True), ), ) ura_dora = [] number_of_dora_indicators = len(self.dora_indicators) # add one more dora for riichi win if winner.player.in_riichi: # 9 10 11 12 indices for x in range(number_of_dora_indicators): next_indicator_index = 9 + x ura_dora.append(self.dead_wall[next_indicator_index]) is_tenhou = False # tenhou.net doesn't have renhou is_renhou = False is_chiihou = False if not self.players_with_open_hands and len( winner.player.discards) == 0 and is_tsumo: if winner.player.is_dealer: is_tenhou = True else: is_chiihou = True is_haitei = False is_houtei = False if not self.tiles: if is_tsumo: is_haitei = True else: is_houtei = True config = HandConfig( is_riichi=winner.player.in_riichi, player_wind=winner.player.player_wind, round_wind=winner.player.table.round_wind_tile, is_tsumo=is_tsumo, is_tenhou=is_tenhou, is_renhou=is_renhou, is_chiihou=is_chiihou, is_daburu_riichi=winner.is_daburi, is_ippatsu=winner.is_ippatsu, is_haitei=is_haitei, is_houtei=is_houtei, is_rinshan=winner.is_rinshan, is_chankan=is_chankan, options=OptionalRules( has_aka_dora=settings.FIVE_REDS, has_open_tanyao=settings.OPEN_TANYAO, has_double_yakuman=False, ), ) hand_value = self.finished_hand.estimate_hand_value( tiles=tiles + [win_tile], win_tile=win_tile, melds=winner.player.melds, dora_indicators=self.dora_indicators + ura_dora, config=config, ) if hand_value.error: logger.error("Can't estimate a hand: {}. Error: {}".format( TilesConverter.to_one_line_string(tiles + [win_tile], print_aka_dora=True), hand_value.error)) raise ValueError("Not correct hand") logger.info("Dora indicators: {}".format( TilesConverter.to_one_line_string(self.dora_indicators, print_aka_dora=True))) logger.info("Hand yaku: {}".format(", ".join( str(x) for x in hand_value.yaku))) if loser is not None: loser_seat = loser.seat else: # tsumo loser_seat = winner.seat self.replay.win( winner.seat, loser_seat, win_tile, self.honba_sticks, self.riichi_sticks, hand_value.han, hand_value.fu, hand_value.cost, hand_value.yaku, self.dora_indicators, ura_dora, ) riichi_bonus = self.riichi_sticks * 1000 self.riichi_sticks = 0 honba_bonus = self.honba_sticks * 300 # win by ron if loser: scores_to_pay = hand_value.cost["main"] + honba_bonus win_amount = scores_to_pay + riichi_bonus winner.player.scores += win_amount loser.player.scores -= scores_to_pay logger.info("Win: {0} +{1:,d} +{2:,d}".format( winner.player.name, scores_to_pay, riichi_bonus)) logger.info("Lose: {0} -{1:,d}".format(loser.player.name, scores_to_pay)) # win by tsumo else: calculated_cost = hand_value.cost[ "main"] + hand_value.cost["additional"] * 2 win_amount = calculated_cost + riichi_bonus + honba_bonus winner.player.scores += win_amount logger.info("Win: {0} +{1:,d} +{2:,d}".format( winner.player.name, calculated_cost, riichi_bonus + honba_bonus)) for client in self.clients: if client != winner: if client.player.is_dealer: scores_to_pay = hand_value.cost["main"] else: scores_to_pay = hand_value.cost["additional"] scores_to_pay += honba_bonus / 3 client.player.scores -= scores_to_pay logger.info("Lose: {0} -{1:,d}".format( client.player.name, int(scores_to_pay))) return { "winner": winner, "loser": loser, "is_tsumo": is_tsumo, "is_abortive_retake": False } def retake(self): logger.info("Retake") tempai_users = [] for client in self.clients: if client.player.in_tempai: tempai_users.append(client.seat) tempai_users_count = len(tempai_users) if tempai_users_count == 0 or tempai_users_count == 4: self.honba_sticks += 1 else: # 1 tempai user will get 3000 # 2 tempai users will get 1500 each # 3 tempai users will get 1000 each scores_to_pay = 3000 / tempai_users_count for client in self.clients: if client.player.in_tempai: client.player.scores += scores_to_pay logger.info("{0} +{1:,d}".format(client.player.name, int(scores_to_pay))) # dealer was tempai, we need to add honba stick if client.player.is_dealer: self.honba_sticks += 1 else: client.player.scores -= 3000 / (4 - tempai_users_count) self.replay.retake(tempai_users, self.honba_sticks, self.riichi_sticks) return { "winner": None, "loser": None, "is_tsumo": False, "is_abortive_retake": False } def abortive_retake(self, reason): logger.info("Abortive retake. Reason: {}".format(reason)) self.replay.abortive_retake(reason, self.honba_sticks, self.riichi_sticks) return { "winner": None, "loser": None, "is_tsumo": False, "is_abortive_retake": True } def players_sorted_by_scores(self): return sorted([i.player for i in self.clients], key=lambda x: x.scores, reverse=True) def _check_same_winds(self): if not self._need_to_check_same_winds: return None # with called melds this abortive retake is not possible if self.players_with_open_hands: self._need_to_check_same_winds = False return None # it is possible only for the first 4 discards if len(self.discards) > 4: self._need_to_check_same_winds = False return None # it is too early if len(self.discards) != 4: return None tiles = [x // 4 for x in self.discards] unique_tiles = list(set(tiles)) # first 4 discards wasn't same tiles if len(unique_tiles) != 1: self._need_to_check_same_winds = False return None tile = unique_tiles[1] if tile in WINDS: return self.abortive_retake(AbortiveDraw.SAME_FIRST_WIND) else: self._need_to_check_same_winds = False return None def _check_the_end_of_game(self, dealer_seat): dealer_has_higher_scores = True has_player_with_30000_plus_scores = False dealer = [x for x in self.clients if x.seat == dealer_seat][0] for client in self.clients: # if someone has negative scores at the end of round, we need to end the game if client.player.scores < 0: logger.info("Game end: negative scores") return True if client.player.scores >= 30000: has_player_with_30000_plus_scores = True if client.seat == dealer.seat: continue if client.player.scores > dealer.player.scores: dealer_has_higher_scores = False # orasu ended if self._unique_dealers == 8 and dealer_has_higher_scores: logger.info( "Game end: dealer has higher scores at the end of south wind") return True # we have played all 8 winds (starting from wind 0) # and there is player with 30000+ scores if self._unique_dealers > 7 and has_player_with_30000_plus_scores: logger.info("Game end: 30000+ scores and the end of south wind") return True # west round was finished, we don't want to play north round if self._unique_dealers > 11: logger.info("Game end: the end of west wind") return True return False def _get_current_client(self) -> LocalClient: return self.clients[self.current_client_seat] def _cut_tiles(self, count_of_tiles) -> []: """ Cut the tiles array :param count_of_tiles: how much tiles to cut :return: the array with specified count of tiles """ result = self.tiles[0:count_of_tiles] self.tiles = self.tiles[count_of_tiles:len(self.tiles)] return result def _move_position(self, current_position, shift=1): """ Loop 0 -> 1 -> 2 -> 3 -> 0 """ current_position += shift if current_position > 3: current_position = 0 if current_position < 0: current_position = 3 return current_position def _enemy_position(self, who, from_who): positions = [0, 1, 2, 3] return positions[who - from_who] def _generate_wall(self): # round of played numbers here to be sure that each wall will be unique wall_seed = shuffle_seed() + self.round_number # init seed for random generator seed(wall_seed) wall = [i for i in range(0, 136)] # let's shuffle wall two times just in case wall = self._randomly_shuffle_array(wall) wall = self._randomly_shuffle_array(wall) return wall def _randomly_shuffle_array(self, array): rand_seeds = [randint(0, len(array) - 1) for _ in range(0, len(array))] # for better wall shuffling we had to do it manually # shuffle() didn't make wall to be really random for x in range(0, len(array)): src = x dst = rand_seeds[x] swap = array[x] array[src] = array[dst] array[dst] = swap return array def add_new_dora_indicator(self): number_of_dora_indicators = len(self.dora_indicators) # 2 3 4 5 indices next_indicator_index = 2 + number_of_dora_indicators self.dora_indicators.append(self.dead_wall[next_indicator_index]) if number_of_dora_indicators > 0: self.replay.add_new_dora(self.dora_indicators[-1]) for _client in self.clients: _client.table.add_dora_indicator(self.dora_indicators[-1]) def player_can_call_kyuushu_kyuuhai(self, player): if len(player.discards) > 0 or len(player.melds) > 0: return False tiles_34 = [x // 4 for x in player.tiles] terminals_and_honors = [ x for x in tiles_34 if is_honor(x) or is_terminal(x) ] return len(list(set(terminals_and_honors))) >= 9
class ImplementationAI(InterfaceAI): """ AI that will discard tiles to maximize expected shanten. Assumes that tiles are drawn randomly from those not on the table or in hand - aka not revealed to player. Does not account for hidden tiles in opponent's hands. Always calls wins, never calls riichi. TODO: Everything """ version = 'shantenMCS' shanten = None agari = None shdict = {} def __init__(self, player): super(ImplementationAI, self).__init__(player) self.shanten = Shanten() self.hand_divider = HandDivider() self.agari = Agari() self.iterations = 200 def simulate_single(self, hand, hand_open, unaccounted_tiles): """ #simulates a single random draw and calculates shanten hand, hand_open -- hand in 34 format unaccounted_tiles -- all the unused tiles in 34 format turn -- a number from 0-3 (0 is the player) """ hand = list(hand) unaccounted = list(unaccounted_tiles) #14 in dead wall 13*3= 39 in other hand -> total 53 unaccounted_nonzero = np.nonzero(unaccounted)[0] #get a random card draw_tile = random.choice(unaccounted_nonzero) unaccounted[draw_tile] -= 1 hand[draw_tile] += 1 return self.shanten.calculate_shanten(hand, hand_open) # TODO: Merge all discard functions into one to prevent code reuse and unnecessary duplication of variables def discard_tile(self, discard_tile): if discard_tile is not None: return discard_tile tiles_34 = TilesConverter.to_34_array(self.player.tiles) closed_tiles_34 = TilesConverter.to_34_array(self.player.closed_hand) results = [] for tile in range(0, 34): # Can the tile be discarded from the concealed hand? if not closed_tiles_34[tile]: continue # discard the tile from hand tiles_34[tile] -= 1 # calculate shanten and store shanten = self.shanten.calculate_shanten( tiles_34, self.player.open_hand_34_tiles) results.append((shanten, tile)) # return tile to hand tiles_34[tile] += 1 (minshanten, discard_34) = min(results) results2 = [] unaccounted = (np.array([4]*34) - closed_tiles_34)\ - TilesConverter.to_34_array(self.table.revealed_tiles) self.shdict = {} for shanten, tile in results: if shanten != minshanten: continue h = sum( self.simulate_single(closed_tiles_34, self.player. open_hand_34_tiles, unaccounted) for _ in range(self.iterations)) / self.iterations results2.append((h, tile)) (h, discard_34) = min(results2) discard_136 = TilesConverter.find_34_tile_in_136_array( discard_34, self.player.closed_hand) if discard_136 is None: logger.debug('Failure') discard_136 = random.randrange(len(self.player.tiles) - 1) discard_136 = self.player.tiles[discard_136] logger.info('Shanten after discard:' + str(shanten)) logger.info('Discard heuristic:' + str(h)) return discard_136 # UNUSED def calculate_outs(self, discard_34, shanten, depth=2): closed_tiles_34 = TilesConverter.to_34_array(self.player.closed_hand) table_34 = list(self.table.revealed_tiles) tiles_34 = TilesConverter.to_34_array(self.player.tiles) table_34[discard_34] += 1 closed_tiles_34[discard_34] -= 1 tiles_34[discard_34] -= 1 hidden_34 = np.array( [4] * 34) - np.array(closed_tiles_34) - np.array(table_34) # print(hidden_34) # want to sample? use this # reveal_num = sum(hidden_34) # draw_p = [float(i)/reveal_num for i in hidden_34] # draw = np.random.choice(34, p=draw_p) return self.out_search(tiles_34, closed_tiles_34, hidden_34, depth, shanten - 1) def calculate_outs_meld(self, discard_34, shanten, tiles_34, closed_tiles_34, open_hand_34, depth=2): table_34 = list(self.table.revealed_tiles) tiles_34 = copy.deepcopy(tiles_34) closed_tiles_34 = copy.deepcopy(closed_tiles_34) table_34[discard_34] += 1 closed_tiles_34[discard_34] -= 1 tiles_34[discard_34] -= 1 hidden_34 = np.array( [4] * 34) - np.array(closed_tiles_34) - np.array(table_34) return self.out_search(tiles_34, closed_tiles_34, hidden_34, depth, shanten - 1, open_hand_34) def out_search(self, tiles_34, closed_tiles_34, hidden_34, depth, shanten, open_hand_34=None): outs = 0 for i in range(34): if hidden_34[i] <= 0: continue ct = hidden_34[i] # draw tile from hidden to concealed hand hidden_34[i] -= 1 closed_tiles_34[i] += 1 tiles_34[i] += 1 if self.agari.is_agari(tiles_34, open_hand_34): outs += 2 else: for tile in range(0, 34): # Can the tile be discarded from the concealed hand? if not closed_tiles_34[tile]: continue # discard the tile from hand closed_tiles_34[tile] -= 1 tiles_34[tile] -= 1 tuple_34 = tuple(tiles_34) # calculate shanten and add outs if appropriate if tuple_34 in self.shdict.keys(): sh = self.shdict[tuple_34] else: if open_hand_34 is None: sh = self.shanten.calculate_shanten( tiles_34, self.player.open_hand_34_tiles) else: sh = self.shanten.calculate_shanten( tiles_34, open_hand_34) self.shdict[tuple_34] = sh if sh == shanten: if depth <= 1 or shanten == -1: outs += 1 else: outs += ct * self.out_search( tiles_34, closed_tiles_34, hidden_34, depth - 1, shanten - 1) if sh == shanten + 1: outs += 0.01 # return tile to hand closed_tiles_34[tile] += 1 tiles_34[tile] += 1 # return tile from closed hand to hidden hidden_34[i] += 1 closed_tiles_34[i] -= 1 tiles_34[i] -= 1 return outs def should_call_riichi(self): # if len(self.player.open_hand_34_tiles) != 0: # return False return True # tiles_34 = TilesConverter.to_34_array(self.player.tiles) # shanten = self.shanten.calculate_shanten(tiles_34, None) # logger.debug('Riichi check, shanten = ' + str(shanten)) # return shanten == 0 def should_call_win(self, tile, enemy_seat): return True def should_call_kan(self, tile, open_kan): """ When bot can call kan or chankan this method will be called :param tile: 136 tile format :param is_open_kan: boolean :return: kan type (Meld.KAN, Meld.CHANKAN) or None """ if open_kan: # don't start open hand from called kan if not self.player.is_open_hand: return None # don't call open kan if not waiting for win if not self.player.in_tempai: 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)] # upgrade open pon to kan if possible if pon_melds: for meld in pon_melds: 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 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) # check for improvement in shanten if new_shanten <= previous_shanten: return Meld.KAN return None def try_to_call_meld(self, tile, is_kamicha_discard): """ When bot can open hand with a set (chi or pon/kan) this method will be called :param tile: 136 format tile :param is_kamicha_discard: boolean :return: Meld and DiscardOption objects or None, None """ # can't call if in riichi if self.player.in_riichi: return None, None closed_hand = self.player.closed_hand[:] # check for appropriate hand size, seems to solve a bug if len(closed_hand) == 1: return None, None # get old shanten value old_tiles_34 = TilesConverter.to_34_array(self.player.tiles) old_shanten = self.shanten.calculate_shanten( old_tiles_34, self.player.open_hand_34_tiles) # setup discarded_tile = tile // 4 new_closed_hand_34 = TilesConverter.to_34_array(closed_hand + [tile]) # We will use hand_divider to find possible melds involving the discarded tile. # Check its suit and number to narrow the search conditions # skipping this will break the default mahjong functions combinations = [] first_index = 0 second_index = 0 if is_man(discarded_tile): first_index = 0 second_index = 8 elif is_pin(discarded_tile): first_index = 9 second_index = 17 elif is_sou(discarded_tile): first_index = 18 second_index = 26 if second_index == 0: # honor tiles if new_closed_hand_34[discarded_tile] == 3: combinations = [[[discarded_tile] * 3]] else: # to avoid not necessary calculations # we can check only tiles around +-2 discarded tile first_limit = discarded_tile - 2 if first_limit < first_index: first_limit = first_index second_limit = discarded_tile + 2 if second_limit > second_index: second_limit = second_index combinations = self.hand_divider.find_valid_combinations( new_closed_hand_34, first_limit, second_limit, True) # Reduce combinations to list of melds if combinations: combinations = combinations[0] # Verify that a meld can be called possible_melds = [] for meld_34 in combinations: # we can call pon from everyone if is_pon(meld_34) and discarded_tile in meld_34: if meld_34 not in possible_melds: possible_melds.append(meld_34) # we can call chi only from left player if is_chi(meld_34 ) and is_kamicha_discard and discarded_tile in meld_34: if meld_34 not in possible_melds: possible_melds.append(meld_34) # For each possible meld, check if calling it and discarding can improve shanten new_shanten = float('inf') discard_136 = None tiles = None for meld_34 in possible_melds: shanten, disc = self.meldDiscard(meld_34, tile) if shanten < new_shanten: new_shanten, discard_136 = shanten, disc tiles = meld_34 # If shanten can be improved by calling meld, call it if new_shanten < old_shanten: meld = Meld() meld.type = is_chi(tiles) and Meld.CHI or Meld.PON # convert meld tiles back to 136 format for Meld type return # find them in a copy of the closed hand and remove tiles.remove(discarded_tile) first_tile = TilesConverter.find_34_tile_in_136_array( tiles[0], closed_hand) closed_hand.remove(first_tile) second_tile = TilesConverter.find_34_tile_in_136_array( tiles[1], closed_hand) closed_hand.remove(second_tile) tiles_136 = [first_tile, second_tile, tile] discard_136 = TilesConverter.find_34_tile_in_136_array( discard_136 // 4, closed_hand) meld.tiles = sorted(tiles_136) return meld, discard_136 return None, None # TODO: Merge all discard functions into one to prevent code reuse and unnecessary duplication of variables def meldDiscard(self, meld_34, discardtile): tiles_34 = TilesConverter.to_34_array(self.player.tiles + [discardtile]) closed_tiles_34 = TilesConverter.to_34_array(self.player.closed_hand + [discardtile]) open_hand_34 = copy.deepcopy(self.player.open_hand_34_tiles) # remove meld from closed and and add to open hand open_hand_34.append(meld_34) for tile_34 in meld_34: closed_tiles_34[tile_34] -= 1 results = [] for tile in range(0, 34): # Can the tile be discarded from the concealed hand? if not closed_tiles_34[tile]: continue # discard the tile from hand tiles_34[tile] -= 1 # calculate shanten and store shanten = self.shanten.calculate_shanten(tiles_34, open_hand_34) results.append((shanten, tile)) # return tile to hand tiles_34[tile] += 1 (minshanten, discard_34) = min(results) results2 = [] unaccounted = (np.array([4]*34) - closed_tiles_34)\ - TilesConverter.to_34_array(self.table.revealed_tiles) self.shdict = {} for shanten, tile in results: if shanten != minshanten: continue h = sum( self.simulate_single(closed_tiles_34, open_hand_34, unaccounted) for _ in range(self.iterations)) / self.iterations results2.append((h, tile)) (h, discard_34) = min(results2) discard_136 = TilesConverter.find_34_tile_in_136_array( discard_34, self.player.closed_hand) return minshanten, discard_136
class LogParser: data_to_save = None def __init__(self): self.shanten = Shanten() self.agari = Agari() self.finished_hand = HandCalculator() self.data_to_save = [] self.csv_exporter = CSVExporter() def on_player_draw(self, player, table): pass def on_player_discard(self, player, table, discarded_tile): pass def on_player_tenpai(self, player, table): pass def get_game_rounds(self, log_content, log_id): """ XML parser was really slow here, so I built simple parser to separate log content on tags (grouped by rounds) """ tag_start = 0 rounds = [] tag = None current_tags = [] for x in range(0, len(log_content)): if log_content[x] == ">": tag = log_content[tag_start:x + 1] tag_start = x + 1 # not useful tags skip_tags = ["SHUFFLE", "TAIKYOKU", "mjloggm", "GO", "UN"] if tag and any([x in tag for x in skip_tags]): tag = None # new hand was started if self.is_init_tag(tag) and current_tags: rounds.append(current_tags) current_tags = [] # the end of the game if tag and "owari" in tag: rounds.append(current_tags) if tag: if self.is_init_tag(tag): # we dont need seed information # it appears in old logs format find = re.compile(r'shuffle="[^"]*"') tag = find.sub("", tag) current_tags.append('<LOG_ID id="{}" />'.format(log_id)) # add processed tag to the hand current_tags.append(tag) tag = None return rounds def parse_game_rounds(self, game): self.data_to_save = [] step = 0 for round_item in game: table = Table() log_id = None who_called_meld_on_this_step = None try: for tag in round_item: if self.is_log_id(tag): log_id = self.get_attribute_content(tag, "id") table.log_id = log_id if self.is_init_tag(tag): seed = [ int(x) for x in self.get_attribute_content( tag, "seed").split(",") ] current_hand = seed[0] dora_indicator = seed[5] dealer_seat = int( self.get_attribute_content(tag, "oya")) scores = [ int(x) for x in self.get_attribute_content( tag, "ten").split(",") ] table.init(dealer_seat, current_hand, dora_indicator, step, scores) table.get_player(0).init_hand( self.get_attribute_content(tag, "hai0")) table.get_player(1).init_hand( self.get_attribute_content(tag, "hai1")) table.get_player(2).init_hand( self.get_attribute_content(tag, "hai2")) table.get_player(3).init_hand( self.get_attribute_content(tag, "hai3")) step += 1 if self.is_draw(tag): tile = self.parse_tile(tag) player_seat = self.get_player_seat(tag) player = table.get_player(player_seat) player.draw_tile(tile) self.on_player_draw(player, table) if self.is_discard(tag): tile = self.parse_tile(tag) player_seat = self.get_player_seat(tag) player = table.get_player(player_seat) is_tsumogiri = tile == player.last_drawn_tile after_meld = player_seat == who_called_meld_on_this_step discard = Discard(tile, is_tsumogiri, after_meld, False) player.discard_tile(discard) tenpai_after_discard = False tiles_34 = TilesConverter.to_34_array(player.tiles) melds_34 = player.melds_34 if self.shanten.calculate_shanten(tiles_34, melds_34) == 0: tenpai_after_discard = True self.on_player_tenpai(player, table) else: player.in_tempai = False player.discards[ -1].tenpai_after_discard = tenpai_after_discard who_called_meld_on_this_step = None self.on_player_discard(player, table, tile) if self.is_meld_set(tag): meld = self.parse_meld(tag) player = table.get_player(meld.who) # when we called chankan we need to remove pon set from hand if meld.type == ParserMeld.CHANKAN: player.tiles.remove(meld.called_tile) pon_set = [ x for x in player.melds if x.tiles[0] == meld.tiles[0] ][0] player.melds.remove(pon_set) player.add_meld(meld) # if it was not kan/chankan let's add it to the hand if meld.type != ParserMeld.CHANKAN and meld.type != ParserMeld.KAN: player.tiles.append(meld.called_tile) # indication that tile was taken from discard if meld.opened: for meld_player in table.players: if meld_player.discards and meld_player.discards[ -1].tile == meld.called_tile: meld_player.discards[ -1].was_given_for_meld = True # for closed kan we had to remove tile from hand if (meld.type == ParserMeld.KAN and not meld.opened and meld.called_tile in player.tiles): player.tiles.remove(meld.called_tile) who_called_meld_on_this_step = meld.who if self.is_riichi(tag): riichi_step = int( self.get_attribute_content(tag, "step")) who = int(self.get_attribute_content(tag, "who")) player = table.get_player(who) if riichi_step == 1: player.in_riichi = True if riichi_step == 2: player.discards[-1].after_riichi = True if self.is_new_dora(tag): dora = int(self.get_attribute_content(tag, "hai")) table.add_dora(dora) except Exception as e: logger.error("Failed to process log: {}".format(log_id)) logger.error(e, exc_info=True) return self.data_to_save def get_player_waiting(self, player): tiles = player.closed_hand if len(tiles) == 1: return [tiles[0] // 4] tiles_34 = TilesConverter.to_34_array(tiles) waiting = [] for j in range(0, 34): # we already have 4 tiles in hand # and we can't wait on 5th if tiles_34[j] == 4: continue tiles_34[j] += 1 if self.agari.is_agari(tiles_34): waiting.append(j) tiles_34[j] -= 1 return waiting def calculate_waiting_costs(self, player, player_waiting): waiting = [] for tile in player_waiting: config = HandConfig( is_riichi=player.discards[-1].after_riichi, player_wind=player.player_wind, round_wind=player.table.round_wind, options=OptionalRules(has_aka_dora=True, has_open_tanyao=True), ) win_tile = tile * 4 # we don't need to think, that our waiting is aka dora if win_tile in AKA_DORA_LIST: win_tile += 1 tiles = player.tiles + [win_tile] result = self.finished_hand.estimate_hand_value( tiles, win_tile, player.melds, player.table.dora_indicators, config) if result.error: waiting.append({ "tile": win_tile, "han": None, "fu": None, "cost": 0, "yaku": [] }) else: waiting.append({ "tile": win_tile, "han": result.han, "fu": result.fu, "cost": result.cost["main"], "yaku": [{ "id": x.yaku_id, "name": x.name } for x in result.yaku], }) return waiting def get_attribute_content(self, tag, attribute_name): result = re.findall(r'{}="([^"]*)"'.format(attribute_name), tag) return result and result[0] or None def is_discard(self, tag): skip_tags = ["<GO", "<FURITEN", "<DORA"] if any([x in tag for x in skip_tags]): return False match_discard = re.match(r"^<[defgDEFG]+\d*", tag) if match_discard: return True return False def is_draw(self, tag): match_discard = re.match(r"^<[tuvwTUVW]+\d*", tag) if match_discard: return True return False def parse_tile(self, message): result = re.match(r"^<[defgtuvwDEFGTUVW]+\d*", message).group() return int(result[2:]) def get_player_seat(self, tag): player_sign = tag.lower()[1] if player_sign == "d" or player_sign == "t": player_seat = 0 elif player_sign == "e" or player_sign == "u": player_seat = 1 elif player_sign == "f" or player_sign == "v": player_seat = 2 else: player_seat = 3 return player_seat def parse_meld(self, tag): who = int(self.get_attribute_content(tag, "who")) data = int(self.get_attribute_content(tag, "m")) meld = ParserMeld() meld.who = who meld.from_who = ((data & 0x3) + meld.who) % 4 if data & 0x4: self.parse_chi(data, meld) elif data & 0x18: self.parse_pon(data, meld) elif data & 0x20: # nuki pass else: self.parse_kan(data, meld) return meld def parse_chi(self, data, meld): meld.type = ParserMeld.CHI t0, t1, t2 = (data >> 3) & 0x3, (data >> 5) & 0x3, (data >> 7) & 0x3 base_and_called = data >> 10 base = base_and_called // 3 called = base_and_called % 3 base = (base // 7) * 9 + base % 7 meld.tiles = [ t0 + 4 * (base + 0), t1 + 4 * (base + 1), t2 + 4 * (base + 2) ] meld.called_tile = meld.tiles[called] def parse_pon(self, data, meld): t4 = (data >> 5) & 0x3 t0, t1, t2 = ((1, 2, 3), (0, 2, 3), (0, 1, 3), (0, 1, 2))[t4] base_and_called = data >> 9 base = base_and_called // 3 called = base_and_called % 3 if data & 0x8: meld.type = ParserMeld.PON meld.tiles = [t0 + 4 * base, t1 + 4 * base, t2 + 4 * base] meld.called_tile = meld.tiles[called] else: meld.type = ParserMeld.CHANKAN meld.tiles = [ t0 + 4 * base, t1 + 4 * base, t2 + 4 * base, t4 + 4 * base ] meld.called_tile = meld.tiles[3] def parse_kan(self, data, meld): base_and_called = data >> 8 base = base_and_called // 4 meld.type = ParserMeld.KAN meld.tiles = [4 * base, 1 + 4 * base, 2 + 4 * base, 3 + 4 * base] called = base_and_called % 4 meld.called_tile = meld.tiles[called] # to mark closed\opened kans meld.opened = meld.who != meld.from_who def is_init_tag(self, tag): return tag and "INIT" in tag def is_redraw_tag(self, tag): return tag and "RYUUKYOKU" in tag def is_agari_tag(self, tag): return tag and "AGARI" in tag def is_log_id(self, tag): return tag and "LOG_ID" in tag def is_meld_set(self, tag): return tag and "<N who=" in tag def is_riichi(self, tag): return tag and "REACH " in tag def is_new_dora(self, tag): return tag and "<DORA" in tag
def __init__(self, player): super(ImplementationAI, self).__init__(player) self.shanten = Shanten() self.hand_divider = HandDivider() self.agari = Agari() self.iterations = 200
import numpy as np from mahjong.tile import TilesConverter from mahjong.meld import Meld import mahjong.constants as const from mahjong.hand_calculating.hand import HandCalculator, HandConfig from mahjong.agari import Agari calculator = HandCalculator() agari = Agari() class Wall: def __init__(self): # creates a random wall, just like shuffling self.contents = list(range(136)) np.random.shuffle(self.contents) self.length = 136 def __str__(self): return 'length=' + str(self.length) def draw(self): # draws a random tile. a = self.contents[0] self.contents = self.contents[1:] self.length -= 1 return a class Hand: def __init__(self, wall): self.hand = [] for i in range(13):