From d559c34556e7abbcefecc3c7c1e77014108334c2 Mon Sep 17 00:00:00 2001 From: Nihisil Date: Tue, 27 Nov 2018 23:03:42 +0700 Subject: [PATCH 01/10] Add two failed tests for second level ukeire --- .../ai/first_version/tests/tests_discards.py | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/project/game/ai/first_version/tests/tests_discards.py b/project/game/ai/first_version/tests/tests_discards.py index d55c0175..79e21f71 100644 --- a/project/game/ai/first_version/tests/tests_discards.py +++ b/project/game/ai/first_version/tests/tests_discards.py @@ -718,3 +718,51 @@ def test_choose_furiten_over_karaten(self): self._string_to_136_tile(honors='2'), '6z' ) + + def test_discard_tile_based_on_second_level_ukeire(self): + table = Table() + player = table.player + + table.add_dora_indicator(self._string_to_136_tile(man='2')) + table.add_discarded_tile(1, self._string_to_136_tile(man='2'), False) + + tiles = self._string_to_136_array(man='34678', pin='2356', sou='4467') + tile = self._string_to_136_tile(sou='8') + + player.init_hand(tiles) + player.draw_tile(tile) + + discarded_tile = player.discard_tile() + self.assertEqual(self._to_string([discarded_tile]), '2p') + + def test_calculate_second_level_ukeire(self): + """ + There was a bug with 2356 form and second level ukeire + """ + table = Table() + player = table.player + + table.add_dora_indicator(self._string_to_136_tile(man='2')) + table.add_discarded_tile(1, self._string_to_136_tile(man='2'), False) + table.add_discarded_tile(1, self._string_to_136_tile(pin='3'), False) + table.add_discarded_tile(1, self._string_to_136_tile(pin='3'), False) + + tiles = self._string_to_136_array(man='34678', pin='2356', sou='4467') + tile = self._string_to_136_tile(sou='8') + + player.init_hand(tiles) + player.draw_tile(tile) + + discard_options, _ = player.ai.hand_builder.find_discard_options( + player.tiles, + player.closed_hand, + player.melds + ) + + tile = self._string_to_136_tile(man='4') + discard_option = [x for x in discard_options if x.tile_to_discard == tile // 4][0] + + print('') + player.ai.hand_builder.calculate_second_level_ukeire(discard_option) + + self.assertEqual(discard_option.ukeire_second, 108) From d781b01f4d50dde3a8f9c776aafb68eb585db3b9 Mon Sep 17 00:00:00 2001 From: Alexey <475367+Nihisil@users.noreply.github.com> Date: Tue, 27 Nov 2018 23:16:14 +0700 Subject: [PATCH 02/10] Create CODE_OF_CONDUCT.md --- CODE_OF_CONDUCT.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 CODE_OF_CONDUCT.md diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..4087cbea --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,5 @@ +## Tenhou bot code of conduct: + +1. A robot may not injure a human being or, through inaction, allow a human being to come to harm. +2. A robot must obey orders given it by human beings except where such orders would conflict with the First Law. +3. A robot must protect its own existence as long as such protection does not conflict with the First or Second Law. From 2c5b195b2b5a7aaf7bc536bdb3632ace5e3e3087 Mon Sep 17 00:00:00 2001 From: bogachev-pa Date: Tue, 27 Nov 2018 23:25:14 +0300 Subject: [PATCH 03/10] fix ukeire2 calculation #109 We should consider that drawn tile is already in our hand. --- project/game/ai/first_version/hand_builder.py | 17 ++++++++++++++--- .../ai/first_version/tests/tests_discards.py | 1 - 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/project/game/ai/first_version/hand_builder.py b/project/game/ai/first_version/hand_builder.py index f010c014..6788be36 100644 --- a/project/game/ai/first_version/hand_builder.py +++ b/project/game/ai/first_version/hand_builder.py @@ -635,23 +635,33 @@ def process_discard_option(self, discard_option, closed_hand, force_discard=Fals return discard_option.find_tile_in_hand(closed_hand) def calculate_second_level_ukeire(self, discard_option): - closed_hand_34 = TilesConverter.to_34_array(self.player.closed_hand) not_suitable_tiles = self.ai.current_strategy and self.ai.current_strategy.not_suitable_tiles or [] + tile_in_hand = discard_option.find_tile_in_hand(self.player.closed_hand) + tiles = copy.copy(self.player.tiles) - tiles.remove(discard_option.find_tile_in_hand(self.player.closed_hand)) + tiles.remove(tile_in_hand) + + closed_hand = copy.copy(self.player.closed_hand) + closed_hand.remove(tile_in_hand) sum_tiles = 0 for wait_34 in discard_option.waiting: if self.player.is_open_hand and wait_34 in not_suitable_tiles: continue + if discard_option.wait_to_ukeire[wait_34] == 0: + continue + wait_136 = wait_34 * 4 tiles.append(wait_136) + closed_hand.append(wait_136) + + closed_hand_34 = TilesConverter.to_34_array(self.player.closed_hand) results, shanten = self.find_discard_options( tiles, - self.player.closed_hand, + closed_hand, self.player.melds ) results = [x for x in results if x.shanten == discard_option.shanten - 1] @@ -663,6 +673,7 @@ def calculate_second_level_ukeire(self, discard_option): sum_tiles += best_one.ukeire * live_tiles tiles.remove(wait_136) + closed_hand.remove(wait_136) discard_option.ukeire_second = sum_tiles diff --git a/project/game/ai/first_version/tests/tests_discards.py b/project/game/ai/first_version/tests/tests_discards.py index 79e21f71..ca8e5b30 100644 --- a/project/game/ai/first_version/tests/tests_discards.py +++ b/project/game/ai/first_version/tests/tests_discards.py @@ -762,7 +762,6 @@ def test_calculate_second_level_ukeire(self): tile = self._string_to_136_tile(man='4') discard_option = [x for x in discard_options if x.tile_to_discard == tile // 4][0] - print('') player.ai.hand_builder.calculate_second_level_ukeire(discard_option) self.assertEqual(discard_option.ukeire_second, 108) From 3f4401f3bfcaadbcacaa7e132165096b6eda4152 Mon Sep 17 00:00:00 2001 From: bogachev-pa Date: Wed, 28 Nov 2018 01:59:43 +0300 Subject: [PATCH 04/10] hand cost in 1-shanten consideration [DRAFT] --- project/game/ai/discard.py | 2 + project/game/ai/first_version/hand_builder.py | 165 +++++++++++------- .../ai/first_version/tests/tests_discards.py | 32 +++- 3 files changed, 130 insertions(+), 69 deletions(-) diff --git a/project/game/ai/discard.py b/project/game/ai/discard.py index 9ebfa595..7a5e0802 100644 --- a/project/game/ai/discard.py +++ b/project/game/ai/discard.py @@ -32,6 +32,8 @@ class DiscardOption(object): danger = None # wait to ukeire map wait_to_ukeire = None + # second level cost approximation for 1-shanten hands + second_level_cost = None def __init__(self, player, tile_to_discard, shanten, waiting, ukeire, danger=100, wait_to_ukeire=None): """ diff --git a/project/game/ai/first_version/hand_builder.py b/project/game/ai/first_version/hand_builder.py index 6788be36..f0cee28b 100644 --- a/project/game/ai/first_version/hand_builder.py +++ b/project/game/ai/first_version/hand_builder.py @@ -304,15 +304,19 @@ def _choose_best_tanki_wait(self, discard_desc): # if everything is the same we just choose the first one return best_discard_desc[0]['discard_option'] - def _is_furiten(self, tile_34): + def _is_waiting_furiten(self, tile_34): discarded_tiles = [x.value // 4 for x in self.player.discards] return tile_34 in discarded_tiles - def _choose_best_discard_in_tempai(self, tiles, melds, discard_options): - # only 1 option, nothing to choose - if len(discard_options) == 1: - return discard_options[0] + def _is_discard_option_furiten(self, discard_option): + is_furiten = False + + for waiting in discard_option.waiting: + is_furiten = is_furiten or self._is_waiting_furiten(waiting) + return is_furiten + + 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 @@ -333,31 +337,12 @@ def _choose_best_discard_in_tempai(self, tiles, melds, discard_options): discarded_tile = Tile(tile, False) self.player.discards.append(discarded_tile) - hand_cost = 0 + is_furiten = self._is_discard_option_furiten(discard_option) + if len(discard_option.waiting) == 1: waiting = discard_option.waiting[0] - is_furiten = self._is_furiten(waiting) - hand_cost_tsumo = 0 - cost_x_ukeire_tsumo = 0 - hand_value = self.player.ai.estimate_hand_value(waiting, call_riichi=call_riichi, is_tsumo=True) - if hand_value.error is None: - hand_cost_tsumo = hand_value.cost['main'] + 2 * hand_value.cost['additional'] - cost_x_ukeire_tsumo = hand_cost_tsumo * discard_option.ukeire - - hand_cost_ron = 0 - cost_x_ukeire_ron = 0 - if not is_furiten: - hand_value = self.player.ai.estimate_hand_value(waiting, call_riichi=call_riichi, is_tsumo=False) - if hand_value.error is None: - hand_cost_ron = hand_value.cost['main'] - cost_x_ukeire_ron = hand_cost_ron * discard_option.ukeire - - # these are abstract numbers used to compare different waits - # some don't have yaku, some furiten, etc. - # so we use an abstract formula of 1 tsumo cost + 3 ron costs - hand_cost = hand_cost_tsumo + 3 * hand_cost_ron - cost_x_ukeire = cost_x_ukeire_tsumo + 3 * cost_x_ukeire_ron + 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) @@ -416,30 +401,7 @@ def _choose_best_discard_in_tempai(self, tiles, melds, discard_options): 'tanki_type': tanki_type }) else: - cost_x_ukeire_tsumo = 0 - cost_x_ukeire_ron = 0 - is_furiten = False - - for waiting in discard_option.waiting: - is_furiten = is_furiten or self._is_furiten(waiting) - - for waiting in discard_option.waiting: - hand_value = self.player.ai.estimate_hand_value(waiting, - call_riichi=call_riichi, - is_tsumo=True) - if hand_value.error is None: - cost_x_ukeire_tsumo += (hand_value.cost['main'] - + 2 * hand_value.cost['additional'] - ) * discard_option.wait_to_ukeire[waiting] - - if not is_furiten: - hand_value = self.player.ai.estimate_hand_value(waiting, - call_riichi=call_riichi, - is_tsumo=False) - if hand_value.error is None: - cost_x_ukeire_ron += hand_value.cost['main'] * discard_option.wait_to_ukeire[waiting] - - cost_x_ukeire = cost_x_ukeire_tsumo + 3 * cost_x_ukeire_ron + cost_x_ukeire, _ = self._estimate_cost_x_ukeire(discard_option, call_riichi) discard_desc.append({ 'discard_option': discard_option, @@ -544,6 +506,10 @@ def choose_tile_to_discard(self, tiles, closed_hand, melds, print_log=True): ukeire_field = 'ukeire' possible_options = sorted(possible_options, key=lambda x: -getattr(x, ukeire_field)) + # only one option - so we choose it + if len(possible_options) == 1: + return possible_options[0] + # tempai state has a special handling if first_option.shanten == 0: other_tiles_with_same_shanten = [x for x in possible_options if x.shanten == 0] @@ -564,8 +530,16 @@ def choose_tile_to_discard(self, tiles, closed_hand, melds, print_log=True): return sorted(min_dora_list, key=lambda x: -getattr(x, ukeire_field))[0] - # we filter 10% of options here + # only one option - so we choose it + if len(tiles_without_dora) == 1: + return tiles_without_dora[0] + + # 1-shanten hands have special handling - we can consider future hand cost here + if first_option.shanten == 1: + return sorted(tiles_without_dora, key=lambda x: -x.second_level_cost)[0] + if first_option.shanten == 2 or first_option.shanten == 3: + # we filter 10% of options here second_filter_percentage = 10 filtered_options = self._filter_list_by_percentage( tiles_without_dora, @@ -637,15 +611,15 @@ def process_discard_option(self, discard_option, closed_hand, force_discard=Fals def calculate_second_level_ukeire(self, discard_option): not_suitable_tiles = self.ai.current_strategy and self.ai.current_strategy.not_suitable_tiles or [] - tile_in_hand = discard_option.find_tile_in_hand(self.player.closed_hand) + # we are going to do manipulations that require player hand to be updated + # so we save original tiles here and restore it at the end of the function + player_tiles_original = self.player.tiles.copy() - tiles = copy.copy(self.player.tiles) - tiles.remove(tile_in_hand) - - closed_hand = copy.copy(self.player.closed_hand) - closed_hand.remove(tile_in_hand) + tile_in_hand = discard_option.find_tile_in_hand(self.player.closed_hand) + self.player.tiles.remove(tile_in_hand) sum_tiles = 0 + sum_cost = 0 for wait_34 in discard_option.waiting: if self.player.is_open_hand and wait_34 in not_suitable_tiles: continue @@ -654,14 +628,13 @@ def calculate_second_level_ukeire(self, discard_option): continue wait_136 = wait_34 * 4 - tiles.append(wait_136) - closed_hand.append(wait_136) + self.player.tiles.append(wait_136) closed_hand_34 = TilesConverter.to_34_array(self.player.closed_hand) results, shanten = self.find_discard_options( - tiles, - closed_hand, + self.player.tiles, + self.player.closed_hand, self.player.melds ) results = [x for x in results if x.shanten == discard_option.shanten - 1] @@ -672,12 +645,31 @@ def calculate_second_level_ukeire(self, discard_option): live_tiles = 4 - self.player.total_tiles(wait_34, closed_hand_34) sum_tiles += best_one.ukeire * live_tiles - tiles.remove(wait_136) - closed_hand.remove(wait_136) + # if we are going to have a tempai (on our second level) - let's also count its cost + if shanten == 0: + next_tile_in_hand = best_one.find_tile_in_hand(self.player.closed_hand) + self.player.tiles.remove(next_tile_in_hand) + cost_x_ukeire, _ = self._estimate_cost_x_ukeire(best_one, True) + sum_cost += cost_x_ukeire + print("hand - tile - ukeire - tiles - cost_x_ukeire") + print(TilesConverter.to_one_line_string(self.player.tiles)) + print(wait_34) + print(best_one.ukeire) + print(best_one.ukeire * live_tiles) + print(cost_x_ukeire) + self.player.tiles.append(next_tile_in_hand) + + self.player.tiles.remove(wait_136) discard_option.ukeire_second = sum_tiles + if discard_option.shanten == 1: + discard_option.second_level_cost = sum_cost - def _filter_list_by_percentage(self, items, attribute, percentage): + # restore original state of player hand + self.player.tiles = player_tiles_original + + @staticmethod + def _filter_list_by_percentage(items, attribute, percentage): filtered_options = [] first_option = items[0] ukeire_borders = round((getattr(first_option, attribute) / 100) * percentage) @@ -686,7 +678,8 @@ def _filter_list_by_percentage(self, items, attribute, percentage): filtered_options.append(x) return filtered_options - def _choose_ukeire_borders(self, first_option, border_percentage, border_field): + @staticmethod + def _choose_ukeire_borders(first_option, border_percentage, border_field): ukeire_borders = round((getattr(first_option, border_field) / 100) * border_percentage) if first_option.shanten == 0 and ukeire_borders < 2: @@ -699,3 +692,43 @@ def _choose_ukeire_borders(self, first_option, border_percentage, border_field): ukeire_borders = 8 return ukeire_borders + + def _estimate_cost_x_ukeire(self, discard_option, call_riichi): + cost_x_ukeire_tsumo = 0 + cost_x_ukeire_ron = 0 + hand_cost_tsumo = 0 + hand_cost_ron = 0 + + is_furiten = self._is_discard_option_furiten(discard_option) + + for waiting in discard_option.waiting: + hand_value = self.player.ai.estimate_hand_value(waiting, + call_riichi=call_riichi, + is_tsumo=True) + if hand_value.error is None: + hand_cost_tsumo = hand_value.cost['main'] + 2 * hand_value.cost['additional'] + cost_x_ukeire_tsumo += hand_cost_tsumo * discard_option.wait_to_ukeire[waiting] + else: + print("hand value error tsumo") + + if not is_furiten: + hand_value = self.player.ai.estimate_hand_value(waiting, + call_riichi=call_riichi, + is_tsumo=False) + if hand_value.error is None: + hand_cost_ron = hand_value.cost['main'] + cost_x_ukeire_ron += hand_cost_ron * discard_option.wait_to_ukeire[waiting] + else: + print("hand value error ron") + + # these are abstract numbers used to compare different waits + # some don't have yaku, some furiten, etc. + # so we use an abstract formula of 1 tsumo cost + 3 ron costs + cost_x_ukeire = cost_x_ukeire_tsumo + 3 * cost_x_ukeire_ron + + if len(discard_option.waiting) == 1: + hand_cost = hand_cost_tsumo + 3 * hand_cost_ron + else: + hand_cost = None + + return cost_x_ukeire, hand_cost diff --git a/project/game/ai/first_version/tests/tests_discards.py b/project/game/ai/first_version/tests/tests_discards.py index ca8e5b30..c2830737 100644 --- a/project/game/ai/first_version/tests/tests_discards.py +++ b/project/game/ai/first_version/tests/tests_discards.py @@ -719,7 +719,7 @@ def test_choose_furiten_over_karaten(self): '6z' ) - def test_discard_tile_based_on_second_level_ukeire(self): + def test_discard_tile_based_on_second_level_ukeire_and_cost(self): table = Table() player = table.player @@ -732,8 +732,11 @@ def test_discard_tile_based_on_second_level_ukeire(self): player.init_hand(tiles) player.draw_tile(tile) + print("============================") discarded_tile = player.discard_tile() - self.assertEqual(self._to_string([discarded_tile]), '2p') + discard_correct = self._to_string([discarded_tile]) == '2p' or self._to_string([discarded_tile]) == '3p' + print("============================") + self.assertEqual(discard_correct, True) def test_calculate_second_level_ukeire(self): """ @@ -761,7 +764,30 @@ def test_calculate_second_level_ukeire(self): tile = self._string_to_136_tile(man='4') discard_option = [x for x in discard_options if x.tile_to_discard == tile // 4][0] - player.ai.hand_builder.calculate_second_level_ukeire(discard_option) + self.assertEqual(discard_option.ukeire_second, 108) + tile = self._string_to_136_tile(man='3') + discard_option = [x for x in discard_options if x.tile_to_discard == tile // 4][0] + player.ai.hand_builder.calculate_second_level_ukeire(discard_option) self.assertEqual(discard_option.ukeire_second, 108) + + tile = self._string_to_136_tile(pin='2') + discard_option = [x for x in discard_options if x.tile_to_discard == tile // 4][0] + player.ai.hand_builder.calculate_second_level_ukeire(discard_option) + self.assertEqual(discard_option.ukeire_second, 96) + + tile = self._string_to_136_tile(pin='3') + discard_option = [x for x in discard_options if x.tile_to_discard == tile // 4][0] + player.ai.hand_builder.calculate_second_level_ukeire(discard_option) + self.assertEqual(discard_option.ukeire_second, 96) + + tile = self._string_to_136_tile(pin='5') + discard_option = [x for x in discard_options if x.tile_to_discard == tile // 4][0] + player.ai.hand_builder.calculate_second_level_ukeire(discard_option) + self.assertEqual(discard_option.ukeire_second, 96) + + tile = self._string_to_136_tile(pin='6') + discard_option = [x for x in discard_options if x.tile_to_discard == tile // 4][0] + player.ai.hand_builder.calculate_second_level_ukeire(discard_option) + self.assertEqual(discard_option.ukeire_second, 96) From 71ace763d77923ce24ddef468b456e60f20e86e4 Mon Sep 17 00:00:00 2001 From: bogachev-pa Date: Wed, 28 Nov 2018 14:57:16 +0300 Subject: [PATCH 05/10] various fixes --- project/game/ai/first_version/hand_builder.py | 33 ++++++++++--------- .../ai/first_version/tests/tests_discards.py | 2 -- 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/project/game/ai/first_version/hand_builder.py b/project/game/ai/first_version/hand_builder.py index f0cee28b..aebf436d 100644 --- a/project/game/ai/first_version/hand_builder.py +++ b/project/game/ai/first_version/hand_builder.py @@ -536,7 +536,7 @@ def choose_tile_to_discard(self, tiles, closed_hand, melds, print_log=True): # 1-shanten hands have special handling - we can consider future hand cost here if first_option.shanten == 1: - return sorted(tiles_without_dora, key=lambda x: -x.second_level_cost)[0] + return sorted(tiles_without_dora, key=lambda x: (-x.second_level_cost, -x.ukeire_second, x.valuation))[0] if first_option.shanten == 2 or first_option.shanten == 3: # we filter 10% of options here @@ -610,6 +610,7 @@ def process_discard_option(self, discard_option, closed_hand, force_discard=Fals def calculate_second_level_ukeire(self, discard_option): not_suitable_tiles = self.ai.current_strategy and self.ai.current_strategy.not_suitable_tiles or [] + call_riichi = not self.player.is_open_hand # we are going to do manipulations that require player hand to be updated # so we save original tiles here and restore it at the end of the function @@ -624,14 +625,15 @@ def calculate_second_level_ukeire(self, discard_option): if self.player.is_open_hand and wait_34 in not_suitable_tiles: continue - if discard_option.wait_to_ukeire[wait_34] == 0: + closed_hand_34 = TilesConverter.to_34_array(self.player.closed_hand) + live_tiles = 4 - self.player.total_tiles(wait_34, closed_hand_34) + + if live_tiles == 0: continue wait_136 = wait_34 * 4 self.player.tiles.append(wait_136) - closed_hand_34 = TilesConverter.to_34_array(self.player.closed_hand) - results, shanten = self.find_discard_options( self.player.tiles, self.player.closed_hand, @@ -641,22 +643,25 @@ def calculate_second_level_ukeire(self, discard_option): # let's take best ukeire here if results: + # TODO: find best one considering atodzuke best_one = sorted(results, key=lambda x: -x.ukeire)[0] - live_tiles = 4 - self.player.total_tiles(wait_34, closed_hand_34) sum_tiles += best_one.ukeire * live_tiles + has_atodzuke = False + if self.player.is_open_hand: + for wait_34 in best_one.waiting: + if wait_34 in not_suitable_tiles: + has_atodzuke = True + # if we are going to have a tempai (on our second level) - let's also count its cost if shanten == 0: next_tile_in_hand = best_one.find_tile_in_hand(self.player.closed_hand) self.player.tiles.remove(next_tile_in_hand) - cost_x_ukeire, _ = self._estimate_cost_x_ukeire(best_one, True) + cost_x_ukeire, _ = self._estimate_cost_x_ukeire(best_one, call_riichi=call_riichi) + # we reduce tile valuation for atodzuke + if has_atodzuke: + cost_x_ukeire /= 2 sum_cost += cost_x_ukeire - print("hand - tile - ukeire - tiles - cost_x_ukeire") - print(TilesConverter.to_one_line_string(self.player.tiles)) - print(wait_34) - print(best_one.ukeire) - print(best_one.ukeire * live_tiles) - print(cost_x_ukeire) self.player.tiles.append(next_tile_in_hand) self.player.tiles.remove(wait_136) @@ -708,8 +713,6 @@ def _estimate_cost_x_ukeire(self, discard_option, call_riichi): if hand_value.error is None: hand_cost_tsumo = hand_value.cost['main'] + 2 * hand_value.cost['additional'] cost_x_ukeire_tsumo += hand_cost_tsumo * discard_option.wait_to_ukeire[waiting] - else: - print("hand value error tsumo") if not is_furiten: hand_value = self.player.ai.estimate_hand_value(waiting, @@ -718,8 +721,6 @@ def _estimate_cost_x_ukeire(self, discard_option, call_riichi): if hand_value.error is None: hand_cost_ron = hand_value.cost['main'] cost_x_ukeire_ron += hand_cost_ron * discard_option.wait_to_ukeire[waiting] - else: - print("hand value error ron") # these are abstract numbers used to compare different waits # some don't have yaku, some furiten, etc. diff --git a/project/game/ai/first_version/tests/tests_discards.py b/project/game/ai/first_version/tests/tests_discards.py index c2830737..2e383f75 100644 --- a/project/game/ai/first_version/tests/tests_discards.py +++ b/project/game/ai/first_version/tests/tests_discards.py @@ -732,10 +732,8 @@ def test_discard_tile_based_on_second_level_ukeire_and_cost(self): player.init_hand(tiles) player.draw_tile(tile) - print("============================") discarded_tile = player.discard_tile() discard_correct = self._to_string([discarded_tile]) == '2p' or self._to_string([discarded_tile]) == '3p' - print("============================") self.assertEqual(discard_correct, True) def test_calculate_second_level_ukeire(self): From f52f79ce6d6e0a494899c3f7cbed7bb822e5abd1 Mon Sep 17 00:00:00 2001 From: bogachev-pa Date: Thu, 29 Nov 2018 02:19:56 +0300 Subject: [PATCH 06/10] don't try to "discard" tile from 13-tile hand --- project/game/ai/first_version/main.py | 18 ++++++----- .../tests/strategies/tests_chinitsu.py | 30 +++++++++---------- .../tests/strategies/tests_tanyao.py | 4 +-- .../tests/strategies/tests_yakuhai.py | 12 ++++++-- .../game/ai/first_version/tests/tests_ai.py | 19 +++++++++--- 5 files changed, 52 insertions(+), 31 deletions(-) diff --git a/project/game/ai/first_version/main.py b/project/game/ai/first_version/main.py index 17b2af71..c895c908 100644 --- a/project/game/ai/first_version/main.py +++ b/project/game/ai/first_version/main.py @@ -81,13 +81,17 @@ def init_hand(self): 'Hand: {}'.format(self.player.format_hand_for_print()), ]) - # it will set correct hand shanten number and ukeire to the new hand - # tile will not be removed from the hand - self.discard_tile(None, print_log=False) - self.player.in_tempai = False - - # Let's decide what we will do with our hand (like open for tanyao and etc.) - self.determine_strategy(self.player.tiles) + # we only do this if we initialized out hand directly with 14 tiles + # in case of initialization with 13 tiles, we will find tile to discard + # and determine strategy on our first action possibility + if len(self.player.tiles) == 14: + # it will set correct hand shanten number and ukeire to the new hand + # tile will not be removed from the hand + self.discard_tile(None, print_log=False) + self.player.in_tempai = False + + # Let's decide what we will do with our hand (like open for tanyao and etc.) + self.determine_strategy(self.player.tiles) def draw_tile(self, tile_136): self.determine_strategy(self.player.tiles) diff --git a/project/game/ai/first_version/tests/strategies/tests_chinitsu.py b/project/game/ai/first_version/tests/strategies/tests_chinitsu.py index 34c1acd2..d571b4f5 100644 --- a/project/game/ai/first_version/tests/strategies/tests_chinitsu.py +++ b/project/game/ai/first_version/tests/strategies/tests_chinitsu.py @@ -20,78 +20,78 @@ def test_should_activate_strategy(self): table.add_dora_indicator(self._string_to_136_tile(man='1')) table.add_dora_indicator(self._string_to_136_tile(sou='8')) - tiles = self._string_to_136_array(sou='12355', man='34589', honors='123') + tiles = self._string_to_136_array(sou='12355', man='34589', honors='1234') player.init_hand(tiles) self.assertEqual(strategy.should_activate_strategy(player.tiles), False) - tiles = self._string_to_136_array(sou='12355', man='458', honors='11234') + tiles = self._string_to_136_array(sou='12355', man='458', honors='112345') player.init_hand(tiles) self.assertEqual(strategy.should_activate_strategy(player.tiles), False) # we shouldn't go for chinitsu if we have a valued pair or pon - tiles = self._string_to_136_array(sou='111222578', man='8', honors='555') + tiles = self._string_to_136_array(sou='111222578', man='8', honors='5556') player.init_hand(tiles) self.assertEqual(strategy.should_activate_strategy(player.tiles), False) - tiles = self._string_to_136_array(sou='1112227788', man='7', honors='55') + tiles = self._string_to_136_array(sou='1112227788', man='7', honors='556') player.init_hand(tiles) self.assertEqual(strategy.should_activate_strategy(player.tiles), False) # if we have a pon of non-valued honors, this is not chinitsu - tiles = self._string_to_136_array(sou='1112224688', honors='222') + tiles = self._string_to_136_array(sou='1112224688', honors='2224') player.init_hand(tiles) self.assertEqual(strategy.should_activate_strategy(player.tiles), False) # if we have just a pair of non-valued tiles, we can go for chinitsu # if we have 11 chinitsu tiles and it's early - tiles = self._string_to_136_array(sou='11122234688', honors='22') + tiles = self._string_to_136_array(sou='11122234688', honors='224') player.init_hand(tiles) self.assertEqual(strategy.should_activate_strategy(player.tiles), True) # if we have a complete set with dora, we shouldn't go for chinitsu - tiles = self._string_to_136_array(sou='1112223688', pin='123') + tiles = self._string_to_136_array(sou='1112223688', pin='1239') player.init_hand(tiles) self.assertEqual(strategy.should_activate_strategy(player.tiles), False) # even if the set may be interpreted as two forms - tiles = self._string_to_136_array(sou='111223688', pin='2334') + tiles = self._string_to_136_array(sou='111223688', pin='23349') player.init_hand(tiles) self.assertEqual(strategy.should_activate_strategy(player.tiles), False) # even if the set may be interpreted as two forms v2 - tiles = self._string_to_136_array(sou='111223688', pin='2345') + tiles = self._string_to_136_array(sou='111223688', pin='23459') player.init_hand(tiles) self.assertEqual(strategy.should_activate_strategy(player.tiles), False) # if we have a long form with dora, we shouldn't go for chinitsu - tiles = self._string_to_136_array(sou='111223688', pin='2333') + tiles = self._string_to_136_array(sou='111223688', pin='23339') player.init_hand(tiles) self.assertEqual(strategy.should_activate_strategy(player.tiles), False) # buf it it's just a ryanmen - no problem - tiles = self._string_to_136_array(sou='1112223688', pin='238') + tiles = self._string_to_136_array(sou='1112223688', pin='238', man='9') player.init_hand(tiles) self.assertEqual(strategy.should_activate_strategy(player.tiles), True) # we have three non-isolated doras in other suits, this is not chinitsu - tiles = self._string_to_136_array(sou='111223688', man='22', pin='23') + tiles = self._string_to_136_array(sou='111223688', man='22', pin='239') player.init_hand(tiles) self.assertEqual(strategy.should_activate_strategy(player.tiles), False) # we have two non-isolated doras in other suits and no doras in our suit # this is not chinitsu - tiles = self._string_to_136_array(sou='111223688', man='24', pin='24') + tiles = self._string_to_136_array(sou='111223688', man='24', pin='249') player.init_hand(tiles) self.assertEqual(strategy.should_activate_strategy(player.tiles), False) # we have two non-isolated doras in other suits and 1 shanten, not chinitsu - tiles = self._string_to_136_array(sou='111222789', man='23', pin='23') + tiles = self._string_to_136_array(sou='111222789', man='23', pin='239') player.init_hand(tiles) self.assertEqual(strategy.should_activate_strategy(player.tiles), False) # we don't want to open on 9th tile into chinitsu, but it's ok to # switch to chinitsu if we get in from the wall - tiles = self._string_to_136_array(sou='11223578', man='57', pin='466') + tiles = self._string_to_136_array(sou='11223578', man='57', pin='4669') player.init_hand(tiles) # plus one tile to open hand tiles = self._string_to_136_array(sou='112223578', man='57', pin='466') diff --git a/project/game/ai/first_version/tests/strategies/tests_tanyao.py b/project/game/ai/first_version/tests/strategies/tests_tanyao.py index 137560b4..23f2a26c 100644 --- a/project/game/ai/first_version/tests/strategies/tests_tanyao.py +++ b/project/game/ai/first_version/tests/strategies/tests_tanyao.py @@ -224,11 +224,11 @@ def test_choose_correct_waiting_and_first_opened_meld(self): tiles = self._string_to_136_array(man='2337788', sou='222', pin='234') self.player.init_hand(tiles) - self._assert_tanyao(self.player) - tile = self._string_to_136_tile(man='8') meld, tile_to_discard = self.player.try_to_call_meld(tile, False) + self._assert_tanyao(self.player) + discard = self.player.discard_tile(tile_to_discard) self.assertEqual(self._to_string([discard]), '2m') diff --git a/project/game/ai/first_version/tests/strategies/tests_yakuhai.py b/project/game/ai/first_version/tests/strategies/tests_yakuhai.py index 439ea1c7..d20a0d4f 100644 --- a/project/game/ai/first_version/tests/strategies/tests_yakuhai.py +++ b/project/game/ai/first_version/tests/strategies/tests_yakuhai.py @@ -480,23 +480,29 @@ def test_atodzuke_dont_open_no_yaku_tempai(self): meld = self._make_meld(Meld.PON, man='111') player.add_called_meld(meld) - strategy = YakuhaiStrategy(BaseStrategy.YAKUHAI, player) - self.assertEqual(strategy.should_activate_strategy(player.tiles), True) - # 6 man is bad meld, we lose our second pair and so is 4 man tile = self._string_to_136_tile(man='6') meld, _ = player.try_to_call_meld(tile, True) self.assertEqual(meld, None) + strategy = YakuhaiStrategy(BaseStrategy.YAKUHAI, player) + self.assertEqual(strategy.should_activate_strategy(player.tiles), True) + tile = self._string_to_136_tile(man='4') meld, _ = player.try_to_call_meld(tile, True) self.assertEqual(meld, None) + strategy = YakuhaiStrategy(BaseStrategy.YAKUHAI, player) + self.assertEqual(strategy.should_activate_strategy(player.tiles), True) + # 7 pin is a good meld, we get to tempai keeping yakuhai wait tile = self._string_to_136_tile(pin='7') meld, _ = player.try_to_call_meld(tile, True) self.assertNotEqual(meld, None) + strategy = YakuhaiStrategy(BaseStrategy.YAKUHAI, player) + self.assertEqual(strategy.should_activate_strategy(player.tiles), True) + def test_atodzuke_choose_hidden_syanpon(self): # make sure yakuhai strategy is activated by adding 3 doras to the hand table = Table() diff --git a/project/game/ai/first_version/tests/tests_ai.py b/project/game/ai/first_version/tests/tests_ai.py index 2de7825f..9868ad46 100644 --- a/project/game/ai/first_version/tests/tests_ai.py +++ b/project/game/ai/first_version/tests/tests_ai.py @@ -267,23 +267,34 @@ def test_chose_strategy_and_reset_strategy(self): # add 3 doras so we are sure to go for tanyao table.add_dora_indicator(self._string_to_136_tile(man='2')) + # we draw a tile that will set tanyao as our selected strategy tiles = self._string_to_136_array(man='33355788', sou='3479', honors='3') player.init_hand(tiles) + + tile = self._string_to_136_tile(sou='7') + player.draw_tile(tile) self.assertNotEqual(player.ai.current_strategy, None) self.assertEqual(player.ai.current_strategy.type, BaseStrategy.TANYAO) # we draw a tile that will change our selected strategy + tiles = self._string_to_136_array(man='33355788', sou='3479', honors='3') + player.init_hand(tiles) + + tile = self._string_to_136_tile(sou='2') + meld, _ = player.try_to_call_meld(tile, False) + self.assertNotEqual(player.ai.current_strategy, None) + self.assertEqual(player.ai.current_strategy.type, BaseStrategy.TANYAO) + self.assertEqual(meld, None) + tile = self._string_to_136_tile(sou='8') player.draw_tile(tile) self.assertEqual(player.ai.current_strategy, None) + # for already opened hand we don't need to give up on selected strategy tiles = self._string_to_136_array(man='33355788', sou='3479', honors='3') player.init_hand(tiles) - self.assertEqual(player.ai.current_strategy.type, BaseStrategy.TANYAO) - # for already opened hand we don't need to give up on selected strategy - meld = Meld() - meld.tiles = [1, 2, 3] + meld = self._make_meld(Meld.PON, man='333') player.add_called_meld(meld) tile = self._string_to_136_tile(sou='8') player.draw_tile(tile) From c19dc56923c6fc477ad69252b2fe0fbf47fe5b3b Mon Sep 17 00:00:00 2001 From: Nihisil Date: Thu, 29 Nov 2018 12:44:45 +0700 Subject: [PATCH 07/10] Fix failed tests --- project/game/ai/first_version/defence/main.py | 3 +++ project/game/ai/first_version/main.py | 14 +++----------- project/game/ai/first_version/strategies/main.py | 1 - .../tests/strategies/tests_chiitoitsu.py | 10 +++++++--- .../tests/strategies/tests_formal_tempai.py | 5 ++--- .../tests/strategies/tests_honitsu.py | 3 ++- project/game/ai/first_version/tests/tests_ai.py | 3 +++ 7 files changed, 20 insertions(+), 19 deletions(-) diff --git a/project/game/ai/first_version/defence/main.py b/project/game/ai/first_version/defence/main.py index d70ed69d..a9dc14c6 100644 --- a/project/game/ai/first_version/defence/main.py +++ b/project/game/ai/first_version/defence/main.py @@ -48,6 +48,9 @@ def should_go_to_defence_mode(self, discard_candidate=None): shanten = self.player.ai.shanten waiting = self.player.ai.waiting + if not waiting: + waiting = [] + # if we are in riichi, we can't defence if self.player.in_riichi: return False diff --git a/project/game/ai/first_version/main.py b/project/game/ai/first_version/main.py index c895c908..38d07d1f 100644 --- a/project/game/ai/first_version/main.py +++ b/project/game/ai/first_version/main.py @@ -81,17 +81,9 @@ def init_hand(self): 'Hand: {}'.format(self.player.format_hand_for_print()), ]) - # we only do this if we initialized out hand directly with 14 tiles - # in case of initialization with 13 tiles, we will find tile to discard - # and determine strategy on our first action possibility - if len(self.player.tiles) == 14: - # it will set correct hand shanten number and ukeire to the new hand - # tile will not be removed from the hand - self.discard_tile(None, print_log=False) - self.player.in_tempai = False - - # Let's decide what we will do with our hand (like open for tanyao and etc.) - self.determine_strategy(self.player.tiles) + self.shanten = self.shanten_calculator.calculate_shanten( + TilesConverter.to_34_array(self.player.tiles) + ) def draw_tile(self, tile_136): self.determine_strategy(self.player.tiles) diff --git a/project/game/ai/first_version/strategies/main.py b/project/game/ai/first_version/strategies/main.py index b19e2bd7..37824471 100644 --- a/project/game/ai/first_version/strategies/main.py +++ b/project/game/ai/first_version/strategies/main.py @@ -247,7 +247,6 @@ def _find_best_meld_to_open(self, possible_melds, new_tiles, closed_hand, discar for meld_34 in possible_melds: meld_34_copy = meld_34.copy() closed_hand_copy = closed_hand.copy() - open_sets_34 = self.player.meld_34_tiles + [meld_34] meld_type = is_chi(meld_34_copy) and Meld.CHI or Meld.PON meld_34_copy.remove(discarded_tile_34) diff --git a/project/game/ai/first_version/tests/strategies/tests_chiitoitsu.py b/project/game/ai/first_version/tests/strategies/tests_chiitoitsu.py index 75605f31..dd6fe76a 100644 --- a/project/game/ai/first_version/tests/strategies/tests_chiitoitsu.py +++ b/project/game/ai/first_version/tests/strategies/tests_chiitoitsu.py @@ -16,18 +16,22 @@ def test_should_activate_strategy(self): strategy = ChiitoitsuStrategy(BaseStrategy.CHIITOITSU, player) # obvious chiitoitsu, let's activate - tiles = self._string_to_136_array(sou='2266', man='3399', pin='289', honors='116') + tiles = self._string_to_136_array(sou='2266', man='3399', pin='289', honors='11') player.init_hand(tiles) + player.draw_tile(self._string_to_136_tile(honors='6')) self.assertEqual(strategy.should_activate_strategy(player.tiles), True) # less than 5 pairs, don't activate - tiles = self._string_to_136_array(sou='2266', man='3389', pin='289', honors='116') + tiles = self._string_to_136_array(sou='2266', man='3389', pin='289', honors='11') + player.draw_tile(self._string_to_136_tile(honors='6')) player.init_hand(tiles) self.assertEqual(strategy.should_activate_strategy(player.tiles), False) # 5 pairs, but we are already tempai, let's no consider this hand as chiitoitsu - tiles = self._string_to_136_array(sou='234', man='223344', pin='55669') + tiles = self._string_to_136_array(sou='234', man='223344', pin='5669') player.init_hand(tiles) + player.draw_tile(self._string_to_136_tile(pin='5')) + player.ai.shanten = 0 self.assertEqual(strategy.should_activate_strategy(player.tiles), False) tiles = self._string_to_136_array(sou='234', man='22334455669') diff --git a/project/game/ai/first_version/tests/strategies/tests_formal_tempai.py b/project/game/ai/first_version/tests/strategies/tests_formal_tempai.py index c93144ad..f712d7b7 100644 --- a/project/game/ai/first_version/tests/strategies/tests_formal_tempai.py +++ b/project/game/ai/first_version/tests/strategies/tests_formal_tempai.py @@ -51,7 +51,7 @@ def test_get_tempai(self): tile_to_discard = self.player.discard_tile() self.assertEqual(self._to_string([tile_to_discard]), '8s') - # We shouldn't open when we are already in tempai expect for some + # we shouldn't open when we are already in tempai expect for some # special cases def test_dont_meld_agari(self): strategy = FormalTempaiStrategy(BaseStrategy.FORMAL_TEMPAI, self.player) @@ -67,11 +67,10 @@ def test_dont_meld_agari(self): 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) - self.assertEqual(strategy.should_activate_strategy(self.player.tiles), True) - tile = self._string_to_136_tile(man='4') meld, _ = self.player.try_to_call_meld(tile, True) self.assertEqual(meld, None) diff --git a/project/game/ai/first_version/tests/strategies/tests_honitsu.py b/project/game/ai/first_version/tests/strategies/tests_honitsu.py index ca604924..16873f6d 100644 --- a/project/game/ai/first_version/tests/strategies/tests_honitsu.py +++ b/project/game/ai/first_version/tests/strategies/tests_honitsu.py @@ -186,8 +186,9 @@ def test_open_hand_and_not_go_for_chiitoitsu(self): table = Table() player = table.player - tiles = self._string_to_136_array(sou='1122559', honors='134557', pin='4') + tiles = self._string_to_136_array(sou='1122559', honors='134557') player.init_hand(tiles) + player.draw_tile(self._string_to_136_tile(pin='4')) tile = player.discard_tile() self.assertEqual(self._to_string([tile]), '4p') diff --git a/project/game/ai/first_version/tests/tests_ai.py b/project/game/ai/first_version/tests/tests_ai.py index 9868ad46..ba7b220e 100644 --- a/project/game/ai/first_version/tests/tests_ai.py +++ b/project/game/ai/first_version/tests/tests_ai.py @@ -293,11 +293,14 @@ def test_chose_strategy_and_reset_strategy(self): # for already opened hand we don't need to give up on selected strategy tiles = self._string_to_136_array(man='33355788', sou='3479', honors='3') player.init_hand(tiles) + player.draw_tile(self._string_to_136_tile(honors='5')) + player.discard_tile() meld = self._make_meld(Meld.PON, man='333') player.add_called_meld(meld) tile = self._string_to_136_tile(sou='8') player.draw_tile(tile) + self.assertNotEqual(player.ai.current_strategy, None) self.assertEqual(player.ai.current_strategy.type, BaseStrategy.TANYAO) From d368f3be770262ff3653fcf3d90abbe19ebda96d Mon Sep 17 00:00:00 2001 From: Nihisil Date: Thu, 29 Nov 2018 13:00:21 +0700 Subject: [PATCH 08/10] Fix lint warnings --- project/game/ai/first_version/defence/suji.py | 1 - project/game/ai/first_version/hand_builder.py | 8 ++------ project/game/ai/first_version/riichi.py | 2 -- .../ai/first_version/strategies/yakuhai.py | 6 +++++- .../tests/strategies/tests_formal_tempai.py | 6 +++--- .../tests/strategies/tests_yakuhai.py | 2 +- .../ai/first_version/tests/tests_discards.py | 6 +++--- .../ai/first_version/tests/tests_riichi.py | 12 ++++++------ project/game/tests/tests_table.py | 18 ++++++++++++++++-- project/requirements.txt | 8 +++++++- project/tenhou/client.py | 2 +- 11 files changed, 44 insertions(+), 27 deletions(-) diff --git a/project/game/ai/first_version/defence/suji.py b/project/game/ai/first_version/defence/suji.py index 04b4cd94..cde1f749 100644 --- a/project/game/ai/first_version/defence/suji.py +++ b/project/game/ai/first_version/defence/suji.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- from mahjong.utils import is_man, simplify, is_pin, is_sou, plus_dora, is_aka_dora -from mahjong.tile import TilesConverter from game.ai.first_version.defence.defence import Defence, DefenceTile diff --git a/project/game/ai/first_version/hand_builder.py b/project/game/ai/first_version/hand_builder.py index aebf436d..20e5db94 100644 --- a/project/game/ai/first_version/hand_builder.py +++ b/project/game/ai/first_version/hand_builder.py @@ -1,16 +1,12 @@ -import copy - from mahjong.constants import AKA_DORA_LIST from mahjong.shanten import Shanten from mahjong.tile import TilesConverter, Tile -from mahjong.utils import is_tile_strictly_isolated, is_pair, is_honor, simplify, is_chi -from mahjong.meld import Meld +from mahjong.utils import is_tile_strictly_isolated, is_pair, is_honor, simplify import utils.decisions_constants as log from game.ai.discard import DiscardOption -from utils.decisions_logger import DecisionsLogger - from game.ai.first_version.defence.kabe import KabeTile +from utils.decisions_logger import DecisionsLogger class HandBuilder: diff --git a/project/game/ai/first_version/riichi.py b/project/game/ai/first_version/riichi.py index 656ee4d5..321037f0 100644 --- a/project/game/ai/first_version/riichi.py +++ b/project/game/ai/first_version/riichi.py @@ -1,8 +1,6 @@ from mahjong.tile import TilesConverter from mahjong.utils import is_honor, simplify, is_pair, is_chi -from game.ai.first_version.defence.kabe import KabeTile - class Riichi: diff --git a/project/game/ai/first_version/strategies/yakuhai.py b/project/game/ai/first_version/strategies/yakuhai.py index 428c746d..6cb3698a 100644 --- a/project/game/ai/first_version/strategies/yakuhai.py +++ b/project/game/ai/first_version/strategies/yakuhai.py @@ -84,9 +84,13 @@ def should_activate_strategy(self, tiles_136): for pair in self.valued_pairs: # last chance to get that yakuhai, let's go for it - if opportunity_to_meld_yakuhai and self.player.total_tiles(pair, player_closed_hand_tiles_34) == 3 and self.player.ai.shanten >= 1: + if (opportunity_to_meld_yakuhai and + self.player.total_tiles(pair, player_closed_hand_tiles_34) == 3 and + self.player.ai.shanten >= 1): + if pair not in self.last_chance_calls: self.last_chance_calls.append(pair) + return True return False diff --git a/project/game/ai/first_version/tests/strategies/tests_formal_tempai.py b/project/game/ai/first_version/tests/strategies/tests_formal_tempai.py index f712d7b7..197c5244 100644 --- a/project/game/ai/first_version/tests/strategies/tests_formal_tempai.py +++ b/project/game/ai/first_version/tests/strategies/tests_formal_tempai.py @@ -25,7 +25,7 @@ def test_should_activate_strategy(self): self.assertEqual(strategy.should_activate_strategy(self.player.tiles), False) # Let's move to 10th round step - for i in range(0, 10): + for _ in range(0, 10): self.player.add_discarded_tile(Tile(0, False)) self.assertEqual(strategy.should_activate_strategy(self.player.tiles), False) @@ -40,7 +40,7 @@ def test_get_tempai(self): self.player.init_hand(tiles) # Let's move to 15th round step - for i in range(0, 15): + for _ in range(0, 15): self.player.add_discarded_tile(Tile(0, False)) tile = self._string_to_136_tile(man='8') @@ -60,7 +60,7 @@ def test_dont_meld_agari(self): self.player.init_hand(tiles) # Let's move to 15th round step - for i in range(0, 15): + for _ in range(0, 15): self.player.add_discarded_tile(Tile(0, False)) self.assertEqual(strategy.should_activate_strategy(self.player.tiles), True) diff --git a/project/game/ai/first_version/tests/strategies/tests_yakuhai.py b/project/game/ai/first_version/tests/strategies/tests_yakuhai.py index d20a0d4f..3eedd088 100644 --- a/project/game/ai/first_version/tests/strategies/tests_yakuhai.py +++ b/project/game/ai/first_version/tests/strategies/tests_yakuhai.py @@ -518,7 +518,7 @@ def test_atodzuke_choose_hidden_syanpon(self): strategy = YakuhaiStrategy(BaseStrategy.YAKUHAI, player) self.assertEqual(strategy.should_activate_strategy(player.tiles), True) - for i in range(0, 4): + for _ in range(0, 4): table.add_discarded_tile(1, self._string_to_136_tile(sou='9'), False) player.draw_tile(self._string_to_136_tile(man='6')) diff --git a/project/game/ai/first_version/tests/tests_discards.py b/project/game/ai/first_version/tests/tests_discards.py index 2e383f75..5a07175c 100644 --- a/project/game/ai/first_version/tests/tests_discards.py +++ b/project/game/ai/first_version/tests/tests_discards.py @@ -486,7 +486,7 @@ def _choose_tanki_with_kabe_helper(self, tiles, kabe_tiles, tile_to_draw, tile_t player.dealer_seat = 3 for tile in kabe_tiles: - for i in range(0, 4): + for _ in range(0, 4): table.add_discarded_tile(1, tile, False) player.init_hand(tiles) @@ -661,7 +661,7 @@ def _avoid_furiten_helper(self, tiles, furiten_tile, other_tile, tile_to_draw, t player.add_discarded_tile(Tile(furiten_tile, True)) - for i in range(0, 2): + for _ in range(0, 2): table.add_discarded_tile(1, other_tile, False) player.draw_tile(tile_to_draw) @@ -695,7 +695,7 @@ def _choose_furiten_over_karaten_helper(self, tiles, furiten_tile, karaten_tile, player.add_discarded_tile(Tile(furiten_tile, True)) - for i in range(0, 3): + for _ in range(0, 3): table.add_discarded_tile(1, karaten_tile, False) player.draw_tile(tile_to_draw) diff --git a/project/game/ai/first_version/tests/tests_riichi.py b/project/game/ai/first_version/tests/tests_riichi.py index 5ad0e29b..574e0f25 100644 --- a/project/game/ai/first_version/tests/tests_riichi.py +++ b/project/game/ai/first_version/tests/tests_riichi.py @@ -112,7 +112,7 @@ def test_dont_call_karaten_tanki_riichi(self): tiles = self._string_to_136_array(man='22336688', sou='99', pin='99', honors='2') self.player.init_hand(tiles) - for i in range(0, 3): + for _ in range(0, 3): self.table.add_discarded_tile(1, self._string_to_136_tile(honors='2'), False) self.table.add_discarded_tile(1, self._string_to_136_tile(honors='3'), False) @@ -130,7 +130,7 @@ def test_dont_call_karaten_ryanmen_riichi(self): tiles = self._string_to_136_array(man='222', sou='22278', pin='22789') self.player.init_hand(tiles) - for i in range(0, 4): + for _ in range(0, 4): self.table.add_discarded_tile(1, self._string_to_136_tile(sou='6'), False) self.table.add_discarded_tile(1, self._string_to_136_tile(sou='9'), False) @@ -155,10 +155,10 @@ def test_call_riichi_tanki_with_kabe(self): self._string_to_136_tile(pin='1'), ]) - for i in range(0, 3): + for _ in range(0, 3): self.table.add_discarded_tile(1, self._string_to_136_tile(honors='1'), False) - for i in range(0, 4): + for _ in range(0, 4): self.table.add_discarded_tile(1, self._string_to_136_tile(sou='8'), False) tiles = self._string_to_136_array(sou='1119', pin='234567', man='666') @@ -173,7 +173,7 @@ def test_call_riichi_chiitoitsu_with_suji(self): self._string_to_136_tile(man='1'), ]) - for i in range(0, 3): + 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') @@ -189,7 +189,7 @@ def test_dont_call_riichi_chiitoitsu_bad_wait(self): self._string_to_136_tile(man='1'), ]) - for i in range(0, 3): + 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='4', pin='99', honors='22') diff --git a/project/game/tests/tests_table.py b/project/game/tests/tests_table.py index b1b4f905..70106e78 100644 --- a/project/game/tests/tests_table.py +++ b/project/game/tests/tests_table.py @@ -26,7 +26,14 @@ def test_init_round(self): dealer = 3 scores = [250, 250, 250, 250] - table.init_round(round_wind_number, count_of_honba_sticks, count_of_riichi_sticks, dora_indicator, dealer, scores) + table.init_round( + round_wind_number, + count_of_honba_sticks, + count_of_riichi_sticks, + dora_indicator, + dealer, + scores + ) self.assertEqual(table.round_wind_number, round_wind_number) self.assertEqual(table.count_of_honba_sticks, count_of_honba_sticks) @@ -38,7 +45,14 @@ def test_init_round(self): dealer = 2 table.player.in_tempai = True table.player.in_riichi = True - table.init_round(round_wind_number, count_of_honba_sticks, count_of_riichi_sticks, dora_indicator, dealer, scores) + table.init_round( + round_wind_number, + count_of_honba_sticks, + count_of_riichi_sticks, + dora_indicator, + dealer, + scores + ) # test that we reinit round properly self.assertEqual(table.get_player(3).is_dealer, False) diff --git a/project/requirements.txt b/project/requirements.txt index 11e76a09..d48ea8f9 100644 --- a/project/requirements.txt +++ b/project/requirements.txt @@ -1,3 +1,9 @@ +# our core library mahjong==1.1.5 + +# to send information about games to statistics server requests==2.20.1 -flake8==3.4.1 + +# for dev needs +flake8==3.6.0 +flake8-bugbear==18.8.0 \ No newline at end of file diff --git a/project/tenhou/client.py b/project/tenhou/client.py index 8352676e..fb7118f7 100644 --- a/project/tenhou/client.py +++ b/project/tenhou/client.py @@ -495,7 +495,7 @@ def send_request(): # we can't use sleep(15), because we want to be able # end thread in the middle of running seconds_to_sleep = 15 - for x in range(0, seconds_to_sleep * 2): + for _ in range(0, seconds_to_sleep * 2): if self.game_is_continue: sleep(0.5) From 1704d6b8ecb4974b64ba3a40130dca513782de8d Mon Sep 17 00:00:00 2001 From: bogachev-pa Date: Sat, 1 Dec 2018 16:58:08 +0300 Subject: [PATCH 09/10] improve atodzuke handling rules #72 1. Fixed ukeire2 calculation with atodzuke. 2. For tanyao now only strictly avoid atodzuke in tempai. 3. Don't consider wait to be atodzuke if there are no live non-suitable tiles. --- project/game/ai/first_version/hand_builder.py | 39 ++++++++++++------ .../ai/first_version/strategies/tanyao.py | 10 +++-- .../tests/strategies/tests_chiitoitsu.py | 2 +- .../tests/strategies/tests_tanyao.py | 40 +++++++++++++++++++ 4 files changed, 74 insertions(+), 17 deletions(-) diff --git a/project/game/ai/first_version/hand_builder.py b/project/game/ai/first_version/hand_builder.py index 20e5db94..a2e984f2 100644 --- a/project/game/ai/first_version/hand_builder.py +++ b/project/game/ai/first_version/hand_builder.py @@ -198,11 +198,7 @@ def find_discard_options(self, tiles, closed_hand, melds=None): def count_tiles(self, waiting, tiles_34): n = 0 - not_suitable_tiles = self.ai.current_strategy and self.ai.current_strategy.not_suitable_tiles or [] for tile_34 in waiting: - if self.player.is_open_hand and tile_34 in not_suitable_tiles: - continue - n += 4 - self.player.total_tiles(tile_34, tiles_34) return n @@ -639,15 +635,32 @@ def calculate_second_level_ukeire(self, discard_option): # let's take best ukeire here if results: - # TODO: find best one considering atodzuke - best_one = sorted(results, key=lambda x: -x.ukeire)[0] - sum_tiles += best_one.ukeire * live_tiles - - has_atodzuke = False + result_has_atodzuke = False if self.player.is_open_hand: - for wait_34 in best_one.waiting: - if wait_34 in not_suitable_tiles: - has_atodzuke = True + 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 + + if (ukeire > best_ukeire) or (ukeire >= best_ukeire and not has_atodzuke): + best_ukeire = ukeire + best_one = result + result_has_atodzuke = has_atodzuke + 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: @@ -655,7 +668,7 @@ def calculate_second_level_ukeire(self, discard_option): self.player.tiles.remove(next_tile_in_hand) cost_x_ukeire, _ = self._estimate_cost_x_ukeire(best_one, call_riichi=call_riichi) # we reduce tile valuation for atodzuke - if has_atodzuke: + if result_has_atodzuke: cost_x_ukeire /= 2 sum_cost += cost_x_ukeire self.player.tiles.append(next_tile_in_hand) diff --git a/project/game/ai/first_version/strategies/tanyao.py b/project/game/ai/first_version/strategies/tanyao.py index 93c8ffc2..0c6e1d50 100644 --- a/project/game/ai/first_version/strategies/tanyao.py +++ b/project/game/ai/first_version/strategies/tanyao.py @@ -140,9 +140,13 @@ def determine_what_to_discard(self, discard_options, hand, open_melds): continue # there is no sense to wait 1-4 if we have open hand - all_waiting_are_fine = all([self.is_tile_suitable(x * 4) for x in item.waiting]) - if all_waiting_are_fine: - results.append(item) + # but let's only avoid atodzuke tiles in tempai, the rest will be dealt with in + # generic logic + if item.shanten == 0: + all_waiting_are_fine = all( + [(self.is_tile_suitable(x * 4) or item.wait_to_ukeire[x] == 0) for x in item.waiting]) + if all_waiting_are_fine: + results.append(item) if not_suitable_tiles: return not_suitable_tiles diff --git a/project/game/ai/first_version/tests/strategies/tests_chiitoitsu.py b/project/game/ai/first_version/tests/strategies/tests_chiitoitsu.py index dd6fe76a..c72e5bd2 100644 --- a/project/game/ai/first_version/tests/strategies/tests_chiitoitsu.py +++ b/project/game/ai/first_version/tests/strategies/tests_chiitoitsu.py @@ -31,7 +31,7 @@ def test_should_activate_strategy(self): tiles = self._string_to_136_array(sou='234', man='223344', pin='5669') player.init_hand(tiles) player.draw_tile(self._string_to_136_tile(pin='5')) - player.ai.shanten = 0 + player.discard_tile() self.assertEqual(strategy.should_activate_strategy(player.tiles), False) tiles = self._string_to_136_array(sou='234', man='22334455669') diff --git a/project/game/ai/first_version/tests/strategies/tests_tanyao.py b/project/game/ai/first_version/tests/strategies/tests_tanyao.py index 23f2a26c..6884c81f 100644 --- a/project/game/ai/first_version/tests/strategies/tests_tanyao.py +++ b/project/game/ai/first_version/tests/strategies/tests_tanyao.py @@ -220,6 +220,46 @@ def test_choose_correct_waiting(self): discard = player.discard_tile() self.assertEqual(self._to_string([discard]), '7s') + def test_choose_balanced_ukeire_in_1_shanten(self): + table = self._make_table() + player = table.player + + meld = self._make_meld(Meld.CHI, man='678') + player.add_called_meld(meld) + + tiles = self._string_to_136_array(man='22678', sou='234568', pin='45') + player.init_hand(tiles) + player.draw_tile(self._string_to_136_tile(man='2')) + + self._assert_tanyao(player) + + # there are lost of options to avoid atodzuke and even if it is atodzuke, + # it is still a good one, so let's choose more efficient 8s discard instead of 2s + discard = player.discard_tile() + self.assertEqual(self._to_string([discard]), '8s') + + def test_choose_pseudo_atodzuke(self): + table = self._make_table() + table.has_aka_dora = False + player = table.player + + # one tile is dora indicator and 3 are out + # so this 1-4 wait is not atodzuke + for _ in range(0, 3): + table.add_discarded_tile(1, self._string_to_136_tile(pin='1'), False) + + meld = self._make_meld(Meld.CHI, man='678') + player.add_called_meld(meld) + + tiles = self._string_to_136_array(man='222678', sou='23488', pin='35') + player.init_hand(tiles) + player.draw_tile(self._string_to_136_tile(pin='2')) + + self._assert_tanyao(player) + + discard = player.discard_tile() + self.assertEqual(self._to_string([discard]), '5p') + def test_choose_correct_waiting_and_first_opened_meld(self): tiles = self._string_to_136_array(man='2337788', sou='222', pin='234') self.player.init_hand(tiles) From bb9a19d8468ae84158df2e30a870b5defd155f67 Mon Sep 17 00:00:00 2001 From: Nihisil Date: Sat, 1 Dec 2018 21:59:27 +0700 Subject: [PATCH 10/10] Fix a bug with yakuhai strategy and tempai --- .../ai/first_version/strategies/yakuhai.py | 4 ++++ .../tests/strategies/tests_yakuhai.py | 19 +++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/project/game/ai/first_version/strategies/yakuhai.py b/project/game/ai/first_version/strategies/yakuhai.py index 6cb3698a..a6a10a92 100644 --- a/project/game/ai/first_version/strategies/yakuhai.py +++ b/project/game/ai/first_version/strategies/yakuhai.py @@ -101,7 +101,11 @@ def determine_what_to_discard(self, discard_options, hand, open_melds): tiles_34 = TilesConverter.to_34_array(hand) valued_pairs = [x for x in self.player.valued_honors if tiles_34[x] == 2] + + # closed pon sets valued_pons = [x for x in self.player.valued_honors if tiles_34[x] == 3] + # open pon sets + valued_pons += [x for x in open_melds if x.type == Meld.PON and x.tiles[0] // 4 in self.player.valued_honors] acceptable_options = [] for item in discard_options: diff --git a/project/game/ai/first_version/tests/strategies/tests_yakuhai.py b/project/game/ai/first_version/tests/strategies/tests_yakuhai.py index 3eedd088..543dc2a9 100644 --- a/project/game/ai/first_version/tests/strategies/tests_yakuhai.py +++ b/project/game/ai/first_version/tests/strategies/tests_yakuhai.py @@ -524,3 +524,22 @@ def test_atodzuke_choose_hidden_syanpon(self): player.draw_tile(self._string_to_136_tile(man='6')) discarded_tile = player.discard_tile() self.assertNotEqual(self._to_string([discarded_tile]), '6m') + + def test_tempai_with_open_yakuhai_meld_and_yakuhai_pair_in_the_hand(self): + """ + there was a bug where bot didn't handle tempai properly + with opened yakuhai pon and pair in the hand + 56m555p6678s55z + [777z] + """ + table = Table() + player = table.player + + tiles = self._string_to_136_array(man='56', pin='555', sou='667', honors='55777') + player.init_hand(tiles) + player.add_called_meld(self._make_meld(Meld.PON, honors='777')) + player.draw_tile(self._string_to_136_tile(sou='8')) + + player.ai.current_strategy = YakuhaiStrategy(BaseStrategy.YAKUHAI, player) + + discarded_tile = player.discard_tile() + self.assertEqual(self._to_string([discarded_tile]), '6s')