def test_calculate_suit_tiles_value_and_dora(self): table = Table() table.dora_indicators = [self._string_to_136_tile(sou='9')] player = table.player tile = self._string_to_34_tile(sou='1') option = DiscardOption(player, tile, 0, [], 0) self.assertEqual(option.valuation, 160) # double dora table.dora_indicators = [self._string_to_136_tile(sou='9'), self._string_to_136_tile(sou='9')] tile = self._string_to_34_tile(sou='1') option = DiscardOption(player, tile, 0, [], 0) self.assertEqual(option.valuation, 210)
def test_calculate_suit_tiles_value_and_tanyao_hand(): table = Table() player = table.player table.has_aka_dora = False player.ai.current_strategy = TanyaoStrategy(BaseStrategy.TANYAO, player) # 0 - 8 man # 9 - 17 pin # 18 - 26 sou results = [ [0, 110], [9, 110], [18, 110], [1, 120], [10, 120], [19, 120], [2, 130], [11, 130], [20, 130], [3, 150], [12, 150], [21, 150], [4, 140], [13, 140], [22, 140], [5, 150], [14, 150], [23, 150], [6, 130], [15, 130], [24, 130], [7, 120], [16, 120], [25, 120], [8, 110], [17, 110], [26, 110], ] for item in results: tile = item[0] value = item[1] assert DiscardOption(player, tile * 4, 0, [], 0).valuation == value assert DiscardOption(player, tile * 4 + 1, 0, [], 0).valuation == value assert DiscardOption(player, tile * 4 + 2, 0, [], 0).valuation == value assert DiscardOption(player, tile * 4 + 3, 0, [], 0).valuation == value
def find_discard_options(self): """ :param tiles: array of tiles in 136 format :param closed_hand: array of tiles in 136 format :return: """ self._assert_hand_correctness() tiles = self.player.tiles closed_hand = self.player.closed_hand tiles_34 = TilesConverter.to_34_array(tiles) closed_tiles_34 = TilesConverter.to_34_array(closed_hand) is_agari = self.ai.agari.is_agari(tiles_34, self.player.meld_34_tiles) # we decide beforehand if we need to consider chiitoitsu for all of our possible discards min_shanten, use_chiitoitsu = self.calculate_shanten_and_decide_hand_structure( closed_tiles_34) results = [] tile_34_prev = None # we iterate in reverse order to naturally handle aka-doras, i.e. discard regular 5 if we have it for tile_136 in reversed(self.player.closed_hand): tile_34 = tile_136 // 4 # already added if tile_34 == tile_34_prev: continue else: tile_34_prev = tile_34 closed_tiles_34[tile_34] -= 1 waiting, shanten = self.calculate_waits( closed_tiles_34, tiles_34, use_chiitoitsu=use_chiitoitsu) assert shanten >= min_shanten closed_tiles_34[tile_34] += 1 if waiting: wait_to_ukeire = dict( zip(waiting, [ self.count_tiles([x], closed_tiles_34) for x in waiting ])) results.append( DiscardOption( player=self.player, shanten=shanten, tile_to_discard_136=tile_136, waiting=waiting, ukeire=sum(wait_to_ukeire.values()), wait_to_ukeire=wait_to_ukeire, )) if is_agari: shanten = Shanten.AGARI_STATE else: shanten = min_shanten return results, shanten
def test_calculate_suit_tiles_value_and_dora(self): table = Table() table.dora_indicators = [self._string_to_136_tile(sou='9')] player = table.player tile = self._string_to_34_tile(sou='1') option = DiscardOption(player, tile, 0, [], 0) self.assertEqual(option.valuation, DiscardOption.DORA_VALUE + 110) # double dora table.dora_indicators = [ self._string_to_136_tile(sou='9'), self._string_to_136_tile(sou='9') ] tile = self._string_to_34_tile(sou='1') option = DiscardOption(player, tile, 0, [], 0) self.assertEqual(option.valuation, DiscardOption.DORA_VALUE * 2 + 110) # tile close to dora table.dora_indicators = [self._string_to_136_tile(sou='9')] tile = self._string_to_34_tile(sou='2') option = DiscardOption(player, tile, 0, [], 0) self.assertEqual(option.valuation, DiscardOption.DORA_FIRST_NEIGHBOUR + 120) # tile not far away from dora table.dora_indicators = [self._string_to_136_tile(sou='9')] tile = self._string_to_34_tile(sou='3') option = DiscardOption(player, tile, 0, [], 0) self.assertEqual(option.valuation, DiscardOption.DORA_SECOND_NEIGHBOUR + 140) # tile from other suit table.dora_indicators = [self._string_to_136_tile(sou='9')] tile = self._string_to_34_tile(man='3') option = DiscardOption(player, tile, 0, [], 0) self.assertEqual(option.valuation, 140)
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 test_calculate_suit_tiles_value_and_dora(): table = Table() table.dora_indicators = [string_to_136_tile(sou="9")] player = table.player table.has_aka_dora = False tile = string_to_136_tile(sou="1") option = DiscardOption(player, tile, 0, [], 0) assert option.valuation == (DiscardOption.DORA_VALUE + 110) # double dora table.dora_indicators = [ string_to_136_tile(sou="9"), string_to_136_tile(sou="9") ] tile = string_to_136_tile(sou="1") option = DiscardOption(player, tile, 0, [], 0) assert option.valuation == ((DiscardOption.DORA_VALUE * 2) + 110) # tile close to dora table.dora_indicators = [string_to_136_tile(sou="9")] tile = string_to_136_tile(sou="2") option = DiscardOption(player, tile, 0, [], 0) assert option.valuation == (DiscardOption.DORA_FIRST_NEIGHBOUR + 120) # tile not far away from dora table.dora_indicators = [string_to_136_tile(sou="9")] tile = string_to_136_tile(sou="3") option = DiscardOption(player, tile, 0, [], 0) assert option.valuation == (DiscardOption.DORA_SECOND_NEIGHBOUR + 140) # tile from other suit table.dora_indicators = [string_to_136_tile(sou="9")] tile = string_to_136_tile(man="3") option = DiscardOption(player, tile, 0, [], 0) assert option.valuation == 140
def find_discard_options(self, tiles, closed_hand, melds=None): """ :param tiles: array of tiles in 136 format :param closed_hand: array of tiles in 136 format :param melds: :return: """ if melds is None: melds = [] open_sets_34 = [x.tiles_34 for x in melds] tiles_34 = TilesConverter.to_34_array(tiles) closed_tiles_34 = TilesConverter.to_34_array(closed_hand) is_agari = self.ai.agari.is_agari(tiles_34, self.player.meld_34_tiles) results = [] for hand_tile in range(0, 34): if not closed_tiles_34[hand_tile]: continue tiles_34[hand_tile] -= 1 waiting, shanten = self.calculate_waits(tiles_34, open_sets_34) tiles_34[hand_tile] += 1 if waiting: wait_to_ukeire = dict( zip(waiting, [ self.count_tiles([x], closed_tiles_34) for x in waiting ])) results.append( DiscardOption(player=self.player, shanten=shanten, tile_to_discard=hand_tile, waiting=waiting, ukeire=self.count_tiles( waiting, closed_tiles_34), wait_to_ukeire=wait_to_ukeire)) if is_agari: shanten = Shanten.AGARI_STATE else: shanten, _ = self.calculate_shanten(tiles_34, open_sets_34) return results, shanten
def test_calculate_suit_tiles_value(self): table = Table() player = table.player # 0 - 8 man # 9 - 17 pin # 18 - 26 sou results = [[0, 110], [9, 110], [18, 110], [1, 120], [10, 120], [19, 120], [2, 140], [11, 140], [20, 140], [3, 150], [12, 150], [21, 150], [4, 130], [13, 130], [22, 130], [5, 150], [14, 150], [23, 150], [6, 140], [15, 140], [24, 140], [7, 120], [16, 120], [25, 120], [8, 110], [17, 110], [26, 110]] for item in results: tile = item[0] value = item[1] option = DiscardOption(player, tile, 0, [], 0) self.assertEqual(option.valuation, value)
def test_calculate_honor_tiles_value(): table = Table() player = table.player player.dealer_seat = 3 table.has_aka_dora = False # valuable honor, wind of the round option = DiscardOption(player, EAST * 4, 0, [], 0) assert option.valuation == 120 # valuable honor, wind of the player option = DiscardOption(player, SOUTH * 4, 0, [], 0) assert option.valuation == 120 # not valuable wind option = DiscardOption(player, WEST * 4, 0, [], 0) assert option.valuation == 100 # not valuable wind option = DiscardOption(player, NORTH * 4, 0, [], 0) assert option.valuation == 100 # valuable dragon option = DiscardOption(player, HAKU * 4, 0, [], 0) assert option.valuation == 120 # valuable dragon option = DiscardOption(player, HATSU * 4, 0, [], 0) assert option.valuation == 120 # valuable dragon option = DiscardOption(player, CHUN * 4, 0, [], 0) assert option.valuation == 120 player.dealer_seat = 0 # double wind option = DiscardOption(player, EAST * 4, 0, [], 0) assert option.valuation == 140
def test_calculate_honor_tiles_value(self): table = Table() player = table.player player.dealer_seat = 3 # valuable honor, wind of the round option = DiscardOption(player, EAST, 0, [], 0) self.assertEqual(option.valuation, 120) # valuable honor, wind of the player option = DiscardOption(player, SOUTH, 0, [], 0) self.assertEqual(option.valuation, 120) # not valuable wind option = DiscardOption(player, WEST, 0, [], 0) self.assertEqual(option.valuation, 100) # not valuable wind option = DiscardOption(player, NORTH, 0, [], 0) self.assertEqual(option.valuation, 100) # valuable dragon option = DiscardOption(player, HAKU, 0, [], 0) self.assertEqual(option.valuation, 120) # valuable dragon option = DiscardOption(player, HATSU, 0, [], 0) self.assertEqual(option.valuation, 120) # valuable dragon option = DiscardOption(player, CHUN, 0, [], 0) self.assertEqual(option.valuation, 120) player.dealer_seat = 0 # double wind option = DiscardOption(player, EAST, 0, [], 0) self.assertEqual(option.valuation, 140)
def test_calculate_suit_tiles_value_and_tanyao_hand(self): table = Table() player = table.player player.ai.current_strategy = TanyaoStrategy(BaseStrategy.TANYAO, player) # 0 - 8 man # 9 - 17 pin # 18 - 26 sou results = [[0, 110], [9, 110], [18, 110], [1, 120], [10, 120], [19, 120], [2, 130], [11, 130], [20, 130], [3, 150], [12, 150], [21, 150], [4, 140], [13, 140], [22, 140], [5, 150], [14, 150], [23, 150], [6, 130], [15, 130], [24, 130], [7, 120], [16, 120], [25, 120], [8, 110], [17, 110], [26, 110]] for item in results: tile = item[0] value = item[1] option = DiscardOption(player, tile, 0, [], 0) self.assertEqual(option.valuation, value)
def try_to_find_safe_tile_to_discard(self, discard_results): self.hand_34 = TilesConverter.to_34_array(self.player.tiles) self.closed_hand_34 = TilesConverter.to_34_array( self.player.closed_hand) threatening_players = self._get_threatening_players() # safe tiles that can be safe based on the table situation safe_tiles = self.impossible_wait.find_tiles_to_discard( threatening_players) # first try to check common safe tiles to discard for all players if len(threatening_players) > 1: against_honitsu = [] for player in threatening_players: if player.chosen_suit: against_honitsu += [ self._mark_safe_tiles_against_honitsu(player) ] common_safe_tiles = [x.all_safe_tiles for x in threatening_players] common_safe_tiles += against_honitsu # let's find a common tiles that will be safe against all threatening players common_safe_tiles = list( set.intersection(*map(set, common_safe_tiles))) common_safe_tiles = [ DefenceTile(x, DefenceTile.SAFE) for x in common_safe_tiles ] # there is no sense to calculate suji tiles for honitsu players not_honitsu_players = [ x for x in threatening_players if x.chosen_suit is None ] common_suji_tiles = self.suji.find_tiles_to_discard( not_honitsu_players) if common_safe_tiles: # it can be that safe tile will be mark as "almost safe", # but we already have "safe" tile in our hand validated_safe_tiles = common_safe_tiles for tile in safe_tiles: already_added_tile = [ x for x in common_safe_tiles if x.value == tile.value ] if not already_added_tile: validated_safe_tiles.append(tile) # first try to check 100% safe tiles for all players result = self._find_tile_to_discard(validated_safe_tiles, discard_results) if result: return result if common_suji_tiles: # if there is no 100% safe tiles try to check common suji tiles result = self._find_tile_to_discard(common_suji_tiles, discard_results) if result: return result # there are only one threatening player or we wasn't able to find common safe tiles # let's find safe tiles for most dangerous player first # and than for all other players if we failed find tile for dangerous player for player in threatening_players: player_safe_tiles = [ DefenceTile(x, DefenceTile.SAFE) for x in player.player.all_safe_tiles ] player_suji_tiles = self.suji.find_tiles_to_discard([player]) # it can be that safe tile will be mark as "almost safe", # but we already have "safe" tile in our hand validated_safe_tiles = player_safe_tiles for tile in safe_tiles: already_added_tile = [ x for x in player_safe_tiles if x.value == tile.value ] if not already_added_tile: validated_safe_tiles.append(tile) # better to not use suji for honitsu hands if not player.chosen_suit: validated_safe_tiles += player_suji_tiles result = self._find_tile_to_discard(validated_safe_tiles, discard_results) if result: return result # try to find safe tiles against honitsu if player.chosen_suit: against_honitsu = self._mark_safe_tiles_against_honitsu(player) against_honitsu = [ DefenceTile(x, DefenceTile.SAFE) for x in against_honitsu ] result = self._find_tile_to_discard(against_honitsu, discard_results) if result: return result # we wasn't able to find safe tile to discard logger.info("No safe tiles, try to defence in other ways.") logger.info("With such a hand: {}".format( TilesConverter.to_one_line_string(self.player.closed_hand))) # find honors for i, c in enumerate(self.closed_hand_34[27:]): if c >= 1: logger.info("Defence with honors.") logger.info("Discard {}".format( TilesConverter.to_one_line_string([(27 + i) * 4]))) return DiscardOption(self.player, 27 + i, 7, [], 4) # it's 27+i instead of i # find 19 for i, c in enumerate(self.closed_hand_34): if i % 9 in [0, 8] and c >= 1: logger.info("Defence with 19.") return DiscardOption(self.player, i, 7, [], 4) # find triplets for i, c in enumerate(self.closed_hand_34): if c >= 3: logger.info("Defence with triplets.") return DiscardOption(self.player, i, 7, [], 4) # find pairs for i, c in enumerate(self.closed_hand_34): if c >= 2: logger.info("Defence with pairs.") return DiscardOption(self.player, i, 7, [], 4) # find 28 for i, c in enumerate(self.closed_hand_34): if i % 9 in [1, 7] and c >= 1: return DiscardOption(self.player, i, 7, [], 4) # we really wasn't able to find safe tile to discard logger.info("Really cannot defence.") return None