def parse_kan(self, data, meld): base_and_called = data >> 8 base = base_and_called // 4 meld.type = Meld.KAN meld.tiles = [ Tile(4 * base), Tile(1 + 4 * base), Tile(2 + 4 * base), Tile(3 + 4 * base) ]
def parse_chi(self, data, meld): meld.type = Meld.CHI t0, t1, t2 = (data >> 3) & 0x3, (data >> 5) & 0x3, (data >> 7) & 0x3 base_and_called = data >> 10 base = base_and_called // 3 base = (base // 7) * 9 + base % 7 meld.tiles = [ Tile(t0 + 4 * (base + 0)), Tile(t1 + 4 * (base + 1)), Tile(t2 + 4 * (base + 2)) ]
def add_discarded_tile(self, player_seat, tile, is_tsumogiri, drawn_tile=None): """ :param player_seat: :param tile: 136 format tile :param is_tsumogiri: was tile discarded from hand or not :param drawn_tile: tile drawn from table """ logger.debug( " check 1: %s, %s" % (self.count_of_remaining_tiles, len(self.remaining_tiles))) self.count_of_remaining_tiles -= 1 if drawn_tile is not None: self.remaining_tiles.remove(drawn_tile) # add this info logger.debug(" add_discarded_tile:%s,%s,%s,%s" % (player_seat, tile, is_tsumogiri, drawn_tile)) logger.debug( " check 2: %s, %s" % (self.count_of_remaining_tiles, len(self.remaining_tiles))) tile = Tile(tile, is_tsumogiri) self.get_player(player_seat).add_discarded_tile(tile) # cache already revealed tiles self._add_revealed_tile(tile.value)
def test_dont_meld_agari(): """ We shouldn't open when we are already in tempai expect for some special cases """ table = Table() table.player.dealer_seat = 3 strategy = FormalTempaiStrategy(BaseStrategy.FORMAL_TEMPAI, table.player) tiles = string_to_136_array(man="2379", sou="4568", pin="22299") table.player.init_hand(tiles) # Let's move to 15th round step for _ in range(0, 15): table.player.add_discarded_tile(Tile(0, False)) assert strategy.should_activate_strategy(table.player.tiles) is True tiles = string_to_136_array(man="23789", sou="456", pin="22299") table.player.init_hand(tiles) meld = make_meld(MeldPrint.CHI, man="789") table.player.add_called_meld(meld) tile = string_to_136_tile(man="4") meld, _ = table.player.try_to_call_meld(tile, True) assert meld is None
def emulate_discard(self, discard_option): player_tiles_original = self.player.tiles[:] player_discards_original = self.player.discards[:] tile_in_hand = discard_option.tile_to_discard_136 self.player.discards.append(Tile(tile_in_hand, False)) self.player.tiles.remove(tile_in_hand) return player_tiles_original, player_discards_original
def test_should_activate_strategy(): table = Table() table.player.dealer_seat = 3 strategy = FormalTempaiStrategy(BaseStrategy.FORMAL_TEMPAI, table.player) tiles = string_to_136_array(sou="12355689", man="89", pin="339") table.player.init_hand(tiles) assert strategy.should_activate_strategy(table.player.tiles) is False # Let's move to 10th round step for _ in range(0, 10): table.player.add_discarded_tile(Tile(0, False)) assert strategy.should_activate_strategy(table.player.tiles) is False # Now we move to 11th turn, we have 2 shanten and no doras, # we should go for formal tempai table.player.add_discarded_tile(Tile(0, True)) assert strategy.should_activate_strategy(table.player.tiles) is True
def test_should_activate_strategy(self): strategy = FormalTempaiStrategy(BaseStrategy.FORMAL_TEMPAI, self.player) tiles = self._string_to_136_array(sou='12355689', man='89', pin='339') self.player.init_hand(tiles) self.assertEqual(strategy.should_activate_strategy(self.player.tiles), False) # Let's move to 10th round step for _ in range(0, 10): self.player.add_discarded_tile(Tile(0, False)) self.assertEqual(strategy.should_activate_strategy(self.player.tiles), False) # Now we move to 11th turn, we have 2 shanten and no doras, # we should go for formal tempai self.player.add_discarded_tile(Tile(0, True)) self.assertEqual(strategy.should_activate_strategy(self.player.tiles), True)
def add_discarded_tile(self, player_seat, tile, is_tsumogiri): """ :param player_seat: :param tile: 136 format tile :param is_tsumogiri: was tile discarded from hand or not """ self.count_of_remaining_tiles -= 1 tile = Tile(tile, is_tsumogiri) self.get_player(player_seat).add_discarded_tile(tile) # cache already revealed tiles self._add_revealed_tile(tile.value)
def test_call_riichi_chiitoitsu_with_suji(): table = _make_table(dora_indicators=[ string_to_136_tile(man="1"), ]) for _ in range(0, 3): table.add_discarded_tile(1, string_to_136_tile(honors="3"), False) tiles = string_to_136_array(man="22336688", sou="9", pin="99", honors="22") table.player.init_hand(tiles) table.player.add_discarded_tile(Tile(string_to_136_tile(sou="6"), True)) table.player.draw_tile(string_to_136_tile(honors="3")) table.player.discard_tile() assert table.player.can_call_riichi() is True
def test_call_riichi_chiitoitsu_with_suji(self): self._make_table(dora_indicators=[ self._string_to_136_tile(man='1'), ]) for _ in range(0, 3): self.table.add_discarded_tile(1, self._string_to_136_tile(honors='3'), False) tiles = self._string_to_136_array(man='22336688', sou='9', pin='99', honors='22') self.player.init_hand(tiles) self.player.add_discarded_tile(Tile(self._string_to_136_tile(sou='6'), True)) self.player.draw_tile(self._string_to_136_tile(honors='3')) self.player.discard_tile() self.assertEqual(self.player.can_call_riichi(), True)
def test_get_tempai(self): tiles = self._string_to_136_array(man='2379', sou='4568', pin='22299') self.player.init_hand(tiles) # Let's move to 15th round step for _ in range(0, 15): self.player.add_discarded_tile(Tile(0, False)) tile = self._string_to_136_tile(man='8') meld, _ = self.player.try_to_call_meld(tile, True) self.assertNotEqual(meld, None) self.assertEqual(self._to_string(meld.tiles), '789m') tile_to_discard = self.player.discard_tile() self.assertEqual(self._to_string([tile_to_discard]), '8s')
def _choose_tanki_with_suji_helper(tiles, suji_tiles, tile_to_draw, tile_to_discard_str): table = Table() player = table.player player.round_step = 2 player.dealer_seat = 3 player.init_hand(tiles) for tile in suji_tiles: player.add_discarded_tile(Tile(tile, True)) player.draw_tile(tile_to_draw) discarded_tile, _ = player.discard_tile() assert tiles_to_string([discarded_tile]) == tile_to_discard_str
def _choose_furiten_over_karaten_helper(tiles, furiten_tile, karaten_tile, tile_to_draw, tile_to_discard_str): table = Table() player = table.player player.round_step = 2 player.dealer_seat = 3 player.init_hand(tiles) player.add_discarded_tile(Tile(furiten_tile, True)) for _ in range(0, 3): table.add_discarded_tile(1, karaten_tile, False) player.draw_tile(tile_to_draw) discarded_tile, _ = player.discard_tile() assert tiles_to_string([discarded_tile]) == tile_to_discard_str
def _avoid_furiten_helper(self, tiles, furiten_tile, other_tile, tile_to_draw, tile_to_discard_str): table = Table() player = table.player player.round_step = 2 player.dealer_seat = 3 player.init_hand(tiles) player.add_discarded_tile(Tile(furiten_tile, True)) for _ in range(0, 2): table.add_discarded_tile(1, other_tile, False) player.draw_tile(tile_to_draw) discarded_tile = player.discard_tile() self.assertEqual(self._to_string([discarded_tile]), tile_to_discard_str)
def add_discarded_tile(self, player_seat, tile_136, is_tsumogiri): """ :param player_seat: :param tile_136: 136 format tile :param is_tsumogiri: was tile discarded from hand or not """ if player_seat != 0: self.count_of_remaining_tiles -= 1 tile = Tile(tile_136, is_tsumogiri) player = self.get_player(player_seat) player.add_discarded_tile(tile) self._add_revealed_tile(tile_136) if self.latest_riichi_player_seat == player_seat: self.latest_riichi_player_seat = None player.riichi_tile_136 = tile_136 player.is_ippatsu = False
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 if data & 0x8: meld.type = Meld.PON meld.tiles = [ Tile(t0 + 4 * base), Tile(t1 + 4 * base), Tile(t2 + 4 * base) ] else: meld.type = Meld.CHAKAN meld.tiles = [ Tile(t0 + 4 * base), Tile(t1 + 4 * base), Tile(t2 + 4 * base), Tile(t4 + 4 * base) ]
def test_dont_meld_agari(self): strategy = FormalTempaiStrategy(BaseStrategy.FORMAL_TEMPAI, self.player) tiles = self._string_to_136_array(man='2379', sou='4568', pin='22299') self.player.init_hand(tiles) # Let's move to 15th round step for _ in range(0, 15): self.player.add_discarded_tile(Tile(0, False)) self.assertEqual(strategy.should_activate_strategy(self.player.tiles), True) tiles = self._string_to_136_array(man='23789', sou='456', pin='22299') self.player.init_hand(tiles) meld = self._make_meld(Meld.CHI, man='789') self.player.add_called_meld(meld) tile = self._string_to_136_tile(man='4') meld, _ = self.player.try_to_call_meld(tile, True) self.assertEqual(meld, None)
def test_get_tempai(): table = Table() table.player.dealer_seat = 3 tiles = string_to_136_array(man="2379", sou="4568", pin="22299") table.player.init_hand(tiles) # Let's move to 15th round step for _ in range(0, 15): table.player.add_discarded_tile(Tile(0, False)) tile = string_to_136_tile(man="8") meld, _ = table.player.try_to_call_meld(tile, True) assert meld is not None assert tiles_to_string(meld.tiles) == "789m" # reinit hand with meld tiles = string_to_136_array(man="23789", sou="4568", pin="22299") table.player.init_hand(tiles) table.player.add_called_meld(meld) tile_to_discard, _ = table.player.discard_tile() assert tiles_to_string([tile_to_discard]) == "8s"
def test_open_hand_on_fifth_round_step(self): """ If we have valuable pair in the hand, 1+ dora and 5+ round step let's open on this valuable pair """ tiles = self._string_to_136_array(man='59', sou='1235', pin='12789', honors='11') self.player.init_hand(tiles) tile = self._string_to_136_tile(honors='1') meld, _ = self.player.try_to_call_meld(tile, True) self.assertEqual(meld, None) # add doras to the hand self.table.dora_indicators.append(self._string_to_136_tile(pin='7')) self.player.init_hand(tiles) tile = self._string_to_136_tile(honors='1') meld, _ = self.player.try_to_call_meld(tile, True) self.assertEqual(meld, None) # one discard == one round step self.player.add_discarded_tile(Tile(0, False)) self.player.add_discarded_tile(Tile(0, False)) self.player.add_discarded_tile(Tile(0, False)) self.player.add_discarded_tile(Tile(0, False)) self.player.add_discarded_tile(Tile(0, False)) self.player.add_discarded_tile(Tile(0, False)) self.player.init_hand(tiles) # after 5 round step we can open hand tile = self._string_to_136_tile(honors='1') meld, _ = self.player.try_to_call_meld(tile, True) self.assertNotEqual(meld, None) # but we don't need to open hand for atodzuke here tile = self._string_to_136_tile(pin='3') meld, _ = self.player.try_to_call_meld(tile, True) self.assertEqual(meld, None)
def test_open_hand_on_fifth_round_step(): """ If we have valuable pair in the hand, 1+ dora and 5+ round step let's open on this valuable pair """ table = Table() table.player.dealer_seat = 3 tiles = string_to_136_array(man="59", sou="1235", pin="12789", honors="11") table.player.init_hand(tiles) tile = string_to_136_tile(honors="1") meld, _ = table.player.try_to_call_meld(tile, True) assert meld is None # add doras to the hand table.dora_indicators.append(string_to_136_tile(pin="7")) table.player.init_hand(tiles) tile = string_to_136_tile(honors="1") meld, _ = table.player.try_to_call_meld(tile, True) assert meld is None # one discard == one round step table.player.add_discarded_tile(Tile(0, False)) table.player.add_discarded_tile(Tile(0, False)) table.player.add_discarded_tile(Tile(0, False)) table.player.add_discarded_tile(Tile(0, False)) table.player.add_discarded_tile(Tile(0, False)) table.player.add_discarded_tile(Tile(0, False)) table.player.init_hand(tiles) # after 5 round step we can open hand tile = string_to_136_tile(honors="1") meld, _ = table.player.try_to_call_meld(tile, True) assert meld is not None # but we don't need to open hand for atodzuke here tile = string_to_136_tile(pin="3") meld, _ = table.player.try_to_call_meld(tile, True) assert meld is None
def draw_tile(self, tile): self.tiles.append(Tile(tile)) # we need sort it to have a better string presentation self.tiles = sorted(self.tiles)
def add_discarded_tile(self, tile): self.discards.append(Tile(tile))
def init_hand(self, tiles): self.tiles = [Tile(i) for i in tiles]
def parse_nuki(self, data, meld): meld.type = Meld.NUKI meld.tiles = [Tile(data >> 8)]
def _choose_best_discard_in_tempai(self, discard_options, after_meld): # first of all we find tiles that have the best hand cost * ukeire value call_riichi = not (self.player.is_open_hand or after_meld) discard_desc = [] player_tiles_copy = self.player.tiles[:] closed_tiles_34 = TilesConverter.to_34_array(self.player.closed_hand) for discard_option in discard_options: tile = discard_option.find_tile_in_hand(self.player.closed_hand) # temporary remove discard option to estimate hand value self.player.tiles = player_tiles_copy[:] self.player.tiles.remove(tile) # for kabe/suji handling discarded_tile = Tile(tile, False) self.player.discards.append(discarded_tile) is_furiten = self._is_discard_option_furiten(discard_option) if len(discard_option.waiting) == 1: waiting = discard_option.waiting[0] cost_x_ukeire, hand_cost = self._estimate_cost_x_ukeire(discard_option, call_riichi) # let's check if this is a tanki wait results, tiles_34 = self.divide_hand(self.player.tiles, waiting) result = results[0] tanki_type = None is_tanki = False for hand_set in result: if waiting not in hand_set: continue if is_pair(hand_set): is_tanki = True if is_honor(waiting): # TODO: differentiate between self honor and honor for all players if waiting in self.player.valued_honors: tanki_type = TankiWait.TANKI_WAIT_ALL_YAKUHAI else: tanki_type = TankiWait.TANKI_WAIT_NON_YAKUHAI break simplified_waiting = simplify(waiting) have_suji, have_kabe = self.check_suji_and_kabe(closed_tiles_34, waiting) # TODO: not sure about suji/kabe priority, so we keep them same for now if 3 <= simplified_waiting <= 5: if have_suji or have_kabe: tanki_type = TankiWait.TANKI_WAIT_456_KABE else: tanki_type = TankiWait.TANKI_WAIT_456_RAW elif 2 <= simplified_waiting <= 6: if have_suji or have_kabe: tanki_type = TankiWait.TANKI_WAIT_37_KABE else: tanki_type = TankiWait.TANKI_WAIT_37_RAW elif 1 <= simplified_waiting <= 7: if have_suji or have_kabe: tanki_type = TankiWait.TANKI_WAIT_28_KABE else: tanki_type = TankiWait.TANKI_WAIT_28_RAW else: if have_suji or have_kabe: tanki_type = TankiWait.TANKI_WAIT_69_KABE else: tanki_type = TankiWait.TANKI_WAIT_69_RAW break tempai_descriptor = { "discard_option": discard_option, "hand_cost": hand_cost, "cost_x_ukeire": cost_x_ukeire, "is_furiten": is_furiten, "is_tanki": is_tanki, "tanki_type": tanki_type, "max_danger": discard_option.danger.get_max_danger(), "sum_danger": discard_option.danger.get_sum_danger(), "weighted_danger": discard_option.danger.get_weighted_danger(), } discard_desc.append(tempai_descriptor) else: cost_x_ukeire, _ = self._estimate_cost_x_ukeire(discard_option, call_riichi) tempai_descriptor = { "discard_option": discard_option, "hand_cost": None, "cost_x_ukeire": cost_x_ukeire, "is_furiten": is_furiten, "is_tanki": False, "tanki_type": None, "max_danger": discard_option.danger.get_max_danger(), "sum_danger": discard_option.danger.get_sum_danger(), "weighted_danger": discard_option.danger.get_weighted_danger(), } discard_desc.append(tempai_descriptor) # save descriptor to discard option for future users discard_option.tempai_descriptor = tempai_descriptor # reverse all temporary tile tweaks self.player.tiles = player_tiles_copy self.player.discards.remove(discarded_tile) discard_desc = sorted(discard_desc, key=lambda k: (-k["cost_x_ukeire"], k["is_furiten"], k["weighted_danger"])) # if we don't have any good options, e.g. all our possible waits are karaten if discard_desc[0]["cost_x_ukeire"] == 0: # we still choose between options that give us tempai, because we may be going to formal tempai # with no hand cost return self._choose_safest_tile(discard_options) num_tanki_waits = len([x for x in discard_desc if x["is_tanki"]]) # what if all our waits are tanki waits? we need a special handling for that case if num_tanki_waits == len(discard_options): return self._choose_best_tanki_wait(discard_desc) best_discard_desc = [x for x in discard_desc if x["cost_x_ukeire"] == discard_desc[0]["cost_x_ukeire"]] best_discard_desc = sorted(best_discard_desc, key=lambda k: (k["is_furiten"], k["weighted_danger"])) # if we have several options that give us similar wait return best_discard_desc[0]["discard_option"]
def calculate_second_level_ukeire(self, discard_option, after_meld=False): self._assert_hand_correctness() not_suitable_tiles = self.ai.current_strategy and self.ai.current_strategy.not_suitable_tiles or [] call_riichi = discard_option.with_riichi # we are going to do manipulations that require player hand and discards to be updated # so we save original tiles and discards here and restore it at the end of the function player_tiles_original, player_discards_original = self.emulate_discard( discard_option) sum_tiles = 0 sum_cost = 0 average_costs = [] for wait_34 in discard_option.waiting: if self.player.is_open_hand and wait_34 in not_suitable_tiles: continue closed_hand_34 = TilesConverter.to_34_array( self.player.closed_hand) live_tiles = 4 - self.player.number_of_revealed_tiles( wait_34, closed_hand_34) if live_tiles == 0: continue wait_136 = self._find_live_tile(wait_34) assert wait_136 is not None self.player.tiles.append(wait_136) results, shanten = self.find_discard_options() results = [ x for x in results if x.shanten == discard_option.shanten - 1 ] # let's take best ukeire here if results: result_has_atodzuke = False if self.player.is_open_hand: best_one = results[0] best_ukeire = 0 for result in results: has_atodzuke = False ukeire = 0 for wait_34 in result.waiting: if wait_34 in not_suitable_tiles: has_atodzuke = True else: ukeire += result.wait_to_ukeire[wait_34] # let's consider atodzuke waits to be worse than non-atodzuke ones if has_atodzuke: ukeire /= 2 # FIXME consider sorting by cost_x_ukeire as well if (ukeire > best_ukeire) or (ukeire >= best_ukeire and not has_atodzuke): best_ukeire = ukeire best_one = result result_has_atodzuke = has_atodzuke else: if shanten == 0: # FIXME save cost_x_ukeire to not calculate it twice best_one = sorted( results, key=lambda x: (-x.ukeire, -self._estimate_cost_x_ukeire( x, call_riichi=call_riichi)[0]), )[0] else: best_one = sorted(results, key=lambda x: -x.ukeire)[0] best_ukeire = best_one.ukeire sum_tiles += best_ukeire * live_tiles # if we are going to have a tempai (on our second level) - let's also count its cost if shanten == 0: # temporary update players hand and discards for calculations next_tile_in_hand = best_one.tile_to_discard_136 tile_for_discard = Tile(next_tile_in_hand, False) self.player.tiles.remove(next_tile_in_hand) self.player.discards.append(tile_for_discard) cost_x_ukeire, _ = self._estimate_cost_x_ukeire( best_one, call_riichi=call_riichi) if best_ukeire != 0: average_costs.append(cost_x_ukeire / best_ukeire) # we reduce tile valuation for atodzuke if result_has_atodzuke: cost_x_ukeire /= 2 sum_cost += cost_x_ukeire # restore original players hand and discard state self.player.tiles.append(next_tile_in_hand) self.player.discards.remove(tile_for_discard) self.player.tiles.remove(wait_136) discard_option.ukeire_second = sum_tiles if discard_option.shanten == 1: if discard_option.ukeire != 0: discard_option.average_second_level_waits = round( sum_tiles / discard_option.ukeire, 2) discard_option.second_level_cost = sum_cost if not average_costs: discard_option.average_second_level_cost = 0 else: discard_option.average_second_level_cost = int( sum(average_costs) / len(average_costs)) # restore original state of player hand and discards self.restore_after_emulate_discard(player_tiles_original, player_discards_original)
def _choose_best_discard_in_tempai(self, tiles, melds, discard_options): # first of all we find tiles that have the best hand cost * ukeire value call_riichi = not self.player.is_open_hand discard_desc = [] player_tiles_copy = self.player.tiles.copy() player_melds_copy = self.player.melds.copy() closed_tiles_34 = TilesConverter.to_34_array(self.player.closed_hand) for discard_option in discard_options: tile = discard_option.find_tile_in_hand(self.player.closed_hand) # temporary remove discard option to estimate hand value self.player.tiles = tiles.copy() self.player.tiles.remove(tile) # temporary replace melds self.player.melds = melds.copy() # for kabe/suji handling discarded_tile = Tile(tile, False) self.player.discards.append(discarded_tile) is_furiten = self._is_discard_option_furiten(discard_option) if len(discard_option.waiting) == 1: waiting = discard_option.waiting[0] cost_x_ukeire, hand_cost = self._estimate_cost_x_ukeire(discard_option, call_riichi) # let's check if this is a tanki wait results, tiles_34 = self.divide_hand(self.player.tiles, waiting) result = results[0] tanki_type = None is_tanki = False for hand_set in result: if waiting not in hand_set: continue if is_pair(hand_set): is_tanki = True if is_honor(waiting): # TODO: differentiate between self honor and honor for all players if waiting in self.player.valued_honors: tanki_type = self.TankiWait.TANKI_WAIT_ALL_YAKUHAI else: tanki_type = self.TankiWait.TANKI_WAIT_NON_YAKUHAI break simplified_waiting = simplify(waiting) have_suji, have_kabe = self.check_suji_and_kabe(closed_tiles_34, waiting) # TODO: not sure about suji/kabe priority, so we keep them same for now if 3 <= simplified_waiting <= 5: if have_suji or have_kabe: tanki_type = self.TankiWait.TANKI_WAIT_456_KABE else: tanki_type = self.TankiWait.TANKI_WAIT_456_RAW elif 2 <= simplified_waiting <= 6: if have_suji or have_kabe: tanki_type = self.TankiWait.TANKI_WAIT_37_KABE else: tanki_type = self.TankiWait.TANKI_WAIT_37_RAW elif 1 <= simplified_waiting <= 7: if have_suji or have_kabe: tanki_type = self.TankiWait.TANKI_WAIT_28_KABE else: tanki_type = self.TankiWait.TANKI_WAIT_28_RAW else: if have_suji or have_kabe: tanki_type = self.TankiWait.TANKI_WAIT_69_KABE else: tanki_type = self.TankiWait.TANKI_WAIT_69_RAW break discard_desc.append({ 'discard_option': discard_option, 'hand_cost': hand_cost, 'cost_x_ukeire': cost_x_ukeire, 'is_furiten': is_furiten, 'is_tanki': is_tanki, 'tanki_type': tanki_type }) else: cost_x_ukeire, _ = self._estimate_cost_x_ukeire(discard_option, call_riichi) discard_desc.append({ 'discard_option': discard_option, 'hand_cost': None, 'cost_x_ukeire': cost_x_ukeire, 'is_furiten': is_furiten, 'is_tanki': False, 'tanki_type': None }) # reverse all temporary tile tweaks self.player.tiles = player_tiles_copy self.player.melds = player_melds_copy self.player.discards.remove(discarded_tile) discard_desc = sorted(discard_desc, key=lambda k: (k['cost_x_ukeire'], not k['is_furiten']), reverse=True) # if we don't have any good options, e.g. all our possible waits ara karaten # FIXME: in that case, discard the safest tile if discard_desc[0]['cost_x_ukeire'] == 0: return sorted(discard_options, key=lambda x: x.valuation)[0] num_tanki_waits = len([x for x in discard_desc if x['is_tanki']]) # what if all our waits are tanki waits? we need a special handling for that case if num_tanki_waits == len(discard_options): return self._choose_best_tanki_wait(discard_desc) best_discard_desc = [x for x in discard_desc if x['cost_x_ukeire'] == discard_desc[0]['cost_x_ukeire']] # we only have one best option based on ukeire and cost, nothing more to do here if len(best_discard_desc) == 1: return best_discard_desc[0]['discard_option'] # if we have several options that give us similar wait # FIXME: 1. we find the safest tile to discard # FIXME: 2. if safeness is the same, we try to discard non-dora tiles return best_discard_desc[0]['discard_option']