From be9b70c3d75d6595ae03ea0679100297c1c6a316 Mon Sep 17 00:00:00 2001 From: Nihisil Date: Tue, 21 Aug 2018 00:57:45 +0800 Subject: [PATCH 001/126] Make sleeps smarter during the game --- project/tenhou/client.py | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/project/tenhou/client.py b/project/tenhou/client.py index dd102d4d..91ffa2b2 100644 --- a/project/tenhou/client.py +++ b/project/tenhou/client.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- import datetime import logging +import random import socket from threading import Thread from time import sleep @@ -20,8 +21,6 @@ class TenhouClient(Client): - SLEEP_BETWEEN_ACTIONS = 1 - statistics = None socket = None game_is_continue = True @@ -44,7 +43,7 @@ def connect(self): # for reproducer if self._socket_mock: self.socket = self._socket_mock - TenhouClient.SLEEP_BETWEEN_ACTIONS = 0 + TenhouClient._random_sleep = lambda x, y: 0 else: self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) @@ -121,18 +120,18 @@ def start_game(self): if settings.IS_TOURNAMENT: logger.info('Go to the tournament lobby: {}'.format(settings.LOBBY)) self._send_message(''.format(settings.LOBBY)) - sleep(TenhouClient.SLEEP_BETWEEN_ACTIONS * 2) + self._random_sleep() self._send_message('') else: logger.info('Go to the lobby: {}'.format(settings.LOBBY)) self._send_message(''.format(quote('/lobby {}'.format(settings.LOBBY)))) - sleep(TenhouClient.SLEEP_BETWEEN_ACTIONS * 2) + self._random_sleep() if self.reconnected_messages: # we already in the game self.looking_for_game = False self._send_message('') - sleep(TenhouClient.SLEEP_BETWEEN_ACTIONS) + self._random_sleep() else: selected_game_type = self._build_game_type() game_type = '{},{}'.format(settings.LOBBY, selected_game_type) @@ -144,7 +143,7 @@ def start_game(self): start_time = datetime.datetime.now() while self.looking_for_game: - sleep(TenhouClient.SLEEP_BETWEEN_ACTIONS) + self._random_sleep() messages = self._get_multiple_messages() @@ -207,7 +206,7 @@ def start_game(self): tile_to_discard = None while self.game_is_continue: - sleep(TenhouClient.SLEEP_BETWEEN_ACTIONS) + self._random_sleep() messages = self._get_multiple_messages() if self.reconnected_messages: @@ -275,7 +274,7 @@ def start_game(self): logger.info('Hand: {}'.format(main_player.format_hand_for_print(drawn_tile))) self.player.draw_tile(drawn_tile) - sleep(TenhouClient.SLEEP_BETWEEN_ACTIONS) + self._random_sleep(2, 4) kan_type = self.player.should_call_kan(drawn_tile, False) if kan_type and self.table.count_of_remaining_tiles > 1: @@ -295,7 +294,7 @@ def start_game(self): # let's call riichi if can_call_riichi: self._send_message(''.format(discarded_tile)) - sleep(TenhouClient.SLEEP_BETWEEN_ACTIONS) + self._random_sleep() main_player.in_riichi = True else: # we had to add it to discards, to calculate remaining tiles correctly @@ -320,7 +319,7 @@ def start_game(self): # the end of round if '') # set was called @@ -354,7 +353,7 @@ def start_game(self): if any(i in message for i in win_suggestions): tile = self.decoder.parse_tile(message) enemy_seat = self.decoder.get_enemy_seat(message) - sleep(TenhouClient.SLEEP_BETWEEN_ACTIONS) + self._random_sleep(4, 7) if main_player.should_call_win(tile, enemy_seat): self._send_message('') @@ -373,6 +372,8 @@ def start_game(self): # open hand suggestions if 't=' in message: + self._random_sleep(2, 5) + # Possible t="" suggestions # 1 pon # 2 kan (it is a closed kan and can be send only to the self draw) @@ -416,7 +417,6 @@ def start_game(self): )) # this meld will not improve our hand else: - sleep(TenhouClient.SLEEP_BETWEEN_ACTIONS) self._send_message('') if 'owari' in message: @@ -572,3 +572,6 @@ def _set_game_rules(self, game_type): logger.info('Game type: {}'.format(is_hanchan and 'hanchan' or 'tonpusen')) return True + + def _random_sleep(self, min_sleep=1, max_sleep=3): + sleep(random.randint(min_sleep, max_sleep + 1)) From 873d2ef41230ab357e1cf6445b67567d69303a5c Mon Sep 17 00:00:00 2001 From: Nihisil Date: Tue, 21 Aug 2018 00:59:12 +0800 Subject: [PATCH 002/126] Increase bot AI version --- project/game/ai/first_version/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/game/ai/first_version/main.py b/project/game/ai/first_version/main.py index 872d8c88..eea925d2 100644 --- a/project/game/ai/first_version/main.py +++ b/project/game/ai/first_version/main.py @@ -23,7 +23,7 @@ class ImplementationAI(InterfaceAI): - version = '0.3.2' + version = '0.4.0-dev' agari = None shanten = None From 19f5fd62b89b435384ec7d3df61b52f811b8bd95 Mon Sep 17 00:00:00 2001 From: Nihisil Date: Tue, 21 Aug 2018 01:01:00 +0800 Subject: [PATCH 003/126] Fix tests --- project/tenhou/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/tenhou/client.py b/project/tenhou/client.py index 91ffa2b2..f46b7276 100644 --- a/project/tenhou/client.py +++ b/project/tenhou/client.py @@ -43,7 +43,7 @@ def connect(self): # for reproducer if self._socket_mock: self.socket = self._socket_mock - TenhouClient._random_sleep = lambda x, y: 0 + TenhouClient._random_sleep = lambda x, y=0, z=0: 0 else: self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) From 821c8df4f2430476d8dcf97ea65cac49f98af5ec Mon Sep 17 00:00:00 2001 From: Nihisil Date: Wed, 22 Aug 2018 21:16:35 +0800 Subject: [PATCH 004/126] Improve hand building logic. Introduce second level ukeire --- project/game/ai/discard.py | 28 +++- project/game/ai/first_version/main.py | 149 +++++++++++++----- .../ai/first_version/tests/tests_discards.py | 69 +++++++- .../first_version/tests/tests_strategies.py | 10 +- project/requirements.txt | 2 +- project/tenhou/client.py | 22 ++- project/tenhou/tests/tests_client.py | 82 ---------- 7 files changed, 223 insertions(+), 139 deletions(-) delete mode 100644 project/tenhou/tests/tests_client.py diff --git a/project/game/ai/discard.py b/project/game/ai/discard.py index 0b460b54..b79cc336 100644 --- a/project/game/ai/discard.py +++ b/project/game/ai/discard.py @@ -3,6 +3,8 @@ from mahjong.tile import TilesConverter from mahjong.utils import is_honor, simplify, plus_dora, is_aka_dora +from game.ai.first_version.strategies.main import BaseStrategy + class DiscardOption(object): player = None @@ -13,6 +15,7 @@ class DiscardOption(object): waiting = None # how much tiles will improve our hand tiles_count = None + tiles_count_second_level = None # number of shanten for that tile shanten = None # sometimes we had to force tile to be discarded @@ -36,12 +39,26 @@ def __init__(self, player, tile_to_discard, shanten, waiting, tiles_count, dange self.shanten = shanten self.waiting = waiting self.tiles_count = tiles_count + self.tiles_count_second_level = 0 + self.count_of_dora = 0 self.danger = danger self.had_to_be_saved = False self.had_to_be_discarded = False self.calculate_value() + def __unicode__(self): + tile_format_136 = TilesConverter.to_one_line_string([self.tile_to_discard*4]) + return 'tile={}, ukeire={}, ukeire2={}, valuation={}'.format( + tile_format_136, + self.tiles_count, + self.tiles_count_second_level, + self.valuation + ) + + def __repr__(self): + return '{}'.format(self.__unicode__()) + def find_tile_in_hand(self, closed_hand): """ Find and return 136 tile in closed player hand @@ -84,8 +101,12 @@ def calculate_value(self, shanten=None): # for west-west, east-east we had to double tile value value += honored_value * len(count_of_winds) else: - # suits - suit_tile_grades = [10, 20, 30, 40, 50, 40, 30, 20, 10] + # aim for tanyao + if self.player.ai.current_strategy and self.player.ai.current_strategy.type == BaseStrategy.TANYAO: + suit_tile_grades = [10, 20, 30, 50, 40, 50, 30, 20, 10] + # usual hand + else: + suit_tile_grades = [10, 20, 40, 50, 30, 50, 40, 20, 10] simplified_tile = simplify(self.tile_to_discard) value += suit_tile_grades[simplified_tile] @@ -93,7 +114,8 @@ def calculate_value(self, shanten=None): if is_aka_dora(self.tile_to_discard * 4, self.player.table.has_open_tanyao): count_of_dora += 1 - value += 50 * count_of_dora + self.count_of_dora = count_of_dora + value += count_of_dora * 50 if is_honor(self.tile_to_discard): # depends on how much honor tiles were discarded diff --git a/project/game/ai/first_version/main.py b/project/game/ai/first_version/main.py index eea925d2..39daf41b 100644 --- a/project/game/ai/first_version/main.py +++ b/project/game/ai/first_version/main.py @@ -1,15 +1,16 @@ # -*- coding: utf-8 -*- +import copy import logging from mahjong.agari import Agari -from mahjong.constants import AKA_DORA_LIST, CHUN, HAKU, HATSU +from mahjong.constants import AKA_DORA_LIST from mahjong.hand_calculating.divider import HandDivider from mahjong.hand_calculating.hand import HandCalculator from mahjong.hand_calculating.hand_config import HandConfig from mahjong.meld import Meld from mahjong.shanten import Shanten from mahjong.tile import TilesConverter -from mahjong.utils import is_pair, is_pon +from mahjong.utils import is_pair, is_pon, is_tile_strictly_isolated from game.ai.base.main import InterfaceAI from game.ai.discard import DiscardOption @@ -119,7 +120,7 @@ def process_discard_options_and_select_tile_to_discard(self, results, shanten, h None, had_was_open) - return self.chose_tile_to_discard(results) + return self.choose_tile_to_discard(results) def calculate_outs(self, tiles, closed_hand, open_sets_34=None): """ @@ -219,50 +220,92 @@ def determine_strategy(self): return self.current_strategy and True or False - def chose_tile_to_discard(self, results: [DiscardOption]) -> DiscardOption: + def choose_tile_to_discard(self, results: [DiscardOption]) -> DiscardOption: """ - Try to find best tile to discard, based on different valuations + Try to find best tile to discard, based on different rules """ - def sorting(x): - # - is important for x.tiles_count - # in that case we will discard tile that will give for us more tiles - # to complete a hand - return x.shanten, -x.tiles_count, x.valuation - had_to_be_discarded_tiles = [x for x in results if x.had_to_be_discarded] if had_to_be_discarded_tiles: - had_to_be_discarded_tiles = sorted(had_to_be_discarded_tiles, key=sorting) - selected_tile = had_to_be_discarded_tiles[0] - else: - results = sorted(results, key=sorting) - # remove needed tiles from discard options - results = [x for x in results if not x.had_to_be_saved] - - # let's chose most valuable tile first - temp_tile = results[0] - # and let's find all tiles with same shanten - results_with_same_shanten = [x for x in results if x.shanten == temp_tile.shanten] - possible_options = [temp_tile] - for discard_option in results_with_same_shanten: - # there is no sense to check already chosen tile - if discard_option.tile_to_discard == temp_tile.tile_to_discard: - continue + return sorted(had_to_be_discarded_tiles, key=lambda x: (x.shanten, -x.tiles_count, x.valuation))[0] - # we don't need to select tiles almost dead waits - if discard_option.tiles_count <= 2: - continue + # remove needed tiles from discard options + results = [x for x in results if not x.had_to_be_saved] - # let's check all other tiles with same shanten - # maybe we can find tiles that have almost same tiles count number - if temp_tile.tiles_count - 2 < discard_option.tiles_count < temp_tile.tiles_count + 2: - possible_options.append(discard_option) + results = sorted(results, key=lambda x: (x.shanten, -x.tiles_count)) + first_option = results[0] + results_with_same_shanten = [x for x in results if x.shanten == first_option.shanten] + + possible_options = [first_option] + border_percentage = 20 + for discard_option in results_with_same_shanten: + # there is no sense to check already chosen tile + if discard_option.tile_to_discard == first_option.tile_to_discard: + continue + + # we don't need to select tiles almost dead waits + if discard_option.tiles_count <= 2: + continue - # let's sort got tiles by value and let's chose less valuable tile to discard - possible_options = sorted(possible_options, key=lambda x: x.valuation) - selected_tile = possible_options[0] + tiles_count_borders = round((first_option.tiles_count / 100) * border_percentage) - return selected_tile + if first_option.shanten == 0 and tiles_count_borders < 2: + tiles_count_borders = 2 + + if first_option.shanten == 1 and tiles_count_borders < 4: + tiles_count_borders = 4 + + if first_option.shanten >= 2 and tiles_count_borders < 8: + tiles_count_borders = 8 + + # let's choose tiles that are close to the max ukeire tile + if discard_option.tiles_count >= first_option.tiles_count - tiles_count_borders: + possible_options.append(discard_option) + + if first_option.shanten <= 1: + # let's sort tiles by value and let's choose less valuable tile to discard + return sorted(possible_options, key=lambda x: x.valuation)[0] + + # as second step + # let's choose tiles that are close to the max ukeire2 tile + for x in possible_options: + self.calculate_second_level_tiles_count(x) + + possible_options = sorted(possible_options, key=lambda x: -x.tiles_count_second_level) + + filter_percentage = 20 + filtered_options = self._filter_list_by_percentage( + possible_options, + 'tiles_count_second_level', + filter_percentage + ) + + dora_tiles = [x for x in filtered_options if x.count_of_dora > 0] + # we have only dora candidates to discard + if len(dora_tiles) == len(filtered_options): + min_dora = min([x.count_of_dora for x in filtered_options]) + min_dora_list = [x for x in filtered_options if x.count_of_dora == min_dora] + + # let's discard tile with greater ukeire2 + return sorted(min_dora_list, key=lambda x: -x.tiles_count_second_level)[0] + + second_filter_percentage = 10 + filtered_options = self._filter_list_by_percentage( + filtered_options, + 'tiles_count_second_level', + second_filter_percentage + ) + + closed_hand_34 = TilesConverter.to_34_array(self.player.closed_hand) + isolated_tiles = [x for x in filtered_options if is_tile_strictly_isolated(closed_hand_34, x.tile_to_discard)] + # isolated tiles should be discarded first + if isolated_tiles: + # let's sort tiles by value and let's choose less valuable tile to discard + return sorted(isolated_tiles, key=lambda x: x.valuation)[0] + + # there are no isolated tiles + # let's discard tile with greater ukeire2 + return sorted(filtered_options, key=lambda x: -x.tiles_count_second_level)[0] def process_discard_option(self, discard_option, closed_hand, force_discard=False): self.waiting = discard_option.waiting @@ -423,9 +466,39 @@ def enemy_called_riichi(self, enemy_seat): if self.defence.should_go_to_defence_mode(): self.in_defence = True + def calculate_second_level_tiles_count(self, discard_option): + tiles = copy.copy(self.player.tiles) + tiles.remove(discard_option.find_tile_in_hand(self.player.closed_hand)) + + sum_tiles = 0 + for wait in discard_option.waiting: + wait = wait * 4 + tiles.append(wait) + + results, shanten = self.calculate_outs( + tiles, + self.player.closed_hand, + self.player.open_hand_34_tiles + ) + results = [x for x in results if x.shanten == discard_option.shanten - 1] + sum_tiles += sum([x.tiles_count for x in results]) + + tiles.remove(wait) + + discard_option.tiles_count_second_level = sum_tiles + @property def enemy_players(self): """ Return list of players except our bot """ return self.player.table.players[1:] + + def _filter_list_by_percentage(self, items, attribute, percentage): + filtered_options = [] + first_option = items[0] + tiles_count_borders = round((getattr(first_option, attribute) / 100) * percentage) + for x in items: + if getattr(x, attribute) >= getattr(first_option, attribute) - tiles_count_borders: + filtered_options.append(x) + return filtered_options diff --git a/project/game/ai/first_version/tests/tests_discards.py b/project/game/ai/first_version/tests/tests_discards.py index 88aa1d52..2a5f3941 100644 --- a/project/game/ai/first_version/tests/tests_discards.py +++ b/project/game/ai/first_version/tests/tests_discards.py @@ -5,6 +5,8 @@ from mahjong.tests_mixin import TestMixin from game.ai.discard import DiscardOption +from game.ai.first_version.strategies.main import BaseStrategy +from game.ai.first_version.strategies.tanyao import TanyaoStrategy from game.table import Table @@ -66,6 +68,32 @@ 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_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 @@ -73,9 +101,9 @@ def test_calculate_suit_tiles_value(self): [0, 110], [9, 110], [18, 110], [1, 120], [10, 120], [19, 120], [2, 130], [11, 130], [20, 130], - [3, 140], [12, 140], [21, 140], - [4, 150], [13, 150], [22, 150], - [5, 140], [14, 140], [23, 140], + [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] @@ -203,3 +231,38 @@ def test_prefer_valuable_tiles_with_almost_same_tiles_count(self): discarded_tile = player.discard_tile() self.assertEqual(self._to_string([discarded_tile]), '1s') + + def test_discard_less_valuable_isolated_tile_first(self): + table = Table() + player = table.player + table.add_dora_indicator(self._string_to_136_tile(sou='4')) + + tiles = self._string_to_136_array(sou='2456', pin='129', man='234458') + player.init_hand(tiles) + player.draw_tile(self._string_to_136_tile(sou='7')) + + discarded_tile = player.discard_tile() + # we have a choice what to discard: 9p or 8m + # 9p is less valuable + self.assertEqual(self._to_string([discarded_tile]), '9p') + + table.dora_indicators.append(self._string_to_136_tile(pin='8')) + tiles = self._string_to_136_array(sou='2456', pin='129', man='234458') + player.init_hand(tiles) + player.draw_tile(self._string_to_136_tile(sou='7')) + discarded_tile = player.discard_tile() + # but if 9p is dora + # let's discard 8m instead + self.assertEqual(self._to_string([discarded_tile]), '8m') + + def test_discard_tile_with_max_ukeire_second_level(self): + table = Table() + player = table.player + table.add_dora_indicator(self._string_to_136_tile(sou='4')) + + tiles = self._string_to_136_array(sou='11367', pin='45', man='344778') + player.init_hand(tiles) + player.draw_tile(self._string_to_136_tile(pin='6')) + + discarded_tile = player.discard_tile() + self.assertEqual(self._to_string([discarded_tile]), '3s') diff --git a/project/game/ai/first_version/tests/tests_strategies.py b/project/game/ai/first_version/tests/tests_strategies.py index d9bae4b7..89f1a600 100644 --- a/project/game/ai/first_version/tests/tests_strategies.py +++ b/project/game/ai/first_version/tests/tests_strategies.py @@ -222,7 +222,6 @@ def test_atodzuke_opened_hand(self): table = Table() player = table.player - # 456m12355p22z + 5p [678s] tiles = self._string_to_136_array(sou='4589', pin='123', man='1236', honors='66') player.init_hand(tiles) @@ -312,19 +311,22 @@ def test_open_hand_and_discard_tiles_logic(self): table = Table() player = table.player - tiles = self._string_to_136_array(sou='112235589', man='24', honors='22') + tiles = self._string_to_136_array(sou='112235589', man='23', honors='22') player.init_hand(tiles) # we don't need to call meld even if it improves our hand, - # because we are collecting honitsu + # because we are aim for honitsu tile = self._string_to_136_tile(man='1') meld, _ = player.try_to_call_meld(tile, False) self.assertEqual(meld, None) # any honor tile is suitable tile = self._string_to_136_tile(honors='2') - meld, _ = player.try_to_call_meld(tile, False) + meld, tile_to_discard = player.try_to_call_meld(tile, False) self.assertNotEqual(meld, None) + self.assertEqual(self._to_string([tile_to_discard * 4]), '2m') + + player.discard_tile(tile_to_discard * 4) tile = self._string_to_136_tile(man='1') player.draw_tile(tile) diff --git a/project/requirements.txt b/project/requirements.txt index 73bea36d..91c6f81f 100644 --- a/project/requirements.txt +++ b/project/requirements.txt @@ -1,3 +1,3 @@ -mahjong==1.1.1 +mahjong==1.1.3 requests==2.18.4 flake8==3.4.1 \ No newline at end of file diff --git a/project/tenhou/client.py b/project/tenhou/client.py index f46b7276..abf26d82 100644 --- a/project/tenhou/client.py +++ b/project/tenhou/client.py @@ -146,13 +146,13 @@ def start_game(self): self._random_sleep() messages = self._get_multiple_messages() - for message in messages: if ''.format(game_type)) if '') self._send_message('') @@ -206,7 +206,8 @@ def start_game(self): tile_to_discard = None while self.game_is_continue: - self._random_sleep() + self._random_sleep(1, 2) + messages = self._get_multiple_messages() if self.reconnected_messages: @@ -258,12 +259,14 @@ def start_game(self): win_suggestions = ['t="16"', 't="48"'] # we won by self draw (tsumo) if any(i in message for i in win_suggestions): + self._random_sleep(1, 2) self._send_message('') continue # Kyuushuu kyuuhai 「九種九牌」 # (9 kinds of honor or terminal tiles) if 't="64"' in message: + self._random_sleep(1, 2) # TODO aim for kokushi self._send_message('') continue @@ -274,10 +277,11 @@ def start_game(self): logger.info('Hand: {}'.format(main_player.format_hand_for_print(drawn_tile))) self.player.draw_tile(drawn_tile) - self._random_sleep(2, 4) kan_type = self.player.should_call_kan(drawn_tile, False) if kan_type and self.table.count_of_remaining_tiles > 1: + self._random_sleep() + if kan_type == Meld.CHANKAN: meld_type = 5 else: @@ -293,8 +297,8 @@ def start_game(self): # let's call riichi if can_call_riichi: - self._send_message(''.format(discarded_tile)) self._random_sleep() + self._send_message(''.format(discarded_tile)) main_player.in_riichi = True else: # we had to add it to discards, to calculate remaining tiles correctly @@ -319,7 +323,7 @@ def start_game(self): # the end of round if '') # set was called @@ -353,7 +357,7 @@ def start_game(self): if any(i in message for i in win_suggestions): tile = self.decoder.parse_tile(message) enemy_seat = self.decoder.get_enemy_seat(message) - self._random_sleep(4, 7) + self._random_sleep(2, 4) if main_player.should_call_win(tile, enemy_seat): self._send_message('') @@ -372,8 +376,6 @@ def start_game(self): # open hand suggestions if 't=' in message: - self._random_sleep(2, 5) - # Possible t="" suggestions # 1 pon # 2 kan (it is a closed kan and can be send only to the self draw) @@ -385,6 +387,8 @@ def start_game(self): # should we call a kan? if 't="3"' in message or 't="7"' in message: if self.player.should_call_kan(tile, True): + self._random_sleep() + # 2 is open kan self._send_message('') logger.info('We called an open kan set!') @@ -397,6 +401,8 @@ def start_game(self): meld, tile_to_discard = self.player.try_to_call_meld(tile, is_kamicha_discard) if meld: + self._random_sleep(2, 3) + meld_tile = tile # 1 is pon diff --git a/project/tenhou/tests/tests_client.py b/project/tenhou/tests/tests_client.py deleted file mode 100644 index c0a107ce..00000000 --- a/project/tenhou/tests/tests_client.py +++ /dev/null @@ -1,82 +0,0 @@ -# -*- coding: utf-8 -*- -import unittest - -from reproducer import TenhouLogReproducer, SocketMock -from tenhou.client import TenhouClient -from tenhou.decoder import TenhouDecoder, Meld - - -class TenhouClientTestCase(unittest.TestCase): - - def setUp(self): - self.client = None - - def tearDown(self): - self.client.end_game(False) - - def test_fixed_crash_after_called_kan(self): - log = """ - Get: - Get: - Get: - Get: - Get: - Get: - Get: - Get: - Get: - Get: - Get: - Get: - Get: - Get: - Get: - Get: - Get: - Get: - Get: - Get: - Get: - Get: - Get: - Get: - Get: - Get: - Get: - Get: - Get: - Get: - Get: - Get: - Get: - Get: - Get: - Get: - Get: - Get: - Get: - Get: - Get: - Get: - Get: - Get: - Get: - Get: - Get: - Get: - Get: - Get: - Get: - """ - - self.client = TenhouClient(SocketMock(None, log)) - with self.assertRaises(KeyboardInterrupt) as context: - self.client.connect() - self.client.authenticate() - self.client.start_game() - - # close all threads - self.client.end_game() - - # end of commands is correct way to end log reproducing - self.assertTrue('End of commands' in str(context.exception)) From ae8b30ad37e7aa7c532e0967c20987cfe6f0e835 Mon Sep 17 00:00:00 2001 From: Nihisil Date: Wed, 22 Aug 2018 21:20:49 +0800 Subject: [PATCH 005/126] Add Python 3.7 to the travis --- .travis.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.travis.yml b/.travis.yml index dad4a5be..480b8b8c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,10 @@ language: python +dist: xenial +sudo: true python: - "3.5" - "3.6" + - "3.7" before_script: - cd project install: "pip install -r project/requirements.txt" From 2c1dc510da4c0a62fd7861d534a13f0577f983c3 Mon Sep 17 00:00:00 2001 From: Nihisil Date: Fri, 24 Aug 2018 13:19:16 +0800 Subject: [PATCH 006/126] Improve yakuhai open hand strategy with new rules --- .../game/ai/first_version/strategies/main.py | 2 + .../ai/first_version/strategies/yakuhai.py | 64 +- .../tests/strategies/__init__.py | 0 .../tests/strategies/tests_honitsu.py | 145 ++++ .../tests/strategies/tests_tanyao.py | 267 +++++++ .../tests/strategies/tests_yakuhai.py | 289 ++++++++ .../game/ai/first_version/tests/tests_ai.py | 3 + .../first_version/tests/tests_strategies.py | 652 ------------------ project/game/player.py | 5 + 9 files changed, 772 insertions(+), 655 deletions(-) create mode 100644 project/game/ai/first_version/tests/strategies/__init__.py create mode 100644 project/game/ai/first_version/tests/strategies/tests_honitsu.py create mode 100644 project/game/ai/first_version/tests/strategies/tests_tanyao.py create mode 100644 project/game/ai/first_version/tests/strategies/tests_yakuhai.py delete mode 100644 project/game/ai/first_version/tests/tests_strategies.py diff --git a/project/game/ai/first_version/strategies/main.py b/project/game/ai/first_version/strategies/main.py index f062d6fe..84e7c787 100644 --- a/project/game/ai/first_version/strategies/main.py +++ b/project/game/ai/first_version/strategies/main.py @@ -19,10 +19,12 @@ class BaseStrategy(object): type = None # number of shanten where we can start to open hand min_shanten = 7 + go_for_atodzuke = False def __init__(self, strategy_type, player): self.type = strategy_type self.player = player + self.go_for_atodzuke = False def __str__(self): return self.TYPES[self.type] diff --git a/project/game/ai/first_version/strategies/yakuhai.py b/project/game/ai/first_version/strategies/yakuhai.py index 6d9d941f..ff40e83a 100644 --- a/project/game/ai/first_version/strategies/yakuhai.py +++ b/project/game/ai/first_version/strategies/yakuhai.py @@ -1,11 +1,20 @@ # -*- coding: utf-8 -*- from mahjong.meld import Meld from mahjong.tile import TilesConverter +from mahjong.utils import plus_dora, is_aka_dora from game.ai.first_version.strategies.main import BaseStrategy class YakuhaiStrategy(BaseStrategy): + valued_pairs = None + has_valued_pon = None + + def __init__(self, strategy_type, player): + super().__init__(strategy_type, player) + + self.valued_pairs = [] + self.has_valued_pon = False def should_activate_strategy(self): """ @@ -17,12 +26,47 @@ def should_activate_strategy(self): return False tiles_34 = TilesConverter.to_34_array(self.player.tiles) - valued_pairs = [x for x in self.player.valued_honors if tiles_34[x] >= 2] + self.valued_pairs = [x for x in self.player.valued_honors if tiles_34[x] >= 2] + self.valued_pairs = list(set(self.valued_pairs)) + self.has_valued_pon = len([x for x in self.player.valued_honors if tiles_34[x] == 3]) > 1 + + has_valued_pair = False - for pair in valued_pairs: - # we have valued pair in the hand and there is enough tiles + for pair in self.valued_pairs: + # we have valued pair in the hand and there are enough tiles # in the wall if self.player.total_tiles(pair, tiles_34) < 4: + has_valued_pair = True + break + + # we don't have valuable pair to open our hand + if not has_valued_pair: + return False + + dora_count = sum([plus_dora(x, self.player.table.dora_indicators) for x in self.player.tiles]) + dora_count += sum([1 for x in self.player.tiles if is_aka_dora(x, self.player.table.has_open_tanyao)]) + + # If we have 1+ dora in the hand and there are 2+ valuable pairs let's open hand + if len(self.valued_pairs) >= 2 and dora_count >= 1: + return True + + # If we have 2+ dora in the hand let's open hand + if dora_count >= 2: + for x in range(0, 34): + # we have other pair in the hand + # so we can open hand for atodzuke + if tiles_34[x] >= 2 and x not in self.valued_pairs: + self.go_for_atodzuke = True + return True + + # If we have 1+ dora in the hand and there is 5+ round step let's open hand + if dora_count >= 1 and self.player.round_step > 5: + return True + + for pair in self.valued_pairs: + # this valuable tile was discarded once + # let's open on it in that case + if self.player.total_tiles(pair, tiles_34) == 3 and self.player.ai.previous_shanten > 1: return True return False @@ -96,5 +140,19 @@ def meld_had_to_be_called(self, tile): return False + def try_to_call_meld(self, tile, is_kamicha_discard): + if self.has_valued_pon: + return super(YakuhaiStrategy, self).try_to_call_meld(tile, is_kamicha_discard) + + tile_34 = tile // 4 + # we will open hand for atodzuke only in the special cases + if not self.player.is_open_hand and tile_34 not in self.valued_pairs: + if self.go_for_atodzuke: + return super(YakuhaiStrategy, self).try_to_call_meld(tile, is_kamicha_discard) + + return None, None + + return super(YakuhaiStrategy, self).try_to_call_meld(tile, is_kamicha_discard) + def _is_yakuhai_pon(self, meld): return meld.type == Meld.PON and meld.tiles[0] // 4 in self.player.valued_honors diff --git a/project/game/ai/first_version/tests/strategies/__init__.py b/project/game/ai/first_version/tests/strategies/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/project/game/ai/first_version/tests/strategies/tests_honitsu.py b/project/game/ai/first_version/tests/strategies/tests_honitsu.py new file mode 100644 index 00000000..d672dd02 --- /dev/null +++ b/project/game/ai/first_version/tests/strategies/tests_honitsu.py @@ -0,0 +1,145 @@ +# -*- coding: utf-8 -*- +import unittest + +from mahjong.tests_mixin import TestMixin + +from game.ai.first_version.strategies.honitsu import HonitsuStrategy +from game.ai.first_version.strategies.main import BaseStrategy +from game.table import Table + + +class HonitsuStrategyTestCase(unittest.TestCase, TestMixin): + + def test_should_activate_strategy(self): + table = Table() + player = table.player + strategy = HonitsuStrategy(BaseStrategy.HONITSU, player) + + tiles = self._string_to_136_array(sou='12355', man='12389', honors='123') + player.init_hand(tiles) + self.assertEqual(strategy.should_activate_strategy(), False) + + tiles = self._string_to_136_array(sou='12355', man='238', honors='11234') + player.init_hand(tiles) + self.assertEqual(strategy.should_activate_strategy(), True) + + # with hand without pairs we not should go for honitsu, + # because it is far away from tempai + tiles = self._string_to_136_array(sou='12358', man='238', honors='12345') + player.init_hand(tiles) + self.assertEqual(strategy.should_activate_strategy(), False) + + # with chitoitsu-like hand we don't need to go for honitsu + tiles = self._string_to_136_array(pin='77', man='3355677899', sou='11') + player.init_hand(tiles) + self.assertEqual(strategy.should_activate_strategy(), False) + + def test_suitable_tiles(self): + table = Table() + player = table.player + strategy = HonitsuStrategy(BaseStrategy.HONITSU, player) + + tiles = self._string_to_136_array(sou='12355', man='238', honors='11234') + player.init_hand(tiles) + self.assertEqual(strategy.should_activate_strategy(), True) + + tile = self._string_to_136_tile(man='1') + self.assertEqual(strategy.is_tile_suitable(tile), False) + + tile = self._string_to_136_tile(pin='1') + self.assertEqual(strategy.is_tile_suitable(tile), False) + + tile = self._string_to_136_tile(sou='1') + self.assertEqual(strategy.is_tile_suitable(tile), True) + + tile = self._string_to_136_tile(honors='1') + self.assertEqual(strategy.is_tile_suitable(tile), True) + + def test_open_hand_and_discard_tiles_logic(self): + table = Table() + player = table.player + + tiles = self._string_to_136_array(sou='112235589', man='23', honors='22') + player.init_hand(tiles) + + # we don't need to call meld even if it improves our hand, + # because we are aim for honitsu + tile = self._string_to_136_tile(man='1') + meld, _ = player.try_to_call_meld(tile, False) + self.assertEqual(meld, None) + + # any honor tile is suitable + tile = self._string_to_136_tile(honors='2') + meld, tile_to_discard = player.try_to_call_meld(tile, False) + self.assertNotEqual(meld, None) + self.assertEqual(self._to_string([tile_to_discard * 4]), '2m') + + player.discard_tile(tile_to_discard * 4) + + tile = self._string_to_136_tile(man='1') + player.draw_tile(tile) + tile_to_discard = player.discard_tile() + + # we are in honitsu mode, so we should discard man suits + self.assertEqual(self._to_string([tile_to_discard]), '1m') + + def test_riichi_and_tiles_from_another_suit_in_the_hand(self): + table = Table() + player = table.player + player.scores = 25000 + table.count_of_remaining_tiles = 100 + + tiles = self._string_to_136_array(man='33345678', pin='22', honors='155') + player.init_hand(tiles) + + player.draw_tile(self._string_to_136_tile(man='9')) + tile_to_discard = player.discard_tile() + + # we don't need to go for honitsu here + # we already in tempai + self.assertEqual(self._to_string([tile_to_discard]), '1z') + + def test_discard_not_needed_winds(self): + table = Table() + player = table.player + player.scores = 25000 + table.count_of_remaining_tiles = 100 + + tiles = self._string_to_136_array(man='24', pin='4', sou='12344668', honors='36') + player.init_hand(tiles) + player.draw_tile(self._string_to_136_tile(sou='5')) + + table.add_discarded_tile(1, self._string_to_136_tile(honors='3'), False) + table.add_discarded_tile(1, self._string_to_136_tile(honors='3'), False) + table.add_discarded_tile(1, self._string_to_136_tile(honors='3'), False) + + tile_to_discard = player.discard_tile() + + # west was discarded three times, we don't need it + self.assertEqual(self._to_string([tile_to_discard]), '3z') + + def test_discard_not_effective_tiles_first(self): + table = Table() + player = table.player + player.scores = 25000 + table.count_of_remaining_tiles = 100 + + tiles = self._string_to_136_array(man='33', pin='12788999', sou='5', honors='23') + player.init_hand(tiles) + player.draw_tile(self._string_to_136_tile(honors='6')) + tile_to_discard = player.discard_tile() + + self.assertEqual(self._to_string([tile_to_discard]), '5s') + + def test_dont_go_for_honitsu_with_ryanmen_in_other_suit(self): + table = Table() + player = table.player + strategy = HonitsuStrategy(BaseStrategy.HONITSU, player) + + tiles = self._string_to_136_array(man='14489', sou='45', pin='67', honors='44456') + player.init_hand(tiles) + + self.assertEqual(strategy.should_activate_strategy(), False) + + + diff --git a/project/game/ai/first_version/tests/strategies/tests_tanyao.py b/project/game/ai/first_version/tests/strategies/tests_tanyao.py new file mode 100644 index 00000000..ae3c7902 --- /dev/null +++ b/project/game/ai/first_version/tests/strategies/tests_tanyao.py @@ -0,0 +1,267 @@ +# -*- coding: utf-8 -*- +import unittest + +from mahjong.meld import Meld +from mahjong.tests_mixin import TestMixin + +from game.ai.first_version.strategies.main import BaseStrategy +from game.ai.first_version.strategies.tanyao import TanyaoStrategy +from game.table import Table + + +class TanyaoStrategyTestCase(unittest.TestCase, TestMixin): + + def _make_table(self): + table = Table() + table.has_open_tanyao = True + return table + + def test_should_activate_strategy_and_terminal_pon_sets(self): + table = self._make_table() + player = table.player + strategy = TanyaoStrategy(BaseStrategy.TANYAO, player) + + tiles = self._string_to_136_array(sou='234', man='3459', pin='233', honors='111') + player.init_hand(tiles) + self.assertEqual(strategy.should_activate_strategy(), False) + + tiles = self._string_to_136_array(sou='234', man='3459', pin='233999') + player.init_hand(tiles) + self.assertEqual(strategy.should_activate_strategy(), False) + + tiles = self._string_to_136_array(sou='234', man='3459', pin='233444') + player.init_hand(tiles) + self.assertEqual(strategy.should_activate_strategy(), True) + + def test_should_activate_strategy_and_terminal_pairs(self): + table = self._make_table() + player = table.player + strategy = TanyaoStrategy(BaseStrategy.TANYAO, player) + + tiles = self._string_to_136_array(sou='234', man='3459', pin='2399', honors='11') + player.init_hand(tiles) + self.assertEqual(strategy.should_activate_strategy(), False) + + tiles = self._string_to_136_array(sou='234', man='345669', pin='2399') + player.init_hand(tiles) + self.assertEqual(strategy.should_activate_strategy(), True) + + def test_should_activate_strategy_and_valued_pair(self): + table = self._make_table() + player = table.player + strategy = TanyaoStrategy(BaseStrategy.TANYAO, player) + + tiles = self._string_to_136_array(man='23446679', sou='345', honors='55') + player.init_hand(tiles) + self.assertEqual(strategy.should_activate_strategy(), False) + + tiles = self._string_to_136_array(man='23446679', sou='345', honors='22') + player.init_hand(tiles) + self.assertEqual(strategy.should_activate_strategy(), True) + + def test_should_activate_strategy_and_chitoitsu_like_hand(self): + table = self._make_table() + player = table.player + strategy = TanyaoStrategy(BaseStrategy.TANYAO, player) + + tiles = self._string_to_136_array(sou='223388', man='3344', pin='6687') + player.init_hand(tiles) + self.assertEqual(strategy.should_activate_strategy(), False) + + def test_should_activate_strategy_and_already_completed_sided_set(self): + table = self._make_table() + player = table.player + strategy = TanyaoStrategy(BaseStrategy.TANYAO, player) + + tiles = self._string_to_136_array(sou='123234', man='3459', pin='234') + player.init_hand(tiles) + self.assertEqual(strategy.should_activate_strategy(), False) + + tiles = self._string_to_136_array(sou='234789', man='3459', pin='234') + player.init_hand(tiles) + self.assertEqual(strategy.should_activate_strategy(), False) + + tiles = self._string_to_136_array(sou='234', man='1233459', pin='234') + player.init_hand(tiles) + self.assertEqual(strategy.should_activate_strategy(), False) + + tiles = self._string_to_136_array(sou='234', man='3457899', pin='234') + player.init_hand(tiles) + self.assertEqual(strategy.should_activate_strategy(), False) + + tiles = self._string_to_136_array(sou='234', man='3459', pin='122334') + player.init_hand(tiles) + self.assertEqual(strategy.should_activate_strategy(), False) + + tiles = self._string_to_136_array(sou='234', man='3459', pin='234789') + player.init_hand(tiles) + self.assertEqual(strategy.should_activate_strategy(), False) + + tiles = self._string_to_136_array(sou='223344', man='3459', pin='234') + player.init_hand(tiles) + self.assertEqual(strategy.should_activate_strategy(), True) + + def test_suitable_tiles(self): + table = self._make_table() + player = table.player + strategy = TanyaoStrategy(BaseStrategy.TANYAO, player) + + tile = self._string_to_136_tile(man='1') + self.assertEqual(strategy.is_tile_suitable(tile), False) + + tile = self._string_to_136_tile(pin='1') + self.assertEqual(strategy.is_tile_suitable(tile), False) + + tile = self._string_to_136_tile(sou='9') + self.assertEqual(strategy.is_tile_suitable(tile), False) + + tile = self._string_to_136_tile(honors='1') + self.assertEqual(strategy.is_tile_suitable(tile), False) + + tile = self._string_to_136_tile(honors='6') + self.assertEqual(strategy.is_tile_suitable(tile), False) + + tile = self._string_to_136_tile(man='2') + self.assertEqual(strategy.is_tile_suitable(tile), True) + + tile = self._string_to_136_tile(pin='5') + self.assertEqual(strategy.is_tile_suitable(tile), True) + + tile = self._string_to_136_tile(sou='8') + self.assertEqual(strategy.is_tile_suitable(tile), True) + + def test_dont_open_hand_with_high_shanten(self): + table = self._make_table() + player = table.player + + # with 4 shanten we don't need to aim for open tanyao + tiles = self._string_to_136_array(man='369', pin='378', sou='3488', honors='123') + tile = self._string_to_136_tile(sou='2') + player.init_hand(tiles) + meld, _ = player.try_to_call_meld(tile, False) + self.assertEqual(meld, None) + + # with 3 shanten we can open a hand + tiles = self._string_to_136_array(man='236', pin='378', sou='3488', honors='123') + tile = self._string_to_136_tile(sou='2') + player.init_hand(tiles) + meld, _ = player.try_to_call_meld(tile, True) + self.assertNotEqual(meld, None) + + def test_dont_open_hand_with_not_suitable_melds(self): + table = self._make_table() + player = table.player + + tiles = self._string_to_136_array(man='33355788', sou='3479', honors='3') + tile = self._string_to_136_tile(sou='8') + player.init_hand(tiles) + meld, _ = player.try_to_call_meld(tile, False) + self.assertEqual(meld, None) + + def test_open_hand_and_discard_tiles_logic(self): + table = self._make_table() + player = table.player + + # 2345779m1p256s44z + tiles = self._string_to_136_array(man='22345', sou='238', pin='256', honors='44') + player.init_hand(tiles) + + # if we are in tanyao + # we need to discard terminals and honors + tile = self._string_to_136_tile(sou='4') + meld, discard_option = player.try_to_call_meld(tile, True) + discarded_tile = table.player.discard_tile(discard_option) + self.assertNotEqual(meld, None) + self.assertEqual(self._to_string([discarded_tile]), '4z') + + tile = self._string_to_136_tile(pin='5') + player.draw_tile(tile) + tile_to_discard = player.discard_tile() + + # we are in tanyao, so we should discard honors and terminals + self.assertEqual(self._to_string([tile_to_discard]), '4z') + + def test_dont_count_pairs_in_already_opened_hand(self): + table = self._make_table() + player = table.player + + meld = self._make_meld(Meld.PON, sou='222') + player.add_called_meld(meld) + + tiles = self._string_to_136_array(man='33556788', sou='22266') + player.init_hand(tiles) + + tile = self._string_to_136_tile(sou='6') + meld, _ = player.try_to_call_meld(tile, False) + # even if it looks like chitoitsu we can open hand and get tempai here + self.assertNotEqual(meld, None) + + def test_we_cant_win_with_this_hand(self): + table = self._make_table() + + tiles = self._string_to_136_array(man='34577', sou='23', pin='233445') + table.player.init_hand(tiles) + meld = self._make_meld(Meld.CHI, pin='234') + table.player.add_called_meld(meld) + + table.player.draw_tile(self._string_to_136_tile(sou='1')) + discard = table.player.discard_tile() + # but for already open hand we cant do tsumo + # because we don't have a yaku here + # so, let's do tsumogiri + self.assertEqual(table.player.ai.previous_shanten, 0) + self.assertEqual(self._to_string([discard]), '1s') + + def test_choose_correct_waiting(self): + table = self._make_table() + player = table.player + + tiles = self._string_to_136_array(man='234678', sou='234', pin='3588') + player.init_hand(tiles) + player.draw_tile(self._string_to_136_tile(pin='2')) + + # discard 5p and riichi + discard = player.discard_tile() + self.assertEqual(self._to_string([discard]), '5p') + + table = self._make_table() + player = table.player + + meld = self._make_meld(Meld.CHI, man='234') + player.add_called_meld(meld) + + tiles = self._string_to_136_array(man='234678', sou='234', pin='3588') + player.init_hand(tiles) + player.draw_tile(self._string_to_136_tile(pin='2')) + + # it is not a good idea to wait on 1-4, since we can't win on 1 with open hand + # so let's continue to wait on 4 only + discard = player.discard_tile() + self.assertEqual(self._to_string([discard]), '2p') + + table = table = self._make_table() + player = table.player + + meld = self._make_meld(Meld.CHI, man='234') + player.add_called_meld(meld) + + tiles = self._string_to_136_array(man='234678', sou='234', pin='2388') + player.init_hand(tiles) + player.draw_tile(self._string_to_136_tile(sou='7')) + + # we can wait only on 1-4, so let's do it even if we can't get yaku on 1 + discard = player.discard_tile() + self.assertEqual(self._to_string([discard]), '7s') + + def test_choose_correct_waiting_and_fist_opened_meld(self): + table = self._make_table() + player = table.player + + tiles = self._string_to_136_array(man='2337788', sou='345', pin='234') + player.init_hand(tiles) + + tile = self._string_to_136_tile(man='8') + meld, tile_to_discard = player.try_to_call_meld(tile, False) + + discard = 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 new file mode 100644 index 00000000..64555dff --- /dev/null +++ b/project/game/ai/first_version/tests/strategies/tests_yakuhai.py @@ -0,0 +1,289 @@ +# -*- coding: utf-8 -*- +import unittest + +from mahjong.meld import Meld +from mahjong.tests_mixin import TestMixin +from mahjong.tile import Tile + +from game.ai.first_version.strategies.main import BaseStrategy +from game.ai.first_version.strategies.yakuhai import YakuhaiStrategy +from game.table import Table + + +class YakuhaiStrategyTestCase(unittest.TestCase, TestMixin): + + def setUp(self): + self.table = Table() + self.player = self.table.player + + def test_should_activate_strategy(self): + strategy = YakuhaiStrategy(BaseStrategy.YAKUHAI, self.player) + + tiles = self._string_to_136_array(sou='12355689', man='89', honors='123') + self.player.init_hand(tiles) + self.assertEqual(strategy.should_activate_strategy(), False) + + self.table.dora_indicators.append(self._string_to_136_tile(honors='7')) + tiles = self._string_to_136_array(sou='12355689', man='89', honors='55') + self.player.init_hand(tiles) + self.assertEqual(strategy.should_activate_strategy(), True) + + tiles = self._string_to_136_array(sou='12355689', man='89', honors='666') + self.player.init_hand(tiles) + self.assertEqual(strategy.should_activate_strategy(), True) + + # with chitoitsu-like hand we don't need to go for yakuhai + tiles = self._string_to_136_array(sou='1235566', man='8899', honors='66') + self.player.init_hand(tiles) + self.assertEqual(strategy.should_activate_strategy(), False) + + def test_dont_activate_strategy_if_we_dont_have_enough_tiles_in_the_wall(self): + strategy = YakuhaiStrategy(BaseStrategy.YAKUHAI, self.player) + + self.table.dora_indicators.append(self._string_to_136_tile(honors='7')) + tiles = self._string_to_136_array(man='59', sou='1235', pin='12789', honors='55') + self.player.init_hand(tiles) + + self.assertEqual(strategy.should_activate_strategy(), True) + + self.table.add_discarded_tile(3, self._string_to_136_tile(honors='5'), False) + self.table.add_discarded_tile(3, self._string_to_136_tile(honors='5'), False) + + # we can't complete yakuhai, because there is not enough honor tiles + self.assertEqual(strategy.should_activate_strategy(), False) + + def test_suitable_tiles(self): + strategy = YakuhaiStrategy(BaseStrategy.YAKUHAI, self.player) + + # for yakuhai we can use any tile + for tile in range(0, 136): + self.assertEqual(strategy.is_tile_suitable(tile), True) + + def test_force_yakuhai_pair_waiting_for_tempai_hand(self): + """ + If hand shanten = 1 don't open hand except the situation where is + we have tempai on yakuhai tile after open set + """ + self.table.dora_indicators.append(self._string_to_136_tile(man='3')) + tiles = self._string_to_136_array(sou='123', pin='678', man='34468', honors='66') + self.player.init_hand(tiles) + + # we will not get tempai on yakuhai pair with this hand, so let's skip this call + tile = self._string_to_136_tile(man='5') + meld, _ = self.player.try_to_call_meld(tile, False) + self.assertEqual(meld, None) + + # but here we will have atodzuke tempai + tile = self._string_to_136_tile(man='7') + meld, _ = self.player.try_to_call_meld(tile, True) + self.assertNotEqual(meld, None) + self.assertEqual(meld.type, Meld.CHI) + self.assertEqual(self._to_string(meld.tiles), '678m') + + self.table = Table() + self.player = self.table.player + + # we can open hand in that case + self.table.dora_indicators.append(self._string_to_136_tile(sou='5')) + tiles = self._string_to_136_array(man='44556', sou='366789', honors='77') + self.player.init_hand(tiles) + + tile = self._string_to_136_tile(honors='7') + meld, _ = self.player.try_to_call_meld(tile, True) + self.assertNotEqual(meld, None) + self.assertEqual(self._to_string(meld.tiles), '777z') + + def test_tempai_without_yaku(self): + tiles = self._string_to_136_array(sou='678', pin='12355', man='456', honors='77') + self.player.init_hand(tiles) + + tile = self._string_to_136_tile(pin='5') + self.player.draw_tile(tile) + meld = self._make_meld(Meld.CHI, sou='678') + self.player.add_called_meld(meld) + + discard = self.player.discard_tile() + self.assertEqual(self._to_string([discard]), '1p') + + def test_wrong_shanten_improvements_detection(self): + """ + With hand 2345s1p11z bot wanted to open set on 2s, + so after opened set we will get 25s1p11z + it is not correct logic, because we ruined our hand + :return: + """ + tiles = self._string_to_136_array(sou='2345999', honors='114446') + self.player.init_hand(tiles) + + meld = self._make_meld(Meld.PON, sou='999') + self.player.add_called_meld(meld) + meld = self._make_meld(Meld.PON, honors='444') + self.player.add_called_meld(meld) + + # to rebuild all caches + self.player.draw_tile(self._string_to_136_tile(pin='2')) + self.player.discard_tile() + + tile = self._string_to_136_tile(sou='2') + meld, _ = self.table.player.try_to_call_meld(tile, True) + self.assertEqual(meld, None) + + def test_open_hand_with_doras_in_the_hand(self): + """ + If we have valuable pair in the hand, and 2+ dora 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.table.dora_indicators.append(self._string_to_136_tile(pin='8')) + self.player.init_hand(tiles) + + # and now we can open hand on the valuable pair + 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_with_doras_in_the_hand_and_atodzuke(self): + """ + If we have valuable pair in the hand, and 2+ dora we can open hand on any tile + but only if we have other pair in the hand + """ + + tiles = self._string_to_136_array(man='59', sou='1235', pin='12788', honors='11') + self.player.init_hand(tiles) + + tile = self._string_to_136_tile(pin='3') + 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) + + # we have other pair in the hand, so we can open atodzuke here + tile = self._string_to_136_tile(pin='3') + meld, _ = self.player.try_to_call_meld(tile, True) + self.assertNotEqual(meld, None) + + 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_with_two_valuable_pairs(self): + """ + If we have two valuable pairs in the hand and 1+ dora + let's open on one of this valuable pairs + """ + + tiles = self._string_to_136_array(man='59', sou='12', pin='12789', honors='5566') + self.player.init_hand(tiles) + + tile = self._string_to_136_tile(honors='5') + 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='5') + meld, _ = self.player.try_to_call_meld(tile, True) + self.assertNotEqual(meld, None) + + tile = self._string_to_136_tile(honors='6') + 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_and_once_discarded_tile(self): + """ + If we have valuable pair in the hand, this tile was discarded once and we have 1+ shanten + let's open on this valuable pair + 5. Если в руке есть ценная пара, один тайл этой ценной пары уже вышел, в руке больше ишантена, то открываемся на этой ценной паре. + """ + + tiles = self._string_to_136_array(man='29', sou='1189', pin='12789', honors='11') + self.player.init_hand(tiles) + self.player.draw_tile(self._string_to_136_tile(man='6')) + # to calculate hand shanten number + self.player.discard_tile() + + tile = self._string_to_136_tile(honors='1') + meld, _ = self.player.try_to_call_meld(tile, True) + self.assertEqual(meld, None) + + self.table.add_discarded_tile(1, self._string_to_136_tile(honors='1'), False) + tiles = self._string_to_136_array(man='29', sou='1189', pin='12789', honors='11') + self.player.init_hand(tiles) + self.player.draw_tile(self._string_to_136_tile(man='6')) + self.player.discard_tile() + + 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_when_yakuhai_already_in_the_hand(self): + tiles = self._string_to_136_array(man='46', pin='4679', sou='1348', honors='111') + self.player.init_hand(tiles) + + tile = self._string_to_136_tile(sou='2') + meld, _ = self.player.try_to_call_meld(tile, True) + self.assertNotEqual(meld, None) diff --git a/project/game/ai/first_version/tests/tests_ai.py b/project/game/ai/first_version/tests/tests_ai.py index 8bb30aa7..01f958d7 100644 --- a/project/game/ai/first_version/tests/tests_ai.py +++ b/project/game/ai/first_version/tests/tests_ai.py @@ -75,6 +75,7 @@ def test_chose_right_set_to_open_hand(self): table = Table() player = table.player + table.dora_indicators.append(self._string_to_136_tile(honors='7')) tiles = self._string_to_136_array(man='335666', pin='22', sou='345', honors='55') player.init_hand(tiles) @@ -104,6 +105,7 @@ def test_not_open_hand_for_not_needed_set(self): table = Table() player = table.player + table.dora_indicators.append(self._string_to_136_tile(honors='7')) tiles = self._string_to_136_array(man='22457', sou='12234', pin='9', honors='55') player.init_hand(tiles) @@ -226,6 +228,7 @@ def test_using_tiles_of_different_suit_for_chi(self): player = table.player # 16m2679p1348s111z + table.dora_indicators.append(self._string_to_136_tile(honors='4')) tiles = [0, 21, 41, 56, 61, 70, 74, 80, 84, 102, 108, 110, 111] player.init_hand(tiles) diff --git a/project/game/ai/first_version/tests/tests_strategies.py b/project/game/ai/first_version/tests/tests_strategies.py deleted file mode 100644 index 89f1a600..00000000 --- a/project/game/ai/first_version/tests/tests_strategies.py +++ /dev/null @@ -1,652 +0,0 @@ -# -*- coding: utf-8 -*- -import unittest - -from mahjong.meld import Meld -from mahjong.tests_mixin import TestMixin - -from game.ai.first_version.strategies.honitsu import HonitsuStrategy -from game.ai.first_version.strategies.main import BaseStrategy -from game.ai.first_version.strategies.tanyao import TanyaoStrategy -from game.ai.first_version.strategies.yakuhai import YakuhaiStrategy -from game.table import Table - - -class YakuhaiStrategyTestCase(unittest.TestCase, TestMixin): - - def test_should_activate_strategy(self): - table = Table() - player = table.player - strategy = YakuhaiStrategy(BaseStrategy.YAKUHAI, player) - - tiles = self._string_to_136_array(sou='12355689', man='89', honors='123') - player.init_hand(tiles) - self.assertEqual(strategy.should_activate_strategy(), False) - - tiles = self._string_to_136_array(sou='12355689', man='89', honors='44') - player.init_hand(tiles) - player.dealer_seat = 1 - self.assertEqual(strategy.should_activate_strategy(), True) - - tiles = self._string_to_136_array(sou='12355689', man='89', honors='666') - player.init_hand(tiles) - self.assertEqual(strategy.should_activate_strategy(), True) - - # with chitoitsu-like hand we don't need to go for yakuhai - tiles = self._string_to_136_array(sou='1235566', man='8899', honors='66') - player.init_hand(tiles) - self.assertEqual(strategy.should_activate_strategy(), False) - - def test_dont_activate_strategy_if_we_dont_have_enough_tiles_in_the_wall(self): - table = Table() - player = table.player - strategy = YakuhaiStrategy(BaseStrategy.YAKUHAI, player) - - tiles = self._string_to_136_array(sou='12355689', man='89', honors='44') - player.init_hand(tiles) - player.dealer_seat = 1 - self.assertEqual(strategy.should_activate_strategy(), True) - - table.add_discarded_tile(3, self._string_to_136_tile(honors='4'), False) - table.add_discarded_tile(3, self._string_to_136_tile(honors='4'), False) - - # we can't complete yakuhai, because there is not enough honor tiles - self.assertEqual(strategy.should_activate_strategy(), False) - - def test_suitable_tiles(self): - table = Table() - player = table.player - strategy = YakuhaiStrategy(BaseStrategy.YAKUHAI, player) - - # for yakuhai we can use any tile - for tile in range(0, 136): - self.assertEqual(strategy.is_tile_suitable(tile), True) - - def test_open_hand_with_yakuhai_pair_in_hand(self): - table = Table() - player = table.player - - tiles = self._string_to_136_array(sou='123678', pin='25899', honors='44') - tile = self._string_to_136_tile(honors='4') - player.init_hand(tiles) - - # we don't need to open hand with not our wind - meld, _ = player.try_to_call_meld(tile, False) - self.assertEqual(meld, None) - - # with dragon pair in hand let's open our hand - tiles = self._string_to_136_array(sou='1689', pin='2358', man='1', honors='4455') - tile = self._string_to_136_tile(honors='4') - player.init_hand(tiles) - meld, _ = player.try_to_call_meld(tile, False) - self.assertNotEqual(meld, None) - player.add_called_meld(meld) - player.tiles.append(tile) - - self.assertEqual(meld.type, Meld.PON) - self.assertEqual(self._to_string(meld.tiles), '444z') - self.assertEqual(len(player.closed_hand), 11) - self.assertEqual(len(player.tiles), 14) - player.discard_tile() - - tile = self._string_to_136_tile(honors='5') - meld, _ = player.try_to_call_meld(tile, False) - self.assertNotEqual(meld, None) - player.add_called_meld(meld) - player.tiles.append(tile) - - self.assertEqual(meld.type, Meld.PON) - self.assertEqual(self._to_string(meld.tiles), '555z') - self.assertEqual(len(player.closed_hand), 8) - self.assertEqual(len(player.tiles), 14) - player.discard_tile() - - tile = self._string_to_136_tile(sou='7') - # we can call chi only from left player - meld, _ = player.try_to_call_meld(tile, False) - self.assertEqual(meld, None) - - meld, _ = player.try_to_call_meld(tile, True) - self.assertNotEqual(meld, None) - player.add_called_meld(meld) - player.tiles.append(tile) - - self.assertEqual(meld.type, Meld.CHI) - self.assertEqual(self._to_string(meld.tiles), '678s') - self.assertEqual(len(player.closed_hand), 5) - self.assertEqual(len(player.tiles), 14) - - def test_force_yakuhai_pair_waiting_for_tempai_hand(self): - """ - If hand shanten = 1 don't open hand except the situation where is - we have tempai on yakuhai tile after open set - """ - table = Table() - player = table.player - - tiles = self._string_to_136_array(sou='123', pin='678', man='34468', honors='66') - player.init_hand(tiles) - - # we will not get tempai on yakuhai pair with this hand, so let's skip this call - tile = self._string_to_136_tile(man='5') - meld, _ = player.try_to_call_meld(tile, False) - self.assertEqual(meld, None) - - # but here we will have atodzuke tempai - tile = self._string_to_136_tile(man='7') - meld, _ = player.try_to_call_meld(tile, True) - self.assertNotEqual(meld, None) - self.assertEqual(meld.type, Meld.CHI) - self.assertEqual(self._to_string(meld.tiles), '678m') - - table = Table() - player = table.player - - # we can open hand in that case - tiles = self._string_to_136_array(man='44556', sou='366789', honors='77') - player.init_hand(tiles) - - tile = self._string_to_136_tile(honors='7') - meld, _ = player.try_to_call_meld(tile, True) - self.assertNotEqual(meld, None) - self.assertEqual(self._to_string(meld.tiles), '777z') - - def test_call_yakuhai_pair_and_special_conditions(self): - table = Table() - player = table.player - - tiles = self._string_to_136_array(man='56', sou='1235', pin='12888', honors='11') - player.init_hand(tiles) - - meld = self._make_meld(Meld.PON, pin='888') - player.add_called_meld(meld) - - # to update previous_shanten attribute - player.draw_tile(self._string_to_136_tile(honors='3')) - player.discard_tile() - - tile = self._string_to_136_tile(honors='1') - meld, _ = player.try_to_call_meld(tile, True) - self.assertNotEqual(meld, None) - - def test_tempai_without_yaku(self): - table = Table() - player = table.player - - tiles = self._string_to_136_array(sou='678', pin='12355', man='456', honors='77') - player.init_hand(tiles) - - tile = self._string_to_136_tile(pin='5') - player.draw_tile(tile) - meld = self._make_meld(Meld.CHI, sou='678') - player.add_called_meld(meld) - - discard = player.discard_tile() - self.assertEqual(self._to_string([discard]), '1p') - - def test_get_more_yakuhai_sets_in_hand(self): - table = Table() - - tiles = self._string_to_136_array(sou='1378', pin='67', man='68', honors='5566') - table.player.init_hand(tiles) - - tile = self._string_to_136_tile(honors='5') - meld, discard_option = table.player.try_to_call_meld(tile, False) - self.assertNotEqual(meld, None) - - table.add_called_meld(0, meld) - table.player.tiles.append(tile) - table.player.discard_tile(discard_option) - - tile = self._string_to_136_tile(honors='6') - meld, _ = table.player.try_to_call_meld(tile, False) - self.assertNotEqual(meld, None) - - table = Table() - - tiles = self._string_to_136_array(sou='234', pin='788', man='567', honors='5566') - table.player.init_hand(tiles) - - tile = self._string_to_136_tile(honors='5') - meld, discard_option = table.player.try_to_call_meld(tile, False) - self.assertNotEqual(meld, None) - - table.add_called_meld(0, meld) - table.player.tiles.append(tile) - table.player.discard_tile(discard_option) - - tile = self._string_to_136_tile(honors='6') - meld, _ = table.player.try_to_call_meld(tile, False) - self.assertEqual(meld, None) - - def test_atodzuke_opened_hand(self): - table = Table() - player = table.player - - tiles = self._string_to_136_array(sou='4589', pin='123', man='1236', honors='66') - player.init_hand(tiles) - - tile = self._string_to_136_tile(man='6') - player.draw_tile(tile) - meld = self._make_meld(Meld.CHI, pin='123') - player.add_called_meld(meld) - - discard = player.discard_tile() - self.assertEqual(self._to_string([discard]), '9s') - - def test_wrong_shanten_improvements_detection(self): - """ - With hand 2345s1p11z bot wanted to open set on 2s, - so after opened set we will get 25s1p11z - it is not correct logic, because we ruined our hand - :return: - """ - table = Table() - player = table.player - - tiles = self._string_to_136_array(sou='2345999', honors='114446') - player.init_hand(tiles) - - meld = self._make_meld(Meld.PON, sou='999') - player.add_called_meld(meld) - meld = self._make_meld(Meld.PON, honors='444') - player.add_called_meld(meld) - - # to rebuild all caches - player.draw_tile(self._string_to_136_tile(pin='2')) - player.discard_tile() - - tile = self._string_to_136_tile(sou='2') - meld, _ = table.player.try_to_call_meld(tile, True) - self.assertEqual(meld, None) - - -class HonitsuStrategyTestCase(unittest.TestCase, TestMixin): - - def test_should_activate_strategy(self): - table = Table() - player = table.player - strategy = HonitsuStrategy(BaseStrategy.HONITSU, player) - - tiles = self._string_to_136_array(sou='12355', man='12389', honors='123') - player.init_hand(tiles) - self.assertEqual(strategy.should_activate_strategy(), False) - - tiles = self._string_to_136_array(sou='12355', man='238', honors='11234') - player.init_hand(tiles) - self.assertEqual(strategy.should_activate_strategy(), True) - - # with hand without pairs we not should go for honitsu, - # because it is far away from tempai - tiles = self._string_to_136_array(sou='12358', man='238', honors='12345') - player.init_hand(tiles) - self.assertEqual(strategy.should_activate_strategy(), False) - - # with chitoitsu-like hand we don't need to go for honitsu - tiles = self._string_to_136_array(pin='77', man='3355677899', sou='11') - player.init_hand(tiles) - self.assertEqual(strategy.should_activate_strategy(), False) - - def test_suitable_tiles(self): - table = Table() - player = table.player - strategy = HonitsuStrategy(BaseStrategy.HONITSU, player) - - tiles = self._string_to_136_array(sou='12355', man='238', honors='11234') - player.init_hand(tiles) - self.assertEqual(strategy.should_activate_strategy(), True) - - tile = self._string_to_136_tile(man='1') - self.assertEqual(strategy.is_tile_suitable(tile), False) - - tile = self._string_to_136_tile(pin='1') - self.assertEqual(strategy.is_tile_suitable(tile), False) - - tile = self._string_to_136_tile(sou='1') - self.assertEqual(strategy.is_tile_suitable(tile), True) - - tile = self._string_to_136_tile(honors='1') - self.assertEqual(strategy.is_tile_suitable(tile), True) - - def test_open_hand_and_discard_tiles_logic(self): - table = Table() - player = table.player - - tiles = self._string_to_136_array(sou='112235589', man='23', honors='22') - player.init_hand(tiles) - - # we don't need to call meld even if it improves our hand, - # because we are aim for honitsu - tile = self._string_to_136_tile(man='1') - meld, _ = player.try_to_call_meld(tile, False) - self.assertEqual(meld, None) - - # any honor tile is suitable - tile = self._string_to_136_tile(honors='2') - meld, tile_to_discard = player.try_to_call_meld(tile, False) - self.assertNotEqual(meld, None) - self.assertEqual(self._to_string([tile_to_discard * 4]), '2m') - - player.discard_tile(tile_to_discard * 4) - - tile = self._string_to_136_tile(man='1') - player.draw_tile(tile) - tile_to_discard = player.discard_tile() - - # we are in honitsu mode, so we should discard man suits - self.assertEqual(self._to_string([tile_to_discard]), '1m') - - def test_riichi_and_tiles_from_another_suit_in_the_hand(self): - table = Table() - player = table.player - player.scores = 25000 - table.count_of_remaining_tiles = 100 - - tiles = self._string_to_136_array(man='33345678', pin='22', honors='155') - player.init_hand(tiles) - - player.draw_tile(self._string_to_136_tile(man='9')) - tile_to_discard = player.discard_tile() - - # we don't need to go for honitsu here - # we already in tempai - self.assertEqual(self._to_string([tile_to_discard]), '1z') - - def test_discard_not_needed_winds(self): - table = Table() - player = table.player - player.scores = 25000 - table.count_of_remaining_tiles = 100 - - tiles = self._string_to_136_array(man='24', pin='4', sou='12344668', honors='36') - player.init_hand(tiles) - player.draw_tile(self._string_to_136_tile(sou='5')) - - table.add_discarded_tile(1, self._string_to_136_tile(honors='3'), False) - table.add_discarded_tile(1, self._string_to_136_tile(honors='3'), False) - table.add_discarded_tile(1, self._string_to_136_tile(honors='3'), False) - - tile_to_discard = player.discard_tile() - - # west was discarded three times, we don't need it - self.assertEqual(self._to_string([tile_to_discard]), '3z') - - def test_discard_not_effective_tiles_first(self): - table = Table() - player = table.player - player.scores = 25000 - table.count_of_remaining_tiles = 100 - - tiles = self._string_to_136_array(man='33', pin='12788999', sou='5', honors='23') - player.init_hand(tiles) - player.draw_tile(self._string_to_136_tile(honors='6')) - tile_to_discard = player.discard_tile() - - self.assertEqual(self._to_string([tile_to_discard]), '5s') - - def test_dont_go_for_honitsu_with_ryanmen_in_other_suit(self): - table = Table() - player = table.player - strategy = HonitsuStrategy(BaseStrategy.HONITSU, player) - - tiles = self._string_to_136_array(man='14489', sou='45', pin='67', honors='44456') - player.init_hand(tiles) - - self.assertEqual(strategy.should_activate_strategy(), False) - - -class TanyaoStrategyTestCase(unittest.TestCase, TestMixin): - - def _make_table(self): - table = Table() - table.has_open_tanyao = True - return table - - def test_should_activate_strategy_and_terminal_pon_sets(self): - table = self._make_table() - player = table.player - strategy = TanyaoStrategy(BaseStrategy.TANYAO, player) - - tiles = self._string_to_136_array(sou='234', man='3459', pin='233', honors='111') - player.init_hand(tiles) - self.assertEqual(strategy.should_activate_strategy(), False) - - tiles = self._string_to_136_array(sou='234', man='3459', pin='233999') - player.init_hand(tiles) - self.assertEqual(strategy.should_activate_strategy(), False) - - tiles = self._string_to_136_array(sou='234', man='3459', pin='233444') - player.init_hand(tiles) - self.assertEqual(strategy.should_activate_strategy(), True) - - def test_should_activate_strategy_and_terminal_pairs(self): - table = self._make_table() - player = table.player - strategy = TanyaoStrategy(BaseStrategy.TANYAO, player) - - tiles = self._string_to_136_array(sou='234', man='3459', pin='2399', honors='11') - player.init_hand(tiles) - self.assertEqual(strategy.should_activate_strategy(), False) - - tiles = self._string_to_136_array(sou='234', man='345669', pin='2399') - player.init_hand(tiles) - self.assertEqual(strategy.should_activate_strategy(), True) - - def test_should_activate_strategy_and_valued_pair(self): - table = self._make_table() - player = table.player - strategy = TanyaoStrategy(BaseStrategy.TANYAO, player) - - tiles = self._string_to_136_array(man='23446679', sou='345', honors='55') - player.init_hand(tiles) - self.assertEqual(strategy.should_activate_strategy(), False) - - tiles = self._string_to_136_array(man='23446679', sou='345', honors='22') - player.init_hand(tiles) - self.assertEqual(strategy.should_activate_strategy(), True) - - def test_should_activate_strategy_and_chitoitsu_like_hand(self): - table = self._make_table() - player = table.player - strategy = TanyaoStrategy(BaseStrategy.TANYAO, player) - - tiles = self._string_to_136_array(sou='223388', man='3344', pin='6687') - player.init_hand(tiles) - self.assertEqual(strategy.should_activate_strategy(), False) - - def test_should_activate_strategy_and_already_completed_sided_set(self): - table = self._make_table() - player = table.player - strategy = TanyaoStrategy(BaseStrategy.TANYAO, player) - - tiles = self._string_to_136_array(sou='123234', man='3459', pin='234') - player.init_hand(tiles) - self.assertEqual(strategy.should_activate_strategy(), False) - - tiles = self._string_to_136_array(sou='234789', man='3459', pin='234') - player.init_hand(tiles) - self.assertEqual(strategy.should_activate_strategy(), False) - - tiles = self._string_to_136_array(sou='234', man='1233459', pin='234') - player.init_hand(tiles) - self.assertEqual(strategy.should_activate_strategy(), False) - - tiles = self._string_to_136_array(sou='234', man='3457899', pin='234') - player.init_hand(tiles) - self.assertEqual(strategy.should_activate_strategy(), False) - - tiles = self._string_to_136_array(sou='234', man='3459', pin='122334') - player.init_hand(tiles) - self.assertEqual(strategy.should_activate_strategy(), False) - - tiles = self._string_to_136_array(sou='234', man='3459', pin='234789') - player.init_hand(tiles) - self.assertEqual(strategy.should_activate_strategy(), False) - - tiles = self._string_to_136_array(sou='223344', man='3459', pin='234') - player.init_hand(tiles) - self.assertEqual(strategy.should_activate_strategy(), True) - - def test_suitable_tiles(self): - table = self._make_table() - player = table.player - strategy = TanyaoStrategy(BaseStrategy.TANYAO, player) - - tile = self._string_to_136_tile(man='1') - self.assertEqual(strategy.is_tile_suitable(tile), False) - - tile = self._string_to_136_tile(pin='1') - self.assertEqual(strategy.is_tile_suitable(tile), False) - - tile = self._string_to_136_tile(sou='9') - self.assertEqual(strategy.is_tile_suitable(tile), False) - - tile = self._string_to_136_tile(honors='1') - self.assertEqual(strategy.is_tile_suitable(tile), False) - - tile = self._string_to_136_tile(honors='6') - self.assertEqual(strategy.is_tile_suitable(tile), False) - - tile = self._string_to_136_tile(man='2') - self.assertEqual(strategy.is_tile_suitable(tile), True) - - tile = self._string_to_136_tile(pin='5') - self.assertEqual(strategy.is_tile_suitable(tile), True) - - tile = self._string_to_136_tile(sou='8') - self.assertEqual(strategy.is_tile_suitable(tile), True) - - def test_dont_open_hand_with_high_shanten(self): - table = self._make_table() - player = table.player - - # with 4 shanten we don't need to aim for open tanyao - tiles = self._string_to_136_array(man='369', pin='378', sou='3488', honors='123') - tile = self._string_to_136_tile(sou='2') - player.init_hand(tiles) - meld, _ = player.try_to_call_meld(tile, False) - self.assertEqual(meld, None) - - # with 3 shanten we can open a hand - tiles = self._string_to_136_array(man='236', pin='378', sou='3488', honors='123') - tile = self._string_to_136_tile(sou='2') - player.init_hand(tiles) - meld, _ = player.try_to_call_meld(tile, True) - self.assertNotEqual(meld, None) - - def test_dont_open_hand_with_not_suitable_melds(self): - table = self._make_table() - player = table.player - - tiles = self._string_to_136_array(man='33355788', sou='3479', honors='3') - tile = self._string_to_136_tile(sou='8') - player.init_hand(tiles) - meld, _ = player.try_to_call_meld(tile, False) - self.assertEqual(meld, None) - - def test_open_hand_and_discard_tiles_logic(self): - table = self._make_table() - player = table.player - - # 2345779m1p256s44z - tiles = self._string_to_136_array(man='22345', sou='238', pin='256', honors='44') - player.init_hand(tiles) - - # if we are in tanyao - # we need to discard terminals and honors - tile = self._string_to_136_tile(sou='4') - meld, discard_option = player.try_to_call_meld(tile, True) - discarded_tile = table.player.discard_tile(discard_option) - self.assertNotEqual(meld, None) - self.assertEqual(self._to_string([discarded_tile]), '4z') - - tile = self._string_to_136_tile(pin='5') - player.draw_tile(tile) - tile_to_discard = player.discard_tile() - - # we are in tanyao, so we should discard honors and terminals - self.assertEqual(self._to_string([tile_to_discard]), '4z') - - def test_dont_count_pairs_in_already_opened_hand(self): - table = self._make_table() - player = table.player - - meld = self._make_meld(Meld.PON, sou='222') - player.add_called_meld(meld) - - tiles = self._string_to_136_array(man='33556788', sou='22266') - player.init_hand(tiles) - - tile = self._string_to_136_tile(sou='6') - meld, _ = player.try_to_call_meld(tile, False) - # even if it looks like chitoitsu we can open hand and get tempai here - self.assertNotEqual(meld, None) - - def test_we_cant_win_with_this_hand(self): - table = self._make_table() - - tiles = self._string_to_136_array(man='34577', sou='23', pin='233445') - table.player.init_hand(tiles) - meld = self._make_meld(Meld.CHI, pin='234') - table.player.add_called_meld(meld) - - table.player.draw_tile(self._string_to_136_tile(sou='1')) - discard = table.player.discard_tile() - # but for already open hand we cant do tsumo - # because we don't have a yaku here - # so, let's do tsumogiri - self.assertEqual(table.player.ai.previous_shanten, 0) - self.assertEqual(self._to_string([discard]), '1s') - - def test_choose_correct_waiting(self): - table = self._make_table() - player = table.player - - tiles = self._string_to_136_array(man='234678', sou='234', pin='3588') - player.init_hand(tiles) - player.draw_tile(self._string_to_136_tile(pin='2')) - - # discard 5p and riichi - discard = player.discard_tile() - self.assertEqual(self._to_string([discard]), '5p') - - table = self._make_table() - player = table.player - - meld = self._make_meld(Meld.CHI, man='234') - player.add_called_meld(meld) - - tiles = self._string_to_136_array(man='234678', sou='234', pin='3588') - player.init_hand(tiles) - player.draw_tile(self._string_to_136_tile(pin='2')) - - # it is not a good idea to wait on 1-4, since we can't win on 1 with open hand - # so let's continue to wait on 4 only - discard = player.discard_tile() - self.assertEqual(self._to_string([discard]), '2p') - - table = table = self._make_table() - player = table.player - - meld = self._make_meld(Meld.CHI, man='234') - player.add_called_meld(meld) - - tiles = self._string_to_136_array(man='234678', sou='234', pin='2388') - player.init_hand(tiles) - player.draw_tile(self._string_to_136_tile(sou='7')) - - # we can wait only on 1-4, so let's do it even if we can't get yaku on 1 - discard = player.discard_tile() - self.assertEqual(self._to_string([discard]), '7s') - - def test_choose_correct_waiting_and_fist_opened_meld(self): - table = self._make_table() - player = table.player - - tiles = self._string_to_136_array(man='2337788', sou='345', pin='234') - player.init_hand(tiles) - - tile = self._string_to_136_tile(man='8') - meld, tile_to_discard = player.try_to_call_meld(tile, False) - - discard = player.discard_tile(tile_to_discard) - self.assertEqual(self._to_string([discard]), '2m') diff --git a/project/game/player.py b/project/game/player.py index e020c8b8..20cddfd5 100644 --- a/project/game/player.py +++ b/project/game/player.py @@ -16,6 +16,7 @@ class PlayerInterface(object): discards = None melds = None in_riichi = None + round_step = None # current player seat seat = 0 @@ -59,6 +60,7 @@ def erase_state(self): self.position = 0 self.scores = None self.uma = 0 + self.round_step = 0 def add_called_meld(self, meld: Meld): # we already added chankan as a pon set @@ -79,6 +81,9 @@ def add_discarded_tile(self, tile: Tile): if player.in_riichi and tile not in player.safe_tiles: player.safe_tiles.append(tile) + # one discard == one round step + self.round_step += 1 + @property def player_wind(self): position = self.dealer_seat From ccf7cab9986a746859ac29597f2be3c9af4d6dbc Mon Sep 17 00:00:00 2001 From: Nihisil Date: Fri, 24 Aug 2018 17:27:54 +0800 Subject: [PATCH 007/126] Add additional rules about double east\south to the yakuhai strategy --- .../ai/first_version/strategies/yakuhai.py | 16 +++++- .../tests/strategies/tests_yakuhai.py | 53 ++++++++++++++++++- 2 files changed, 67 insertions(+), 2 deletions(-) diff --git a/project/game/ai/first_version/strategies/yakuhai.py b/project/game/ai/first_version/strategies/yakuhai.py index ff40e83a..b992f5cb 100644 --- a/project/game/ai/first_version/strategies/yakuhai.py +++ b/project/game/ai/first_version/strategies/yakuhai.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +from mahjong.constants import EAST, SOUTH from mahjong.meld import Meld from mahjong.tile import TilesConverter from mahjong.utils import plus_dora, is_aka_dora @@ -27,8 +28,12 @@ def should_activate_strategy(self): tiles_34 = TilesConverter.to_34_array(self.player.tiles) self.valued_pairs = [x for x in self.player.valued_honors if tiles_34[x] >= 2] + + is_double_east_wind = len([x for x in self.valued_pairs if x == EAST]) == 2 + is_double_south_wind = len([x for x in self.valued_pairs if x == SOUTH]) == 2 + self.valued_pairs = list(set(self.valued_pairs)) - self.has_valued_pon = len([x for x in self.player.valued_honors if tiles_34[x] == 3]) > 1 + self.has_valued_pon = len([x for x in self.player.valued_honors if tiles_34[x] == 3]) >= 1 has_valued_pair = False @@ -46,6 +51,15 @@ def should_activate_strategy(self): dora_count = sum([plus_dora(x, self.player.table.dora_indicators) for x in self.player.tiles]) dora_count += sum([1 for x in self.player.tiles if is_aka_dora(x, self.player.table.has_open_tanyao)]) + # let's always open double east + if is_double_east_wind: + return True + + # let's open double south if we have a dora in the hand + # or we have other valuable pairs + if is_double_south_wind and (dora_count >= 1 or len(self.valued_pairs) >= 2): + return True + # If we have 1+ dora in the hand and there are 2+ valuable pairs let's open hand if len(self.valued_pairs) >= 2 and dora_count >= 1: return 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 64555dff..a0d784cd 100644 --- a/project/game/ai/first_version/tests/strategies/tests_yakuhai.py +++ b/project/game/ai/first_version/tests/strategies/tests_yakuhai.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- import unittest +from mahjong.constants import WEST, EAST, SOUTH from mahjong.meld import Meld from mahjong.tests_mixin import TestMixin from mahjong.tile import Tile @@ -15,6 +16,7 @@ class YakuhaiStrategyTestCase(unittest.TestCase, TestMixin): def setUp(self): self.table = Table() self.player = self.table.player + self.player.dealer_seat = 3 def test_should_activate_strategy(self): strategy = YakuhaiStrategy(BaseStrategy.YAKUHAI, self.player) @@ -281,9 +283,58 @@ def test_open_hand_and_once_discarded_tile(self): self.assertEqual(meld, None) def test_open_hand_when_yakuhai_already_in_the_hand(self): - tiles = self._string_to_136_array(man='46', pin='4679', sou='1348', honors='111') + tiles = self._string_to_136_array(man='46', pin='4679', sou='1348', honors='555') self.player.init_hand(tiles) tile = self._string_to_136_tile(sou='2') meld, _ = self.player.try_to_call_meld(tile, True) self.assertNotEqual(meld, None) + + def test_always_open_double_east_wind(self): + tiles = self._string_to_136_array(man='59', sou='1235', pin='12788', honors='11') + self.player.init_hand(tiles) + + # player is is not east + self.player.dealer_seat = 2 + self.assertEqual(self.player.player_wind, WEST) + + 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) + + # player is is east + self.player.dealer_seat = 0 + self.assertEqual(self.player.player_wind, EAST) + + self.player.init_hand(tiles) + tile = self._string_to_136_tile(honors='1') + meld, _ = self.player.try_to_call_meld(tile, True) + self.assertNotEqual(meld, None) + + def test_open_double_south_wind(self): + tiles = self._string_to_136_array(man='59', sou='1235', pin='12788', honors='22') + self.player.init_hand(tiles) + + self.player.init_hand(tiles) + tile = self._string_to_136_tile(honors='2') + meld, _ = self.player.try_to_call_meld(tile, True) + self.assertEqual(meld, None) + + # player is south and round is south + self.table.round_number = 5 + self.player.dealer_seat = 3 + self.assertEqual(self.player.player_wind, SOUTH) + + self.player.init_hand(tiles) + tile = self._string_to_136_tile(honors='2') + meld, _ = self.player.try_to_call_meld(tile, True) + self.assertEqual(meld, None) + + # add dora in the hand and after that we can open a hand + self.table.dora_indicators.append(self._string_to_136_tile(pin='6')) + + self.player.init_hand(tiles) + tile = self._string_to_136_tile(honors='2') + meld, _ = self.player.try_to_call_meld(tile, True) + self.assertNotEqual(meld, None) From ddd449e222abb8214ff6517b17aa3b6515f94c31 Mon Sep 17 00:00:00 2001 From: Pavel Bogachev Date: Sat, 25 Aug 2018 05:10:41 +0300 Subject: [PATCH 008/126] Add 'Formal Tempai' strategy (#45) --- .flake8 | 2 +- project/game/ai/discard.py | 16 ++-- project/game/ai/first_version/defence/main.py | 2 +- project/game/ai/first_version/defence/suji.py | 4 +- project/game/ai/first_version/main.py | 63 +++++++++------ .../first_version/strategies/formal_tempai.py | 80 +++++++++++++++++++ .../game/ai/first_version/strategies/main.py | 2 + .../tests/strategies/tests_formal_tempai.py | 40 ++++++++++ .../tests/strategies/tests_honitsu.py | 3 - .../tests/strategies/tests_yakuhai.py | 5 +- .../game/ai/first_version/tests/tests_ai.py | 16 ++-- .../ai/first_version/tests/tests_discards.py | 8 +- project/game/tests/tests_player.py | 1 - 13 files changed, 188 insertions(+), 54 deletions(-) create mode 100644 project/game/ai/first_version/strategies/formal_tempai.py create mode 100644 project/game/ai/first_version/tests/strategies/tests_formal_tempai.py diff --git a/.flake8 b/.flake8 index 2d5c5816..42f0565a 100644 --- a/.flake8 +++ b/.flake8 @@ -1,3 +1,3 @@ [flake8] max-line-length = 120 -exclude = settings.py \ No newline at end of file +exclude = *settings.py,tests_validate_hand.py \ No newline at end of file diff --git a/project/game/ai/discard.py b/project/game/ai/discard.py index b79cc336..3a1ac4f6 100644 --- a/project/game/ai/discard.py +++ b/project/game/ai/discard.py @@ -14,8 +14,8 @@ class DiscardOption(object): # array of tiles that will improve our hand waiting = None # how much tiles will improve our hand - tiles_count = None - tiles_count_second_level = None + ukeire = None + ukeire_second = None # number of shanten for that tile shanten = None # sometimes we had to force tile to be discarded @@ -27,19 +27,19 @@ class DiscardOption(object): # how danger this tile is danger = None - def __init__(self, player, tile_to_discard, shanten, waiting, tiles_count, danger=100): + def __init__(self, player, tile_to_discard, shanten, waiting, ukeire, danger=100): """ :param player: :param tile_to_discard: tile in 34 format :param waiting: list of tiles in 34 format - :param tiles_count: count of tiles to wait after discard + :param ukeire: count of tiles to wait after discard """ self.player = player self.tile_to_discard = tile_to_discard self.shanten = shanten self.waiting = waiting - self.tiles_count = tiles_count - self.tiles_count_second_level = 0 + self.ukeire = ukeire + self.ukeire_second = 0 self.count_of_dora = 0 self.danger = danger self.had_to_be_saved = False @@ -51,8 +51,8 @@ def __unicode__(self): tile_format_136 = TilesConverter.to_one_line_string([self.tile_to_discard*4]) return 'tile={}, ukeire={}, ukeire2={}, valuation={}'.format( tile_format_136, - self.tiles_count, - self.tiles_count_second_level, + self.ukeire, + self.ukeire_second, self.valuation ) diff --git a/project/game/ai/first_version/defence/main.py b/project/game/ai/first_version/defence/main.py index c1bc1205..326d9fe3 100644 --- a/project/game/ai/first_version/defence/main.py +++ b/project/game/ai/first_version/defence/main.py @@ -205,7 +205,7 @@ def _find_tile_to_discard(self, safe_tiles, discard_tiles): if not was_safe_tiles: return None - final_results = sorted(discard_tiles, key=lambda x: (x.danger, x.shanten, -x.tiles_count, x.valuation)) + final_results = sorted(discard_tiles, key=lambda x: (x.danger, x.shanten, -x.ukeire, x.valuation)) return final_results[0] diff --git a/project/game/ai/first_version/defence/suji.py b/project/game/ai/first_version/defence/suji.py index e9cf7432..eeea3acc 100644 --- a/project/game/ai/first_version/defence/suji.py +++ b/project/game/ai/first_version/defence/suji.py @@ -108,7 +108,9 @@ def _suji_tiles(self, suji): # mark dora tiles as dangerous tiles to discard for tile in result: - if plus_dora(tile.value * 4, self.table.dora_indicators) or is_aka_dora(tile.value * 4, self.table.has_open_tanyao): + is_dora = plus_dora(tile.value * 4, self.table.dora_indicators) \ + or is_aka_dora(tile.value * 4, self.table.has_open_tanyao) + if is_dora: tile.danger += 100 return result diff --git a/project/game/ai/first_version/main.py b/project/game/ai/first_version/main.py index 39daf41b..3ba00147 100644 --- a/project/game/ai/first_version/main.py +++ b/project/game/ai/first_version/main.py @@ -19,6 +19,7 @@ from game.ai.first_version.strategies.main import BaseStrategy from game.ai.first_version.strategies.tanyao import TanyaoStrategy from game.ai.first_version.strategies.yakuhai import YakuhaiStrategy +from game.ai.first_version.strategies.formal_tempai import FormalTempaiStrategy logger = logging.getLogger('ai') @@ -34,6 +35,8 @@ class ImplementationAI(InterfaceAI): last_discard_option = None previous_shanten = 7 + ukeire = 0 + ukeire_second = 0 in_defence = False waiting = None @@ -48,6 +51,8 @@ def __init__(self, player): self.hand_divider = HandDivider() self.finished_hand = HandCalculator() self.previous_shanten = 7 + self.ukeire = 0 + self.ukeire_second = 0 self.current_strategy = None self.waiting = [] self.in_defence = False @@ -61,8 +66,12 @@ def init_hand(self): def erase_state(self): self.current_strategy = None + self.waiting = [] self.in_defence = False self.last_discard_option = None + self.previous_shanten = 7 + self.ukeire = 0 + self.ukeire_second = 0 def draw_tile(self, tile): """ @@ -107,7 +116,7 @@ def process_discard_options_and_select_tile_to_discard(self, results, shanten, h # we had to update tiles value there # because it is related with shanten number for result in results: - result.tiles_count = self.count_tiles(result.waiting, tiles_34) + result.ukeire = self.count_tiles(result.waiting, tiles_34) result.calculate_value(shanten) # current strategy can affect on our discard options @@ -159,7 +168,7 @@ def calculate_outs(self, tiles, closed_hand, open_sets_34=None): shanten=shanten, tile_to_discard=hand_tile, waiting=waiting, - tiles_count=self.count_tiles(waiting, tiles_34))) + ukeire=self.count_tiles(waiting, tiles_34))) if is_agari: shanten = Shanten.AGARI_STATE @@ -203,6 +212,8 @@ def determine_strategy(self): if self.player.table.has_open_tanyao: strategies.append(TanyaoStrategy(BaseStrategy.TANYAO, self.player)) + strategies.append(FormalTempaiStrategy(BaseStrategy.FORMAL_TEMPAI, self.player)) + for strategy in strategies: if strategy.should_activate_strategy(): self.current_strategy = strategy @@ -227,12 +238,12 @@ def choose_tile_to_discard(self, results: [DiscardOption]) -> DiscardOption: had_to_be_discarded_tiles = [x for x in results if x.had_to_be_discarded] if had_to_be_discarded_tiles: - return sorted(had_to_be_discarded_tiles, key=lambda x: (x.shanten, -x.tiles_count, x.valuation))[0] + return sorted(had_to_be_discarded_tiles, key=lambda x: (x.shanten, -x.ukeire, x.valuation))[0] # remove needed tiles from discard options results = [x for x in results if not x.had_to_be_saved] - results = sorted(results, key=lambda x: (x.shanten, -x.tiles_count)) + results = sorted(results, key=lambda x: (x.shanten, -x.ukeire)) first_option = results[0] results_with_same_shanten = [x for x in results if x.shanten == first_option.shanten] @@ -244,22 +255,22 @@ def choose_tile_to_discard(self, results: [DiscardOption]) -> DiscardOption: continue # we don't need to select tiles almost dead waits - if discard_option.tiles_count <= 2: + if discard_option.ukeire <= 2: continue - tiles_count_borders = round((first_option.tiles_count / 100) * border_percentage) + ukeire_borders = round((first_option.ukeire / 100) * border_percentage) - if first_option.shanten == 0 and tiles_count_borders < 2: - tiles_count_borders = 2 + if first_option.shanten == 0 and ukeire_borders < 2: + ukeire_borders = 2 - if first_option.shanten == 1 and tiles_count_borders < 4: - tiles_count_borders = 4 + if first_option.shanten == 1 and ukeire_borders < 4: + ukeire_borders = 4 - if first_option.shanten >= 2 and tiles_count_borders < 8: - tiles_count_borders = 8 + if first_option.shanten >= 2 and ukeire_borders < 8: + ukeire_borders = 8 # let's choose tiles that are close to the max ukeire tile - if discard_option.tiles_count >= first_option.tiles_count - tiles_count_borders: + if discard_option.ukeire >= first_option.ukeire - ukeire_borders: possible_options.append(discard_option) if first_option.shanten <= 1: @@ -269,14 +280,14 @@ def choose_tile_to_discard(self, results: [DiscardOption]) -> DiscardOption: # as second step # let's choose tiles that are close to the max ukeire2 tile for x in possible_options: - self.calculate_second_level_tiles_count(x) + self.calculate_second_level_ukeire(x) - possible_options = sorted(possible_options, key=lambda x: -x.tiles_count_second_level) + possible_options = sorted(possible_options, key=lambda x: -x.ukeire_second) filter_percentage = 20 filtered_options = self._filter_list_by_percentage( possible_options, - 'tiles_count_second_level', + 'ukeire_second', filter_percentage ) @@ -287,12 +298,12 @@ def choose_tile_to_discard(self, results: [DiscardOption]) -> DiscardOption: min_dora_list = [x for x in filtered_options if x.count_of_dora == min_dora] # let's discard tile with greater ukeire2 - return sorted(min_dora_list, key=lambda x: -x.tiles_count_second_level)[0] + return sorted(min_dora_list, key=lambda x: -x.ukeire_second)[0] second_filter_percentage = 10 filtered_options = self._filter_list_by_percentage( filtered_options, - 'tiles_count_second_level', + 'ukeire_second', second_filter_percentage ) @@ -305,18 +316,20 @@ def choose_tile_to_discard(self, results: [DiscardOption]) -> DiscardOption: # there are no isolated tiles # let's discard tile with greater ukeire2 - return sorted(filtered_options, key=lambda x: -x.tiles_count_second_level)[0] + return sorted(filtered_options, key=lambda x: -x.ukeire_second)[0] def process_discard_option(self, discard_option, closed_hand, force_discard=False): self.waiting = discard_option.waiting self.player.ai.previous_shanten = discard_option.shanten self.player.in_tempai = self.player.ai.previous_shanten == 0 + self.player.ai.ukeire = discard_option.ukeire + self.player.ai.ukeire_second = discard_option.ukeire_second # when we called meld we don't need "smart" discard if force_discard: return discard_option.find_tile_in_hand(closed_hand) - last_draw_34 = self.player.last_draw and self.player.last_draw // 4 or None + last_draw_34 = self.player.last_draw and self.player.last_draw // 4 or None if self.player.last_draw not in AKA_DORA_LIST and last_draw_34 == discard_option.tile_to_discard: return self.player.last_draw else: @@ -466,7 +479,7 @@ def enemy_called_riichi(self, enemy_seat): if self.defence.should_go_to_defence_mode(): self.in_defence = True - def calculate_second_level_tiles_count(self, discard_option): + def calculate_second_level_ukeire(self, discard_option): tiles = copy.copy(self.player.tiles) tiles.remove(discard_option.find_tile_in_hand(self.player.closed_hand)) @@ -481,11 +494,11 @@ def calculate_second_level_tiles_count(self, discard_option): self.player.open_hand_34_tiles ) results = [x for x in results if x.shanten == discard_option.shanten - 1] - sum_tiles += sum([x.tiles_count for x in results]) + sum_tiles += sum([x.ukeire for x in results]) tiles.remove(wait) - discard_option.tiles_count_second_level = sum_tiles + discard_option.ukeire_second = sum_tiles @property def enemy_players(self): @@ -497,8 +510,8 @@ def enemy_players(self): def _filter_list_by_percentage(self, items, attribute, percentage): filtered_options = [] first_option = items[0] - tiles_count_borders = round((getattr(first_option, attribute) / 100) * percentage) + ukeire_borders = round((getattr(first_option, attribute) / 100) * percentage) for x in items: - if getattr(x, attribute) >= getattr(first_option, attribute) - tiles_count_borders: + if getattr(x, attribute) >= getattr(first_option, attribute) - ukeire_borders: filtered_options.append(x) return filtered_options diff --git a/project/game/ai/first_version/strategies/formal_tempai.py b/project/game/ai/first_version/strategies/formal_tempai.py new file mode 100644 index 00000000..5e941486 --- /dev/null +++ b/project/game/ai/first_version/strategies/formal_tempai.py @@ -0,0 +1,80 @@ +# -*- coding: utf-8 -*- +from mahjong.utils import plus_dora, is_aka_dora + +from game.ai.first_version.strategies.main import BaseStrategy + + +class FormalTempaiStrategy(BaseStrategy): + + def should_activate_strategy(self): + """ + When we get closer to the end of the round, we start to consider + going for formal tempai. + :return: boolean + """ + + result = super(FormalTempaiStrategy, self).should_activate_strategy() + if not result: + return False + + # if we already in tempai, we don't need this strategy + if self.player.in_tempai: + return False + + # it's too early to go for formal tempai before 11th turn + if self.player.round_step < 11: + return False + + # it's 11th turn or later and we still have 3 shanten or more, + # let's try to go for formal tempai at least + if self.player.ai.previous_shanten >= 3: + return True + + dora_count = sum([plus_dora(x, self.player.table.dora_indicators) for x in self.player.tiles]) + dora_count += sum([1 for x in self.player.tiles if is_aka_dora(x, self.player.table.has_open_tanyao)]) + + if self.player.ai.previous_shanten == 2: + if dora_count < 2: + # having 0 or 1 dora and 2 shanten, let's go for formal tempai + # starting from 11th turn + return True + # having 2 or more doras and 2 shanten, let's go for formal + # tempai starting from 12th turn + return self.player.round_step >= 12 + + # for 1 shanten we check number of doras and ukeire to determine + # correct time to go for formal tempai + if self.player.ai.previous_shanten == 1: + if dora_count == 0: + if self.player.ai.ukeire <= 16: + return True + + if self.player.ai.ukeire <= 28: + return self.player.round_step >= 12 + + return self.player.round_step >= 13 + + if dora_count == 1: + if self.player.ai.ukeire <= 16: + return self.player.round_step >= 12 + + if self.player.ai.ukeire <= 28: + return self.player.round_step >= 13 + + return self.player.round_step >= 14 + + if self.player.ai.ukeire <= 16: + return self.player.round_step >= 13 + + return self.player.round_step >= 14 + + # we actually never reach here + return False + + def is_tile_suitable(self, tile): + """ + All tiles are suitable for formal tempai. + :param tile: 136 tiles format + :return: True + """ + return True diff --git a/project/game/ai/first_version/strategies/main.py b/project/game/ai/first_version/strategies/main.py index 84e7c787..bf714801 100644 --- a/project/game/ai/first_version/strategies/main.py +++ b/project/game/ai/first_version/strategies/main.py @@ -8,11 +8,13 @@ class BaseStrategy(object): YAKUHAI = 0 HONITSU = 1 TANYAO = 2 + FORMAL_TEMPAI = 3 TYPES = { YAKUHAI: 'Yakuhai', HONITSU: 'Honitsu', TANYAO: 'Tanyao', + FORMAL_TEMPAI: 'Formal Tempai' } player = None 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 new file mode 100644 index 00000000..77c91a74 --- /dev/null +++ b/project/game/ai/first_version/tests/strategies/tests_formal_tempai.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +import unittest + +from mahjong.tests_mixin import TestMixin +from mahjong.tile import Tile + +from game.ai.first_version.strategies.formal_tempai import FormalTempaiStrategy +from game.ai.first_version.strategies.main import BaseStrategy +from game.table import Table + + +class FormalTempaiStrategyTestCase(unittest.TestCase, TestMixin): + + def setUp(self): + self.table = Table() + self.player = self.table.player + self.player.dealer_seat = 3 + + 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(), False) + + self.player.draw_tile(self._string_to_136_tile(honors='1')) + # to calculate hand shanten number + self.player.discard_tile() + + # Let's move to 10th round step, one tile was already discarded, 9 more + # to go + for i in range(0, 9): + self.player.add_discarded_tile(Tile(0, False)) + + self.assertEqual(strategy.should_activate_strategy(), 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(), True) 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 d672dd02..2165ba15 100644 --- a/project/game/ai/first_version/tests/strategies/tests_honitsu.py +++ b/project/game/ai/first_version/tests/strategies/tests_honitsu.py @@ -140,6 +140,3 @@ def test_dont_go_for_honitsu_with_ryanmen_in_other_suit(self): player.init_hand(tiles) self.assertEqual(strategy.should_activate_strategy(), False) - - - 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 a0d784cd..cbc01eff 100644 --- a/project/game/ai/first_version/tests/strategies/tests_yakuhai.py +++ b/project/game/ai/first_version/tests/strategies/tests_yakuhai.py @@ -12,7 +12,7 @@ class YakuhaiStrategyTestCase(unittest.TestCase, TestMixin): - + def setUp(self): self.table = Table() self.player = self.table.player @@ -132,7 +132,7 @@ def test_wrong_shanten_improvements_detection(self): def test_open_hand_with_doras_in_the_hand(self): """ - If we have valuable pair in the hand, and 2+ dora let's open on this + If we have valuable pair in the hand, and 2+ dora let's open on this valuable pair """ @@ -254,7 +254,6 @@ def test_open_hand_and_once_discarded_tile(self): """ If we have valuable pair in the hand, this tile was discarded once and we have 1+ shanten let's open on this valuable pair - 5. Если в руке есть ценная пара, один тайл этой ценной пары уже вышел, в руке больше ишантена, то открываемся на этой ценной паре. """ tiles = self._string_to_136_array(man='29', sou='1189', pin='12789', honors='11') diff --git a/project/game/ai/first_version/tests/tests_ai.py b/project/game/ai/first_version/tests/tests_ai.py index 01f958d7..87713b16 100644 --- a/project/game/ai/first_version/tests/tests_ai.py +++ b/project/game/ai/first_version/tests/tests_ai.py @@ -155,20 +155,20 @@ def test_remaining_tiles_and_enemy_discard(self): results, shanten = player.ai.calculate_outs(tiles, tiles) result = [x for x in results if x.tile_to_discard == self._string_to_34_tile(sou='1')][0] - self.assertEqual(result.tiles_count, 8) + self.assertEqual(result.ukeire, 8) player.table.add_discarded_tile(1, self._string_to_136_tile(sou='5'), False) results, shanten = player.ai.calculate_outs(tiles, tiles) result = [x for x in results if x.tile_to_discard == self._string_to_34_tile(sou='1')][0] - self.assertEqual(result.tiles_count, 7) + self.assertEqual(result.ukeire, 7) player.table.add_discarded_tile(2, self._string_to_136_tile(sou='5'), False) player.table.add_discarded_tile(3, self._string_to_136_tile(sou='8'), False) results, shanten = player.ai.calculate_outs(tiles, tiles) result = [x for x in results if x.tile_to_discard == self._string_to_34_tile(sou='1')][0] - self.assertEqual(result.tiles_count, 5) + self.assertEqual(result.ukeire, 5) def test_remaining_tiles_and_opened_meld(self): table = Table() @@ -179,7 +179,7 @@ def test_remaining_tiles_and_opened_meld(self): results, shanten = player.ai.calculate_outs(tiles, tiles) result = [x for x in results if x.tile_to_discard == self._string_to_34_tile(sou='1')][0] - self.assertEqual(result.tiles_count, 8) + self.assertEqual(result.ukeire, 8) # was discard and set was opened tile = self._string_to_136_tile(sou='8') @@ -190,7 +190,7 @@ def test_remaining_tiles_and_opened_meld(self): results, shanten = player.ai.calculate_outs(tiles, tiles) result = [x for x in results if x.tile_to_discard == self._string_to_34_tile(sou='1')][0] - self.assertEqual(result.tiles_count, 5) + self.assertEqual(result.ukeire, 5) # was discard and set was opened tile = self._string_to_136_tile(sou='3') @@ -201,7 +201,7 @@ def test_remaining_tiles_and_opened_meld(self): results, shanten = player.ai.calculate_outs(tiles, tiles) result = [x for x in results if x.tile_to_discard == self._string_to_34_tile(sou='1')][0] - self.assertEqual(result.tiles_count, 4) + self.assertEqual(result.ukeire, 4) def test_remaining_tiles_and_dora_indicators(self): table = Table() @@ -212,13 +212,13 @@ def test_remaining_tiles_and_dora_indicators(self): results, shanten = player.ai.calculate_outs(tiles, tiles) result = [x for x in results if x.tile_to_discard == self._string_to_34_tile(sou='1')][0] - self.assertEqual(result.tiles_count, 8) + self.assertEqual(result.ukeire, 8) table.add_dora_indicator(self._string_to_136_tile(sou='8')) results, shanten = player.ai.calculate_outs(tiles, tiles) result = [x for x in results if x.tile_to_discard == self._string_to_34_tile(sou='1')][0] - self.assertEqual(result.tiles_count, 7) + self.assertEqual(result.ukeire, 7) def test_using_tiles_of_different_suit_for_chi(self): """ diff --git a/project/game/ai/first_version/tests/tests_discards.py b/project/game/ai/first_version/tests/tests_discards.py index 2a5f3941..30606e57 100644 --- a/project/game/ai/first_version/tests/tests_discards.py +++ b/project/game/ai/first_version/tests/tests_discards.py @@ -46,7 +46,8 @@ def test_discard_tile_force_tsumogiri(self): player = table.player tiles = self._string_to_136_array(sou='11134567', pin='456', man='45') - tile = 57 # 6p + # 6p + tile = 57 player.init_hand(tiles) player.draw_tile(tile) @@ -54,7 +55,8 @@ def test_discard_tile_force_tsumogiri(self): discarded_tile = player.discard_tile() self.assertEqual(discarded_tile, tile) - tiles = self._string_to_136_array(sou='11134567', pin='46', man='45') + [53] # simple five pin + # add not red five pin + tiles = self._string_to_136_array(sou='11134567', pin='46', man='45') + [53] tile = FIVE_RED_PIN player.init_hand(tiles) @@ -220,7 +222,7 @@ def test_dont_keep_honor_with_small_number_of_shanten(self): discarded_tile = player.discard_tile() self.assertEqual(self._to_string([discarded_tile]), '7z') - def test_prefer_valuable_tiles_with_almost_same_tiles_count(self): + def test_prefer_valuable_tiles_with_almost_same_ukeire(self): table = Table() player = table.player table.add_dora_indicator(self._string_to_136_tile(sou='4')) diff --git a/project/game/tests/tests_player.py b/project/game/tests/tests_player.py index 019414a3..56cad7b3 100644 --- a/project/game/tests/tests_player.py +++ b/project/game/tests/tests_player.py @@ -7,7 +7,6 @@ from game.player import Player from game.table import Table -from utils.settings_handler import settings class PlayerTestCase(unittest.TestCase, TestMixin): From b4bdf590d73c3b495a934a9948e2c6964a2e896a Mon Sep 17 00:00:00 2001 From: Alexey Date: Sat, 25 Aug 2018 19:26:13 +0800 Subject: [PATCH 009/126] Various small fixes for log reproducer (#63) * Don't save log reproducer output to the file * Introduce log name prefix setting * Improve tenhou reproducer output --- project/reproducer.py | 55 ++++++++++++++++++++++++++++++++++++----- project/utils/logger.py | 26 ++++++++++--------- 2 files changed, 63 insertions(+), 18 deletions(-) diff --git a/project/reproducer.py b/project/reproducer.py index 6492469b..97e1e083 100644 --- a/project/reproducer.py +++ b/project/reproducer.py @@ -41,8 +41,26 @@ def reproduce(self, dry_run=False): table = Table() for tag in self.round_content: + if player_draw_regex.match(tag) and 'UN' not in tag: + print('Player draw') + tile = self.decoder.parse_tile(tag) + table.player.draw_tile(tile) + if dry_run: - print(tag) + if self._is_draw(tag): + print('<-', TilesConverter.to_one_line_string([self._parse_tile(tag)]), tag) + elif self._is_discard(tag): + print('->', TilesConverter.to_one_line_string([self._parse_tile(tag)]), tag) + elif self._is_init_tag(tag): + hands = { + 0: [int(x) for x in self._get_attribute_content(tag, 'hai0').split(',')], + 1: [int(x) for x in self._get_attribute_content(tag, 'hai1').split(',')], + 2: [int(x) for x in self._get_attribute_content(tag, 'hai2').split(',')], + 3: [int(x) for x in self._get_attribute_content(tag, 'hai3').split(',')], + } + print('Initial hand:', TilesConverter.to_one_line_string(hands[self.player_position])) + else: + print(tag) if not dry_run and tag == self.stop_tag: break @@ -72,10 +90,6 @@ def reproduce(self, dry_run=False): table.player.init_hand(hands[self.player_position]) - if player_draw_regex.match(tag) and 'UN' not in tag: - tile = self.decoder.parse_tile(tag) - table.player.draw_tile(tile) - if discard_regex.match(tag) and 'DORA' not in tag: tile = self.decoder.parse_tile(tag) player_sign = tag.upper()[1] @@ -198,6 +212,35 @@ def _parse_rounds(self, log_content): return rounds[1:] + def _is_discard(self, tag): + skip_tags = [' Date: Sat, 25 Aug 2018 20:19:08 +0800 Subject: [PATCH 010/126] Issue #48. Improve closed kan call logic --- project/game/ai/first_version/main.py | 20 +++--- .../game/ai/first_version/strategies/main.py | 4 +- .../game/ai/first_version/tests/tests_ai.py | 65 +++++++++---------- .../ai/first_version/tests/tests_defence.py | 2 +- project/game/player.py | 26 ++++---- project/tenhou/client.py | 4 ++ 6 files changed, 59 insertions(+), 62 deletions(-) diff --git a/project/game/ai/first_version/main.py b/project/game/ai/first_version/main.py index 3ba00147..01ceff6d 100644 --- a/project/game/ai/first_version/main.py +++ b/project/game/ai/first_version/main.py @@ -90,7 +90,7 @@ def discard_tile(self, discard_tile): results, shanten = self.calculate_outs(self.player.tiles, self.player.closed_hand, - self.player.open_hand_34_tiles) + self.player.meld_34_tiles) selected_tile = self.process_discard_options_and_select_tile_to_discard(results, shanten) @@ -140,7 +140,7 @@ def calculate_outs(self, tiles, closed_hand, open_sets_34=None): """ 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) + is_agari = self.agari.is_agari(tiles_34, self.player.meld_34_tiles) results = [] for hand_tile in range(0, 34): @@ -431,8 +431,9 @@ def should_call_kan(self, tile, open_kan): tile_34 = tile // 4 tiles_34 = TilesConverter.to_34_array(self.player.tiles) + closed_hand_34 = TilesConverter.to_34_array(self.player.closed_hand) - pon_melds = [x for x in self.player.open_hand_34_tiles if is_pon(x)] + pon_melds = [x for x in self.player.meld_34_tiles if is_pon(x)] # let's check can we upgrade opened pon to the kan if pon_melds: @@ -447,17 +448,14 @@ def should_call_kan(self, tile, open_kan): if open_kan: count_of_needed_tiles = 3 - # we have 3 tiles in our hand, - # so we can try to call closed meld + # we can try to call closed meld if closed_hand_34[tile_34] == count_of_needed_tiles: + melds = self.player.meld_34_tiles + previous_shanten = self.shanten.calculate_shanten(tiles_34, melds) + if not open_kan: - # to correctly count shanten in the hand - # we had do subtract drown tile tiles_34[tile_34] -= 1 - melds = self.player.open_hand_34_tiles - previous_shanten = self.shanten.calculate_shanten(tiles_34, melds) - melds += [[tile_34, tile_34, tile_34]] new_shanten = self.shanten.calculate_shanten(tiles_34, melds) @@ -491,7 +489,7 @@ def calculate_second_level_ukeire(self, discard_option): results, shanten = self.calculate_outs( tiles, self.player.closed_hand, - self.player.open_hand_34_tiles + self.player.meld_34_tiles ) results = [x for x in results if x.shanten == discard_option.shanten - 1] sum_tiles += sum([x.ukeire for x in results]) diff --git a/project/game/ai/first_version/strategies/main.py b/project/game/ai/first_version/strategies/main.py index bf714801..9ad68c14 100644 --- a/project/game/ai/first_version/strategies/main.py +++ b/project/game/ai/first_version/strategies/main.py @@ -178,7 +178,7 @@ def try_to_call_meld(self, tile, is_kamicha_discard): if best_meld_34: # we need to calculate count of shanten with supposed meld # to prevent bad hand openings - melds = self.player.open_hand_34_tiles + [best_meld_34] + melds = self.player.meld_34_tiles + [best_meld_34] outs_results, shanten = self.player.ai.calculate_outs(new_tiles, closed_hand, melds) # each strategy can use their own value to min shanten number @@ -258,7 +258,7 @@ def _find_best_meld_to_open(self, possible_melds, completed_hand): results = [] for meld in possible_melds: - melds = self.player.open_hand_34_tiles + [meld] + melds = self.player.meld_34_tiles + [meld] shanten = self.player.ai.shanten.calculate_shanten(completed_hand_34, melds) results.append({'shanten': shanten, 'meld': meld}) diff --git a/project/game/ai/first_version/tests/tests_ai.py b/project/game/ai/first_version/tests/tests_ai.py index 87713b16..a6b4061f 100644 --- a/project/game/ai/first_version/tests/tests_ai.py +++ b/project/game/ai/first_version/tests/tests_ai.py @@ -319,50 +319,45 @@ def test_opened_kan(self): tile = self._string_to_136_tile(sou='1') self.assertEqual(player.should_call_kan(tile, True), Meld.KAN) - def test_closed_kan_and_riichi(self): + def test_dont_call_kan_in_defence_mode(self): table = Table() - table.count_of_remaining_tiles = 60 - player = table.player - player.scores = 25000 - kan_tiles = self._string_to_136_array(pin='7777') - tiles = self._string_to_136_array(pin='568', sou='1235788') + kan_tiles[:3] - player.init_hand(tiles) + tiles = self._string_to_136_array(man='12589', sou='111459', pin='12') + table.player.init_hand(tiles) - # +3 to avoid tile duplication of 7 pin - tile = kan_tiles[3] - player.draw_tile(tile) + table.add_called_riichi(1) - kan_type = player.should_call_kan(tile, False) - self.assertEqual(kan_type, Meld.KAN) + tile = self._string_to_136_tile(sou='1') + self.assertEqual(table.player.should_call_kan(tile, False), None) - meld = Meld() - meld.type = Meld.KAN - meld.tiles = kan_tiles - meld.called_tile = tile - meld.who = 0 - meld.from_who = 0 - meld.opened = False + def test_closed_kan_and_wrong_shanten_number_calculation(self): + """ + Bot tried to call riichi with 567m666p14578s + [9999s] hand + """ + table = Table() + player = table.player - # replacement from the dead wall - player.draw_tile(self._string_to_136_tile(pin='4')) - table.add_called_meld(meld.who, meld) - discard = player.discard_tile() + tiles = self._string_to_136_array(man='56', sou='14578999', pin='666') + player.init_hand(tiles) + tile = self._string_to_136_tile(man='7') + player.melds.append(self._make_meld(Meld.KAN, False, sou='9999')) + player.draw_tile(tile) - self.assertEqual(self._to_string([discard]), '8p') - self.assertEqual(player.can_call_riichi(), True) + player.discard_tile() - # with closed kan we can't call riichi - player.melds[0].opened = True - self.assertEqual(player.can_call_riichi(), False) + # bot not in the tempai, because all 9s in the closed kan + self.assertEqual(player.ai.previous_shanten, 1) - def test_dont_call_kan_in_defence_mode(self): + def test_closed_kan_and_not_necessary_call(self): + """ + Bot tried to call closed kan with 568m669p1478999s + 9s hand + """ table = Table() + player = table.player - tiles = self._string_to_136_array(man='12589', sou='111459', pin='12') - table.player.init_hand(tiles) - - table.add_called_riichi(1) + tiles = self._string_to_136_array(man='568', sou='1478999', pin='669') + player.init_hand(tiles) + tile = self._string_to_136_tile(sou='9') + player.draw_tile(tile) - tile = self._string_to_136_tile(sou='1') - self.assertEqual(table.player.should_call_kan(tile, False), None) + self.assertEqual(player.should_call_kan(tile, False), None) diff --git a/project/game/ai/first_version/tests/tests_defence.py b/project/game/ai/first_version/tests/tests_defence.py index 17ce0e02..6550b149 100644 --- a/project/game/ai/first_version/tests/tests_defence.py +++ b/project/game/ai/first_version/tests/tests_defence.py @@ -152,7 +152,7 @@ def test_should_go_for_defence_and_good_hand_with_drawn_tile(self): results, shanten = table.player.ai.calculate_outs(table.player.tiles, table.player.closed_hand, - table.player.open_hand_34_tiles) + table.player.meld_34_tiles) selected_tile = table.player.ai.process_discard_options_and_select_tile_to_discard(results, shanten) self.assertEqual(table.player.ai.defence.should_go_to_defence_mode(selected_tile), False) diff --git a/project/game/player.py b/project/game/player.py index 20cddfd5..a3e789bf 100644 --- a/project/game/player.py +++ b/project/game/player.py @@ -116,6 +116,19 @@ def meld_tiles(self): result.extend(meld.tiles) return result + @property + def meld_34_tiles(self): + """ + Array of array with 34 tiles indices + :return: array + """ + melds = [x.tiles for x in self.melds] + melds = copy.deepcopy(melds) + results = [] + for meld in melds: + results.append([meld[0] // 4, meld[1] // 4, meld[2] // 4]) + return results + class Player(PlayerInterface): ai = None @@ -236,19 +249,6 @@ def closed_hand(self): tiles = self.tiles[:] return [item for item in tiles if item not in self.meld_tiles] - @property - def open_hand_34_tiles(self): - """ - Array of array with 34 tiles indices - :return: array - """ - melds = [x.tiles for x in self.melds if x.opened] - melds = copy.deepcopy(melds) - results = [] - for meld in melds: - results.append([meld[0] // 4, meld[1] // 4, meld[2] // 4]) - return results - @property def valued_honors(self): return [CHUN, HAKU, HATSU, self.table.round_wind, self.player_wind] diff --git a/project/tenhou/client.py b/project/tenhou/client.py index abf26d82..e9487952 100644 --- a/project/tenhou/client.py +++ b/project/tenhou/client.py @@ -273,6 +273,10 @@ def start_game(self): drawn_tile = self.decoder.parse_tile(message) + if drawn_tile == 26: + d = 1 + print(TilesConverter.to_one_line_string(self.player.tiles)) + if not main_player.in_riichi: logger.info('Hand: {}'.format(main_player.format_hand_for_print(drawn_tile))) From f22e4a421876e850869e66fdce9f270faa17f337 Mon Sep 17 00:00:00 2001 From: Nihisil Date: Sat, 25 Aug 2018 20:24:00 +0800 Subject: [PATCH 011/126] Remove debug code --- project/tenhou/client.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/project/tenhou/client.py b/project/tenhou/client.py index e9487952..abf26d82 100644 --- a/project/tenhou/client.py +++ b/project/tenhou/client.py @@ -273,10 +273,6 @@ def start_game(self): drawn_tile = self.decoder.parse_tile(message) - if drawn_tile == 26: - d = 1 - print(TilesConverter.to_one_line_string(self.player.tiles)) - if not main_player.in_riichi: logger.info('Hand: {}'.format(main_player.format_hand_for_print(drawn_tile))) From 1cbe3948b716dd76ebdebd65d404e4fa1af81506 Mon Sep 17 00:00:00 2001 From: Nihisil Date: Sat, 25 Aug 2018 20:42:53 +0800 Subject: [PATCH 012/126] Issue #40. Allow to call closed kan when in riichi --- project/game/ai/base/main.py | 3 +- project/game/ai/first_version/main.py | 20 +++++++----- .../game/ai/first_version/tests/tests_ai.py | 8 ++--- project/game/player.py | 10 ++---- project/tenhou/client.py | 31 ++++++++++--------- 5 files changed, 34 insertions(+), 38 deletions(-) diff --git a/project/game/ai/base/main.py b/project/game/ai/base/main.py index 65318521..eec5ee19 100644 --- a/project/game/ai/base/main.py +++ b/project/game/ai/base/main.py @@ -56,11 +56,12 @@ def should_call_riichi(self): """ return False - def should_call_kan(self, tile, is_open_kan): + def should_call_kan(self, tile, is_open_kan, from_riichi=False): """ When bot can call kan or chankan this method will be called :param tile: 136 tile format :param is_open_kan: boolean + :param from_riichi: boolean :return: kan type (Meld.KAN, Meld.CHANKAN) or None """ return False diff --git a/project/game/ai/first_version/main.py b/project/game/ai/first_version/main.py index 01ceff6d..2558874d 100644 --- a/project/game/ai/first_version/main.py +++ b/project/game/ai/first_version/main.py @@ -404,14 +404,20 @@ def should_call_riichi(self): return True - def should_call_kan(self, tile, open_kan): + def should_call_kan(self, tile, open_kan, from_riichi=False): """ Method will decide should we call a kan, or upgrade pon to kan :param tile: 136 tile format :param open_kan: boolean + :param from_riichi: boolean :return: kan type """ + + # we can't call kan on the latest tile + if self.table.count_of_remaining_tiles <= 1: + return None + # we don't need to add dora for other players if self.player.ai.in_defence: return None @@ -443,17 +449,15 @@ def should_call_kan(self, tile, open_kan): if tile_34 in meld: return Meld.CHANKAN - count_of_needed_tiles = 4 - # for open kan 3 tiles is enough to call a kan - if open_kan: - count_of_needed_tiles = 3 - # we can try to call closed meld - if closed_hand_34[tile_34] == count_of_needed_tiles: + if closed_hand_34[tile_34] == 3: + if not open_kan and not from_riichi: + tiles_34[tile_34] += 1 + melds = self.player.meld_34_tiles previous_shanten = self.shanten.calculate_shanten(tiles_34, melds) - if not open_kan: + if not open_kan and not from_riichi: tiles_34[tile_34] -= 1 melds += [[tile_34, tile_34, tile_34]] diff --git a/project/game/ai/first_version/tests/tests_ai.py b/project/game/ai/first_version/tests/tests_ai.py index a6b4061f..e898daf0 100644 --- a/project/game/ai/first_version/tests/tests_ai.py +++ b/project/game/ai/first_version/tests/tests_ai.py @@ -18,16 +18,16 @@ def test_set_is_tempai_flag_to_the_player(self): tile = self._string_to_136_array(man='9')[0] player.init_hand(tiles) player.draw_tile(tile) - player.discard_tile() + self.assertEqual(player.in_tempai, False) tiles = self._string_to_136_array(sou='11145677', pin='345', man='56') tile = self._string_to_136_array(man='9')[0] player.init_hand(tiles) player.draw_tile(tile) - player.discard_tile() + self.assertEqual(player.in_tempai, True) def test_not_open_hand_in_riichi(self): @@ -269,7 +269,6 @@ def test_call_closed_kan(self): tiles = self._string_to_136_array(man='12223', sou='111456', pin='12') player.init_hand(tiles) tile = self._string_to_136_tile(man='2') - player.draw_tile(tile) # it is pretty stupid to call closed kan with 2m self.assertEqual(player.should_call_kan(tile, False), None) @@ -277,7 +276,6 @@ def test_call_closed_kan(self): tiles = self._string_to_136_array(man='12223', sou='111456', pin='12') player.init_hand(tiles) tile = self._string_to_136_tile(sou='1') - player.draw_tile(tile) # call closed kan with 1s is fine self.assertEqual(player.should_call_kan(tile, False), Meld.KAN) @@ -342,7 +340,6 @@ def test_closed_kan_and_wrong_shanten_number_calculation(self): tile = self._string_to_136_tile(man='7') player.melds.append(self._make_meld(Meld.KAN, False, sou='9999')) player.draw_tile(tile) - player.discard_tile() # bot not in the tempai, because all 9s in the closed kan @@ -358,6 +355,5 @@ def test_closed_kan_and_not_necessary_call(self): tiles = self._string_to_136_array(man='568', sou='1478999', pin='669') player.init_hand(tiles) tile = self._string_to_136_tile(sou='9') - player.draw_tile(tile) self.assertEqual(player.should_call_kan(tile, False), None) diff --git a/project/game/player.py b/project/game/player.py index a3e789bf..e2d886b3 100644 --- a/project/game/player.py +++ b/project/game/player.py @@ -198,14 +198,8 @@ def formal_riichi_conditions(self): self.table.count_of_remaining_tiles > 4 ]) - def should_call_kan(self, tile, open_kan): - """ - Method will decide should we call a kan, - or upgrade pon to kan - :param tile: 136 tile format - :return: - """ - return self.ai.should_call_kan(tile, open_kan) + def should_call_kan(self, tile, open_kan, from_riichi=False): + return self.ai.should_call_kan(tile, open_kan, from_riichi) def should_call_win(self, tile, enemy_seat): return self.ai.should_call_win(tile, enemy_seat) diff --git a/project/tenhou/client.py b/project/tenhou/client.py index abf26d82..f722c3ef 100644 --- a/project/tenhou/client.py +++ b/project/tenhou/client.py @@ -273,26 +273,26 @@ def start_game(self): drawn_tile = self.decoder.parse_tile(message) - if not main_player.in_riichi: - logger.info('Hand: {}'.format(main_player.format_hand_for_print(drawn_tile))) + logger.info('Hand: {}'.format(main_player.format_hand_for_print(drawn_tile))) - self.player.draw_tile(drawn_tile) + kan_type = self.player.should_call_kan(drawn_tile, False, main_player.in_riichi) + if kan_type: + self._random_sleep() - kan_type = self.player.should_call_kan(drawn_tile, False) - if kan_type and self.table.count_of_remaining_tiles > 1: - self._random_sleep() + if kan_type == Meld.CHANKAN: + meld_type = 5 + else: + meld_type = 4 - if kan_type == Meld.CHANKAN: - meld_type = 5 - else: - meld_type = 4 - self._send_message(''.format(meld_type, drawn_tile)) - logger.info('We called a closed kan\chankan set!') - continue + self._send_message(''.format(meld_type, drawn_tile)) + logger.info('We called a closed kan\chankan set!') + continue - discarded_tile = self.player.discard_tile() - logger.info('Discard: {}'.format(TilesConverter.to_one_line_string([discarded_tile]))) + if not main_player.in_riichi: + logger.info('Hand: {}'.format(main_player.format_hand_for_print(drawn_tile))) + self.player.draw_tile(drawn_tile) + discarded_tile = self.player.discard_tile() can_call_riichi = main_player.can_call_riichi() # let's call riichi @@ -307,6 +307,7 @@ def start_game(self): # tenhou format: self._send_message(''.format(discarded_tile)) + logger.info('Discard: {}'.format(TilesConverter.to_one_line_string([discarded_tile]))) logger.info('Remaining tiles: {}'.format(self.table.count_of_remaining_tiles)) From 29485abbc7ce2a3c6db2791915bec4011d0b6bf7 Mon Sep 17 00:00:00 2001 From: Nihisil Date: Mon, 27 Aug 2018 18:39:23 +0800 Subject: [PATCH 013/126] Fix failed tests --- .../game/ai/first_version/tests/tests_ai.py | 64 +++++++++---------- project/tenhou/client.py | 2 - 2 files changed, 32 insertions(+), 34 deletions(-) diff --git a/project/game/ai/first_version/tests/tests_ai.py b/project/game/ai/first_version/tests/tests_ai.py index e898daf0..55cc614d 100644 --- a/project/game/ai/first_version/tests/tests_ai.py +++ b/project/game/ai/first_version/tests/tests_ai.py @@ -239,83 +239,83 @@ def test_using_tiles_of_different_suit_for_chi(self): def test_upgrade_opened_pon_to_kan(self): table = Table() - player = table.player + table.count_of_remaining_tiles = 10 tiles = self._string_to_136_array(man='34445', sou='123456', pin='89') - player.init_hand(tiles) + table.player.init_hand(tiles) tile = self._string_to_136_tile(man='4') - player.draw_tile(tile) + table.player.draw_tile(tile) - self.assertEqual(player.should_call_kan(tile, False), None) + self.assertEqual(table.player.should_call_kan(tile, False), None) - player.add_called_meld(self._make_meld(Meld.PON, man='444')) + table.player.add_called_meld(self._make_meld(Meld.PON, man='444')) - self.assertEqual(len(player.melds), 1) - self.assertEqual(len(player.tiles), 14) - self.assertEqual(player.should_call_kan(tile, False), Meld.CHANKAN) + self.assertEqual(len(table.player.melds), 1) + self.assertEqual(len(table.player.tiles), 14) + self.assertEqual(table.player.should_call_kan(tile, False), Meld.CHANKAN) - player.discard_tile() - player.draw_tile(tile) - player.add_called_meld(self._make_meld(Meld.CHANKAN, man='4444')) + table.player.discard_tile() + table.player.draw_tile(tile) + table.player.add_called_meld(self._make_meld(Meld.CHANKAN, man='4444')) - self.assertEqual(len(player.melds), 1) - self.assertEqual(player.melds[0].type, Meld.CHANKAN) - self.assertEqual(len(player.tiles), 13) + self.assertEqual(len(table.player.melds), 1) + self.assertEqual(table.player.melds[0].type, Meld.CHANKAN) + self.assertEqual(len(table.player.tiles), 13) def test_call_closed_kan(self): table = Table() - player = table.player + table.count_of_remaining_tiles = 10 tiles = self._string_to_136_array(man='12223', sou='111456', pin='12') - player.init_hand(tiles) + table.player.init_hand(tiles) tile = self._string_to_136_tile(man='2') # it is pretty stupid to call closed kan with 2m - self.assertEqual(player.should_call_kan(tile, False), None) + self.assertEqual(table.player.should_call_kan(tile, False), None) tiles = self._string_to_136_array(man='12223', sou='111456', pin='12') - player.init_hand(tiles) + table.player.init_hand(tiles) tile = self._string_to_136_tile(sou='1') # call closed kan with 1s is fine - self.assertEqual(player.should_call_kan(tile, False), Meld.KAN) + self.assertEqual(table.player.should_call_kan(tile, False), Meld.KAN) def test_opened_kan(self): table = Table() - player = table.player + table.count_of_remaining_tiles = 10 tiles = self._string_to_136_array(man='299', sou='111456', pin='1', honors='111') - player.init_hand(tiles) + table.player.init_hand(tiles) # to rebuild all caches - player.draw_tile(self._string_to_136_tile(pin='9')) - player.discard_tile() + table.player.draw_tile(self._string_to_136_tile(pin='9')) + table.player.discard_tile() # our hand is closed, we don't need to call opened kan here tile = self._string_to_136_tile(sou='1') - self.assertEqual(player.should_call_kan(tile, True), None) + self.assertEqual(table.player.should_call_kan(tile, True), None) - player.add_called_meld(self._make_meld(Meld.PON, honors='111')) + table.player.add_called_meld(self._make_meld(Meld.PON, honors='111')) # our hand is open, but it is not tempai # we don't need to open kan here tile = self._string_to_136_tile(sou='1') - self.assertEqual(player.should_call_kan(tile, True), None) + self.assertEqual(table.player.should_call_kan(tile, True), None) table = Table() - player = table.player + table.count_of_remaining_tiles = 10 tiles = self._string_to_136_array(man='2399', sou='111456', honors='111') - player.init_hand(tiles) - player.add_called_meld(self._make_meld(Meld.PON, honors='111')) + table.player.init_hand(tiles) + table.player.add_called_meld(self._make_meld(Meld.PON, honors='111')) # to rebuild all caches - player.draw_tile(self._string_to_136_tile(pin='9')) - player.discard_tile() + table.player.draw_tile(self._string_to_136_tile(pin='9')) + table.player.discard_tile() # our hand is open, in tempai and with a good wait tile = self._string_to_136_tile(sou='1') - self.assertEqual(player.should_call_kan(tile, True), Meld.KAN) + self.assertEqual(table.player.should_call_kan(tile, True), Meld.KAN) def test_dont_call_kan_in_defence_mode(self): table = Table() diff --git a/project/tenhou/client.py b/project/tenhou/client.py index f722c3ef..9d4d7caf 100644 --- a/project/tenhou/client.py +++ b/project/tenhou/client.py @@ -289,8 +289,6 @@ def start_game(self): continue if not main_player.in_riichi: - logger.info('Hand: {}'.format(main_player.format_hand_for_print(drawn_tile))) - self.player.draw_tile(drawn_tile) discarded_tile = self.player.discard_tile() can_call_riichi = main_player.can_call_riichi() From 352a9b818da63d6d0ace4ddfb7499e243fa4bcaf Mon Sep 17 00:00:00 2001 From: Nihisil Date: Mon, 27 Aug 2018 18:59:02 +0800 Subject: [PATCH 014/126] Issue #46. Improve hand building logic --- project/game/ai/first_version/main.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/project/game/ai/first_version/main.py b/project/game/ai/first_version/main.py index 2558874d..43f024ff 100644 --- a/project/game/ai/first_version/main.py +++ b/project/game/ai/first_version/main.py @@ -273,10 +273,6 @@ def choose_tile_to_discard(self, results: [DiscardOption]) -> DiscardOption: if discard_option.ukeire >= first_option.ukeire - ukeire_borders: possible_options.append(discard_option) - if first_option.shanten <= 1: - # let's sort tiles by value and let's choose less valuable tile to discard - return sorted(possible_options, key=lambda x: x.valuation)[0] - # as second step # let's choose tiles that are close to the max ukeire2 tile for x in possible_options: @@ -316,7 +312,19 @@ def choose_tile_to_discard(self, results: [DiscardOption]) -> DiscardOption: # there are no isolated tiles # let's discard tile with greater ukeire2 - return sorted(filtered_options, key=lambda x: -x.ukeire_second)[0] + filtered_options = sorted(filtered_options, key=lambda x: -x.ukeire_second) + first_option = filtered_options[0] + + other_tiles_with_same_ukeire_second = [x for x in filtered_options + if x.ukeire_second == first_option.ukeire_second] + + # it will happen with shanten=1, all tiles will have ukeire_second == 0 + if other_tiles_with_same_ukeire_second: + # let's sort tiles by value and let's choose less valuable tile to discard + return sorted(other_tiles_with_same_ukeire_second, key=lambda x: x.valuation)[0] + + # we have only one candidate to discard with greater ukeire + return first_option def process_discard_option(self, discard_option, closed_hand, force_discard=False): self.waiting = discard_option.waiting From ed567644080a3d12e9112160edd81eeb8e347d3e Mon Sep 17 00:00:00 2001 From: Nihisil Date: Mon, 27 Aug 2018 19:21:46 +0800 Subject: [PATCH 015/126] Issue #47. Fix bug with dora tiles in hand building logic --- project/game/ai/discard.py | 4 +++- project/game/ai/first_version/main.py | 6 +++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/project/game/ai/discard.py b/project/game/ai/discard.py index 3a1ac4f6..5ee075b7 100644 --- a/project/game/ai/discard.py +++ b/project/game/ai/discard.py @@ -111,7 +111,9 @@ def calculate_value(self, shanten=None): value += suit_tile_grades[simplified_tile] count_of_dora = plus_dora(self.tile_to_discard * 4, self.player.table.dora_indicators) - if is_aka_dora(self.tile_to_discard * 4, self.player.table.has_open_tanyao): + + tile_136 = self.find_tile_in_hand(self.player.closed_hand) + if is_aka_dora(tile_136, self.player.table.has_aka_dora): count_of_dora += 1 self.count_of_dora = count_of_dora diff --git a/project/game/ai/first_version/main.py b/project/game/ai/first_version/main.py index 43f024ff..ac94a52d 100644 --- a/project/game/ai/first_version/main.py +++ b/project/game/ai/first_version/main.py @@ -287,9 +287,9 @@ def choose_tile_to_discard(self, results: [DiscardOption]) -> DiscardOption: filter_percentage ) - dora_tiles = [x for x in filtered_options if x.count_of_dora > 0] + tiles_without_dora = [x for x in filtered_options if x.count_of_dora == 0] # we have only dora candidates to discard - if len(dora_tiles) == len(filtered_options): + if not tiles_without_dora: min_dora = min([x.count_of_dora for x in filtered_options]) min_dora_list = [x for x in filtered_options if x.count_of_dora == min_dora] @@ -298,7 +298,7 @@ def choose_tile_to_discard(self, results: [DiscardOption]) -> DiscardOption: second_filter_percentage = 10 filtered_options = self._filter_list_by_percentage( - filtered_options, + tiles_without_dora, 'ukeire_second', second_filter_percentage ) From ef60ab336db85a9790d664796f116bafdad0e811 Mon Sep 17 00:00:00 2001 From: Nihisil Date: Mon, 27 Aug 2018 20:28:16 +0800 Subject: [PATCH 016/126] Issue #60. Add additional value to tiles close to the dora --- project/game/ai/discard.py | 18 +++++++++++++++++- .../ai/first_version/tests/tests_discards.py | 16 ++++++++++++++-- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/project/game/ai/discard.py b/project/game/ai/discard.py index 5ee075b7..9355ca7a 100644 --- a/project/game/ai/discard.py +++ b/project/game/ai/discard.py @@ -107,9 +107,25 @@ def calculate_value(self, shanten=None): # usual hand else: suit_tile_grades = [10, 20, 40, 50, 30, 50, 40, 20, 10] + simplified_tile = simplify(self.tile_to_discard) value += suit_tile_grades[simplified_tile] + for indicator in self.player.table.dora_indicators: + simplified_indicator = simplify(indicator // 4) + simplified_dora = simplified_indicator + 1 + # indicator is 9 man + if simplified_dora == 9: + simplified_dora = 0 + + # tile close to the dora + if simplified_tile + 1 == simplified_dora or simplified_tile - 1 == simplified_dora: + value += 1000 + + # tile not far away from dora + if simplified_tile + 2 == simplified_dora or simplified_tile - 2 == simplified_dora: + value += 100 + count_of_dora = plus_dora(self.tile_to_discard * 4, self.player.table.dora_indicators) tile_136 = self.find_tile_in_hand(self.player.closed_hand) @@ -117,7 +133,7 @@ def calculate_value(self, shanten=None): count_of_dora += 1 self.count_of_dora = count_of_dora - value += count_of_dora * 50 + value += count_of_dora * 10000 if is_honor(self.tile_to_discard): # depends on how much honor tiles were discarded diff --git a/project/game/ai/first_version/tests/tests_discards.py b/project/game/ai/first_version/tests/tests_discards.py index 30606e57..b6adcf15 100644 --- a/project/game/ai/first_version/tests/tests_discards.py +++ b/project/game/ai/first_version/tests/tests_discards.py @@ -163,13 +163,25 @@ def test_calculate_suit_tiles_value_and_dora(self): tile = self._string_to_34_tile(sou='1') option = DiscardOption(player, tile, 0, [], 0) - self.assertEqual(option.valuation, 160) + self.assertEqual(option.valuation, 10110) # 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) + self.assertEqual(option.valuation, 20110) + + # 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, 1120) + + # 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, 240) def test_discard_not_valuable_honor_first(self): table = Table() From 1baddb4db1d073f0b446caee8ba98e6dcddb4f5a Mon Sep 17 00:00:00 2001 From: Nihisil Date: Mon, 27 Aug 2018 20:36:58 +0800 Subject: [PATCH 017/126] Small refactoring --- project/game/ai/discard.py | 10 +++++++--- project/game/ai/first_version/tests/tests_discards.py | 8 ++++---- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/project/game/ai/discard.py b/project/game/ai/discard.py index 9355ca7a..8626ca49 100644 --- a/project/game/ai/discard.py +++ b/project/game/ai/discard.py @@ -7,6 +7,10 @@ class DiscardOption(object): + DORA_VALUE = 10000 + DORA_FIRST_NEIGHBOUR = 1000 + DORA_SECOND_NEIGHBOUR = 1000 + player = None # in 34 tile format @@ -120,11 +124,11 @@ def calculate_value(self, shanten=None): # tile close to the dora if simplified_tile + 1 == simplified_dora or simplified_tile - 1 == simplified_dora: - value += 1000 + value += DiscardOption.DORA_FIRST_NEIGHBOUR # tile not far away from dora if simplified_tile + 2 == simplified_dora or simplified_tile - 2 == simplified_dora: - value += 100 + value += DiscardOption.DORA_SECOND_NEIGHBOUR count_of_dora = plus_dora(self.tile_to_discard * 4, self.player.table.dora_indicators) @@ -133,7 +137,7 @@ def calculate_value(self, shanten=None): count_of_dora += 1 self.count_of_dora = count_of_dora - value += count_of_dora * 10000 + value += count_of_dora * DiscardOption.DORA_VALUE if is_honor(self.tile_to_discard): # depends on how much honor tiles were discarded diff --git a/project/game/ai/first_version/tests/tests_discards.py b/project/game/ai/first_version/tests/tests_discards.py index b6adcf15..0c256eba 100644 --- a/project/game/ai/first_version/tests/tests_discards.py +++ b/project/game/ai/first_version/tests/tests_discards.py @@ -163,25 +163,25 @@ def test_calculate_suit_tiles_value_and_dora(self): tile = self._string_to_34_tile(sou='1') option = DiscardOption(player, tile, 0, [], 0) - self.assertEqual(option.valuation, 10110) + 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, 20110) + 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, 1120) + 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, 240) + self.assertEqual(option.valuation, DiscardOption.DORA_SECOND_NEIGHBOUR + 140) def test_discard_not_valuable_honor_first(self): table = Table() From e7bd1fc2c8935cf64b75c4081df2f69ab06a49cc Mon Sep 17 00:00:00 2001 From: Nihisil Date: Mon, 27 Aug 2018 20:42:21 +0800 Subject: [PATCH 018/126] Fix typo --- project/game/ai/discard.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/game/ai/discard.py b/project/game/ai/discard.py index 8626ca49..5bce4fde 100644 --- a/project/game/ai/discard.py +++ b/project/game/ai/discard.py @@ -9,7 +9,7 @@ class DiscardOption(object): DORA_VALUE = 10000 DORA_FIRST_NEIGHBOUR = 1000 - DORA_SECOND_NEIGHBOUR = 1000 + DORA_SECOND_NEIGHBOUR = 100 player = None From 51ca1b56b66c399aa22fe6ceb93f5a173e8368d6 Mon Sep 17 00:00:00 2001 From: Nihisil Date: Mon, 27 Aug 2018 20:57:09 +0800 Subject: [PATCH 019/126] Small refactoring of tanyao tests --- .../tests/strategies/tests_tanyao.py | 138 +++++++----------- 1 file changed, 55 insertions(+), 83 deletions(-) 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 ae3c7902..802241d3 100644 --- a/project/game/ai/first_version/tests/strategies/tests_tanyao.py +++ b/project/game/ai/first_version/tests/strategies/tests_tanyao.py @@ -11,100 +11,87 @@ class TanyaoStrategyTestCase(unittest.TestCase, TestMixin): - def _make_table(self): - table = Table() - table.has_open_tanyao = True - return table + def setUp(self): + self.table = self._make_table() + self.player = self.table.player def test_should_activate_strategy_and_terminal_pon_sets(self): - table = self._make_table() - player = table.player - strategy = TanyaoStrategy(BaseStrategy.TANYAO, player) + strategy = TanyaoStrategy(BaseStrategy.TANYAO, self.player) tiles = self._string_to_136_array(sou='234', man='3459', pin='233', honors='111') - player.init_hand(tiles) + self.player.init_hand(tiles) self.assertEqual(strategy.should_activate_strategy(), False) tiles = self._string_to_136_array(sou='234', man='3459', pin='233999') - player.init_hand(tiles) + self.player.init_hand(tiles) self.assertEqual(strategy.should_activate_strategy(), False) tiles = self._string_to_136_array(sou='234', man='3459', pin='233444') - player.init_hand(tiles) + self.player.init_hand(tiles) self.assertEqual(strategy.should_activate_strategy(), True) def test_should_activate_strategy_and_terminal_pairs(self): - table = self._make_table() - player = table.player - strategy = TanyaoStrategy(BaseStrategy.TANYAO, player) + strategy = TanyaoStrategy(BaseStrategy.TANYAO, self.player) tiles = self._string_to_136_array(sou='234', man='3459', pin='2399', honors='11') - player.init_hand(tiles) + self.player.init_hand(tiles) self.assertEqual(strategy.should_activate_strategy(), False) tiles = self._string_to_136_array(sou='234', man='345669', pin='2399') - player.init_hand(tiles) + self.player.init_hand(tiles) self.assertEqual(strategy.should_activate_strategy(), True) def test_should_activate_strategy_and_valued_pair(self): - table = self._make_table() - player = table.player - strategy = TanyaoStrategy(BaseStrategy.TANYAO, player) + strategy = TanyaoStrategy(BaseStrategy.TANYAO, self.player) tiles = self._string_to_136_array(man='23446679', sou='345', honors='55') - player.init_hand(tiles) + self.player.init_hand(tiles) self.assertEqual(strategy.should_activate_strategy(), False) tiles = self._string_to_136_array(man='23446679', sou='345', honors='22') - player.init_hand(tiles) + self.player.init_hand(tiles) self.assertEqual(strategy.should_activate_strategy(), True) def test_should_activate_strategy_and_chitoitsu_like_hand(self): - table = self._make_table() - player = table.player - strategy = TanyaoStrategy(BaseStrategy.TANYAO, player) + strategy = TanyaoStrategy(BaseStrategy.TANYAO, self.player) tiles = self._string_to_136_array(sou='223388', man='3344', pin='6687') - player.init_hand(tiles) + self.player.init_hand(tiles) self.assertEqual(strategy.should_activate_strategy(), False) def test_should_activate_strategy_and_already_completed_sided_set(self): - table = self._make_table() - player = table.player - strategy = TanyaoStrategy(BaseStrategy.TANYAO, player) + strategy = TanyaoStrategy(BaseStrategy.TANYAO, self.player) tiles = self._string_to_136_array(sou='123234', man='3459', pin='234') - player.init_hand(tiles) + self.player.init_hand(tiles) self.assertEqual(strategy.should_activate_strategy(), False) tiles = self._string_to_136_array(sou='234789', man='3459', pin='234') - player.init_hand(tiles) + self.player.init_hand(tiles) self.assertEqual(strategy.should_activate_strategy(), False) tiles = self._string_to_136_array(sou='234', man='1233459', pin='234') - player.init_hand(tiles) + self.player.init_hand(tiles) self.assertEqual(strategy.should_activate_strategy(), False) tiles = self._string_to_136_array(sou='234', man='3457899', pin='234') - player.init_hand(tiles) + self.player.init_hand(tiles) self.assertEqual(strategy.should_activate_strategy(), False) tiles = self._string_to_136_array(sou='234', man='3459', pin='122334') - player.init_hand(tiles) + self.player.init_hand(tiles) self.assertEqual(strategy.should_activate_strategy(), False) tiles = self._string_to_136_array(sou='234', man='3459', pin='234789') - player.init_hand(tiles) + self.player.init_hand(tiles) self.assertEqual(strategy.should_activate_strategy(), False) tiles = self._string_to_136_array(sou='223344', man='3459', pin='234') - player.init_hand(tiles) + self.player.init_hand(tiles) self.assertEqual(strategy.should_activate_strategy(), True) def test_suitable_tiles(self): - table = self._make_table() - player = table.player - strategy = TanyaoStrategy(BaseStrategy.TANYAO, player) + strategy = TanyaoStrategy(BaseStrategy.TANYAO, self.player) tile = self._string_to_136_tile(man='1') self.assertEqual(strategy.is_tile_suitable(tile), False) @@ -131,97 +118,80 @@ def test_suitable_tiles(self): self.assertEqual(strategy.is_tile_suitable(tile), True) def test_dont_open_hand_with_high_shanten(self): - table = self._make_table() - player = table.player - # with 4 shanten we don't need to aim for open tanyao tiles = self._string_to_136_array(man='369', pin='378', sou='3488', honors='123') tile = self._string_to_136_tile(sou='2') - player.init_hand(tiles) - meld, _ = player.try_to_call_meld(tile, False) + self.player.init_hand(tiles) + meld, _ = self.player.try_to_call_meld(tile, False) self.assertEqual(meld, None) # with 3 shanten we can open a hand tiles = self._string_to_136_array(man='236', pin='378', sou='3488', honors='123') tile = self._string_to_136_tile(sou='2') - player.init_hand(tiles) - meld, _ = player.try_to_call_meld(tile, True) + self.player.init_hand(tiles) + meld, _ = self.player.try_to_call_meld(tile, True) self.assertNotEqual(meld, None) def test_dont_open_hand_with_not_suitable_melds(self): - table = self._make_table() - player = table.player - tiles = self._string_to_136_array(man='33355788', sou='3479', honors='3') tile = self._string_to_136_tile(sou='8') - player.init_hand(tiles) - meld, _ = player.try_to_call_meld(tile, False) + self.player.init_hand(tiles) + meld, _ = self.player.try_to_call_meld(tile, False) self.assertEqual(meld, None) def test_open_hand_and_discard_tiles_logic(self): - table = self._make_table() - player = table.player - # 2345779m1p256s44z tiles = self._string_to_136_array(man='22345', sou='238', pin='256', honors='44') - player.init_hand(tiles) + self.player.init_hand(tiles) # if we are in tanyao # we need to discard terminals and honors tile = self._string_to_136_tile(sou='4') - meld, discard_option = player.try_to_call_meld(tile, True) - discarded_tile = table.player.discard_tile(discard_option) + meld, discard_option = self.player.try_to_call_meld(tile, True) + discarded_tile = self.player.discard_tile(discard_option) self.assertNotEqual(meld, None) self.assertEqual(self._to_string([discarded_tile]), '4z') tile = self._string_to_136_tile(pin='5') - player.draw_tile(tile) - tile_to_discard = player.discard_tile() + self.player.draw_tile(tile) + tile_to_discard = self.player.discard_tile() # we are in tanyao, so we should discard honors and terminals self.assertEqual(self._to_string([tile_to_discard]), '4z') def test_dont_count_pairs_in_already_opened_hand(self): - table = self._make_table() - player = table.player - meld = self._make_meld(Meld.PON, sou='222') - player.add_called_meld(meld) + self.player.add_called_meld(meld) tiles = self._string_to_136_array(man='33556788', sou='22266') - player.init_hand(tiles) + self.player.init_hand(tiles) tile = self._string_to_136_tile(sou='6') - meld, _ = player.try_to_call_meld(tile, False) + meld, _ = self.player.try_to_call_meld(tile, False) # even if it looks like chitoitsu we can open hand and get tempai here self.assertNotEqual(meld, None) def test_we_cant_win_with_this_hand(self): - table = self._make_table() - tiles = self._string_to_136_array(man='34577', sou='23', pin='233445') - table.player.init_hand(tiles) + self.player.init_hand(tiles) meld = self._make_meld(Meld.CHI, pin='234') - table.player.add_called_meld(meld) + self.player.add_called_meld(meld) - table.player.draw_tile(self._string_to_136_tile(sou='1')) - discard = table.player.discard_tile() + self.player.draw_tile(self._string_to_136_tile(sou='1')) + discard = self.player.discard_tile() # but for already open hand we cant do tsumo # because we don't have a yaku here # so, let's do tsumogiri - self.assertEqual(table.player.ai.previous_shanten, 0) + self.assertEqual(self.player.ai.previous_shanten, 0) self.assertEqual(self._to_string([discard]), '1s') def test_choose_correct_waiting(self): - table = self._make_table() - player = table.player - tiles = self._string_to_136_array(man='234678', sou='234', pin='3588') - player.init_hand(tiles) - player.draw_tile(self._string_to_136_tile(pin='2')) + self.player.init_hand(tiles) + self.player.draw_tile(self._string_to_136_tile(pin='2')) # discard 5p and riichi - discard = player.discard_tile() + discard = self.player.discard_tile() self.assertEqual(self._to_string([discard]), '5p') table = self._make_table() @@ -239,7 +209,7 @@ def test_choose_correct_waiting(self): discard = player.discard_tile() self.assertEqual(self._to_string([discard]), '2p') - table = table = self._make_table() + table = self._make_table() player = table.player meld = self._make_meld(Meld.CHI, man='234') @@ -254,14 +224,16 @@ def test_choose_correct_waiting(self): self.assertEqual(self._to_string([discard]), '7s') def test_choose_correct_waiting_and_fist_opened_meld(self): - table = self._make_table() - player = table.player - tiles = self._string_to_136_array(man='2337788', sou='345', pin='234') - player.init_hand(tiles) + self.player.init_hand(tiles) tile = self._string_to_136_tile(man='8') - meld, tile_to_discard = player.try_to_call_meld(tile, False) + meld, tile_to_discard = self.player.try_to_call_meld(tile, False) - discard = player.discard_tile(tile_to_discard) + discard = self.player.discard_tile(tile_to_discard) self.assertEqual(self._to_string([discard]), '2m') + + def _make_table(self): + table = Table() + table.has_open_tanyao = True + return table From 50c944b412109b5242ba7a247e18825dc56913d8 Mon Sep 17 00:00:00 2001 From: Nihisil Date: Mon, 27 Aug 2018 21:56:09 +0800 Subject: [PATCH 020/126] Issue #54. Don't try to ruin our closed hand when we are aim for tanyao --- .../game/ai/first_version/strategies/tanyao.py | 18 +++++++++++------- .../tests/strategies/tests_tanyao.py | 11 +++++++++++ 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/project/game/ai/first_version/strategies/tanyao.py b/project/game/ai/first_version/strategies/tanyao.py index ec88af1b..25d80e8e 100644 --- a/project/game/ai/first_version/strategies/tanyao.py +++ b/project/game/ai/first_version/strategies/tanyao.py @@ -76,6 +76,10 @@ def determine_what_to_discard(self, closed_hand, outs_results, shanten, for_open is_open_hand = self.player.is_open_hand or hand_was_open + # our hand is closed, we don't need to discard terminal tiles here + if not is_open_hand: + return outs_results + if shanten == 0 and is_open_hand: results = [] # there is no sense to wait 1-4 if we have open hand @@ -90,13 +94,13 @@ def determine_what_to_discard(self, closed_hand, outs_results, shanten, for_open return outs_results return results - else: - return super(TanyaoStrategy, self).determine_what_to_discard(closed_hand, - outs_results, - shanten, - for_open_hand, - tile_for_open_hand, - hand_was_open) + + return super(TanyaoStrategy, self).determine_what_to_discard(closed_hand, + outs_results, + shanten, + for_open_hand, + tile_for_open_hand, + hand_was_open) def is_tile_suitable(self, tile): """ 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 802241d3..4f2f18d4 100644 --- a/project/game/ai/first_version/tests/strategies/tests_tanyao.py +++ b/project/game/ai/first_version/tests/strategies/tests_tanyao.py @@ -233,6 +233,17 @@ def test_choose_correct_waiting_and_fist_opened_meld(self): discard = self.player.discard_tile(tile_to_discard) self.assertEqual(self._to_string([discard]), '2m') + def test_we_dont_need_to_discard_terminals_from_closed_hand(self): + tiles = self._string_to_136_array(man='22345', sou='13588', pin='558') + self.player.init_hand(tiles) + + tile = self._string_to_136_tile(pin='5') + self.player.draw_tile(tile) + tile_to_discard = self.player.discard_tile() + + # our hand is closed, let's keep terminal for now + self.assertEqual(self._to_string([tile_to_discard]), '8p') + def _make_table(self): table = Table() table.has_open_tanyao = True From 8f308e59152890d5ca2e9b2c8de60a452db64830 Mon Sep 17 00:00:00 2001 From: Nihisil Date: Mon, 27 Aug 2018 22:06:18 +0800 Subject: [PATCH 021/126] Don't try to aim for tanyao when there are 5+ terminal and honor tiles in the hand --- project/game/ai/first_version/strategies/tanyao.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/project/game/ai/first_version/strategies/tanyao.py b/project/game/ai/first_version/strategies/tanyao.py index 25d80e8e..34f61f13 100644 --- a/project/game/ai/first_version/strategies/tanyao.py +++ b/project/game/ai/first_version/strategies/tanyao.py @@ -24,6 +24,7 @@ def should_activate_strategy(self): count_of_terminal_pon_sets = 0 count_of_terminal_pairs = 0 count_of_valued_pairs = 0 + count_of_not_suitable_tiles = 0 for x in range(0, 34): tile = tiles[x] if not tile: @@ -38,6 +39,13 @@ def should_activate_strategy(self): if x in self.player.valued_honors: count_of_valued_pairs += 1 + if x in self.not_suitable_tiles: + count_of_not_suitable_tiles += 1 + + # we have too much terminals and honors + if count_of_not_suitable_tiles >= 5: + return False + # if we already have pon of honor\terminal tiles # we don't need to open hand for tanyao if count_of_terminal_pon_sets > 0: From e9ec4fc55b98be41ae7103d9ebb5138db886f85f Mon Sep 17 00:00:00 2001 From: Nihisil Date: Tue, 28 Aug 2018 19:43:48 +0800 Subject: [PATCH 022/126] Fix file logging --- project/utils/logger.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/project/utils/logger.py b/project/utils/logger.py index 9d837b37..53c23035 100644 --- a/project/utils/logger.py +++ b/project/utils/logger.py @@ -21,25 +21,23 @@ def set_up_logging(save_to_file=True): ch = logging.StreamHandler() ch.setLevel(logging.DEBUG) - formatter = logging.Formatter('%(asctime)s %(levelname)s: %(message)s', datefmt='%Y-%m-%d %H:%M:%S') ch.setFormatter(formatter) - logger.addHandler(ch) - logger = logging.getLogger('ai') - logger.setLevel(logging.DEBUG) - logger.addHandler(ch) + ai_logger = logging.getLogger('ai') + ai_logger.setLevel(logging.DEBUG) + ai_logger.addHandler(ch) if save_to_file: # we need it to distinguish different bots logs (if they were run in the same time) log_prefix = settings.LOG_PREFIX if not log_prefix: log_prefix = hashlib.sha1(settings.USER_ID.encode('utf-8')).hexdigest()[:5] - file_name = '{}_{}.log'.format(log_prefix, datetime.datetime.now().strftime('%Y-%m-%d_%H_%M_%S')) + fh = logging.FileHandler(os.path.join(logs_directory, file_name), encoding='utf-8') fh.setLevel(logging.DEBUG) fh.setFormatter(formatter) logger.addHandler(fh) - logger.addHandler(fh) + ai_logger.addHandler(fh) From bccb2483f2c9fca4c7d2bb5f85bd191987e6382d Mon Sep 17 00:00:00 2001 From: Nihisil Date: Tue, 28 Aug 2018 19:44:41 +0800 Subject: [PATCH 023/126] Increase delay after agari --- project/tenhou/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/tenhou/client.py b/project/tenhou/client.py index 9d4d7caf..3c2e2dec 100644 --- a/project/tenhou/client.py +++ b/project/tenhou/client.py @@ -322,7 +322,7 @@ def start_game(self): # the end of round if '') # set was called From 8e06da6ffb88370b7da75c25584bbcb152aba1fb Mon Sep 17 00:00:00 2001 From: Nihisil Date: Tue, 28 Aug 2018 21:34:08 +0800 Subject: [PATCH 024/126] Issue #69. Fix a crash after called chankan --- project/game/ai/first_version/tests/tests_ai.py | 11 +---------- project/game/player.py | 7 ------- 2 files changed, 1 insertion(+), 17 deletions(-) diff --git a/project/game/ai/first_version/tests/tests_ai.py b/project/game/ai/first_version/tests/tests_ai.py index 55cc614d..cef0dc8a 100644 --- a/project/game/ai/first_version/tests/tests_ai.py +++ b/project/game/ai/first_version/tests/tests_ai.py @@ -244,23 +244,14 @@ def test_upgrade_opened_pon_to_kan(self): tiles = self._string_to_136_array(man='34445', sou='123456', pin='89') table.player.init_hand(tiles) tile = self._string_to_136_tile(man='4') - table.player.draw_tile(tile) self.assertEqual(table.player.should_call_kan(tile, False), None) table.player.add_called_meld(self._make_meld(Meld.PON, man='444')) self.assertEqual(len(table.player.melds), 1) - self.assertEqual(len(table.player.tiles), 14) - self.assertEqual(table.player.should_call_kan(tile, False), Meld.CHANKAN) - - table.player.discard_tile() - table.player.draw_tile(tile) - table.player.add_called_meld(self._make_meld(Meld.CHANKAN, man='4444')) - - self.assertEqual(len(table.player.melds), 1) - self.assertEqual(table.player.melds[0].type, Meld.CHANKAN) self.assertEqual(len(table.player.tiles), 13) + self.assertEqual(table.player.should_call_kan(tile, False), Meld.CHANKAN) def test_call_closed_kan(self): table = Table() diff --git a/project/game/player.py b/project/game/player.py index e2d886b3..d3932f50 100644 --- a/project/game/player.py +++ b/project/game/player.py @@ -219,13 +219,6 @@ def total_tiles(self, tile, tiles_34): """ return tiles_34[tile] + self.table.revealed_tiles[tile] - def add_called_meld(self, meld: Meld): - # we had to remove tile from the hand for closed kan set - if (meld.type == Meld.KAN and not meld.opened) or meld.type == Meld.CHANKAN: - self.tiles.remove(meld.called_tile) - - super().add_called_meld(meld) - def format_hand_for_print(self, tile): hand_string = '{} + {}'.format( TilesConverter.to_one_line_string(self.closed_hand), From 2d7c1e70e82d6bd36aeb731cb4241656e418d8ad Mon Sep 17 00:00:00 2001 From: Nihisil Date: Wed, 29 Aug 2018 11:41:01 +0800 Subject: [PATCH 025/126] Add profile files to the gitignore --- .gitignore | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 6a71182b..a720f7d1 100644 --- a/.gitignore +++ b/.gitignore @@ -20,4 +20,7 @@ project/game/data/* project/analytics/data/* # temporary files -experiments \ No newline at end of file +experiments + +*.prof +profile.py \ No newline at end of file From 486722d35db728e711e901ebb02edc3df4521049 Mon Sep 17 00:00:00 2001 From: Nihisil Date: Wed, 29 Aug 2018 18:17:01 +0800 Subject: [PATCH 026/126] Issue #67. Increase hand building speed --- project/game/ai/first_version/main.py | 31 ++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/project/game/ai/first_version/main.py b/project/game/ai/first_version/main.py index ac94a52d..08242dbf 100644 --- a/project/game/ai/first_version/main.py +++ b/project/game/ai/first_version/main.py @@ -42,6 +42,8 @@ class ImplementationAI(InterfaceAI): current_strategy = None + hand_cache = {} + def __init__(self, player): super(ImplementationAI, self).__init__(player) @@ -73,6 +75,8 @@ def erase_state(self): self.ukeire = 0 self.ukeire_second = 0 + self.hand_cache = {} + def draw_tile(self, tile): """ :param tile: 136 tile format @@ -138,6 +142,9 @@ def calculate_outs(self, tiles, closed_hand, open_sets_34=None): :param open_sets_34: array of array with tiles in 34 format :return: """ + if open_sets_34 is None: + open_sets_34 = [] + 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.meld_34_tiles) @@ -157,8 +164,21 @@ def calculate_outs(self, tiles, closed_hand, open_sets_34=None): continue tiles_34[j] += 1 - if self.shanten.calculate_shanten(tiles_34, open_sets_34) == shanten - 1: + + key = '{},{}'.format( + ''.join([str(x) for x in tiles_34]), + ';'.join([str(x) for x in open_sets_34]) + ) + + if key in self.hand_cache: + new_shanten = self.hand_cache[key] + else: + new_shanten = self.shanten.calculate_shanten(tiles_34, open_sets_34) + self.hand_cache[key] = new_shanten + + if new_shanten == shanten - 1: waiting.append(j) + tiles_34[j] -= 1 tiles_34[hand_tile] += 1 @@ -273,10 +293,11 @@ def choose_tile_to_discard(self, results: [DiscardOption]) -> DiscardOption: if discard_option.ukeire >= first_option.ukeire - ukeire_borders: possible_options.append(discard_option) - # as second step - # let's choose tiles that are close to the max ukeire2 tile - for x in possible_options: - self.calculate_second_level_ukeire(x) + if 1 < first_option.shanten < 4: + # as second step + # let's choose tiles that are close to the max ukeire2 tile + for x in possible_options: + self.calculate_second_level_ukeire(x) possible_options = sorted(possible_options, key=lambda x: -x.ukeire_second) From 686d705035feee7c5f2da7a504933c35c1e63b04 Mon Sep 17 00:00:00 2001 From: Nihisil Date: Wed, 29 Aug 2018 18:19:35 +0800 Subject: [PATCH 027/126] Change delays a little bit --- project/tenhou/client.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/project/tenhou/client.py b/project/tenhou/client.py index 3c2e2dec..b014b21b 100644 --- a/project/tenhou/client.py +++ b/project/tenhou/client.py @@ -43,7 +43,7 @@ def connect(self): # for reproducer if self._socket_mock: self.socket = self._socket_mock - TenhouClient._random_sleep = lambda x, y=0, z=0: 0 + TenhouClient._random_sleep = lambda x, y, z: 0 else: self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) @@ -120,18 +120,18 @@ def start_game(self): if settings.IS_TOURNAMENT: logger.info('Go to the tournament lobby: {}'.format(settings.LOBBY)) self._send_message(''.format(settings.LOBBY)) - self._random_sleep() + self._random_sleep(1, 2) self._send_message('') else: logger.info('Go to the lobby: {}'.format(settings.LOBBY)) self._send_message(''.format(quote('/lobby {}'.format(settings.LOBBY)))) - self._random_sleep() + self._random_sleep(1, 2) if self.reconnected_messages: # we already in the game self.looking_for_game = False self._send_message('') - self._random_sleep() + self._random_sleep(1, 2) else: selected_game_type = self._build_game_type() game_type = '{},{}'.format(settings.LOBBY, selected_game_type) @@ -143,7 +143,7 @@ def start_game(self): start_time = datetime.datetime.now() while self.looking_for_game: - self._random_sleep() + self._random_sleep(1, 2) messages = self._get_multiple_messages() for message in messages: @@ -152,7 +152,7 @@ def start_game(self): self._send_message(''.format(game_type)) if '') self._send_message('') @@ -277,7 +277,7 @@ def start_game(self): kan_type = self.player.should_call_kan(drawn_tile, False, main_player.in_riichi) if kan_type: - self._random_sleep() + self._random_sleep(1, 2) if kan_type == Meld.CHANKAN: meld_type = 5 @@ -295,7 +295,7 @@ def start_game(self): # let's call riichi if can_call_riichi: - self._random_sleep() + self._random_sleep(1, 2) self._send_message(''.format(discarded_tile)) main_player.in_riichi = True else: @@ -322,7 +322,7 @@ def start_game(self): # the end of round if '') # set was called @@ -356,7 +356,7 @@ def start_game(self): if any(i in message for i in win_suggestions): tile = self.decoder.parse_tile(message) enemy_seat = self.decoder.get_enemy_seat(message) - self._random_sleep(2, 4) + self._random_sleep(1, 2) if main_player.should_call_win(tile, enemy_seat): self._send_message('') @@ -386,7 +386,7 @@ def start_game(self): # should we call a kan? if 't="3"' in message or 't="7"' in message: if self.player.should_call_kan(tile, True): - self._random_sleep() + self._random_sleep(1, 2) # 2 is open kan self._send_message('') @@ -578,5 +578,5 @@ def _set_game_rules(self, game_type): return True - def _random_sleep(self, min_sleep=1, max_sleep=3): + def _random_sleep(self, min_sleep, max_sleep): sleep(random.randint(min_sleep, max_sleep + 1)) From 3940256cc1465ff64a1493f7e42546f322d42e92 Mon Sep 17 00:00:00 2001 From: Nihisil Date: Wed, 29 Aug 2018 23:33:47 +0800 Subject: [PATCH 028/126] Fix possible issues with hand building --- project/game/ai/first_version/main.py | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/project/game/ai/first_version/main.py b/project/game/ai/first_version/main.py index 08242dbf..be124cb4 100644 --- a/project/game/ai/first_version/main.py +++ b/project/game/ai/first_version/main.py @@ -293,34 +293,35 @@ def choose_tile_to_discard(self, results: [DiscardOption]) -> DiscardOption: if discard_option.ukeire >= first_option.ukeire - ukeire_borders: possible_options.append(discard_option) - if 1 < first_option.shanten < 4: - # as second step - # let's choose tiles that are close to the max ukeire2 tile + if first_option.shanten == 2 or first_option.shanten == 3: + sorting_field = 'ukeire_second' for x in possible_options: self.calculate_second_level_ukeire(x) + else: + sorting_field = 'ukeire' - possible_options = sorted(possible_options, key=lambda x: -x.ukeire_second) + possible_options = sorted(possible_options, key=lambda x: -getattr(x, sorting_field)) filter_percentage = 20 filtered_options = self._filter_list_by_percentage( possible_options, - 'ukeire_second', + sorting_field, filter_percentage ) tiles_without_dora = [x for x in filtered_options if x.count_of_dora == 0] + # we have only dora candidates to discard if not tiles_without_dora: min_dora = min([x.count_of_dora for x in filtered_options]) min_dora_list = [x for x in filtered_options if x.count_of_dora == min_dora] - # let's discard tile with greater ukeire2 - return sorted(min_dora_list, key=lambda x: -x.ukeire_second)[0] + return sorted(min_dora_list, key=lambda x: -getattr(x, sorting_field))[0] second_filter_percentage = 10 filtered_options = self._filter_list_by_percentage( tiles_without_dora, - 'ukeire_second', + sorting_field, second_filter_percentage ) @@ -333,16 +334,16 @@ def choose_tile_to_discard(self, results: [DiscardOption]) -> DiscardOption: # there are no isolated tiles # let's discard tile with greater ukeire2 - filtered_options = sorted(filtered_options, key=lambda x: -x.ukeire_second) + filtered_options = sorted(filtered_options, key=lambda x: -getattr(x, sorting_field)) first_option = filtered_options[0] - other_tiles_with_same_ukeire_second = [x for x in filtered_options - if x.ukeire_second == first_option.ukeire_second] + other_tiles_with_same_ukeire = [x for x in filtered_options + if getattr(x, sorting_field) == getattr(first_option, sorting_field)] # it will happen with shanten=1, all tiles will have ukeire_second == 0 - if other_tiles_with_same_ukeire_second: + if other_tiles_with_same_ukeire: # let's sort tiles by value and let's choose less valuable tile to discard - return sorted(other_tiles_with_same_ukeire_second, key=lambda x: x.valuation)[0] + return sorted(other_tiles_with_same_ukeire, key=lambda x: x.valuation)[0] # we have only one candidate to discard with greater ukeire return first_option From bd217a65681d36bf1faaf027ee4fa42bd053c88c Mon Sep 17 00:00:00 2001 From: Pavel Bogachev Date: Fri, 31 Aug 2018 11:30:51 +0300 Subject: [PATCH 029/126] Improve tanyao strategy (#73) * Issue #53. Change open hand rules for tanyao * Issue #66. When considering ukeire for open hand don't count tiles that don't suit our yaku --- project/game/ai/first_version/main.py | 8 +- .../first_version/strategies/formal_tempai.py | 2 +- .../game/ai/first_version/strategies/main.py | 1 + .../ai/first_version/strategies/tanyao.py | 57 +++++++- .../ai/first_version/strategies/yakuhai.py | 2 +- .../tests/strategies/tests_tanyao.py | 131 ++++++++++++------ .../game/ai/first_version/tests/tests_ai.py | 15 +- project/requirements.txt | 2 +- 8 files changed, 172 insertions(+), 46 deletions(-) diff --git a/project/game/ai/first_version/main.py b/project/game/ai/first_version/main.py index be124cb4..5573329a 100644 --- a/project/game/ai/first_version/main.py +++ b/project/game/ai/first_version/main.py @@ -199,8 +199,12 @@ def calculate_outs(self, tiles, closed_hand, open_sets_34=None): def count_tiles(self, waiting, tiles_34): n = 0 - for item in waiting: - n += 4 - self.player.total_tiles(item, tiles_34) + not_suitable_tiles = self.current_strategy and self.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 def try_to_call_meld(self, tile, is_kamicha_discard): diff --git a/project/game/ai/first_version/strategies/formal_tempai.py b/project/game/ai/first_version/strategies/formal_tempai.py index 5e941486..411ee736 100644 --- a/project/game/ai/first_version/strategies/formal_tempai.py +++ b/project/game/ai/first_version/strategies/formal_tempai.py @@ -31,7 +31,7 @@ def should_activate_strategy(self): return True dora_count = sum([plus_dora(x, self.player.table.dora_indicators) for x in self.player.tiles]) - dora_count += sum([1 for x in self.player.tiles if is_aka_dora(x, self.player.table.has_open_tanyao)]) + dora_count += sum([1 for x in self.player.tiles if is_aka_dora(x, self.player.table.has_aka_dora)]) if self.player.ai.previous_shanten == 2: if dora_count < 2: diff --git a/project/game/ai/first_version/strategies/main.py b/project/game/ai/first_version/strategies/main.py index 9ad68c14..534f4361 100644 --- a/project/game/ai/first_version/strategies/main.py +++ b/project/game/ai/first_version/strategies/main.py @@ -17,6 +17,7 @@ class BaseStrategy(object): FORMAL_TEMPAI: 'Formal Tempai' } + not_suitable_tiles = [] player = None type = None # number of shanten where we can start to open hand diff --git a/project/game/ai/first_version/strategies/tanyao.py b/project/game/ai/first_version/strategies/tanyao.py index 34f61f13..235a7b73 100644 --- a/project/game/ai/first_version/strategies/tanyao.py +++ b/project/game/ai/first_version/strategies/tanyao.py @@ -1,6 +1,9 @@ # -*- coding: utf-8 -*- from mahjong.constants import TERMINAL_INDICES, HONOR_INDICES from mahjong.tile import TilesConverter +from mahjong.utils import is_tile_strictly_isolated, is_terminal +from mahjong.utils import plus_dora, is_aka_dora +from mahjong.utils import is_honor from game.ai.first_version.strategies.main import BaseStrategy @@ -21,10 +24,15 @@ def should_activate_strategy(self): return False tiles = TilesConverter.to_34_array(self.player.tiles) + + closed_hand_34 = TilesConverter.to_34_array(self.player.closed_hand) + isolated_tiles = [x // 4 for x in self.player.tiles if is_tile_strictly_isolated(closed_hand_34, x // 4) or is_honor(x // 4)] + count_of_terminal_pon_sets = 0 count_of_terminal_pairs = 0 count_of_valued_pairs = 0 count_of_not_suitable_tiles = 0 + count_of_not_suitable_not_isolated_tiles = 0 for x in range(0, 34): tile = tiles[x] if not tile: @@ -40,7 +48,10 @@ def should_activate_strategy(self): count_of_valued_pairs += 1 if x in self.not_suitable_tiles: - count_of_not_suitable_tiles += 1 + count_of_not_suitable_tiles += tile + + if x in self.not_suitable_tiles and x not in isolated_tiles: + count_of_not_suitable_not_isolated_tiles += tile # we have too much terminals and honors if count_of_not_suitable_tiles >= 5: @@ -61,6 +72,17 @@ def should_activate_strategy(self): if count_of_terminal_pairs > 1: return False + # 3 or more not suitable tiles that + # are not isolated is too much + if count_of_not_suitable_not_isolated_tiles >= 3: + return False + + # if we are 1 shanten, even 2 tiles + # that are not suitable and not isolated + # is too much + if count_of_not_suitable_not_isolated_tiles >= 2 and self.player.ai.previous_shanten == 1: + return False + # 123 and 789 indices indices = [ [0, 1, 2], [6, 7, 8], @@ -75,6 +97,39 @@ def should_activate_strategy(self): if first >= 1 and second >= 1 and third >= 1: return False + dora_count_central = 0 + dora_count_not_central = 0 + for tile_136 in self.player.tiles: + tile_34 = tile_136 // 4 + + dora = plus_dora(tile_136, self.player.table.dora_indicators) + + if is_aka_dora(tile_136, self.player.table.has_aka_dora): + dora_count_central += 1 + + if not dora: + continue + + if is_honor(tile_34) or is_terminal(tile_34): + dora_count_not_central += 1 + else: + dora_count_central += 1 + + # if we have 2 or more non-central doras + # we don't want to go for tanyao + if dora_count_not_central >= 2: + return False + + # if we have less than two central doras + # let's not consider open tanyao + if dora_count_central < 2: + return False + + # if we have only two central doras let's + # wait for 5th turn before opening our hand + if dora_count_central == 2 and self.player.round_step < 5: + return False + return True def determine_what_to_discard(self, closed_hand, outs_results, shanten, for_open_hand, tile_for_open_hand, diff --git a/project/game/ai/first_version/strategies/yakuhai.py b/project/game/ai/first_version/strategies/yakuhai.py index b992f5cb..656ae85b 100644 --- a/project/game/ai/first_version/strategies/yakuhai.py +++ b/project/game/ai/first_version/strategies/yakuhai.py @@ -49,7 +49,7 @@ def should_activate_strategy(self): return False dora_count = sum([plus_dora(x, self.player.table.dora_indicators) for x in self.player.tiles]) - dora_count += sum([1 for x in self.player.tiles if is_aka_dora(x, self.player.table.has_open_tanyao)]) + dora_count += sum([1 for x in self.player.tiles if is_aka_dora(x, self.player.table.has_aka_dora)]) # let's always open double east if is_double_east_wind: 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 4f2f18d4..2da29873 100644 --- a/project/game/ai/first_version/tests/strategies/tests_tanyao.py +++ b/project/game/ai/first_version/tests/strategies/tests_tanyao.py @@ -18,55 +18,55 @@ def setUp(self): def test_should_activate_strategy_and_terminal_pon_sets(self): strategy = TanyaoStrategy(BaseStrategy.TANYAO, self.player) - tiles = self._string_to_136_array(sou='234', man='3459', pin='233', honors='111') + tiles = self._string_to_136_array(sou='222', man='3459', pin='233', honors='111') self.player.init_hand(tiles) self.assertEqual(strategy.should_activate_strategy(), False) - tiles = self._string_to_136_array(sou='234', man='3459', pin='233999') + tiles = self._string_to_136_array(sou='222', man='3459', pin='233999') self.player.init_hand(tiles) self.assertEqual(strategy.should_activate_strategy(), False) - tiles = self._string_to_136_array(sou='234', man='3459', pin='233444') + tiles = self._string_to_136_array(sou='222', man='3459', pin='233444') self.player.init_hand(tiles) self.assertEqual(strategy.should_activate_strategy(), True) def test_should_activate_strategy_and_terminal_pairs(self): strategy = TanyaoStrategy(BaseStrategy.TANYAO, self.player) - tiles = self._string_to_136_array(sou='234', man='3459', pin='2399', honors='11') + tiles = self._string_to_136_array(sou='222', man='3459', pin='2399', honors='11') self.player.init_hand(tiles) self.assertEqual(strategy.should_activate_strategy(), False) - tiles = self._string_to_136_array(sou='234', man='345669', pin='2399') + tiles = self._string_to_136_array(sou='222', man='345669', pin='2399') self.player.init_hand(tiles) self.assertEqual(strategy.should_activate_strategy(), True) def test_should_activate_strategy_and_valued_pair(self): strategy = TanyaoStrategy(BaseStrategy.TANYAO, self.player) - tiles = self._string_to_136_array(man='23446679', sou='345', honors='55') + tiles = self._string_to_136_array(man='23446679', sou='222', honors='55') self.player.init_hand(tiles) self.assertEqual(strategy.should_activate_strategy(), False) - tiles = self._string_to_136_array(man='23446679', sou='345', honors='22') + tiles = self._string_to_136_array(man='23446679', sou='222', honors='22') self.player.init_hand(tiles) self.assertEqual(strategy.should_activate_strategy(), True) def test_should_activate_strategy_and_chitoitsu_like_hand(self): strategy = TanyaoStrategy(BaseStrategy.TANYAO, self.player) - tiles = self._string_to_136_array(sou='223388', man='3344', pin='6687') + tiles = self._string_to_136_array(sou='223388', man='2244', pin='6687') self.player.init_hand(tiles) self.assertEqual(strategy.should_activate_strategy(), False) def test_should_activate_strategy_and_already_completed_sided_set(self): strategy = TanyaoStrategy(BaseStrategy.TANYAO, self.player) - tiles = self._string_to_136_array(sou='123234', man='3459', pin='234') + tiles = self._string_to_136_array(sou='123234', man='2349', pin='234') self.player.init_hand(tiles) self.assertEqual(strategy.should_activate_strategy(), False) - tiles = self._string_to_136_array(sou='234789', man='3459', pin='234') + tiles = self._string_to_136_array(sou='234789', man='2349', pin='234') self.player.init_hand(tiles) self.assertEqual(strategy.should_activate_strategy(), False) @@ -74,19 +74,19 @@ def test_should_activate_strategy_and_already_completed_sided_set(self): self.player.init_hand(tiles) self.assertEqual(strategy.should_activate_strategy(), False) - tiles = self._string_to_136_array(sou='234', man='3457899', pin='234') + tiles = self._string_to_136_array(sou='234', man='2227899', pin='234') self.player.init_hand(tiles) self.assertEqual(strategy.should_activate_strategy(), False) - tiles = self._string_to_136_array(sou='234', man='3459', pin='122334') + tiles = self._string_to_136_array(sou='234', man='2229', pin='122334') self.player.init_hand(tiles) self.assertEqual(strategy.should_activate_strategy(), False) - tiles = self._string_to_136_array(sou='234', man='3459', pin='234789') + tiles = self._string_to_136_array(sou='234', man='2229', pin='234789') self.player.init_hand(tiles) self.assertEqual(strategy.should_activate_strategy(), False) - tiles = self._string_to_136_array(sou='223344', man='3459', pin='234') + tiles = self._string_to_136_array(sou='223344', man='2229', pin='234') self.player.init_hand(tiles) self.assertEqual(strategy.should_activate_strategy(), True) @@ -119,36 +119,34 @@ def test_suitable_tiles(self): def test_dont_open_hand_with_high_shanten(self): # with 4 shanten we don't need to aim for open tanyao - tiles = self._string_to_136_array(man='369', pin='378', sou='3488', honors='123') - tile = self._string_to_136_tile(sou='2') + tiles = self._string_to_136_array(man='269', pin='247', sou='2488', honors='123') + tile = self._string_to_136_tile(sou='3') self.player.init_hand(tiles) meld, _ = self.player.try_to_call_meld(tile, False) self.assertEqual(meld, None) # with 3 shanten we can open a hand - tiles = self._string_to_136_array(man='236', pin='378', sou='3488', honors='123') - tile = self._string_to_136_tile(sou='2') + tiles = self._string_to_136_array(man='236', pin='247', sou='2488', honors='123') + tile = self._string_to_136_tile(sou='3') self.player.init_hand(tiles) meld, _ = self.player.try_to_call_meld(tile, True) self.assertNotEqual(meld, None) def test_dont_open_hand_with_not_suitable_melds(self): - tiles = self._string_to_136_array(man='33355788', sou='3479', honors='3') + tiles = self._string_to_136_array(man='22255788', sou='3479', honors='3') tile = self._string_to_136_tile(sou='8') self.player.init_hand(tiles) meld, _ = self.player.try_to_call_meld(tile, False) self.assertEqual(meld, None) def test_open_hand_and_discard_tiles_logic(self): - # 2345779m1p256s44z - tiles = self._string_to_136_array(man='22345', sou='238', pin='256', honors='44') + tiles = self._string_to_136_array(man='22234', sou='238', pin='256', honors='44') self.player.init_hand(tiles) - # if we are in tanyao - # we need to discard terminals and honors tile = self._string_to_136_tile(sou='4') meld, discard_option = self.player.try_to_call_meld(tile, True) discarded_tile = self.player.discard_tile(discard_option) + self.assertNotEqual(meld, None) self.assertEqual(self._to_string([discarded_tile]), '4z') @@ -156,7 +154,6 @@ def test_open_hand_and_discard_tiles_logic(self): self.player.draw_tile(tile) tile_to_discard = self.player.discard_tile() - # we are in tanyao, so we should discard honors and terminals self.assertEqual(self._to_string([tile_to_discard]), '4z') def test_dont_count_pairs_in_already_opened_hand(self): @@ -172,7 +169,7 @@ def test_dont_count_pairs_in_already_opened_hand(self): self.assertNotEqual(meld, None) def test_we_cant_win_with_this_hand(self): - tiles = self._string_to_136_array(man='34577', sou='23', pin='233445') + tiles = self._string_to_136_array(man='22277', sou='23', pin='233445') self.player.init_hand(tiles) meld = self._make_meld(Meld.CHI, pin='234') self.player.add_called_meld(meld) @@ -186,36 +183,35 @@ def test_we_cant_win_with_this_hand(self): self.assertEqual(self._to_string([discard]), '1s') def test_choose_correct_waiting(self): - tiles = self._string_to_136_array(man='234678', sou='234', pin='3588') + tiles = self._string_to_136_array(man='222678', sou='234', pin='3588') self.player.init_hand(tiles) self.player.draw_tile(self._string_to_136_tile(pin='2')) + self._assert_tanyao(self.player) + # discard 5p and riichi discard = self.player.discard_tile() self.assertEqual(self._to_string([discard]), '5p') - table = self._make_table() - player = table.player - meld = self._make_meld(Meld.CHI, man='234') - player.add_called_meld(meld) + self.player.add_called_meld(meld) - tiles = self._string_to_136_array(man='234678', sou='234', pin='3588') - player.init_hand(tiles) - player.draw_tile(self._string_to_136_tile(pin='2')) + tiles = self._string_to_136_array(man='234888', sou='234', pin='3588') + self.player.init_hand(tiles) + self.player.draw_tile(self._string_to_136_tile(pin='2')) # it is not a good idea to wait on 1-4, since we can't win on 1 with open hand # so let's continue to wait on 4 only - discard = player.discard_tile() + discard = self.player.discard_tile() self.assertEqual(self._to_string([discard]), '2p') table = self._make_table() player = table.player - meld = self._make_meld(Meld.CHI, man='234') + meld = self._make_meld(Meld.CHI, man='678') player.add_called_meld(meld) - tiles = self._string_to_136_array(man='234678', sou='234', pin='2388') + tiles = self._string_to_136_array(man='222678', sou='234', pin='2388') player.init_hand(tiles) player.draw_tile(self._string_to_136_tile(sou='7')) @@ -223,10 +219,12 @@ def test_choose_correct_waiting(self): discard = player.discard_tile() self.assertEqual(self._to_string([discard]), '7s') - def test_choose_correct_waiting_and_fist_opened_meld(self): - tiles = self._string_to_136_array(man='2337788', sou='345', pin='234') + 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) @@ -234,7 +232,7 @@ def test_choose_correct_waiting_and_fist_opened_meld(self): self.assertEqual(self._to_string([discard]), '2m') def test_we_dont_need_to_discard_terminals_from_closed_hand(self): - tiles = self._string_to_136_array(man='22345', sou='13588', pin='558') + tiles = self._string_to_136_array(man='22234', sou='13588', pin='558') self.player.init_hand(tiles) tile = self._string_to_136_tile(pin='5') @@ -244,7 +242,62 @@ def test_we_dont_need_to_discard_terminals_from_closed_hand(self): # our hand is closed, let's keep terminal for now self.assertEqual(self._to_string([tile_to_discard]), '8p') + def test_dont_open_tanyao_with_two_non_central_doras(self): + table = self._make_table() + player = table.player + + table.add_dora_indicator(self._string_to_136_tile(pin='8')) + + tiles = self._string_to_136_array(man='22234', sou='888', pin='5599') + player.init_hand(tiles) + + tile = self._string_to_136_tile(pin='5') + meld, _ = player.try_to_call_meld(tile, False) + self.assertEqual(meld, None) + + def test_dont_open_tanyao_with_three_not_isolated_terminals(self): + tiles = self._string_to_136_array(man='2226', sou='2799', pin='5579') + self.player.init_hand(tiles) + + tile = self._string_to_136_tile(pin='5') + meld, _ = self.player.try_to_call_meld(tile, False) + self.assertEqual(meld, None) + + def test_dont_open_tanyao_with_two_not_isolated_terminals_one_shanten(self): + tiles = self._string_to_136_array(man='22234', sou='79', pin='55579') + self.player.init_hand(tiles) + + tile = self._string_to_136_tile(man='5') + meld, _ = self.player.try_to_call_meld(tile, False) + self.assertEqual(meld, None) + + def test_dont_count_terminal_tiles_in_ukeire(self): + # for closed hand let's chose tile with best ukeire + tiles = self._string_to_136_array(man='234578', sou='235', pin='2246') + self.player.init_hand(tiles) + self.player.draw_tile(self._string_to_136_tile(pin='5')) + discard = self.player.discard_tile() + self.assertEqual(self._to_string([discard]), '5m') + + # but with opened hand we don't need to count not suitable tiles as ukeire + tiles = self._string_to_136_array(man='234578', sou='235', pin='2246') + self.player.init_hand(tiles) + self.player.add_called_meld(self._make_meld(Meld.CHI, man='234')) + self.player.draw_tile(self._string_to_136_tile(pin='5')) + discard = self.player.discard_tile() + self.assertEqual(self._to_string([discard]), '8m') + def _make_table(self): table = Table() table.has_open_tanyao = True + + # add doras so we are sure to go for tanyao + table.add_dora_indicator(self._string_to_136_tile(sou='1')) + table.add_dora_indicator(self._string_to_136_tile(man='1')) + table.add_dora_indicator(self._string_to_136_tile(pin='1')) + return table + + def _assert_tanyao(self, player): + self.assertNotEqual(player.ai.current_strategy, None) + self.assertEqual(player.ai.current_strategy.type, BaseStrategy.TANYAO) diff --git a/project/game/ai/first_version/tests/tests_ai.py b/project/game/ai/first_version/tests/tests_ai.py index cef0dc8a..f24ac7be 100644 --- a/project/game/ai/first_version/tests/tests_ai.py +++ b/project/game/ai/first_version/tests/tests_ai.py @@ -64,6 +64,10 @@ def test_chose_right_set_to_open_hand(self): table.has_open_tanyao = True player = table.player + # add 3 doras so we are sure to go for tanyao + table.add_dora_indicator(self._string_to_136_tile(pin='2')) + table.add_dora_indicator(self._string_to_136_tile(pin='3')) + tiles = self._string_to_136_array(man='23455', pin='3445678', honors='1') tile = self._string_to_136_tile(man='5') player.init_hand(tiles) @@ -75,7 +79,8 @@ def test_chose_right_set_to_open_hand(self): table = Table() player = table.player - table.dora_indicators.append(self._string_to_136_tile(honors='7')) + # add 3 doras so we are sure to go for tanyao + table.add_dora_indicator(self._string_to_136_tile(man='5')) tiles = self._string_to_136_array(man='335666', pin='22', sou='345', honors='55') player.init_hand(tiles) @@ -88,6 +93,9 @@ def test_chose_right_set_to_open_hand(self): table = Table() table.has_open_tanyao = True player = table.player + # add 3 doras so we are sure to go for tanyao + table.add_dora_indicator(self._string_to_136_tile(man='1')) + table.add_dora_indicator(self._string_to_136_tile(man='4')) tiles = self._string_to_136_array(man='23557', pin='556788', honors='22') player.init_hand(tiles) @@ -125,8 +133,12 @@ def test_chose_strategy_and_reset_strategy(self): table.has_open_tanyao = True player = table.player + # add 3 doras so we are sure to go for tanyao + table.add_dora_indicator(self._string_to_136_tile(man='2')) + tiles = self._string_to_136_array(man='33355788', sou='3479', honors='3') player.init_hand(tiles) + 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 @@ -144,6 +156,7 @@ def test_chose_strategy_and_reset_strategy(self): 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) def test_remaining_tiles_and_enemy_discard(self): diff --git a/project/requirements.txt b/project/requirements.txt index 91c6f81f..c2dde972 100644 --- a/project/requirements.txt +++ b/project/requirements.txt @@ -1,3 +1,3 @@ -mahjong==1.1.3 +mahjong==1.1.4 requests==2.18.4 flake8==3.4.1 \ No newline at end of file From 8b589946a9755c8d92f1f669955c22dfd69540bf Mon Sep 17 00:00:00 2001 From: Alexey Date: Fri, 31 Aug 2018 17:50:43 +0800 Subject: [PATCH 030/126] Initialize shanten, ukeire, ukeire_second on init hand event --- project/game/ai/first_version/defence/main.py | 2 +- project/game/ai/first_version/main.py | 43 +++++++------------ .../first_version/strategies/formal_tempai.py | 6 +-- .../game/ai/first_version/strategies/main.py | 4 +- .../ai/first_version/strategies/tanyao.py | 2 +- .../ai/first_version/strategies/yakuhai.py | 4 +- .../tests/strategies/tests_tanyao.py | 10 ++--- .../tests/strategies/tests_yakuhai.py | 14 +----- .../game/ai/first_version/tests/tests_ai.py | 2 +- .../ai/first_version/tests/tests_defence.py | 2 +- .../ai/first_version/tests/tests_discards.py | 8 ++-- 11 files changed, 36 insertions(+), 61 deletions(-) diff --git a/project/game/ai/first_version/defence/main.py b/project/game/ai/first_version/defence/main.py index 326d9fe3..82bc4e41 100644 --- a/project/game/ai/first_version/defence/main.py +++ b/project/game/ai/first_version/defence/main.py @@ -45,7 +45,7 @@ def should_go_to_defence_mode(self, discard_candidate=None): waiting = discard_candidate.waiting # we have 13 tiles in hand (this is not our turn) else: - shanten = self.player.ai.previous_shanten + shanten = self.player.ai.shanten waiting = self.player.ai.waiting # if we are in riichi, we can't defence diff --git a/project/game/ai/first_version/main.py b/project/game/ai/first_version/main.py index 5573329a..09aeea25 100644 --- a/project/game/ai/first_version/main.py +++ b/project/game/ai/first_version/main.py @@ -28,19 +28,19 @@ class ImplementationAI(InterfaceAI): version = '0.4.0-dev' agari = None - shanten = None + shanten_calculator = None defence = None hand_divider = None finished_hand = None - last_discard_option = None - previous_shanten = 7 + shanten = 7 ukeire = 0 ukeire_second = 0 in_defence = False waiting = None current_strategy = None + last_discard_option = None hand_cache = {} @@ -48,17 +48,12 @@ def __init__(self, player): super(ImplementationAI, self).__init__(player) self.agari = Agari() - self.shanten = Shanten() + self.shanten_calculator = Shanten() self.defence = DefenceHandler(player) self.hand_divider = HandDivider() self.finished_hand = HandCalculator() - self.previous_shanten = 7 - self.ukeire = 0 - self.ukeire_second = 0 - self.current_strategy = None - self.waiting = [] - self.in_defence = False - self.last_discard_option = None + + self.erase_state() def init_hand(self): """ @@ -66,16 +61,8 @@ def init_hand(self): """ self.determine_strategy() - def erase_state(self): - self.current_strategy = None - self.waiting = [] - self.in_defence = False - self.last_discard_option = None - self.previous_shanten = 7 - self.ukeire = 0 - self.ukeire_second = 0 - - self.hand_cache = {} + # it will set correct hand shanten number and ukeire to the new hand + self.discard_tile(None) def draw_tile(self, tile): """ @@ -156,7 +143,7 @@ def calculate_outs(self, tiles, closed_hand, open_sets_34=None): tiles_34[hand_tile] -= 1 - shanten = self.shanten.calculate_shanten(tiles_34, open_sets_34) + shanten = self.shanten_calculator.calculate_shanten(tiles_34, open_sets_34) waiting = [] for j in range(0, 34): @@ -173,7 +160,7 @@ def calculate_outs(self, tiles, closed_hand, open_sets_34=None): if key in self.hand_cache: new_shanten = self.hand_cache[key] else: - new_shanten = self.shanten.calculate_shanten(tiles_34, open_sets_34) + new_shanten = self.shanten_calculator.calculate_shanten(tiles_34, open_sets_34) self.hand_cache[key] = new_shanten if new_shanten == shanten - 1: @@ -193,7 +180,7 @@ def calculate_outs(self, tiles, closed_hand, open_sets_34=None): if is_agari: shanten = Shanten.AGARI_STATE else: - shanten = self.shanten.calculate_shanten(tiles_34, open_sets_34) + shanten = self.shanten_calculator.calculate_shanten(tiles_34, open_sets_34) return results, shanten @@ -354,8 +341,8 @@ def choose_tile_to_discard(self, results: [DiscardOption]) -> DiscardOption: def process_discard_option(self, discard_option, closed_hand, force_discard=False): self.waiting = discard_option.waiting - self.player.ai.previous_shanten = discard_option.shanten - self.player.in_tempai = self.player.ai.previous_shanten == 0 + self.player.ai.shanten = discard_option.shanten + self.player.in_tempai = self.player.ai.shanten == 0 self.player.ai.ukeire = discard_option.ukeire self.player.ai.ukeire_second = discard_option.ukeire_second @@ -489,13 +476,13 @@ def should_call_kan(self, tile, open_kan, from_riichi=False): tiles_34[tile_34] += 1 melds = self.player.meld_34_tiles - previous_shanten = self.shanten.calculate_shanten(tiles_34, melds) + previous_shanten = self.shanten_calculator.calculate_shanten(tiles_34, melds) if not open_kan and not from_riichi: tiles_34[tile_34] -= 1 melds += [[tile_34, tile_34, tile_34]] - new_shanten = self.shanten.calculate_shanten(tiles_34, melds) + new_shanten = self.shanten_calculator.calculate_shanten(tiles_34, melds) # called kan will not ruin our hand if new_shanten <= previous_shanten: diff --git a/project/game/ai/first_version/strategies/formal_tempai.py b/project/game/ai/first_version/strategies/formal_tempai.py index 411ee736..52c7b4d4 100644 --- a/project/game/ai/first_version/strategies/formal_tempai.py +++ b/project/game/ai/first_version/strategies/formal_tempai.py @@ -27,13 +27,13 @@ def should_activate_strategy(self): # it's 11th turn or later and we still have 3 shanten or more, # let's try to go for formal tempai at least - if self.player.ai.previous_shanten >= 3: + if self.player.ai.shanten >= 3: return True dora_count = sum([plus_dora(x, self.player.table.dora_indicators) for x in self.player.tiles]) dora_count += sum([1 for x in self.player.tiles if is_aka_dora(x, self.player.table.has_aka_dora)]) - if self.player.ai.previous_shanten == 2: + if self.player.ai.shanten == 2: if dora_count < 2: # having 0 or 1 dora and 2 shanten, let's go for formal tempai # starting from 11th turn @@ -44,7 +44,7 @@ def should_activate_strategy(self): # for 1 shanten we check number of doras and ukeire to determine # correct time to go for formal tempai - if self.player.ai.previous_shanten == 1: + if self.player.ai.shanten == 1: if dora_count == 0: if self.player.ai.ukeire <= 16: return True diff --git a/project/game/ai/first_version/strategies/main.py b/project/game/ai/first_version/strategies/main.py index 534f4361..09b4581e 100644 --- a/project/game/ai/first_version/strategies/main.py +++ b/project/game/ai/first_version/strategies/main.py @@ -192,7 +192,7 @@ def try_to_call_meld(self, tile, is_kamicha_discard): # sometimes we had to call tile, even if it will not improve our hand # otherwise we can call only with improvements of shanten - if not self.meld_had_to_be_called(tile) and shanten >= self.player.ai.previous_shanten: + if not self.meld_had_to_be_called(tile) and shanten >= self.player.ai.shanten: return None, None meld_type = is_chi(best_meld_34) and Meld.CHI or Meld.PON @@ -260,7 +260,7 @@ def _find_best_meld_to_open(self, possible_melds, completed_hand): results = [] for meld in possible_melds: melds = self.player.meld_34_tiles + [meld] - shanten = self.player.ai.shanten.calculate_shanten(completed_hand_34, melds) + shanten = self.player.ai.shanten_calculator.calculate_shanten(completed_hand_34, melds) results.append({'shanten': shanten, 'meld': meld}) results = sorted(results, key=lambda i: i['shanten']) diff --git a/project/game/ai/first_version/strategies/tanyao.py b/project/game/ai/first_version/strategies/tanyao.py index 235a7b73..9dae9d2c 100644 --- a/project/game/ai/first_version/strategies/tanyao.py +++ b/project/game/ai/first_version/strategies/tanyao.py @@ -80,7 +80,7 @@ def should_activate_strategy(self): # if we are 1 shanten, even 2 tiles # that are not suitable and not isolated # is too much - if count_of_not_suitable_not_isolated_tiles >= 2 and self.player.ai.previous_shanten == 1: + if count_of_not_suitable_not_isolated_tiles >= 2 and self.player.ai.shanten == 1: return False # 123 and 789 indices diff --git a/project/game/ai/first_version/strategies/yakuhai.py b/project/game/ai/first_version/strategies/yakuhai.py index 656ae85b..f4a84caf 100644 --- a/project/game/ai/first_version/strategies/yakuhai.py +++ b/project/game/ai/first_version/strategies/yakuhai.py @@ -80,7 +80,7 @@ def should_activate_strategy(self): for pair in self.valued_pairs: # this valuable tile was discarded once # let's open on it in that case - if self.player.total_tiles(pair, tiles_34) == 3 and self.player.ai.previous_shanten > 1: + if self.player.total_tiles(pair, tiles_34) == 3 and self.player.ai.shanten > 1: return True return False @@ -139,7 +139,7 @@ def meld_had_to_be_called(self, tile): for meld in self.player.melds: # for big shanten number we don't need to check already opened pon set, # because it will improve pur hand anyway - if self.player.ai.previous_shanten >= 1: + if self.player.ai.shanten >= 1: break # we have already opened yakuhai pon 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 2da29873..ff904db0 100644 --- a/project/game/ai/first_version/tests/strategies/tests_tanyao.py +++ b/project/game/ai/first_version/tests/strategies/tests_tanyao.py @@ -37,7 +37,7 @@ def test_should_activate_strategy_and_terminal_pairs(self): self.player.init_hand(tiles) self.assertEqual(strategy.should_activate_strategy(), False) - tiles = self._string_to_136_array(sou='222', man='345669', pin='2399') + tiles = self._string_to_136_array(sou='22258', man='3566', pin='2399') self.player.init_hand(tiles) self.assertEqual(strategy.should_activate_strategy(), True) @@ -179,7 +179,7 @@ def test_we_cant_win_with_this_hand(self): # but for already open hand we cant do tsumo # because we don't have a yaku here # so, let's do tsumogiri - self.assertEqual(self.player.ai.previous_shanten, 0) + self.assertEqual(self.player.ai.shanten, 0) self.assertEqual(self._to_string([discard]), '1s') def test_choose_correct_waiting(self): @@ -248,7 +248,7 @@ def test_dont_open_tanyao_with_two_non_central_doras(self): table.add_dora_indicator(self._string_to_136_tile(pin='8')) - tiles = self._string_to_136_array(man='22234', sou='888', pin='5599') + tiles = self._string_to_136_array(man='22234', sou='6888', pin='5599') player.init_hand(tiles) tile = self._string_to_136_tile(pin='5') @@ -256,7 +256,7 @@ def test_dont_open_tanyao_with_two_non_central_doras(self): self.assertEqual(meld, None) def test_dont_open_tanyao_with_three_not_isolated_terminals(self): - tiles = self._string_to_136_array(man='2226', sou='2799', pin='5579') + tiles = self._string_to_136_array(man='22256', sou='2799', pin='5579') self.player.init_hand(tiles) tile = self._string_to_136_tile(pin='5') @@ -264,7 +264,7 @@ def test_dont_open_tanyao_with_three_not_isolated_terminals(self): self.assertEqual(meld, None) def test_dont_open_tanyao_with_two_not_isolated_terminals_one_shanten(self): - tiles = self._string_to_136_array(man='22234', sou='79', pin='55579') + tiles = self._string_to_136_array(man='22234', sou='379', pin='55579') self.player.init_hand(tiles) tile = self._string_to_136_tile(man='5') 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 cbc01eff..174055bc 100644 --- a/project/game/ai/first_version/tests/strategies/tests_yakuhai.py +++ b/project/game/ai/first_version/tests/strategies/tests_yakuhai.py @@ -26,11 +26,7 @@ def test_should_activate_strategy(self): self.assertEqual(strategy.should_activate_strategy(), False) self.table.dora_indicators.append(self._string_to_136_tile(honors='7')) - tiles = self._string_to_136_array(sou='12355689', man='89', honors='55') - self.player.init_hand(tiles) - self.assertEqual(strategy.should_activate_strategy(), True) - - tiles = self._string_to_136_array(sou='12355689', man='89', honors='666') + tiles = self._string_to_136_array(sou='12355689', man='899', honors='55') self.player.init_hand(tiles) self.assertEqual(strategy.should_activate_strategy(), True) @@ -122,10 +118,6 @@ def test_wrong_shanten_improvements_detection(self): meld = self._make_meld(Meld.PON, honors='444') self.player.add_called_meld(meld) - # to rebuild all caches - self.player.draw_tile(self._string_to_136_tile(pin='2')) - self.player.discard_tile() - tile = self._string_to_136_tile(sou='2') meld, _ = self.table.player.try_to_call_meld(tile, True) self.assertEqual(meld, None) @@ -259,8 +251,6 @@ def test_open_hand_and_once_discarded_tile(self): tiles = self._string_to_136_array(man='29', sou='1189', pin='12789', honors='11') self.player.init_hand(tiles) self.player.draw_tile(self._string_to_136_tile(man='6')) - # to calculate hand shanten number - self.player.discard_tile() tile = self._string_to_136_tile(honors='1') meld, _ = self.player.try_to_call_meld(tile, True) @@ -269,8 +259,6 @@ def test_open_hand_and_once_discarded_tile(self): self.table.add_discarded_tile(1, self._string_to_136_tile(honors='1'), False) tiles = self._string_to_136_array(man='29', sou='1189', pin='12789', honors='11') self.player.init_hand(tiles) - self.player.draw_tile(self._string_to_136_tile(man='6')) - self.player.discard_tile() tile = self._string_to_136_tile(honors='1') meld, _ = self.player.try_to_call_meld(tile, True) diff --git a/project/game/ai/first_version/tests/tests_ai.py b/project/game/ai/first_version/tests/tests_ai.py index f24ac7be..a7155c89 100644 --- a/project/game/ai/first_version/tests/tests_ai.py +++ b/project/game/ai/first_version/tests/tests_ai.py @@ -347,7 +347,7 @@ def test_closed_kan_and_wrong_shanten_number_calculation(self): player.discard_tile() # bot not in the tempai, because all 9s in the closed kan - self.assertEqual(player.ai.previous_shanten, 1) + self.assertEqual(player.ai.shanten, 1) def test_closed_kan_and_not_necessary_call(self): """ diff --git a/project/game/ai/first_version/tests/tests_defence.py b/project/game/ai/first_version/tests/tests_defence.py index 6550b149..1f340a23 100644 --- a/project/game/ai/first_version/tests/tests_defence.py +++ b/project/game/ai/first_version/tests/tests_defence.py @@ -423,7 +423,7 @@ def test_defence_against_honitsu_first_case(self): def test_defence_against_honitsu_second_case(self): table = Table() - tiles = self._string_to_136_array(sou='4', pin='223456', man='678', honors='66') + tiles = self._string_to_136_array(sou='4', pin='2223456', man='678', honors='66') table.player.init_hand(tiles) table.add_called_meld(1, self._make_meld(Meld.CHI, sou='789')) diff --git a/project/game/ai/first_version/tests/tests_discards.py b/project/game/ai/first_version/tests/tests_discards.py index 0c256eba..cd21b669 100644 --- a/project/game/ai/first_version/tests/tests_discards.py +++ b/project/game/ai/first_version/tests/tests_discards.py @@ -23,22 +23,22 @@ def test_discard_tile(self): discarded_tile = player.discard_tile() self.assertEqual(self._to_string([discarded_tile]), '9m') - self.assertEqual(player.ai.previous_shanten, 2) + self.assertEqual(player.ai.shanten, 2) player.draw_tile(self._string_to_136_tile(pin='4')) discarded_tile = player.discard_tile() self.assertEqual(self._to_string([discarded_tile]), '1p') - self.assertEqual(player.ai.previous_shanten, 2) + self.assertEqual(player.ai.shanten, 2) player.draw_tile(self._string_to_136_tile(pin='3')) discarded_tile = player.discard_tile() self.assertEqual(self._to_string([discarded_tile]), '9p') - self.assertEqual(player.ai.previous_shanten, 1) + self.assertEqual(player.ai.shanten, 1) player.draw_tile(self._string_to_136_tile(man='4')) discarded_tile = player.discard_tile() self.assertEqual(self._to_string([discarded_tile]), '5m') - self.assertEqual(player.ai.previous_shanten, 0) + self.assertEqual(player.ai.shanten, 0) def test_discard_tile_force_tsumogiri(self): table = Table() From 53dd43979f857387ecae394d111e131bede712dc Mon Sep 17 00:00:00 2001 From: Nihisil Date: Fri, 31 Aug 2018 18:08:21 +0800 Subject: [PATCH 031/126] Fix --- project/game/ai/first_version/main.py | 8 +++----- .../ai/first_version/tests/strategies/tests_yakuhai.py | 1 - 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/project/game/ai/first_version/main.py b/project/game/ai/first_version/main.py index 09aeea25..c13d3525 100644 --- a/project/game/ai/first_version/main.py +++ b/project/game/ai/first_version/main.py @@ -56,14 +56,12 @@ def __init__(self, player): self.erase_state() def init_hand(self): - """ - Let's decide what we will do with our hand (like open for tanyao and etc.) - """ - self.determine_strategy() - # it will set correct hand shanten number and ukeire to the new hand self.discard_tile(None) + # Let's decide what we will do with our hand (like open for tanyao and etc.) + self.determine_strategy() + def draw_tile(self, tile): """ :param tile: 136 tile format 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 174055bc..f17269e1 100644 --- a/project/game/ai/first_version/tests/strategies/tests_yakuhai.py +++ b/project/game/ai/first_version/tests/strategies/tests_yakuhai.py @@ -303,7 +303,6 @@ def test_open_double_south_wind(self): tiles = self._string_to_136_array(man='59', sou='1235', pin='12788', honors='22') self.player.init_hand(tiles) - self.player.init_hand(tiles) tile = self._string_to_136_tile(honors='2') meld, _ = self.player.try_to_call_meld(tile, True) self.assertEqual(meld, None) From 7ac65cc19033f95460bd88b55e98775de1139a7b Mon Sep 17 00:00:00 2001 From: Nihisil Date: Fri, 31 Aug 2018 18:11:01 +0800 Subject: [PATCH 032/126] Add comment --- project/game/ai/first_version/main.py | 1 + 1 file changed, 1 insertion(+) diff --git a/project/game/ai/first_version/main.py b/project/game/ai/first_version/main.py index c13d3525..2207327a 100644 --- a/project/game/ai/first_version/main.py +++ b/project/game/ai/first_version/main.py @@ -57,6 +57,7 @@ def __init__(self, player): def init_hand(self): # 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) # Let's decide what we will do with our hand (like open for tanyao and etc.) From 31f4c510b5efc5da641466f1b7aaa4d34397d21c Mon Sep 17 00:00:00 2001 From: Nihisil Date: Fri, 31 Aug 2018 18:20:30 +0800 Subject: [PATCH 033/126] Refactor dora calculations and strategies a bit --- .../first_version/strategies/formal_tempai.py | 11 ++----- .../game/ai/first_version/strategies/main.py | 33 ++++++++++++++++++- .../ai/first_version/strategies/tanyao.py | 27 +++------------ .../ai/first_version/strategies/yakuhai.py | 12 +++---- 4 files changed, 43 insertions(+), 40 deletions(-) diff --git a/project/game/ai/first_version/strategies/formal_tempai.py b/project/game/ai/first_version/strategies/formal_tempai.py index 52c7b4d4..05d79f27 100644 --- a/project/game/ai/first_version/strategies/formal_tempai.py +++ b/project/game/ai/first_version/strategies/formal_tempai.py @@ -1,6 +1,4 @@ # -*- coding: utf-8 -*- -from mahjong.utils import plus_dora, is_aka_dora - from game.ai.first_version.strategies.main import BaseStrategy @@ -30,11 +28,8 @@ def should_activate_strategy(self): if self.player.ai.shanten >= 3: return True - dora_count = sum([plus_dora(x, self.player.table.dora_indicators) for x in self.player.tiles]) - dora_count += sum([1 for x in self.player.tiles if is_aka_dora(x, self.player.table.has_aka_dora)]) - if self.player.ai.shanten == 2: - if dora_count < 2: + if self.dora_count_total < 2: # having 0 or 1 dora and 2 shanten, let's go for formal tempai # starting from 11th turn return True @@ -45,7 +40,7 @@ def should_activate_strategy(self): # for 1 shanten we check number of doras and ukeire to determine # correct time to go for formal tempai if self.player.ai.shanten == 1: - if dora_count == 0: + if self.dora_count_total == 0: if self.player.ai.ukeire <= 16: return True @@ -54,7 +49,7 @@ def should_activate_strategy(self): return self.player.round_step >= 13 - if dora_count == 1: + if self.dora_count_total == 1: if self.player.ai.ukeire <= 16: return self.player.round_step >= 12 diff --git a/project/game/ai/first_version/strategies/main.py b/project/game/ai/first_version/strategies/main.py index 09b4581e..23b1f399 100644 --- a/project/game/ai/first_version/strategies/main.py +++ b/project/game/ai/first_version/strategies/main.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- from mahjong.meld import Meld from mahjong.tile import TilesConverter -from mahjong.utils import is_man, is_pin, is_sou, is_pon, is_chi +from mahjong.utils import is_man, is_pin, is_sou, is_pon, is_chi, plus_dora, is_aka_dora, is_honor, is_terminal class BaseStrategy(object): @@ -24,6 +24,11 @@ class BaseStrategy(object): min_shanten = 7 go_for_atodzuke = False + dora_count_total = 0 + dora_count_central = 0 + dora_count_not_central = 0 + aka_dora_count = 0 + def __init__(self, strategy_type, player): self.type = strategy_type self.player = player @@ -39,6 +44,8 @@ def should_activate_strategy(self): For now default rule for all strategies: don't open hand with 5+ pairs :return: boolean """ + self.calculate_dora_count() + if self.player.is_open_hand: return True @@ -242,6 +249,30 @@ def meld_had_to_be_called(self, tile): """ return False + def calculate_dora_count(self): + self.dora_count_central = 0 + self.dora_count_not_central = 0 + self.aka_dora_count = 0 + + for tile_136 in self.player.tiles: + tile_34 = tile_136 // 4 + + dora = plus_dora(tile_136, self.player.table.dora_indicators) + + if is_aka_dora(tile_136, self.player.table.has_aka_dora): + self.aka_dora_count += 1 + + if not dora: + continue + + if is_honor(tile_34) or is_terminal(tile_34): + self.dora_count_not_central += 1 + else: + self.dora_count_central += 1 + + self.dora_count_central += self.aka_dora_count + self.dora_count_total = self.dora_count_central + self.dora_count_not_central + def _find_best_meld_to_open(self, possible_melds, completed_hand): """ :param possible_melds: diff --git a/project/game/ai/first_version/strategies/tanyao.py b/project/game/ai/first_version/strategies/tanyao.py index 9dae9d2c..6b6eb59c 100644 --- a/project/game/ai/first_version/strategies/tanyao.py +++ b/project/game/ai/first_version/strategies/tanyao.py @@ -1,9 +1,8 @@ # -*- coding: utf-8 -*- from mahjong.constants import TERMINAL_INDICES, HONOR_INDICES from mahjong.tile import TilesConverter -from mahjong.utils import is_tile_strictly_isolated, is_terminal -from mahjong.utils import plus_dora, is_aka_dora from mahjong.utils import is_honor +from mahjong.utils import is_tile_strictly_isolated from game.ai.first_version.strategies.main import BaseStrategy @@ -97,37 +96,19 @@ def should_activate_strategy(self): if first >= 1 and second >= 1 and third >= 1: return False - dora_count_central = 0 - dora_count_not_central = 0 - for tile_136 in self.player.tiles: - tile_34 = tile_136 // 4 - - dora = plus_dora(tile_136, self.player.table.dora_indicators) - - if is_aka_dora(tile_136, self.player.table.has_aka_dora): - dora_count_central += 1 - - if not dora: - continue - - if is_honor(tile_34) or is_terminal(tile_34): - dora_count_not_central += 1 - else: - dora_count_central += 1 - # if we have 2 or more non-central doras # we don't want to go for tanyao - if dora_count_not_central >= 2: + if self.dora_count_not_central >= 2: return False # if we have less than two central doras # let's not consider open tanyao - if dora_count_central < 2: + if self.dora_count_central < 2: return False # if we have only two central doras let's # wait for 5th turn before opening our hand - if dora_count_central == 2 and self.player.round_step < 5: + if self.dora_count_central == 2 and self.player.round_step < 5: return False return True diff --git a/project/game/ai/first_version/strategies/yakuhai.py b/project/game/ai/first_version/strategies/yakuhai.py index f4a84caf..27f6f376 100644 --- a/project/game/ai/first_version/strategies/yakuhai.py +++ b/project/game/ai/first_version/strategies/yakuhai.py @@ -2,7 +2,6 @@ from mahjong.constants import EAST, SOUTH from mahjong.meld import Meld from mahjong.tile import TilesConverter -from mahjong.utils import plus_dora, is_aka_dora from game.ai.first_version.strategies.main import BaseStrategy @@ -48,24 +47,21 @@ def should_activate_strategy(self): if not has_valued_pair: return False - dora_count = sum([plus_dora(x, self.player.table.dora_indicators) for x in self.player.tiles]) - dora_count += sum([1 for x in self.player.tiles if is_aka_dora(x, self.player.table.has_aka_dora)]) - # let's always open double east if is_double_east_wind: return True # let's open double south if we have a dora in the hand # or we have other valuable pairs - if is_double_south_wind and (dora_count >= 1 or len(self.valued_pairs) >= 2): + if is_double_south_wind and (self.dora_count_total >= 1 or len(self.valued_pairs) >= 2): return True # If we have 1+ dora in the hand and there are 2+ valuable pairs let's open hand - if len(self.valued_pairs) >= 2 and dora_count >= 1: + if len(self.valued_pairs) >= 2 and self.dora_count_total >= 1: return True # If we have 2+ dora in the hand let's open hand - if dora_count >= 2: + if self.dora_count_total >= 2: for x in range(0, 34): # we have other pair in the hand # so we can open hand for atodzuke @@ -74,7 +70,7 @@ def should_activate_strategy(self): return True # If we have 1+ dora in the hand and there is 5+ round step let's open hand - if dora_count >= 1 and self.player.round_step > 5: + if self.dora_count_total >= 1 and self.player.round_step > 5: return True for pair in self.valued_pairs: From 586221c2cb5ce9c66d94db99c18e7b326e82dfaa Mon Sep 17 00:00:00 2001 From: Nihisil Date: Fri, 31 Aug 2018 19:22:56 +0800 Subject: [PATCH 034/126] #50. Activate strategy after enemy discard as well --- project/game/ai/first_version/main.py | 13 +++-- .../first_version/strategies/formal_tempai.py | 5 +- .../ai/first_version/strategies/honitsu.py | 7 ++- .../game/ai/first_version/strategies/main.py | 15 +++--- .../ai/first_version/strategies/tanyao.py | 8 +-- .../ai/first_version/strategies/yakuhai.py | 19 +++---- .../tests/strategies/tests_formal_tempai.py | 6 +-- .../tests/strategies/tests_honitsu.py | 12 ++--- .../tests/strategies/tests_tanyao.py | 54 +++++++++++-------- .../tests/strategies/tests_yakuhai.py | 10 ++-- 10 files changed, 82 insertions(+), 67 deletions(-) diff --git a/project/game/ai/first_version/main.py b/project/game/ai/first_version/main.py index 2207327a..e6ff7ff4 100644 --- a/project/game/ai/first_version/main.py +++ b/project/game/ai/first_version/main.py @@ -61,14 +61,14 @@ def init_hand(self): self.discard_tile(None) # Let's decide what we will do with our hand (like open for tanyao and etc.) - self.determine_strategy() + self.determine_strategy(self.player.tiles) def draw_tile(self, tile): """ :param tile: 136 tile format :return: """ - self.determine_strategy() + self.determine_strategy(self.player.tiles) def discard_tile(self, discard_tile): # we called meld and we had discard tile that we wanted to discard @@ -194,10 +194,13 @@ def count_tiles(self, waiting, tiles_34): return n def try_to_call_meld(self, tile, is_kamicha_discard): + tiles_136 = self.player.tiles[:] + [tile] + self.determine_strategy(tiles_136) + if not self.current_strategy: return None, None - meld, discard_option = self.current_strategy.try_to_call_meld(tile, is_kamicha_discard) + meld, discard_option = self.current_strategy.try_to_call_meld(tile, is_kamicha_discard, tiles_136) tile_to_discard = None if discard_option: self.last_discard_option = discard_option @@ -205,7 +208,7 @@ def try_to_call_meld(self, tile, is_kamicha_discard): return meld, tile_to_discard - def determine_strategy(self): + def determine_strategy(self, tiles_136): # for already opened hand we don't need to give up on selected strategy if self.player.is_open_hand and self.current_strategy: return False @@ -225,7 +228,7 @@ def determine_strategy(self): strategies.append(FormalTempaiStrategy(BaseStrategy.FORMAL_TEMPAI, self.player)) for strategy in strategies: - if strategy.should_activate_strategy(): + if strategy.should_activate_strategy(tiles_136): self.current_strategy = strategy if self.current_strategy: diff --git a/project/game/ai/first_version/strategies/formal_tempai.py b/project/game/ai/first_version/strategies/formal_tempai.py index 05d79f27..6b40dce0 100644 --- a/project/game/ai/first_version/strategies/formal_tempai.py +++ b/project/game/ai/first_version/strategies/formal_tempai.py @@ -4,14 +4,13 @@ class FormalTempaiStrategy(BaseStrategy): - def should_activate_strategy(self): + def should_activate_strategy(self, tiles_136): """ When we get closer to the end of the round, we start to consider going for formal tempai. - :return: boolean """ - result = super(FormalTempaiStrategy, self).should_activate_strategy() + result = super(FormalTempaiStrategy, self).should_activate_strategy(tiles_136) if not result: return False diff --git a/project/game/ai/first_version/strategies/honitsu.py b/project/game/ai/first_version/strategies/honitsu.py index a9048194..499cf9dd 100644 --- a/project/game/ai/first_version/strategies/honitsu.py +++ b/project/game/ai/first_version/strategies/honitsu.py @@ -11,17 +11,16 @@ class HonitsuStrategy(BaseStrategy): chosen_suit = None - def should_activate_strategy(self): + def should_activate_strategy(self, tiles_136): """ We can go for honitsu/chinitsu strategy if we have prevalence of one suit and honor tiles - :return: boolean """ - result = super(HonitsuStrategy, self).should_activate_strategy() + result = super(HonitsuStrategy, self).should_activate_strategy(tiles_136) if not result: return False - tiles_34 = TilesConverter.to_34_array(self.player.tiles) + tiles_34 = TilesConverter.to_34_array(tiles_136) suits = count_tiles_by_suits(tiles_34) honor = [x for x in suits if x['name'] == 'honor'][0] diff --git a/project/game/ai/first_version/strategies/main.py b/project/game/ai/first_version/strategies/main.py index 23b1f399..7bf07eef 100644 --- a/project/game/ai/first_version/strategies/main.py +++ b/project/game/ai/first_version/strategies/main.py @@ -37,19 +37,20 @@ def __init__(self, strategy_type, player): def __str__(self): return self.TYPES[self.type] - def should_activate_strategy(self): + def should_activate_strategy(self, tiles_136): """ Based on player hand and table situation we can determine should we use this strategy or not. For now default rule for all strategies: don't open hand with 5+ pairs + :param: tiles_136 :return: boolean """ - self.calculate_dora_count() + self.calculate_dora_count(tiles_136) if self.player.is_open_hand: return True - tiles_34 = TilesConverter.to_34_array(self.player.tiles) + tiles_34 = TilesConverter.to_34_array(tiles_136) count_of_pairs = len([x for x in range(0, 34) if tiles_34[x] >= 2]) return count_of_pairs < 5 @@ -95,12 +96,13 @@ def determine_what_to_discard(self, closed_hand, outs_results, shanten, for_open return outs_results - def try_to_call_meld(self, tile, is_kamicha_discard): + def try_to_call_meld(self, tile, is_kamicha_discard, new_tiles): """ Determine should we call a meld or not. If yes, it will return Meld object and tile to discard :param tile: 136 format tile :param is_kamicha_discard: boolean + :param new_tiles: :return: Meld and DiscardOption objects """ if self.player.in_riichi: @@ -120,7 +122,6 @@ def try_to_call_meld(self, tile, is_kamicha_discard): return None, None discarded_tile = tile // 4 - new_tiles = self.player.tiles[:] + [tile] closed_hand_34 = TilesConverter.to_34_array(closed_hand + [tile]) combinations = [] @@ -249,12 +250,12 @@ def meld_had_to_be_called(self, tile): """ return False - def calculate_dora_count(self): + def calculate_dora_count(self, tiles_136): self.dora_count_central = 0 self.dora_count_not_central = 0 self.aka_dora_count = 0 - for tile_136 in self.player.tiles: + for tile_136 in tiles_136: tile_34 = tile_136 // 4 dora = plus_dora(tile_136, self.player.table.dora_indicators) diff --git a/project/game/ai/first_version/strategies/tanyao.py b/project/game/ai/first_version/strategies/tanyao.py index 6b6eb59c..bcc74f36 100644 --- a/project/game/ai/first_version/strategies/tanyao.py +++ b/project/game/ai/first_version/strategies/tanyao.py @@ -11,21 +11,21 @@ class TanyaoStrategy(BaseStrategy): min_shanten = 3 not_suitable_tiles = TERMINAL_INDICES + HONOR_INDICES - def should_activate_strategy(self): + def should_activate_strategy(self, tiles_136): """ Tanyao hand is a hand without terminal and honor tiles, to achieve this we will use different approaches :return: boolean """ - result = super(TanyaoStrategy, self).should_activate_strategy() + result = super(TanyaoStrategy, self).should_activate_strategy(tiles_136) if not result: return False - tiles = TilesConverter.to_34_array(self.player.tiles) + tiles = TilesConverter.to_34_array(tiles_136) closed_hand_34 = TilesConverter.to_34_array(self.player.closed_hand) - isolated_tiles = [x // 4 for x in self.player.tiles if is_tile_strictly_isolated(closed_hand_34, x // 4) or is_honor(x // 4)] + isolated_tiles = [x // 4 for x in tiles_136 if is_tile_strictly_isolated(closed_hand_34, x // 4) or is_honor(x // 4)] count_of_terminal_pon_sets = 0 count_of_terminal_pairs = 0 diff --git a/project/game/ai/first_version/strategies/yakuhai.py b/project/game/ai/first_version/strategies/yakuhai.py index 27f6f376..999c0109 100644 --- a/project/game/ai/first_version/strategies/yakuhai.py +++ b/project/game/ai/first_version/strategies/yakuhai.py @@ -16,16 +16,17 @@ def __init__(self, strategy_type, player): self.valued_pairs = [] self.has_valued_pon = False - def should_activate_strategy(self): + def should_activate_strategy(self, tiles_136): """ We can go for yakuhai strategy if we have at least one yakuhai pair in the hand :return: boolean """ - result = super(YakuhaiStrategy, self).should_activate_strategy() + result = super(YakuhaiStrategy, self).should_activate_strategy(tiles_136) if not result: return False - tiles_34 = TilesConverter.to_34_array(self.player.tiles) + tiles_34 = TilesConverter.to_34_array(tiles_136) + player_hand_tiles_34 = TilesConverter.to_34_array(self.player.tiles) self.valued_pairs = [x for x in self.player.valued_honors if tiles_34[x] >= 2] is_double_east_wind = len([x for x in self.valued_pairs if x == EAST]) == 2 @@ -39,7 +40,7 @@ def should_activate_strategy(self): for pair in self.valued_pairs: # we have valued pair in the hand and there are enough tiles # in the wall - if self.player.total_tiles(pair, tiles_34) < 4: + if self.player.total_tiles(pair, player_hand_tiles_34) < 4: has_valued_pair = True break @@ -76,7 +77,7 @@ def should_activate_strategy(self): for pair in self.valued_pairs: # this valuable tile was discarded once # let's open on it in that case - if self.player.total_tiles(pair, tiles_34) == 3 and self.player.ai.shanten > 1: + if self.player.total_tiles(pair, player_hand_tiles_34) == 3 and self.player.ai.shanten > 1: return True return False @@ -150,19 +151,19 @@ def meld_had_to_be_called(self, tile): return False - def try_to_call_meld(self, tile, is_kamicha_discard): + def try_to_call_meld(self, tile, is_kamicha_discard, tiles_136): if self.has_valued_pon: - return super(YakuhaiStrategy, self).try_to_call_meld(tile, is_kamicha_discard) + return super(YakuhaiStrategy, self).try_to_call_meld(tile, is_kamicha_discard, tiles_136) tile_34 = tile // 4 # we will open hand for atodzuke only in the special cases if not self.player.is_open_hand and tile_34 not in self.valued_pairs: if self.go_for_atodzuke: - return super(YakuhaiStrategy, self).try_to_call_meld(tile, is_kamicha_discard) + return super(YakuhaiStrategy, self).try_to_call_meld(tile, is_kamicha_discard, tiles_136) return None, None - return super(YakuhaiStrategy, self).try_to_call_meld(tile, is_kamicha_discard) + return super(YakuhaiStrategy, self).try_to_call_meld(tile, is_kamicha_discard, tiles_136) def _is_yakuhai_pon(self, meld): return meld.type == Meld.PON and meld.tiles[0] // 4 in self.player.valued_honors 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 77c91a74..4f2ce6b9 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 @@ -21,7 +21,7 @@ def test_should_activate_strategy(self): tiles = self._string_to_136_array(sou='12355689', man='89', pin='339') self.player.init_hand(tiles) - self.assertEqual(strategy.should_activate_strategy(), False) + self.assertEqual(strategy.should_activate_strategy(self.player.tiles), False) self.player.draw_tile(self._string_to_136_tile(honors='1')) # to calculate hand shanten number @@ -32,9 +32,9 @@ def test_should_activate_strategy(self): for i in range(0, 9): self.player.add_discarded_tile(Tile(0, False)) - self.assertEqual(strategy.should_activate_strategy(), 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(), True) + self.assertEqual(strategy.should_activate_strategy(self.player.tiles), True) 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 2165ba15..973b9548 100644 --- a/project/game/ai/first_version/tests/strategies/tests_honitsu.py +++ b/project/game/ai/first_version/tests/strategies/tests_honitsu.py @@ -17,22 +17,22 @@ def test_should_activate_strategy(self): tiles = self._string_to_136_array(sou='12355', man='12389', honors='123') player.init_hand(tiles) - self.assertEqual(strategy.should_activate_strategy(), False) + self.assertEqual(strategy.should_activate_strategy(player.tiles), False) tiles = self._string_to_136_array(sou='12355', man='238', honors='11234') player.init_hand(tiles) - self.assertEqual(strategy.should_activate_strategy(), True) + self.assertEqual(strategy.should_activate_strategy(player.tiles), True) # with hand without pairs we not should go for honitsu, # because it is far away from tempai tiles = self._string_to_136_array(sou='12358', man='238', honors='12345') player.init_hand(tiles) - self.assertEqual(strategy.should_activate_strategy(), False) + self.assertEqual(strategy.should_activate_strategy(player.tiles), False) # with chitoitsu-like hand we don't need to go for honitsu tiles = self._string_to_136_array(pin='77', man='3355677899', sou='11') player.init_hand(tiles) - self.assertEqual(strategy.should_activate_strategy(), False) + self.assertEqual(strategy.should_activate_strategy(player.tiles), False) def test_suitable_tiles(self): table = Table() @@ -41,7 +41,7 @@ def test_suitable_tiles(self): tiles = self._string_to_136_array(sou='12355', man='238', honors='11234') player.init_hand(tiles) - self.assertEqual(strategy.should_activate_strategy(), True) + self.assertEqual(strategy.should_activate_strategy(player.tiles), True) tile = self._string_to_136_tile(man='1') self.assertEqual(strategy.is_tile_suitable(tile), False) @@ -139,4 +139,4 @@ def test_dont_go_for_honitsu_with_ryanmen_in_other_suit(self): tiles = self._string_to_136_array(man='14489', sou='45', pin='67', honors='44456') player.init_hand(tiles) - self.assertEqual(strategy.should_activate_strategy(), False) + self.assertEqual(strategy.should_activate_strategy(player.tiles), False) 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 ff904db0..430d37ee 100644 --- a/project/game/ai/first_version/tests/strategies/tests_tanyao.py +++ b/project/game/ai/first_version/tests/strategies/tests_tanyao.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- import unittest +from mahjong.constants import FIVE_RED_PIN from mahjong.meld import Meld from mahjong.tests_mixin import TestMixin @@ -20,75 +21,75 @@ def test_should_activate_strategy_and_terminal_pon_sets(self): tiles = self._string_to_136_array(sou='222', man='3459', pin='233', honors='111') self.player.init_hand(tiles) - self.assertEqual(strategy.should_activate_strategy(), False) + self.assertEqual(strategy.should_activate_strategy(self.player.tiles), False) tiles = self._string_to_136_array(sou='222', man='3459', pin='233999') self.player.init_hand(tiles) - self.assertEqual(strategy.should_activate_strategy(), False) + self.assertEqual(strategy.should_activate_strategy(self.player.tiles), False) tiles = self._string_to_136_array(sou='222', man='3459', pin='233444') self.player.init_hand(tiles) - self.assertEqual(strategy.should_activate_strategy(), True) + self.assertEqual(strategy.should_activate_strategy(self.player.tiles), True) def test_should_activate_strategy_and_terminal_pairs(self): strategy = TanyaoStrategy(BaseStrategy.TANYAO, self.player) tiles = self._string_to_136_array(sou='222', man='3459', pin='2399', honors='11') self.player.init_hand(tiles) - self.assertEqual(strategy.should_activate_strategy(), False) + self.assertEqual(strategy.should_activate_strategy(self.player.tiles), False) tiles = self._string_to_136_array(sou='22258', man='3566', pin='2399') self.player.init_hand(tiles) - self.assertEqual(strategy.should_activate_strategy(), True) + self.assertEqual(strategy.should_activate_strategy(self.player.tiles), True) def test_should_activate_strategy_and_valued_pair(self): strategy = TanyaoStrategy(BaseStrategy.TANYAO, self.player) tiles = self._string_to_136_array(man='23446679', sou='222', honors='55') self.player.init_hand(tiles) - self.assertEqual(strategy.should_activate_strategy(), False) + self.assertEqual(strategy.should_activate_strategy(self.player.tiles), False) tiles = self._string_to_136_array(man='23446679', sou='222', honors='22') self.player.init_hand(tiles) - self.assertEqual(strategy.should_activate_strategy(), True) + self.assertEqual(strategy.should_activate_strategy(self.player.tiles), True) def test_should_activate_strategy_and_chitoitsu_like_hand(self): strategy = TanyaoStrategy(BaseStrategy.TANYAO, self.player) tiles = self._string_to_136_array(sou='223388', man='2244', pin='6687') self.player.init_hand(tiles) - self.assertEqual(strategy.should_activate_strategy(), False) + self.assertEqual(strategy.should_activate_strategy(self.player.tiles), False) def test_should_activate_strategy_and_already_completed_sided_set(self): strategy = TanyaoStrategy(BaseStrategy.TANYAO, self.player) tiles = self._string_to_136_array(sou='123234', man='2349', pin='234') self.player.init_hand(tiles) - self.assertEqual(strategy.should_activate_strategy(), False) + self.assertEqual(strategy.should_activate_strategy(self.player.tiles), False) tiles = self._string_to_136_array(sou='234789', man='2349', pin='234') self.player.init_hand(tiles) - self.assertEqual(strategy.should_activate_strategy(), False) + self.assertEqual(strategy.should_activate_strategy(self.player.tiles), False) tiles = self._string_to_136_array(sou='234', man='1233459', pin='234') self.player.init_hand(tiles) - self.assertEqual(strategy.should_activate_strategy(), False) + self.assertEqual(strategy.should_activate_strategy(self.player.tiles), False) tiles = self._string_to_136_array(sou='234', man='2227899', pin='234') self.player.init_hand(tiles) - self.assertEqual(strategy.should_activate_strategy(), False) + self.assertEqual(strategy.should_activate_strategy(self.player.tiles), False) tiles = self._string_to_136_array(sou='234', man='2229', pin='122334') self.player.init_hand(tiles) - self.assertEqual(strategy.should_activate_strategy(), False) + self.assertEqual(strategy.should_activate_strategy(self.player.tiles), False) tiles = self._string_to_136_array(sou='234', man='2229', pin='234789') self.player.init_hand(tiles) - self.assertEqual(strategy.should_activate_strategy(), False) + self.assertEqual(strategy.should_activate_strategy(self.player.tiles), False) tiles = self._string_to_136_array(sou='223344', man='2229', pin='234') self.player.init_hand(tiles) - self.assertEqual(strategy.should_activate_strategy(), True) + self.assertEqual(strategy.should_activate_strategy(self.player.tiles), True) def test_suitable_tiles(self): strategy = TanyaoStrategy(BaseStrategy.TANYAO, self.player) @@ -243,16 +244,13 @@ def test_we_dont_need_to_discard_terminals_from_closed_hand(self): self.assertEqual(self._to_string([tile_to_discard]), '8p') def test_dont_open_tanyao_with_two_non_central_doras(self): - table = self._make_table() - player = table.player - - table.add_dora_indicator(self._string_to_136_tile(pin='8')) + self.table.add_dora_indicator(self._string_to_136_tile(pin='8')) tiles = self._string_to_136_array(man='22234', sou='6888', pin='5599') - player.init_hand(tiles) + self.table.player.init_hand(tiles) tile = self._string_to_136_tile(pin='5') - meld, _ = player.try_to_call_meld(tile, False) + meld, _ = self.table.player.try_to_call_meld(tile, False) self.assertEqual(meld, None) def test_dont_open_tanyao_with_three_not_isolated_terminals(self): @@ -287,6 +285,20 @@ def test_dont_count_terminal_tiles_in_ukeire(self): discard = self.player.discard_tile() self.assertEqual(self._to_string([discard]), '8m') + def test_determine_strategy_when_we_try_to_call_meld(self): + self.table.has_aka_dora = True + + self.table.add_dora_indicator(self._string_to_136_tile(sou='5')) + tiles = self._string_to_136_array(man='66678', sou='6888', pin='5588') + self.table.player.init_hand(tiles) + + # with this red five we will have 2 dora in the hand + # and in that case we can open our hand + meld, _ = self.table.player.try_to_call_meld(FIVE_RED_PIN, False) + self.assertNotEqual(meld, None) + + self._assert_tanyao(self.player) + def _make_table(self): table = Table() table.has_open_tanyao = 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 f17269e1..55b276a9 100644 --- a/project/game/ai/first_version/tests/strategies/tests_yakuhai.py +++ b/project/game/ai/first_version/tests/strategies/tests_yakuhai.py @@ -23,17 +23,17 @@ def test_should_activate_strategy(self): tiles = self._string_to_136_array(sou='12355689', man='89', honors='123') self.player.init_hand(tiles) - self.assertEqual(strategy.should_activate_strategy(), False) + self.assertEqual(strategy.should_activate_strategy(self.player.tiles), False) self.table.dora_indicators.append(self._string_to_136_tile(honors='7')) tiles = self._string_to_136_array(sou='12355689', man='899', honors='55') self.player.init_hand(tiles) - self.assertEqual(strategy.should_activate_strategy(), True) + self.assertEqual(strategy.should_activate_strategy(self.player.tiles), True) # with chitoitsu-like hand we don't need to go for yakuhai tiles = self._string_to_136_array(sou='1235566', man='8899', honors='66') self.player.init_hand(tiles) - self.assertEqual(strategy.should_activate_strategy(), False) + self.assertEqual(strategy.should_activate_strategy(self.player.tiles), False) def test_dont_activate_strategy_if_we_dont_have_enough_tiles_in_the_wall(self): strategy = YakuhaiStrategy(BaseStrategy.YAKUHAI, self.player) @@ -42,13 +42,13 @@ def test_dont_activate_strategy_if_we_dont_have_enough_tiles_in_the_wall(self): tiles = self._string_to_136_array(man='59', sou='1235', pin='12789', honors='55') self.player.init_hand(tiles) - self.assertEqual(strategy.should_activate_strategy(), True) + self.assertEqual(strategy.should_activate_strategy(self.player.tiles), True) self.table.add_discarded_tile(3, self._string_to_136_tile(honors='5'), False) self.table.add_discarded_tile(3, self._string_to_136_tile(honors='5'), False) # we can't complete yakuhai, because there is not enough honor tiles - self.assertEqual(strategy.should_activate_strategy(), False) + self.assertEqual(strategy.should_activate_strategy(self.player.tiles), False) def test_suitable_tiles(self): strategy = YakuhaiStrategy(BaseStrategy.YAKUHAI, self.player) From d5009804b35327746982acd2438cb9405b7e76f8 Mon Sep 17 00:00:00 2001 From: Nihisil Date: Fri, 31 Aug 2018 19:27:00 +0800 Subject: [PATCH 035/126] Print closed kan meld in log as well --- project/game/player.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/project/game/player.py b/project/game/player.py index d3932f50..1460fe3f 100644 --- a/project/game/player.py +++ b/project/game/player.py @@ -224,11 +224,14 @@ def format_hand_for_print(self, tile): TilesConverter.to_one_line_string(self.closed_hand), TilesConverter.to_one_line_string([tile]) ) - if self.is_open_hand: - melds = [] - for item in self.melds: - melds.append('{}'.format(TilesConverter.to_one_line_string(item.tiles))) + + melds = [] + for item in self.melds: + melds.append('{}'.format(TilesConverter.to_one_line_string(item.tiles))) + + if melds: hand_string += ' [{}]'.format(', '.join(melds)) + return hand_string @property From 3a2a927481e577a88dce56d6c013dc75676d8bbd Mon Sep 17 00:00:00 2001 From: Nihisil Date: Sat, 1 Sep 2018 08:26:13 +0800 Subject: [PATCH 036/126] Print to the log the point where we stop defence --- project/game/ai/first_version/main.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/project/game/ai/first_version/main.py b/project/game/ai/first_version/main.py index e6ff7ff4..ea1441ae 100644 --- a/project/game/ai/first_version/main.py +++ b/project/game/ai/first_version/main.py @@ -96,6 +96,8 @@ def discard_tile(self, discard_tile): if defence_tile: return self.process_discard_option(defence_tile, self.player.closed_hand) else: + if self.in_defence: + logger.info('Stop defence mode') self.in_defence = False return self.process_discard_option(selected_tile, self.player.closed_hand) From 3d338acea9ea34032554d914a9ea93fe6ab91e3e Mon Sep 17 00:00:00 2001 From: bogachev-pa Date: Fri, 31 Aug 2018 22:09:58 +0300 Subject: [PATCH 037/126] improve honitsu and add chinitsu strategies (#55) --- project/game/ai/first_version/main.py | 4 +- .../ai/first_version/strategies/chinitsu.py | 171 ++++++++++++++++ .../ai/first_version/strategies/honitsu.py | 192 ++++++++++++++++-- .../game/ai/first_version/strategies/main.py | 18 +- .../tests/strategies/tests_chinitsu.py | 95 +++++++++ .../tests/strategies/tests_honitsu.py | 36 ++-- .../tests/strategies/tests_yakuhai.py | 2 +- 7 files changed, 472 insertions(+), 46 deletions(-) create mode 100644 project/game/ai/first_version/strategies/chinitsu.py create mode 100644 project/game/ai/first_version/tests/strategies/tests_chinitsu.py diff --git a/project/game/ai/first_version/main.py b/project/game/ai/first_version/main.py index e6ff7ff4..54cb109f 100644 --- a/project/game/ai/first_version/main.py +++ b/project/game/ai/first_version/main.py @@ -20,6 +20,7 @@ from game.ai.first_version.strategies.tanyao import TanyaoStrategy from game.ai.first_version.strategies.yakuhai import YakuhaiStrategy from game.ai.first_version.strategies.formal_tempai import FormalTempaiStrategy +from game.ai.first_version.strategies.chinitsu import ChinitsuStrategy logger = logging.getLogger('ai') @@ -218,8 +219,9 @@ def determine_strategy(self, tiles_136): # order is important strategies = [ - YakuhaiStrategy(BaseStrategy.YAKUHAI, self.player), + ChinitsuStrategy(BaseStrategy.CHINITSU, self.player), HonitsuStrategy(BaseStrategy.HONITSU, self.player), + YakuhaiStrategy(BaseStrategy.YAKUHAI, self.player), ] if self.player.table.has_open_tanyao: diff --git a/project/game/ai/first_version/strategies/chinitsu.py b/project/game/ai/first_version/strategies/chinitsu.py new file mode 100644 index 00000000..ca65ec1c --- /dev/null +++ b/project/game/ai/first_version/strategies/chinitsu.py @@ -0,0 +1,171 @@ +# -*- coding: utf-8 -*- +from mahjong.tile import TilesConverter +from mahjong.utils import count_tiles_by_suits, is_tile_strictly_isolated +from mahjong.utils import is_man, is_pin, is_sou, is_pon, is_chi, plus_dora, is_aka_dora, is_honor, is_pair, simplify + +from game.ai.first_version.strategies.main import BaseStrategy +from game.ai.first_version.strategies.honitsu import HonitsuStrategy + + +class ChinitsuStrategy(BaseStrategy): + min_shanten = 4 + + chosen_suit = None + + dora_count_suitable = 0 + dora_count_not_suitable = 0 + + def should_activate_strategy(self, tiles_136): + """ + We can go for chinitsu strategy if we have prevalence of one suit + """ + + result = super(ChinitsuStrategy, self).should_activate_strategy(tiles_136) + if not result: + return False + + tiles_34 = TilesConverter.to_34_array(tiles_136) + suits = count_tiles_by_suits(tiles_34) + + suits = [x for x in suits if x['name'] != 'honor'] + suits = sorted(suits, key=lambda x: x['count'], reverse=True) + suit = suits[0] + + count_of_shuntsu_other_suits = 0 + count_of_koutsu_other_suits = 0 + + count_of_shuntsu_other_suits += HonitsuStrategy._count_of_shuntsu(tiles_34, suits[1]['function']) + count_of_shuntsu_other_suits += HonitsuStrategy._count_of_shuntsu(tiles_34, suits[2]['function']) + + count_of_koutsu_other_suits += HonitsuStrategy._count_of_koutsu(tiles_34, suits[1]['function']) + count_of_koutsu_other_suits += HonitsuStrategy._count_of_koutsu(tiles_34, suits[2]['function']) + + # we need to have at least 9 tiles of one suit to fo for chinitsu + if suit['count'] < 9: + return False + + # here we only check doras in different suits, we will deal + # with honors later + self._initialize_chinitsu_dora_count(tiles_136, suit) + + # 3 non-isolated doras in other suits is too much + # to even try + if self.dora_count_not_suitable >= 3: + return False + + if self.dora_count_not_suitable == 2: + # 2 doras in other suits, no doras in our suit + # let's not consider chinitsu + if self.dora_count_suitable == 0: + return False + + # we have 2 doras in other suits and we + # are 1 shanten, let's not rush chinitsu + if self.player.ai.shanten == 1: + return False + + # too late to get rid of doras in other suits + if self.player.round_step > 8: + return False + + # we are almost tempai, chinitsu is slower + if suit['count'] == 9 and self.player.ai.shanten == 1: + return False + + # only 10 tiles by 8th turn is too slow, considering alternative + if suit['count'] == 10 and self.player.ai.shanten == 1 and self.player.round_step > 8: + return False + + # if we have a pon of honors, let's not go for chinitsu + honor_pons = len([x for x in range(0, 34) if is_honor(x) and tiles_34[x] >= 3]) + if honor_pons >= 1: + return False + + # if we have a valued pair, let's not go for chinitsu + valued_pairs = len([x for x in self.player.valued_honors if tiles_34[x] == 2]) + if valued_pairs >= 1: + return False + + # if we have a pair of honor doras, let's not go for chinitsu + honor_doras_pairs = len([x for x in range(0, 34) if is_honor(x) and tiles_34[x] == 2 + and plus_dora(x * 4, self.player.table.dora_indicators)]) + if honor_doras_pairs >= 1: + return False + + # if we have a honor pair, we will only throw them away if it's early in the game + # and if we have lots of tiles in our suit + honor_pairs = len([x for x in range(0, 34) if is_honor(x) and tiles_34[x] == 2]) + if honor_pairs >= 2: + return False + if honor_pairs == 1: + if suit['count'] < 11: + return False + if self.player.round_step > 8: + return False + + # if we have a complete set in other suits, we can only throw it away if it's early in the game + # TODO: also check that it doesn't contain dora + if count_of_shuntsu_other_suits + count_of_koutsu_other_suits >= 1: + # too late to throw away chi after 8 step + if self.player.round_step > 8: + return False + + # already 1 shanten, no need to throw away complete set + if self.player.round_step > 5 and self.player.ai.shanten == 1: + return False + + self.chosen_suit = suit['function'] + + return True + + def is_tile_suitable(self, tile): + """ + We can use only tiles of chosen suit and honor tiles + :param tile: 136 tiles format + :return: True + """ + tile //= 4 + return self.chosen_suit(tile) + + def _initialize_chinitsu_dora_count(self, tiles_136, suit): + tiles_34 = TilesConverter.to_34_array(tiles_136) + + dora_count_man = 0 + dora_count_man_not_isolated = 0 + dora_count_pin = 0 + dora_count_pin_not_isolated = 0 + dora_count_sou = 0 + dora_count_sou_not_isolated = 0 + + for tile_136 in tiles_136: + tile_34 = tile_136 // 4 + + dora_count = plus_dora(tile_136, self.player.table.dora_indicators) + + if is_aka_dora(tile_136, self.player.table.has_aka_dora): + dora_count += 1 + + if is_man(tile_34): + dora_count_man += dora_count + if not is_tile_strictly_isolated(tiles_34, tile_34): + dora_count_man_not_isolated += dora_count + + if is_pin(tile_34): + dora_count_pin += dora_count + if not is_tile_strictly_isolated(tiles_34, tile_34): + dora_count_pin_not_isolated += dora_count + + if is_sou(tile_34): + dora_count_sou += dora_count + if not is_tile_strictly_isolated(tiles_34, tile_34): + dora_count_sou_not_isolated += dora_count + + if suit['name'] == 'pin': + self.dora_count_suitable = dora_count_pin + self.dora_count_not_suitable= dora_count_man_not_isolated + dora_count_sou_not_isolated + elif suit['name'] == 'sou': + self.dora_count_suitable = dora_count_sou + self.dora_count_not_suitable= dora_count_man_not_isolated + dora_count_pin_not_isolated + elif suit['name'] == 'man': + self.dora_count_suitable = dora_count_man + self.dora_count_not_suitable = dora_count_sou_not_isolated + dora_count_pin_not_isolated diff --git a/project/game/ai/first_version/strategies/honitsu.py b/project/game/ai/first_version/strategies/honitsu.py index 499cf9dd..90937c5c 100644 --- a/project/game/ai/first_version/strategies/honitsu.py +++ b/project/game/ai/first_version/strategies/honitsu.py @@ -1,19 +1,22 @@ # -*- coding: utf-8 -*- from mahjong.tile import TilesConverter -from mahjong.utils import count_tiles_by_suits, is_honor, simplify +from mahjong.utils import count_tiles_by_suits, simplify, is_tile_strictly_isolated +from mahjong.utils import is_man, is_pin, is_sou, is_pon, is_chi, plus_dora, is_aka_dora, is_honor, is_pair from game.ai.first_version.strategies.main import BaseStrategy class HonitsuStrategy(BaseStrategy): - REQUIRED_TILES = 10 min_shanten = 4 chosen_suit = None + dora_count_not_suitable = 0 + tiles_count_not_suitable = 0 + def should_activate_strategy(self, tiles_136): """ - We can go for honitsu/chinitsu strategy if we have prevalence of one suit and honor tiles + We can go for honitsu strategy if we have prevalence of one suit and honor tiles """ result = super(HonitsuStrategy, self).should_activate_strategy(tiles_136) @@ -23,33 +26,90 @@ def should_activate_strategy(self, tiles_136): tiles_34 = TilesConverter.to_34_array(tiles_136) suits = count_tiles_by_suits(tiles_34) - honor = [x for x in suits if x['name'] == 'honor'][0] suits = [x for x in suits if x['name'] != 'honor'] suits = sorted(suits, key=lambda x: x['count'], reverse=True) suit = suits[0] - count_of_pairs = 0 - for x in range(0, 34): - if tiles_34[x] >= 2: - count_of_pairs += 1 - suits.remove(suit) - count_of_ryanmens = self._find_ryanmen_waits(tiles_34, suits[0]['function']) - count_of_ryanmens += self._find_ryanmen_waits(tiles_34, suits[1]['function']) + count_of_shuntsu_other_suits = 0 + count_of_koutsu_other_suits = 0 + + count_of_shuntsu_other_suits += self._count_of_shuntsu(tiles_34, suits[1]['function']) + count_of_shuntsu_other_suits += self._count_of_shuntsu(tiles_34, suits[2]['function']) + + count_of_koutsu_other_suits += self._count_of_koutsu(tiles_34, suits[1]['function']) + count_of_koutsu_other_suits += self._count_of_koutsu(tiles_34, suits[2]['function']) - # it is a bad idea go for honitsu with ryanmen in other suit - if count_of_ryanmens > 0 and not self.player.is_open_hand: + self._calculate_not_suitable_tiles_cnt(tiles_34, suit['function']) + self._initialize_honitsu_dora_count(tiles_136, suit) + + # let's not go for honitsu if we have 5 or more non-isolated + # tiles in other suits + if self.tiles_count_not_suitable >= 5: return False - # we need to have prevalence of one suit and completed forms in the hand - # for now let's check only pairs in the hand - # TODO check ryanmen forms as well and honor tiles count - if suit['count'] + honor['count'] >= HonitsuStrategy.REQUIRED_TILES: - self.chosen_suit = suit['function'] - return count_of_pairs > 0 - else: + # let's not go for honitsu if we have 2 or more non-isolated doras + # in other suits + if self.dora_count_not_suitable >= 2: return False + # if we have a pon of valued doras, let's not go for honitsu + # we have a mangan anyway, let's go for fastest hand + valued_pons = [x for x in self.player.valued_honors if tiles_34[x] >= 3] + for pon in valued_pons: + dora_count = plus_dora(pon * 4, self.player.table.dora_indicators) + if dora_count > 0: + return False + + valued_pairs = len([x for x in self.player.valued_honors if tiles_34[x] == 2]) + honor_pairs_or_pons = len([x for x in range(0, 34) if is_honor(x) and tiles_34[x] >= 2]) + honor_doras_pairs_or_pons = len([x for x in range(0, 34) if is_honor(x) and tiles_34[x] >= 2 + and plus_dora(x * 4, self.player.table.dora_indicators)]) + unvalued_singles = len([x for x in range(0, 34) if is_honor(x) + and x not in self.player.valued_honors + and tiles_34[x] == 1]) + + # there are not many non-suitable tiles, but let's check other patterns, + # maybe it's not enough to assume honitsu + if self.tiles_count_not_suitable >= 3: + # if we don't have pair or pon of honored doras + if honor_doras_pairs_or_pons == 0: + # we need to either have a valued pair or have at least two honor + # pairs to consider honitsu + if valued_pairs == 0 and honor_pairs_or_pons < 2: + return False + + # doesn't matter valued or not, if we have just one honor pair + # and have some single unvalued tiles, let's throw them away + # first + if honor_pairs_or_pons == 1 and unvalued_singles >= 2: + return False + + # 3 non-isolated unsuitable tiles, 1-shanen and already 8th turn + # let's not consider honitsu here + if self.player.ai.shanten == 1 and self.player.round_step > 8: + return False + else: + # we have a pon of unvalued honor doras, but it looks like + # it's faster to build our hand without honitsu + if self.player.ai.shanten == 1: + return False + + # if we have a complete set in other suits, we can only throw it away if it's early in the game + # TODO: also check that it doesn't contain dora + if count_of_shuntsu_other_suits + count_of_koutsu_other_suits >= 1: + # too late to throw away chi after 8 step + if self.player.round_step > 8: + return False + + # already 1 shanten, no need to throw away complete set + if self.player.ai.shanten == 1: + return False + + self.chosen_suit = suit['function'] + + return True + def is_tile_suitable(self, tile): """ We can use only tiles of chosen suit and honor tiles @@ -59,6 +119,53 @@ def is_tile_suitable(self, tile): tile //= 4 return self.chosen_suit(tile) or is_honor(tile) + # TODO: differentiate between all not-suitable tiles and not-suitable not isolated tiles + def _calculate_not_suitable_tiles_cnt(self, tiles, suit): + suit_tiles_cnt = 0 + for x in range(0, 34): + tile = tiles[x] + if not tile: + continue + + if not suit(x) and not is_honor(x): + suit_tiles_cnt += 1 + + self.tiles_count_not_suitable = suit_tiles_cnt + + def _initialize_honitsu_dora_count(self, tiles_136, suit): + tiles_34 = TilesConverter.to_34_array(tiles_136) + + dora_count_man_not_isolated = 0 + dora_count_pin_not_isolated = 0 + dora_count_sou_not_isolated = 0 + + for tile_136 in tiles_136: + tile_34 = tile_136 // 4 + + dora_count = plus_dora(tile_136, self.player.table.dora_indicators) + + if is_aka_dora(tile_136, self.player.table.has_aka_dora): + dora_count += 1 + + if is_man(tile_34): + if not is_tile_strictly_isolated(tiles_34, tile_34): + dora_count_man_not_isolated += dora_count + + if is_pin(tile_34): + if not is_tile_strictly_isolated(tiles_34, tile_34): + dora_count_pin_not_isolated += dora_count + + if is_sou(tile_34): + if not is_tile_strictly_isolated(tiles_34, tile_34): + dora_count_sou_not_isolated += dora_count + + if suit['name'] == 'pin': + self.dora_count_not_suitable= dora_count_man_not_isolated + dora_count_sou_not_isolated + elif suit['name'] == 'sou': + self.dora_count_not_suitable= dora_count_man_not_isolated + dora_count_pin_not_isolated + elif suit['name'] == 'man': + self.dora_count_not_suitable = dora_count_sou_not_isolated + dora_count_pin_not_isolated + def _find_ryanmen_waits(self, tiles, suit): suit_tiles = [] for x in range(0, 34): @@ -85,3 +192,48 @@ def _find_ryanmen_waits(self, tiles, suit): count_of_ryanmen_waits += 1 return count_of_ryanmen_waits + + # we know we have no more that 5 tiles of other suit, + # so this is a simplified version + @staticmethod + def _count_of_shuntsu(tiles, suit): + suit_tiles = [] + for x in range(0, 34): + tile = tiles[x] + if not tile: + continue + + if suit(x): + suit_tiles.append(x) + + count_of_shuntsu = 0 + simple_tiles = [simplify(x) for x in suit_tiles] + for x in range(0, len(simple_tiles)): + tile = simple_tiles[x] + + # not enough room to make shuntsu + if x + 2 >= len(simple_tiles): + continue + + if tile + 1 >= 1 and simple_tiles[x + 1] >= 1 and simple_tiles[x + 2] >= 1: + count_of_shuntsu += 1 + + count_of_shuntsu //= 3 + + return count_of_shuntsu + + # we know we have no more that 5 tiles of other suit, + # so this is a simplified version + @staticmethod + def _count_of_koutsu( tiles, suit): + count_of_koutsu = 0 + + for x in range(0, 34): + tile = tiles[x] + if not tile: + continue + + if suit(x) and tile >= 3: + count_of_koutsu += 1 + + return count_of_koutsu diff --git a/project/game/ai/first_version/strategies/main.py b/project/game/ai/first_version/strategies/main.py index 7bf07eef..f1b70dfa 100644 --- a/project/game/ai/first_version/strategies/main.py +++ b/project/game/ai/first_version/strategies/main.py @@ -9,12 +9,14 @@ class BaseStrategy(object): HONITSU = 1 TANYAO = 2 FORMAL_TEMPAI = 3 + CHINITSU = 4 TYPES = { YAKUHAI: 'Yakuhai', HONITSU: 'Honitsu', TANYAO: 'Tanyao', - FORMAL_TEMPAI: 'Formal Tempai' + FORMAL_TEMPAI: 'Formal Tempai', + CHINITSU: 'Chinitsu' } not_suitable_tiles = [] @@ -28,6 +30,7 @@ class BaseStrategy(object): dora_count_central = 0 dora_count_not_central = 0 aka_dora_count = 0 + dora_count_honor = 0 def __init__(self, strategy_type, player): self.type = strategy_type @@ -258,18 +261,21 @@ def calculate_dora_count(self, tiles_136): for tile_136 in tiles_136: tile_34 = tile_136 // 4 - dora = plus_dora(tile_136, self.player.table.dora_indicators) + dora_count = plus_dora(tile_136, self.player.table.dora_indicators) if is_aka_dora(tile_136, self.player.table.has_aka_dora): self.aka_dora_count += 1 - if not dora: + if not dora_count: continue - if is_honor(tile_34) or is_terminal(tile_34): - self.dora_count_not_central += 1 + if is_honor(tile_34): + self.dora_count_not_central += dora_count + self.dora_count_honor += dora_count + elif is_terminal(tile_34): + self.dora_count_not_central += dora_count else: - self.dora_count_central += 1 + self.dora_count_central += dora_count self.dora_count_central += self.aka_dora_count self.dora_count_total = self.dora_count_central + self.dora_count_not_central diff --git a/project/game/ai/first_version/tests/strategies/tests_chinitsu.py b/project/game/ai/first_version/tests/strategies/tests_chinitsu.py new file mode 100644 index 00000000..1034a576 --- /dev/null +++ b/project/game/ai/first_version/tests/strategies/tests_chinitsu.py @@ -0,0 +1,95 @@ +# -*- coding: utf-8 -*- +import unittest + +from mahjong.tests_mixin import TestMixin + +from game.ai.first_version.strategies.chinitsu import ChinitsuStrategy +from game.ai.first_version.strategies.main import BaseStrategy +from game.table import Table + + +class ChinitsuStrategyTestCase(unittest.TestCase, TestMixin): + + def test_should_activate_strategy(self): + table = Table() + player = table.player + strategy = ChinitsuStrategy(BaseStrategy.CHINITSU, player) + + table.add_dora_indicator(self._string_to_136_tile(pin='1')) + 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') + 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') + 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') + 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') + 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') + 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') + 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') + 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') + 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') + 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') + 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') + player.init_hand(tiles) + self.assertEqual(strategy.should_activate_strategy(player.tiles), False) + + def test_suitable_tiles(self): + table = Table() + player = table.player + strategy = ChinitsuStrategy(BaseStrategy.CHINITSU, player) + + tiles = self._string_to_136_array(sou='111222479', man='78', honors='12') + player.init_hand(tiles) + self.assertEqual(strategy.should_activate_strategy(player.tiles), True) + + tile = self._string_to_136_tile(man='1') + self.assertEqual(strategy.is_tile_suitable(tile), False) + + tile = self._string_to_136_tile(pin='1') + self.assertEqual(strategy.is_tile_suitable(tile), False) + + tile = self._string_to_136_tile(sou='1') + self.assertEqual(strategy.is_tile_suitable(tile), True) + + tile = self._string_to_136_tile(honors='1') + self.assertEqual(strategy.is_tile_suitable(tile), False) 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 973b9548..bba8d415 100644 --- a/project/game/ai/first_version/tests/strategies/tests_honitsu.py +++ b/project/game/ai/first_version/tests/strategies/tests_honitsu.py @@ -15,22 +15,32 @@ def test_should_activate_strategy(self): player = table.player strategy = HonitsuStrategy(BaseStrategy.HONITSU, player) + table.add_dora_indicator(self._string_to_136_tile(pin='1')) + table.add_dora_indicator(self._string_to_136_tile(honors='5')) + tiles = self._string_to_136_array(sou='12355', man='12389', honors='123') player.init_hand(tiles) self.assertEqual(strategy.should_activate_strategy(player.tiles), False) - tiles = self._string_to_136_array(sou='12355', man='238', honors='11234') + # many tiles in one suit and yakuhai pair, but still many useless winds + tiles = self._string_to_136_array(sou='12355', man='238', honors='23455') + player.init_hand(tiles) + self.assertEqual(strategy.should_activate_strategy(player.tiles), False) + + # many tiles in one suit and yakuhai pair and another honor pair, so + # now this is honitsu + tiles = self._string_to_136_array(sou='12355', man='238', honors='22355') player.init_hand(tiles) self.assertEqual(strategy.should_activate_strategy(player.tiles), True) - # with hand without pairs we not should go for honitsu, - # because it is far away from tempai - tiles = self._string_to_136_array(sou='12358', man='238', honors='12345') + # same conditions, but ready suit with dora in another suit, so no honitsu + tiles = self._string_to_136_array(sou='12355', pin='234', honors='22355') player.init_hand(tiles) - self.assertEqual(strategy.should_activate_strategy(player.tiles), False) + self.assertEqual(strategy.should_activate_strategy(player.tiles), True) - # with chitoitsu-like hand we don't need to go for honitsu - tiles = self._string_to_136_array(pin='77', man='3355677899', sou='11') + # same conditions, but we have a pon of yakuhai doras, we shouldn't + # force honitsu with this hand + tiles = self._string_to_136_array(sou='12355', pin='238', honors='22666') player.init_hand(tiles) self.assertEqual(strategy.should_activate_strategy(player.tiles), False) @@ -39,7 +49,7 @@ def test_suitable_tiles(self): player = table.player strategy = HonitsuStrategy(BaseStrategy.HONITSU, player) - tiles = self._string_to_136_array(sou='12355', man='238', honors='11234') + tiles = self._string_to_136_array(sou='12355', man='238', honors='23455') player.init_hand(tiles) self.assertEqual(strategy.should_activate_strategy(player.tiles), True) @@ -130,13 +140,3 @@ def test_discard_not_effective_tiles_first(self): tile_to_discard = player.discard_tile() self.assertEqual(self._to_string([tile_to_discard]), '5s') - - def test_dont_go_for_honitsu_with_ryanmen_in_other_suit(self): - table = Table() - player = table.player - strategy = HonitsuStrategy(BaseStrategy.HONITSU, player) - - tiles = self._string_to_136_array(man='14489', sou='45', pin='67', honors='44456') - player.init_hand(tiles) - - self.assertEqual(strategy.should_activate_strategy(player.tiles), False) 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 55b276a9..51c62ca2 100644 --- a/project/game/ai/first_version/tests/strategies/tests_yakuhai.py +++ b/project/game/ai/first_version/tests/strategies/tests_yakuhai.py @@ -218,7 +218,7 @@ def test_open_hand_with_two_valuable_pairs(self): let's open on one of this valuable pairs """ - tiles = self._string_to_136_array(man='59', sou='12', pin='12789', honors='5566') + tiles = self._string_to_136_array(man='159', sou='128', pin='789', honors='5566') self.player.init_hand(tiles) tile = self._string_to_136_tile(honors='5') From f9e92dfc8cab63bc91ef3bf78b4aa5627c29727c Mon Sep 17 00:00:00 2001 From: bogachev-pa Date: Sat, 1 Sep 2018 13:16:03 +0300 Subject: [PATCH 038/126] fixs TODOs in honitsu/chinitsu strategies --- .../ai/first_version/strategies/chinitsu.py | 11 +-- .../ai/first_version/strategies/honitsu.py | 68 +++++++++++-------- .../ai/first_version/strategies/tanyao.py | 3 +- .../tests/strategies/tests_chinitsu.py | 15 ++++ .../tests/strategies/tests_honitsu.py | 25 ++++++- 5 files changed, 86 insertions(+), 36 deletions(-) diff --git a/project/game/ai/first_version/strategies/chinitsu.py b/project/game/ai/first_version/strategies/chinitsu.py index ca65ec1c..6130f838 100644 --- a/project/game/ai/first_version/strategies/chinitsu.py +++ b/project/game/ai/first_version/strategies/chinitsu.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- from mahjong.tile import TilesConverter from mahjong.utils import count_tiles_by_suits, is_tile_strictly_isolated -from mahjong.utils import is_man, is_pin, is_sou, is_pon, is_chi, plus_dora, is_aka_dora, is_honor, is_pair, simplify +from mahjong.utils import is_man, is_pin, is_sou, plus_dora, is_aka_dora, is_honor from game.ai.first_version.strategies.main import BaseStrategy from game.ai.first_version.strategies.honitsu import HonitsuStrategy @@ -104,7 +104,6 @@ def should_activate_strategy(self, tiles_136): return False # if we have a complete set in other suits, we can only throw it away if it's early in the game - # TODO: also check that it doesn't contain dora if count_of_shuntsu_other_suits + count_of_koutsu_other_suits >= 1: # too late to throw away chi after 8 step if self.player.round_step > 8: @@ -114,6 +113,10 @@ def should_activate_strategy(self, tiles_136): if self.player.round_step > 5 and self.player.ai.shanten == 1: return False + # dora is not isolated and we have a complete set, let's not go for chinitsu + if self.dora_count_not_suitable >= 1: + return False + self.chosen_suit = suit['function'] return True @@ -162,10 +165,10 @@ def _initialize_chinitsu_dora_count(self, tiles_136, suit): if suit['name'] == 'pin': self.dora_count_suitable = dora_count_pin - self.dora_count_not_suitable= dora_count_man_not_isolated + dora_count_sou_not_isolated + self.dora_count_not_suitable = dora_count_man_not_isolated + dora_count_sou_not_isolated elif suit['name'] == 'sou': self.dora_count_suitable = dora_count_sou - self.dora_count_not_suitable= dora_count_man_not_isolated + dora_count_pin_not_isolated + self.dora_count_not_suitable = dora_count_man_not_isolated + dora_count_pin_not_isolated elif suit['name'] == 'man': self.dora_count_suitable = dora_count_man self.dora_count_not_suitable = dora_count_sou_not_isolated + dora_count_pin_not_isolated diff --git a/project/game/ai/first_version/strategies/honitsu.py b/project/game/ai/first_version/strategies/honitsu.py index 90937c5c..d3775752 100644 --- a/project/game/ai/first_version/strategies/honitsu.py +++ b/project/game/ai/first_version/strategies/honitsu.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- from mahjong.tile import TilesConverter from mahjong.utils import count_tiles_by_suits, simplify, is_tile_strictly_isolated -from mahjong.utils import is_man, is_pin, is_sou, is_pon, is_chi, plus_dora, is_aka_dora, is_honor, is_pair +from mahjong.utils import is_man, is_pin, is_sou, plus_dora, is_aka_dora, is_honor from game.ai.first_version.strategies.main import BaseStrategy @@ -11,8 +11,9 @@ class HonitsuStrategy(BaseStrategy): chosen_suit = None - dora_count_not_suitable = 0 - tiles_count_not_suitable = 0 + dora_count_other_suits_not_isolated = 0 + tiles_count_other_suits = 0 + tiles_count_other_suits_not_isolated = 0 def should_activate_strategy(self, tiles_136): """ @@ -45,12 +46,12 @@ def should_activate_strategy(self, tiles_136): # let's not go for honitsu if we have 5 or more non-isolated # tiles in other suits - if self.tiles_count_not_suitable >= 5: + if self.tiles_count_other_suits >= 5: return False # let's not go for honitsu if we have 2 or more non-isolated doras # in other suits - if self.dora_count_not_suitable >= 2: + if self.dora_count_other_suits_not_isolated >= 2: return False # if we have a pon of valued doras, let's not go for honitsu @@ -69,9 +70,9 @@ def should_activate_strategy(self, tiles_136): and x not in self.player.valued_honors and tiles_34[x] == 1]) - # there are not many non-suitable tiles, but let's check other patterns, - # maybe it's not enough to assume honitsu - if self.tiles_count_not_suitable >= 3: + # if we have some decent amount of not isolated tiles in other suits + # we may not rush for honitsu considering other conditions + if self.tiles_count_other_suits_not_isolated >= 3: # if we don't have pair or pon of honored doras if honor_doras_pairs_or_pons == 0: # we need to either have a valued pair or have at least two honor @@ -96,7 +97,6 @@ def should_activate_strategy(self, tiles_136): return False # if we have a complete set in other suits, we can only throw it away if it's early in the game - # TODO: also check that it doesn't contain dora if count_of_shuntsu_other_suits + count_of_koutsu_other_suits >= 1: # too late to throw away chi after 8 step if self.player.round_step > 8: @@ -106,6 +106,10 @@ def should_activate_strategy(self, tiles_136): if self.player.ai.shanten == 1: return False + # dora is not isolated and we have a complete set, let's not go for honitsu + if self.dora_count_other_suits_not_isolated >= 1: + return False + self.chosen_suit = suit['function'] return True @@ -119,18 +123,19 @@ def is_tile_suitable(self, tile): tile //= 4 return self.chosen_suit(tile) or is_honor(tile) - # TODO: differentiate between all not-suitable tiles and not-suitable not isolated tiles - def _calculate_not_suitable_tiles_cnt(self, tiles, suit): - suit_tiles_cnt = 0 + def _calculate_not_suitable_tiles_cnt(self, tiles_34, suit): + self.tiles_count_other_suits = 0 + self.tiles_count_other_suits_not_isolated = 0 + for x in range(0, 34): - tile = tiles[x] + tile = tiles_34[x] if not tile: continue if not suit(x) and not is_honor(x): - suit_tiles_cnt += 1 - - self.tiles_count_not_suitable = suit_tiles_cnt + self.tiles_count_other_suits += tile + if not is_tile_strictly_isolated(tiles_34, x): + self.tiles_count_other_suits_not_isolated += tile def _initialize_honitsu_dora_count(self, tiles_136, suit): tiles_34 = TilesConverter.to_34_array(tiles_136) @@ -160,13 +165,14 @@ def _initialize_honitsu_dora_count(self, tiles_136, suit): dora_count_sou_not_isolated += dora_count if suit['name'] == 'pin': - self.dora_count_not_suitable= dora_count_man_not_isolated + dora_count_sou_not_isolated + self.dora_count_other_suits_not_isolated = dora_count_man_not_isolated + dora_count_sou_not_isolated elif suit['name'] == 'sou': - self.dora_count_not_suitable= dora_count_man_not_isolated + dora_count_pin_not_isolated + self.dora_count_other_suits_not_isolated = dora_count_man_not_isolated + dora_count_pin_not_isolated elif suit['name'] == 'man': - self.dora_count_not_suitable = dora_count_sou_not_isolated + dora_count_pin_not_isolated + self.dora_count_other_suits_not_isolated = dora_count_sou_not_isolated + dora_count_pin_not_isolated - def _find_ryanmen_waits(self, tiles, suit): + @staticmethod + def _find_ryanmen_waits(tiles, suit): suit_tiles = [] for x in range(0, 34): tile = tiles[x] @@ -195,6 +201,7 @@ def _find_ryanmen_waits(self, tiles, suit): # we know we have no more that 5 tiles of other suit, # so this is a simplified version + # be aware, that it will return 2 for 2345 form so use with care @staticmethod def _count_of_shuntsu(tiles, suit): suit_tiles = [] @@ -206,26 +213,29 @@ def _count_of_shuntsu(tiles, suit): if suit(x): suit_tiles.append(x) - count_of_shuntsu = 0 + count_of_left_tiles = 0 + count_of_middle_tiles = 0 + count_of_right_tiles = 0 + simple_tiles = [simplify(x) for x in suit_tiles] for x in range(0, len(simple_tiles)): tile = simple_tiles[x] - # not enough room to make shuntsu - if x + 2 >= len(simple_tiles): - continue + if tile + 1 in simple_tiles and tile + 2 in simple_tiles: + count_of_left_tiles += 1 - if tile + 1 >= 1 and simple_tiles[x + 1] >= 1 and simple_tiles[x + 2] >= 1: - count_of_shuntsu += 1 + if tile - 1 in simple_tiles and tile + 1 in simple_tiles: + count_of_middle_tiles += 1 - count_of_shuntsu //= 3 + if tile - 2 in simple_tiles and tile - 1 in simple_tiles: + count_of_right_tiles += 1 - return count_of_shuntsu + return (count_of_left_tiles + count_of_middle_tiles + count_of_right_tiles) // 3 # we know we have no more that 5 tiles of other suit, # so this is a simplified version @staticmethod - def _count_of_koutsu( tiles, suit): + def _count_of_koutsu(tiles, suit): count_of_koutsu = 0 for x in range(0, 34): diff --git a/project/game/ai/first_version/strategies/tanyao.py b/project/game/ai/first_version/strategies/tanyao.py index bcc74f36..b8ea5807 100644 --- a/project/game/ai/first_version/strategies/tanyao.py +++ b/project/game/ai/first_version/strategies/tanyao.py @@ -25,7 +25,8 @@ def should_activate_strategy(self, tiles_136): tiles = TilesConverter.to_34_array(tiles_136) closed_hand_34 = TilesConverter.to_34_array(self.player.closed_hand) - isolated_tiles = [x // 4 for x in tiles_136 if is_tile_strictly_isolated(closed_hand_34, x // 4) or is_honor(x // 4)] + isolated_tiles = [x // 4 for x in tiles_136 + if is_tile_strictly_isolated(closed_hand_34, x // 4) or is_honor(x // 4)] count_of_terminal_pon_sets = 0 count_of_terminal_pairs = 0 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 1034a576..e3aaf900 100644 --- a/project/game/ai/first_version/tests/strategies/tests_chinitsu.py +++ b/project/game/ai/first_version/tests/strategies/tests_chinitsu.py @@ -52,6 +52,21 @@ def test_should_activate_strategy(self): 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') + 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') + 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') + 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') player.init_hand(tiles) 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 bba8d415..eebf0de0 100644 --- a/project/game/ai/first_version/tests/strategies/tests_honitsu.py +++ b/project/game/ai/first_version/tests/strategies/tests_honitsu.py @@ -23,7 +23,7 @@ def test_should_activate_strategy(self): self.assertEqual(strategy.should_activate_strategy(player.tiles), False) # many tiles in one suit and yakuhai pair, but still many useless winds - tiles = self._string_to_136_array(sou='12355', man='238', honors='23455') + tiles = self._string_to_136_array(sou='12355', man='23', pin='68', honors='2355') player.init_hand(tiles) self.assertEqual(strategy.should_activate_strategy(player.tiles), False) @@ -36,7 +36,7 @@ def test_should_activate_strategy(self): # same conditions, but ready suit with dora in another suit, so no honitsu tiles = self._string_to_136_array(sou='12355', pin='234', honors='22355') player.init_hand(tiles) - self.assertEqual(strategy.should_activate_strategy(player.tiles), True) + self.assertEqual(strategy.should_activate_strategy(player.tiles), False) # same conditions, but we have a pon of yakuhai doras, we shouldn't # force honitsu with this hand @@ -44,6 +44,27 @@ def test_should_activate_strategy(self): player.init_hand(tiles) self.assertEqual(strategy.should_activate_strategy(player.tiles), False) + # if we have a complete set with dora, we shouldn't go for honitsu + tiles = self._string_to_136_array(sou='11123688', pin='123', honors='55') + 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='1223688', pin='2334', honors='55') + 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='1223688', pin='2345', honors='55') + 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 honitsu + tiles = self._string_to_136_array(sou='1223688', pin='2333', honors='55') + player.init_hand(tiles) + self.assertEqual(strategy.should_activate_strategy(player.tiles), False) + + def test_suitable_tiles(self): table = Table() player = table.player From 7e1b0ceb224a5532cf53968469b18934f5a6e536 Mon Sep 17 00:00:00 2001 From: bogachev-pa Date: Sat, 1 Sep 2018 19:03:26 +0300 Subject: [PATCH 039/126] fix agari without yaku handling --- project/game/ai/first_version/main.py | 8 +++++++- .../game/ai/first_version/tests/tests_ai.py | 18 ++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/project/game/ai/first_version/main.py b/project/game/ai/first_version/main.py index 54cb109f..7bc5b9bd 100644 --- a/project/game/ai/first_version/main.py +++ b/project/game/ai/first_version/main.py @@ -147,7 +147,13 @@ def calculate_outs(self, tiles, closed_hand, open_sets_34=None): waiting = [] for j in range(0, 34): - if hand_tile == j or tiles_34[j] == 4: + if tiles_34[j] == 4: + continue + + # agari is a special case, we are forced to make number + # of shanten larger, so we don't skip any tiles + # in the end we let the strategy decide what to do if agari without yaku happened + if not is_agari and hand_tile == j: continue tiles_34[j] += 1 diff --git a/project/game/ai/first_version/tests/tests_ai.py b/project/game/ai/first_version/tests/tests_ai.py index a7155c89..9e340319 100644 --- a/project/game/ai/first_version/tests/tests_ai.py +++ b/project/game/ai/first_version/tests/tests_ai.py @@ -361,3 +361,21 @@ def test_closed_kan_and_not_necessary_call(self): tile = self._string_to_136_tile(sou='9') self.assertEqual(player.should_call_kan(tile, False), None) + + def test_agari_without_yaku(self): + """ + Bot tried to call closed kan with 568m669p1478999s + 9s hand + """ + table = Table() + player = table.player + + tiles = self._string_to_136_array(man='111234677889', sou='1', pin='') + player.init_hand(tiles) + tile = self._string_to_136_tile(sou='1') + + meld = self._make_meld(Meld.CHI, man='789') + player.add_called_meld(meld) + + player.draw_tile(tile) + discard = player.discard_tile() + self.assertEqual(self._to_string([discard]), '1s') From d88b5dcff0f709cf4c0043bf1823f71b49df98f3 Mon Sep 17 00:00:00 2001 From: bogachev-pa Date: Sat, 1 Sep 2018 20:02:50 +0300 Subject: [PATCH 040/126] yakuhai: fix yakuhai keeping conditions --- .../game/ai/first_version/strategies/yakuhai.py | 17 +++++++++-------- .../tests/strategies/tests_yakuhai.py | 10 ++++++++++ 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/project/game/ai/first_version/strategies/yakuhai.py b/project/game/ai/first_version/strategies/yakuhai.py index 999c0109..e2169577 100644 --- a/project/game/ai/first_version/strategies/yakuhai.py +++ b/project/game/ai/first_version/strategies/yakuhai.py @@ -97,6 +97,7 @@ def determine_what_to_discard(self, closed_hand, outs_results, shanten, for_open tiles_34 = TilesConverter.to_34_array(self.player.tiles) valued_pairs = [x for x in self.player.valued_honors if tiles_34[x] == 2] + valued_pons = [x for x in self.player.valued_honors if tiles_34[x] == 3] # when we trying to open hand with tempai state, we need to chose a valued pair waiting if shanten == 0 and valued_pairs and for_open_hand and tile_for_open_hand not in valued_pairs: @@ -108,14 +109,14 @@ def determine_what_to_discard(self, closed_hand, outs_results, shanten, for_open results.append(item) return results - if self.player.is_open_hand: - has_yakuhai_pon = any([self._is_yakuhai_pon(meld) for meld in self.player.melds]) - # we opened our hand for atodzuke - if not has_yakuhai_pon: - for item in outs_results: - for valued_pair in valued_pairs: - if valued_pair == item.tile_to_discard: - item.had_to_be_saved = True + for item in outs_results: + for valued_pair in valued_pairs: + if valued_pair == item.tile_to_discard: + item.had_to_be_saved = True + + for valued_pon in valued_pons: + if valued_pon == item.tile_to_discard: + item.had_to_be_saved = True return super(YakuhaiStrategy, self).determine_what_to_discard(closed_hand, outs_results, 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 51c62ca2..7ee2699a 100644 --- a/project/game/ai/first_version/tests/strategies/tests_yakuhai.py +++ b/project/game/ai/first_version/tests/strategies/tests_yakuhai.py @@ -324,3 +324,13 @@ def test_open_double_south_wind(self): tile = self._string_to_136_tile(honors='2') meld, _ = self.player.try_to_call_meld(tile, True) self.assertNotEqual(meld, None) + + def test_keep_yakuhai_in_closed_hand(self): + tiles = self._string_to_136_array(man='14', sou='15', pin='113347', honors='777') + self.player.init_hand(tiles) + + tile = self._string_to_136_tile(honors='3') + self.player.draw_tile(tile) + + discard = self.player.discard_tile() + self.assertNotEqual(self._to_string([discard]), '7z') From 918a9a08cfd054d02734d25c29ec067f7e3722b6 Mon Sep 17 00:00:00 2001 From: bogachev-pa Date: Sat, 1 Sep 2018 23:14:55 +0300 Subject: [PATCH 041/126] implement basic chiitoitsu strategy (#57) --- project/game/ai/first_version/main.py | 2 + .../ai/first_version/strategies/chiitoitsu.py | 44 +++++++++++++++++++ .../game/ai/first_version/strategies/main.py | 10 ++--- .../tests/strategies/tests_chiitoitsu.py | 35 +++++++++++++++ .../tests/strategies/tests_tanyao.py | 2 +- 5 files changed, 86 insertions(+), 7 deletions(-) create mode 100644 project/game/ai/first_version/strategies/chiitoitsu.py create mode 100644 project/game/ai/first_version/tests/strategies/tests_chiitoitsu.py diff --git a/project/game/ai/first_version/main.py b/project/game/ai/first_version/main.py index 7bc5b9bd..b0aff6d0 100644 --- a/project/game/ai/first_version/main.py +++ b/project/game/ai/first_version/main.py @@ -21,6 +21,7 @@ from game.ai.first_version.strategies.yakuhai import YakuhaiStrategy from game.ai.first_version.strategies.formal_tempai import FormalTempaiStrategy from game.ai.first_version.strategies.chinitsu import ChinitsuStrategy +from game.ai.first_version.strategies.chiitoitsu import ChiitoitsuStrategy logger = logging.getLogger('ai') @@ -233,6 +234,7 @@ def determine_strategy(self, tiles_136): if self.player.table.has_open_tanyao: strategies.append(TanyaoStrategy(BaseStrategy.TANYAO, self.player)) + strategies.append(ChiitoitsuStrategy(BaseStrategy.CHIITOITSU, self.player)) strategies.append(FormalTempaiStrategy(BaseStrategy.FORMAL_TEMPAI, self.player)) for strategy in strategies: diff --git a/project/game/ai/first_version/strategies/chiitoitsu.py b/project/game/ai/first_version/strategies/chiitoitsu.py new file mode 100644 index 00000000..d8796d0e --- /dev/null +++ b/project/game/ai/first_version/strategies/chiitoitsu.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +from mahjong.tile import TilesConverter + +from game.ai.first_version.strategies.main import BaseStrategy + + +class ChiitoitsuStrategy(BaseStrategy): + min_shanten = 2 + + def should_activate_strategy(self, tiles_136): + """ + We can go for chiitoitsu strategy if we have 5 pairs + """ + + result = super(ChiitoitsuStrategy, self).should_activate_strategy(tiles_136) + if not result: + return False + + tiles_34 = TilesConverter.to_34_array(self.player.tiles) + + num_pairs = len([x for x in range(0, 34) if tiles_34[x] == 2]) + num_pons = len([x for x in range(0, 34) if tiles_34[x] == 3]) + + # for now we don't consider chiitoitsu with less than 5 pair + if num_pairs < 5: + return False + + # if we have 5 pairs and tempai, this is obviously not chiitoitsu + if num_pairs == 5 and self.player.ai.shanten == 0: + return False + + # for now we won't go for chiitoitsu if we have 5 pairs and pon + if num_pairs == 5 and num_pons > 0: + return False + + return True + + def is_tile_suitable(self, tile): + """ + For yakuhai we don't have any limits + :param tile: 136 tiles format + :return: True + """ + return True diff --git a/project/game/ai/first_version/strategies/main.py b/project/game/ai/first_version/strategies/main.py index f1b70dfa..36ffd63f 100644 --- a/project/game/ai/first_version/strategies/main.py +++ b/project/game/ai/first_version/strategies/main.py @@ -10,13 +10,15 @@ class BaseStrategy(object): TANYAO = 2 FORMAL_TEMPAI = 3 CHINITSU = 4 + CHIITOITSU = 5 TYPES = { YAKUHAI: 'Yakuhai', HONITSU: 'Honitsu', TANYAO: 'Tanyao', FORMAL_TEMPAI: 'Formal Tempai', - CHINITSU: 'Chinitsu' + CHINITSU: 'Chinitsu', + CHIITOITSU: 'Chiitoitsu' } not_suitable_tiles = [] @@ -44,7 +46,6 @@ def should_activate_strategy(self, tiles_136): """ Based on player hand and table situation we can determine should we use this strategy or not. - For now default rule for all strategies: don't open hand with 5+ pairs :param: tiles_136 :return: boolean """ @@ -53,10 +54,7 @@ def should_activate_strategy(self, tiles_136): if self.player.is_open_hand: return True - tiles_34 = TilesConverter.to_34_array(tiles_136) - count_of_pairs = len([x for x in range(0, 34) if tiles_34[x] >= 2]) - - return count_of_pairs < 5 + return True def is_tile_suitable(self, tile): """ diff --git a/project/game/ai/first_version/tests/strategies/tests_chiitoitsu.py b/project/game/ai/first_version/tests/strategies/tests_chiitoitsu.py new file mode 100644 index 00000000..ad312925 --- /dev/null +++ b/project/game/ai/first_version/tests/strategies/tests_chiitoitsu.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +import unittest + +from mahjong.tests_mixin import TestMixin + +from game.ai.first_version.strategies.chiitoitsu import ChiitoitsuStrategy +from game.ai.first_version.strategies.main import BaseStrategy +from game.table import Table + + +class ChiitoitsuStrategyTestCase(unittest.TestCase, TestMixin): + + def test_should_activate_strategy(self): + table = Table() + player = table.player + strategy = ChiitoitsuStrategy(BaseStrategy.CHIITOITSU, player) + + # obvious chiitoitsu, let's activate + tiles = self._string_to_136_array(sou='2266', man='3399', pin='289', honors='116') + player.init_hand(tiles) + 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') + 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') + player.init_hand(tiles) + self.assertEqual(strategy.should_activate_strategy(player.tiles), False) + + tiles = self._string_to_136_array(sou='234', man='22334455669') + player.init_hand(tiles) + self.assertEqual(strategy.should_activate_strategy(player.tiles), False) 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 430d37ee..68c08f8d 100644 --- a/project/game/ai/first_version/tests/strategies/tests_tanyao.py +++ b/project/game/ai/first_version/tests/strategies/tests_tanyao.py @@ -58,7 +58,7 @@ def test_should_activate_strategy_and_chitoitsu_like_hand(self): tiles = self._string_to_136_array(sou='223388', man='2244', pin='6687') self.player.init_hand(tiles) - self.assertEqual(strategy.should_activate_strategy(self.player.tiles), False) + self.assertEqual(strategy.should_activate_strategy(self.player.tiles), True) def test_should_activate_strategy_and_already_completed_sided_set(self): strategy = TanyaoStrategy(BaseStrategy.TANYAO, self.player) From 59ced854ad51277d16aab55a79de76dcedfed8b5 Mon Sep 17 00:00:00 2001 From: Nihisil Date: Sun, 2 Sep 2018 08:10:24 +0800 Subject: [PATCH 042/126] Fix typo --- project/game/ai/first_version/strategies/chiitoitsu.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/game/ai/first_version/strategies/chiitoitsu.py b/project/game/ai/first_version/strategies/chiitoitsu.py index d8796d0e..56ec2b67 100644 --- a/project/game/ai/first_version/strategies/chiitoitsu.py +++ b/project/game/ai/first_version/strategies/chiitoitsu.py @@ -37,7 +37,7 @@ def should_activate_strategy(self, tiles_136): def is_tile_suitable(self, tile): """ - For yakuhai we don't have any limits + For chiitoitsu we don't have any limits :param tile: 136 tiles format :return: True """ From eb014726e2206d834e80ce952bf90c4e1fad5ca1 Mon Sep 17 00:00:00 2001 From: Nihisil Date: Sun, 2 Sep 2018 08:12:22 +0800 Subject: [PATCH 043/126] Add missed LOG_PREFIX to settings --- project/settings.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/project/settings.py b/project/settings.py index 7046404d..8599c19e 100644 --- a/project/settings.py +++ b/project/settings.py @@ -19,6 +19,8 @@ # class will be loaded automatically AI_CLASS = None +LOG_PREFIX = '' + """ Game type decoding: From 5b64905e4577759dc1fa48bcf177f3e647ef9023 Mon Sep 17 00:00:00 2001 From: bogachev-pa Date: Sun, 2 Sep 2018 14:32:30 +0300 Subject: [PATCH 044/126] forbid meld for chiitoitsu --- .../game/ai/first_version/strategies/chiitoitsu.py | 10 ++++++++++ .../tests/strategies/tests_chiitoitsu.py | 13 +++++++++++++ 2 files changed, 23 insertions(+) diff --git a/project/game/ai/first_version/strategies/chiitoitsu.py b/project/game/ai/first_version/strategies/chiitoitsu.py index 56ec2b67..fb6c1d14 100644 --- a/project/game/ai/first_version/strategies/chiitoitsu.py +++ b/project/game/ai/first_version/strategies/chiitoitsu.py @@ -42,3 +42,13 @@ def is_tile_suitable(self, tile): :return: True """ return True + + def try_to_call_meld(self, tile, is_kamicha_discard, new_tiles): + """ + Never meld with chiitoitsu + :param tile: 136 format tile + :param is_kamicha_discard: boolean + :param new_tiles: + :return: Meld and DiscardOption objects + """ + return None, None 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 ad312925..75605f31 100644 --- a/project/game/ai/first_version/tests/strategies/tests_chiitoitsu.py +++ b/project/game/ai/first_version/tests/strategies/tests_chiitoitsu.py @@ -33,3 +33,16 @@ def test_should_activate_strategy(self): tiles = self._string_to_136_array(sou='234', man='22334455669') player.init_hand(tiles) self.assertEqual(strategy.should_activate_strategy(player.tiles), False) + + def test_dont_call_meld(self): + table = Table() + player = table.player + strategy = ChiitoitsuStrategy(BaseStrategy.CHIITOITSU, player) + + tiles = self._string_to_136_array(sou='112234', man='2334499') + player.init_hand(tiles) + self.assertEqual(strategy.should_activate_strategy(player.tiles), True) + + tile = self._string_to_136_tile(man='9') + meld, _ = player.try_to_call_meld(tile, True) + self.assertEqual(meld, None) From 1c46b1b386f1cc62e632389121febaa6b2a2a3a3 Mon Sep 17 00:00:00 2001 From: bogachev-pa Date: Sun, 2 Sep 2018 23:17:22 +0300 Subject: [PATCH 045/126] fix bug with skipping first yakuhai (#79) --- .../ai/first_version/strategies/yakuhai.py | 57 +++++++++++-------- .../tests/strategies/tests_yakuhai.py | 37 ++++++++---- 2 files changed, 59 insertions(+), 35 deletions(-) diff --git a/project/game/ai/first_version/strategies/yakuhai.py b/project/game/ai/first_version/strategies/yakuhai.py index e2169577..74e748e6 100644 --- a/project/game/ai/first_version/strategies/yakuhai.py +++ b/project/game/ai/first_version/strategies/yakuhai.py @@ -15,6 +15,7 @@ def __init__(self, strategy_type, player): self.valued_pairs = [] self.has_valued_pon = False + self.last_chance_calls = [] def should_activate_strategy(self, tiles_136): """ @@ -27,25 +28,31 @@ def should_activate_strategy(self, tiles_136): tiles_34 = TilesConverter.to_34_array(tiles_136) player_hand_tiles_34 = TilesConverter.to_34_array(self.player.tiles) - self.valued_pairs = [x for x in self.player.valued_honors if tiles_34[x] >= 2] + self.valued_pairs = [x for x in self.player.valued_honors if player_hand_tiles_34[x] == 2] is_double_east_wind = len([x for x in self.valued_pairs if x == EAST]) == 2 is_double_south_wind = len([x for x in self.valued_pairs if x == SOUTH]) == 2 self.valued_pairs = list(set(self.valued_pairs)) - self.has_valued_pon = len([x for x in self.player.valued_honors if tiles_34[x] == 3]) >= 1 + self.has_valued_pon = len([x for x in self.player.valued_honors if player_hand_tiles_34[x] >= 3]) >= 1 + + opportunity_to_meld_yakuhai = False + + for x in range(0, 34): + if x in self.valued_pairs and tiles_34[x] - player_hand_tiles_34[x] == 1: + opportunity_to_meld_yakuhai = True has_valued_pair = False for pair in self.valued_pairs: # we have valued pair in the hand and there are enough tiles # in the wall - if self.player.total_tiles(pair, player_hand_tiles_34) < 4: + if opportunity_to_meld_yakuhai or self.player.total_tiles(pair, player_hand_tiles_34) < 4: has_valued_pair = True break - # we don't have valuable pair to open our hand - if not has_valued_pair: + # we don't have valuable pair or pon to open our hand + if not has_valued_pair and not self.has_valued_pon: return False # let's always open double east @@ -66,7 +73,7 @@ def should_activate_strategy(self, tiles_136): for x in range(0, 34): # we have other pair in the hand # so we can open hand for atodzuke - if tiles_34[x] >= 2 and x not in self.valued_pairs: + if player_hand_tiles_34[x] >= 2 and x not in self.valued_pairs: self.go_for_atodzuke = True return True @@ -75,9 +82,10 @@ def should_activate_strategy(self, tiles_136): return True for pair in self.valued_pairs: - # this valuable tile was discarded once - # let's open on it in that case - if self.player.total_tiles(pair, player_hand_tiles_34) == 3 and self.player.ai.shanten > 1: + # last chance to get that yakuhai, let's go for it + if opportunity_to_meld_yakuhai and self.player.total_tiles(pair, player_hand_tiles_34) == 4 and self.player.ai.shanten >= 1: + if pair not in self.last_chance_calls: + self.last_chance_calls.append(pair) return True return False @@ -126,26 +134,29 @@ def determine_what_to_discard(self, closed_hand, outs_results, shanten, for_open hand_was_open) def meld_had_to_be_called(self, tile): - # for closed hand we don't need to open hand with special conditions - if not self.player.is_open_hand: - return False - tile //= 4 tiles_34 = TilesConverter.to_34_array(self.player.tiles) valued_pairs = [x for x in self.player.valued_honors if tiles_34[x] == 2] - for meld in self.player.melds: - # for big shanten number we don't need to check already opened pon set, - # because it will improve pur hand anyway - if self.player.ai.shanten >= 1: - break + # for big shanten number we don't need to check already opened pon set, + # because it will improve our hand anyway + if self.player.ai.shanten < 2: + for meld in self.player.melds: + # we have already opened yakuhai pon + # so we don't need to open hand without shanten improvement + if self._is_yakuhai_pon(meld): + return False + + # if we don't have any yakuhai pon and this is our last chance, we must call this tile + if tile in self.last_chance_calls: + return True - # we have already opened yakuhai pon - # so we don't need to open hand without shanten improvement - if self._is_yakuhai_pon(meld): - return False + # in all other cases for closed hand we don't need to open hand with special conditions + if not self.player.is_open_hand: + return False - # open hand for valued pon + # we have opened the hand already and don't yet have yakuhai pon + # so we now must get it for valued_pair in valued_pairs: if valued_pair == tile: return 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 7ee2699a..1b438e98 100644 --- a/project/game/ai/first_version/tests/strategies/tests_yakuhai.py +++ b/project/game/ai/first_version/tests/strategies/tests_yakuhai.py @@ -248,33 +248,46 @@ def test_open_hand_and_once_discarded_tile(self): let's open on this valuable pair """ - tiles = self._string_to_136_array(man='29', sou='1189', pin='12789', honors='11') + strategy = YakuhaiStrategy(BaseStrategy.YAKUHAI, self.player) + + tiles = self._string_to_136_array(sou='678', pin='14689', man='456', honors='77') self.player.init_hand(tiles) - self.player.draw_tile(self._string_to_136_tile(man='6')) - tile = self._string_to_136_tile(honors='1') + # we don't activate strategy yet + self.assertEqual(strategy.should_activate_strategy(self.player.tiles), False) + + # let's skip first yakuhai early in the game + tile = self._string_to_136_tile(honors='7') + # when we are melding tile it's already counted as discarded + self.table.add_discarded_tile(1, tile, False) meld, _ = self.player.try_to_call_meld(tile, True) self.assertEqual(meld, None) - self.table.add_discarded_tile(1, self._string_to_136_tile(honors='1'), False) - tiles = self._string_to_136_array(man='29', sou='1189', pin='12789', honors='11') - self.player.init_hand(tiles) - - tile = self._string_to_136_tile(honors='1') + # now the second one is out + self.table.add_discarded_tile(1, tile, False) meld, _ = self.player.try_to_call_meld(tile, True) self.assertNotEqual(meld, None) + self.assertEqual(self._to_string(meld.tiles), '777z') # but we don't need to open hand for atodzuke here - tile = self._string_to_136_tile(pin='3') + tile = self._string_to_136_tile(pin='7') meld, _ = self.player.try_to_call_meld(tile, True) self.assertEqual(meld, None) def test_open_hand_when_yakuhai_already_in_the_hand(self): - tiles = self._string_to_136_array(man='46', pin='4679', sou='1348', honors='555') - self.player.init_hand(tiles) + # make sure yakuhai strategy is activated by adding 3 doras to the hand + table = Table() + player = table.player + table.add_dora_indicator(self._string_to_136_tile(honors='5')) + + tiles = self._string_to_136_array(man='46', pin='4679', sou='1348', honors='666') + player.init_hand(tiles) + + strategy = YakuhaiStrategy(BaseStrategy.YAKUHAI, player) + self.assertEqual(strategy.should_activate_strategy(player.tiles), True) tile = self._string_to_136_tile(sou='2') - meld, _ = self.player.try_to_call_meld(tile, True) + meld, _ = player.try_to_call_meld(tile, True) self.assertNotEqual(meld, None) def test_always_open_double_east_wind(self): From b9447b9a89ea4e86d82009e314ab665f4f259e84 Mon Sep 17 00:00:00 2001 From: bogachev-pa Date: Mon, 3 Sep 2018 01:44:31 +0300 Subject: [PATCH 046/126] don't skip honor pons with honitsu if we still have non-suitable tiles in hand --- .../ai/first_version/strategies/honitsu.py | 15 +++++++++++++ .../tests/strategies/tests_honitsu.py | 21 +++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/project/game/ai/first_version/strategies/honitsu.py b/project/game/ai/first_version/strategies/honitsu.py index d3775752..dd95fdd5 100644 --- a/project/game/ai/first_version/strategies/honitsu.py +++ b/project/game/ai/first_version/strategies/honitsu.py @@ -123,6 +123,21 @@ def is_tile_suitable(self, tile): tile //= 4 return self.chosen_suit(tile) or is_honor(tile) + def meld_had_to_be_called(self, tile): + has_not_suitable_tiles = False + + for hand_tile in self.player.tiles: + if not self.is_tile_suitable(hand_tile): + has_not_suitable_tiles = True + break + + # if we still have unsuitable tiles, let's call honor pons + # even if they don't change number of shanten + if has_not_suitable_tiles and is_honor(tile // 4): + return True + + return False + def _calculate_not_suitable_tiles_cnt(self, tiles_34, suit): self.tiles_count_other_suits = 0 self.tiles_count_other_suits_not_isolated = 0 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 eebf0de0..0aa28041 100644 --- a/project/game/ai/first_version/tests/strategies/tests_honitsu.py +++ b/project/game/ai/first_version/tests/strategies/tests_honitsu.py @@ -2,6 +2,7 @@ import unittest from mahjong.tests_mixin import TestMixin +from mahjong.meld import Meld from game.ai.first_version.strategies.honitsu import HonitsuStrategy from game.ai.first_version.strategies.main import BaseStrategy @@ -161,3 +162,23 @@ def test_discard_not_effective_tiles_first(self): tile_to_discard = player.discard_tile() self.assertEqual(self._to_string([tile_to_discard]), '5s') + + def test_open_yakuhai_same_shanten(self): + table = Table() + player = table.player + player.scores = 25000 + table.count_of_remaining_tiles = 100 + + tiles = self._string_to_136_array(man='34556778', pin='3', sou='78', honors='77') + player.init_hand(tiles) + + meld = self._make_meld(Meld.CHI, man='345') + player.add_called_meld(meld) + + strategy = HonitsuStrategy(BaseStrategy.HONITSU, player) + self.assertEqual(strategy.should_activate_strategy(player.tiles), True) + + tile = self._string_to_136_tile(honors='7') + meld, _ = player.try_to_call_meld(tile, True) + self.assertNotEqual(meld, None) + self.assertEqual(self._to_string(meld.tiles), '777z') From 25ab8d86b89db6f345f345e23f91b6d8b8ea79a2 Mon Sep 17 00:00:00 2001 From: bogachev-pa Date: Mon, 3 Sep 2018 20:19:59 +0300 Subject: [PATCH 047/126] add tests for known bugs (#84, #85) --- .../tests/strategies/tests_chinitsu.py | 58 +++++++++++++++++++ .../tests/strategies/tests_formal_tempai.py | 52 ++++++++++++++--- .../tests/strategies/tests_honitsu.py | 23 +++++++- .../tests/strategies/tests_tanyao.py | 26 +++++++++ .../game/ai/first_version/tests/tests_ai.py | 18 ------ 5 files changed, 151 insertions(+), 26 deletions(-) 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 e3aaf900..15da6f33 100644 --- a/project/game/ai/first_version/tests/strategies/tests_chinitsu.py +++ b/project/game/ai/first_version/tests/strategies/tests_chinitsu.py @@ -2,6 +2,7 @@ import unittest from mahjong.tests_mixin import TestMixin +from mahjong.meld import Meld from game.ai.first_version.strategies.chinitsu import ChinitsuStrategy from game.ai.first_version.strategies.main import BaseStrategy @@ -108,3 +109,60 @@ def test_suitable_tiles(self): tile = self._string_to_136_tile(honors='1') self.assertEqual(strategy.is_tile_suitable(tile), False) + + # issue #84 + @unittest.expectedFailure + def test_open_suit_same_shanten(self): + table = Table() + player = table.player + player.scores = 25000 + table.count_of_remaining_tiles = 100 + + tiles = self._string_to_136_array(man='1134556999', pin='3', sou='78') + player.init_hand(tiles) + + meld = self._make_meld(Meld.CHI, man='345') + player.add_called_meld(meld) + + strategy = ChinitsuStrategy(BaseStrategy.CHINITSU, player) + self.assertEqual(strategy.should_activate_strategy(player.tiles), True) + + tile = self._string_to_136_tile(man='1') + meld, _ = player.try_to_call_meld(tile, True) + self.assertNotEqual(meld, None) + self.assertEqual(self._to_string(meld.tiles), '111m') + + def test_correct_discard_agari_no_yaku(self): + table = Table() + player = table.player + + tiles = self._string_to_136_array(man='111234677889', sou='1', pin='') + player.init_hand(tiles) + + meld = self._make_meld(Meld.CHI, man='789') + player.add_called_meld(meld) + + tile = self._string_to_136_tile(sou='1') + player.draw_tile(tile) + discard = player.discard_tile() + self.assertEqual(self._to_string([discard]), '1s') + + def test_open_suit_agari_no_yaku(self): + table = Table() + player = table.player + player.scores = 25000 + table.count_of_remaining_tiles = 100 + + tiles = self._string_to_136_array(man='11123455589', pin='22') + player.init_hand(tiles) + + meld = self._make_meld(Meld.CHI, man='234') + player.add_called_meld(meld) + + strategy = ChinitsuStrategy(BaseStrategy.CHINITSU, player) + self.assertEqual(strategy.should_activate_strategy(player.tiles), True) + + tile = self._string_to_136_tile(man='7') + meld, _ = player.try_to_call_meld(tile, True) + self.assertNotEqual(meld, None) + self.assertEqual(self._to_string(meld.tiles), '789m') 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 4f2ce6b9..671b1b8d 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 @@ -3,6 +3,7 @@ from mahjong.tests_mixin import TestMixin from mahjong.tile import Tile +from mahjong.meld import Meld from game.ai.first_version.strategies.formal_tempai import FormalTempaiStrategy from game.ai.first_version.strategies.main import BaseStrategy @@ -23,13 +24,8 @@ def test_should_activate_strategy(self): self.player.init_hand(tiles) self.assertEqual(strategy.should_activate_strategy(self.player.tiles), False) - self.player.draw_tile(self._string_to_136_tile(honors='1')) - # to calculate hand shanten number - self.player.discard_tile() - - # Let's move to 10th round step, one tile was already discarded, 9 more - # to go - for i in range(0, 9): + # Let's move to 10th round step + for i in range(0, 10): self.player.add_discarded_tile(Tile(0, False)) self.assertEqual(strategy.should_activate_strategy(self.player.tiles), False) @@ -38,3 +34,45 @@ def test_should_activate_strategy(self): # 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 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 i 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') + + # We shouldn't open when we are already in tempai expect for some + # special cases + @unittest.expectedFailure + 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 i 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) + + 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 0aa28041..9f8dcb35 100644 --- a/project/game/ai/first_version/tests/strategies/tests_honitsu.py +++ b/project/game/ai/first_version/tests/strategies/tests_honitsu.py @@ -65,7 +65,6 @@ def test_should_activate_strategy(self): player.init_hand(tiles) self.assertEqual(strategy.should_activate_strategy(player.tiles), False) - def test_suitable_tiles(self): table = Table() player = table.player @@ -182,3 +181,25 @@ def test_open_yakuhai_same_shanten(self): meld, _ = player.try_to_call_meld(tile, True) self.assertNotEqual(meld, None) self.assertEqual(self._to_string(meld.tiles), '777z') + + # issue #84 + @unittest.expectedFailure + def test_open_suit_same_shanten(self): + table = Table() + player = table.player + player.scores = 25000 + table.count_of_remaining_tiles = 100 + + tiles = self._string_to_136_array(man='1134556', pin='3', sou='78', honors='777') + player.init_hand(tiles) + + meld = self._make_meld(Meld.CHI, man='345') + player.add_called_meld(meld) + + strategy = HonitsuStrategy(BaseStrategy.HONITSU, player) + self.assertEqual(strategy.should_activate_strategy(player.tiles), True) + + tile = self._string_to_136_tile(man='1') + meld, _ = player.try_to_call_meld(tile, True) + self.assertNotEqual(meld, None) + self.assertEqual(self._to_string(meld.tiles), '111m') 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 68c08f8d..8721bf4b 100644 --- a/project/game/ai/first_version/tests/strategies/tests_tanyao.py +++ b/project/game/ai/first_version/tests/strategies/tests_tanyao.py @@ -299,6 +299,32 @@ def test_determine_strategy_when_we_try_to_call_meld(self): self._assert_tanyao(self.player) + def test_correct_discard_agari_no_yaku(self): + tiles = self._string_to_136_array(man='23567', sou='456', pin='22244') + self.player.init_hand(tiles) + + meld = self._make_meld(Meld.CHI, man='567') + self.player.add_called_meld(meld) + + tile = self._string_to_136_tile(man='1') + self.player.draw_tile(tile) + discard = self.player.discard_tile() + self.assertEqual(self._to_string([discard]), '1m') + + # In case we are in temporary furiten, we can't call ron, but can still + # make chi. We assume this chi to be bad, so let's not call it. + @unittest.expectedFailure + def test_dont_meld_agari(self): + tiles = self._string_to_136_array(man='23567', sou='456', pin='22244') + self.player.init_hand(tiles) + + meld = self._make_meld(Meld.CHI, man='567') + 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 _make_table(self): table = Table() table.has_open_tanyao = True diff --git a/project/game/ai/first_version/tests/tests_ai.py b/project/game/ai/first_version/tests/tests_ai.py index 9e340319..a7155c89 100644 --- a/project/game/ai/first_version/tests/tests_ai.py +++ b/project/game/ai/first_version/tests/tests_ai.py @@ -361,21 +361,3 @@ def test_closed_kan_and_not_necessary_call(self): tile = self._string_to_136_tile(sou='9') self.assertEqual(player.should_call_kan(tile, False), None) - - def test_agari_without_yaku(self): - """ - Bot tried to call closed kan with 568m669p1478999s + 9s hand - """ - table = Table() - player = table.player - - tiles = self._string_to_136_array(man='111234677889', sou='1', pin='') - player.init_hand(tiles) - tile = self._string_to_136_tile(sou='1') - - meld = self._make_meld(Meld.CHI, man='789') - player.add_called_meld(meld) - - player.draw_tile(tile) - discard = player.discard_tile() - self.assertEqual(self._to_string([discard]), '1s') From e26dc638d0ac0166b1497dbc662422591d5af9e4 Mon Sep 17 00:00:00 2001 From: bogachev-pa Date: Mon, 3 Sep 2018 02:56:26 +0300 Subject: [PATCH 048/126] add basic damaten logic (#59) --- project/game/ai/first_version/main.py | 175 ++++++++++++++++-- .../ai/first_version/tests/tests_riichi.py | 82 ++++++-- 2 files changed, 225 insertions(+), 32 deletions(-) diff --git a/project/game/ai/first_version/main.py b/project/game/ai/first_version/main.py index 2b0336aa..587c97b9 100644 --- a/project/game/ai/first_version/main.py +++ b/project/game/ai/first_version/main.py @@ -10,7 +10,7 @@ from mahjong.meld import Meld from mahjong.shanten import Shanten from mahjong.tile import TilesConverter -from mahjong.utils import is_pair, is_pon, is_tile_strictly_isolated +from mahjong.utils import is_pair, is_pon, is_tile_strictly_isolated, is_honor, simplify, is_chi from game.ai.base.main import InterfaceAI from game.ai.discard import DiscardOption @@ -384,7 +384,7 @@ def estimate_hand_value(self, win_tile, tiles=None, call_riichi=False): win_tile += 1 if not tiles: - tiles = self.player.tiles + tiles = copy.copy(self.player.tiles) tiles += [win_tile] @@ -408,35 +408,170 @@ def should_call_riichi(self): if not self.waiting: return False + # TODO: always call daburi + if self.in_defence: return False - # we have a good wait, let's riichi - if len(self.waiting) > 1: - return True + # don't call karaten riichi + count_tiles = self.count_tiles(self.waiting, TilesConverter.to_34_array(self.player.tiles)) + if count_tiles == 0: + return False - waiting = self.waiting[0] - tiles = self.player.closed_hand + [waiting * 4] - closed_melds = [x for x in self.player.melds if not x.opened] - for meld in closed_melds: - tiles.extend(meld.tiles[:3]) + # first of all let's consider 1-sided waits + if len(self.waiting) == 1: + waiting = self.waiting[0] + hand_value = self.estimate_hand_value(waiting, call_riichi=False) - tiles_34 = TilesConverter.to_34_array(tiles) + tiles = self.player.closed_hand + [waiting * 4] + closed_melds = [x for x in self.player.melds if not x.opened] + for meld in closed_melds: + tiles.extend(meld.tiles[:3]) + + tiles_34 = TilesConverter.to_34_array(tiles) + + results = self.hand_divider.divide_hand(tiles_34) + result = results[0] + + # what if we have yaku + if hand_value.yaku is not None and hand_value.cost is not None: + min_cost = hand_value.cost['main'] + + # tanki honor is a good wait, let's damaten only if hand is already expensive + if is_honor(waiting): + if self.player.is_dealer and min_cost < 12000: + return True + + if not self.player.is_dealer and min_cost < 8000: + return True + + return False + + simplified_waiting = simplify(waiting) + + for hand_set in result: + if waiting not in hand_set: + continue + + if is_pair(hand_set): + # let's not riichi tanki 4, 5, 6 + if 3 <= simplified_waiting <= 5: + return False + + # don't riichi tanki wait on 1, 2, 3, 7, 8, 9 if it's only 1 tile + if count_tiles == 1: + return False + + # don't riichi 2378 tanki if hand has good value + if simplified_waiting != 0 and simplified_waiting != 8: + if self.player.is_dealer and min_cost >= 7700: + return False + + if not self.player.is_dealer and min_cost >= 5200: + return False + + # TODO: check for kabe and suji + return True + + # 1-sided wait means kanchan or penchan + if is_chi(hand_set): + # let's not riichi kanchan on 4, 5, 6 + if 4 <= simplified_waiting <= 6: + return False + + # now checking waiting for 2, 3, 7, 8 + # if we only have 1 tile to wait for, let's damaten + if count_tiles == 1: + return False + + # if we have 2 tiles to wait for and hand cost is good without riichi, + # let's damaten + if count_tiles == 2: + if self.player.is_dealer and min_cost >= 7700: + return False - results = self.hand_divider.divide_hand(tiles_34) - result = results[0] + if not self.player.is_dealer and min_cost >= 5200: + return False + + # TODO: check for kabe and suji + return True + + # what if we don't have yaku + # our tanki wait is good, let's riichi + if is_honor(waiting): + return True + + simplified_waiting = simplify(waiting) + + for hand_set in result: + if not waiting in hand_set: + continue + + if is_pair(hand_set): + # let's not riichi tanki 4, 5, 6 + if 3 <= simplified_waiting <= 5: + return False + + # 1-sided wait means kanchan or penchan + if is_chi(hand_set): + # let's only riichi this bad wait if + # it has all 4 tiles available or it + # it's not too early + if 4 <= simplified_waiting <= 6: + return count_tiles == 4 or self.player.round_step >= 6 + + # TODO: implement rest of the logic - count_of_pairs = len([x for x in result if is_pair(x)]) - # with chitoitsu we can call a riichi with pair wait - if count_of_pairs == 7: return True - for hand_set in result: - # better to not call a riichi for a pair wait - # it can be easily improved - if is_pair(hand_set) and waiting in hand_set: + # now we are looking at two or more sided waits only + hand_costs = [] + waits_with_yaku = 0 + for waiting in self.waiting: + hand_value = self.estimate_hand_value(waiting, call_riichi=False) + if hand_value.error is None: + hand_costs.append(hand_value.cost['main']) + if hand_value.yaku is not None and hand_value.cost is not None: + waits_with_yaku += 1 + + # if we have yaku on every wait + if waits_with_yaku == len(self.waiting): + min_cost = min(hand_costs) + + # let's not riichi this bad wait + if count_tiles <= 2: + return False + + # if wait is slighly better, we will riichi only a cheap hand + if count_tiles <= 4: + if self.player.is_dealer and min_cost >= 7700: + return False + + if not self.player.is_dealer and min_cost >= 5200: + return False + + return True + + # wait is even better, but still don't call riichi on damaten mangan + if count_tiles <= 6: + if self.player.is_dealer and min_cost >= 11600: + return False + + if not self.player.is_dealer and min_cost >= 7700: + return False + + return True + + # if wait is good we only damaten haneman + if self.player.is_dealer and min_cost >= 18000: + return False + + if not self.player.is_dealer and min_cost >= 12000: return False + return True + + # if we don't have yaku on every wait and it's two-sided or more, we call riichi return True def should_call_kan(self, tile, open_kan, from_riichi=False): diff --git a/project/game/ai/first_version/tests/tests_riichi.py b/project/game/ai/first_version/tests/tests_riichi.py index bdf65a73..0119803a 100644 --- a/project/game/ai/first_version/tests/tests_riichi.py +++ b/project/game/ai/first_version/tests/tests_riichi.py @@ -8,44 +8,102 @@ class CallRiichiTestCase(unittest.TestCase, TestMixin): - def test_dont_call_riichi_with_tanki_wait(self): + def test_dont_call_riichi_with_yaku_and_central_tanki_wait(self): table = Table() table.count_of_remaining_tiles = 60 player = table.player player.scores = 25000 - tiles = self._string_to_136_array(sou='123456', pin='123456', man='3') + tiles = self._string_to_136_array(sou='234567', pin='234567', man='4') player.init_hand(tiles) - - player.draw_tile(self._string_to_136_tile(man='4')) + player.draw_tile(self._string_to_136_tile(man='5')) player.discard_tile() self.assertEqual(player.can_call_riichi(), False) + def test_call_riichi_and_penchan_wait(self): table = Table() table.count_of_remaining_tiles = 60 player = table.player player.scores = 25000 - tiles = self._string_to_136_array(sou='1133557799', pin='113') - tile = self._string_to_136_tile(pin='6') + tiles = self._string_to_136_array(sou='11223', pin='234567', man='66') player.init_hand(tiles) - player.draw_tile(tile) + player.draw_tile(self._string_to_136_tile(man='9')) player.discard_tile() - # for chitoitsu it is ok to have a pair wait self.assertEqual(player.can_call_riichi(), True) - def test_call_riichi_and_penchan_wait(self): + def test_dont_call_riichi_expensive_damaten_with_yaku(self): table = Table() table.count_of_remaining_tiles = 60 player = table.player player.scores = 25000 - tiles = self._string_to_136_array(sou='11223', pin='234567', man='66') - tile = self._string_to_136_tile(man='9') + table.add_dora_indicator(self._string_to_136_tile(man='7')) + table.add_dora_indicator(self._string_to_136_tile(man='5')) + table.add_dora_indicator(self._string_to_136_tile(sou='1')) + + # tanyao pinfu sanshoku dora 4 - this is damaten baiman, let's not riichi it + tiles = self._string_to_136_array(man='67888', sou='678', pin='34678') + player.init_hand(tiles) + player.draw_tile(self._string_to_136_tile(honors='3')) + player.discard_tile() + self.assertEqual(player.can_call_riichi(), False) + + # let's test lots of doras hand, tanyao dora 8, also damaten baiman + tiles = self._string_to_136_array(man='666888', sou='22', pin='34678') + player.init_hand(tiles) + player.draw_tile(self._string_to_136_tile(honors='3')) + player.discard_tile() + self.assertEqual(player.can_call_riichi(), False) + + # chuuren + tiles = self._string_to_136_array(man='1112345678999') + player.init_hand(tiles) + player.draw_tile(self._string_to_136_tile(honors='3')) + player.discard_tile() + self.assertEqual(player.can_call_riichi(), False) + + def test_riichi_expensive_hand_without_yaku(self): + table = Table() + table.count_of_remaining_tiles = 60 + player = table.player + player.scores = 25000 + + table.add_dora_indicator(self._string_to_136_tile(man='1')) + table.add_dora_indicator(self._string_to_136_tile(sou='1')) + table.add_dora_indicator(self._string_to_136_tile(pin='1')) + + tiles = self._string_to_136_array(man='222', sou='22278', pin='22789') player.init_hand(tiles) - player.draw_tile(tile) + player.draw_tile(self._string_to_136_tile(honors='3')) player.discard_tile() + self.assertEqual(player.can_call_riichi(), True) + def test_riichi_tanki_honor_without_yaku(self): + table = Table() + table.count_of_remaining_tiles = 60 + player = table.player + player.scores = 25000 + + table.add_dora_indicator(self._string_to_136_tile(man='2')) + table.add_dora_indicator(self._string_to_136_tile(sou='6')) + + tiles = self._string_to_136_array(man='345678', sou='789', pin='123', honors='2') + player.init_hand(tiles) + player.draw_tile(self._string_to_136_tile(honors='3')) + player.discard_tile() + self.assertEqual(player.can_call_riichi(), True) + + def test_riichi_tanki_honor_chiitoitsu(self): + table = Table() + table.count_of_remaining_tiles = 60 + player = table.player + player.scores = 25000 + + tiles = self._string_to_136_array(man='22336688', sou='99', pin='99', honors='2') + player.init_hand(tiles) + player.draw_tile(self._string_to_136_tile(honors='3')) + player.discard_tile() self.assertEqual(player.can_call_riichi(), True) From 4df0253e1f2afe97c3005aebdcb375436bfadea7 Mon Sep 17 00:00:00 2001 From: bogachev-pa Date: Wed, 5 Sep 2018 01:32:02 +0300 Subject: [PATCH 049/126] avoid melding into agari if that's not good for hand building #85 --- project/game/ai/first_version/main.py | 6 ++++++ project/game/ai/first_version/strategies/main.py | 15 +++++++++++++++ .../tests/strategies/tests_formal_tempai.py | 1 - .../tests/strategies/tests_tanyao.py | 1 - 4 files changed, 21 insertions(+), 2 deletions(-) diff --git a/project/game/ai/first_version/main.py b/project/game/ai/first_version/main.py index 2b0336aa..5d7df64e 100644 --- a/project/game/ai/first_version/main.py +++ b/project/game/ai/first_version/main.py @@ -210,6 +210,12 @@ def try_to_call_meld(self, tile, is_kamicha_discard): if not self.current_strategy: return None, None + tiles_34 = TilesConverter.to_34_array(tiles_136) + previous_shanten = self.shanten_calculator.calculate_shanten(tiles_34, self.player.meld_34_tiles) + if previous_shanten == Shanten.AGARI_STATE: + if not self.current_strategy.can_meld_into_agari(): + return None, None + meld, discard_option = self.current_strategy.try_to_call_meld(tile, is_kamicha_discard, tiles_136) tile_to_discard = None if discard_option: diff --git a/project/game/ai/first_version/strategies/main.py b/project/game/ai/first_version/strategies/main.py index 36ffd63f..e1a6f9ea 100644 --- a/project/game/ai/first_version/strategies/main.py +++ b/project/game/ai/first_version/strategies/main.py @@ -56,6 +56,21 @@ def should_activate_strategy(self, tiles_136): return True + def can_meld_into_agari(self): + """ + Is melding into agari allowed with this strategy + :return: boolean + """ + # By default, the logic is the following: if we have any + # non-suitable tiles, we can meld into agari state, because we'll + # throw them away after meld. + # Otherwise, there is no point. + for tile in self.player.tiles: + if not self.is_tile_suitable(tile): + return True + + return False + def is_tile_suitable(self, tile): """ Can tile be used for open hand strategy or not 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 671b1b8d..c93144ad 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 @@ -53,7 +53,6 @@ def test_get_tempai(self): # We shouldn't open when we are already in tempai expect for some # special cases - @unittest.expectedFailure def test_dont_meld_agari(self): strategy = FormalTempaiStrategy(BaseStrategy.FORMAL_TEMPAI, self.player) 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 8721bf4b..f80fc0f4 100644 --- a/project/game/ai/first_version/tests/strategies/tests_tanyao.py +++ b/project/game/ai/first_version/tests/strategies/tests_tanyao.py @@ -313,7 +313,6 @@ def test_correct_discard_agari_no_yaku(self): # In case we are in temporary furiten, we can't call ron, but can still # make chi. We assume this chi to be bad, so let's not call it. - @unittest.expectedFailure def test_dont_meld_agari(self): tiles = self._string_to_136_array(man='23567', sou='456', pin='22244') self.player.init_hand(tiles) From ab9400918da02496627028cff06a0b7dfe7344e4 Mon Sep 17 00:00:00 2001 From: Nihisil Date: Wed, 5 Sep 2018 08:30:29 +0800 Subject: [PATCH 050/126] Upgrade mahjong package to 1.1.5 --- project/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/requirements.txt b/project/requirements.txt index c2dde972..891a7cf1 100644 --- a/project/requirements.txt +++ b/project/requirements.txt @@ -1,3 +1,3 @@ -mahjong==1.1.4 +mahjong==1.1.5 requests==2.18.4 flake8==3.4.1 \ No newline at end of file From c464fd57ad39cc94d249eadcff104111b5a62325 Mon Sep 17 00:00:00 2001 From: Nihisil Date: Wed, 5 Sep 2018 08:45:04 +0800 Subject: [PATCH 051/126] Calculate shanten for chiitoitsu only when we are with chiitoitsu strategy --- project/game/ai/first_version/main.py | 25 ++++++++++++++----- .../game/ai/first_version/strategies/main.py | 6 ++++- .../tests/strategies/tests_honitsu.py | 15 +++++++++++ 3 files changed, 39 insertions(+), 7 deletions(-) diff --git a/project/game/ai/first_version/main.py b/project/game/ai/first_version/main.py index 5d7df64e..bef8b42c 100644 --- a/project/game/ai/first_version/main.py +++ b/project/game/ai/first_version/main.py @@ -44,6 +44,8 @@ class ImplementationAI(InterfaceAI): current_strategy = None last_discard_option = None + use_chitoitsu = False + hand_cache = {} def __init__(self, player): @@ -146,7 +148,7 @@ def calculate_outs(self, tiles, closed_hand, open_sets_34=None): tiles_34[hand_tile] -= 1 - shanten = self.shanten_calculator.calculate_shanten(tiles_34, open_sets_34) + shanten = self.shanten_calculator.calculate_shanten(tiles_34, open_sets_34, chiitoitsu=self.use_chitoitsu) waiting = [] for j in range(0, 34): @@ -169,7 +171,11 @@ def calculate_outs(self, tiles, closed_hand, open_sets_34=None): if key in self.hand_cache: new_shanten = self.hand_cache[key] else: - new_shanten = self.shanten_calculator.calculate_shanten(tiles_34, open_sets_34) + new_shanten = self.shanten_calculator.calculate_shanten( + tiles_34, + open_sets_34, + chiitoitsu=self.use_chitoitsu + ) self.hand_cache[key] = new_shanten if new_shanten == shanten - 1: @@ -189,7 +195,7 @@ def calculate_outs(self, tiles, closed_hand, open_sets_34=None): if is_agari: shanten = Shanten.AGARI_STATE else: - shanten = self.shanten_calculator.calculate_shanten(tiles_34, open_sets_34) + shanten = self.shanten_calculator.calculate_shanten(tiles_34, open_sets_34, chiitoitsu=self.use_chitoitsu) return results, shanten @@ -211,7 +217,11 @@ def try_to_call_meld(self, tile, is_kamicha_discard): return None, None tiles_34 = TilesConverter.to_34_array(tiles_136) - previous_shanten = self.shanten_calculator.calculate_shanten(tiles_34, self.player.meld_34_tiles) + previous_shanten = self.shanten_calculator.calculate_shanten( + tiles_34, + self.player.meld_34_tiles, + chiitoitsu=self.use_chitoitsu + ) if previous_shanten == Shanten.AGARI_STATE: if not self.current_strategy.can_meld_into_agari(): return None, None @@ -250,6 +260,9 @@ def determine_strategy(self, tiles_136): self.current_strategy = strategy if self.current_strategy: + if self.current_strategy.type == BaseStrategy.CHIITOITSU: + self.use_chitoitsu = True + if not old_strategy or self.current_strategy.type != old_strategy.type: message = '{} switched to {} strategy'.format(self.player.name, self.current_strategy) if old_strategy: @@ -496,13 +509,13 @@ def should_call_kan(self, tile, open_kan, from_riichi=False): tiles_34[tile_34] += 1 melds = self.player.meld_34_tiles - previous_shanten = self.shanten_calculator.calculate_shanten(tiles_34, melds) + previous_shanten = self.shanten_calculator.calculate_shanten(tiles_34, melds, chiitoitsu=self.use_chitoitsu) if not open_kan and not from_riichi: tiles_34[tile_34] -= 1 melds += [[tile_34, tile_34, tile_34]] - new_shanten = self.shanten_calculator.calculate_shanten(tiles_34, melds) + new_shanten = self.shanten_calculator.calculate_shanten(tiles_34, melds, chiitoitsu=self.use_chitoitsu) # called kan will not ruin our hand if new_shanten <= previous_shanten: diff --git a/project/game/ai/first_version/strategies/main.py b/project/game/ai/first_version/strategies/main.py index e1a6f9ea..041be4ce 100644 --- a/project/game/ai/first_version/strategies/main.py +++ b/project/game/ai/first_version/strategies/main.py @@ -311,7 +311,11 @@ def _find_best_meld_to_open(self, possible_melds, completed_hand): results = [] for meld in possible_melds: melds = self.player.meld_34_tiles + [meld] - shanten = self.player.ai.shanten_calculator.calculate_shanten(completed_hand_34, melds) + shanten = self.player.ai.shanten_calculator.calculate_shanten( + completed_hand_34, + melds, + chiitoitsu=self.player.ai.use_chitoitsu + ) results.append({'shanten': shanten, 'meld': meld}) results = sorted(results, key=lambda i: i['shanten']) 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 9f8dcb35..e0569f71 100644 --- a/project/game/ai/first_version/tests/strategies/tests_honitsu.py +++ b/project/game/ai/first_version/tests/strategies/tests_honitsu.py @@ -182,6 +182,21 @@ def test_open_yakuhai_same_shanten(self): self.assertNotEqual(meld, None) self.assertEqual(self._to_string(meld.tiles), '777z') + 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') + player.init_hand(tiles) + + tile = player.discard_tile() + self.assertEqual(self._to_string([tile]), '4p') + + tile = self._string_to_136_tile(honors='5') + meld, _ = player.try_to_call_meld(tile, False) + self.assertNotEqual(meld, None) + self.assertEqual(self._to_string(meld.tiles), '555z') + # issue #84 @unittest.expectedFailure def test_open_suit_same_shanten(self): From 8aa0a3fe33828dd7b9a27e3516ee36faaae31f62 Mon Sep 17 00:00:00 2001 From: Nihisil Date: Wed, 5 Sep 2018 15:18:01 +0800 Subject: [PATCH 052/126] Fix potential issue with chitoitsu --- project/game/ai/first_version/main.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/project/game/ai/first_version/main.py b/project/game/ai/first_version/main.py index bef8b42c..b1778826 100644 --- a/project/game/ai/first_version/main.py +++ b/project/game/ai/first_version/main.py @@ -235,6 +235,8 @@ def try_to_call_meld(self, tile, is_kamicha_discard): return meld, tile_to_discard def determine_strategy(self, tiles_136): + self.use_chitoitsu = False + # for already opened hand we don't need to give up on selected strategy if self.player.is_open_hand and self.current_strategy: return False @@ -260,8 +262,7 @@ def determine_strategy(self, tiles_136): self.current_strategy = strategy if self.current_strategy: - if self.current_strategy.type == BaseStrategy.CHIITOITSU: - self.use_chitoitsu = True + self.use_chitoitsu = self.current_strategy.type == BaseStrategy.CHIITOITSU if not old_strategy or self.current_strategy.type != old_strategy.type: message = '{} switched to {} strategy'.format(self.player.name, self.current_strategy) From 19bb3fe024715fba80e7abc681c28131575b1e98 Mon Sep 17 00:00:00 2001 From: Nihisil Date: Wed, 5 Sep 2018 15:28:35 +0800 Subject: [PATCH 053/126] We will not be in tempai state after init hand event --- project/game/ai/first_version/main.py | 1 + 1 file changed, 1 insertion(+) diff --git a/project/game/ai/first_version/main.py b/project/game/ai/first_version/main.py index b1778826..8a6070bd 100644 --- a/project/game/ai/first_version/main.py +++ b/project/game/ai/first_version/main.py @@ -63,6 +63,7 @@ def init_hand(self): # 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) + 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) From cd0a6ed230707e910b6140fa21848d1f0a4d7ceb Mon Sep 17 00:00:00 2001 From: Nihisil Date: Wed, 5 Sep 2018 15:48:16 +0800 Subject: [PATCH 054/126] Small refactoring --- project/game/ai/first_version/damaten.py | 179 +++++++++++++++++ project/game/ai/first_version/main.py | 181 +----------------- .../ai/first_version/tests/tests_riichi.py | 128 ++++++------- 3 files changed, 248 insertions(+), 240 deletions(-) create mode 100644 project/game/ai/first_version/damaten.py diff --git a/project/game/ai/first_version/damaten.py b/project/game/ai/first_version/damaten.py new file mode 100644 index 00000000..d8090cce --- /dev/null +++ b/project/game/ai/first_version/damaten.py @@ -0,0 +1,179 @@ +from mahjong.tile import TilesConverter +from mahjong.utils import is_honor, simplify, is_pair, is_chi + + +class Damaten: + + def __init__(self, player): + self.player = player + + def should_call_riichi(self): + # empty waiting can be found in some cases + if not self.player.ai.waiting: + return False + + # TODO: always call daburi + + if self.player.ai.in_defence: + return False + + # don't call karaten riichi + count_tiles = self.player.ai.count_tiles(self.player.ai.waiting, TilesConverter.to_34_array(self.player.tiles)) + if count_tiles == 0: + return False + + # first of all let's consider 1-sided waits + if len(self.player.ai.waiting) == 1: + waiting = self.player.ai.waiting[0] + hand_value = self.player.ai.estimate_hand_value(waiting, call_riichi=False) + + tiles = self.player.closed_hand + [waiting * 4] + closed_melds = [x for x in self.player.melds if not x.opened] + for meld in closed_melds: + tiles.extend(meld.tiles[:3]) + + tiles_34 = TilesConverter.to_34_array(tiles) + + results = self.player.ai.hand_divider.divide_hand(tiles_34) + result = results[0] + + # what if we have yaku + if hand_value.yaku is not None and hand_value.cost is not None: + min_cost = hand_value.cost['main'] + + # tanki honor is a good wait, let's damaten only if hand is already expensive + if is_honor(waiting): + if self.player.is_dealer and min_cost < 12000: + return True + + if not self.player.is_dealer and min_cost < 8000: + return True + + return False + + simplified_waiting = simplify(waiting) + + for hand_set in result: + if waiting not in hand_set: + continue + + if is_pair(hand_set): + # let's not riichi tanki 4, 5, 6 + if 3 <= simplified_waiting <= 5: + return False + + # don't riichi tanki wait on 1, 2, 3, 7, 8, 9 if it's only 1 tile + if count_tiles == 1: + return False + + # don't riichi 2378 tanki if hand has good value + if simplified_waiting != 0 and simplified_waiting != 8: + if self.player.is_dealer and min_cost >= 7700: + return False + + if not self.player.is_dealer and min_cost >= 5200: + return False + + # TODO: check for kabe and suji + return True + + # 1-sided wait means kanchan or penchan + if is_chi(hand_set): + # let's not riichi kanchan on 4, 5, 6 + if 4 <= simplified_waiting <= 6: + return False + + # now checking waiting for 2, 3, 7, 8 + # if we only have 1 tile to wait for, let's damaten + if count_tiles == 1: + return False + + # if we have 2 tiles to wait for and hand cost is good without riichi, + # let's damaten + if count_tiles == 2: + if self.player.is_dealer and min_cost >= 7700: + return False + + if not self.player.is_dealer and min_cost >= 5200: + return False + + # TODO: check for kabe and suji + return True + + # what if we don't have yaku + # our tanki wait is good, let's riichi + if is_honor(waiting): + return True + + simplified_waiting = simplify(waiting) + + for hand_set in result: + if not waiting in hand_set: + continue + + if is_pair(hand_set): + # let's not riichi tanki 4, 5, 6 + if 3 <= simplified_waiting <= 5: + return False + + # 1-sided wait means kanchan or penchan + if is_chi(hand_set): + # let's only riichi this bad wait if + # it has all 4 tiles available or it + # it's not too early + if 4 <= simplified_waiting <= 6: + return count_tiles == 4 or self.player.round_step >= 6 + + # TODO: implement rest of the logic + + return True + + # now we are looking at two or more sided waits only + hand_costs = [] + waits_with_yaku = 0 + for waiting in self.player.ai.waiting: + hand_value = self.player.ai.estimate_hand_value(waiting, call_riichi=False) + if hand_value.error is None: + hand_costs.append(hand_value.cost['main']) + if hand_value.yaku is not None and hand_value.cost is not None: + waits_with_yaku += 1 + + # if we have yaku on every wait + if waits_with_yaku == len(self.player.ai.waiting): + min_cost = min(hand_costs) + + # let's not riichi this bad wait + if count_tiles <= 2: + return False + + # if wait is slighly better, we will riichi only a cheap hand + if count_tiles <= 4: + if self.player.is_dealer and min_cost >= 7700: + return False + + if not self.player.is_dealer and min_cost >= 5200: + return False + + return True + + # wait is even better, but still don't call riichi on damaten mangan + if count_tiles <= 6: + if self.player.is_dealer and min_cost >= 11600: + return False + + if not self.player.is_dealer and min_cost >= 7700: + return False + + return True + + # if wait is good we only damaten haneman + if self.player.is_dealer and min_cost >= 18000: + return False + + if not self.player.is_dealer and min_cost >= 12000: + return False + + return True + + # if we don't have yaku on every wait and it's two-sided or more, we call riichi + return True diff --git a/project/game/ai/first_version/main.py b/project/game/ai/first_version/main.py index 1692698a..a35628b5 100644 --- a/project/game/ai/first_version/main.py +++ b/project/game/ai/first_version/main.py @@ -10,18 +10,19 @@ from mahjong.meld import Meld from mahjong.shanten import Shanten from mahjong.tile import TilesConverter -from mahjong.utils import is_pair, is_pon, is_tile_strictly_isolated, is_honor, simplify, is_chi +from mahjong.utils import is_pon, is_tile_strictly_isolated from game.ai.base.main import InterfaceAI from game.ai.discard import DiscardOption +from game.ai.first_version.damaten import Damaten from game.ai.first_version.defence.main import DefenceHandler +from game.ai.first_version.strategies.chiitoitsu import ChiitoitsuStrategy +from game.ai.first_version.strategies.chinitsu import ChinitsuStrategy +from game.ai.first_version.strategies.formal_tempai import FormalTempaiStrategy from game.ai.first_version.strategies.honitsu import HonitsuStrategy from game.ai.first_version.strategies.main import BaseStrategy from game.ai.first_version.strategies.tanyao import TanyaoStrategy from game.ai.first_version.strategies.yakuhai import YakuhaiStrategy -from game.ai.first_version.strategies.formal_tempai import FormalTempaiStrategy -from game.ai.first_version.strategies.chinitsu import ChinitsuStrategy -from game.ai.first_version.strategies.chiitoitsu import ChiitoitsuStrategy logger = logging.getLogger('ai') @@ -32,6 +33,7 @@ class ImplementationAI(InterfaceAI): agari = None shanten_calculator = None defence = None + damaten = None hand_divider = None finished_hand = None @@ -54,6 +56,7 @@ def __init__(self, player): self.agari = Agari() self.shanten_calculator = Shanten() self.defence = DefenceHandler(player) + self.damaten = Damaten(player) self.hand_divider = HandDivider() self.finished_hand = HandCalculator() @@ -425,175 +428,7 @@ def estimate_hand_value(self, win_tile, tiles=None, call_riichi=False): return result def should_call_riichi(self): - # empty waiting can be found in some cases - if not self.waiting: - return False - - # TODO: always call daburi - - if self.in_defence: - return False - - # don't call karaten riichi - count_tiles = self.count_tiles(self.waiting, TilesConverter.to_34_array(self.player.tiles)) - if count_tiles == 0: - return False - - # first of all let's consider 1-sided waits - if len(self.waiting) == 1: - waiting = self.waiting[0] - hand_value = self.estimate_hand_value(waiting, call_riichi=False) - - tiles = self.player.closed_hand + [waiting * 4] - closed_melds = [x for x in self.player.melds if not x.opened] - for meld in closed_melds: - tiles.extend(meld.tiles[:3]) - - tiles_34 = TilesConverter.to_34_array(tiles) - - results = self.hand_divider.divide_hand(tiles_34) - result = results[0] - - # what if we have yaku - if hand_value.yaku is not None and hand_value.cost is not None: - min_cost = hand_value.cost['main'] - - # tanki honor is a good wait, let's damaten only if hand is already expensive - if is_honor(waiting): - if self.player.is_dealer and min_cost < 12000: - return True - - if not self.player.is_dealer and min_cost < 8000: - return True - - return False - - simplified_waiting = simplify(waiting) - - for hand_set in result: - if waiting not in hand_set: - continue - - if is_pair(hand_set): - # let's not riichi tanki 4, 5, 6 - if 3 <= simplified_waiting <= 5: - return False - - # don't riichi tanki wait on 1, 2, 3, 7, 8, 9 if it's only 1 tile - if count_tiles == 1: - return False - - # don't riichi 2378 tanki if hand has good value - if simplified_waiting != 0 and simplified_waiting != 8: - if self.player.is_dealer and min_cost >= 7700: - return False - - if not self.player.is_dealer and min_cost >= 5200: - return False - - # TODO: check for kabe and suji - return True - - # 1-sided wait means kanchan or penchan - if is_chi(hand_set): - # let's not riichi kanchan on 4, 5, 6 - if 4 <= simplified_waiting <= 6: - return False - - # now checking waiting for 2, 3, 7, 8 - # if we only have 1 tile to wait for, let's damaten - if count_tiles == 1: - return False - - # if we have 2 tiles to wait for and hand cost is good without riichi, - # let's damaten - if count_tiles == 2: - if self.player.is_dealer and min_cost >= 7700: - return False - - if not self.player.is_dealer and min_cost >= 5200: - return False - - # TODO: check for kabe and suji - return True - - # what if we don't have yaku - # our tanki wait is good, let's riichi - if is_honor(waiting): - return True - - simplified_waiting = simplify(waiting) - - for hand_set in result: - if not waiting in hand_set: - continue - - if is_pair(hand_set): - # let's not riichi tanki 4, 5, 6 - if 3 <= simplified_waiting <= 5: - return False - - # 1-sided wait means kanchan or penchan - if is_chi(hand_set): - # let's only riichi this bad wait if - # it has all 4 tiles available or it - # it's not too early - if 4 <= simplified_waiting <= 6: - return count_tiles == 4 or self.player.round_step >= 6 - - # TODO: implement rest of the logic - - return True - - # now we are looking at two or more sided waits only - hand_costs = [] - waits_with_yaku = 0 - for waiting in self.waiting: - hand_value = self.estimate_hand_value(waiting, call_riichi=False) - if hand_value.error is None: - hand_costs.append(hand_value.cost['main']) - if hand_value.yaku is not None and hand_value.cost is not None: - waits_with_yaku += 1 - - # if we have yaku on every wait - if waits_with_yaku == len(self.waiting): - min_cost = min(hand_costs) - - # let's not riichi this bad wait - if count_tiles <= 2: - return False - - # if wait is slighly better, we will riichi only a cheap hand - if count_tiles <= 4: - if self.player.is_dealer and min_cost >= 7700: - return False - - if not self.player.is_dealer and min_cost >= 5200: - return False - - return True - - # wait is even better, but still don't call riichi on damaten mangan - if count_tiles <= 6: - if self.player.is_dealer and min_cost >= 11600: - return False - - if not self.player.is_dealer and min_cost >= 7700: - return False - - return True - - # if wait is good we only damaten haneman - if self.player.is_dealer and min_cost >= 18000: - return False - - if not self.player.is_dealer and min_cost >= 12000: - return False - - return True - - # if we don't have yaku on every wait and it's two-sided or more, we call riichi - return True + return self.damaten.should_call_riichi() def should_call_kan(self, tile, open_kan, from_riichi=False): """ diff --git a/project/game/ai/first_version/tests/tests_riichi.py b/project/game/ai/first_version/tests/tests_riichi.py index 0119803a..e6f986bd 100644 --- a/project/game/ai/first_version/tests/tests_riichi.py +++ b/project/game/ai/first_version/tests/tests_riichi.py @@ -9,101 +9,95 @@ class CallRiichiTestCase(unittest.TestCase, TestMixin): def test_dont_call_riichi_with_yaku_and_central_tanki_wait(self): - table = Table() - table.count_of_remaining_tiles = 60 - player = table.player - player.scores = 25000 + self._make_table() tiles = self._string_to_136_array(sou='234567', pin='234567', man='4') - player.init_hand(tiles) - player.draw_tile(self._string_to_136_tile(man='5')) - player.discard_tile() + self.player.init_hand(tiles) + self.player.draw_tile(self._string_to_136_tile(man='5')) + self.player.discard_tile() - self.assertEqual(player.can_call_riichi(), False) + self.assertEqual(self.player.can_call_riichi(), False) def test_call_riichi_and_penchan_wait(self): - table = Table() - table.count_of_remaining_tiles = 60 - player = table.player - player.scores = 25000 + self._make_table() tiles = self._string_to_136_array(sou='11223', pin='234567', man='66') - player.init_hand(tiles) - player.draw_tile(self._string_to_136_tile(man='9')) - player.discard_tile() + self.player.init_hand(tiles) + self.player.draw_tile(self._string_to_136_tile(man='9')) + self.player.discard_tile() - self.assertEqual(player.can_call_riichi(), True) + self.assertEqual(self.player.can_call_riichi(), True) def test_dont_call_riichi_expensive_damaten_with_yaku(self): - table = Table() - table.count_of_remaining_tiles = 60 - player = table.player - player.scores = 25000 - - table.add_dora_indicator(self._string_to_136_tile(man='7')) - table.add_dora_indicator(self._string_to_136_tile(man='5')) - table.add_dora_indicator(self._string_to_136_tile(sou='1')) + self._make_table(dora_indicators=[ + self._string_to_136_tile(man='7'), + self._string_to_136_tile(man='5'), + self._string_to_136_tile(sou='1'), + ]) # tanyao pinfu sanshoku dora 4 - this is damaten baiman, let's not riichi it tiles = self._string_to_136_array(man='67888', sou='678', pin='34678') - player.init_hand(tiles) - player.draw_tile(self._string_to_136_tile(honors='3')) - player.discard_tile() - self.assertEqual(player.can_call_riichi(), False) + self.player.init_hand(tiles) + self.player.draw_tile(self._string_to_136_tile(honors='3')) + self.player.discard_tile() + self.assertEqual(self.player.can_call_riichi(), False) # let's test lots of doras hand, tanyao dora 8, also damaten baiman tiles = self._string_to_136_array(man='666888', sou='22', pin='34678') - player.init_hand(tiles) - player.draw_tile(self._string_to_136_tile(honors='3')) - player.discard_tile() - self.assertEqual(player.can_call_riichi(), False) + self.player.init_hand(tiles) + self.player.draw_tile(self._string_to_136_tile(honors='3')) + self.player.discard_tile() + self.assertEqual(self.player.can_call_riichi(), False) # chuuren tiles = self._string_to_136_array(man='1112345678999') - player.init_hand(tiles) - player.draw_tile(self._string_to_136_tile(honors='3')) - player.discard_tile() - self.assertEqual(player.can_call_riichi(), False) + self.player.init_hand(tiles) + self.player.draw_tile(self._string_to_136_tile(honors='3')) + self.player.discard_tile() + self.assertEqual(self.player.can_call_riichi(), False) def test_riichi_expensive_hand_without_yaku(self): - table = Table() - table.count_of_remaining_tiles = 60 - player = table.player - player.scores = 25000 - - table.add_dora_indicator(self._string_to_136_tile(man='1')) - table.add_dora_indicator(self._string_to_136_tile(sou='1')) - table.add_dora_indicator(self._string_to_136_tile(pin='1')) + self._make_table(dora_indicators=[ + self._string_to_136_tile(man='1'), + self._string_to_136_tile(sou='1'), + self._string_to_136_tile(pin='1') + ]) tiles = self._string_to_136_array(man='222', sou='22278', pin='22789') - player.init_hand(tiles) - player.draw_tile(self._string_to_136_tile(honors='3')) - player.discard_tile() - self.assertEqual(player.can_call_riichi(), True) + self.player.init_hand(tiles) + 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_riichi_tanki_honor_without_yaku(self): - table = Table() - table.count_of_remaining_tiles = 60 - player = table.player - player.scores = 25000 - - table.add_dora_indicator(self._string_to_136_tile(man='2')) - table.add_dora_indicator(self._string_to_136_tile(sou='6')) + self._make_table( + dora_indicators=[ + self._string_to_136_tile(man='2'), + self._string_to_136_tile(sou='6') + ] + ) tiles = self._string_to_136_array(man='345678', sou='789', pin='123', honors='2') - player.init_hand(tiles) - player.draw_tile(self._string_to_136_tile(honors='3')) - player.discard_tile() - self.assertEqual(player.can_call_riichi(), True) + self.player.init_hand(tiles) + 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_riichi_tanki_honor_chiitoitsu(self): - table = Table() - table.count_of_remaining_tiles = 60 - player = table.player - player.scores = 25000 + self._make_table() tiles = self._string_to_136_array(man='22336688', sou='99', pin='99', honors='2') - player.init_hand(tiles) - player.draw_tile(self._string_to_136_tile(honors='3')) - player.discard_tile() - self.assertEqual(player.can_call_riichi(), True) + self.player.init_hand(tiles) + self.player.draw_tile(self._string_to_136_tile(honors='3')) + self.player.discard_tile() + self.assertEqual(self.player.can_call_riichi(), True) + + def _make_table(self, dora_indicators=None): + self.table = Table() + self.table.count_of_remaining_tiles = 60 + self.player = self.table.player + self.player.scores = 25000 + + if dora_indicators: + for x in dora_indicators: + self.table.add_dora_indicator(x) From 7960750faa22ebd4e2ae502b0f13ca8e10856f72 Mon Sep 17 00:00:00 2001 From: Nihisil Date: Wed, 5 Sep 2018 17:39:17 +0800 Subject: [PATCH 055/126] Always call daburi riichi --- project/game/ai/first_version/damaten.py | 6 +++-- .../ai/first_version/tests/tests_riichi.py | 22 +++++++++++++++++++ project/game/table.py | 6 ++--- 3 files changed, 29 insertions(+), 5 deletions(-) diff --git a/project/game/ai/first_version/damaten.py b/project/game/ai/first_version/damaten.py index d8090cce..f1dc63bb 100644 --- a/project/game/ai/first_version/damaten.py +++ b/project/game/ai/first_version/damaten.py @@ -12,8 +12,6 @@ def should_call_riichi(self): if not self.player.ai.waiting: return False - # TODO: always call daburi - if self.player.ai.in_defence: return False @@ -22,6 +20,10 @@ def should_call_riichi(self): if count_tiles == 0: return False + # It is daburi! + if self.player.round_step == 1: + return True + # first of all let's consider 1-sided waits if len(self.player.ai.waiting) == 1: waiting = self.player.ai.waiting[0] diff --git a/project/game/ai/first_version/tests/tests_riichi.py b/project/game/ai/first_version/tests/tests_riichi.py index e6f986bd..9cccefdc 100644 --- a/project/game/ai/first_version/tests/tests_riichi.py +++ b/project/game/ai/first_version/tests/tests_riichi.py @@ -28,6 +28,14 @@ def test_call_riichi_and_penchan_wait(self): self.assertEqual(self.player.can_call_riichi(), True) + self.table.add_discarded_tile(1, self._string_to_136_tile(sou='3'), False) + self.table.add_discarded_tile(1, self._string_to_136_tile(sou='3'), False) + self.table.add_discarded_tile(1, self._string_to_136_tile(sou='3'), False) + + # we are in karaten + # so we don't need to riichi it + self.assertEqual(self.player.can_call_riichi(), False) + def test_dont_call_riichi_expensive_damaten_with_yaku(self): self._make_table(dora_indicators=[ self._string_to_136_tile(man='7'), @@ -92,12 +100,26 @@ def test_riichi_tanki_honor_chiitoitsu(self): self.player.discard_tile() self.assertEqual(self.player.can_call_riichi(), True) + def test_always_call_daburi(self): + self._make_table() + self.player.round_step = 0 + + tiles = self._string_to_136_array(sou='234567', pin='234567', man='4') + self.player.init_hand(tiles) + self.player.draw_tile(self._string_to_136_tile(man='5')) + self.player.discard_tile() + + self.assertEqual(self.player.can_call_riichi(), True) + def _make_table(self, dora_indicators=None): self.table = Table() self.table.count_of_remaining_tiles = 60 self.player = self.table.player self.player.scores = 25000 + # with that we don't have daburi anymore + self.player.round_step = 1 + if dora_indicators: for x in dora_indicators: self.table.add_dora_indicator(x) diff --git a/project/game/table.py b/project/game/table.py index eb20ebcd..d9119b2d 100644 --- a/project/game/table.py +++ b/project/game/table.py @@ -102,15 +102,15 @@ def add_called_riichi(self, player_seat): if player_seat != 0: self.player.enemy_called_riichi(player_seat) - def add_discarded_tile(self, player_seat, tile, is_tsumogiri): + def add_discarded_tile(self, player_seat, tile_136, is_tsumogiri): """ :param player_seat: - :param tile: 136 format tile + :param tile_136: 136 format tile :param is_tsumogiri: was tile discarded from hand or not """ self.count_of_remaining_tiles -= 1 - tile = Tile(tile, is_tsumogiri) + tile = Tile(tile_136, is_tsumogiri) self.get_player(player_seat).add_discarded_tile(tile) # cache already revealed tiles From 2c2d2cad7cfb3f5b2c77a931911dac2ce38cb0f0 Mon Sep 17 00:00:00 2001 From: Nihisil Date: Wed, 5 Sep 2018 18:09:09 +0800 Subject: [PATCH 056/126] After called meld it is not daburi anymore --- project/game/ai/first_version/damaten.py | 3 ++- project/game/table.py | 5 +++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/project/game/ai/first_version/damaten.py b/project/game/ai/first_version/damaten.py index f1dc63bb..a1968391 100644 --- a/project/game/ai/first_version/damaten.py +++ b/project/game/ai/first_version/damaten.py @@ -21,7 +21,8 @@ def should_call_riichi(self): return False # It is daburi! - if self.player.round_step == 1: + first_discard = self.player.round_step == 1 + if first_discard and not self.player.table.meld_was_called: return True # first of all let's consider 1-sided waits diff --git a/project/game/table.py b/project/game/table.py index d9119b2d..6d01c5de 100644 --- a/project/game/table.py +++ b/project/game/table.py @@ -23,6 +23,8 @@ class Table(object): count_of_remaining_tiles = 0 count_of_players = 4 + meld_was_called = False + # array of tiles in 34 format revealed_tiles = None @@ -42,6 +44,7 @@ def __str__(self): def init_round(self, round_number, count_of_honba_sticks, count_of_riichi_sticks, dora_indicator, dealer_seat, scores): + self.meld_was_called = False self.dealer_seat = dealer_seat self.round_number = round_number self.count_of_honba_sticks = count_of_honba_sticks @@ -71,6 +74,8 @@ def init_round(self, round_number, count_of_honba_sticks, count_of_riichi_sticks i += 1 def add_called_meld(self, player_seat, meld): + self.meld_was_called = True + # when opponent called meld it is means # that he discards tile from hand, not from wall self.count_of_remaining_tiles += 1 From f956ebfb60b77917e8e4b3b26aed28e4ee81f31b Mon Sep 17 00:00:00 2001 From: bogachev-pa Date: Wed, 5 Sep 2018 22:29:08 +0300 Subject: [PATCH 057/126] add kabe and suji traps handling to damaten logic (#59) --- project/game/ai/first_version/damaten.py | 195 +++++++++++------- project/game/ai/first_version/defence/kabe.py | 119 ++++++++--- project/game/ai/first_version/defence/suji.py | 109 ++++++---- .../ai/first_version/tests/tests_riichi.py | 80 +++++-- 4 files changed, 344 insertions(+), 159 deletions(-) diff --git a/project/game/ai/first_version/damaten.py b/project/game/ai/first_version/damaten.py index a1968391..5d7e6cdb 100644 --- a/project/game/ai/first_version/damaten.py +++ b/project/game/ai/first_version/damaten.py @@ -1,6 +1,8 @@ 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 Damaten: @@ -25,113 +27,156 @@ def should_call_riichi(self): if first_discard and not self.player.table.meld_was_called: return True - # first of all let's consider 1-sided waits if len(self.player.ai.waiting) == 1: - waiting = self.player.ai.waiting[0] - hand_value = self.player.ai.estimate_hand_value(waiting, call_riichi=False) + return self._should_call_riichi_one_sided() - tiles = self.player.closed_hand + [waiting * 4] - closed_melds = [x for x in self.player.melds if not x.opened] - for meld in closed_melds: - tiles.extend(meld.tiles[:3]) + return self._should_call_riichi_many_sided() - tiles_34 = TilesConverter.to_34_array(tiles) + def _should_call_riichi_one_sided(self): + count_tiles = self.player.ai.count_tiles(self.player.ai.waiting, TilesConverter.to_34_array(self.player.tiles)) + waiting = self.player.ai.waiting[0] + hand_value = self.player.ai.estimate_hand_value(waiting, call_riichi=False) - results = self.player.ai.hand_divider.divide_hand(tiles_34) - result = results[0] + tiles = self.player.closed_hand + [waiting * 4] + closed_melds = [x for x in self.player.melds if not x.opened] + for meld in closed_melds: + tiles.extend(meld.tiles[:3]) - # what if we have yaku - if hand_value.yaku is not None and hand_value.cost is not None: - min_cost = hand_value.cost['main'] + tiles_34 = TilesConverter.to_34_array(tiles) - # tanki honor is a good wait, let's damaten only if hand is already expensive - if is_honor(waiting): - if self.player.is_dealer and min_cost < 12000: - return True + results = self.player.ai.hand_divider.divide_hand(tiles_34) + result = results[0] - if not self.player.is_dealer and min_cost < 8000: - return True + # let's find suji-traps in our discard + suji_tiles = self.player.ai.defence.suji.find_suji_against_self(self.player) + have_suji = waiting in suji_tiles - return False + # let's find kabe + kabe_tiles = self.player.ai.defence.kabe.find_all_kabe(tiles_34) + have_kabe = False + for kabe in kabe_tiles: + if waiting == kabe.tile_34 and kabe.kabe_type == KabeTile.STRONG_KABE: + have_kabe = True - simplified_waiting = simplify(waiting) + # what if we have yaku + if hand_value.yaku is not None and hand_value.cost is not None: + min_cost = hand_value.cost['main'] - for hand_set in result: - if waiting not in hand_set: - continue + # tanki honor is a good wait, let's damaten only if hand is already expensive + if is_honor(waiting): + if self.player.is_dealer and min_cost < 12000: + return True - if is_pair(hand_set): - # let's not riichi tanki 4, 5, 6 - if 3 <= simplified_waiting <= 5: - return False + if not self.player.is_dealer and min_cost < 8000: + return True - # don't riichi tanki wait on 1, 2, 3, 7, 8, 9 if it's only 1 tile - if count_tiles == 1: - return False + return False - # don't riichi 2378 tanki if hand has good value - if simplified_waiting != 0 and simplified_waiting != 8: - if self.player.is_dealer and min_cost >= 7700: - return False + is_chiitoitsu = len([x for x in result if is_pair(x)]) == 7 + simplified_waiting = simplify(waiting) - if not self.player.is_dealer and min_cost >= 5200: - return False + for hand_set in result: + if waiting not in hand_set: + continue - # TODO: check for kabe and suji - return True + # tanki wait but not chiitoitsu + if is_pair(hand_set) and not is_chiitoitsu: + # let's not riichi tanki 4, 5, 6 + if 3 <= simplified_waiting <= 5: + return False - # 1-sided wait means kanchan or penchan - if is_chi(hand_set): - # let's not riichi kanchan on 4, 5, 6 - if 4 <= simplified_waiting <= 6: - return False + # don't riichi tanki wait on 1, 2, 3, 7, 8, 9 if it's only 1 tile + if count_tiles == 1: + return False - # now checking waiting for 2, 3, 7, 8 - # if we only have 1 tile to wait for, let's damaten - if count_tiles == 1: + # don't riichi 2378 tanki if hand has good value + if simplified_waiting != 0 and simplified_waiting != 8: + if self.player.is_dealer and min_cost >= 7700: return False - # if we have 2 tiles to wait for and hand cost is good without riichi, - # let's damaten - if count_tiles == 2: - if self.player.is_dealer and min_cost >= 7700: - return False + if not self.player.is_dealer and min_cost >= 5200: + return False - if not self.player.is_dealer and min_cost >= 5200: - return False + # only riichi if we have suji-trab or there is kabe + if not have_suji and not have_kabe: + return False - # TODO: check for kabe and suji - return True + return True - # what if we don't have yaku - # our tanki wait is good, let's riichi - if is_honor(waiting): - return True + # tanki wait with chiitoitsu + if is_pair(hand_set) and is_chiitoitsu: + # chiitoitsu on last suit tile is no the best + if count_tiles == 1: + return False - simplified_waiting = simplify(waiting) + # only riichi if we have suji-trab or there is kabe + if not have_suji and not have_kabe: + return False - for hand_set in result: - if not waiting in hand_set: - continue + return True - if is_pair(hand_set): - # let's not riichi tanki 4, 5, 6 + # 1-sided wait means kanchan or penchan + if is_chi(hand_set): + # let's not riichi kanchan on 4, 5, 6 if 3 <= simplified_waiting <= 5: return False - # 1-sided wait means kanchan or penchan - if is_chi(hand_set): - # let's only riichi this bad wait if - # it has all 4 tiles available or it - # it's not too early - if 4 <= simplified_waiting <= 6: - return count_tiles == 4 or self.player.round_step >= 6 + # now checking waiting for 2, 3, 7, 8 + # if we only have 1 tile to wait for, let's damaten + if count_tiles == 1: + return False + + # if we have 2 tiles to wait for and hand cost is good without riichi, + # let's damaten + if count_tiles == 2: + if self.player.is_dealer and min_cost >= 7700: + return False + + if not self.player.is_dealer and min_cost >= 5200: + return False + + # only riichi if we have suji-trab or there is kabe + if not have_suji and not have_kabe: + return False - # TODO: implement rest of the logic + return True + # what if we don't have yaku + # our tanki wait is good, let's riichi + if is_honor(waiting): return True - # now we are looking at two or more sided waits only + simplified_waiting = simplify(waiting) + + for hand_set in result: + if waiting not in hand_set: + continue + + if is_pair(hand_set): + # let's not riichi tanki wait without suji-trap or kabe + if not have_suji and not have_kabe: + return False + + # let's not riichi tanki on last suit tile if it's early + if count_tiles == 1 and self.player.round_step < 6: + return False + + # let's not riichi tanki 4, 5, 6 if it's early + if 3 <= simplified_waiting <= 5 and self.player.round_step < 6: + return False + + # 1-sided wait means kanchan or penchan + if is_chi(hand_set): + # let's only riichi this bad wait if + # it has all 4 tiles available or it + # it's not too early + if 4 <= simplified_waiting <= 6: + return count_tiles == 4 or self.player.round_step >= 6 + + return True + + def _should_call_riichi_many_sided(self): + count_tiles = self.player.ai.count_tiles(self.player.ai.waiting, TilesConverter.to_34_array(self.player.tiles)) hand_costs = [] waits_with_yaku = 0 for waiting in self.player.ai.waiting: diff --git a/project/game/ai/first_version/defence/kabe.py b/project/game/ai/first_version/defence/kabe.py index 8d534da8..478211a3 100644 --- a/project/game/ai/first_version/defence/kabe.py +++ b/project/game/ai/first_version/defence/kabe.py @@ -7,52 +7,96 @@ class Kabe(Defence): - def find_tiles_to_discard(self, _): - results = [] - + def find_all_kabe(self, tiles_34): # all indices shifted to -1 kabe_matrix = [ - {'indices': [1], 'blocked_tiles': [0]}, - {'indices': [2], 'blocked_tiles': [0, 1]}, - {'indices': [3], 'blocked_tiles': [1, 2]}, - {'indices': [4], 'blocked_tiles': [2, 6]}, - {'indices': [5], 'blocked_tiles': [6, 7]}, - {'indices': [6], 'blocked_tiles': [7, 8]}, - {'indices': [7], 'blocked_tiles': [8]}, - {'indices': [1, 5], 'blocked_tiles': [3]}, - {'indices': [2, 6], 'blocked_tiles': [4]}, - {'indices': [3, 7], 'blocked_tiles': [5]}, - {'indices': [1, 4], 'blocked_tiles': [2, 3]}, - {'indices': [2, 5], 'blocked_tiles': [3, 4]}, - {'indices': [3, 6], 'blocked_tiles': [4, 5]}, - {'indices': [4, 7], 'blocked_tiles': [5, 6]}, + {'indices': [1], 'blocked_tiles': [0], 'type': KabeTile.STRONG_KABE}, + {'indices': [2], 'blocked_tiles': [0, 1], 'type': KabeTile.STRONG_KABE}, + {'indices': [6], 'blocked_tiles': [7, 8], 'type': KabeTile.STRONG_KABE}, + {'indices': [7], 'blocked_tiles': [8], 'type': KabeTile.STRONG_KABE}, + {'indices': [0, 3], 'blocked_tiles': [2, 3], 'type': KabeTile.STRONG_KABE}, + {'indices': [1, 3], 'blocked_tiles': [2], 'type': KabeTile.STRONG_KABE}, + {'indices': [1, 4], 'blocked_tiles': [2, 3], 'type': KabeTile.STRONG_KABE}, + {'indices': [2, 4], 'blocked_tiles': [3], 'type': KabeTile.STRONG_KABE}, + {'indices': [2, 5], 'blocked_tiles': [3, 4], 'type': KabeTile.STRONG_KABE}, + {'indices': [3, 5], 'blocked_tiles': [4], 'type': KabeTile.STRONG_KABE}, + {'indices': [3, 6], 'blocked_tiles': [4, 5], 'type': KabeTile.STRONG_KABE}, + {'indices': [4, 6], 'blocked_tiles': [5], 'type': KabeTile.STRONG_KABE}, + {'indices': [4, 7], 'blocked_tiles': [5, 6], 'type': KabeTile.STRONG_KABE}, + {'indices': [5, 7], 'blocked_tiles': [6], 'type': KabeTile.STRONG_KABE}, + {'indices': [5, 8], 'blocked_tiles': [6, 7], 'type': KabeTile.STRONG_KABE}, + + {'indices': [3], 'blocked_tiles': [1, 2], 'type': KabeTile.WEAK_KABE}, + {'indices': [4], 'blocked_tiles': [2, 6], 'type': KabeTile.WEAK_KABE}, + {'indices': [5], 'blocked_tiles': [6, 7], 'type': KabeTile.WEAK_KABE}, + {'indices': [1, 5], 'blocked_tiles': [3], 'type': KabeTile.WEAK_KABE}, + {'indices': [2, 6], 'blocked_tiles': [4], 'type': KabeTile.WEAK_KABE}, + {'indices': [3, 7], 'blocked_tiles': [5], 'type': KabeTile.WEAK_KABE}, ] - suits = self._suits_tiles(self.defence.hand_34) + kabe_tiles_strong = [] + kabe_tiles_weak = [] + kabe_tiles_partial = [] + + suits = self._suits_tiles(tiles_34) for x in range(0, 3): suit = suits[x] # "kabe" - 4 revealed tiles kabe_tiles = [] + partial_kabe_tiles = [] for y in range(0, 9): suit_tile = suit[y] if suit_tile == 4: kabe_tiles.append(y) + elif suit_tile == 3: + partial_kabe_tiles.append(y) - blocked_indices = [] for matrix_item in kabe_matrix: - all_indices = len(list(set(matrix_item['indices']) - set(kabe_tiles))) == 0 - if all_indices: - blocked_indices.extend(matrix_item['blocked_tiles']) + if len(list(set(matrix_item['indices']) - set(kabe_tiles))) == 0: + for tile in matrix_item['blocked_tiles']: + if matrix_item['type'] == KabeTile.STRONG_KABE: + kabe_tiles_strong.append(tile + x * 9) + else: + kabe_tiles_weak.append(tile + x * 9) + + if len(list(set(matrix_item['indices']) - set(partial_kabe_tiles))) == 0: + for tile in matrix_item['blocked_tiles']: + kabe_tiles_partial.append(tile + x * 9) + + kabe_tiles_unique = [] + kabe_tiles_strong = list(set(kabe_tiles_strong)) + kabe_tiles_weak = list(set(kabe_tiles_weak)) + kabe_tiles_partial = list(set(kabe_tiles_partial)) + + for tile in kabe_tiles_strong: + kabe_tiles_unique.append(KabeTile(tile, KabeTile.STRONG_KABE)) + + for tile in kabe_tiles_weak: + if tile not in kabe_tiles_strong: + kabe_tiles_unique.append(KabeTile(tile, KabeTile.WEAK_KABE)) + + for tile in kabe_tiles_partial: + if tile not in kabe_tiles_strong and tile not in kabe_tiles_weak: + kabe_tiles_unique.append(KabeTile(tile, KabeTile.PARTIAL_KABE)) + + return kabe_tiles_unique - blocked_indices = list(set(blocked_indices)) - for index in blocked_indices: - # let's find 34 tile index - tile = index + x * 9 - if self.player.total_tiles(tile, self.defence.hand_34) == 4: - results.append(DefenceTile(tile, DefenceTile.SAFE)) + def find_tiles_to_discard(self, _): + all_kabe = self.find_all_kabe(self.defence.hand_34) + + results = [] + + for kabe in all_kabe: + # we don't use it for defence now + if kabe.kabe_type == KabeTile.PARTIAL_KABE: + continue - if self.player.total_tiles(tile, self.defence.hand_34) == 3: - results.append(DefenceTile(tile, DefenceTile.ALMOST_SAFE_TILE)) + tile = kabe.tile_34 + if self.player.total_tiles(tile, self.defence.hand_34) == 4: + results.append(DefenceTile(tile, DefenceTile.SAFE)) + + if self.player.total_tiles(tile, self.defence.hand_34) == 3: + results.append(DefenceTile(tile, DefenceTile.ALMOST_SAFE_TILE)) return results @@ -88,3 +132,18 @@ def _suits_tiles(self, tiles_34): suits[suit_index][simplified_tile] += total_tiles return suits + + +class KabeTile(object): + STRONG_KABE = 0 + WEAK_KABE = 1 + PARTIAL_KABE = 2 + + # how danger this tile is + tile_34 = None + # kabe type + kabe_type = None + + def __init__(self, tile_34, kabe_type): + self.tile_34 = tile_34 + self.kabe_type = kabe_type diff --git a/project/game/ai/first_version/defence/suji.py b/project/game/ai/first_version/defence/suji.py index eeea3acc..cd73bad7 100644 --- a/project/game/ai/first_version/defence/suji.py +++ b/project/game/ai/first_version/defence/suji.py @@ -1,5 +1,6 @@ # -*- 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 @@ -12,54 +13,90 @@ class Suji(Defence): # 3-6-9 THIRD_SUJI = 3 - def find_tiles_to_discard(self, players): - found_suji = [] - for player in players: - suji = [] - suits = [[], [], []] + def find_suji(self, safe_tiles_34): + suji = [] + suits = [[], [], []] + + # let's cast each tile to 0-8 presentation + for tile in safe_tiles_34: + if is_man(tile): + suits[0].append(simplify(tile)) + + if is_pin(tile): + suits[1].append(simplify(tile)) - # let's cast each tile to 0-8 presentation - safe_tiles = player.all_safe_tiles - for tile in safe_tiles: - if is_man(tile): - suits[0].append(simplify(tile)) + if is_sou(tile): + suits[2].append(simplify(tile)) - if is_pin(tile): - suits[1].append(simplify(tile)) + for x in range(0, 3): + simplified_tiles = suits[x] + base = x * 9 - if is_sou(tile): - suits[2].append(simplify(tile)) + # 1-4-7 + if 3 in simplified_tiles: + suji.append(self.FIRST_SUJI + base) - for x in range(0, 3): - simplified_tiles = suits[x] - base = x * 9 + # double 1-4-7 + if 0 in simplified_tiles and 6 in simplified_tiles: + suji.append(self.FIRST_SUJI + base) - # 1-4-7 - if 3 in simplified_tiles: - suji.append(self.FIRST_SUJI + base) + # 2-5-8 + if 4 in simplified_tiles: + suji.append(self.SECOND_SUJI + base) - # double 1-4-7 - if 0 in simplified_tiles and 6 in simplified_tiles: - suji.append(self.FIRST_SUJI + base) + # double 2-5-8 + if 1 in simplified_tiles and 7 in simplified_tiles: + suji.append(self.SECOND_SUJI + base) - # 2-5-8 - if 4 in simplified_tiles: - suji.append(self.SECOND_SUJI + base) + # 3-6-9 + if 5 in simplified_tiles: + suji.append(self.THIRD_SUJI + base) - # double 2-5-8 - if 1 in simplified_tiles and 7 in simplified_tiles: - suji.append(self.SECOND_SUJI + base) + # double 3-6-9 + if 2 in simplified_tiles and 8 in simplified_tiles: + suji.append(self.THIRD_SUJI + base) - # 3-6-9 - if 5 in simplified_tiles: - suji.append(self.THIRD_SUJI + base) + suji = list(set(suji)) - # double 3-6-9 - if 2 in simplified_tiles and 8 in simplified_tiles: - suji.append(self.THIRD_SUJI + base) + return suji - suji = list(set(suji)) + def find_suji_against_self(self, player): + discards_34 = list(set([x.value // 4 for x in player.discards])) + all_suji = self.find_suji(discards_34) + + result = [] + for suji in all_suji: + suji_temp = suji % 9 + base = suji - suji_temp - 1 + + if suji_temp == self.FIRST_SUJI: + result = [ + base + 1, + base + 4, + base + 7 + ] + + if suji_temp == self.SECOND_SUJI: + result = [ + base + 2, + base + 5, + base + 8 + ] + + if suji_temp == self.THIRD_SUJI: + result = [ + base + 3, + base + 6, + base + 9 + ] + + return result + + def find_tiles_to_discard(self, enemies): + found_suji = [] + for enemy in enemies: + suji = self.find_suji(enemy.all_safe_tiles) found_suji.append(suji) if not found_suji: diff --git a/project/game/ai/first_version/tests/tests_riichi.py b/project/game/ai/first_version/tests/tests_riichi.py index 9cccefdc..8b3f09fb 100644 --- a/project/game/ai/first_version/tests/tests_riichi.py +++ b/project/game/ai/first_version/tests/tests_riichi.py @@ -18,24 +18,6 @@ def test_dont_call_riichi_with_yaku_and_central_tanki_wait(self): self.assertEqual(self.player.can_call_riichi(), False) - def test_call_riichi_and_penchan_wait(self): - self._make_table() - - tiles = self._string_to_136_array(sou='11223', pin='234567', man='66') - self.player.init_hand(tiles) - self.player.draw_tile(self._string_to_136_tile(man='9')) - self.player.discard_tile() - - self.assertEqual(self.player.can_call_riichi(), True) - - self.table.add_discarded_tile(1, self._string_to_136_tile(sou='3'), False) - self.table.add_discarded_tile(1, self._string_to_136_tile(sou='3'), False) - self.table.add_discarded_tile(1, self._string_to_136_tile(sou='3'), False) - - # we are in karaten - # so we don't need to riichi it - self.assertEqual(self.player.can_call_riichi(), False) - def test_dont_call_riichi_expensive_damaten_with_yaku(self): self._make_table(dora_indicators=[ self._string_to_136_tile(man='7'), @@ -111,6 +93,38 @@ def test_always_call_daburi(self): self.assertEqual(self.player.can_call_riichi(), True) + def test_dont_call_karaten_tanki_riichi(self): + self._make_table() + + 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): + 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) + + self.player.draw_tile(self._string_to_136_tile(honors='3')) + self.player.discard_tile() + self.assertEqual(self.player.can_call_riichi(), False) + + def test_dont_call_karaten_ryanmen_riichi(self): + self._make_table(dora_indicators=[ + self._string_to_136_tile(man='1'), + self._string_to_136_tile(sou='1'), + self._string_to_136_tile(pin='1') + ]) + + tiles = self._string_to_136_array(man='222', sou='22278', pin='22789') + self.player.init_hand(tiles) + + for i 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) + + self.player.draw_tile(self._string_to_136_tile(honors='3')) + self.player.discard_tile() + self.assertEqual(self.player.can_call_riichi(), False) + def _make_table(self, dora_indicators=None): self.table = Table() self.table.count_of_remaining_tiles = 60 @@ -123,3 +137,33 @@ def _make_table(self, dora_indicators=None): if dora_indicators: for x in dora_indicators: self.table.add_dora_indicator(x) + + def test_call_riichi_penchan_with_suji(self): + self._make_table(dora_indicators=[ + self._string_to_136_tile(pin='1'), + ]) + + tiles = self._string_to_136_array(sou='11223', pin='234567', man='66') + self.player.init_hand(tiles) + self.player.draw_tile(self._string_to_136_tile(sou='6')) + self.player.discard_tile() + + self.assertEqual(self.player.can_call_riichi(), True) + + def test_call_riichi_tanki_with_kabe(self): + self._make_table(dora_indicators=[ + self._string_to_136_tile(pin='1'), + ]) + + for i in range(0, 3): + self.table.add_discarded_tile(1, self._string_to_136_tile(honors='1'), False) + + for i 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') + self.player.init_hand(tiles) + self.player.draw_tile(self._string_to_136_tile(honors='1')) + self.player.discard_tile() + + self.assertEqual(self.player.can_call_riichi(), True) From 2b887855b9e6c7f069c0054f61a155b8155f4792 Mon Sep 17 00:00:00 2001 From: Nihisil Date: Thu, 6 Sep 2018 23:24:04 +0800 Subject: [PATCH 058/126] Fix an issue with not correct tiles count calculation --- project/game/ai/first_version/damaten.py | 15 ++++++++++++--- project/game/ai/first_version/main.py | 6 ++---- project/game/player.py | 4 +++- 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/project/game/ai/first_version/damaten.py b/project/game/ai/first_version/damaten.py index 5d7e6cdb..3d62be39 100644 --- a/project/game/ai/first_version/damaten.py +++ b/project/game/ai/first_version/damaten.py @@ -18,7 +18,10 @@ def should_call_riichi(self): return False # don't call karaten riichi - count_tiles = self.player.ai.count_tiles(self.player.ai.waiting, TilesConverter.to_34_array(self.player.tiles)) + count_tiles = self.player.ai.count_tiles( + self.player.ai.waiting, + TilesConverter.to_34_array(self.player.closed_hand) + ) if count_tiles == 0: return False @@ -33,7 +36,10 @@ def should_call_riichi(self): return self._should_call_riichi_many_sided() def _should_call_riichi_one_sided(self): - count_tiles = self.player.ai.count_tiles(self.player.ai.waiting, TilesConverter.to_34_array(self.player.tiles)) + count_tiles = self.player.ai.count_tiles( + self.player.ai.waiting, + TilesConverter.to_34_array(self.player.closed_hand) + ) waiting = self.player.ai.waiting[0] hand_value = self.player.ai.estimate_hand_value(waiting, call_riichi=False) @@ -176,7 +182,10 @@ def _should_call_riichi_one_sided(self): return True def _should_call_riichi_many_sided(self): - count_tiles = self.player.ai.count_tiles(self.player.ai.waiting, TilesConverter.to_34_array(self.player.tiles)) + count_tiles = self.player.ai.count_tiles( + self.player.ai.waiting, + TilesConverter.to_34_array(self.player.closed_hand) + ) hand_costs = [] waits_with_yaku = 0 for waiting in self.player.ai.waiting: diff --git a/project/game/ai/first_version/main.py b/project/game/ai/first_version/main.py index a35628b5..e56f984b 100644 --- a/project/game/ai/first_version/main.py +++ b/project/game/ai/first_version/main.py @@ -111,12 +111,10 @@ def discard_tile(self, discard_tile): return self.process_discard_option(selected_tile, self.player.closed_hand) def process_discard_options_and_select_tile_to_discard(self, results, shanten, had_was_open=False): - tiles_34 = TilesConverter.to_34_array(self.player.tiles) - # we had to update tiles value there # because it is related with shanten number for result in results: - result.ukeire = self.count_tiles(result.waiting, tiles_34) + result.ukeire = self.count_tiles(result.waiting, TilesConverter.to_34_array(self.player.closed_hand)) result.calculate_value(shanten) # current strategy can affect on our discard options @@ -194,7 +192,7 @@ def calculate_outs(self, tiles, closed_hand, open_sets_34=None): shanten=shanten, tile_to_discard=hand_tile, waiting=waiting, - ukeire=self.count_tiles(waiting, tiles_34))) + ukeire=self.count_tiles(waiting, closed_tiles_34))) if is_agari: shanten = Shanten.AGARI_STATE diff --git a/project/game/player.py b/project/game/player.py index 1460fe3f..bf802b66 100644 --- a/project/game/player.py +++ b/project/game/player.py @@ -217,7 +217,9 @@ def total_tiles(self, tile, tiles_34): :param tiles_34: cached list of tiles (to not build it for each iteration) :return: int """ - return tiles_34[tile] + self.table.revealed_tiles[tile] + revealed_tiles = tiles_34[tile] + self.table.revealed_tiles[tile] + assert revealed_tiles <= 4, 'we have only 4 tiles in the game' + return revealed_tiles def format_hand_for_print(self, tile): hand_string = '{} + {}'.format( From c886866b71adba5c34dd760f4d218debc59ad0f3 Mon Sep 17 00:00:00 2001 From: bogachev-pa Date: Fri, 7 Sep 2018 00:32:16 +0300 Subject: [PATCH 059/126] add more tests also tests for task #88 --- .../tests/strategies/tests_yakuhai.py | 126 ++++++++++++++++++ .../ai/first_version/tests/tests_discards.py | 44 ++++++ .../ai/first_version/tests/tests_riichi.py | 57 ++++++-- 3 files changed, 214 insertions(+), 13 deletions(-) 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 1b438e98..bef3f5de 100644 --- a/project/game/ai/first_version/tests/strategies/tests_yakuhai.py +++ b/project/game/ai/first_version/tests/strategies/tests_yakuhai.py @@ -347,3 +347,129 @@ def test_keep_yakuhai_in_closed_hand(self): discard = self.player.discard_tile() self.assertNotEqual(self._to_string([discard]), '7z') + + # issue #88 + @unittest.expectedFailure + def test_atodzuke_keep_yakuhai_wait(self): + # make sure yakuhai strategy is activated by adding 3 doras to the hand + table = Table() + player = table.player + table.add_dora_indicator(self._string_to_136_tile(man='9')) + + tiles = self._string_to_136_array(man='11144', sou='567', pin='567', honors='77') + player.init_hand(tiles) + + meld = self._make_meld(Meld.PON, man='111') + player.add_called_meld(meld) + + # two of 4 man tiles are already out, so it would seem our wait is worse, but we know + # we must keep two pairs in order to be atodzuke tempai + table.add_discarded_tile(1, self._string_to_136_tile(man='4'), False) + table.add_discarded_tile(1, self._string_to_136_tile(man='4'), False) + + strategy = YakuhaiStrategy(BaseStrategy.YAKUHAI, player) + self.assertEqual(strategy.should_activate_strategy(player.tiles), True) + + player.draw_tile(self._string_to_136_tile(man='2')) + discarded_tile = player.discard_tile() + self.assertEqual(self._to_string([discarded_tile]), '2m') + + # issue #88 + @unittest.expectedFailure + def test_atodzuke_dont_destroy_second_pair(self): + # make sure yakuhai strategy is activated by adding 3 doras to the hand + table = Table() + player = table.player + table.add_dora_indicator(self._string_to_136_tile(man='9')) + + tiles = self._string_to_136_array(man='111445', sou='468', pin='56', honors='77') + player.init_hand(tiles) + + 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) + + tile = self._string_to_136_tile(man='4') + meld, _ = player.try_to_call_meld(tile, True) + self.assertEqual(meld, None) + + # but if we have backup pair it's ok + tiles = self._string_to_136_array(man='111445', sou='468', pin='88', honors='77') + player.init_hand(tiles) + + 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.assertNotEqual(meld, None) + + tile = self._string_to_136_tile(man='4') + meld, _ = player.try_to_call_meld(tile, True) + self.assertNotEqual(meld, None) + + # issue #88 + @unittest.expectedFailure + def test_atodzuke_dont_open_no_yaku_tempai(self): + # make sure yakuhai strategy is activated by adding 3 doras to the hand + table = Table() + player = table.player + table.add_dora_indicator(self._string_to_136_tile(man='9')) + + tiles = self._string_to_136_array(man='111445', sou='567', pin='56', honors='77') + player.init_hand(tiles) + + 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) + + tile = self._string_to_136_tile(man='4') + meld, _ = player.try_to_call_meld(tile, True) + self.assertEqual(meld, None) + + # 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) + + # issue #88 + @unittest.expectedFailure + def test_atodzuke_choose_hidden_syanpon(self): + # make sure yakuhai strategy is activated by adding 3 doras to the hand + table = Table() + player = table.player + table.add_dora_indicator(self._string_to_136_tile(man='9')) + + tiles = self._string_to_136_array(man='111678', sou='56678', honors='77') + player.init_hand(tiles) + + 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) + + for i 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')) + discarded_tile = player.discard_tile() + self.assertNotEqual(self._to_string([discarded_tile]), '6m') diff --git a/project/game/ai/first_version/tests/tests_discards.py b/project/game/ai/first_version/tests/tests_discards.py index cd21b669..8655d8d2 100644 --- a/project/game/ai/first_version/tests/tests_discards.py +++ b/project/game/ai/first_version/tests/tests_discards.py @@ -3,6 +3,7 @@ from mahjong.constants import EAST, SOUTH, WEST, NORTH, HAKU, HATSU, CHUN, FIVE_RED_SOU, FIVE_RED_PIN from mahjong.tests_mixin import TestMixin +from mahjong.meld import Meld from game.ai.discard import DiscardOption from game.ai.first_version.strategies.main import BaseStrategy @@ -280,3 +281,46 @@ def test_discard_tile_with_max_ukeire_second_level(self): discarded_tile = player.discard_tile() self.assertEqual(self._to_string([discarded_tile]), '3s') + + # There was a bug with count of live tiles that are used in melds, + # hence this test + def test_choose_best_option_with_melds(self): + table = Table() + player = table.player + table.has_aka_dora = False + + tiles = self._string_to_136_array(sou='245666789', honors='2266') + player.init_hand(tiles) + + meld = self._make_meld(Meld.PON, sou='666') + player.add_called_meld(meld) + meld = self._make_meld(Meld.CHI, sou='789') + player.add_called_meld(meld) + + player.draw_tile(self._string_to_136_tile(sou='5')) + + discarded_tile = player.discard_tile() + # we should discard best ukeire option here - 2s + self.assertEqual(self._to_string([discarded_tile]), '2s') + + def test_choose_best_wait_with_melds(self): + table = Table() + player = table.player + table.has_aka_dora = False + + tiles = self._string_to_136_array(sou='1222233455599') + player.init_hand(tiles) + + meld = self._make_meld(Meld.CHI, sou='123') + player.add_called_meld(meld) + meld = self._make_meld(Meld.PON, sou='222') + player.add_called_meld(meld) + meld = self._make_meld(Meld.PON, sou='555') + player.add_called_meld(meld) + + player.draw_tile(self._string_to_136_tile(sou='4')) + + discarded_tile = player.discard_tile() + # double-pairs wait becomes better, because it has 4 tiles to wait for + # against just 1 in ryanmen + self.assertEqual(self._to_string([discarded_tile]), '3s') diff --git a/project/game/ai/first_version/tests/tests_riichi.py b/project/game/ai/first_version/tests/tests_riichi.py index 8b3f09fb..5ad0e29b 100644 --- a/project/game/ai/first_version/tests/tests_riichi.py +++ b/project/game/ai/first_version/tests/tests_riichi.py @@ -2,11 +2,24 @@ import unittest from mahjong.tests_mixin import TestMixin +from mahjong.tile import Tile from game.table import Table class CallRiichiTestCase(unittest.TestCase, TestMixin): + def _make_table(self, dora_indicators=None): + self.table = Table() + self.table.count_of_remaining_tiles = 60 + self.player = self.table.player + self.player.scores = 25000 + + # with that we don't have daburi anymore + self.player.round_step = 1 + + if dora_indicators: + for x in dora_indicators: + self.table.add_dora_indicator(x) def test_dont_call_riichi_with_yaku_and_central_tanki_wait(self): self._make_table() @@ -125,19 +138,6 @@ def test_dont_call_karaten_ryanmen_riichi(self): self.player.discard_tile() self.assertEqual(self.player.can_call_riichi(), False) - def _make_table(self, dora_indicators=None): - self.table = Table() - self.table.count_of_remaining_tiles = 60 - self.player = self.table.player - self.player.scores = 25000 - - # with that we don't have daburi anymore - self.player.round_step = 1 - - if dora_indicators: - for x in dora_indicators: - self.table.add_dora_indicator(x) - def test_call_riichi_penchan_with_suji(self): self._make_table(dora_indicators=[ self._string_to_136_tile(pin='1'), @@ -167,3 +167,34 @@ def test_call_riichi_tanki_with_kabe(self): self.player.discard_tile() self.assertEqual(self.player.can_call_riichi(), True) + + def test_call_riichi_chiitoitsu_with_suji(self): + self._make_table(dora_indicators=[ + self._string_to_136_tile(man='1'), + ]) + + for i 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_dont_call_riichi_chiitoitsu_bad_wait(self): + self._make_table(dora_indicators=[ + self._string_to_136_tile(man='1'), + ]) + + for i 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') + self.player.init_hand(tiles) + + self.player.draw_tile(self._string_to_136_tile(honors='3')) + self.player.discard_tile() + self.assertEqual(self.player.can_call_riichi(), False) From a77301fe241d38c787b6be527bcf8e5af7846d51 Mon Sep 17 00:00:00 2001 From: bogachev-pa Date: Fri, 7 Sep 2018 03:36:18 +0300 Subject: [PATCH 060/126] refactor ukeire/ukeire2 filtration --- project/game/ai/first_version/main.py | 84 ++++++++++++++++----------- 1 file changed, 49 insertions(+), 35 deletions(-) diff --git a/project/game/ai/first_version/main.py b/project/game/ai/first_version/main.py index e56f984b..06a5e911 100644 --- a/project/game/ai/first_version/main.py +++ b/project/game/ai/first_version/main.py @@ -295,62 +295,60 @@ def choose_tile_to_discard(self, results: [DiscardOption]) -> DiscardOption: results_with_same_shanten = [x for x in results if x.shanten == first_option.shanten] possible_options = [first_option] - border_percentage = 20 + + ukeire_borders = self._choose_ukeire_borders(first_option, 20) + for discard_option in results_with_same_shanten: # there is no sense to check already chosen tile if discard_option.tile_to_discard == first_option.tile_to_discard: continue - # we don't need to select tiles almost dead waits - if discard_option.ukeire <= 2: - continue - - ukeire_borders = round((first_option.ukeire / 100) * border_percentage) - - if first_option.shanten == 0 and ukeire_borders < 2: - ukeire_borders = 2 - - if first_option.shanten == 1 and ukeire_borders < 4: - ukeire_borders = 4 - - if first_option.shanten >= 2 and ukeire_borders < 8: - ukeire_borders = 8 - # let's choose tiles that are close to the max ukeire tile if discard_option.ukeire >= first_option.ukeire - ukeire_borders: possible_options.append(discard_option) + # for 2 or 3 shanten hand we consider ukeire one step ahead if first_option.shanten == 2 or first_option.shanten == 3: sorting_field = 'ukeire_second' for x in possible_options: self.calculate_second_level_ukeire(x) - else: - sorting_field = 'ukeire' - possible_options = sorted(possible_options, key=lambda x: -getattr(x, sorting_field)) + possible_options = sorted(possible_options, key=lambda x: -getattr(x, sorting_field)) - filter_percentage = 20 - filtered_options = self._filter_list_by_percentage( - possible_options, - sorting_field, - filter_percentage - ) + filter_percentage = 20 + possible_options = self._filter_list_by_percentage( + possible_options, + sorting_field, + filter_percentage + ) + else: + sorting_field = 'ukeire' + possible_options = sorted(possible_options, key=lambda x: -getattr(x, sorting_field)) - tiles_without_dora = [x for x in filtered_options if x.count_of_dora == 0] + tiles_without_dora = [x for x in possible_options if x.count_of_dora == 0] # we have only dora candidates to discard if not tiles_without_dora: - min_dora = min([x.count_of_dora for x in filtered_options]) - min_dora_list = [x for x in filtered_options if x.count_of_dora == min_dora] + min_dora = min([x.count_of_dora for x in possible_options]) + min_dora_list = [x for x in possible_options if x.count_of_dora == min_dora] return sorted(min_dora_list, key=lambda x: -getattr(x, sorting_field))[0] - second_filter_percentage = 10 - filtered_options = self._filter_list_by_percentage( - tiles_without_dora, - sorting_field, - second_filter_percentage - ) + # we filter 10% of options, but if we use ukeire, we should also consider borders + if first_option.shanten == 2 or first_option.shanten == 3: + second_filter_percentage = 10 + filtered_options = self._filter_list_by_percentage( + tiles_without_dora, + sorting_field, + second_filter_percentage + ) + else: + best_option_without_dora = tiles_without_dora[0] + ukeire_borders = self._choose_ukeire_borders(best_option_without_dora, 10) + filtered_options = [best_option_without_dora] + for discard_option in tiles_without_dora: + if discard_option.ukeire >= best_option_without_dora.ukeire - ukeire_borders: + filtered_options.append(discard_option) closed_hand_34 = TilesConverter.to_34_array(self.player.closed_hand) isolated_tiles = [x for x in filtered_options if is_tile_strictly_isolated(closed_hand_34, x.tile_to_discard)] @@ -533,7 +531,8 @@ def enemy_players(self): """ return self.player.table.players[1:] - def _filter_list_by_percentage(self, items, attribute, percentage): + @staticmethod + def _filter_list_by_percentage(items, attribute, percentage): filtered_options = [] first_option = items[0] ukeire_borders = round((getattr(first_option, attribute) / 100) * percentage) @@ -541,3 +540,18 @@ def _filter_list_by_percentage(self, items, attribute, percentage): if getattr(x, attribute) >= getattr(first_option, attribute) - ukeire_borders: filtered_options.append(x) return filtered_options + + @staticmethod + def _choose_ukeire_borders(first_option, border_percentage): + ukeire_borders = round((first_option.ukeire / 100) * border_percentage) + + if first_option.shanten == 0 and ukeire_borders < 2: + ukeire_borders = 2 + + if first_option.shanten == 1 and ukeire_borders < 4: + ukeire_borders = 4 + + if first_option.shanten >= 2 and ukeire_borders < 8: + ukeire_borders = 8 + + return ukeire_borders From 4079b12265e957744e3516da6b53b6f560cc36de Mon Sep 17 00:00:00 2001 From: Nihisil Date: Fri, 7 Sep 2018 16:19:54 +0800 Subject: [PATCH 061/126] Fix one more issue with not correct tiles count calculation --- project/game/ai/first_version/defence/impossible_wait.py | 4 ++-- project/game/ai/first_version/defence/kabe.py | 6 +++--- project/game/ai/first_version/strategies/yakuhai.py | 5 +++-- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/project/game/ai/first_version/defence/impossible_wait.py b/project/game/ai/first_version/defence/impossible_wait.py index a6104f61..e1198f2a 100644 --- a/project/game/ai/first_version/defence/impossible_wait.py +++ b/project/game/ai/first_version/defence/impossible_wait.py @@ -14,10 +14,10 @@ def find_tiles_to_discard(self, _): results = [] for x in HONOR_INDICES: - if self.player.total_tiles(x, self.defence.hand_34) == 4: + if self.player.total_tiles(x, self.defence.closed_hand_34) == 4: results.append(DefenceTile(x, DefenceTile.SAFE)) - if self.player.total_tiles(x, self.defence.hand_34) == 3: + if self.player.total_tiles(x, self.defence.closed_hand_34) == 3: results.append(DefenceTile(x, DefenceTile.ALMOST_SAFE_TILE)) return results diff --git a/project/game/ai/first_version/defence/kabe.py b/project/game/ai/first_version/defence/kabe.py index 478211a3..e2c74c16 100644 --- a/project/game/ai/first_version/defence/kabe.py +++ b/project/game/ai/first_version/defence/kabe.py @@ -38,7 +38,7 @@ def find_all_kabe(self, tiles_34): kabe_tiles_weak = [] kabe_tiles_partial = [] - suits = self._suits_tiles(tiles_34) + suits = self._suits_tiles(self.defence.closed_hand_34) for x in range(0, 3): suit = suits[x] # "kabe" - 4 revealed tiles @@ -92,10 +92,10 @@ def find_tiles_to_discard(self, _): continue tile = kabe.tile_34 - if self.player.total_tiles(tile, self.defence.hand_34) == 4: + if self.player.total_tiles(tile, self.defence.closed_hand_34) == 4: results.append(DefenceTile(tile, DefenceTile.SAFE)) - if self.player.total_tiles(tile, self.defence.hand_34) == 3: + if self.player.total_tiles(tile, self.defence.closed_hand_34) == 3: results.append(DefenceTile(tile, DefenceTile.ALMOST_SAFE_TILE)) return results diff --git a/project/game/ai/first_version/strategies/yakuhai.py b/project/game/ai/first_version/strategies/yakuhai.py index 74e748e6..f0de35a2 100644 --- a/project/game/ai/first_version/strategies/yakuhai.py +++ b/project/game/ai/first_version/strategies/yakuhai.py @@ -28,6 +28,7 @@ def should_activate_strategy(self, tiles_136): tiles_34 = TilesConverter.to_34_array(tiles_136) player_hand_tiles_34 = TilesConverter.to_34_array(self.player.tiles) + player_closed_hand_tiles_34 = TilesConverter.to_34_array(self.player.closed_hand) self.valued_pairs = [x for x in self.player.valued_honors if player_hand_tiles_34[x] == 2] is_double_east_wind = len([x for x in self.valued_pairs if x == EAST]) == 2 @@ -47,7 +48,7 @@ def should_activate_strategy(self, tiles_136): for pair in self.valued_pairs: # we have valued pair in the hand and there are enough tiles # in the wall - if opportunity_to_meld_yakuhai or self.player.total_tiles(pair, player_hand_tiles_34) < 4: + if opportunity_to_meld_yakuhai or self.player.total_tiles(pair, player_closed_hand_tiles_34) < 4: has_valued_pair = True break @@ -83,7 +84,7 @@ 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_hand_tiles_34) == 4 and self.player.ai.shanten >= 1: + if opportunity_to_meld_yakuhai and self.player.total_tiles(pair, player_closed_hand_tiles_34) == 4 and self.player.ai.shanten >= 1: if pair not in self.last_chance_calls: self.last_chance_calls.append(pair) return True From c84a6229fdec51dc1f1f476d709f32f341a0745e Mon Sep 17 00:00:00 2001 From: Nihisil Date: Fri, 7 Sep 2018 16:28:40 +0800 Subject: [PATCH 062/126] Fix failed tests --- project/game/ai/first_version/defence/kabe.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/project/game/ai/first_version/defence/kabe.py b/project/game/ai/first_version/defence/kabe.py index e2c74c16..10ef81d3 100644 --- a/project/game/ai/first_version/defence/kabe.py +++ b/project/game/ai/first_version/defence/kabe.py @@ -38,7 +38,7 @@ def find_all_kabe(self, tiles_34): kabe_tiles_weak = [] kabe_tiles_partial = [] - suits = self._suits_tiles(self.defence.closed_hand_34) + suits = self._suits_tiles(tiles_34) for x in range(0, 3): suit = suits[x] # "kabe" - 4 revealed tiles @@ -82,7 +82,7 @@ def find_all_kabe(self, tiles_34): return kabe_tiles_unique def find_tiles_to_discard(self, _): - all_kabe = self.find_all_kabe(self.defence.hand_34) + all_kabe = self.find_all_kabe(self.defence.closed_hand_34) results = [] From 0b1aa9c3cc9fb5c97852750fcea4904242c0ce57 Mon Sep 17 00:00:00 2001 From: Nihisil Date: Fri, 7 Sep 2018 16:40:52 +0800 Subject: [PATCH 063/126] Fix crash with chiitoitsu like hand --- project/game/ai/first_version/main.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/project/game/ai/first_version/main.py b/project/game/ai/first_version/main.py index 06a5e911..2bda0b9c 100644 --- a/project/game/ai/first_version/main.py +++ b/project/game/ai/first_version/main.py @@ -165,9 +165,10 @@ def calculate_outs(self, tiles, closed_hand, open_sets_34=None): tiles_34[j] += 1 - key = '{},{}'.format( + key = '{},{},{}'.format( ''.join([str(x) for x in tiles_34]), - ';'.join([str(x) for x in open_sets_34]) + ';'.join([str(x) for x in open_sets_34]), + self.use_chitoitsu ) if key in self.hand_cache: From 51bb03c0b4f566c9c119c3a5373d50824b6390fd Mon Sep 17 00:00:00 2001 From: Nihisil Date: Fri, 7 Sep 2018 16:42:01 +0800 Subject: [PATCH 064/126] Fix --- project/game/ai/first_version/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/game/ai/first_version/main.py b/project/game/ai/first_version/main.py index 2bda0b9c..cc727eac 100644 --- a/project/game/ai/first_version/main.py +++ b/project/game/ai/first_version/main.py @@ -168,7 +168,7 @@ def calculate_outs(self, tiles, closed_hand, open_sets_34=None): key = '{},{},{}'.format( ''.join([str(x) for x in tiles_34]), ';'.join([str(x) for x in open_sets_34]), - self.use_chitoitsu + self.use_chitoitsu and 1 or 0 ) if key in self.hand_cache: From 42557481796480e0c07f4a4601b1e2f9e92f5c23 Mon Sep 17 00:00:00 2001 From: Nihisil Date: Fri, 7 Sep 2018 16:48:12 +0800 Subject: [PATCH 065/126] Rename damaten.py to riichi.py --- project/game/ai/first_version/main.py | 6 +++--- project/game/ai/first_version/{damaten.py => riichi.py} | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) rename project/game/ai/first_version/{damaten.py => riichi.py} (99%) diff --git a/project/game/ai/first_version/main.py b/project/game/ai/first_version/main.py index cc727eac..fcabf93f 100644 --- a/project/game/ai/first_version/main.py +++ b/project/game/ai/first_version/main.py @@ -14,7 +14,7 @@ from game.ai.base.main import InterfaceAI from game.ai.discard import DiscardOption -from game.ai.first_version.damaten import Damaten +from game.ai.first_version.riichi import Riichi from game.ai.first_version.defence.main import DefenceHandler from game.ai.first_version.strategies.chiitoitsu import ChiitoitsuStrategy from game.ai.first_version.strategies.chinitsu import ChinitsuStrategy @@ -56,7 +56,7 @@ def __init__(self, player): self.agari = Agari() self.shanten_calculator = Shanten() self.defence = DefenceHandler(player) - self.damaten = Damaten(player) + self.riichi = Riichi(player) self.hand_divider = HandDivider() self.finished_hand = HandCalculator() @@ -425,7 +425,7 @@ def estimate_hand_value(self, win_tile, tiles=None, call_riichi=False): return result def should_call_riichi(self): - return self.damaten.should_call_riichi() + return self.riichi.should_call_riichi() def should_call_kan(self, tile, open_kan, from_riichi=False): """ diff --git a/project/game/ai/first_version/damaten.py b/project/game/ai/first_version/riichi.py similarity index 99% rename from project/game/ai/first_version/damaten.py rename to project/game/ai/first_version/riichi.py index 3d62be39..ee12095e 100644 --- a/project/game/ai/first_version/damaten.py +++ b/project/game/ai/first_version/riichi.py @@ -4,7 +4,7 @@ from game.ai.first_version.defence.kabe import KabeTile -class Damaten: +class Riichi: def __init__(self, player): self.player = player From 152b2a32032056a76fdc7ff0ba3e9290508e8984 Mon Sep 17 00:00:00 2001 From: Nihisil Date: Fri, 7 Sep 2018 16:49:07 +0800 Subject: [PATCH 066/126] Decrease delay before meld call --- project/tenhou/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/tenhou/client.py b/project/tenhou/client.py index b014b21b..e24190c3 100644 --- a/project/tenhou/client.py +++ b/project/tenhou/client.py @@ -400,7 +400,7 @@ def start_game(self): meld, tile_to_discard = self.player.try_to_call_meld(tile, is_kamicha_discard) if meld: - self._random_sleep(2, 3) + self._random_sleep(1, 2) meld_tile = tile From f04b6f0c76a539884ba4a603a43ed875cf161d38 Mon Sep 17 00:00:00 2001 From: Nihisil Date: Fri, 7 Sep 2018 19:03:05 +0800 Subject: [PATCH 067/126] Fix ukeire2 calculation --- project/game/ai/first_version/main.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/project/game/ai/first_version/main.py b/project/game/ai/first_version/main.py index fcabf93f..dfb3879a 100644 --- a/project/game/ai/first_version/main.py +++ b/project/game/ai/first_version/main.py @@ -519,7 +519,9 @@ def calculate_second_level_ukeire(self, discard_option): self.player.meld_34_tiles ) results = [x for x in results if x.shanten == discard_option.shanten - 1] - sum_tiles += sum([x.ukeire for x in results]) + # let's take best ukeire here + if results: + sum_tiles += sorted(results, key=lambda x: -x.ukeire)[0].ukeire tiles.remove(wait) From 700095e45ccd35bd74dcfd6ad06712fc9959c8f4 Mon Sep 17 00:00:00 2001 From: Nihisil Date: Fri, 7 Sep 2018 19:57:03 +0800 Subject: [PATCH 068/126] Improve ukeire2 a little bit more --- project/game/ai/first_version/main.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/project/game/ai/first_version/main.py b/project/game/ai/first_version/main.py index dfb3879a..f34cf55e 100644 --- a/project/game/ai/first_version/main.py +++ b/project/game/ai/first_version/main.py @@ -505,13 +505,15 @@ def enemy_called_riichi(self, enemy_seat): self.in_defence = True def calculate_second_level_ukeire(self, discard_option): + closed_hand_34 = TilesConverter.to_34_array(self.player.closed_hand) + tiles = copy.copy(self.player.tiles) tiles.remove(discard_option.find_tile_in_hand(self.player.closed_hand)) sum_tiles = 0 - for wait in discard_option.waiting: - wait = wait * 4 - tiles.append(wait) + for wait_34 in discard_option.waiting: + wait_136 = wait_34 * 4 + tiles.append(wait_136) results, shanten = self.calculate_outs( tiles, @@ -519,11 +521,14 @@ def calculate_second_level_ukeire(self, discard_option): self.player.meld_34_tiles ) results = [x for x in results if x.shanten == discard_option.shanten - 1] + # let's take best ukeire here if results: - sum_tiles += sorted(results, key=lambda x: -x.ukeire)[0].ukeire + 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 - tiles.remove(wait) + tiles.remove(wait_136) discard_option.ukeire_second = sum_tiles From db252b3c1ffe4e36e43ef698e4893d6a66695ff9 Mon Sep 17 00:00:00 2001 From: Nihisil Date: Fri, 7 Sep 2018 20:59:53 +0800 Subject: [PATCH 069/126] Calculate ukeire2 in iishanten as well --- project/game/ai/first_version/main.py | 15 +++++++++++---- .../game/ai/first_version/tests/tests_discards.py | 11 +++++++++++ 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/project/game/ai/first_version/main.py b/project/game/ai/first_version/main.py index f34cf55e..f8898fd2 100644 --- a/project/game/ai/first_version/main.py +++ b/project/game/ai/first_version/main.py @@ -308,8 +308,7 @@ def choose_tile_to_discard(self, results: [DiscardOption]) -> DiscardOption: if discard_option.ukeire >= first_option.ukeire - ukeire_borders: possible_options.append(discard_option) - # for 2 or 3 shanten hand we consider ukeire one step ahead - if first_option.shanten == 2 or first_option.shanten == 3: + if first_option.shanten <= 3: sorting_field = 'ukeire_second' for x in possible_options: self.calculate_second_level_ukeire(x) @@ -335,14 +334,18 @@ def choose_tile_to_discard(self, results: [DiscardOption]) -> DiscardOption: return sorted(min_dora_list, key=lambda x: -getattr(x, sorting_field))[0] - # we filter 10% of options, but if we use ukeire, we should also consider borders - if first_option.shanten == 2 or first_option.shanten == 3: + # there is no need to filter tiles with one shanten + if first_option.shanten == 1: + filtered_options = tiles_without_dora + # we filter 10% of options here + elif first_option.shanten == 2 or first_option.shanten == 3: second_filter_percentage = 10 filtered_options = self._filter_list_by_percentage( tiles_without_dora, sorting_field, second_filter_percentage ) + # we should also consider borders for 3+ shanten hands else: best_option_without_dora = tiles_without_dora[0] ukeire_borders = self._choose_ukeire_borders(best_option_without_dora, 10) @@ -506,12 +509,16 @@ def enemy_called_riichi(self, enemy_seat): def calculate_second_level_ukeire(self, discard_option): closed_hand_34 = TilesConverter.to_34_array(self.player.closed_hand) + not_suitable_tiles = self.current_strategy and self.current_strategy.not_suitable_tiles or [] tiles = copy.copy(self.player.tiles) tiles.remove(discard_option.find_tile_in_hand(self.player.closed_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 + wait_136 = wait_34 * 4 tiles.append(wait_136) diff --git a/project/game/ai/first_version/tests/tests_discards.py b/project/game/ai/first_version/tests/tests_discards.py index 8655d8d2..2e382fa7 100644 --- a/project/game/ai/first_version/tests/tests_discards.py +++ b/project/game/ai/first_version/tests/tests_discards.py @@ -324,3 +324,14 @@ def test_choose_best_wait_with_melds(self): # double-pairs wait becomes better, because it has 4 tiles to wait for # against just 1 in ryanmen self.assertEqual(self._to_string([discarded_tile]), '3s') + + def test_discard_tile_with_better_wait_in_iishanten(self): + table = Table() + player = table.player + table.add_dora_indicator(self._string_to_136_tile(sou='4')) + + tiles = self._string_to_136_array(man='123567', pin='113788', sou='99') + player.init_hand(tiles) + + discarded_tile = player.discard_tile() + self.assertEqual(self._to_string([discarded_tile]), '8p') From c07c0039a352f840f24d50bbdab908d6e58965ff Mon Sep 17 00:00:00 2001 From: Nihisil Date: Fri, 7 Sep 2018 21:08:58 +0800 Subject: [PATCH 070/126] Fix for one shanten and filter ukeire borders --- project/game/ai/first_version/main.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/project/game/ai/first_version/main.py b/project/game/ai/first_version/main.py index f8898fd2..2a0cc7ff 100644 --- a/project/game/ai/first_version/main.py +++ b/project/game/ai/first_version/main.py @@ -297,7 +297,7 @@ def choose_tile_to_discard(self, results: [DiscardOption]) -> DiscardOption: possible_options = [first_option] - ukeire_borders = self._choose_ukeire_borders(first_option, 20) + ukeire_borders = self._choose_ukeire_borders(first_option, 20, 'ukeire') for discard_option in results_with_same_shanten: # there is no sense to check already chosen tile @@ -348,7 +348,7 @@ def choose_tile_to_discard(self, results: [DiscardOption]) -> DiscardOption: # we should also consider borders for 3+ shanten hands else: best_option_without_dora = tiles_without_dora[0] - ukeire_borders = self._choose_ukeire_borders(best_option_without_dora, 10) + ukeire_borders = self._choose_ukeire_borders(best_option_without_dora, 10, sorting_field) filtered_options = [best_option_without_dora] for discard_option in tiles_without_dora: if discard_option.ukeire >= best_option_without_dora.ukeire - ukeire_borders: @@ -557,8 +557,8 @@ def _filter_list_by_percentage(items, attribute, percentage): return filtered_options @staticmethod - def _choose_ukeire_borders(first_option, border_percentage): - ukeire_borders = round((first_option.ukeire / 100) * border_percentage) + 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: ukeire_borders = 2 From a400a35d2f0856f9dcff1af9dfb044eed1d18852 Mon Sep 17 00:00:00 2001 From: Nihisil Date: Fri, 7 Sep 2018 21:11:23 +0800 Subject: [PATCH 071/126] Fix --- project/game/ai/first_version/main.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/project/game/ai/first_version/main.py b/project/game/ai/first_version/main.py index 2a0cc7ff..ffd00b66 100644 --- a/project/game/ai/first_version/main.py +++ b/project/game/ai/first_version/main.py @@ -334,11 +334,8 @@ def choose_tile_to_discard(self, results: [DiscardOption]) -> DiscardOption: return sorted(min_dora_list, key=lambda x: -getattr(x, sorting_field))[0] - # there is no need to filter tiles with one shanten - if first_option.shanten == 1: - filtered_options = tiles_without_dora # we filter 10% of options here - elif first_option.shanten == 2 or first_option.shanten == 3: + if first_option.shanten == 2 or first_option.shanten == 3: second_filter_percentage = 10 filtered_options = self._filter_list_by_percentage( tiles_without_dora, From 0ddbc9e7d4dfc4c9cf8c6b9cdfbbc37cec83e2f7 Mon Sep 17 00:00:00 2001 From: Nihisil Date: Fri, 7 Sep 2018 21:19:54 +0800 Subject: [PATCH 072/126] Fix --- project/game/ai/first_version/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/game/ai/first_version/main.py b/project/game/ai/first_version/main.py index ffd00b66..75b07bb1 100644 --- a/project/game/ai/first_version/main.py +++ b/project/game/ai/first_version/main.py @@ -348,7 +348,7 @@ def choose_tile_to_discard(self, results: [DiscardOption]) -> DiscardOption: ukeire_borders = self._choose_ukeire_borders(best_option_without_dora, 10, sorting_field) filtered_options = [best_option_without_dora] for discard_option in tiles_without_dora: - if discard_option.ukeire >= best_option_without_dora.ukeire - ukeire_borders: + if getattr(discard_option, sorting_field) >= getattr(best_option_without_dora, sorting_field) - ukeire_borders: filtered_options.append(discard_option) closed_hand_34 = TilesConverter.to_34_array(self.player.closed_hand) From cc6dbd7139064bf8ca365fab1b2245855d9358f1 Mon Sep 17 00:00:00 2001 From: Nihisil Date: Fri, 7 Sep 2018 23:01:45 +0800 Subject: [PATCH 073/126] Fix bug with wrong tiles valuations --- project/game/ai/discard.py | 22 +++++++++++-- .../ai/first_version/tests/tests_discards.py | 33 +++++++++++++++++++ 2 files changed, 52 insertions(+), 3 deletions(-) diff --git a/project/game/ai/discard.py b/project/game/ai/discard.py index 5bce4fde..1e14332c 100644 --- a/project/game/ai/discard.py +++ b/project/game/ai/discard.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- from mahjong.constants import AKA_DORA_LIST from mahjong.tile import TilesConverter -from mahjong.utils import is_honor, simplify, plus_dora, is_aka_dora +from mahjong.utils import is_honor, simplify, plus_dora, is_aka_dora, is_sou, is_man, is_pin from game.ai.first_version.strategies.main import BaseStrategy @@ -116,13 +116,29 @@ def calculate_value(self, shanten=None): value += suit_tile_grades[simplified_tile] for indicator in self.player.table.dora_indicators: - simplified_indicator = simplify(indicator // 4) + indicator_34 = indicator // 4 + if is_honor(indicator_34): + continue + + # indicator and tile not from the same suit + if is_sou(indicator_34) and not is_sou(self.tile_to_discard): + continue + + # indicator and tile not from the same suit + if is_man(indicator_34) and not is_man(self.tile_to_discard): + continue + + # indicator and tile not from the same suit + if is_pin(indicator_34) and not is_pin(self.tile_to_discard): + continue + + simplified_indicator = simplify(indicator_34) simplified_dora = simplified_indicator + 1 # indicator is 9 man if simplified_dora == 9: simplified_dora = 0 - # tile close to the dora + # tile so close to the dora if simplified_tile + 1 == simplified_dora or simplified_tile - 1 == simplified_dora: value += DiscardOption.DORA_FIRST_NEIGHBOUR diff --git a/project/game/ai/first_version/tests/tests_discards.py b/project/game/ai/first_version/tests/tests_discards.py index 2e382fa7..2749fb72 100644 --- a/project/game/ai/first_version/tests/tests_discards.py +++ b/project/game/ai/first_version/tests/tests_discards.py @@ -184,6 +184,12 @@ def test_calculate_suit_tiles_value_and_dora(self): 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 test_discard_not_valuable_honor_first(self): table = Table() player = table.player @@ -335,3 +341,30 @@ def test_discard_tile_with_better_wait_in_iishanten(self): discarded_tile = player.discard_tile() self.assertEqual(self._to_string([discarded_tile]), '8p') + + def test_discard_tile_and_wrong_tiles_valuation(self): + """ + Bot wanted to discard 5m from the first hand, + because valuation for 2p was miscalculated (too high) + + Same issue with wrong valuation was with second hand + """ + table = Table() + player = table.player + table.add_dora_indicator(self._string_to_136_tile(honors='2')) + + tiles = self._string_to_136_array(man='445567', pin='245678', sou='67') + player.init_hand(tiles) + + discarded_tile = player.discard_tile() + self.assertEqual(self._to_string([discarded_tile]), '2p') + + table = Table() + player = table.player + table.add_dora_indicator(self._string_to_136_tile(man='5')) + + tiles = self._string_to_136_array(man='45667', pin='34677', sou='38', honors='22') + player.init_hand(tiles) + + discarded_tile = player.discard_tile() + self.assertEqual(self._to_string([discarded_tile]), '8s') From 3c12e9cfe351b573d3f513b099c7be4ed4542292 Mon Sep 17 00:00:00 2001 From: Nihisil Date: Fri, 7 Sep 2018 23:03:35 +0800 Subject: [PATCH 074/126] Remove not needed comments --- project/game/ai/first_version/defence/kabe.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/project/game/ai/first_version/defence/kabe.py b/project/game/ai/first_version/defence/kabe.py index 10ef81d3..339f7994 100644 --- a/project/game/ai/first_version/defence/kabe.py +++ b/project/game/ai/first_version/defence/kabe.py @@ -139,9 +139,7 @@ class KabeTile(object): WEAK_KABE = 1 PARTIAL_KABE = 2 - # how danger this tile is tile_34 = None - # kabe type kabe_type = None def __init__(self, tile_34, kabe_type): From a05365d36ce46a5bac98f610ff87a9e0f2018fec Mon Sep 17 00:00:00 2001 From: Nihisil Date: Fri, 7 Sep 2018 23:40:19 +0800 Subject: [PATCH 075/126] We don't need to calculate ukeire2 for tempai state --- project/game/ai/first_version/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/game/ai/first_version/main.py b/project/game/ai/first_version/main.py index 75b07bb1..9e9b9cb0 100644 --- a/project/game/ai/first_version/main.py +++ b/project/game/ai/first_version/main.py @@ -308,7 +308,7 @@ def choose_tile_to_discard(self, results: [DiscardOption]) -> DiscardOption: if discard_option.ukeire >= first_option.ukeire - ukeire_borders: possible_options.append(discard_option) - if first_option.shanten <= 3: + if first_option.shanten in [1, 2, 3]: sorting_field = 'ukeire_second' for x in possible_options: self.calculate_second_level_ukeire(x) From 4eb7c58a875a583bd6bb14e451707b4c31612f4c Mon Sep 17 00:00:00 2001 From: Nihisil Date: Sat, 8 Sep 2018 21:37:44 +0800 Subject: [PATCH 076/126] Issue #70. Basic support for decisions logger --- project/game/ai/discard.py | 5 +- project/game/ai/first_version/main.py | 123 ++++++++++++------ .../game/ai/first_version/strategies/main.py | 2 +- .../tests/strategies/tests_yakuhai.py | 2 +- project/game/player.py | 32 +++-- project/game/table.py | 46 +++++-- project/game/tests/tests_table.py | 18 +-- project/reproducer.py | 8 +- project/tenhou/client.py | 17 +-- project/tenhou/decoder.py | 4 +- project/tenhou/tests/tests_decoder.py | 2 +- project/utils/decisions_constants.py | 14 ++ project/utils/decisions_logger.py | 23 ++++ 13 files changed, 205 insertions(+), 91 deletions(-) create mode 100644 project/utils/decisions_constants.py create mode 100644 project/utils/decisions_logger.py diff --git a/project/game/ai/discard.py b/project/game/ai/discard.py index 1e14332c..38d0cf1f 100644 --- a/project/game/ai/discard.py +++ b/project/game/ai/discard.py @@ -53,8 +53,9 @@ def __init__(self, player, tile_to_discard, shanten, waiting, ukeire, danger=100 def __unicode__(self): tile_format_136 = TilesConverter.to_one_line_string([self.tile_to_discard*4]) - return 'tile={}, ukeire={}, ukeire2={}, valuation={}'.format( + return 'tile={}, shanten={}, ukeire={}, ukeire2={}, valuation={}'.format( tile_format_136, + self.shanten, self.ukeire, self.ukeire_second, self.valuation @@ -168,4 +169,4 @@ def calculate_value(self, shanten=None): if value == 0: self.had_to_be_discarded = True - self.valuation = value + self.valuation = int(value) diff --git a/project/game/ai/first_version/main.py b/project/game/ai/first_version/main.py index 9e9b9cb0..e1f2efca 100644 --- a/project/game/ai/first_version/main.py +++ b/project/game/ai/first_version/main.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- import copy -import logging +import utils.decisions_constants as log from mahjong.agari import Agari -from mahjong.constants import AKA_DORA_LIST +from mahjong.constants import AKA_DORA_LIST, DISPLAY_WINDS from mahjong.hand_calculating.divider import HandDivider from mahjong.hand_calculating.hand import HandCalculator from mahjong.hand_calculating.hand_config import HandConfig @@ -23,8 +23,7 @@ from game.ai.first_version.strategies.main import BaseStrategy from game.ai.first_version.strategies.tanyao import TanyaoStrategy from game.ai.first_version.strategies.yakuhai import YakuhaiStrategy - -logger = logging.getLogger('ai') +from utils.decisions_logger import DecisionsLogger class ImplementationAI(InterfaceAI): @@ -33,7 +32,7 @@ class ImplementationAI(InterfaceAI): agari = None shanten_calculator = None defence = None - damaten = None + riichi = None hand_divider = None finished_hand = None @@ -45,7 +44,6 @@ class ImplementationAI(InterfaceAI): current_strategy = None last_discard_option = None - use_chitoitsu = False hand_cache = {} @@ -62,23 +60,38 @@ def __init__(self, player): self.erase_state() + def erase_state(self): + self.shanten = 7 + self.ukeire = 0 + self.ukeire_second = 0 + self.in_defence = False + self.waiting = None + + self.current_strategy = None + self.last_discard_option = None + self.use_chitoitsu = False + + self.hand_cache = {} + def init_hand(self): + DecisionsLogger.debug(log.INIT_HAND, context=[ + 'Round wind: {}'.format(DISPLAY_WINDS[self.table.round_wind_tile]), + 'Player wind: {}'.format(DISPLAY_WINDS[self.player.player_wind]), + '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) + 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): - """ - :param tile: 136 tile format - :return: - """ + def draw_tile(self, tile_136): self.determine_strategy(self.player.tiles) - def discard_tile(self, discard_tile): + def discard_tile(self, discard_tile, print_log=True): # we called meld and we had discard tile that we wanted to discard if discard_tile is not None: if not self.last_discard_option: @@ -90,14 +103,18 @@ def discard_tile(self, discard_tile): self.player.closed_hand, self.player.meld_34_tiles) - selected_tile = self.process_discard_options_and_select_tile_to_discard(results, shanten) + selected_tile = self.process_discard_options_and_select_tile_to_discard( + results, + shanten, + print_log=print_log + ) # bot think that there is a threat on the table # and better to fold # if we can't find safe tiles, let's continue to build our hand if self.defence.should_go_to_defence_mode(selected_tile): if not self.in_defence: - logger.info('We decided to fold against other players') + DecisionsLogger.debug(log.DEFENCE_ACTIVATE) self.in_defence = True defence_tile = self.defence.try_to_find_safe_tile_to_discard(results) @@ -105,12 +122,12 @@ def discard_tile(self, discard_tile): return self.process_discard_option(defence_tile, self.player.closed_hand) else: if self.in_defence: - logger.info('Stop defence mode') + DecisionsLogger.debug(log.DEFENCE_DEACTIVATE) self.in_defence = False - return self.process_discard_option(selected_tile, self.player.closed_hand) + return self.process_discard_option(selected_tile, self.player.closed_hand, print_log=print_log) - def process_discard_options_and_select_tile_to_discard(self, results, shanten, had_was_open=False): + def process_discard_options_and_select_tile_to_discard(self, results, shanten, hand_was_open=False, print_log=True): # we had to update tiles value there # because it is related with shanten number for result in results: @@ -125,9 +142,9 @@ def process_discard_options_and_select_tile_to_discard(self, results, shanten, h shanten, False, None, - had_was_open) + hand_was_open) - return self.choose_tile_to_discard(results) + return self.choose_tile_to_discard(results, print_log=print_log) def calculate_outs(self, tiles, closed_hand, open_sets_34=None): """ @@ -212,8 +229,8 @@ def count_tiles(self, waiting, tiles_34): n += 4 - self.player.total_tiles(tile_34, tiles_34) return n - def try_to_call_meld(self, tile, is_kamicha_discard): - tiles_136 = self.player.tiles[:] + [tile] + def try_to_call_meld(self, tile_136, is_kamicha_discard): + tiles_136 = self.player.tiles[:] + [tile_136] self.determine_strategy(tiles_136) if not self.current_strategy: @@ -225,16 +242,22 @@ def try_to_call_meld(self, tile, is_kamicha_discard): self.player.meld_34_tiles, chiitoitsu=self.use_chitoitsu ) - if previous_shanten == Shanten.AGARI_STATE: - if not self.current_strategy.can_meld_into_agari(): - return None, None - meld, discard_option = self.current_strategy.try_to_call_meld(tile, is_kamicha_discard, tiles_136) + if previous_shanten == Shanten.AGARI_STATE and not self.current_strategy.can_meld_into_agari(): + return None, None + + meld, discard_option = self.current_strategy.try_to_call_meld(tile_136, is_kamicha_discard, tiles_136) tile_to_discard = None if discard_option: self.last_discard_option = discard_option tile_to_discard = discard_option.tile_to_discard + DecisionsLogger.debug(log.CALL_MELD, 'Try to call meld', context=[ + 'Hand: {}'.format(self.player.format_hand_for_print(tile_136)), + 'Meld: {}'.format(meld), + 'Discard after meld: {}'.format(discard_option) + ]) + return meld, tile_to_discard def determine_strategy(self, tiles_136): @@ -268,25 +291,31 @@ def determine_strategy(self, tiles_136): self.use_chitoitsu = self.current_strategy.type == BaseStrategy.CHIITOITSU if not old_strategy or self.current_strategy.type != old_strategy.type: - message = '{} switched to {} strategy'.format(self.player.name, self.current_strategy) - if old_strategy: - message += ' from {}'.format(old_strategy) - logger.debug(message) - logger.debug('With hand: {}'.format(TilesConverter.to_one_line_string(self.player.tiles))) + DecisionsLogger.debug( + log.STRATEGY_ACTIVATE, + context=self.current_strategy, + ) if not self.current_strategy and old_strategy: - logger.debug('{} gave up on {}'.format(self.player.name, old_strategy)) + DecisionsLogger.debug(log.STRATEGY_DROP, context=old_strategy) return self.current_strategy and True or False - def choose_tile_to_discard(self, results: [DiscardOption]) -> DiscardOption: + def choose_tile_to_discard(self, results: [DiscardOption], print_log=True) -> DiscardOption: """ Try to find best tile to discard, based on different rules """ had_to_be_discarded_tiles = [x for x in results if x.had_to_be_discarded] if had_to_be_discarded_tiles: - return sorted(had_to_be_discarded_tiles, key=lambda x: (x.shanten, -x.ukeire, x.valuation))[0] + results = sorted(had_to_be_discarded_tiles, key=lambda x: (x.shanten, -x.ukeire, x.valuation)) + DecisionsLogger.debug( + log.DISCARD_OPTIONS, + 'Discard marked tiles first', + results, + print_log=print_log + ) + return results[0] # remove needed tiles from discard options results = [x for x in results if not x.had_to_be_saved] @@ -296,9 +325,7 @@ def choose_tile_to_discard(self, results: [DiscardOption]) -> DiscardOption: results_with_same_shanten = [x for x in results if x.shanten == first_option.shanten] possible_options = [first_option] - ukeire_borders = self._choose_ukeire_borders(first_option, 20, 'ukeire') - for discard_option in results_with_same_shanten: # there is no sense to check already chosen tile if discard_option.tile_to_discard == first_option.tile_to_discard: @@ -329,6 +356,12 @@ def choose_tile_to_discard(self, results: [DiscardOption]) -> DiscardOption: # we have only dora candidates to discard if not tiles_without_dora: + DecisionsLogger.debug( + log.DISCARD_OPTIONS, + context=possible_options, + print_log=print_log + ) + min_dora = min([x.count_of_dora for x in possible_options]) min_dora_list = [x for x in possible_options if x.count_of_dora == min_dora] @@ -351,6 +384,12 @@ def choose_tile_to_discard(self, results: [DiscardOption]) -> DiscardOption: if getattr(discard_option, sorting_field) >= getattr(best_option_without_dora, sorting_field) - ukeire_borders: filtered_options.append(discard_option) + DecisionsLogger.debug( + log.DISCARD_OPTIONS, + context=possible_options, + print_log=print_log + ) + closed_hand_34 = TilesConverter.to_34_array(self.player.closed_hand) isolated_tiles = [x for x in filtered_options if is_tile_strictly_isolated(closed_hand_34, x.tile_to_discard)] # isolated tiles should be discarded first @@ -374,7 +413,13 @@ def choose_tile_to_discard(self, results: [DiscardOption]) -> DiscardOption: # we have only one candidate to discard with greater ukeire return first_option - def process_discard_option(self, discard_option, closed_hand, force_discard=False): + def process_discard_option(self, discard_option, closed_hand, force_discard=False, print_log=True): + if print_log: + DecisionsLogger.debug( + log.DISCARD, + context=discard_option + ) + self.waiting = discard_option.waiting self.player.ai.shanten = discard_option.shanten self.player.in_tempai = self.player.ai.shanten == 0 @@ -412,7 +457,7 @@ def estimate_hand_value(self, win_tile, tiles=None, call_riichi=False): config = HandConfig( is_riichi=call_riichi, player_wind=self.player.player_wind, - round_wind=self.player.table.round_wind, + round_wind=self.player.table.round_wind_tile, has_aka_dora=self.player.table.has_aka_dora, has_open_tanyao=self.player.table.has_open_tanyao ) @@ -501,8 +546,10 @@ def enemy_called_riichi(self, enemy_seat): it is affect open hand decisions :return: """ + if self.defence.should_go_to_defence_mode(): self.in_defence = True + DecisionsLogger.debug(log.DEFENCE_ACTIVATE) def calculate_second_level_ukeire(self, discard_option): closed_hand_34 = TilesConverter.to_34_array(self.player.closed_hand) diff --git a/project/game/ai/first_version/strategies/main.py b/project/game/ai/first_version/strategies/main.py index 041be4ce..74544806 100644 --- a/project/game/ai/first_version/strategies/main.py +++ b/project/game/ai/first_version/strategies/main.py @@ -251,7 +251,7 @@ def try_to_call_meld(self, tile, is_kamicha_discard, new_tiles): selected_tile = self.player.ai.process_discard_options_and_select_tile_to_discard( filtered_results, shanten, - had_was_open=True + hand_was_open=True ) return meld, selected_tile 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 bef3f5de..cb501be3 100644 --- a/project/game/ai/first_version/tests/strategies/tests_yakuhai.py +++ b/project/game/ai/first_version/tests/strategies/tests_yakuhai.py @@ -321,7 +321,7 @@ def test_open_double_south_wind(self): self.assertEqual(meld, None) # player is south and round is south - self.table.round_number = 5 + self.table.round_wind_number = 5 self.player.dealer_seat = 3 self.assertEqual(self.player.player_wind, SOUTH) diff --git a/project/game/player.py b/project/game/player.py index bf802b66..588bfdcc 100644 --- a/project/game/player.py +++ b/project/game/player.py @@ -1,11 +1,13 @@ # -*- coding: utf-8 -*- import logging import copy +import utils.decisions_constants as log from mahjong.constants import EAST, SOUTH, WEST, NORTH, CHUN, HAKU, HATSU from mahjong.meld import Meld from mahjong.tile import TilesConverter, Tile +from utils.decisions_logger import DecisionsLogger from utils.settings_handler import settings logger = logging.getLogger('tenhou') @@ -158,14 +160,24 @@ def init_hand(self, tiles): self.ai.init_hand() - def draw_tile(self, tile): - self.last_draw = tile - self.tiles.append(tile) + def draw_tile(self, tile_136): + DecisionsLogger.debug( + log.DRAW, + context=[ + 'Step: {}'.format(self.round_step), + 'Hand: {}'.format(self.format_hand_for_print(tile_136)), + 'In defence: {}'.format(self.ai.in_defence), + 'Current strategy: {}'.format(self.ai.current_strategy) + ] + ) + + self.last_draw = tile_136 + self.tiles.append(tile_136) # we need sort it to have a better string presentation self.tiles = sorted(self.tiles) - self.ai.draw_tile(tile) + self.ai.draw_tile(tile_136) def discard_tile(self, discard_tile=None): """ @@ -221,11 +233,11 @@ def total_tiles(self, tile, tiles_34): assert revealed_tiles <= 4, 'we have only 4 tiles in the game' return revealed_tiles - def format_hand_for_print(self, tile): - hand_string = '{} + {}'.format( - TilesConverter.to_one_line_string(self.closed_hand), - TilesConverter.to_one_line_string([tile]) - ) + def format_hand_for_print(self, tile_136=None): + hand_string = '{}'.format(TilesConverter.to_one_line_string(self.closed_hand)) + + if tile_136 is not None: + hand_string += ' + {}'.format(TilesConverter.to_one_line_string([tile_136])) melds = [] for item in self.melds: @@ -243,7 +255,7 @@ def closed_hand(self): @property def valued_honors(self): - return [CHUN, HAKU, HATSU, self.table.round_wind, self.player_wind] + return [CHUN, HAKU, HATSU, self.table.round_wind_tile, self.player_wind] class EnemyPlayer(PlayerInterface): diff --git a/project/game/table.py b/project/game/table.py index 6d01c5de..4b23a16d 100644 --- a/project/game/table.py +++ b/project/game/table.py @@ -16,7 +16,8 @@ class Table(object): dora_indicators = None dealer_seat = 0 - round_number = 0 + round_number = -1 + round_wind_number = 0 count_of_riichi_sticks = 0 count_of_honba_sticks = 0 @@ -38,15 +39,36 @@ def __init__(self): def __str__(self): dora_string = TilesConverter.to_one_line_string(self.dora_indicators) - return 'Round: {0}, Honba: {1}, Dora Indicators: {2}'.format(self.round_number, - self.count_of_honba_sticks, - dora_string) - def init_round(self, round_number, count_of_honba_sticks, count_of_riichi_sticks, - dora_indicator, dealer_seat, scores): + round_settings = { + EAST: ['e', 0], + SOUTH: ['s', 3], + WEST: ['w', 7] + }.get(self.round_wind_tile) + + round_string, round_diff = round_settings + display_round = '{}{}'.format(round_string, (self.round_wind_number + 1) - round_diff) + + return 'Round: {}, Honba: {}, Dora Indicators: {}'.format( + display_round, + self.count_of_honba_sticks, + dora_string + ) + + def init_round(self, + round_wind_number, + count_of_honba_sticks, + count_of_riichi_sticks, + dora_indicator, + dealer_seat, + scores): + + # we need it to properly display log for each round + self.round_number += 1 + self.meld_was_called = False self.dealer_seat = dealer_seat - self.round_number = round_number + self.round_wind_number = round_wind_number self.count_of_honba_sticks = count_of_honba_sticks self.count_of_riichi_sticks = count_of_riichi_sticks @@ -66,7 +88,7 @@ def init_round(self, round_number, count_of_honba_sticks, count_of_riichi_sticks # 13 - tiles in each player hand self.count_of_remaining_tiles = 136 - 14 - self.count_of_players * 13 - if round_number == 0 and count_of_honba_sticks == 0: + if round_wind_number == 0 and count_of_honba_sticks == 0: i = 0 seats = [0, 1, 2, 3] for player in self.players: @@ -155,12 +177,12 @@ def get_players_sorted_by_scores(self): return sorted(self.players, key=lambda x: (x.scores or 0, -x.first_seat), reverse=True) @property - def round_wind(self): - if self.round_number < 4: + def round_wind_tile(self): + if self.round_wind_number < 4: return EAST - elif 4 <= self.round_number < 8: + elif 4 <= self.round_wind_number < 8: return SOUTH - elif 8 <= self.round_number < 12: + elif 8 <= self.round_wind_number < 12: return WEST else: return NORTH diff --git a/project/game/tests/tests_table.py b/project/game/tests/tests_table.py index ee611fbd..b1b4f905 100644 --- a/project/game/tests/tests_table.py +++ b/project/game/tests/tests_table.py @@ -19,16 +19,16 @@ def test_init_hand(self): def test_init_round(self): table = Table() - round_number = 4 + round_wind_number = 4 count_of_honba_sticks = 2 count_of_riichi_sticks = 3 dora_indicator = 126 dealer = 3 scores = [250, 250, 250, 250] - table.init_round(round_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_number, round_number) + self.assertEqual(table.round_wind_number, round_wind_number) self.assertEqual(table.count_of_honba_sticks, count_of_honba_sticks) self.assertEqual(table.count_of_riichi_sticks, count_of_riichi_sticks) self.assertEqual(table.dora_indicators[0], dora_indicator) @@ -38,7 +38,7 @@ def test_init_round(self): dealer = 2 table.player.in_tempai = True table.player.in_riichi = True - table.init_round(round_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) @@ -176,16 +176,16 @@ def test_round_wind(self): table = Table() table.init_round(0, 0, 0, 0, 0, []) - self.assertEqual(table.round_wind, EAST) + self.assertEqual(table.round_wind_tile, EAST) table.init_round(3, 0, 0, 0, 0, []) - self.assertEqual(table.round_wind, EAST) + self.assertEqual(table.round_wind_tile, EAST) table.init_round(7, 0, 0, 0, 0, []) - self.assertEqual(table.round_wind, SOUTH) + self.assertEqual(table.round_wind_tile, SOUTH) table.init_round(11, 0, 0, 0, 0, []) - self.assertEqual(table.round_wind, WEST) + self.assertEqual(table.round_wind_tile, WEST) table.init_round(12, 0, 0, 0, 0, []) - self.assertEqual(table.round_wind, NORTH) + self.assertEqual(table.round_wind_tile, NORTH) diff --git a/project/reproducer.py b/project/reproducer.py index 97e1e083..e2f8c24f 100644 --- a/project/reproducer.py +++ b/project/reproducer.py @@ -73,7 +73,7 @@ def reproduce(self, dry_run=False): shifted_scores.append(values['scores'][self._normalize_position(x, self.player_position)]) table.init_round( - values['round_number'], + values['round_wind_number'], values['count_of_honba_sticks'], values['count_of_riichi_sticks'], values['dora_indicator'], @@ -136,7 +136,7 @@ def _normalize_position(self, who, from_who): def _parse_url(self, log_url): temp = log_url.split('?')[1].split('&') - log_id, player, round_number = '', 0, 0 + log_id, player, round_wind = '', 0, 0 for item in temp: item = item.split('=') if 'log' == item[0]: @@ -144,8 +144,8 @@ def _parse_url(self, log_url): if 'tw' == item[0]: player = int(item[1]) if 'ts' == item[0]: - round_number = int(item[1]) - return log_id, player, round_number + round_wind = int(item[1]) + return log_id, player, round_wind def _download_log_content(self, log_id): """ diff --git a/project/tenhou/client.py b/project/tenhou/client.py index e24190c3..da641109 100644 --- a/project/tenhou/client.py +++ b/project/tenhou/client.py @@ -7,7 +7,6 @@ from time import sleep from urllib.parse import quote -from mahjong.constants import DISPLAY_WINDS from mahjong.meld import Meld from mahjong.tile import TilesConverter @@ -223,8 +222,9 @@ def start_game(self): for message in messages: if ''.format(discarded_tile)) diff --git a/project/tenhou/decoder.py b/project/tenhou/decoder.py index f33a4f3e..1c54a2e1 100644 --- a/project/tenhou/decoder.py +++ b/project/tenhou/decoder.py @@ -61,7 +61,7 @@ def parse_initial_values(self, message): seed = self.get_attribute_content(message, 'seed').split(',') seed = [int(i) for i in seed] - round_number = seed[0] + round_wind_number = seed[0] count_of_honba_sticks = seed[1] count_of_riichi_sticks = seed[2] dora_indicator = seed[5] @@ -71,7 +71,7 @@ def parse_initial_values(self, message): scores = [int(i) for i in scores] return { - 'round_number': round_number, + 'round_wind_number': round_wind_number, 'count_of_honba_sticks': count_of_honba_sticks, 'count_of_riichi_sticks': count_of_riichi_sticks, 'dora_indicator': dora_indicator, diff --git a/project/tenhou/tests/tests_decoder.py b/project/tenhou/tests/tests_decoder.py index 839ec659..28ace4d3 100644 --- a/project/tenhou/tests/tests_decoder.py +++ b/project/tenhou/tests/tests_decoder.py @@ -12,7 +12,7 @@ def test_parse_initial_round_values(self): 'hai="30,67,44,21,133,123,87,69,36,34,94,4,128"/>' values = decoder.parse_initial_values(message) - self.assertEqual(values['round_number'], 0) + self.assertEqual(values['round_wind_number'], 0) self.assertEqual(values['count_of_honba_sticks'], 2) self.assertEqual(values['count_of_riichi_sticks'], 3) self.assertEqual(values['dora_indicator'], 126) diff --git a/project/utils/decisions_constants.py b/project/utils/decisions_constants.py new file mode 100644 index 00000000..8ab28785 --- /dev/null +++ b/project/utils/decisions_constants.py @@ -0,0 +1,14 @@ +DRAW = 'draw' + +DISCARD_OPTIONS = 'discard_options' +DISCARD = 'discard' + +DEFENCE_ACTIVATE = 'activate_defence' +DEFENCE_DEACTIVATE = 'deactivate_defence' + +STRATEGY_ACTIVATE = 'activate_strategy' +STRATEGY_DROP = 'drop_strategy' + +INIT_HAND = 'init_hand' + +CALL_MELD = 'meld' diff --git a/project/utils/decisions_logger.py b/project/utils/decisions_logger.py new file mode 100644 index 00000000..a4be89c2 --- /dev/null +++ b/project/utils/decisions_logger.py @@ -0,0 +1,23 @@ +import logging + +logger = logging.getLogger('ai') + + +class DecisionsLogger: + + @staticmethod + def debug(message_id, message='', context=None, print_log=True): + if not print_log: + return + + logger.debug('id={}'.format(message_id)) + + if message: + logger.debug(message) + + if context: + if type(context) == list: + for x in context: + logger.debug(x) + else: + logger.debug(context) From 38de79c60c7bd62c84cc8cc428e743c19384870c Mon Sep 17 00:00:00 2001 From: bogachev-pa Date: Sat, 8 Sep 2018 16:45:57 +0300 Subject: [PATCH 077/126] fix tiles counting for strategy activation decisions (#89) --- project/game/ai/first_version/strategies/chinitsu.py | 4 +++- project/game/ai/first_version/strategies/tanyao.py | 4 ++-- .../first_version/tests/strategies/tests_chinitsu.py | 12 ++++++++++++ .../first_version/tests/strategies/tests_yakuhai.py | 6 ++++++ 4 files changed, 23 insertions(+), 3 deletions(-) diff --git a/project/game/ai/first_version/strategies/chinitsu.py b/project/game/ai/first_version/strategies/chinitsu.py index 6130f838..7de7074c 100644 --- a/project/game/ai/first_version/strategies/chinitsu.py +++ b/project/game/ai/first_version/strategies/chinitsu.py @@ -24,7 +24,9 @@ def should_activate_strategy(self, tiles_136): if not result: return False - tiles_34 = TilesConverter.to_34_array(tiles_136) + # when making decisions about chinitsu, we should consider + # the state of our own hand, + tiles_34 = TilesConverter.to_34_array(self.player.tiles) suits = count_tiles_by_suits(tiles_34) suits = [x for x in suits if x['name'] != 'honor'] diff --git a/project/game/ai/first_version/strategies/tanyao.py b/project/game/ai/first_version/strategies/tanyao.py index b8ea5807..863002e4 100644 --- a/project/game/ai/first_version/strategies/tanyao.py +++ b/project/game/ai/first_version/strategies/tanyao.py @@ -22,10 +22,10 @@ def should_activate_strategy(self, tiles_136): if not result: return False - tiles = TilesConverter.to_34_array(tiles_136) + tiles = TilesConverter.to_34_array(self.player.tiles) closed_hand_34 = TilesConverter.to_34_array(self.player.closed_hand) - isolated_tiles = [x // 4 for x in tiles_136 + isolated_tiles = [x // 4 for x in self.player.tiles if is_tile_strictly_isolated(closed_hand_34, x // 4) or is_honor(x // 4)] count_of_terminal_pon_sets = 0 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 15da6f33..0e809107 100644 --- a/project/game/ai/first_version/tests/strategies/tests_chinitsu.py +++ b/project/game/ai/first_version/tests/strategies/tests_chinitsu.py @@ -89,6 +89,18 @@ def test_should_activate_strategy(self): 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') + player.init_hand(tiles) + # plus one tile to open hand + tiles = self._string_to_136_array(sou='112223578', man='57', pin='466') + self.assertEqual(strategy.should_activate_strategy(tiles), False) + # but now let's init hand with these tiles, we can now slowly move to chinitsu + tiles = self._string_to_136_array(sou='112223578', man='57', pin='466') + player.init_hand(tiles) + self.assertEqual(strategy.should_activate_strategy(tiles), True) + def test_suitable_tiles(self): table = Table() player = table.player 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 bef3f5de..f631fa74 100644 --- a/project/game/ai/first_version/tests/strategies/tests_yakuhai.py +++ b/project/game/ai/first_version/tests/strategies/tests_yakuhai.py @@ -35,6 +35,12 @@ def test_should_activate_strategy(self): self.player.init_hand(tiles) self.assertEqual(strategy.should_activate_strategy(self.player.tiles), False) + # don't count tile discarded by other player as our pair + tiles = self._string_to_136_array(sou='12355689', man='899', honors='25') + self.player.init_hand(tiles) + tiles = self._string_to_136_array(sou='12355689', man='899', honors='255') + self.assertEqual(strategy.should_activate_strategy(tiles), False) + def test_dont_activate_strategy_if_we_dont_have_enough_tiles_in_the_wall(self): strategy = YakuhaiStrategy(BaseStrategy.YAKUHAI, self.player) From 1b391a38620c326bb533184b8491a17311c9f0c7 Mon Sep 17 00:00:00 2001 From: bogachev-pa Date: Sun, 9 Sep 2018 00:38:14 +0300 Subject: [PATCH 078/126] don't call kan if it reduces ukeire (#49) --- project/game/ai/first_version/main.py | 132 +++++++++++------- .../game/ai/first_version/tests/tests_ai.py | 39 ++++++ 2 files changed, 119 insertions(+), 52 deletions(-) diff --git a/project/game/ai/first_version/main.py b/project/game/ai/first_version/main.py index e1f2efca..46339f99 100644 --- a/project/game/ai/first_version/main.py +++ b/project/game/ai/first_version/main.py @@ -146,6 +146,44 @@ def process_discard_options_and_select_tile_to_discard(self, results, shanten, h return self.choose_tile_to_discard(results, print_log=print_log) + def calculate_waits(self, tiles_34, open_sets_34=None): + """ + :param tiles_34: array of tiles in 34 formant, 13 of them (this is important) + :param open_sets_34: array of array with tiles in 34 format + :return: array of waits in 34 format and number of shanten + """ + shanten = self.shanten_calculator.calculate_shanten(tiles_34, open_sets_34, chiitoitsu=self.use_chitoitsu) + waiting = [] + for j in range(0, 34): + if tiles_34[j] == 4: + continue + + tiles_34[j] += 1 + + key = '{},{},{}'.format( + ''.join([str(x) for x in tiles_34]), + ';'.join([str(x) for x in open_sets_34]), + self.use_chitoitsu and 1 or 0 + ) + + if key in self.hand_cache: + new_shanten = self.hand_cache[key] + else: + new_shanten = self.shanten_calculator.calculate_shanten( + tiles_34, + open_sets_34, + chiitoitsu=self.use_chitoitsu + ) + self.hand_cache[key] = new_shanten + + if new_shanten == shanten - 1: + waiting.append(j) + + tiles_34[j] -= 1 + + return waiting, shanten + + def calculate_outs(self, tiles, closed_hand, open_sets_34=None): """ :param tiles: array of tiles in 136 format @@ -166,43 +204,7 @@ def calculate_outs(self, tiles, closed_hand, open_sets_34=None): continue tiles_34[hand_tile] -= 1 - - shanten = self.shanten_calculator.calculate_shanten(tiles_34, open_sets_34, chiitoitsu=self.use_chitoitsu) - - waiting = [] - for j in range(0, 34): - if tiles_34[j] == 4: - continue - - # agari is a special case, we are forced to make number - # of shanten larger, so we don't skip any tiles - # in the end we let the strategy decide what to do if agari without yaku happened - if not is_agari and hand_tile == j: - continue - - tiles_34[j] += 1 - - key = '{},{},{}'.format( - ''.join([str(x) for x in tiles_34]), - ';'.join([str(x) for x in open_sets_34]), - self.use_chitoitsu and 1 or 0 - ) - - if key in self.hand_cache: - new_shanten = self.hand_cache[key] - else: - new_shanten = self.shanten_calculator.calculate_shanten( - tiles_34, - open_sets_34, - chiitoitsu=self.use_chitoitsu - ) - self.hand_cache[key] = new_shanten - - if new_shanten == shanten - 1: - waiting.append(j) - - tiles_34[j] -= 1 - + waiting, shanten = self.calculate_waits(tiles_34, open_sets_34) tiles_34[hand_tile] += 1 if waiting: @@ -517,23 +519,49 @@ def should_call_kan(self, tile, open_kan, from_riichi=False): if tile_34 in meld: return Meld.CHANKAN + melds_34 = copy.copy(self.player.meld_34_tiles) + tiles = copy.copy(self.player.tiles) + closed_hand_tiles = copy.copy(self.player.closed_hand) + # we can try to call closed meld if closed_hand_34[tile_34] == 3: - if not open_kan and not from_riichi: - tiles_34[tile_34] += 1 - - melds = self.player.meld_34_tiles - previous_shanten = self.shanten_calculator.calculate_shanten(tiles_34, melds, chiitoitsu=self.use_chitoitsu) - - if not open_kan and not from_riichi: - tiles_34[tile_34] -= 1 - - melds += [[tile_34, tile_34, tile_34]] - new_shanten = self.shanten_calculator.calculate_shanten(tiles_34, melds, chiitoitsu=self.use_chitoitsu) - - # called kan will not ruin our hand - if new_shanten <= previous_shanten: - return Meld.KAN + if open_kan or from_riichi: + # this 4 tiles can only be used in kan, no other options + previous_waiting, previous_shanten = self.calculate_waits(tiles_34, melds_34) + previous_waits_cnt = self.count_tiles(previous_waiting, closed_hand_34) + + # shanten calculator doesn't like working with kans, so we pretend it's a pon + melds_34 += [[tile_34, tile_34, tile_34]] + closed_hand_34[tile_34] = 0 + + new_waiting, new_shanten = self.calculate_waits(tiles_34, melds_34) + new_waits_cnt = self.count_tiles(new_waiting, closed_hand_34) + else: + # if we can use or tile in the hand for the forms other than KAN + tiles.append(tile) + closed_hand_tiles.append(tile) + closed_hand_34[tile_34] += 1 + + previous_results, previous_shanten = self.calculate_outs(tiles, closed_hand_tiles, melds_34) + previous_results = [x for x in previous_results if x.shanten == previous_shanten] + previous_waits_cnt = sorted(previous_results, key=lambda x: -x.ukeire)[0].ukeire + + # shanten calculator doesn't like working with kans, so we pretend it's a pon + closed_hand_34[tile_34] = 0 + melds_34 += [[tile_34, tile_34, tile_34]] + + new_waiting, new_shanten = self.calculate_waits(tiles_34, melds_34) + new_waits_cnt = self.count_tiles(new_waiting, closed_hand_34) + + # it is not possible to reduce number of shanten by calling a kan + assert new_shanten >= previous_shanten + + # if shanten number is the same, we should only call kan if ukeire didn't become worse + if new_shanten == previous_shanten: + # we cannot improve ukeire by calling kan (not considering the tile we drew from the dead wall) + assert new_waits_cnt <= previous_waits_cnt + if new_waits_cnt == previous_waits_cnt: + return Meld.KAN return None diff --git a/project/game/ai/first_version/tests/tests_ai.py b/project/game/ai/first_version/tests/tests_ai.py index a7155c89..c9147048 100644 --- a/project/game/ai/first_version/tests/tests_ai.py +++ b/project/game/ai/first_version/tests/tests_ai.py @@ -361,3 +361,42 @@ def test_closed_kan_and_not_necessary_call(self): tile = self._string_to_136_tile(sou='9') self.assertEqual(player.should_call_kan(tile, False), None) + + def test_closed_kan_same_shanten_bad_ukeire(self): + """ + Bot tried to call closed kan with 4557888899m2z + 333m melded hand + Shanten number is the same, but ukeire becomes much worse + """ + table = Table() + player = table.player + + table.add_dora_indicator(self._string_to_136_tile(honors='2')) + table.add_dora_indicator(self._string_to_136_tile(honors='4')) + + table.count_of_remaining_tiles = 10 + + tiles = self._string_to_136_array(man='333455788899', honors='3') + player.init_hand(tiles) + player.melds.append(self._make_meld(Meld.PON, man='333')) + + tile = self._string_to_136_tile(man='8') + + self.assertEqual(player.should_call_kan(tile, False), None) + + + def test_closed_kan_same_shanten_same_ukeire(self): + table = Table() + player = table.player + + table.add_dora_indicator(self._string_to_136_tile(honors='2')) + table.add_dora_indicator(self._string_to_136_tile(honors='4')) + + table.count_of_remaining_tiles = 10 + + tiles = self._string_to_136_array(man='3334557889', honors='333') + player.init_hand(tiles) + player.melds.append(self._make_meld(Meld.PON, man='333')) + + tile = self._string_to_136_tile(honors='3') + + self.assertEqual(player.should_call_kan(tile, False), Meld.KAN) From 630fd61040ad6edfc6be1b7f1910cb1a13e7d3cb Mon Sep 17 00:00:00 2001 From: Nihisil Date: Mon, 10 Sep 2018 16:05:41 +0800 Subject: [PATCH 079/126] Refacatoring. Move all hand building logic to separate file --- project/game/ai/first_version/hand_builder.py | 353 ++++++++++++++++++ project/game/ai/first_version/main.py | 352 +---------------- project/game/ai/first_version/riichi.py | 6 +- .../game/ai/first_version/strategies/main.py | 4 +- .../game/ai/first_version/tests/tests_ai.py | 16 +- .../ai/first_version/tests/tests_defence.py | 8 +- 6 files changed, 382 insertions(+), 357 deletions(-) create mode 100644 project/game/ai/first_version/hand_builder.py diff --git a/project/game/ai/first_version/hand_builder.py b/project/game/ai/first_version/hand_builder.py new file mode 100644 index 00000000..1f9b8ad2 --- /dev/null +++ b/project/game/ai/first_version/hand_builder.py @@ -0,0 +1,353 @@ +import copy + +from mahjong.constants import AKA_DORA_LIST +from mahjong.shanten import Shanten +from mahjong.tile import TilesConverter +from mahjong.utils import is_tile_strictly_isolated + +import utils.decisions_constants as log +from game.ai.discard import DiscardOption +from utils.decisions_logger import DecisionsLogger + + +class HandBuilder: + player = None + ai = None + + def __init__(self, player, ai): + self.player = player + self.ai = ai + + def discard_tile(self, discard_tile, print_log=True): + # we called meld and we had discard tile that we wanted to discard + if discard_tile is not None: + if not self.ai.last_discard_option: + return discard_tile + + return self.process_discard_option(self.ai.last_discard_option, self.player.closed_hand, True) + + results, shanten = self.calculate_outs(self.player.tiles, + self.player.closed_hand, + self.player.meld_34_tiles) + + selected_tile = self.process_discard_options_and_select_tile_to_discard( + results, + shanten, + print_log=print_log + ) + + # bot think that there is a threat on the table + # and better to fold + # if we can't find safe tiles, let's continue to build our hand + if self.ai.defence.should_go_to_defence_mode(selected_tile): + if not self.ai.in_defence: + DecisionsLogger.debug(log.DEFENCE_ACTIVATE) + self.ai.in_defence = True + + defence_tile = self.ai.defence.try_to_find_safe_tile_to_discard(results) + if defence_tile: + return self.process_discard_option(defence_tile, self.player.closed_hand) + else: + if self.ai.in_defence: + DecisionsLogger.debug(log.DEFENCE_DEACTIVATE) + self.ai.in_defence = False + + return self.process_discard_option(selected_tile, self.player.closed_hand, print_log=print_log) + + def process_discard_options_and_select_tile_to_discard(self, results, shanten, hand_was_open=False, print_log=True): + # we had to update tiles value there + # because it is related with shanten number + for result in results: + result.ukeire = self.count_tiles(result.waiting, TilesConverter.to_34_array(self.player.closed_hand)) + result.calculate_value(shanten) + + # current strategy can affect on our discard options + # so, don't use strategy specific choices for calling riichi + if self.ai.current_strategy: + results = self.ai.current_strategy.determine_what_to_discard(self.player.closed_hand, + results, + shanten, + False, + None, + hand_was_open) + + return self.choose_tile_to_discard(results, print_log=print_log) + + def calculate_waits(self, tiles_34, open_sets_34=None): + """ + :param tiles_34: array of tiles in 34 formant, 13 of them (this is important) + :param open_sets_34: array of array with tiles in 34 format + :return: array of waits in 34 format and number of shanten + """ + shanten = self.ai.shanten_calculator.calculate_shanten(tiles_34, open_sets_34, chiitoitsu=self.ai.use_chitoitsu) + waiting = [] + for j in range(0, 34): + if tiles_34[j] == 4: + continue + + tiles_34[j] += 1 + + key = '{},{},{}'.format( + ''.join([str(x) for x in tiles_34]), + ';'.join([str(x) for x in open_sets_34]), + self.ai.use_chitoitsu and 1 or 0 + ) + + if key in self.ai.hand_cache: + new_shanten = self.ai.hand_cache[key] + else: + new_shanten = self.ai.shanten_calculator.calculate_shanten( + tiles_34, + open_sets_34, + chiitoitsu=self.ai.use_chitoitsu + ) + self.ai.hand_cache[key] = new_shanten + + if new_shanten == shanten - 1: + waiting.append(j) + + tiles_34[j] -= 1 + + return waiting, shanten + + 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: + """ + if open_sets_34 is None: + open_sets_34 = [] + + 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: + results.append(DiscardOption(player=self.player, + shanten=shanten, + tile_to_discard=hand_tile, + waiting=waiting, + ukeire=self.count_tiles(waiting, closed_tiles_34))) + + if is_agari: + shanten = Shanten.AGARI_STATE + else: + shanten = self.ai.shanten_calculator.calculate_shanten( + tiles_34, + open_sets_34, + chiitoitsu=self.ai.use_chitoitsu + ) + + return results, shanten + + 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 + + def choose_tile_to_discard(self, results: [DiscardOption], print_log=True) -> DiscardOption: + """ + Try to find best tile to discard, based on different rules + """ + + had_to_be_discarded_tiles = [x for x in results if x.had_to_be_discarded] + if had_to_be_discarded_tiles: + results = sorted(had_to_be_discarded_tiles, key=lambda x: (x.shanten, -x.ukeire, x.valuation)) + DecisionsLogger.debug( + log.DISCARD_OPTIONS, + 'Discard marked tiles first', + results, + print_log=print_log + ) + return results[0] + + # remove needed tiles from discard options + results = [x for x in results if not x.had_to_be_saved] + + results = sorted(results, key=lambda x: (x.shanten, -x.ukeire)) + first_option = results[0] + results_with_same_shanten = [x for x in results if x.shanten == first_option.shanten] + + possible_options = [first_option] + ukeire_borders = self._choose_ukeire_borders(first_option, 20, 'ukeire') + for discard_option in results_with_same_shanten: + # there is no sense to check already chosen tile + if discard_option.tile_to_discard == first_option.tile_to_discard: + continue + + # let's choose tiles that are close to the max ukeire tile + if discard_option.ukeire >= first_option.ukeire - ukeire_borders: + possible_options.append(discard_option) + + if first_option.shanten in [1, 2, 3]: + ukeire_field = 'ukeire_second' + for x in possible_options: + self.calculate_second_level_ukeire(x) + + possible_options = sorted(possible_options, key=lambda x: -getattr(x, ukeire_field)) + + filter_percentage = 20 + possible_options = self._filter_list_by_percentage( + possible_options, + ukeire_field, + filter_percentage + ) + else: + ukeire_field = 'ukeire' + possible_options = sorted(possible_options, key=lambda x: -getattr(x, ukeire_field)) + + tiles_without_dora = [x for x in possible_options if x.count_of_dora == 0] + + # we have only dora candidates to discard + if not tiles_without_dora: + DecisionsLogger.debug( + log.DISCARD_OPTIONS, + context=possible_options, + print_log=print_log + ) + + min_dora = min([x.count_of_dora for x in possible_options]) + min_dora_list = [x for x in possible_options if x.count_of_dora == min_dora] + + return sorted(min_dora_list, key=lambda x: -getattr(x, ukeire_field))[0] + + # we filter 10% of options here + if first_option.shanten == 2 or first_option.shanten == 3: + second_filter_percentage = 10 + filtered_options = self._filter_list_by_percentage( + tiles_without_dora, + ukeire_field, + second_filter_percentage + ) + # we should also consider borders for 3+ shanten hands + else: + best_option_without_dora = tiles_without_dora[0] + ukeire_borders = self._choose_ukeire_borders(best_option_without_dora, 10, ukeire_field) + filtered_options = [best_option_without_dora] + for discard_option in tiles_without_dora: + val = getattr(best_option_without_dora, ukeire_field) - ukeire_borders + if getattr(discard_option, ukeire_field) >= val: + filtered_options.append(discard_option) + + DecisionsLogger.debug( + log.DISCARD_OPTIONS, + context=possible_options, + print_log=print_log + ) + + closed_hand_34 = TilesConverter.to_34_array(self.player.closed_hand) + isolated_tiles = [x for x in filtered_options if is_tile_strictly_isolated(closed_hand_34, x.tile_to_discard)] + # isolated tiles should be discarded first + if isolated_tiles: + # let's sort tiles by value and let's choose less valuable tile to discard + return sorted(isolated_tiles, key=lambda x: x.valuation)[0] + + # there are no isolated tiles + # let's discard tile with greater ukeire2 + filtered_options = sorted(filtered_options, key=lambda x: -getattr(x, ukeire_field)) + first_option = filtered_options[0] + + other_tiles_with_same_ukeire = [x for x in filtered_options + if getattr(x, ukeire_field) == getattr(first_option, ukeire_field)] + + # it will happen with shanten=1, all tiles will have ukeire_second == 0 + if other_tiles_with_same_ukeire: + # let's sort tiles by value and let's choose less valuable tile to discard + return sorted(other_tiles_with_same_ukeire, key=lambda x: x.valuation)[0] + + # we have only one candidate to discard with greater ukeire + return first_option + + def process_discard_option(self, discard_option, closed_hand, force_discard=False, print_log=True): + if print_log: + DecisionsLogger.debug( + log.DISCARD, + context=discard_option + ) + + self.player.in_tempai = discard_option.shanten == 0 + self.ai.waiting = discard_option.waiting + self.ai.shanten = discard_option.shanten + self.ai.ukeire = discard_option.ukeire + self.ai.ukeire_second = discard_option.ukeire_second + + # when we called meld we don't need "smart" discard + if force_discard: + return discard_option.find_tile_in_hand(closed_hand) + + last_draw_34 = self.player.last_draw and self.player.last_draw // 4 or None + if self.player.last_draw not in AKA_DORA_LIST and last_draw_34 == discard_option.tile_to_discard: + return self.player.last_draw + else: + return discard_option.find_tile_in_hand(closed_hand) + + def 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 [] + + tiles = copy.copy(self.player.tiles) + tiles.remove(discard_option.find_tile_in_hand(self.player.closed_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 + + wait_136 = wait_34 * 4 + tiles.append(wait_136) + + results, shanten = self.calculate_outs( + tiles, + self.player.closed_hand, + self.player.meld_34_tiles + ) + results = [x for x in results if x.shanten == discard_option.shanten - 1] + + # let's take best ukeire here + if results: + 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 + + tiles.remove(wait_136) + + discard_option.ukeire_second = sum_tiles + + def _filter_list_by_percentage(self, items, attribute, percentage): + filtered_options = [] + first_option = items[0] + ukeire_borders = round((getattr(first_option, attribute) / 100) * percentage) + for x in items: + if getattr(x, attribute) >= getattr(first_option, attribute) - ukeire_borders: + filtered_options.append(x) + return filtered_options + + def _choose_ukeire_borders(self, 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: + ukeire_borders = 2 + + if first_option.shanten == 1 and ukeire_borders < 4: + ukeire_borders = 4 + + if first_option.shanten >= 2 and ukeire_borders < 8: + ukeire_borders = 8 + + return ukeire_borders diff --git a/project/game/ai/first_version/main.py b/project/game/ai/first_version/main.py index 46339f99..4abdfba7 100644 --- a/project/game/ai/first_version/main.py +++ b/project/game/ai/first_version/main.py @@ -10,12 +10,12 @@ from mahjong.meld import Meld from mahjong.shanten import Shanten from mahjong.tile import TilesConverter -from mahjong.utils import is_pon, is_tile_strictly_isolated +from mahjong.utils import is_pon from game.ai.base.main import InterfaceAI -from game.ai.discard import DiscardOption -from game.ai.first_version.riichi import Riichi from game.ai.first_version.defence.main import DefenceHandler +from game.ai.first_version.hand_builder import HandBuilder +from game.ai.first_version.riichi import Riichi from game.ai.first_version.strategies.chiitoitsu import ChiitoitsuStrategy from game.ai.first_version.strategies.chinitsu import ChinitsuStrategy from game.ai.first_version.strategies.formal_tempai import FormalTempaiStrategy @@ -57,6 +57,7 @@ def __init__(self, player): self.riichi = Riichi(player) self.hand_divider = HandDivider() self.finished_hand = HandCalculator() + self.hand_builder = HandBuilder(player, self) self.erase_state() @@ -92,144 +93,7 @@ def draw_tile(self, tile_136): self.determine_strategy(self.player.tiles) def discard_tile(self, discard_tile, print_log=True): - # we called meld and we had discard tile that we wanted to discard - if discard_tile is not None: - if not self.last_discard_option: - return discard_tile - - return self.process_discard_option(self.last_discard_option, self.player.closed_hand, True) - - results, shanten = self.calculate_outs(self.player.tiles, - self.player.closed_hand, - self.player.meld_34_tiles) - - selected_tile = self.process_discard_options_and_select_tile_to_discard( - results, - shanten, - print_log=print_log - ) - - # bot think that there is a threat on the table - # and better to fold - # if we can't find safe tiles, let's continue to build our hand - if self.defence.should_go_to_defence_mode(selected_tile): - if not self.in_defence: - DecisionsLogger.debug(log.DEFENCE_ACTIVATE) - self.in_defence = True - - defence_tile = self.defence.try_to_find_safe_tile_to_discard(results) - if defence_tile: - return self.process_discard_option(defence_tile, self.player.closed_hand) - else: - if self.in_defence: - DecisionsLogger.debug(log.DEFENCE_DEACTIVATE) - self.in_defence = False - - return self.process_discard_option(selected_tile, self.player.closed_hand, print_log=print_log) - - def process_discard_options_and_select_tile_to_discard(self, results, shanten, hand_was_open=False, print_log=True): - # we had to update tiles value there - # because it is related with shanten number - for result in results: - result.ukeire = self.count_tiles(result.waiting, TilesConverter.to_34_array(self.player.closed_hand)) - result.calculate_value(shanten) - - # current strategy can affect on our discard options - # so, don't use strategy specific choices for calling riichi - if self.current_strategy: - results = self.current_strategy.determine_what_to_discard(self.player.closed_hand, - results, - shanten, - False, - None, - hand_was_open) - - return self.choose_tile_to_discard(results, print_log=print_log) - - def calculate_waits(self, tiles_34, open_sets_34=None): - """ - :param tiles_34: array of tiles in 34 formant, 13 of them (this is important) - :param open_sets_34: array of array with tiles in 34 format - :return: array of waits in 34 format and number of shanten - """ - shanten = self.shanten_calculator.calculate_shanten(tiles_34, open_sets_34, chiitoitsu=self.use_chitoitsu) - waiting = [] - for j in range(0, 34): - if tiles_34[j] == 4: - continue - - tiles_34[j] += 1 - - key = '{},{},{}'.format( - ''.join([str(x) for x in tiles_34]), - ';'.join([str(x) for x in open_sets_34]), - self.use_chitoitsu and 1 or 0 - ) - - if key in self.hand_cache: - new_shanten = self.hand_cache[key] - else: - new_shanten = self.shanten_calculator.calculate_shanten( - tiles_34, - open_sets_34, - chiitoitsu=self.use_chitoitsu - ) - self.hand_cache[key] = new_shanten - - if new_shanten == shanten - 1: - waiting.append(j) - - tiles_34[j] -= 1 - - return waiting, shanten - - - 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: - """ - if open_sets_34 is None: - open_sets_34 = [] - - 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.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: - results.append(DiscardOption(player=self.player, - shanten=shanten, - tile_to_discard=hand_tile, - waiting=waiting, - ukeire=self.count_tiles(waiting, closed_tiles_34))) - - if is_agari: - shanten = Shanten.AGARI_STATE - else: - shanten = self.shanten_calculator.calculate_shanten(tiles_34, open_sets_34, chiitoitsu=self.use_chitoitsu) - - return results, shanten - - def count_tiles(self, waiting, tiles_34): - n = 0 - not_suitable_tiles = self.current_strategy and self.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 + return self.hand_builder.discard_tile(discard_tile, print_log) def try_to_call_meld(self, tile_136, is_kamicha_discard): tiles_136 = self.player.tiles[:] + [tile_136] @@ -303,141 +167,6 @@ def determine_strategy(self, tiles_136): return self.current_strategy and True or False - def choose_tile_to_discard(self, results: [DiscardOption], print_log=True) -> DiscardOption: - """ - Try to find best tile to discard, based on different rules - """ - - had_to_be_discarded_tiles = [x for x in results if x.had_to_be_discarded] - if had_to_be_discarded_tiles: - results = sorted(had_to_be_discarded_tiles, key=lambda x: (x.shanten, -x.ukeire, x.valuation)) - DecisionsLogger.debug( - log.DISCARD_OPTIONS, - 'Discard marked tiles first', - results, - print_log=print_log - ) - return results[0] - - # remove needed tiles from discard options - results = [x for x in results if not x.had_to_be_saved] - - results = sorted(results, key=lambda x: (x.shanten, -x.ukeire)) - first_option = results[0] - results_with_same_shanten = [x for x in results if x.shanten == first_option.shanten] - - possible_options = [first_option] - ukeire_borders = self._choose_ukeire_borders(first_option, 20, 'ukeire') - for discard_option in results_with_same_shanten: - # there is no sense to check already chosen tile - if discard_option.tile_to_discard == first_option.tile_to_discard: - continue - - # let's choose tiles that are close to the max ukeire tile - if discard_option.ukeire >= first_option.ukeire - ukeire_borders: - possible_options.append(discard_option) - - if first_option.shanten in [1, 2, 3]: - sorting_field = 'ukeire_second' - for x in possible_options: - self.calculate_second_level_ukeire(x) - - possible_options = sorted(possible_options, key=lambda x: -getattr(x, sorting_field)) - - filter_percentage = 20 - possible_options = self._filter_list_by_percentage( - possible_options, - sorting_field, - filter_percentage - ) - else: - sorting_field = 'ukeire' - possible_options = sorted(possible_options, key=lambda x: -getattr(x, sorting_field)) - - tiles_without_dora = [x for x in possible_options if x.count_of_dora == 0] - - # we have only dora candidates to discard - if not tiles_without_dora: - DecisionsLogger.debug( - log.DISCARD_OPTIONS, - context=possible_options, - print_log=print_log - ) - - min_dora = min([x.count_of_dora for x in possible_options]) - min_dora_list = [x for x in possible_options if x.count_of_dora == min_dora] - - return sorted(min_dora_list, key=lambda x: -getattr(x, sorting_field))[0] - - # we filter 10% of options here - if first_option.shanten == 2 or first_option.shanten == 3: - second_filter_percentage = 10 - filtered_options = self._filter_list_by_percentage( - tiles_without_dora, - sorting_field, - second_filter_percentage - ) - # we should also consider borders for 3+ shanten hands - else: - best_option_without_dora = tiles_without_dora[0] - ukeire_borders = self._choose_ukeire_borders(best_option_without_dora, 10, sorting_field) - filtered_options = [best_option_without_dora] - for discard_option in tiles_without_dora: - if getattr(discard_option, sorting_field) >= getattr(best_option_without_dora, sorting_field) - ukeire_borders: - filtered_options.append(discard_option) - - DecisionsLogger.debug( - log.DISCARD_OPTIONS, - context=possible_options, - print_log=print_log - ) - - closed_hand_34 = TilesConverter.to_34_array(self.player.closed_hand) - isolated_tiles = [x for x in filtered_options if is_tile_strictly_isolated(closed_hand_34, x.tile_to_discard)] - # isolated tiles should be discarded first - if isolated_tiles: - # let's sort tiles by value and let's choose less valuable tile to discard - return sorted(isolated_tiles, key=lambda x: x.valuation)[0] - - # there are no isolated tiles - # let's discard tile with greater ukeire2 - filtered_options = sorted(filtered_options, key=lambda x: -getattr(x, sorting_field)) - first_option = filtered_options[0] - - other_tiles_with_same_ukeire = [x for x in filtered_options - if getattr(x, sorting_field) == getattr(first_option, sorting_field)] - - # it will happen with shanten=1, all tiles will have ukeire_second == 0 - if other_tiles_with_same_ukeire: - # let's sort tiles by value and let's choose less valuable tile to discard - return sorted(other_tiles_with_same_ukeire, key=lambda x: x.valuation)[0] - - # we have only one candidate to discard with greater ukeire - return first_option - - def process_discard_option(self, discard_option, closed_hand, force_discard=False, print_log=True): - if print_log: - DecisionsLogger.debug( - log.DISCARD, - context=discard_option - ) - - self.waiting = discard_option.waiting - self.player.ai.shanten = discard_option.shanten - self.player.in_tempai = self.player.ai.shanten == 0 - self.player.ai.ukeire = discard_option.ukeire - self.player.ai.ukeire_second = discard_option.ukeire_second - - # when we called meld we don't need "smart" discard - if force_discard: - return discard_option.find_tile_in_hand(closed_hand) - - last_draw_34 = self.player.last_draw and self.player.last_draw // 4 or None - if self.player.last_draw not in AKA_DORA_LIST and last_draw_34 == discard_option.tile_to_discard: - return self.player.last_draw - else: - return discard_option.find_tile_in_hand(closed_hand) - def estimate_hand_value(self, win_tile, tiles=None, call_riichi=False): """ :param win_tile: 34 tile format @@ -527,22 +256,22 @@ def should_call_kan(self, tile, open_kan, from_riichi=False): if closed_hand_34[tile_34] == 3: if open_kan or from_riichi: # this 4 tiles can only be used in kan, no other options - previous_waiting, previous_shanten = self.calculate_waits(tiles_34, melds_34) - previous_waits_cnt = self.count_tiles(previous_waiting, closed_hand_34) + previous_waiting, previous_shanten = self.hand_builder.calculate_waits(tiles_34, melds_34) + previous_waits_cnt = self.hand_builder.count_tiles(previous_waiting, closed_hand_34) # shanten calculator doesn't like working with kans, so we pretend it's a pon melds_34 += [[tile_34, tile_34, tile_34]] closed_hand_34[tile_34] = 0 - new_waiting, new_shanten = self.calculate_waits(tiles_34, melds_34) - new_waits_cnt = self.count_tiles(new_waiting, closed_hand_34) + new_waiting, new_shanten = self.hand_builder.calculate_waits(tiles_34, melds_34) + new_waits_cnt = self.hand_builder.count_tiles(new_waiting, closed_hand_34) else: # if we can use or tile in the hand for the forms other than KAN tiles.append(tile) closed_hand_tiles.append(tile) closed_hand_34[tile_34] += 1 - previous_results, previous_shanten = self.calculate_outs(tiles, closed_hand_tiles, melds_34) + previous_results, previous_shanten = self.hand_builder.calculate_outs(tiles, closed_hand_tiles, melds_34) previous_results = [x for x in previous_results if x.shanten == previous_shanten] previous_waits_cnt = sorted(previous_results, key=lambda x: -x.ukeire)[0].ukeire @@ -550,8 +279,8 @@ def should_call_kan(self, tile, open_kan, from_riichi=False): closed_hand_34[tile_34] = 0 melds_34 += [[tile_34, tile_34, tile_34]] - new_waiting, new_shanten = self.calculate_waits(tiles_34, melds_34) - new_waits_cnt = self.count_tiles(new_waiting, closed_hand_34) + new_waiting, new_shanten = self.hand_builder.calculate_waits(tiles_34, melds_34) + new_waits_cnt = self.hand_builder.count_tiles(new_waiting, closed_hand_34) # it is not possible to reduce number of shanten by calling a kan assert new_shanten >= previous_shanten @@ -579,66 +308,9 @@ def enemy_called_riichi(self, enemy_seat): self.in_defence = True DecisionsLogger.debug(log.DEFENCE_ACTIVATE) - def calculate_second_level_ukeire(self, discard_option): - closed_hand_34 = TilesConverter.to_34_array(self.player.closed_hand) - not_suitable_tiles = self.current_strategy and self.current_strategy.not_suitable_tiles or [] - - tiles = copy.copy(self.player.tiles) - tiles.remove(discard_option.find_tile_in_hand(self.player.closed_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 - - wait_136 = wait_34 * 4 - tiles.append(wait_136) - - results, shanten = self.calculate_outs( - tiles, - self.player.closed_hand, - self.player.meld_34_tiles - ) - results = [x for x in results if x.shanten == discard_option.shanten - 1] - - # let's take best ukeire here - if results: - 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 - - tiles.remove(wait_136) - - discard_option.ukeire_second = sum_tiles - @property def enemy_players(self): """ Return list of players except our bot """ return self.player.table.players[1:] - - @staticmethod - def _filter_list_by_percentage(items, attribute, percentage): - filtered_options = [] - first_option = items[0] - ukeire_borders = round((getattr(first_option, attribute) / 100) * percentage) - for x in items: - if getattr(x, attribute) >= getattr(first_option, attribute) - ukeire_borders: - filtered_options.append(x) - return filtered_options - - @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: - ukeire_borders = 2 - - if first_option.shanten == 1 and ukeire_borders < 4: - ukeire_borders = 4 - - if first_option.shanten >= 2 and ukeire_borders < 8: - ukeire_borders = 8 - - return ukeire_borders diff --git a/project/game/ai/first_version/riichi.py b/project/game/ai/first_version/riichi.py index ee12095e..74dd0571 100644 --- a/project/game/ai/first_version/riichi.py +++ b/project/game/ai/first_version/riichi.py @@ -18,7 +18,7 @@ def should_call_riichi(self): return False # don't call karaten riichi - count_tiles = self.player.ai.count_tiles( + count_tiles = self.player.ai.hand_builder.count_tiles( self.player.ai.waiting, TilesConverter.to_34_array(self.player.closed_hand) ) @@ -36,7 +36,7 @@ def should_call_riichi(self): return self._should_call_riichi_many_sided() def _should_call_riichi_one_sided(self): - count_tiles = self.player.ai.count_tiles( + count_tiles = self.player.ai.hand_builder.count_tiles( self.player.ai.waiting, TilesConverter.to_34_array(self.player.closed_hand) ) @@ -182,7 +182,7 @@ def _should_call_riichi_one_sided(self): return True def _should_call_riichi_many_sided(self): - count_tiles = self.player.ai.count_tiles( + count_tiles = self.player.ai.hand_builder.count_tiles( self.player.ai.waiting, TilesConverter.to_34_array(self.player.closed_hand) ) diff --git a/project/game/ai/first_version/strategies/main.py b/project/game/ai/first_version/strategies/main.py index 74544806..3a6ff509 100644 --- a/project/game/ai/first_version/strategies/main.py +++ b/project/game/ai/first_version/strategies/main.py @@ -204,7 +204,7 @@ def try_to_call_meld(self, tile, is_kamicha_discard, new_tiles): # we need to calculate count of shanten with supposed meld # to prevent bad hand openings melds = self.player.meld_34_tiles + [best_meld_34] - outs_results, shanten = self.player.ai.calculate_outs(new_tiles, closed_hand, melds) + outs_results, shanten = self.player.ai.hand_builder.calculate_outs(new_tiles, closed_hand, melds) # each strategy can use their own value to min shanten number if shanten > self.min_shanten: @@ -248,7 +248,7 @@ def try_to_call_meld(self, tile, is_kamicha_discard, new_tiles): if not filtered_results: return None, None - selected_tile = self.player.ai.process_discard_options_and_select_tile_to_discard( + selected_tile = self.player.ai.hand_builder.process_discard_options_and_select_tile_to_discard( filtered_results, shanten, hand_was_open=True diff --git a/project/game/ai/first_version/tests/tests_ai.py b/project/game/ai/first_version/tests/tests_ai.py index c9147048..fe27dea5 100644 --- a/project/game/ai/first_version/tests/tests_ai.py +++ b/project/game/ai/first_version/tests/tests_ai.py @@ -166,20 +166,20 @@ def test_remaining_tiles_and_enemy_discard(self): tiles = self._string_to_136_array(man='123456789', sou='167', honors='77') player.init_hand(tiles) - results, shanten = player.ai.calculate_outs(tiles, tiles) + results, shanten = player.ai.hand_builder.calculate_outs(tiles, tiles) result = [x for x in results if x.tile_to_discard == self._string_to_34_tile(sou='1')][0] self.assertEqual(result.ukeire, 8) player.table.add_discarded_tile(1, self._string_to_136_tile(sou='5'), False) - results, shanten = player.ai.calculate_outs(tiles, tiles) + results, shanten = player.ai.hand_builder.calculate_outs(tiles, tiles) result = [x for x in results if x.tile_to_discard == self._string_to_34_tile(sou='1')][0] self.assertEqual(result.ukeire, 7) player.table.add_discarded_tile(2, self._string_to_136_tile(sou='5'), False) player.table.add_discarded_tile(3, self._string_to_136_tile(sou='8'), False) - results, shanten = player.ai.calculate_outs(tiles, tiles) + results, shanten = player.ai.hand_builder.calculate_outs(tiles, tiles) result = [x for x in results if x.tile_to_discard == self._string_to_34_tile(sou='1')][0] self.assertEqual(result.ukeire, 5) @@ -190,7 +190,7 @@ def test_remaining_tiles_and_opened_meld(self): tiles = self._string_to_136_array(man='123456789', sou='167', honors='77') player.init_hand(tiles) - results, shanten = player.ai.calculate_outs(tiles, tiles) + results, shanten = player.ai.hand_builder.calculate_outs(tiles, tiles) result = [x for x in results if x.tile_to_discard == self._string_to_34_tile(sou='1')][0] self.assertEqual(result.ukeire, 8) @@ -201,7 +201,7 @@ def test_remaining_tiles_and_opened_meld(self): meld.called_tile = tile player.table.add_called_meld(3, meld) - results, shanten = player.ai.calculate_outs(tiles, tiles) + results, shanten = player.ai.hand_builder.calculate_outs(tiles, tiles) result = [x for x in results if x.tile_to_discard == self._string_to_34_tile(sou='1')][0] self.assertEqual(result.ukeire, 5) @@ -212,7 +212,7 @@ def test_remaining_tiles_and_opened_meld(self): meld.called_tile = tile player.table.add_called_meld(2, meld) - results, shanten = player.ai.calculate_outs(tiles, tiles) + results, shanten = player.ai.hand_builder.calculate_outs(tiles, tiles) result = [x for x in results if x.tile_to_discard == self._string_to_34_tile(sou='1')][0] self.assertEqual(result.ukeire, 4) @@ -223,13 +223,13 @@ def test_remaining_tiles_and_dora_indicators(self): tiles = self._string_to_136_array(man='123456789', sou='167', honors='77') player.init_hand(tiles) - results, shanten = player.ai.calculate_outs(tiles, tiles) + results, shanten = player.ai.hand_builder.calculate_outs(tiles, tiles) result = [x for x in results if x.tile_to_discard == self._string_to_34_tile(sou='1')][0] self.assertEqual(result.ukeire, 8) table.add_dora_indicator(self._string_to_136_tile(sou='8')) - results, shanten = player.ai.calculate_outs(tiles, tiles) + results, shanten = player.ai.hand_builder.calculate_outs(tiles, tiles) result = [x for x in results if x.tile_to_discard == self._string_to_34_tile(sou='1')][0] self.assertEqual(result.ukeire, 7) diff --git a/project/game/ai/first_version/tests/tests_defence.py b/project/game/ai/first_version/tests/tests_defence.py index 1f340a23..85d092fb 100644 --- a/project/game/ai/first_version/tests/tests_defence.py +++ b/project/game/ai/first_version/tests/tests_defence.py @@ -150,10 +150,10 @@ def test_should_go_for_defence_and_good_hand_with_drawn_tile(self): table.add_called_riichi(3) - results, shanten = table.player.ai.calculate_outs(table.player.tiles, - table.player.closed_hand, - table.player.meld_34_tiles) - selected_tile = table.player.ai.process_discard_options_and_select_tile_to_discard(results, shanten) + results, shanten = table.player.ai.hand_builder.calculate_outs(table.player.tiles, + table.player.closed_hand, + table.player.meld_34_tiles) + selected_tile = table.player.ai.hand_builder.process_discard_options_and_select_tile_to_discard(results, shanten) self.assertEqual(table.player.ai.defence.should_go_to_defence_mode(selected_tile), False) result = table.player.discard_tile() From 8ce623c0aa9ce003b0f98ca4673b86f2412ad580 Mon Sep 17 00:00:00 2001 From: Nihisil Date: Wed, 26 Sep 2018 18:29:38 +0800 Subject: [PATCH 080/126] Refactoring for open hand logic --- project/game/ai/discard.py | 6 +- project/game/ai/first_version/defence/main.py | 8 +- project/game/ai/first_version/hand_builder.py | 83 ++++------ project/game/ai/first_version/main.py | 26 +++- .../ai/first_version/strategies/honitsu.py | 10 ++ .../game/ai/first_version/strategies/main.py | 145 +++++++----------- .../ai/first_version/strategies/tanyao.py | 61 ++++---- .../ai/first_version/strategies/yakuhai.py | 67 ++++---- .../tests/strategies/tests_chinitsu.py | 2 - .../tests/strategies/tests_honitsu.py | 8 +- .../tests/strategies/tests_tanyao.py | 6 +- .../game/ai/first_version/tests/tests_ai.py | 46 +++++- .../ai/first_version/tests/tests_defence.py | 6 - project/utils/decisions_constants.py | 3 +- 14 files changed, 244 insertions(+), 233 deletions(-) diff --git a/project/game/ai/discard.py b/project/game/ai/discard.py index 38d0cf1f..a4f2ce77 100644 --- a/project/game/ai/discard.py +++ b/project/game/ai/discard.py @@ -91,15 +91,11 @@ def find_tile_in_hand(self, closed_hand): return TilesConverter.find_34_tile_in_136_array(self.tile_to_discard, closed_hand) - def calculate_value(self, shanten=None): + def calculate_value(self): # base is 100 for ability to mark tiles as not needed (like set value to 50) value = 100 honored_value = 20 - # we don't need to keep honor tiles in almost completed hand - if shanten and shanten <= 2: - honored_value = 0 - if is_honor(self.tile_to_discard): if self.tile_to_discard in self.player.valued_honors: count_of_winds = [x for x in self.player.valued_honors if x == self.tile_to_discard] diff --git a/project/game/ai/first_version/defence/main.py b/project/game/ai/first_version/defence/main.py index 82bc4e41..661d1502 100644 --- a/project/game/ai/first_version/defence/main.py +++ b/project/game/ai/first_version/defence/main.py @@ -108,7 +108,13 @@ def should_go_to_defence_mode(self, discard_candidate=None): return False - def try_to_find_safe_tile_to_discard(self, discard_results): + def try_to_find_safe_tile_to_discard(self): + discard_results, _ = self.player.ai.hand_builder.find_discard_options( + self.player.tiles, + self.player.closed_hand, + self.player.meld_34_tiles + ) + self.hand_34 = TilesConverter.to_34_array(self.player.tiles) self.closed_hand_34 = TilesConverter.to_34_array(self.player.closed_hand) diff --git a/project/game/ai/first_version/hand_builder.py b/project/game/ai/first_version/hand_builder.py index 1f9b8ad2..26ca0d66 100644 --- a/project/game/ai/first_version/hand_builder.py +++ b/project/game/ai/first_version/hand_builder.py @@ -18,21 +18,11 @@ def __init__(self, player, ai): self.player = player self.ai = ai - def discard_tile(self, discard_tile, print_log=True): - # we called meld and we had discard tile that we wanted to discard - if discard_tile is not None: - if not self.ai.last_discard_option: - return discard_tile - - return self.process_discard_option(self.ai.last_discard_option, self.player.closed_hand, True) - - results, shanten = self.calculate_outs(self.player.tiles, - self.player.closed_hand, - self.player.meld_34_tiles) - - selected_tile = self.process_discard_options_and_select_tile_to_discard( - results, - shanten, + def discard_tile(self, tiles, closed_hand, open_sets_34, print_log=True): + selected_tile = self.choose_tile_to_discard( + tiles, + closed_hand, + open_sets_34, print_log=print_log ) @@ -44,34 +34,15 @@ def discard_tile(self, discard_tile, print_log=True): DecisionsLogger.debug(log.DEFENCE_ACTIVATE) self.ai.in_defence = True - defence_tile = self.ai.defence.try_to_find_safe_tile_to_discard(results) + defence_tile = self.ai.defence.try_to_find_safe_tile_to_discard() if defence_tile: - return self.process_discard_option(defence_tile, self.player.closed_hand) + return self.process_discard_option(defence_tile, closed_hand) else: if self.ai.in_defence: DecisionsLogger.debug(log.DEFENCE_DEACTIVATE) self.ai.in_defence = False - return self.process_discard_option(selected_tile, self.player.closed_hand, print_log=print_log) - - def process_discard_options_and_select_tile_to_discard(self, results, shanten, hand_was_open=False, print_log=True): - # we had to update tiles value there - # because it is related with shanten number - for result in results: - result.ukeire = self.count_tiles(result.waiting, TilesConverter.to_34_array(self.player.closed_hand)) - result.calculate_value(shanten) - - # current strategy can affect on our discard options - # so, don't use strategy specific choices for calling riichi - if self.ai.current_strategy: - results = self.ai.current_strategy.determine_what_to_discard(self.player.closed_hand, - results, - shanten, - False, - None, - hand_was_open) - - return self.choose_tile_to_discard(results, print_log=print_log) + return self.process_discard_option(selected_tile, closed_hand, print_log=print_log) def calculate_waits(self, tiles_34, open_sets_34=None): """ @@ -110,7 +81,7 @@ def calculate_waits(self, tiles_34, open_sets_34=None): return waiting, shanten - def calculate_outs(self, tiles, closed_hand, open_sets_34=None): + def find_discard_options(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 @@ -161,28 +132,42 @@ def count_tiles(self, waiting, tiles_34): n += 4 - self.player.total_tiles(tile_34, tiles_34) return n - def choose_tile_to_discard(self, results: [DiscardOption], print_log=True) -> DiscardOption: + def choose_tile_to_discard(self, tiles, closed_hand, open_sets_34, print_log=True): """ Try to find best tile to discard, based on different rules """ - had_to_be_discarded_tiles = [x for x in results if x.had_to_be_discarded] + discard_options, _ = self.find_discard_options( + tiles, + closed_hand, + open_sets_34 + ) + + # our strategy can affect discard options + if self.ai.current_strategy: + discard_options = self.ai.current_strategy.determine_what_to_discard( + discard_options, + closed_hand, + open_sets_34 + ) + + had_to_be_discarded_tiles = [x for x in discard_options if x.had_to_be_discarded] if had_to_be_discarded_tiles: - results = sorted(had_to_be_discarded_tiles, key=lambda x: (x.shanten, -x.ukeire, x.valuation)) + discard_options = sorted(had_to_be_discarded_tiles, key=lambda x: (x.shanten, -x.ukeire, x.valuation)) DecisionsLogger.debug( log.DISCARD_OPTIONS, 'Discard marked tiles first', - results, + discard_options, print_log=print_log ) - return results[0] + return discard_options[0] # remove needed tiles from discard options - results = [x for x in results if not x.had_to_be_saved] + discard_options = [x for x in discard_options if not x.had_to_be_saved] - results = sorted(results, key=lambda x: (x.shanten, -x.ukeire)) - first_option = results[0] - results_with_same_shanten = [x for x in results if x.shanten == first_option.shanten] + discard_options = sorted(discard_options, key=lambda x: (x.shanten, -x.ukeire)) + first_option = discard_options[0] + results_with_same_shanten = [x for x in discard_options if x.shanten == first_option.shanten] possible_options = [first_option] ukeire_borders = self._choose_ukeire_borders(first_option, 20, 'ukeire') @@ -251,7 +236,7 @@ def choose_tile_to_discard(self, results: [DiscardOption], print_log=True) -> Di print_log=print_log ) - closed_hand_34 = TilesConverter.to_34_array(self.player.closed_hand) + closed_hand_34 = TilesConverter.to_34_array(closed_hand) isolated_tiles = [x for x in filtered_options if is_tile_strictly_isolated(closed_hand_34, x.tile_to_discard)] # isolated tiles should be discarded first if isolated_tiles: @@ -312,7 +297,7 @@ def calculate_second_level_ukeire(self, discard_option): wait_136 = wait_34 * 4 tiles.append(wait_136) - results, shanten = self.calculate_outs( + results, shanten = self.find_discard_options( tiles, self.player.closed_hand, self.player.meld_34_tiles diff --git a/project/game/ai/first_version/main.py b/project/game/ai/first_version/main.py index 4abdfba7..6a58f20c 100644 --- a/project/game/ai/first_version/main.py +++ b/project/game/ai/first_version/main.py @@ -93,7 +93,19 @@ def draw_tile(self, tile_136): self.determine_strategy(self.player.tiles) def discard_tile(self, discard_tile, print_log=True): - return self.hand_builder.discard_tile(discard_tile, print_log) + # we called meld and we had discard tile that we wanted to discard + if discard_tile is not None: + if not self.last_discard_option: + return discard_tile + + return self.hand_builder.process_discard_option(self.last_discard_option, self.player.closed_hand, True) + + return self.hand_builder.discard_tile( + self.player.tiles, + self.player.closed_hand, + self.player.meld_34_tiles, + print_log + ) def try_to_call_meld(self, tile_136, is_kamicha_discard): tiles_136 = self.player.tiles[:] + [tile_136] @@ -113,18 +125,16 @@ def try_to_call_meld(self, tile_136, is_kamicha_discard): return None, None meld, discard_option = self.current_strategy.try_to_call_meld(tile_136, is_kamicha_discard, tiles_136) - tile_to_discard = None if discard_option: self.last_discard_option = discard_option - tile_to_discard = discard_option.tile_to_discard - DecisionsLogger.debug(log.CALL_MELD, 'Try to call meld', context=[ + DecisionsLogger.debug(log.MELD_CALL, 'Try to call meld', context=[ 'Hand: {}'.format(self.player.format_hand_for_print(tile_136)), 'Meld: {}'.format(meld), 'Discard after meld: {}'.format(discard_option) ]) - return meld, tile_to_discard + return meld, discard_option def determine_strategy(self, tiles_136): self.use_chitoitsu = False @@ -271,7 +281,11 @@ def should_call_kan(self, tile, open_kan, from_riichi=False): closed_hand_tiles.append(tile) closed_hand_34[tile_34] += 1 - previous_results, previous_shanten = self.hand_builder.calculate_outs(tiles, closed_hand_tiles, melds_34) + previous_results, previous_shanten = self.hand_builder.find_discard_options( + tiles, + closed_hand_tiles, + melds_34 + ) previous_results = [x for x in previous_results if x.shanten == previous_shanten] previous_waits_cnt = sorted(previous_results, key=lambda x: -x.ukeire)[0].ukeire diff --git a/project/game/ai/first_version/strategies/honitsu.py b/project/game/ai/first_version/strategies/honitsu.py index dd95fdd5..692de637 100644 --- a/project/game/ai/first_version/strategies/honitsu.py +++ b/project/game/ai/first_version/strategies/honitsu.py @@ -138,6 +138,16 @@ def meld_had_to_be_called(self, tile): return False + def determine_what_to_discard(self, discard_options, hand, open_melds): + first_option = sorted(discard_options, key=lambda x: x.shanten)[0] + shanten = first_option.shanten + + # we can riichi our hand, so let's not destroy it with not suitable tiles discarding + if shanten == 0 and not self.player.is_open_hand: + return discard_options + else: + return super(HonitsuStrategy, self).determine_what_to_discard(discard_options, hand, open_melds) + def _calculate_not_suitable_tiles_cnt(self, tiles_34, suit): self.tiles_count_other_suits = 0 self.tiles_count_other_suits_not_isolated = 0 diff --git a/project/game/ai/first_version/strategies/main.py b/project/game/ai/first_version/strategies/main.py index 3a6ff509..91ebc6b4 100644 --- a/project/game/ai/first_version/strategies/main.py +++ b/project/game/ai/first_version/strategies/main.py @@ -1,8 +1,12 @@ # -*- coding: utf-8 -*- +import utils.decisions_constants as log + from mahjong.meld import Meld from mahjong.tile import TilesConverter from mahjong.utils import is_man, is_pin, is_sou, is_pon, is_chi, plus_dora, is_aka_dora, is_honor, is_terminal +from utils.decisions_logger import DecisionsLogger + class BaseStrategy(object): YAKUHAI = 0 @@ -51,9 +55,6 @@ def should_activate_strategy(self, tiles_136): """ self.calculate_dora_count(tiles_136) - if self.player.is_open_hand: - return True - return True def can_meld_into_agari(self): @@ -79,38 +80,18 @@ def is_tile_suitable(self, tile): """ raise NotImplemented() - def determine_what_to_discard(self, closed_hand, outs_results, shanten, for_open_hand, tile_for_open_hand, - hand_was_open=False): - """ - - "for_open_hand" and "tile_for_open_hand" we had to use when we want to - determine what melds will be open - - "hand_was_open" we will use in rare cases - when we open hand and before meld was added to the player - it happens between we send a message to tenhou and tenhou send confirmation message to us - sometimes we failed to call a meld because of other player - - :param closed_hand: array of 136 tiles format - :param outs_results: dict - :param shanten: number of shanten - :param for_open_hand: boolean - :param tile_for_open_hand: 136 tile format - :param hand_was_open: boolean - :return: array of DiscardOption - """ - + def determine_what_to_discard(self, discard_options, hand, open_melds): # for riichi we don't need to discard useful tiles - if shanten == 0 and not self.player.is_open_hand: - return outs_results + # if shanten == 0 and not self.player.is_open_hand: + # return outs_results # mark all not suitable tiles as ready to discard # even if they not should be discarded by uke-ire - for j in outs_results: - if not self.is_tile_suitable(j.tile_to_discard * 4): - j.had_to_be_discarded = True + for x in discard_options: + if not self.is_tile_suitable(x.tile_to_discard * 4): + x.had_to_be_discarded = True - return outs_results + return discard_options def try_to_call_meld(self, tile, is_kamicha_discard, new_tiles): """ @@ -168,9 +149,12 @@ def try_to_call_meld(self, tile, is_kamicha_discard, new_tiles): if second_limit > second_index: second_limit = second_index - combinations = self.player.ai.hand_divider.find_valid_combinations(closed_hand_34, - first_limit, - second_limit, True) + combinations = self.player.ai.hand_divider.find_valid_combinations( + closed_hand_34, + first_limit, + second_limit, + True + ) if combinations: combinations = combinations[0] @@ -199,25 +183,11 @@ def try_to_call_meld(self, tile, is_kamicha_discard, new_tiles): if not possible_melds: return None, None - best_meld_34 = self._find_best_meld_to_open(possible_melds, new_tiles) + best_meld_34 = self._find_best_meld_to_open(possible_melds, new_tiles, closed_hand) if best_meld_34: # we need to calculate count of shanten with supposed meld # to prevent bad hand openings - melds = self.player.meld_34_tiles + [best_meld_34] - outs_results, shanten = self.player.ai.hand_builder.calculate_outs(new_tiles, closed_hand, melds) - - # each strategy can use their own value to min shanten number - if shanten > self.min_shanten: - return None, None - - # we can't improve hand, so we don't need to open it - if not outs_results: - return None, None - - # sometimes we had to call tile, even if it will not improve our hand - # otherwise we can call only with improvements of shanten - if not self.meld_had_to_be_called(tile) and shanten >= self.player.ai.shanten: - return None, None + open_sets_34 = self.player.meld_34_tiles + [best_meld_34[:]] meld_type = is_chi(best_meld_34) and Meld.CHI or Meld.PON best_meld_34.remove(discarded_tile) @@ -228,6 +198,26 @@ def try_to_call_meld(self, tile, is_kamicha_discard, new_tiles): second_tile = TilesConverter.find_34_tile_in_136_array(best_meld_34[1], closed_hand) closed_hand.remove(second_tile) + selected_tile = self.player.ai.hand_builder.choose_tile_to_discard( + new_tiles, + closed_hand, + open_sets_34, + print_log=False + ) + + shanten = selected_tile.shanten + had_to_be_called = self.meld_had_to_be_called(tile) + had_to_be_called = had_to_be_called or selected_tile.had_to_be_discarded + + # each strategy can use their own value to min shanten number + if shanten > self.min_shanten: + return None, None + + # sometimes we had to call tile, even if it will not improve our hand + # otherwise we can call only with improvements of shanten + if not had_to_be_called and shanten >= self.player.ai.shanten: + return None, None + tiles = [ first_tile, second_tile, @@ -238,22 +228,6 @@ def try_to_call_meld(self, tile, is_kamicha_discard, new_tiles): meld.type = meld_type meld.tiles = sorted(tiles) - # we had to be sure that all our discard results exists in the closed hand - filtered_results = [] - for result in outs_results: - if result.find_tile_in_hand(closed_hand): - filtered_results.append(result) - - # we can't discard anything, so let's not open our hand - if not filtered_results: - return None, None - - selected_tile = self.player.ai.hand_builder.process_discard_options_and_select_tile_to_discard( - filtered_results, - shanten, - hand_was_open=True - ) - return meld, selected_tile return None, None @@ -293,30 +267,29 @@ def calculate_dora_count(self, tiles_136): self.dora_count_central += self.aka_dora_count self.dora_count_total = self.dora_count_central + self.dora_count_not_central - def _find_best_meld_to_open(self, possible_melds, completed_hand): - """ - :param possible_melds: - :param completed_hand: - :return: - """ - + def _find_best_meld_to_open(self, possible_melds, new_tiles, closed_hand): if len(possible_melds) == 1: return possible_melds[0] - # We will replace possible set with one completed pon set - # and we will calculate remaining shanten in the hand - # and chose the hand with min shanten count - completed_hand_34 = TilesConverter.to_34_array(completed_hand) - - results = [] + final_results = [] for meld in possible_melds: - melds = self.player.meld_34_tiles + [meld] - shanten = self.player.ai.shanten_calculator.calculate_shanten( - completed_hand_34, - melds, - chiitoitsu=self.player.ai.use_chitoitsu + open_sets_34 = self.player.meld_34_tiles + [meld] + + selected_tile = self.player.ai.hand_builder.choose_tile_to_discard( + new_tiles, + closed_hand, + open_sets_34, + print_log=False ) - results.append({'shanten': shanten, 'meld': meld}) - results = sorted(results, key=lambda i: i['shanten']) - return results[0]['meld'] + final_results.append({ + 'discard_tile': selected_tile, + 'meld_print': TilesConverter.to_one_line_string([meld[0] * 4, meld[1] * 4, meld[2] * 4]), + 'meld': meld + }) + + final_results = sorted(final_results, key=lambda x: (x['discard_tile'].shanten, -x['discard_tile'].ukeire)) + + DecisionsLogger.debug(log.MELD_PREPARE, 'Options with meld calling', context=final_results) + + return final_results[0]['meld'] diff --git a/project/game/ai/first_version/strategies/tanyao.py b/project/game/ai/first_version/strategies/tanyao.py index 863002e4..93c8ffc2 100644 --- a/project/game/ai/first_version/strategies/tanyao.py +++ b/project/game/ai/first_version/strategies/tanyao.py @@ -114,38 +114,45 @@ def should_activate_strategy(self, tiles_136): return True - def determine_what_to_discard(self, closed_hand, outs_results, shanten, for_open_hand, tile_for_open_hand, - hand_was_open=False): - if tile_for_open_hand: - tile_for_open_hand //= 4 - - is_open_hand = self.player.is_open_hand or hand_was_open + def determine_what_to_discard(self, discard_options, hand, open_melds): + is_open_hand = len(open_melds) > 0 # our hand is closed, we don't need to discard terminal tiles here if not is_open_hand: - return outs_results + return discard_options + + first_option = sorted(discard_options, key=lambda x: x.shanten)[0] + shanten = first_option.shanten + + if shanten > 1: + return super(TanyaoStrategy, self).determine_what_to_discard( + discard_options, + hand, + open_melds + ) + + results = [] + not_suitable_tiles = [] + for item in discard_options: + if not self.is_tile_suitable(item.tile_to_discard * 4): + item.had_to_be_discarded = True + not_suitable_tiles.append(item) + continue - if shanten == 0 and is_open_hand: - results = [] # there is no sense to wait 1-4 if we have open hand - for item in outs_results: - all_waiting_are_fine = all([self.is_tile_suitable(x * 4) for x in item.waiting]) - if all_waiting_are_fine: - results.append(item) - - # we don't have a choice - # we had to have on bad wait - if not results: - return outs_results - - return results - - return super(TanyaoStrategy, self).determine_what_to_discard(closed_hand, - outs_results, - shanten, - for_open_hand, - tile_for_open_hand, - hand_was_open) + all_waiting_are_fine = all([self.is_tile_suitable(x * 4) for x in item.waiting]) + if all_waiting_are_fine: + results.append(item) + + if not_suitable_tiles: + return not_suitable_tiles + + # we don't have a choice + # we had to have on bad wait + if not results: + return discard_options + + return results def is_tile_suitable(self, tile): """ diff --git a/project/game/ai/first_version/strategies/yakuhai.py b/project/game/ai/first_version/strategies/yakuhai.py index f0de35a2..4a7d5bb2 100644 --- a/project/game/ai/first_version/strategies/yakuhai.py +++ b/project/game/ai/first_version/strategies/yakuhai.py @@ -99,40 +99,39 @@ def is_tile_suitable(self, tile): """ return True - def determine_what_to_discard(self, closed_hand, outs_results, shanten, for_open_hand, tile_for_open_hand, - hand_was_open=False): - if tile_for_open_hand: - tile_for_open_hand //= 4 - - tiles_34 = TilesConverter.to_34_array(self.player.tiles) - valued_pairs = [x for x in self.player.valued_honors if tiles_34[x] == 2] - valued_pons = [x for x in self.player.valued_honors if tiles_34[x] == 3] - - # when we trying to open hand with tempai state, we need to chose a valued pair waiting - if shanten == 0 and valued_pairs and for_open_hand and tile_for_open_hand not in valued_pairs: - valued_pair = valued_pairs[0] - - results = [] - for item in outs_results: - if valued_pair in item.waiting: - results.append(item) - return results - - for item in outs_results: - for valued_pair in valued_pairs: - if valued_pair == item.tile_to_discard: - item.had_to_be_saved = True - - for valued_pon in valued_pons: - if valued_pon == item.tile_to_discard: - item.had_to_be_saved = True - - return super(YakuhaiStrategy, self).determine_what_to_discard(closed_hand, - outs_results, - shanten, - for_open_hand, - tile_for_open_hand, - hand_was_open) + def determine_what_to_discard(self, discard_options, hand, open_melds): + return discard_options + # if tile_for_open_hand: + # tile_for_open_hand //= 4 + # + # tiles_34 = TilesConverter.to_34_array(self.player.tiles) + # valued_pairs = [x for x in self.player.valued_honors if tiles_34[x] == 2] + # valued_pons = [x for x in self.player.valued_honors if tiles_34[x] == 3] + # + # # when we trying to open hand with tempai state, we need to chose a valued pair waiting + # if shanten == 0 and valued_pairs and for_open_hand and tile_for_open_hand not in valued_pairs: + # valued_pair = valued_pairs[0] + # + # results = [] + # for item in outs_results: + # if valued_pair in item.waiting: + # results.append(item) + # return results + # + # for item in outs_results: + # for valued_pair in valued_pairs: + # if valued_pair == item.tile_to_discard: + # item.had_to_be_saved = True + # + # for valued_pon in valued_pons: + # if valued_pon == item.tile_to_discard: + # item.had_to_be_saved = True + # + # return super(YakuhaiStrategy, self).determine_what_to_discard(closed_hand, + # outs_results, + # shanten, + # for_open_hand, + # tile_for_open_hand) def meld_had_to_be_called(self, tile): tile //= 4 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 0e809107..34c1acd2 100644 --- a/project/game/ai/first_version/tests/strategies/tests_chinitsu.py +++ b/project/game/ai/first_version/tests/strategies/tests_chinitsu.py @@ -122,8 +122,6 @@ def test_suitable_tiles(self): tile = self._string_to_136_tile(honors='1') self.assertEqual(strategy.is_tile_suitable(tile), False) - # issue #84 - @unittest.expectedFailure def test_open_suit_same_shanten(self): table = Table() player = table.player 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 e0569f71..ca604924 100644 --- a/project/game/ai/first_version/tests/strategies/tests_honitsu.py +++ b/project/game/ai/first_version/tests/strategies/tests_honitsu.py @@ -101,11 +101,11 @@ def test_open_hand_and_discard_tiles_logic(self): # any honor tile is suitable tile = self._string_to_136_tile(honors='2') - meld, tile_to_discard = player.try_to_call_meld(tile, False) + meld, discard_option = player.try_to_call_meld(tile, False) self.assertNotEqual(meld, None) - self.assertEqual(self._to_string([tile_to_discard * 4]), '2m') + self.assertEqual(self._to_string([discard_option.tile_to_discard * 4]), '2m') - player.discard_tile(tile_to_discard * 4) + player.discard_tile(discard_option) tile = self._string_to_136_tile(man='1') player.draw_tile(tile) @@ -197,8 +197,6 @@ def test_open_hand_and_not_go_for_chiitoitsu(self): self.assertNotEqual(meld, None) self.assertEqual(self._to_string(meld.tiles), '555z') - # issue #84 - @unittest.expectedFailure def test_open_suit_same_shanten(self): table = Table() player = table.player 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 f80fc0f4..ee5eb6cb 100644 --- a/project/game/ai/first_version/tests/strategies/tests_tanyao.py +++ b/project/game/ai/first_version/tests/strategies/tests_tanyao.py @@ -146,10 +146,10 @@ def test_open_hand_and_discard_tiles_logic(self): tile = self._string_to_136_tile(sou='4') meld, discard_option = self.player.try_to_call_meld(tile, True) - discarded_tile = self.player.discard_tile(discard_option) - self.assertNotEqual(meld, None) - self.assertEqual(self._to_string([discarded_tile]), '4z') + self.assertEqual(self._to_string([discard_option.tile_to_discard * 4]), '4z') + + self.player.discard_tile(discard_option) tile = self._string_to_136_tile(pin='5') self.player.draw_tile(tile) diff --git a/project/game/ai/first_version/tests/tests_ai.py b/project/game/ai/first_version/tests/tests_ai.py index fe27dea5..25c86d4a 100644 --- a/project/game/ai/first_version/tests/tests_ai.py +++ b/project/game/ai/first_version/tests/tests_ai.py @@ -105,6 +105,36 @@ def test_chose_right_set_to_open_hand(self): self.assertEqual(meld.type, Meld.PON) self.assertEqual(self._to_string(meld.tiles), '555p') + table = Table() + table.has_open_tanyao = True + player = table.player + # add 3 doras so we are sure to go for tanyao + table.add_dora_indicator(self._string_to_136_tile(man='4')) + table.add_dora_indicator(self._string_to_136_tile(pin='5')) + tiles = self._string_to_136_array(man='3556', pin='234668', sou='248') + player.init_hand(tiles) + + tile = self._string_to_136_tile(man='4') + meld, _ = player.try_to_call_meld(tile, True) + self.assertNotEqual(meld, None) + self.assertEqual(meld.type, Meld.CHI) + self.assertEqual(self._to_string(meld.tiles), '345m') + + table = Table() + table.has_open_tanyao = True + player = table.player + # add 3 doras so we are sure to go for tanyao + table.add_dora_indicator(self._string_to_136_tile(man='4')) + table.add_dora_indicator(self._string_to_136_tile(pin='5')) + tiles = self._string_to_136_array(man='3445', pin='234668', sou='248') + player.init_hand(tiles) + + tile = self._string_to_136_tile(man='4') + meld, _ = player.try_to_call_meld(tile, True) + self.assertNotEqual(meld, None) + self.assertEqual(meld.type, Meld.CHI) + self.assertEqual(self._to_string(meld.tiles), '345m') + def test_not_open_hand_for_not_needed_set(self): """ We don't need to open hand if it is not improve the hand. @@ -166,20 +196,20 @@ def test_remaining_tiles_and_enemy_discard(self): tiles = self._string_to_136_array(man='123456789', sou='167', honors='77') player.init_hand(tiles) - results, shanten = player.ai.hand_builder.calculate_outs(tiles, tiles) + results, shanten = player.ai.hand_builder.find_discard_options(tiles, tiles) result = [x for x in results if x.tile_to_discard == self._string_to_34_tile(sou='1')][0] self.assertEqual(result.ukeire, 8) player.table.add_discarded_tile(1, self._string_to_136_tile(sou='5'), False) - results, shanten = player.ai.hand_builder.calculate_outs(tiles, tiles) + results, shanten = player.ai.hand_builder.find_discard_options(tiles, tiles) result = [x for x in results if x.tile_to_discard == self._string_to_34_tile(sou='1')][0] self.assertEqual(result.ukeire, 7) player.table.add_discarded_tile(2, self._string_to_136_tile(sou='5'), False) player.table.add_discarded_tile(3, self._string_to_136_tile(sou='8'), False) - results, shanten = player.ai.hand_builder.calculate_outs(tiles, tiles) + results, shanten = player.ai.hand_builder.find_discard_options(tiles, tiles) result = [x for x in results if x.tile_to_discard == self._string_to_34_tile(sou='1')][0] self.assertEqual(result.ukeire, 5) @@ -190,7 +220,7 @@ def test_remaining_tiles_and_opened_meld(self): tiles = self._string_to_136_array(man='123456789', sou='167', honors='77') player.init_hand(tiles) - results, shanten = player.ai.hand_builder.calculate_outs(tiles, tiles) + results, shanten = player.ai.hand_builder.find_discard_options(tiles, tiles) result = [x for x in results if x.tile_to_discard == self._string_to_34_tile(sou='1')][0] self.assertEqual(result.ukeire, 8) @@ -201,7 +231,7 @@ def test_remaining_tiles_and_opened_meld(self): meld.called_tile = tile player.table.add_called_meld(3, meld) - results, shanten = player.ai.hand_builder.calculate_outs(tiles, tiles) + results, shanten = player.ai.hand_builder.find_discard_options(tiles, tiles) result = [x for x in results if x.tile_to_discard == self._string_to_34_tile(sou='1')][0] self.assertEqual(result.ukeire, 5) @@ -212,7 +242,7 @@ def test_remaining_tiles_and_opened_meld(self): meld.called_tile = tile player.table.add_called_meld(2, meld) - results, shanten = player.ai.hand_builder.calculate_outs(tiles, tiles) + results, shanten = player.ai.hand_builder.find_discard_options(tiles, tiles) result = [x for x in results if x.tile_to_discard == self._string_to_34_tile(sou='1')][0] self.assertEqual(result.ukeire, 4) @@ -223,13 +253,13 @@ def test_remaining_tiles_and_dora_indicators(self): tiles = self._string_to_136_array(man='123456789', sou='167', honors='77') player.init_hand(tiles) - results, shanten = player.ai.hand_builder.calculate_outs(tiles, tiles) + results, shanten = player.ai.hand_builder.find_discard_options(tiles, tiles) result = [x for x in results if x.tile_to_discard == self._string_to_34_tile(sou='1')][0] self.assertEqual(result.ukeire, 8) table.add_dora_indicator(self._string_to_136_tile(sou='8')) - results, shanten = player.ai.hand_builder.calculate_outs(tiles, tiles) + results, shanten = player.ai.hand_builder.find_discard_options(tiles, tiles) result = [x for x in results if x.tile_to_discard == self._string_to_34_tile(sou='1')][0] self.assertEqual(result.ukeire, 7) diff --git a/project/game/ai/first_version/tests/tests_defence.py b/project/game/ai/first_version/tests/tests_defence.py index 85d092fb..c9b388d5 100644 --- a/project/game/ai/first_version/tests/tests_defence.py +++ b/project/game/ai/first_version/tests/tests_defence.py @@ -150,12 +150,6 @@ def test_should_go_for_defence_and_good_hand_with_drawn_tile(self): table.add_called_riichi(3) - results, shanten = table.player.ai.hand_builder.calculate_outs(table.player.tiles, - table.player.closed_hand, - table.player.meld_34_tiles) - selected_tile = table.player.ai.hand_builder.process_discard_options_and_select_tile_to_discard(results, shanten) - - self.assertEqual(table.player.ai.defence.should_go_to_defence_mode(selected_tile), False) result = table.player.discard_tile() self.assertEqual(self._to_string([result]), '8m') diff --git a/project/utils/decisions_constants.py b/project/utils/decisions_constants.py index 8ab28785..bfeb68e1 100644 --- a/project/utils/decisions_constants.py +++ b/project/utils/decisions_constants.py @@ -11,4 +11,5 @@ INIT_HAND = 'init_hand' -CALL_MELD = 'meld' +MELD_CALL = 'meld' +MELD_PREPARE = 'meld_prepare' From b6f74402b8c89932e28a2c93bed4ecc027108e0f Mon Sep 17 00:00:00 2001 From: Nihisil Date: Fri, 28 Sep 2018 15:25:10 +0800 Subject: [PATCH 081/126] Remove commented code --- .../ai/first_version/strategies/honitsu.py | 10 ------ .../game/ai/first_version/strategies/main.py | 7 ++-- .../ai/first_version/strategies/yakuhai.py | 34 ------------------- 3 files changed, 5 insertions(+), 46 deletions(-) diff --git a/project/game/ai/first_version/strategies/honitsu.py b/project/game/ai/first_version/strategies/honitsu.py index 692de637..dd95fdd5 100644 --- a/project/game/ai/first_version/strategies/honitsu.py +++ b/project/game/ai/first_version/strategies/honitsu.py @@ -138,16 +138,6 @@ def meld_had_to_be_called(self, tile): return False - def determine_what_to_discard(self, discard_options, hand, open_melds): - first_option = sorted(discard_options, key=lambda x: x.shanten)[0] - shanten = first_option.shanten - - # we can riichi our hand, so let's not destroy it with not suitable tiles discarding - if shanten == 0 and not self.player.is_open_hand: - return discard_options - else: - return super(HonitsuStrategy, self).determine_what_to_discard(discard_options, hand, open_melds) - def _calculate_not_suitable_tiles_cnt(self, tiles_34, suit): self.tiles_count_other_suits = 0 self.tiles_count_other_suits_not_isolated = 0 diff --git a/project/game/ai/first_version/strategies/main.py b/project/game/ai/first_version/strategies/main.py index 91ebc6b4..20107afe 100644 --- a/project/game/ai/first_version/strategies/main.py +++ b/project/game/ai/first_version/strategies/main.py @@ -81,9 +81,12 @@ def is_tile_suitable(self, tile): raise NotImplemented() def determine_what_to_discard(self, discard_options, hand, open_melds): + first_option = sorted(discard_options, key=lambda x: x.shanten)[0] + shanten = first_option.shanten + # for riichi we don't need to discard useful tiles - # if shanten == 0 and not self.player.is_open_hand: - # return outs_results + if shanten == 0 and not self.player.is_open_hand: + return discard_options # mark all not suitable tiles as ready to discard # even if they not should be discarded by uke-ire diff --git a/project/game/ai/first_version/strategies/yakuhai.py b/project/game/ai/first_version/strategies/yakuhai.py index 4a7d5bb2..e5da8065 100644 --- a/project/game/ai/first_version/strategies/yakuhai.py +++ b/project/game/ai/first_version/strategies/yakuhai.py @@ -99,40 +99,6 @@ def is_tile_suitable(self, tile): """ return True - def determine_what_to_discard(self, discard_options, hand, open_melds): - return discard_options - # if tile_for_open_hand: - # tile_for_open_hand //= 4 - # - # tiles_34 = TilesConverter.to_34_array(self.player.tiles) - # valued_pairs = [x for x in self.player.valued_honors if tiles_34[x] == 2] - # valued_pons = [x for x in self.player.valued_honors if tiles_34[x] == 3] - # - # # when we trying to open hand with tempai state, we need to chose a valued pair waiting - # if shanten == 0 and valued_pairs and for_open_hand and tile_for_open_hand not in valued_pairs: - # valued_pair = valued_pairs[0] - # - # results = [] - # for item in outs_results: - # if valued_pair in item.waiting: - # results.append(item) - # return results - # - # for item in outs_results: - # for valued_pair in valued_pairs: - # if valued_pair == item.tile_to_discard: - # item.had_to_be_saved = True - # - # for valued_pon in valued_pons: - # if valued_pon == item.tile_to_discard: - # item.had_to_be_saved = True - # - # return super(YakuhaiStrategy, self).determine_what_to_discard(closed_hand, - # outs_results, - # shanten, - # for_open_hand, - # tile_for_open_hand) - def meld_had_to_be_called(self, tile): tile //= 4 tiles_34 = TilesConverter.to_34_array(self.player.tiles) From 2393360db0acb35bcb922f322217c13f85c31d35 Mon Sep 17 00:00:00 2001 From: bogachev-pa Date: Fri, 2 Nov 2018 02:09:08 +0300 Subject: [PATCH 082/126] add more yakuhai tests --- .../tests/strategies/tests_yakuhai.py | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) 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 ad6f2afa..ded418d4 100644 --- a/project/game/ai/first_version/tests/strategies/tests_yakuhai.py +++ b/project/game/ai/first_version/tests/strategies/tests_yakuhai.py @@ -354,6 +354,27 @@ def test_keep_yakuhai_in_closed_hand(self): discard = self.player.discard_tile() self.assertNotEqual(self._to_string([discard]), '7z') + @unittest.expectedFailure + def test_keep_yakuhai_pon(self): + # make sure yakuhai strategy is activated by adding 3 doras to the hand + table = Table() + player = table.player + table.add_dora_indicator(self._string_to_136_tile(man='9')) + table.add_dora_indicator(self._string_to_136_tile(man='3')) + + tiles = self._string_to_136_array(man='11144', sou='567', pin='56', honors='777') + player.init_hand(tiles) + + 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) + + player.draw_tile(self._string_to_136_tile(man='4')) + discarded_tile = player.discard_tile() + self.assertNotEqual(self._to_string([discarded_tile]), '7z') + # issue #88 @unittest.expectedFailure def test_atodzuke_keep_yakuhai_wait(self): From 34c18c33bad88f21d3a71cac13d9d36a612cd6e6 Mon Sep 17 00:00:00 2001 From: bogachev-pa Date: Fri, 2 Nov 2018 03:13:13 +0300 Subject: [PATCH 083/126] add tests for correcly melding sets with dora #51 --- .../game/ai/first_version/tests/tests_ai.py | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/project/game/ai/first_version/tests/tests_ai.py b/project/game/ai/first_version/tests/tests_ai.py index 25c86d4a..cdf2054a 100644 --- a/project/game/ai/first_version/tests/tests_ai.py +++ b/project/game/ai/first_version/tests/tests_ai.py @@ -135,6 +135,73 @@ def test_chose_right_set_to_open_hand(self): self.assertEqual(meld.type, Meld.CHI) self.assertEqual(self._to_string(meld.tiles), '345m') + table = Table() + table.has_open_tanyao = True + player = table.player + # add 3 doras so we are sure to go for tanyao + table.add_dora_indicator(self._string_to_136_tile(man='7')) + tiles = self._string_to_136_array(man='567888', pin='788', sou='3456') + player.init_hand(tiles) + + tile = self._string_to_136_tile(sou='4') + meld, _ = player.try_to_call_meld(tile, True) + self.assertNotEqual(meld, None) + self.assertEqual(meld.type, Meld.CHI) + self.assertEqual(self._to_string(meld.tiles), '456s') + + tile = self._string_to_136_tile(sou='5') + meld, _ = player.try_to_call_meld(tile, True) + self.assertNotEqual(meld, None) + self.assertEqual(meld.type, Meld.CHI) + self.assertEqual(self._to_string(meld.tiles), '345s') + + table = Table() + table.has_open_tanyao = True + player = table.player + # add 3 doras so we are sure to go for tanyao + table.add_dora_indicator(self._string_to_136_tile(man='7')) + tiles = self._string_to_136_array(man='567888', pin='788', sou='2345') + player.init_hand(tiles) + + tile = self._string_to_136_tile(sou='4') + meld, _ = player.try_to_call_meld(tile, True) + self.assertNotEqual(meld, None) + self.assertEqual(meld.type, Meld.CHI) + self.assertEqual(self._to_string(meld.tiles), '234s') + + # issue #51 + @unittest.expectedFailure + def test_chose_right_set_to_open_hand_dora(self): + table = Table() + table.has_open_tanyao = True + player = table.player + # add 3 doras so we are sure to go for tanyao + table.add_dora_indicator(self._string_to_136_tile(man='7')) + table.add_dora_indicator(self._string_to_136_tile(sou='1')) + tiles = self._string_to_136_array(man='3456788', sou='245888') + player.init_hand(tiles) + + tile = self._string_to_136_tile(sou='3') + meld, _ = player.try_to_call_meld(tile, True) + self.assertNotEqual(meld, None) + self.assertEqual(meld.type, Meld.CHI) + self.assertEqual(self._to_string(meld.tiles), '234s') + + table = Table() + table.has_open_tanyao = True + player = table.player + # add 3 doras so we are sure to go for tanyao + table.add_dora_indicator(self._string_to_136_tile(man='7')) + table.add_dora_indicator(self._string_to_136_tile(sou='4')) + tiles = self._string_to_136_array(man='3456788', sou='245888') + player.init_hand(tiles) + + tile = self._string_to_136_tile(sou='3') + meld, _ = player.try_to_call_meld(tile, True) + self.assertNotEqual(meld, None) + self.assertEqual(meld.type, Meld.CHI) + self.assertEqual(self._to_string(meld.tiles), '345s') + def test_not_open_hand_for_not_needed_set(self): """ We don't need to open hand if it is not improve the hand. From 85169904ac6b1e803665d5bae3d72c408c9bcf2b Mon Sep 17 00:00:00 2001 From: bogachev-pa Date: Mon, 5 Nov 2018 17:02:46 +0300 Subject: [PATCH 084/126] consider tiles valuation when choosing best melding option #51 --- .../game/ai/first_version/strategies/main.py | 4 +- .../game/ai/first_version/tests/tests_ai.py | 38 ++++++++++++++++++- 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/project/game/ai/first_version/strategies/main.py b/project/game/ai/first_version/strategies/main.py index 20107afe..708a4865 100644 --- a/project/game/ai/first_version/strategies/main.py +++ b/project/game/ai/first_version/strategies/main.py @@ -291,7 +291,9 @@ def _find_best_meld_to_open(self, possible_melds, new_tiles, closed_hand): 'meld': meld }) - final_results = sorted(final_results, key=lambda x: (x['discard_tile'].shanten, -x['discard_tile'].ukeire)) + final_results = sorted(final_results, key=lambda x: (x['discard_tile'].shanten, + -x['discard_tile'].ukeire, + x['discard_tile'].valuation)) DecisionsLogger.debug(log.MELD_PREPARE, 'Options with meld calling', context=final_results) diff --git a/project/game/ai/first_version/tests/tests_ai.py b/project/game/ai/first_version/tests/tests_ai.py index cdf2054a..202afb82 100644 --- a/project/game/ai/first_version/tests/tests_ai.py +++ b/project/game/ai/first_version/tests/tests_ai.py @@ -169,11 +169,10 @@ def test_chose_right_set_to_open_hand(self): self.assertEqual(meld.type, Meld.CHI) self.assertEqual(self._to_string(meld.tiles), '234s') - # issue #51 - @unittest.expectedFailure def test_chose_right_set_to_open_hand_dora(self): table = Table() table.has_open_tanyao = True + table.has_aka_dora = False player = table.player # add 3 doras so we are sure to go for tanyao table.add_dora_indicator(self._string_to_136_tile(man='7')) @@ -189,6 +188,7 @@ def test_chose_right_set_to_open_hand_dora(self): table = Table() table.has_open_tanyao = True + table.has_aka_dora = False player = table.player # add 3 doras so we are sure to go for tanyao table.add_dora_indicator(self._string_to_136_tile(man='7')) @@ -202,6 +202,40 @@ def test_chose_right_set_to_open_hand_dora(self): self.assertEqual(meld.type, Meld.CHI) self.assertEqual(self._to_string(meld.tiles), '345s') + table = Table() + table.has_open_tanyao = True + table.has_aka_dora = True + player = table.player + # add 3 doras so we are sure to go for tanyao + table.add_dora_indicator(self._string_to_136_tile(man='7')) + # 5 from string is always aka + tiles = self._string_to_136_array(man='3456788', sou='245888') + player.init_hand(tiles) + + tile = self._string_to_136_tile(sou='3') + meld, _ = player.try_to_call_meld(tile, True) + self.assertNotEqual(meld, None) + self.assertEqual(meld.type, Meld.CHI) + self.assertEqual(self._to_string(meld.tiles), '345s') + + table = Table() + table.has_open_tanyao = True + table.has_aka_dora = True + player = table.player + # add 3 doras so we are sure to go for tanyao + table.add_dora_indicator(self._string_to_136_tile(man='7')) + table.add_dora_indicator(self._string_to_136_tile(sou='1')) + table.add_dora_indicator(self._string_to_136_tile(sou='4')) + # double dora versus regular dora, we should keep double dora + tiles = self._string_to_136_array(man='3456788', sou='245888') + player.init_hand(tiles) + + tile = self._string_to_136_tile(sou='3') + meld, _ = player.try_to_call_meld(tile, True) + self.assertNotEqual(meld, None) + self.assertEqual(meld.type, Meld.CHI) + self.assertEqual(self._to_string(meld.tiles), '345s') + def test_not_open_hand_for_not_needed_set(self): """ We don't need to open hand if it is not improve the hand. From 01c97663e27f785f618a9483508e1649a95747d0 Mon Sep 17 00:00:00 2001 From: bogachev-pa Date: Mon, 5 Nov 2018 18:43:56 +0300 Subject: [PATCH 085/126] add/restore discarding rules for yakuhai strategy #88 --- .../ai/first_version/strategies/yakuhai.py | 41 +++++++++++++++++++ .../tests/strategies/tests_yakuhai.py | 36 ++++++++++++---- 2 files changed, 68 insertions(+), 9 deletions(-) diff --git a/project/game/ai/first_version/strategies/yakuhai.py b/project/game/ai/first_version/strategies/yakuhai.py index e5da8065..b6d3bde6 100644 --- a/project/game/ai/first_version/strategies/yakuhai.py +++ b/project/game/ai/first_version/strategies/yakuhai.py @@ -91,6 +91,47 @@ def should_activate_strategy(self, tiles_136): return False + def determine_what_to_discard(self, discard_options, hand, open_melds): + is_open_hand = len(open_melds) > 0 + + tiles_34 = TilesConverter.to_34_array(hand) + + valued_pairs = [x for x in self.player.valued_honors if tiles_34[x] == 2] + valued_pons = [x for x in self.player.valued_honors if tiles_34[x] == 3] + + acceptable_options = [] + for item in discard_options: + if is_open_hand: + if len(valued_pons) == 0: + # don't destroy our only yakuhai pair + if len(valued_pairs) == 1 and item.tile_to_discard in valued_pairs: + continue + elif len(valued_pons) == 1: + # don't destroy our only yakuhai pon + if item.tile_to_discard in valued_pons: + continue + + acceptable_options.append(item) + + # we don't have a choice + if not acceptable_options: + return discard_options + + preferred_options = [] + for item in acceptable_options: + # ignore wait without yakuhai yaku if possible + if is_open_hand and len(valued_pons) == 0 and len(valued_pairs) == 1: + if item.shanten == 0: + if valued_pairs[0] not in item.waiting: + continue + + preferred_options.append(item) + + if not preferred_options: + return acceptable_options + + return preferred_options + def is_tile_suitable(self, tile): """ For yakuhai we don't have any limits 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 ded418d4..fca46d5e 100644 --- a/project/game/ai/first_version/tests/strategies/tests_yakuhai.py +++ b/project/game/ai/first_version/tests/strategies/tests_yakuhai.py @@ -92,6 +92,9 @@ def test_force_yakuhai_pair_waiting_for_tempai_hand(self): tiles = self._string_to_136_array(man='44556', sou='366789', honors='77') self.player.init_hand(tiles) + strategy = YakuhaiStrategy(BaseStrategy.YAKUHAI, self.player) + self.assertEqual(strategy.should_activate_strategy(self.player.tiles), True) + tile = self._string_to_136_tile(honors='7') meld, _ = self.player.try_to_call_meld(tile, True) self.assertNotEqual(meld, None) @@ -354,8 +357,7 @@ def test_keep_yakuhai_in_closed_hand(self): discard = self.player.discard_tile() self.assertNotEqual(self._to_string([discard]), '7z') - @unittest.expectedFailure - def test_keep_yakuhai_pon(self): + def test_keep_only_yakuhai_pon(self): # make sure yakuhai strategy is activated by adding 3 doras to the hand table = Table() player = table.player @@ -375,8 +377,28 @@ def test_keep_yakuhai_pon(self): discarded_tile = player.discard_tile() self.assertNotEqual(self._to_string([discarded_tile]), '7z') - # issue #88 - @unittest.expectedFailure + def test_keep_only_yakuhai_pair(self): + # make sure yakuhai strategy is activated by adding 3 doras to the hand + table = Table() + player = table.player + table.add_dora_indicator(self._string_to_136_tile(man='9')) + table.add_dora_indicator(self._string_to_136_tile(man='3')) + + table.add_discarded_tile(1, self._string_to_136_tile(honors='7'), False) + + tiles = self._string_to_136_array(man='11144', sou='567', pin='156', honors='77') + player.init_hand(tiles) + + 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) + + player.draw_tile(self._string_to_136_tile(pin='1')) + discarded_tile = player.discard_tile() + self.assertNotEqual(self._to_string([discarded_tile]), '7z') + def test_atodzuke_keep_yakuhai_wait(self): # make sure yakuhai strategy is activated by adding 3 doras to the hand table = Table() @@ -401,7 +423,7 @@ def test_atodzuke_keep_yakuhai_wait(self): discarded_tile = player.discard_tile() self.assertEqual(self._to_string([discarded_tile]), '2m') - # issue #88 + # issue #98 @unittest.expectedFailure def test_atodzuke_dont_destroy_second_pair(self): # make sure yakuhai strategy is activated by adding 3 doras to the hand @@ -446,8 +468,6 @@ def test_atodzuke_dont_destroy_second_pair(self): meld, _ = player.try_to_call_meld(tile, True) self.assertNotEqual(meld, None) - # issue #88 - @unittest.expectedFailure def test_atodzuke_dont_open_no_yaku_tempai(self): # make sure yakuhai strategy is activated by adding 3 doras to the hand table = Table() @@ -477,8 +497,6 @@ def test_atodzuke_dont_open_no_yaku_tempai(self): meld, _ = player.try_to_call_meld(tile, True) self.assertNotEqual(meld, None) - # issue #88 - @unittest.expectedFailure def test_atodzuke_choose_hidden_syanpon(self): # make sure yakuhai strategy is activated by adding 3 doras to the hand table = Table() From 32537a42dc6d236c90bb4143868405e13cfb409a Mon Sep 17 00:00:00 2001 From: bogachev-pa Date: Wed, 7 Nov 2018 00:57:04 +0300 Subject: [PATCH 086/126] add tests for chosing correct wait in tempai #61 --- .../ai/first_version/tests/tests_discards.py | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/project/game/ai/first_version/tests/tests_discards.py b/project/game/ai/first_version/tests/tests_discards.py index 2749fb72..7086860d 100644 --- a/project/game/ai/first_version/tests/tests_discards.py +++ b/project/game/ai/first_version/tests/tests_discards.py @@ -368,3 +368,86 @@ def test_discard_tile_and_wrong_tiles_valuation(self): discarded_tile = player.discard_tile() self.assertEqual(self._to_string([discarded_tile]), '8s') + + # issue #61 + @unittest.expectedFailure + def test_choose_correct_wait_finished_yaku(self): + table = Table() + player = table.player + player.round_step = 2 + + tiles = self._string_to_136_array(man='23478', sou='23488', pin='235') + player.init_hand(tiles) + player.draw_tile(self._string_to_136_tile(pin='4')) + discarded_tile = player.discard_tile() + self.assertEqual(self._to_string([discarded_tile]), '5p') + + tiles = self._string_to_136_array(man='34578', sou='34588', pin='235') + player.init_hand(tiles) + player.draw_tile(self._string_to_136_tile(pin='4')) + discarded_tile = player.discard_tile() + self.assertEqual(self._to_string([discarded_tile]), '2p') + + tiles = self._string_to_136_array(man='34578', sou='34588', pin='235') + player.init_hand(tiles) + player.draw_tile(self._string_to_136_tile(pin='4')) + discarded_tile = player.discard_tile() + self.assertEqual(self._to_string([discarded_tile]), '2p') + + tiles = self._string_to_136_array(man='3457', sou='233445588') + player.init_hand(tiles) + player.draw_tile(self._string_to_136_tile(man='8')) + discarded_tile = player.discard_tile() + self.assertEqual(self._to_string([discarded_tile]), '2s') + + tiles = self._string_to_136_array(man='3457', sou='223344588') + player.init_hand(tiles) + player.draw_tile(self._string_to_136_tile(man='8')) + discarded_tile = player.discard_tile() + self.assertEqual(self._to_string([discarded_tile]), '5s') + + # issue #61 + @unittest.expectedFailure + def test_choose_correct_wait_yaku_versus_dora(self): + table = Table() + player = table.player + player.round_step = 2 + + table.add_dora_indicator(self._string_to_136_tile(pin='4')) + + tiles = self._string_to_136_array(man='23478', sou='23488', pin='235') + player.init_hand(tiles) + player.draw_tile(self._string_to_136_tile(pin='4')) + discarded_tile = player.discard_tile() + self.assertEqual(self._to_string([discarded_tile]), '5p') + + table = Table() + player = table.player + player.round_step = 2 + + table.add_dora_indicator(self._string_to_136_tile(pin='1')) + + tiles = self._string_to_136_array(man='23478', sou='23488', pin='235') + player.init_hand(tiles) + player.draw_tile(self._string_to_136_tile(pin='4')) + discarded_tile = player.discard_tile() + self.assertEqual(self._to_string([discarded_tile]), '2p') + + # issue #61 + @unittest.expectedFailure + def test_choose_correct_wait_yaku_potentially(self): + table = Table() + player = table.player + player.round_step = 2 + + tiles = self._string_to_136_array(man='1134578', sou='567788') + player.init_hand(tiles) + player.draw_tile(self._string_to_136_tile(man='9')) + discarded_tile = player.discard_tile() + self.assertEqual(self._to_string([discarded_tile]), '5s') + + tiles = self._string_to_136_array(man='1134578', sou='556678') + player.init_hand(tiles) + player.draw_tile(self._string_to_136_tile(man='9')) + discarded_tile = player.discard_tile() + self.assertEqual(self._to_string([discarded_tile]), '8s') From 153853346a4efa3ea04187225cbf61bccef9fec0 Mon Sep 17 00:00:00 2001 From: bogachev-pa Date: Fri, 9 Nov 2018 03:09:59 +0300 Subject: [PATCH 087/126] when in tempai, choose tile to discard according to (cost * ukeire) value #61 --- project/game/ai/first_version/hand_builder.py | 64 +++++++++++++++++-- .../tests/strategies/tests_yakuhai.py | 3 +- .../ai/first_version/tests/tests_defence.py | 19 ++++-- .../ai/first_version/tests/tests_discards.py | 10 +-- 4 files changed, 79 insertions(+), 17 deletions(-) diff --git a/project/game/ai/first_version/hand_builder.py b/project/game/ai/first_version/hand_builder.py index 26ca0d66..0a561173 100644 --- a/project/game/ai/first_version/hand_builder.py +++ b/project/game/ai/first_version/hand_builder.py @@ -132,6 +132,57 @@ def count_tiles(self, waiting, tiles_34): n += 4 - self.player.total_tiles(tile_34, tiles_34) return n + # FIXME: add special handling for tanki waits + def _choose_best_discard_in_tempai(self, discard_options): + # first of all we find tiles that have the best hand cost * ukeire value + best_cost_x_ukeire = 0 + best_discard_options = [] + 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.remove(tile) + + cost_x_ukeire = 0 + if len(discard_option.waiting) == 1: + waiting = discard_option.waiting[0] + hand_value = self.player.ai.estimate_hand_value(waiting, call_riichi=True) + if hand_value.error is None: + hand_cost = hand_value.cost['main'] + cost_x_ukeire = hand_cost * discard_option.ukeire + else: + cost_x_ukeire_sum = 0 + for waiting in discard_option.waiting: + hand_value = self.player.ai.estimate_hand_value(waiting, call_riichi=True) + if hand_value.error is None: + cost_x_ukeire_sum += hand_value.cost['main'] * discard_option.ukeire + + cost_x_ukeire = cost_x_ukeire_sum / len(discard_option.waiting) + + if cost_x_ukeire == best_cost_x_ukeire: + best_discard_options.append(discard_option) + + if cost_x_ukeire > best_cost_x_ukeire: + best_cost_x_ukeire = cost_x_ukeire + best_discard_options.clear() + best_discard_options.append(discard_option) + + # return tile back to hand + self.player.tiles.append(tile) + + # we only have one best option based on ukeire and cost, nothing more to do here + if len(best_discard_options) == 1: + return best_discard_options[0] + + # if we have several options that give us similar wait + if len(best_discard_options) > 1: + # 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_options[0] + + # if we don't have any good options, e.g. all our possible waits ara karaten + # FIXME: in that case, discard the safest tile + return sorted(discard_options, key=lambda x: x.valuation)[0] + def choose_tile_to_discard(self, tiles, closed_hand, open_sets_34, print_log=True): """ Try to find best tile to discard, based on different rules @@ -197,6 +248,11 @@ def choose_tile_to_discard(self, tiles, closed_hand, open_sets_34, print_log=Tru ukeire_field = 'ukeire' possible_options = sorted(possible_options, key=lambda x: -getattr(x, ukeire_field)) + # 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] + return self._choose_best_discard_in_tempai(other_tiles_with_same_shanten) + tiles_without_dora = [x for x in possible_options if x.count_of_dora == 0] # we have only dora candidates to discard @@ -224,7 +280,7 @@ def choose_tile_to_discard(self, tiles, closed_hand, open_sets_34, print_log=Tru else: best_option_without_dora = tiles_without_dora[0] ukeire_borders = self._choose_ukeire_borders(best_option_without_dora, 10, ukeire_field) - filtered_options = [best_option_without_dora] + filtered_options = [] for discard_option in tiles_without_dora: val = getattr(best_option_without_dora, ukeire_field) - ukeire_borders if getattr(discard_option, ukeire_field) >= val: @@ -243,8 +299,8 @@ def choose_tile_to_discard(self, tiles, closed_hand, open_sets_34, print_log=Tru # let's sort tiles by value and let's choose less valuable tile to discard return sorted(isolated_tiles, key=lambda x: x.valuation)[0] - # there are no isolated tiles - # let's discard tile with greater ukeire2 + # there are no isolated tiles or we don't care about them + # let's discard tile with greater ukeire/ukeire2 filtered_options = sorted(filtered_options, key=lambda x: -getattr(x, ukeire_field)) first_option = filtered_options[0] @@ -252,8 +308,8 @@ def choose_tile_to_discard(self, tiles, closed_hand, open_sets_34, print_log=Tru if getattr(x, ukeire_field) == getattr(first_option, ukeire_field)] # it will happen with shanten=1, all tiles will have ukeire_second == 0 + # or in tempai we can have several tiles with same ukeire if other_tiles_with_same_ukeire: - # let's sort tiles by value and let's choose less valuable tile to discard return sorted(other_tiles_with_same_ukeire, key=lambda x: x.valuation)[0] # we have only one candidate to discard with greater ukeire 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 fca46d5e..b113e896 100644 --- a/project/game/ai/first_version/tests/strategies/tests_yakuhai.py +++ b/project/game/ai/first_version/tests/strategies/tests_yakuhai.py @@ -110,7 +110,8 @@ def test_tempai_without_yaku(self): self.player.add_called_meld(meld) discard = self.player.discard_tile() - self.assertEqual(self._to_string([discard]), '1p') + self.assertNotEqual(self._to_string([discard]), '7z') + self.assertNotEqual(self._to_string([discard]), '5p') def test_wrong_shanten_improvements_detection(self): """ diff --git a/project/game/ai/first_version/tests/tests_defence.py b/project/game/ai/first_version/tests/tests_defence.py index c9b388d5..8bbe4a0c 100644 --- a/project/game/ai/first_version/tests/tests_defence.py +++ b/project/game/ai/first_version/tests/tests_defence.py @@ -245,9 +245,13 @@ def test_try_to_discard_less_valuable_tiles_first_in_defence_mode(self): def test_find_impossible_waits_and_honor_tiles(self): table = Table() - tiles = self._string_to_136_array(honors='1133') + tiles = self._string_to_136_array(honors='1133', man='123', sou='456', pin='999') table.player.init_hand(tiles) + table.player.add_called_meld(self._make_meld(Meld.CHI, man='123')) + table.player.add_called_meld(self._make_meld(Meld.CHI, sou='456')) + table.player.add_called_meld(self._make_meld(Meld.PON, pin='999')) + table.add_discarded_tile(1, self._string_to_136_tile(honors='1'), False) table.add_discarded_tile(1, self._string_to_136_tile(honors='3'), False) @@ -262,9 +266,11 @@ def test_find_impossible_waits_and_honor_tiles(self): def test_find_impossible_waits_and_kabe_technique(self): table = Table() - tiles = self._string_to_136_array(pin='11122777799') + tiles = self._string_to_136_array(pin='11122777799', man='999') table.player.init_hand(tiles) + table.player.add_called_meld(self._make_meld(Meld.PON, man='999')) + table.add_discarded_tile(1, self._string_to_136_tile(pin='2'), False) table.add_discarded_tile(1, self._string_to_136_tile(pin='2'), False) table.add_discarded_tile(1, self._string_to_136_tile(pin='9'), False) @@ -278,9 +284,12 @@ def test_find_impossible_waits_and_kabe_technique(self): self.assertEqual(self._to_string([x.value * 4 for x in result]), '19p') table = Table() - tiles = self._string_to_136_array(pin='33337777') + tiles = self._string_to_136_array(pin='33337777', man='888999') table.player.init_hand(tiles) + table.player.add_called_meld(self._make_meld(Meld.PON, man='888')) + table.player.add_called_meld(self._make_meld(Meld.PON, man='999')) + table.add_discarded_tile(1, self._string_to_136_tile(pin='5'), False) table.add_discarded_tile(1, self._string_to_136_tile(pin='5'), False) table.add_discarded_tile(1, self._string_to_136_tile(pin='5'), False) @@ -294,9 +303,11 @@ def test_find_impossible_waits_and_kabe_technique(self): self.assertEqual(self._to_string([x.value * 4 for x in result]), '5p') table = Table() - tiles = self._string_to_136_array(pin='33334446666') + tiles = self._string_to_136_array(pin='33334446666', man='999') table.player.init_hand(tiles) + table.player.add_called_meld(self._make_meld(Meld.PON, man='999')) + table.add_discarded_tile(1, self._string_to_136_tile(pin='5'), False) table.add_discarded_tile(1, self._string_to_136_tile(pin='5'), False) table.add_discarded_tile(1, self._string_to_136_tile(pin='5'), False) diff --git a/project/game/ai/first_version/tests/tests_discards.py b/project/game/ai/first_version/tests/tests_discards.py index 7086860d..78093fdb 100644 --- a/project/game/ai/first_version/tests/tests_discards.py +++ b/project/game/ai/first_version/tests/tests_discards.py @@ -194,7 +194,7 @@ def test_discard_not_valuable_honor_first(self): table = Table() player = table.player - tiles = self._string_to_136_array(sou='123456', pin='123456', man='9', honors='2') + tiles = self._string_to_136_array(sou='123456', pin='123455', man='9', honors='2') player.init_hand(tiles) discarded_tile = player.discard_tile() @@ -369,8 +369,6 @@ def test_discard_tile_and_wrong_tiles_valuation(self): discarded_tile = player.discard_tile() self.assertEqual(self._to_string([discarded_tile]), '8s') - # issue #61 - @unittest.expectedFailure def test_choose_correct_wait_finished_yaku(self): table = Table() player = table.player @@ -406,8 +404,6 @@ def test_choose_correct_wait_finished_yaku(self): discarded_tile = player.discard_tile() self.assertEqual(self._to_string([discarded_tile]), '5s') - # issue #61 - @unittest.expectedFailure def test_choose_correct_wait_yaku_versus_dora(self): table = Table() player = table.player @@ -427,14 +423,12 @@ def test_choose_correct_wait_yaku_versus_dora(self): table.add_dora_indicator(self._string_to_136_tile(pin='1')) - tiles = self._string_to_136_array(man='23478', sou='23488', pin='235') + tiles = self._string_to_136_array(man='34578', sou='34588', pin='235') player.init_hand(tiles) player.draw_tile(self._string_to_136_tile(pin='4')) discarded_tile = player.discard_tile() self.assertEqual(self._to_string([discarded_tile]), '2p') - # issue #61 - @unittest.expectedFailure def test_choose_correct_wait_yaku_potentially(self): table = Table() player = table.player From cd78c84595a0da91fbf4dc0b5eca49b64082bd2f Mon Sep 17 00:00:00 2001 From: bogachev-pa Date: Sat, 10 Nov 2018 02:20:12 +0300 Subject: [PATCH 088/126] prepare discard choosing logic for special tanki handling #77 --- project/game/ai/first_version/hand_builder.py | 91 ++++++++++++++----- project/game/ai/first_version/riichi.py | 14 +-- 2 files changed, 72 insertions(+), 33 deletions(-) diff --git a/project/game/ai/first_version/hand_builder.py b/project/game/ai/first_version/hand_builder.py index 0a561173..eac7db1d 100644 --- a/project/game/ai/first_version/hand_builder.py +++ b/project/game/ai/first_version/hand_builder.py @@ -3,7 +3,7 @@ from mahjong.constants import AKA_DORA_LIST from mahjong.shanten import Shanten from mahjong.tile import TilesConverter -from mahjong.utils import is_tile_strictly_isolated +from mahjong.utils import is_tile_strictly_isolated, is_pair import utils.decisions_constants as log from game.ai.discard import DiscardOption @@ -132,56 +132,101 @@ def count_tiles(self, waiting, tiles_34): n += 4 - self.player.total_tiles(tile_34, tiles_34) return n - # FIXME: add special handling for tanki waits + def divide_hand(self, closed_hand, melds, waiting): + tiles = closed_hand + [waiting * 4] + closed_melds = [x for x in melds if not x.opened] + for meld in closed_melds: + tiles.extend(meld.tiles[:3]) + + tiles_34 = TilesConverter.to_34_array(tiles) + + results = self.player.ai.hand_divider.divide_hand(tiles_34) + return results, tiles_34 + + def _choose_best_tanki_wait(self, discard_desc): + # FIXME: implement + + return discard_desc[0]['discard_option'] + def _choose_best_discard_in_tempai(self, discard_options): # first of all we find tiles that have the best hand cost * ukeire value - best_cost_x_ukeire = 0 - best_discard_options = [] + call_riichi = not self.player.is_open_hand + + discard_desc = [] + 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.remove(tile) cost_x_ukeire = 0 + hand_cost = 0 if len(discard_option.waiting) == 1: waiting = discard_option.waiting[0] - hand_value = self.player.ai.estimate_hand_value(waiting, call_riichi=True) + hand_value = self.player.ai.estimate_hand_value(waiting, call_riichi=call_riichi) if hand_value.error is None: hand_cost = hand_value.cost['main'] cost_x_ukeire = hand_cost * discard_option.ukeire + + # let's check if this is a tanki wait + results = self.divide_hand(self.player.closed_hand, self.player.melds, waiting) + result = results[0] + + is_tanki = False + for hand_set in result: + if waiting not in hand_set: + continue + + if is_pair(hand_set): + is_tanki = True + break + + discard_desc.append({ + 'discard_option': discard_option, + 'hand_cost': hand_cost, + 'cost_x_ukeire': cost_x_ukeire, + 'is_tanki': is_tanki + }) else: cost_x_ukeire_sum = 0 for waiting in discard_option.waiting: - hand_value = self.player.ai.estimate_hand_value(waiting, call_riichi=True) + hand_value = self.player.ai.estimate_hand_value(waiting, call_riichi=call_riichi) if hand_value.error is None: cost_x_ukeire_sum += hand_value.cost['main'] * discard_option.ukeire cost_x_ukeire = cost_x_ukeire_sum / len(discard_option.waiting) - if cost_x_ukeire == best_cost_x_ukeire: - best_discard_options.append(discard_option) - - if cost_x_ukeire > best_cost_x_ukeire: - best_cost_x_ukeire = cost_x_ukeire - best_discard_options.clear() - best_discard_options.append(discard_option) + discard_desc.append({ + 'discard_option': discard_option, + 'hand_cost': None, + 'cost_x_ukeire': cost_x_ukeire, + 'is_tanki': False + }) # return tile back to hand self.player.tiles.append(tile) - # we only have one best option based on ukeire and cost, nothing more to do here - if len(best_discard_options) == 1: - return best_discard_options[0] - - # if we have several options that give us similar wait - if len(best_discard_options) > 1: - # 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_options[0] + discard_desc = sorted(discard_desc, key=lambda k: k['cost_x_ukeire'], reverse=True) + best_discard_desc = [x for x in discard_desc if x['cost_x_ukeire'] == discard_desc[0]['cost_x_ukeire']] + num_tanki_waits = len([x for x in discard_desc if x['is_tanki'] == 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 - return sorted(discard_options, key=lambda x: x.valuation)[0] + if discard_desc[0]['cost_x_ukeire'] == 0: + return sorted(discard_options, key=lambda x: x.valuation)[0] + + # 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) + + # 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'] def choose_tile_to_discard(self, tiles, closed_hand, open_sets_34, print_log=True): """ diff --git a/project/game/ai/first_version/riichi.py b/project/game/ai/first_version/riichi.py index 74dd0571..04d6b2bd 100644 --- a/project/game/ai/first_version/riichi.py +++ b/project/game/ai/first_version/riichi.py @@ -37,20 +37,14 @@ def should_call_riichi(self): def _should_call_riichi_one_sided(self): count_tiles = self.player.ai.hand_builder.count_tiles( - self.player.ai.waiting, - TilesConverter.to_34_array(self.player.closed_hand) + self.player.ai.waiting, TilesConverter.to_34_array(self.player.closed_hand) ) waiting = self.player.ai.waiting[0] hand_value = self.player.ai.estimate_hand_value(waiting, call_riichi=False) - tiles = self.player.closed_hand + [waiting * 4] - closed_melds = [x for x in self.player.melds if not x.opened] - for meld in closed_melds: - tiles.extend(meld.tiles[:3]) - - tiles_34 = TilesConverter.to_34_array(tiles) - - results = self.player.ai.hand_divider.divide_hand(tiles_34) + results, tiles_34 = self.player.ai.hand_builder.divide_hand(self.player.closed_hand, + self.player.melds, + waiting) result = results[0] # let's find suji-traps in our discard From 9f4a06800c12a6856e0456fcbe2b2e43083e6173 Mon Sep 17 00:00:00 2001 From: bogachev-pa Date: Sun, 11 Nov 2018 15:18:08 +0300 Subject: [PATCH 089/126] choose best tanki tile to wait for in tempai #77 --- project/game/ai/first_version/hand_builder.py | 233 ++++++++++++++++-- project/game/ai/first_version/main.py | 1 + project/game/ai/first_version/riichi.py | 20 +- .../game/ai/first_version/strategies/main.py | 97 ++++---- .../tests/strategies/tests_tanyao.py | 6 +- .../ai/first_version/tests/tests_discards.py | 34 +++ 6 files changed, 305 insertions(+), 86 deletions(-) diff --git a/project/game/ai/first_version/hand_builder.py b/project/game/ai/first_version/hand_builder.py index eac7db1d..58740a3f 100644 --- a/project/game/ai/first_version/hand_builder.py +++ b/project/game/ai/first_version/hand_builder.py @@ -2,13 +2,16 @@ from mahjong.constants import AKA_DORA_LIST from mahjong.shanten import Shanten -from mahjong.tile import TilesConverter -from mahjong.utils import is_tile_strictly_isolated, is_pair +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 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 + class HandBuilder: player = None @@ -18,10 +21,83 @@ def __init__(self, player, ai): self.player = player self.ai = ai - def discard_tile(self, tiles, closed_hand, open_sets_34, print_log=True): + class TankiWait: + TANKI_WAIT_NON_YAKUHAI = 1 + TANKI_WAIT_SELF_YAKUHAI = 2 + TANKI_WAIT_ALL_YAKUHAI = 3 + TANKI_WAIT_69_KABE = 4 + TANKI_WAIT_69_SUJI = 5 + TANKI_WAIT_69_RAW = 6 + TANKI_WAIT_28_KABE = 7 + TANKI_WAIT_28_SUJI = 8 + TANKI_WAIT_28_RAW = 9 + TANKI_WAIT_37_KABE = 10 + TANKI_WAIT_37_SUJI = 11 + TANKI_WAIT_37_RAW = 12 + TANKI_WAIT_456_KABE = 13 + TANKI_WAIT_456_SUJI = 14 + TANKI_WAIT_456_RAW = 15 + + tanki_wait_same_ukeire_2_3_prio = { + TANKI_WAIT_NON_YAKUHAI: 15, + TANKI_WAIT_69_KABE: 14, + TANKI_WAIT_69_SUJI: 14, + TANKI_WAIT_SELF_YAKUHAI: 13, + TANKI_WAIT_ALL_YAKUHAI: 12, + TANKI_WAIT_28_KABE: 11, + TANKI_WAIT_28_SUJI: 11, + TANKI_WAIT_37_KABE: 10, + TANKI_WAIT_37_SUJI: 10, + TANKI_WAIT_69_RAW: 9, + TANKI_WAIT_456_KABE: 8, + TANKI_WAIT_456_SUJI: 8, + TANKI_WAIT_28_RAW: 7, + TANKI_WAIT_456_RAW: 6, + TANKI_WAIT_37_RAW: 5 + } + + tanki_wait_same_ukeire_1_prio = { + TANKI_WAIT_NON_YAKUHAI: 15, + TANKI_WAIT_SELF_YAKUHAI: 14, + TANKI_WAIT_ALL_YAKUHAI: 13, + TANKI_WAIT_69_KABE: 12, + TANKI_WAIT_69_SUJI: 12, + TANKI_WAIT_28_KABE: 11, + TANKI_WAIT_28_SUJI: 11, + TANKI_WAIT_37_KABE: 10, + TANKI_WAIT_37_SUJI: 10, + TANKI_WAIT_69_RAW: 9, + TANKI_WAIT_456_KABE: 8, + TANKI_WAIT_456_SUJI: 8, + TANKI_WAIT_28_RAW: 7, + TANKI_WAIT_456_RAW: 6, + TANKI_WAIT_37_RAW: 5 + } + + tanki_wait_diff_ukeire_prio = { + TANKI_WAIT_NON_YAKUHAI: 1, + TANKI_WAIT_SELF_YAKUHAI: 1, + TANKI_WAIT_ALL_YAKUHAI: 1, + TANKI_WAIT_69_KABE: 1, + TANKI_WAIT_69_SUJI: 1, + TANKI_WAIT_28_KABE: 0, + TANKI_WAIT_28_SUJI: 0, + TANKI_WAIT_37_KABE: 0, + TANKI_WAIT_37_SUJI: 0, + TANKI_WAIT_69_RAW: 0, + TANKI_WAIT_456_KABE: 0, + TANKI_WAIT_456_SUJI: 0, + TANKI_WAIT_28_RAW: 0, + TANKI_WAIT_456_RAW: 0, + TANKI_WAIT_37_RAW: 0 + } + + # FIXME: melds and open_sets_34 duplicate each other, get rid of open_sets_34 + def discard_tile(self, tiles, closed_hand, melds, open_sets_34, print_log=True): selected_tile = self.choose_tile_to_discard( tiles, closed_hand, + melds, open_sets_34, print_log=print_log ) @@ -132,32 +208,113 @@ def count_tiles(self, waiting, tiles_34): n += 4 - self.player.total_tiles(tile_34, tiles_34) return n - def divide_hand(self, closed_hand, melds, waiting): - tiles = closed_hand + [waiting * 4] - closed_melds = [x for x in melds if not x.opened] - for meld in closed_melds: - tiles.extend(meld.tiles[:3]) + def divide_hand(self, tiles, waiting): + for i in range(0, 4): + if waiting * 4 + i not in tiles: + tiles += [waiting * 4 + i] + break tiles_34 = TilesConverter.to_34_array(tiles) results = self.player.ai.hand_divider.divide_hand(tiles_34) + if not results: + print("=============================================") return results, tiles_34 + def check_suji_and_kabe(self, tiles_34, waiting): + # let's find suji-traps in our discard + suji_tiles = self.player.ai.defence.suji.find_suji_against_self(self.player) + have_suji = waiting in suji_tiles + + # let's find kabe + kabe_tiles = self.player.ai.defence.kabe.find_all_kabe(tiles_34) + have_kabe = False + for kabe in kabe_tiles: + if waiting == kabe.tile_34 and kabe.kabe_type == KabeTile.STRONG_KABE: + have_kabe = True + + return have_suji, have_kabe + def _choose_best_tanki_wait(self, discard_desc): - # FIXME: implement + discard_desc = sorted(discard_desc, key=lambda k: k['hand_cost'], reverse=True) + + # we are always choosing between exactly two tanki waits + assert len(discard_desc) == 2 + + discard_desc = [x for x in discard_desc if x['hand_cost'] != 0] - return discard_desc[0]['discard_option'] + # we are guaranteed to have at least one wait with cost by caller logic + assert len(discard_desc) > 0 - def _choose_best_discard_in_tempai(self, discard_options): + if len(discard_desc) == 1: + return discard_desc[0]['discard_option'] + + # if not 1 then 2 + assert len(discard_desc) == 2 + + best_discard_desc = [x for x in discard_desc if x['hand_cost'] == discard_desc[0]['hand_cost']] + + # first of all we choose the most expensive wait + if len(best_discard_desc) == 1: + return best_discard_desc[0]['discard_option'] + + best_ukeire = best_discard_desc[0]['discard_option'].ukeire + diff = best_ukeire - best_discard_desc[1]['discard_option'].ukeire + # if both tanki waits have the same ukeire + if diff == 0: + # case when we have 2 or 3 tiles to wait for + if best_ukeire == 2 or best_ukeire == 3: + best_discard_desc = sorted(best_discard_desc, + key=lambda k: self.TankiWait.tanki_wait_same_ukeire_2_3_prio[ + k['tanki_type']], + reverse=True) + return best_discard_desc[0]['discard_option'] + + # case when we have 1 tile to wait for + if best_ukeire == 1: + best_discard_desc = sorted(best_discard_desc, + key=lambda k: self.TankiWait.tanki_wait_same_ukeire_1_prio[ + k['tanki_type']], + reverse=True) + return best_discard_desc[0]['discard_option'] + + # should never reach here + assert False + + # if one tanki wait has 1 more tile to wait than the other we only choose the latter one if it is + # a wind or alike and the first one is not + if diff == 1: + prio_0 = self.TankiWait.tanki_wait_diff_ukeire_prio[best_discard_desc[0]['tanki_type']] + prio_1 = self.TankiWait.tanki_wait_diff_ukeire_prio[best_discard_desc[1]['tanki_type']] + if prio_1 > prio_0: + return best_discard_desc[1]['discard_option'] + + return best_discard_desc[0]['discard_option'] + + if diff > 1: + return best_discard_desc[0]['discard_option'] + + # if everything is the same we just choose the first one + return best_discard_desc[0]['discard_option'] + + 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() 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) cost_x_ukeire = 0 hand_cost = 0 @@ -169,9 +326,11 @@ def _choose_best_discard_in_tempai(self, discard_options): cost_x_ukeire = hand_cost * discard_option.ukeire # let's check if this is a tanki wait - results = self.divide_hand(self.player.closed_hand, self.player.melds, waiting) + 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: @@ -179,13 +338,47 @@ def _choose_best_discard_in_tempai(self, discard_options): 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(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_tanki': is_tanki + 'is_tanki': is_tanki, + 'tanki_type': tanki_type }) else: cost_x_ukeire_sum = 0 @@ -200,11 +393,14 @@ def _choose_best_discard_in_tempai(self, discard_options): 'discard_option': discard_option, 'hand_cost': None, 'cost_x_ukeire': cost_x_ukeire, - 'is_tanki': False + 'is_tanki': False, + 'tanki_type': None }) - # return tile back to hand - self.player.tiles.append(tile) + # 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'], reverse=True) best_discard_desc = [x for x in discard_desc if x['cost_x_ukeire'] == discard_desc[0]['cost_x_ukeire']] @@ -228,7 +424,8 @@ def _choose_best_discard_in_tempai(self, discard_options): # FIXME: 2. if safeness is the same, we try to discard non-dora tiles return best_discard_desc[0]['discard_option'] - def choose_tile_to_discard(self, tiles, closed_hand, open_sets_34, print_log=True): + # FIXME: melds and open_sets_34 duplicate each other, get rid of open_sets_34 + def choose_tile_to_discard(self, tiles, closed_hand, melds, open_sets_34, print_log=True): """ Try to find best tile to discard, based on different rules """ @@ -296,7 +493,7 @@ def choose_tile_to_discard(self, tiles, closed_hand, open_sets_34, print_log=Tru # 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] - return self._choose_best_discard_in_tempai(other_tiles_with_same_shanten) + return self._choose_best_discard_in_tempai(tiles, melds, other_tiles_with_same_shanten) tiles_without_dora = [x for x in possible_options if x.count_of_dora == 0] diff --git a/project/game/ai/first_version/main.py b/project/game/ai/first_version/main.py index 6a58f20c..d0668f3d 100644 --- a/project/game/ai/first_version/main.py +++ b/project/game/ai/first_version/main.py @@ -103,6 +103,7 @@ def discard_tile(self, discard_tile, print_log=True): return self.hand_builder.discard_tile( self.player.tiles, self.player.closed_hand, + self.player.melds, self.player.meld_34_tiles, print_log ) diff --git a/project/game/ai/first_version/riichi.py b/project/game/ai/first_version/riichi.py index 04d6b2bd..dea325ad 100644 --- a/project/game/ai/first_version/riichi.py +++ b/project/game/ai/first_version/riichi.py @@ -42,21 +42,15 @@ def _should_call_riichi_one_sided(self): waiting = self.player.ai.waiting[0] hand_value = self.player.ai.estimate_hand_value(waiting, call_riichi=False) - results, tiles_34 = self.player.ai.hand_builder.divide_hand(self.player.closed_hand, - self.player.melds, - waiting) - result = results[0] + tiles = self.player.closed_hand.copy() + closed_melds = [x for x in self.player.melds if not x.opened] + for meld in closed_melds: + tiles.extend(meld.tiles[:3]) - # let's find suji-traps in our discard - suji_tiles = self.player.ai.defence.suji.find_suji_against_self(self.player) - have_suji = waiting in suji_tiles + results, tiles_34 = self.player.ai.hand_builder.divide_hand(tiles, waiting) + result = results[0] - # let's find kabe - kabe_tiles = self.player.ai.defence.kabe.find_all_kabe(tiles_34) - have_kabe = False - for kabe in kabe_tiles: - if waiting == kabe.tile_34 and kabe.kabe_type == KabeTile.STRONG_KABE: - have_kabe = True + have_suji, have_kabe = self.player.ai.hand_builder.check_suji_and_kabe(tiles_34, waiting) # what if we have yaku if hand_value.yaku is not None and hand_value.cost is not None: diff --git a/project/game/ai/first_version/strategies/main.py b/project/game/ai/first_version/strategies/main.py index 708a4865..3e27ea78 100644 --- a/project/game/ai/first_version/strategies/main.py +++ b/project/game/ai/first_version/strategies/main.py @@ -186,54 +186,24 @@ def try_to_call_meld(self, tile, is_kamicha_discard, new_tiles): if not possible_melds: return None, None - best_meld_34 = self._find_best_meld_to_open(possible_melds, new_tiles, closed_hand) - if best_meld_34: - # we need to calculate count of shanten with supposed meld - # to prevent bad hand openings - open_sets_34 = self.player.meld_34_tiles + [best_meld_34[:]] + chosen_meld = self._find_best_meld_to_open(possible_melds, new_tiles, closed_hand, tile) + selected_tile = chosen_meld['discard_tile'] + meld = chosen_meld['meld'] - meld_type = is_chi(best_meld_34) and Meld.CHI or Meld.PON - best_meld_34.remove(discarded_tile) + shanten = selected_tile.shanten + had_to_be_called = self.meld_had_to_be_called(tile) + had_to_be_called = had_to_be_called or selected_tile.had_to_be_discarded - first_tile = TilesConverter.find_34_tile_in_136_array(best_meld_34[0], closed_hand) - closed_hand.remove(first_tile) - - second_tile = TilesConverter.find_34_tile_in_136_array(best_meld_34[1], closed_hand) - closed_hand.remove(second_tile) - - selected_tile = self.player.ai.hand_builder.choose_tile_to_discard( - new_tiles, - closed_hand, - open_sets_34, - print_log=False - ) - - shanten = selected_tile.shanten - had_to_be_called = self.meld_had_to_be_called(tile) - had_to_be_called = had_to_be_called or selected_tile.had_to_be_discarded - - # each strategy can use their own value to min shanten number - if shanten > self.min_shanten: - return None, None - - # sometimes we had to call tile, even if it will not improve our hand - # otherwise we can call only with improvements of shanten - if not had_to_be_called and shanten >= self.player.ai.shanten: - return None, None - - tiles = [ - first_tile, - second_tile, - tile - ] - - meld = Meld() - meld.type = meld_type - meld.tiles = sorted(tiles) + # each strategy can use their own value to min shanten number + if shanten > self.min_shanten: + return None, None - return meld, selected_tile + # sometimes we had to call tile, even if it will not improve our hand + # otherwise we can call only with improvements of shanten + if not had_to_be_called and shanten >= self.player.ai.shanten: + return None, None - return None, None + return meld, selected_tile def meld_had_to_be_called(self, tile): """ @@ -270,24 +240,47 @@ def calculate_dora_count(self, tiles_136): self.dora_count_central += self.aka_dora_count self.dora_count_total = self.dora_count_central + self.dora_count_not_central - def _find_best_meld_to_open(self, possible_melds, new_tiles, closed_hand): - if len(possible_melds) == 1: - return possible_melds[0] + def _find_best_meld_to_open(self, possible_melds, new_tiles, closed_hand, discarded_tile): + discarded_tile_34 = discarded_tile // 4 final_results = [] - for meld in possible_melds: - open_sets_34 = self.player.meld_34_tiles + [meld] + 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) + + first_tile = TilesConverter.find_34_tile_in_136_array(meld_34_copy[0], closed_hand_copy) + closed_hand_copy.remove(first_tile) + + second_tile = TilesConverter.find_34_tile_in_136_array(meld_34_copy[1], closed_hand_copy) + closed_hand_copy.remove(second_tile) + + tiles = [ + first_tile, + second_tile, + discarded_tile + ] + + meld = Meld() + meld.type = meld_type + meld.tiles = sorted(tiles) + + melds = self.player.melds + [meld] selected_tile = self.player.ai.hand_builder.choose_tile_to_discard( new_tiles, - closed_hand, + closed_hand_copy, + melds, open_sets_34, print_log=False ) final_results.append({ 'discard_tile': selected_tile, - 'meld_print': TilesConverter.to_one_line_string([meld[0] * 4, meld[1] * 4, meld[2] * 4]), + 'meld_print': TilesConverter.to_one_line_string([meld_34[0] * 4, meld_34[1] * 4, meld_34[2] * 4]), 'meld': meld }) @@ -297,4 +290,4 @@ def _find_best_meld_to_open(self, possible_melds, new_tiles, closed_hand): DecisionsLogger.debug(log.MELD_PREPARE, 'Options with meld calling', context=final_results) - return final_results[0]['meld'] + return final_results[0] 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 ee5eb6cb..137560b4 100644 --- a/project/game/ai/first_version/tests/strategies/tests_tanyao.py +++ b/project/game/ai/first_version/tests/strategies/tests_tanyao.py @@ -158,12 +158,12 @@ def test_open_hand_and_discard_tiles_logic(self): self.assertEqual(self._to_string([tile_to_discard]), '4z') def test_dont_count_pairs_in_already_opened_hand(self): - meld = self._make_meld(Meld.PON, sou='222') - self.player.add_called_meld(meld) - tiles = self._string_to_136_array(man='33556788', sou='22266') self.player.init_hand(tiles) + meld = self._make_meld(Meld.PON, sou='222') + self.player.add_called_meld(meld) + tile = self._string_to_136_tile(sou='6') meld, _ = self.player.try_to_call_meld(tile, False) # even if it looks like chitoitsu we can open hand and get tempai here diff --git a/project/game/ai/first_version/tests/tests_discards.py b/project/game/ai/first_version/tests/tests_discards.py index 78093fdb..c2f23861 100644 --- a/project/game/ai/first_version/tests/tests_discards.py +++ b/project/game/ai/first_version/tests/tests_discards.py @@ -445,3 +445,37 @@ def test_choose_correct_wait_yaku_potentially(self): player.draw_tile(self._string_to_136_tile(man='9')) discarded_tile = player.discard_tile() self.assertEqual(self._to_string([discarded_tile]), '8s') + + def test_choose_better_tanki_honor(self): + table = Table() + player = table.player + player.round_step = 2 + player.dealer_seat = 3 + + table.add_dora_indicator(self._string_to_136_tile(man='8')) + + tiles = self._string_to_136_array(man='11447799', sou='556', honors='45') + player.init_hand(tiles) + player.draw_tile(self._string_to_136_tile(honors='4')) + discarded_tile = player.discard_tile() + self.assertEqual(self._to_string([discarded_tile]), '6s') + + tiles = self._string_to_136_array(man='11447799', sou='556', honors='45') + player.init_hand(tiles) + player.draw_tile(self._string_to_136_tile(honors='5')) + discarded_tile = player.discard_tile() + self.assertEqual(self._to_string([discarded_tile]), '6s') + + tiles = self._string_to_136_array(man='11447799', sou='556', honors='45') + player.init_hand(tiles) + player.draw_tile(self._string_to_136_tile(sou='6')) + discarded_tile = player.discard_tile() + self.assertEqual(self._to_string([discarded_tile]), '5z') + + tiles = self._string_to_136_array(man='11447799', sou='556', honors='34') + player.init_hand(tiles) + player.draw_tile(self._string_to_136_tile(sou='6')) + discarded_tile = player.discard_tile() + self.assertEqual(self._to_string([discarded_tile]), '3z') + + # TODO: more tests about tanki waiting selection From 312f332ce2cdbdec8413be44f6548fa78e6e9512 Mon Sep 17 00:00:00 2001 From: Alexey <475367+Nihisil@users.noreply.github.com> Date: Sun, 11 Nov 2018 22:29:59 +0800 Subject: [PATCH 090/126] Upgrade requests library --- project/requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/project/requirements.txt b/project/requirements.txt index 891a7cf1..11e76a09 100644 --- a/project/requirements.txt +++ b/project/requirements.txt @@ -1,3 +1,3 @@ mahjong==1.1.5 -requests==2.18.4 -flake8==3.4.1 \ No newline at end of file +requests==2.20.1 +flake8==3.4.1 From ffd5b829799b45272359560ddfd35b7e5fae9601 Mon Sep 17 00:00:00 2001 From: Nihisil Date: Mon, 12 Nov 2018 00:05:22 +0800 Subject: [PATCH 091/126] Allow to run multiple bot instances with run.sh script --- bin/run.sh | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/bin/run.sh b/bin/run.sh index 42287e1d..e0742c84 100644 --- a/bin/run.sh +++ b/bin/run.sh @@ -1,19 +1,21 @@ #!/bin/sh -e -# cron will run each 5 minutes -# and will search the run process +# the cron job will be executed each 5 minutes +# and it will try to find bot process # if there is no process, it will run it +# example of usage +# */5 * * * * bash /root/bot/bin/run.sh bot_settings_name -# */5 * * * * bash /root/bot/bin/run.sh +SETTINGS_NAME="$1" -PID=`ps -eaf | grep project/main.py | grep -v grep | awk '{print $2}'` +PID=`ps -eaf | grep "project/main.py -s ${SETTINGS_NAME}" | grep -v grep | awk '{print $2}'` -if [[ "" = "$PID" ]]; then - /root/bot/env/bin/python /root/bot/project/main.py +if [[ "" = "$PID" ]]; then + /root/bot/env/bin/python /root/bot/project/main.py -s ${SETTINGS_NAME} else WORKED_SECONDS=`ps -p "$PID" -o etimes=` - # if process run > 60 minutes, probably it hang and we need to kill it - if [ ${WORKED_SECONDS} -gt "3600" ]; then + # if process run > 60 minutes, probably it hangs and we need to kill it + if [[ ${WORKED_SECONDS} -gt "3600" ]]; then kill ${PID} fi fi \ No newline at end of file From 750d8331aeeeb7cd95ad9460eadf4f17d6431c99 Mon Sep 17 00:00:00 2001 From: bogachev-pa Date: Mon, 12 Nov 2018 02:03:03 +0300 Subject: [PATCH 092/126] fix bug in find_suji_against_self() function It used to overwrite sujis and save only the last one. --- project/game/ai/first_version/defence/suji.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/project/game/ai/first_version/defence/suji.py b/project/game/ai/first_version/defence/suji.py index cd73bad7..04b4cd94 100644 --- a/project/game/ai/first_version/defence/suji.py +++ b/project/game/ai/first_version/defence/suji.py @@ -71,21 +71,21 @@ def find_suji_against_self(self, player): base = suji - suji_temp - 1 if suji_temp == self.FIRST_SUJI: - result = [ + result += [ base + 1, base + 4, base + 7 ] if suji_temp == self.SECOND_SUJI: - result = [ + result += [ base + 2, base + 5, base + 8 ] if suji_temp == self.THIRD_SUJI: - result = [ + result += [ base + 3, base + 6, base + 9 From ed737e8871770ab2d3aaf2f90fdc71f948c4fc79 Mon Sep 17 00:00:00 2001 From: bogachev-pa Date: Mon, 12 Nov 2018 02:04:06 +0300 Subject: [PATCH 093/126] hand builder small code cleanup --- project/game/ai/first_version/hand_builder.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/project/game/ai/first_version/hand_builder.py b/project/game/ai/first_version/hand_builder.py index 58740a3f..2b2f2958 100644 --- a/project/game/ai/first_version/hand_builder.py +++ b/project/game/ai/first_version/hand_builder.py @@ -209,16 +209,16 @@ def count_tiles(self, waiting, tiles_34): return n def divide_hand(self, tiles, waiting): + tiles_copy = tiles.copy() + for i in range(0, 4): - if waiting * 4 + i not in tiles: - tiles += [waiting * 4 + i] + if waiting * 4 + i not in tiles_copy: + tiles_copy += [waiting * 4 + i] break - tiles_34 = TilesConverter.to_34_array(tiles) + tiles_34 = TilesConverter.to_34_array(tiles_copy) results = self.player.ai.hand_divider.divide_hand(tiles_34) - if not results: - print("=============================================") return results, tiles_34 def check_suji_and_kabe(self, tiles_34, waiting): From ef1b621d656f64bd6c69859d636392849326199e Mon Sep 17 00:00:00 2001 From: bogachev-pa Date: Mon, 12 Nov 2018 02:04:30 +0300 Subject: [PATCH 094/126] add more tests on tanki wait selection #77 --- .../ai/first_version/tests/tests_discards.py | 173 +++++++++++++++++- 1 file changed, 172 insertions(+), 1 deletion(-) diff --git a/project/game/ai/first_version/tests/tests_discards.py b/project/game/ai/first_version/tests/tests_discards.py index c2f23861..8c148e97 100644 --- a/project/game/ai/first_version/tests/tests_discards.py +++ b/project/game/ai/first_version/tests/tests_discards.py @@ -4,6 +4,7 @@ from mahjong.constants import EAST, SOUTH, WEST, NORTH, HAKU, HATSU, CHUN, FIVE_RED_SOU, FIVE_RED_PIN from mahjong.tests_mixin import TestMixin from mahjong.meld import Meld +from mahjong.tile import Tile from game.ai.discard import DiscardOption from game.ai.first_version.strategies.main import BaseStrategy @@ -478,4 +479,174 @@ def test_choose_better_tanki_honor(self): discarded_tile = player.discard_tile() self.assertEqual(self._to_string([discarded_tile]), '3z') - # TODO: more tests about tanki waiting selection + def _choose_tanki_with_kabe_helper(self, tiles, kabe_tiles, tile_to_draw, tile_to_discard_str): + table = Table() + player = table.player + player.round_step = 2 + player.dealer_seat = 3 + + for tile in kabe_tiles: + for i in range(0, 4): + table.add_discarded_tile(1, tile, False) + + player.init_hand(tiles) + player.draw_tile(tile_to_draw) + discarded_tile = player.discard_tile() + self.assertEqual(self._to_string([discarded_tile]), tile_to_discard_str) + + def test_choose_tanki_with_kabe(self): + self._choose_tanki_with_kabe_helper( + self._string_to_136_array(sou='119', pin='224477', man='5669'), + [self._string_to_136_tile(sou='8')], + self._string_to_136_tile(man='5'), + '9m' + ) + + self._choose_tanki_with_kabe_helper( + self._string_to_136_array(sou='119', pin='224477', man='5669'), + [self._string_to_136_tile(man='8')], + self._string_to_136_tile(man='5'), + '9s' + ) + + self._choose_tanki_with_kabe_helper( + self._string_to_136_array(sou='118', pin='224477', man='5668'), + [self._string_to_136_tile(sou='7')], + self._string_to_136_tile(man='5'), + '8m' + ) + + self._choose_tanki_with_kabe_helper( + self._string_to_136_array(sou='118', pin='224477', man='5668'), + [self._string_to_136_tile(man='7')], + self._string_to_136_tile(man='5'), + '8s' + ) + + self._choose_tanki_with_kabe_helper( + self._string_to_136_array(sou='117', pin='224477', man='1157'), + [self._string_to_136_tile(sou='6'), self._string_to_136_tile(sou='9')], + self._string_to_136_tile(man='5'), + '7m' + ) + + self._choose_tanki_with_kabe_helper( + self._string_to_136_array(sou='117', pin='224477', man='1157'), + [self._string_to_136_tile(man='6'), self._string_to_136_tile(man='9')], + self._string_to_136_tile(man='5'), + '7s' + ) + + self._choose_tanki_with_kabe_helper( + self._string_to_136_array(sou='116', pin='224477', man='1126'), + [self._string_to_136_tile(sou='5'), self._string_to_136_tile(sou='7')], + self._string_to_136_tile(man='2'), + '6m' + ) + + self._choose_tanki_with_kabe_helper( + self._string_to_136_array(sou='116', pin='224477', man='1126'), + [self._string_to_136_tile(man='5'), self._string_to_136_tile(man='7')], + self._string_to_136_tile(man='2'), + '6s' + ) + + self._choose_tanki_with_kabe_helper( + self._string_to_136_array(sou='115', pin='224477', man='1125'), + [self._string_to_136_tile(sou='4'), self._string_to_136_tile(sou='6')], + self._string_to_136_tile(man='2'), + '5m' + ) + + self._choose_tanki_with_kabe_helper( + self._string_to_136_array(sou='115', pin='224477', man='1125'), + [self._string_to_136_tile(man='4'), self._string_to_136_tile(man='6')], + self._string_to_136_tile(man='2'), + '5s' + ) + + def _choose_tanki_with_suji_helper(self, 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() + self.assertEqual(self._to_string([discarded_tile]), tile_to_discard_str) + + def test_choose_tanki_with_suji(self): + self._choose_tanki_with_suji_helper( + self._string_to_136_array(man='22336688', sou='19', pin='99', honors='2'), + [self._string_to_136_tile(sou='6')], + self._string_to_136_tile(honors='2'), + '1s' + ) + + self._choose_tanki_with_suji_helper( + self._string_to_136_array(man='22336688', sou='19', pin='99', honors='2'), + [self._string_to_136_tile(sou='4')], + self._string_to_136_tile(honors='2'), + '9s' + ) + + self._choose_tanki_with_suji_helper( + self._string_to_136_array(man='22336688', sou='2', pin='299', honors='2'), + [self._string_to_136_tile(sou='5')], + self._string_to_136_tile(honors='2'), + '2p' + ) + + self._choose_tanki_with_suji_helper( + self._string_to_136_array(man='22336688', sou='2', pin='299', honors='2'), + [self._string_to_136_tile(pin='5')], + self._string_to_136_tile(honors='2'), + '2s' + ) + + self._choose_tanki_with_suji_helper( + self._string_to_136_array(man='22336688', sou='3', pin='399', honors='2'), + [self._string_to_136_tile(sou='6')], + self._string_to_136_tile(honors='2'), + '3p' + ) + + self._choose_tanki_with_suji_helper( + self._string_to_136_array(man='22336688', sou='3', pin='399', honors='2'), + [self._string_to_136_tile(pin='6')], + self._string_to_136_tile(honors='2'), + '3s' + ) + + self._choose_tanki_with_suji_helper( + self._string_to_136_array(man='22336688', sou='4', pin='499', honors='2'), + [self._string_to_136_tile(sou='1'), self._string_to_136_tile(sou='7')], + self._string_to_136_tile(honors='2'), + '4p' + ) + + self._choose_tanki_with_suji_helper( + self._string_to_136_array(man='22336688', sou='4', pin='499', honors='2'), + [self._string_to_136_tile(pin='1'), self._string_to_136_tile(pin='7')], + self._string_to_136_tile(honors='2'), + '4s' + ) + + self._choose_tanki_with_suji_helper( + self._string_to_136_array(man='22336688', sou='5', pin='599', honors='2'), + [self._string_to_136_tile(sou='2'), self._string_to_136_tile(sou='8')], + self._string_to_136_tile(honors='2'), + '5p' + ) + + self._choose_tanki_with_suji_helper( + self._string_to_136_array(man='22336688', sou='5', pin='599', honors='2'), + [self._string_to_136_tile(pin='2'), self._string_to_136_tile(pin='8')], + self._string_to_136_tile(honors='2'), + '5s' + ) From af39dca3bb76d6cb4697331822759c8fa1af3645 Mon Sep 17 00:00:00 2001 From: Nihisil Date: Mon, 12 Nov 2018 14:01:34 +0800 Subject: [PATCH 095/126] Fix an issue with not correct number of tiles on the table when open hand --- project/tenhou/client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/project/tenhou/client.py b/project/tenhou/client.py index da641109..e4cade34 100644 --- a/project/tenhou/client.py +++ b/project/tenhou/client.py @@ -366,8 +366,6 @@ def start_game(self): if_tsumogiri = message[1].islower() player_seat = self.decoder.get_enemy_seat(message) - self.table.add_discarded_tile(player_seat, tile, if_tsumogiri) - # open hand suggestions if 't=' in message: # Possible t="" suggestions @@ -419,6 +417,8 @@ def start_game(self): else: self._send_message('') + self.table.add_discarded_tile(player_seat, tile, if_tsumogiri) + if 'owari' in message: values = self.decoder.parse_final_scores_and_uma(message) self.table.set_players_scores(values['scores'], values['uma']) From 8501f87a8ede6c9b693f8716a33428697aae20fb Mon Sep 17 00:00:00 2001 From: Nihisil Date: Mon, 12 Nov 2018 15:47:55 +0800 Subject: [PATCH 096/126] Refactoring. Get rid of duplicate attributes (meld and open_sets_34) --- project/game/ai/first_version/hand_builder.py | 23 +++++++++---------- project/game/ai/first_version/main.py | 3 +-- .../game/ai/first_version/strategies/main.py | 1 - 3 files changed, 12 insertions(+), 15 deletions(-) diff --git a/project/game/ai/first_version/hand_builder.py b/project/game/ai/first_version/hand_builder.py index 2b2f2958..b7e9d8e3 100644 --- a/project/game/ai/first_version/hand_builder.py +++ b/project/game/ai/first_version/hand_builder.py @@ -92,13 +92,11 @@ class TankiWait: TANKI_WAIT_37_RAW: 0 } - # FIXME: melds and open_sets_34 duplicate each other, get rid of open_sets_34 - def discard_tile(self, tiles, closed_hand, melds, open_sets_34, print_log=True): + def discard_tile(self, tiles, closed_hand, melds, print_log=True): selected_tile = self.choose_tile_to_discard( tiles, closed_hand, melds, - open_sets_34, print_log=print_log ) @@ -157,15 +155,17 @@ def calculate_waits(self, tiles_34, open_sets_34=None): return waiting, shanten - def find_discard_options(self, tiles, closed_hand, open_sets_34=None): + 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 open_sets_34: array of array with tiles in 34 format + :param melds: :return: """ - if open_sets_34 is None: - open_sets_34 = [] + 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) @@ -424,8 +424,7 @@ def _choose_best_discard_in_tempai(self, tiles, melds, discard_options): # FIXME: 2. if safeness is the same, we try to discard non-dora tiles return best_discard_desc[0]['discard_option'] - # FIXME: melds and open_sets_34 duplicate each other, get rid of open_sets_34 - def choose_tile_to_discard(self, tiles, closed_hand, melds, open_sets_34, print_log=True): + def choose_tile_to_discard(self, tiles, closed_hand, melds, print_log=True): """ Try to find best tile to discard, based on different rules """ @@ -433,7 +432,7 @@ def choose_tile_to_discard(self, tiles, closed_hand, melds, open_sets_34, print_ discard_options, _ = self.find_discard_options( tiles, closed_hand, - open_sets_34 + melds ) # our strategy can affect discard options @@ -441,7 +440,7 @@ def choose_tile_to_discard(self, tiles, closed_hand, melds, open_sets_34, print_ discard_options = self.ai.current_strategy.determine_what_to_discard( discard_options, closed_hand, - open_sets_34 + melds ) had_to_be_discarded_tiles = [x for x in discard_options if x.had_to_be_discarded] @@ -598,7 +597,7 @@ def calculate_second_level_ukeire(self, discard_option): results, shanten = self.find_discard_options( tiles, self.player.closed_hand, - self.player.meld_34_tiles + self.player.melds ) results = [x for x in results if x.shanten == discard_option.shanten - 1] diff --git a/project/game/ai/first_version/main.py b/project/game/ai/first_version/main.py index d0668f3d..2fce53a0 100644 --- a/project/game/ai/first_version/main.py +++ b/project/game/ai/first_version/main.py @@ -104,7 +104,6 @@ def discard_tile(self, discard_tile, print_log=True): self.player.tiles, self.player.closed_hand, self.player.melds, - self.player.meld_34_tiles, print_log ) @@ -285,7 +284,7 @@ def should_call_kan(self, tile, open_kan, from_riichi=False): previous_results, previous_shanten = self.hand_builder.find_discard_options( tiles, closed_hand_tiles, - melds_34 + self.player.melds ) previous_results = [x for x in previous_results if x.shanten == previous_shanten] previous_waits_cnt = sorted(previous_results, key=lambda x: -x.ukeire)[0].ukeire diff --git a/project/game/ai/first_version/strategies/main.py b/project/game/ai/first_version/strategies/main.py index 3e27ea78..b19e2bd7 100644 --- a/project/game/ai/first_version/strategies/main.py +++ b/project/game/ai/first_version/strategies/main.py @@ -274,7 +274,6 @@ def _find_best_meld_to_open(self, possible_melds, new_tiles, closed_hand, discar new_tiles, closed_hand_copy, melds, - open_sets_34, print_log=False ) From bfcf7a41b2cfd40c1a2733b1af757abc7c4e88be Mon Sep 17 00:00:00 2001 From: bogachev-pa Date: Mon, 12 Nov 2018 13:08:43 +0300 Subject: [PATCH 097/126] fix find_discard_options() parameter --- project/game/ai/first_version/defence/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/game/ai/first_version/defence/main.py b/project/game/ai/first_version/defence/main.py index 661d1502..d70ed69d 100644 --- a/project/game/ai/first_version/defence/main.py +++ b/project/game/ai/first_version/defence/main.py @@ -112,7 +112,7 @@ def try_to_find_safe_tile_to_discard(self): discard_results, _ = self.player.ai.hand_builder.find_discard_options( self.player.tiles, self.player.closed_hand, - self.player.meld_34_tiles + self.player.melds ) self.hand_34 = TilesConverter.to_34_array(self.player.tiles) From b3aeb5093f3bf4055b48ce9ac4a0bd7a9c9877d1 Mon Sep 17 00:00:00 2001 From: bogachev-pa Date: Tue, 13 Nov 2018 00:16:40 +0300 Subject: [PATCH 098/126] pass only closed hand to kabe checking function If we pass whole hand, tiles revealed for everyone are counted twice. --- project/game/ai/first_version/hand_builder.py | 4 +++- project/game/ai/first_version/riichi.py | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/project/game/ai/first_version/hand_builder.py b/project/game/ai/first_version/hand_builder.py index b7e9d8e3..7b724189 100644 --- a/project/game/ai/first_version/hand_builder.py +++ b/project/game/ai/first_version/hand_builder.py @@ -305,6 +305,8 @@ def _choose_best_discard_in_tempai(self, tiles, melds, discard_options): 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 @@ -348,7 +350,7 @@ def _choose_best_discard_in_tempai(self, tiles, melds, discard_options): break simplified_waiting = simplify(waiting) - have_suji, have_kabe = self.check_suji_and_kabe(tiles_34, 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: diff --git a/project/game/ai/first_version/riichi.py b/project/game/ai/first_version/riichi.py index dea325ad..656ee4d5 100644 --- a/project/game/ai/first_version/riichi.py +++ b/project/game/ai/first_version/riichi.py @@ -50,7 +50,9 @@ def _should_call_riichi_one_sided(self): results, tiles_34 = self.player.ai.hand_builder.divide_hand(tiles, waiting) result = results[0] - have_suji, have_kabe = self.player.ai.hand_builder.check_suji_and_kabe(tiles_34, waiting) + closed_tiles_34 = TilesConverter.to_34_array(self.player.closed_hand) + + have_suji, have_kabe = self.player.ai.hand_builder.check_suji_and_kabe(closed_tiles_34, waiting) # what if we have yaku if hand_value.yaku is not None and hand_value.cost is not None: From 4b4e704a24a52f712184e7a794f258336d034b66 Mon Sep 17 00:00:00 2001 From: bogachev-pa Date: Tue, 13 Nov 2018 03:11:31 +0300 Subject: [PATCH 099/126] consider furiten when choosing wait in tempai #61 --- project/game/ai/discard.py | 5 +- project/game/ai/first_version/hand_builder.py | 73 ++++++++++++++++--- project/game/ai/first_version/main.py | 6 +- .../ai/first_version/tests/tests_discards.py | 68 +++++++++++++++++ 4 files changed, 137 insertions(+), 15 deletions(-) diff --git a/project/game/ai/discard.py b/project/game/ai/discard.py index a4f2ce77..9ebfa595 100644 --- a/project/game/ai/discard.py +++ b/project/game/ai/discard.py @@ -30,8 +30,10 @@ class DiscardOption(object): valuation = None # how danger this tile is danger = None + # wait to ukeire map + wait_to_ukeire = None - def __init__(self, player, tile_to_discard, shanten, waiting, ukeire, danger=100): + def __init__(self, player, tile_to_discard, shanten, waiting, ukeire, danger=100, wait_to_ukeire=None): """ :param player: :param tile_to_discard: tile in 34 format @@ -48,6 +50,7 @@ def __init__(self, player, tile_to_discard, shanten, waiting, ukeire, danger=100 self.danger = danger self.had_to_be_saved = False self.had_to_be_discarded = False + self.wait_to_ukeire = wait_to_ukeire self.calculate_value() diff --git a/project/game/ai/first_version/hand_builder.py b/project/game/ai/first_version/hand_builder.py index 7b724189..a35bb4d5 100644 --- a/project/game/ai/first_version/hand_builder.py +++ b/project/game/ai/first_version/hand_builder.py @@ -181,11 +181,13 @@ def find_discard_options(self, tiles, closed_hand, melds=None): 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))) + ukeire=self.count_tiles(waiting, closed_tiles_34), + wait_to_ukeire=wait_to_ukeire)) if is_agari: shanten = Shanten.AGARI_STATE @@ -252,6 +254,11 @@ def _choose_best_tanki_wait(self, discard_desc): # if not 1 then 2 assert len(discard_desc) == 2 + num_furiten_waits = len([x for x in discard_desc if x['is_furiten']]) + # if we choose tanki, we always prefer non-furiten wait over furiten one, no matter what the cost is + if num_furiten_waits == 1: + return [x for x in discard_desc if not x['is_furiten']][0]['discard_option'] + best_discard_desc = [x for x in discard_desc if x['hand_cost'] == discard_desc[0]['hand_cost']] # first of all we choose the most expensive wait @@ -297,6 +304,10 @@ 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): + 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): # first of all we find tiles that have the best hand cost * ukeire value call_riichi = not self.player.is_open_hand @@ -318,14 +329,31 @@ def _choose_best_discard_in_tempai(self, tiles, melds, discard_options): discarded_tile = Tile(tile, False) self.player.discards.append(discarded_tile) - cost_x_ukeire = 0 hand_cost = 0 if len(discard_option.waiting) == 1: waiting = discard_option.waiting[0] - hand_value = self.player.ai.estimate_hand_value(waiting, call_riichi=call_riichi) + 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 = hand_value.cost['main'] - cost_x_ukeire = hand_cost * discard_option.ukeire + 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 # let's check if this is a tanki wait results, tiles_34 = self.divide_hand(self.player.tiles, waiting) @@ -379,22 +407,41 @@ def _choose_best_discard_in_tempai(self, tiles, melds, discard_options): '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_sum = 0 + cost_x_ukeire_tsumo = 0 + cost_x_ukeire_ron = 0 + is_furiten = False + for waiting in discard_option.waiting: - hand_value = self.player.ai.estimate_hand_value(waiting, call_riichi=call_riichi) + 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_sum += hand_value.cost['main'] * discard_option.ukeire + cost_x_ukeire_tsumo += (hand_value.cost['main'] + + 2 * hand_value.cost['additional'] + ) * discard_option.wait_to_ukeire[waiting] - cost_x_ukeire = cost_x_ukeire_sum / len(discard_option.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 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 }) @@ -404,19 +451,21 @@ def _choose_best_discard_in_tempai(self, tiles, melds, discard_options): self.player.melds = player_melds_copy self.player.discards.remove(discarded_tile) - discard_desc = sorted(discard_desc, key=lambda k: k['cost_x_ukeire'], reverse=True) - best_discard_desc = [x for x in discard_desc if x['cost_x_ukeire'] == discard_desc[0]['cost_x_ukeire']] - num_tanki_waits = len([x for x in discard_desc if x['is_tanki'] == True]) + 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'] diff --git a/project/game/ai/first_version/main.py b/project/game/ai/first_version/main.py index 2fce53a0..f48c10be 100644 --- a/project/game/ai/first_version/main.py +++ b/project/game/ai/first_version/main.py @@ -177,11 +177,12 @@ def determine_strategy(self, tiles_136): return self.current_strategy and True or False - def estimate_hand_value(self, win_tile, tiles=None, call_riichi=False): + def estimate_hand_value(self, win_tile, tiles=None, call_riichi=False, is_tsumo=False): """ :param win_tile: 34 tile format :param tiles: :param call_riichi: + :param is_tsumo :return: """ win_tile *= 4 @@ -200,7 +201,8 @@ def estimate_hand_value(self, win_tile, tiles=None, call_riichi=False): player_wind=self.player.player_wind, round_wind=self.player.table.round_wind_tile, has_aka_dora=self.player.table.has_aka_dora, - has_open_tanyao=self.player.table.has_open_tanyao + has_open_tanyao=self.player.table.has_open_tanyao, + is_tsumo=is_tsumo, ) result = self.finished_hand.estimate_hand_value(tiles, diff --git a/project/game/ai/first_version/tests/tests_discards.py b/project/game/ai/first_version/tests/tests_discards.py index 8c148e97..d55c0175 100644 --- a/project/game/ai/first_version/tests/tests_discards.py +++ b/project/game/ai/first_version/tests/tests_discards.py @@ -650,3 +650,71 @@ def test_choose_tanki_with_suji(self): self._string_to_136_tile(honors='2'), '5s' ) + + 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 i 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 test_avoid_furiten(self): + self._avoid_furiten_helper( + self._string_to_136_array(man='22336688', pin='99', honors='267'), + self._string_to_136_tile(honors='6'), + self._string_to_136_tile(honors='7'), + self._string_to_136_tile(honors='2'), + '6z' + ) + + self._avoid_furiten_helper( + self._string_to_136_array(man='22336688', pin='99', honors='267'), + self._string_to_136_tile(honors='7'), + self._string_to_136_tile(honors='6'), + self._string_to_136_tile(honors='2'), + '7z' + ) + + def _choose_furiten_over_karaten_helper(self, 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 i in range(0, 3): + table.add_discarded_tile(1, karaten_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 test_choose_furiten_over_karaten(self): + self._choose_furiten_over_karaten_helper( + self._string_to_136_array(man='22336688', pin='99', honors='267'), + self._string_to_136_tile(honors='6'), + self._string_to_136_tile(honors='7'), + self._string_to_136_tile(honors='2'), + '7z' + ) + + self._choose_furiten_over_karaten_helper( + self._string_to_136_array(man='22336688', pin='99', honors='267'), + self._string_to_136_tile(honors='7'), + self._string_to_136_tile(honors='6'), + self._string_to_136_tile(honors='2'), + '6z' + ) From 9f4fbf76a18b4dc2aa02b1f1a531681ce0ff327f Mon Sep 17 00:00:00 2001 From: bogachev-pa Date: Wed, 14 Nov 2018 02:02:22 +0300 Subject: [PATCH 100/126] update yakuhai strategy logic after changing when tile is added to table discards --- project/game/ai/first_version/strategies/yakuhai.py | 2 +- .../game/ai/first_version/tests/strategies/tests_yakuhai.py | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/project/game/ai/first_version/strategies/yakuhai.py b/project/game/ai/first_version/strategies/yakuhai.py index b6d3bde6..428c746d 100644 --- a/project/game/ai/first_version/strategies/yakuhai.py +++ b/project/game/ai/first_version/strategies/yakuhai.py @@ -84,7 +84,7 @@ 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) == 4 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 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 b113e896..439ea1c7 100644 --- a/project/game/ai/first_version/tests/strategies/tests_yakuhai.py +++ b/project/game/ai/first_version/tests/strategies/tests_yakuhai.py @@ -268,13 +268,12 @@ def test_open_hand_and_once_discarded_tile(self): # let's skip first yakuhai early in the game tile = self._string_to_136_tile(honors='7') - # when we are melding tile it's already counted as discarded - self.table.add_discarded_tile(1, tile, False) meld, _ = self.player.try_to_call_meld(tile, True) self.assertEqual(meld, None) - # now the second one is out + # now one is out self.table.add_discarded_tile(1, tile, False) + meld, _ = self.player.try_to_call_meld(tile, True) self.assertNotEqual(meld, None) self.assertEqual(self._to_string(meld.tiles), '777z') From 8d8f8486225c00d61061c93f596da72743a551f6 Mon Sep 17 00:00:00 2001 From: Nihisil Date: Sun, 18 Nov 2018 19:26:05 +0700 Subject: [PATCH 101/126] Fix a crash for agari state and attempt to call kan --- project/game/ai/first_version/main.py | 8 ++++++++ project/game/ai/first_version/tests/tests_ai.py | 16 +++++++++++++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/project/game/ai/first_version/main.py b/project/game/ai/first_version/main.py index f48c10be..17b2af71 100644 --- a/project/game/ai/first_version/main.py +++ b/project/game/ai/first_version/main.py @@ -280,6 +280,7 @@ def should_call_kan(self, tile, open_kan, from_riichi=False): else: # if we can use or tile in the hand for the forms other than KAN tiles.append(tile) + closed_hand_tiles.append(tile) closed_hand_34[tile_34] += 1 @@ -288,7 +289,14 @@ def should_call_kan(self, tile, open_kan, from_riichi=False): closed_hand_tiles, self.player.melds ) + previous_results = [x for x in previous_results if x.shanten == previous_shanten] + + # it is possible that we don't have results here + # when we are in agari state (but without yaku) + if not previous_results: + return None + previous_waits_cnt = sorted(previous_results, key=lambda x: -x.ukeire)[0].ukeire # shanten calculator doesn't like working with kans, so we pretend it's a pon diff --git a/project/game/ai/first_version/tests/tests_ai.py b/project/game/ai/first_version/tests/tests_ai.py index 202afb82..2de7825f 100644 --- a/project/game/ai/first_version/tests/tests_ai.py +++ b/project/game/ai/first_version/tests/tests_ai.py @@ -514,7 +514,6 @@ def test_closed_kan_same_shanten_bad_ukeire(self): self.assertEqual(player.should_call_kan(tile, False), None) - def test_closed_kan_same_shanten_same_ukeire(self): table = Table() player = table.player @@ -531,3 +530,18 @@ def test_closed_kan_same_shanten_same_ukeire(self): tile = self._string_to_136_tile(honors='3') self.assertEqual(player.should_call_kan(tile, False), Meld.KAN) + + def test_kan_crash(self): + """ + This was a crash in real game + related with open kan logic and agari without yaku state + """ + table = Table() + table.count_of_remaining_tiles = 10 + + tiles = self._string_to_136_array(man='456', pin='78999', sou='666', honors='33') + table.player.init_hand(tiles) + table.player.add_called_meld(self._make_meld(Meld.PON, sou='666')) + tile = self._string_to_136_tile(pin='9') + + self.assertEqual(table.player.should_call_kan(tile, False), None) From d33ebc4e9657bb19b13f8221bff1911f0e9b731b Mon Sep 17 00:00:00 2001 From: Nihisil Date: Sun, 18 Nov 2018 19:31:56 +0700 Subject: [PATCH 102/126] Fix a crash related to reconnect logic and called chankan --- project/game/player.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/project/game/player.py b/project/game/player.py index 588bfdcc..6e3758bb 100644 --- a/project/game/player.py +++ b/project/game/player.py @@ -68,8 +68,13 @@ def add_called_meld(self, meld: Meld): # we already added chankan as a pon set if meld.type == Meld.CHANKAN: tile_34 = meld.tiles[0] // 4 + pon_set = [x for x in self.melds if x.type == Meld.PON and (x.tiles[0] // 4) == tile_34] - self.melds.remove(pon_set[0]) + + # when we are doing reconnect and we have called chankan set + # we will not have called pon set in the hand + if pon_set: + self.melds.remove(pon_set[0]) self.melds.append(meld) From 65a22ee969f1e074483ee39fd735b01e5f3de2fc Mon Sep 17 00:00:00 2001 From: Nihisil Date: Sun, 18 Nov 2018 19:52:31 +0700 Subject: [PATCH 103/126] Fix a crash with chankan and win suggestions --- project/tenhou/client.py | 17 ++++++++++------- project/tenhou/decoder.py | 3 +++ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/project/tenhou/client.py b/project/tenhou/client.py index e4cade34..8ca50eeb 100644 --- a/project/tenhou/client.py +++ b/project/tenhou/client.py @@ -326,11 +326,7 @@ def start_game(self): self._send_message('') # set was called - if ' Date: Mon, 19 Nov 2018 23:10:10 +0300 Subject: [PATCH 104/126] don't go through wait choosing logic if there is only one option #103 --- project/game/ai/first_version/hand_builder.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/project/game/ai/first_version/hand_builder.py b/project/game/ai/first_version/hand_builder.py index a35bb4d5..f010c014 100644 --- a/project/game/ai/first_version/hand_builder.py +++ b/project/game/ai/first_version/hand_builder.py @@ -309,6 +309,10 @@ def _is_furiten(self, tile_34): 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] + # first of all we find tiles that have the best hand cost * ukeire value call_riichi = not self.player.is_open_hand From 17aa81fa746fd4a66d5e602b89f0b6f8b27b25db Mon Sep 17 00:00:00 2001 From: bogachev-pa Date: Tue, 20 Nov 2018 00:12:50 +0300 Subject: [PATCH 105/126] fix meld.called_tile check #105, #106 It used to return False if called_tile is 0, i.e. it's 1s, 1p or 1m. --- project/game/table.py | 2 +- project/game/tests/tests_client.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/project/game/table.py b/project/game/table.py index 4b23a16d..fd7c6bfe 100644 --- a/project/game/table.py +++ b/project/game/table.py @@ -112,7 +112,7 @@ def add_called_meld(self, player_seat, meld): tiles = meld.tiles[:] # called tile was already added to revealed array # because it was called on the discard - if meld.called_tile: + if meld.called_tile is not None: tiles.remove(meld.called_tile) # for chankan we already added 3 tiles diff --git a/project/game/tests/tests_client.py b/project/game/tests/tests_client.py index bc01eee6..d80d07ea 100644 --- a/project/game/tests/tests_client.py +++ b/project/game/tests/tests_client.py @@ -40,7 +40,9 @@ def test_call_meld(self): client.player.tiles = [0] meld = Meld() meld.type = Meld.KAN - meld.called_tile = 0 + # closed kan + meld.called_tile = None + meld.opened = False client.table.add_called_meld(0, meld) self.assertEqual(len(client.player.melds), 2) From fe9eba77fc7296cdd8ff29c3ec574c62471fd2ad Mon Sep 17 00:00:00 2001 From: bogachev-pa Date: Fri, 23 Nov 2018 01:25:05 +0300 Subject: [PATCH 106/126] fix remaining tiles counting when calling melds #107 --- project/game/table.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/project/game/table.py b/project/game/table.py index fd7c6bfe..1f12a5f0 100644 --- a/project/game/table.py +++ b/project/game/table.py @@ -98,13 +98,17 @@ def init_round(self, def add_called_meld(self, player_seat, meld): self.meld_was_called = True - # when opponent called meld it is means - # that he discards tile from hand, not from wall - self.count_of_remaining_tiles += 1 - - # we will decrease count of remaining tiles after called kan - # because we had to complement dead wall - if meld.type == Meld.KAN or meld.type == meld.CHANKAN: + # if meld was called from the other player, then we skip one draw from the wall + if meld.opened: + # but if it's an opened kan, player will get a tile from + # a dead wall, so total number of tiles in the wall is the same + # as if he just draws a tile + if meld.type != Meld.KAN: + self.count_of_remaining_tiles += 1 + else: + # can't have a pon or chi from the hand + assert meld.type == Meld.KAN or meld.type == meld.CHANKAN + # player draws additional tile from the wall in case of closed kan or chankan self.count_of_remaining_tiles -= 1 self.get_player(player_seat).add_called_meld(meld) From ad5edb079652b9a9dd868955baaefbc36d7e995c Mon Sep 17 00:00:00 2001 From: bogachev-pa Date: Fri, 23 Nov 2018 01:54:33 +0300 Subject: [PATCH 107/126] correctly determine player from who meld was called #107 In tenhou message, player from who the meld was called is encoded relative to the player who called the meld, not relative to the player to whom message is sent. --- project/tenhou/decoder.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/project/tenhou/decoder.py b/project/tenhou/decoder.py index e73c85a4..156f3f04 100644 --- a/project/tenhou/decoder.py +++ b/project/tenhou/decoder.py @@ -167,7 +167,9 @@ def parse_meld(self, message): meld = Meld() meld.who = int(self.get_attribute_content(message, 'who')) - meld.from_who = data & 0x3 + # 'from_who' is encoded relative the the 'who', so we want + # to convert it to be relative to our player + meld.from_who = ((data & 0x3) + meld.who) % 4 if data & 0x4: self.parse_chi(data, meld) From 552b0384171085def858872cb66a63b6c7a726bc Mon Sep 17 00:00:00 2001 From: bogachev-pa Date: Fri, 23 Nov 2018 02:24:31 +0300 Subject: [PATCH 108/126] fix tests after melds accouting rework #107 --- project/game/tests/tests_client.py | 31 ++++++++++++++++++++++++--- project/tenhou/tests/tests_decoder.py | 4 ++-- 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/project/game/tests/tests_client.py b/project/game/tests/tests_client.py index d80d07ea..faaacb67 100644 --- a/project/game/tests/tests_client.py +++ b/project/game/tests/tests_client.py @@ -25,7 +25,7 @@ def test_discard_tile(self): self.assertFalse(tile in client.table.player.tiles) self.assertEqual(client.table.count_of_remaining_tiles, 69) - def test_call_meld(self): + def test_call_meld_closed_kan(self): client = Client() client.table.init_round(0, 0, 0, 0, 0, [0, 0, 0, 0]) @@ -41,13 +41,38 @@ def test_call_meld(self): meld = Meld() meld.type = Meld.KAN # closed kan + meld.tiles = [0, 1, 2, 3] meld.called_tile = None meld.opened = False client.table.add_called_meld(0, meld) self.assertEqual(len(client.player.melds), 2) - # +1 for called meld - # -1 for called kan + # kan was closed, so -1 + self.assertEqual(client.table.count_of_remaining_tiles, 70) + + def test_call_meld_kan_from_player(self): + client = Client() + + client.table.init_round(0, 0, 0, 0, 0, [0, 0, 0, 0]) + self.assertEqual(client.table.count_of_remaining_tiles, 70) + + meld = Meld() + client.table.add_called_meld(0, meld) + + self.assertEqual(len(client.player.melds), 1) + self.assertEqual(client.table.count_of_remaining_tiles, 71) + + client.player.tiles = [0] + meld = Meld() + meld.type = Meld.KAN + # closed kan + meld.tiles = [0, 1, 2, 3] + meld.called_tile = 0 + meld.opened = True + client.table.add_called_meld(0, meld) + + self.assertEqual(len(client.player.melds), 2) + # kan was called from another player, total number of remaining tiles stays the same self.assertEqual(client.table.count_of_remaining_tiles, 71) def test_enemy_discard(self): diff --git a/project/tenhou/tests/tests_decoder.py b/project/tenhou/tests/tests_decoder.py index 28ace4d3..660750dc 100644 --- a/project/tenhou/tests/tests_decoder.py +++ b/project/tenhou/tests/tests_decoder.py @@ -124,7 +124,7 @@ def test_parse_called_opened_kan(self): meld = decoder.parse_meld('') self.assertEqual(meld.who, 3) - self.assertEqual(meld.from_who, 1) + self.assertEqual(meld.from_who, 0) self.assertEqual(meld.type, Meld.KAN) self.assertEqual(meld.opened, True) self.assertEqual(meld.tiles, [52, 53, 54, 55]) @@ -174,7 +174,7 @@ def test_parse_who_called_riichi(self): def test_reconnection_information(self): message = ' Date: Sun, 25 Nov 2018 17:04:29 +0700 Subject: [PATCH 109/126] Reduce agari delay --- project/tenhou/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/tenhou/client.py b/project/tenhou/client.py index 8ca50eeb..8352676e 100644 --- a/project/tenhou/client.py +++ b/project/tenhou/client.py @@ -322,7 +322,7 @@ def start_game(self): # the end of round if '') # set was called From d559c34556e7abbcefecc3c7c1e77014108334c2 Mon Sep 17 00:00:00 2001 From: Nihisil Date: Tue, 27 Nov 2018 23:03:42 +0700 Subject: [PATCH 110/126] 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 9723dac45c72278769050e6bd1ade3ea8999583c 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 111/126] 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 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 112/126] 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 113/126] 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 114/126] 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 115/126] 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 116/126] 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 117/126] 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 118/126] 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 119/126] 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 120/126] 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') From 2b3c1345216fba6dd4b808cb872755a4873187e4 Mon Sep 17 00:00:00 2001 From: bogachev-pa Date: Sat, 1 Dec 2018 21:34:12 +0300 Subject: [PATCH 121/126] fix shanten calculation rules in regard with chiitoitsu consideration --- project/game/ai/first_version/hand_builder.py | 33 +++++++++++++++---- project/game/ai/first_version/main.py | 17 ++-------- .../tests/strategies/tests_chiitoitsu.py | 12 +++++++ 3 files changed, 42 insertions(+), 20 deletions(-) diff --git a/project/game/ai/first_version/hand_builder.py b/project/game/ai/first_version/hand_builder.py index a2e984f2..faaa1bbf 100644 --- a/project/game/ai/first_version/hand_builder.py +++ b/project/game/ai/first_version/hand_builder.py @@ -114,13 +114,35 @@ def discard_tile(self, tiles, closed_hand, melds, print_log=True): return self.process_discard_option(selected_tile, closed_hand, print_log=print_log) + def calculate_shanten(self, tiles_34, open_sets_34=None): + shanten_with_chiitoitsu = self.ai.shanten_calculator.calculate_shanten(tiles_34, + open_sets_34, + chiitoitsu=True) + shanten_without_chiitoitsu = self.ai.shanten_calculator.calculate_shanten(tiles_34, + open_sets_34, + chiitoitsu=False) + + if shanten_with_chiitoitsu == 0 and shanten_without_chiitoitsu >= 1: + shanten = shanten_with_chiitoitsu + use_chiitoitsu = True + elif shanten_with_chiitoitsu == 1 and shanten_without_chiitoitsu >= 3: + shanten = shanten_with_chiitoitsu + use_chiitoitsu = True + else: + shanten = shanten_without_chiitoitsu + use_chiitoitsu = False + + return shanten, use_chiitoitsu + def calculate_waits(self, tiles_34, open_sets_34=None): """ :param tiles_34: array of tiles in 34 formant, 13 of them (this is important) :param open_sets_34: array of array with tiles in 34 format :return: array of waits in 34 format and number of shanten """ - shanten = self.ai.shanten_calculator.calculate_shanten(tiles_34, open_sets_34, chiitoitsu=self.ai.use_chitoitsu) + + shanten, use_chiitoitsu = self.calculate_shanten(tiles_34, open_sets_34) + waiting = [] for j in range(0, 34): if tiles_34[j] == 4: @@ -131,7 +153,7 @@ def calculate_waits(self, tiles_34, open_sets_34=None): key = '{},{},{}'.format( ''.join([str(x) for x in tiles_34]), ';'.join([str(x) for x in open_sets_34]), - self.ai.use_chitoitsu and 1 or 0 + use_chiitoitsu and 1 or 0 ) if key in self.ai.hand_cache: @@ -140,7 +162,7 @@ def calculate_waits(self, tiles_34, open_sets_34=None): new_shanten = self.ai.shanten_calculator.calculate_shanten( tiles_34, open_sets_34, - chiitoitsu=self.ai.use_chitoitsu + chiitoitsu=use_chiitoitsu ) self.ai.hand_cache[key] = new_shanten @@ -188,10 +210,9 @@ def find_discard_options(self, tiles, closed_hand, melds=None): if is_agari: shanten = Shanten.AGARI_STATE else: - shanten = self.ai.shanten_calculator.calculate_shanten( + shanten, _ = self.calculate_shanten( tiles_34, - open_sets_34, - chiitoitsu=self.ai.use_chitoitsu + open_sets_34 ) return results, shanten diff --git a/project/game/ai/first_version/main.py b/project/game/ai/first_version/main.py index 38d07d1f..efa65c04 100644 --- a/project/game/ai/first_version/main.py +++ b/project/game/ai/first_version/main.py @@ -44,7 +44,6 @@ class ImplementationAI(InterfaceAI): current_strategy = None last_discard_option = None - use_chitoitsu = False hand_cache = {} @@ -70,7 +69,6 @@ def erase_state(self): self.current_strategy = None self.last_discard_option = None - self.use_chitoitsu = False self.hand_cache = {} @@ -81,9 +79,7 @@ def init_hand(self): 'Hand: {}'.format(self.player.format_hand_for_print()), ]) - self.shanten = self.shanten_calculator.calculate_shanten( - TilesConverter.to_34_array(self.player.tiles) - ) + self.shanten, _ = self.hand_builder.calculate_shanten(TilesConverter.to_34_array(self.player.tiles)) def draw_tile(self, tile_136): self.determine_strategy(self.player.tiles) @@ -111,11 +107,8 @@ def try_to_call_meld(self, tile_136, is_kamicha_discard): return None, None tiles_34 = TilesConverter.to_34_array(tiles_136) - previous_shanten = self.shanten_calculator.calculate_shanten( - tiles_34, - self.player.meld_34_tiles, - chiitoitsu=self.use_chitoitsu - ) + + previous_shanten, _ = self.hand_builder.calculate_shanten(tiles_34, self.player.meld_34_tiles) if previous_shanten == Shanten.AGARI_STATE and not self.current_strategy.can_meld_into_agari(): return None, None @@ -133,8 +126,6 @@ def try_to_call_meld(self, tile_136, is_kamicha_discard): return meld, discard_option def determine_strategy(self, tiles_136): - self.use_chitoitsu = False - # for already opened hand we don't need to give up on selected strategy if self.player.is_open_hand and self.current_strategy: return False @@ -160,8 +151,6 @@ def determine_strategy(self, tiles_136): self.current_strategy = strategy if self.current_strategy: - self.use_chitoitsu = self.current_strategy.type == BaseStrategy.CHIITOITSU - if not old_strategy or self.current_strategy.type != old_strategy.type: DecisionsLogger.debug( log.STRATEGY_ACTIVATE, 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 c72e5bd2..233945dc 100644 --- a/project/game/ai/first_version/tests/strategies/tests_chiitoitsu.py +++ b/project/game/ai/first_version/tests/strategies/tests_chiitoitsu.py @@ -50,3 +50,15 @@ def test_dont_call_meld(self): tile = self._string_to_136_tile(man='9') meld, _ = player.try_to_call_meld(tile, True) self.assertEqual(meld, None) + + def test_keep_chiitoitsu_tempai(self): + table = Table() + player = table.player + + tiles = self._string_to_136_array(sou='113355', man='22669', pin='99') + player.init_hand(tiles) + + player.draw_tile(self._string_to_136_tile(man='6')) + + discard = player.discard_tile() + self.assertEqual(self._to_string([discard]), '6m') From 7eaf1d7d0ba210acb40a46e1e03ced95e212294d Mon Sep 17 00:00:00 2001 From: bogachev-pa Date: Sat, 1 Dec 2018 22:52:14 +0300 Subject: [PATCH 122/126] revise strategies order Also, fixed bug with try_to_call_meld previous shanten calculation. --- project/game/ai/first_version/main.py | 21 ++++++------ .../tests/strategies/tests_chiitoitsu.py | 32 +++++++++++++++++++ 2 files changed, 43 insertions(+), 10 deletions(-) diff --git a/project/game/ai/first_version/main.py b/project/game/ai/first_version/main.py index efa65c04..5b6ee3a8 100644 --- a/project/game/ai/first_version/main.py +++ b/project/game/ai/first_version/main.py @@ -100,15 +100,15 @@ def discard_tile(self, discard_tile, print_log=True): ) def try_to_call_meld(self, tile_136, is_kamicha_discard): - tiles_136 = self.player.tiles[:] + [tile_136] + tiles_136_previous = self.player.tiles[:] + tiles_136 = tiles_136_previous + [tile_136] self.determine_strategy(tiles_136) if not self.current_strategy: return None, None - tiles_34 = TilesConverter.to_34_array(tiles_136) - - previous_shanten, _ = self.hand_builder.calculate_shanten(tiles_34, self.player.meld_34_tiles) + tiles_34_previous = TilesConverter.to_34_array(tiles_136_previous) + previous_shanten, _ = self.hand_builder.calculate_shanten(tiles_34_previous, self.player.meld_34_tiles) if previous_shanten == Shanten.AGARI_STATE and not self.current_strategy.can_meld_into_agari(): return None, None @@ -133,22 +133,23 @@ def determine_strategy(self, tiles_136): old_strategy = self.current_strategy self.current_strategy = None - # order is important - strategies = [ - ChinitsuStrategy(BaseStrategy.CHINITSU, self.player), - HonitsuStrategy(BaseStrategy.HONITSU, self.player), - YakuhaiStrategy(BaseStrategy.YAKUHAI, self.player), - ] + # order is important, we add strategies with the highest priority first + strategies = [] if self.player.table.has_open_tanyao: strategies.append(TanyaoStrategy(BaseStrategy.TANYAO, self.player)) + strategies.append(YakuhaiStrategy(BaseStrategy.YAKUHAI, self.player)) + strategies.append(HonitsuStrategy(BaseStrategy.HONITSU, self.player)) + strategies.append(ChinitsuStrategy(BaseStrategy.CHINITSU, self.player)) + strategies.append(ChiitoitsuStrategy(BaseStrategy.CHIITOITSU, self.player)) strategies.append(FormalTempaiStrategy(BaseStrategy.FORMAL_TEMPAI, self.player)) for strategy in strategies: if strategy.should_activate_strategy(tiles_136): self.current_strategy = strategy + break if self.current_strategy: if not old_strategy or self.current_strategy.type != old_strategy.type: 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 233945dc..07a4e23f 100644 --- a/project/game/ai/first_version/tests/strategies/tests_chiitoitsu.py +++ b/project/game/ai/first_version/tests/strategies/tests_chiitoitsu.py @@ -62,3 +62,35 @@ def test_keep_chiitoitsu_tempai(self): discard = player.discard_tile() self.assertEqual(self._to_string([discard]), '6m') + + def test_5_pairs_yakuhai_not_chiitoitsu(self): + table = Table() + player = table.player + + table.add_dora_indicator(self._string_to_136_tile(sou='9')) + table.add_dora_indicator(self._string_to_136_tile(sou='1')) + + tiles = self._string_to_136_array(sou='112233', pin='16678', honors='66') + player.init_hand(tiles) + + tile = self._string_to_136_tile(honors='6') + meld, _ = player.try_to_call_meld(tile, True) + + self.assertNotEqual(player.ai.current_strategy.type, BaseStrategy.CHIITOITSU) + + self.assertEqual(player.ai.current_strategy.type, BaseStrategy.YAKUHAI) + + self.assertNotEqual(meld, None) + + def chiitoitsu_tanyao_tempai(self): + table = Table() + player = table.player + + tiles = self._string_to_136_array(sou='223344', pin='788', man='4577') + player.init_hand(tiles) + + player.draw_tile(self._string_to_136_tile(man='4')) + + discard = player.discard_tile() + discard_correct = self._to_string([discard]) == '7p' or self._to_string([discard]) == '5m' + self.assertEqual(discard_correct, True) From be3f08a63061286ba09571e493ac155b1e33936e Mon Sep 17 00:00:00 2001 From: bogachev-pa Date: Mon, 3 Dec 2018 00:39:17 +0300 Subject: [PATCH 123/126] fix ukeire2 calculation when we are doing a meld --- project/game/ai/first_version/hand_builder.py | 8 +-- .../ai/first_version/tests/tests_discards.py | 54 ++++++++++++++++--- 2 files changed, 53 insertions(+), 9 deletions(-) diff --git a/project/game/ai/first_version/hand_builder.py b/project/game/ai/first_version/hand_builder.py index faaa1bbf..9ff28a66 100644 --- a/project/game/ai/first_version/hand_builder.py +++ b/project/game/ai/first_version/hand_builder.py @@ -505,7 +505,7 @@ def choose_tile_to_discard(self, tiles, closed_hand, melds, print_log=True): if first_option.shanten in [1, 2, 3]: ukeire_field = 'ukeire_second' for x in possible_options: - self.calculate_second_level_ukeire(x) + self.calculate_second_level_ukeire(x, tiles, melds) possible_options = sorted(possible_options, key=lambda x: -getattr(x, ukeire_field)) @@ -621,7 +621,7 @@ def process_discard_option(self, discard_option, closed_hand, force_discard=Fals else: return discard_option.find_tile_in_hand(closed_hand) - def calculate_second_level_ukeire(self, discard_option): + def calculate_second_level_ukeire(self, discard_option, tiles, melds): not_suitable_tiles = self.ai.current_strategy and self.ai.current_strategy.not_suitable_tiles or [] call_riichi = not self.player.is_open_hand @@ -630,6 +630,8 @@ def calculate_second_level_ukeire(self, discard_option): player_tiles_original = self.player.tiles.copy() tile_in_hand = discard_option.find_tile_in_hand(self.player.closed_hand) + + self.player.tiles = tiles.copy() self.player.tiles.remove(tile_in_hand) sum_tiles = 0 @@ -650,7 +652,7 @@ def calculate_second_level_ukeire(self, discard_option): results, shanten = self.find_discard_options( self.player.tiles, self.player.closed_hand, - self.player.melds + melds ) results = [x for x in results if x.shanten == discard_option.shanten - 1] diff --git a/project/game/ai/first_version/tests/tests_discards.py b/project/game/ai/first_version/tests/tests_discards.py index 5a07175c..ea29fbbf 100644 --- a/project/game/ai/first_version/tests/tests_discards.py +++ b/project/game/ai/first_version/tests/tests_discards.py @@ -762,30 +762,72 @@ 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) + player.ai.hand_builder.calculate_second_level_ukeire(discard_option, player.tiles, player.melds) 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) + player.ai.hand_builder.calculate_second_level_ukeire(discard_option, player.tiles, player.melds) 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) + player.ai.hand_builder.calculate_second_level_ukeire(discard_option, player.tiles, player.melds) 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) + player.ai.hand_builder.calculate_second_level_ukeire(discard_option, player.tiles, player.melds) 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) + player.ai.hand_builder.calculate_second_level_ukeire(discard_option, player.tiles, player.melds) 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) + player.ai.hand_builder.calculate_second_level_ukeire(discard_option, player.tiles, player.melds) self.assertEqual(discard_option.ukeire_second, 96) + + def test_choose_1_shanten_with_cost_possibility_draw(self): + table = Table() + player = table.player + table.add_dora_indicator(self._string_to_136_tile(sou='4')) + + tiles = self._string_to_136_array(man='557', pin='468', sou='55577', honors='66') + player.init_hand(tiles) + + meld = self._make_meld(Meld.PON, sou='555') + player.add_called_meld(meld) + + tile = self._string_to_136_tile(sou='7') + player.draw_tile(tile) + discarded_tile = player.discard_tile() + self.assertNotEqual(player.ai.current_strategy, None) + self.assertEqual(player.ai.current_strategy.type, BaseStrategy.YAKUHAI) + self.assertEqual(self._to_string([discarded_tile]), '7m') + + def test_choose_1_shanten_with_cost_possibility_meld(self): + table = Table() + player = table.player + table.add_dora_indicator(self._string_to_136_tile(sou='4')) + + tiles = self._string_to_136_array(man='557', pin='468', sou='55577', honors='66') + player.init_hand(tiles) + + meld = self._make_meld(Meld.PON, sou='555') + player.add_called_meld(meld) + + tile = self._string_to_136_tile(sou='7') + meld, discard_option = player.try_to_call_meld(tile, False) + self.assertNotEqual(meld, None) + self.assertEqual(meld.type, Meld.PON) + self.assertEqual(self._to_string(meld.tiles), '777s') + + self.assertNotEqual(player.ai.current_strategy, None) + self.assertEqual(player.ai.current_strategy.type, BaseStrategy.YAKUHAI) + + discarded_tile = discard_option.find_tile_in_hand(player.closed_hand) + + self.assertEqual(self._to_string([discarded_tile]), '7m') From 6432fb89a443b7a31912daaa96c9cb82990f3b51 Mon Sep 17 00:00:00 2001 From: Nihisil Date: Tue, 4 Dec 2018 14:27:13 +0700 Subject: [PATCH 124/126] Improve logic to call chankan sets --- project/game/ai/first_version/main.py | 116 +++++++++++------- .../game/ai/first_version/tests/tests_ai.py | 47 ++++++- 2 files changed, 119 insertions(+), 44 deletions(-) diff --git a/project/game/ai/first_version/main.py b/project/game/ai/first_version/main.py index 5b6ee3a8..8499d4a9 100644 --- a/project/game/ai/first_version/main.py +++ b/project/game/ai/first_version/main.py @@ -236,71 +236,83 @@ def should_call_kan(self, tile, open_kan, from_riichi=False): tiles_34 = TilesConverter.to_34_array(self.player.tiles) closed_hand_34 = TilesConverter.to_34_array(self.player.closed_hand) - pon_melds = [x for x in self.player.meld_34_tiles if is_pon(x)] - - # let's check can we upgrade opened pon to the kan - if pon_melds: - for meld in pon_melds: - # tile is equal to our already opened pon, - # so let's call chankan! - if tile_34 in meld: - return Meld.CHANKAN melds_34 = copy.copy(self.player.meld_34_tiles) tiles = copy.copy(self.player.tiles) closed_hand_tiles = copy.copy(self.player.closed_hand) - # we can try to call closed meld - if closed_hand_34[tile_34] == 3: + new_shanten = 0 + previous_shanten = 0 + new_waits_count = 0 + previous_waits_count = 0 + + # let's check can we upgrade opened pon to the kan + pon_melds = [x for x in self.player.meld_34_tiles if is_pon(x)] + has_chankan_candidate = False + for meld in pon_melds: + # tile is equal to our already opened pon + if tile_34 in meld: + has_chankan_candidate = True + + tiles.append(tile) + closed_hand_tiles.append(tile) + + previous_shanten, previous_waits_count = self._calculate_shanten_for_kan( + tiles, + closed_hand_tiles, + self.player.melds + ) + + tiles_34 = TilesConverter.to_34_array(tiles) + tiles_34[tile_34] -= 1 + + new_waiting, new_shanten = self.hand_builder.calculate_waits( + tiles_34, + self.player.meld_34_tiles + ) + new_waits_count = self.hand_builder.count_tiles(new_waiting, tiles_34) + + if not has_chankan_candidate: + # we don't have enough tiles in the hand + if closed_hand_34[tile_34] != 3: + return None + if open_kan or from_riichi: # this 4 tiles can only be used in kan, no other options previous_waiting, previous_shanten = self.hand_builder.calculate_waits(tiles_34, melds_34) - previous_waits_cnt = self.hand_builder.count_tiles(previous_waiting, closed_hand_34) - - # shanten calculator doesn't like working with kans, so we pretend it's a pon - melds_34 += [[tile_34, tile_34, tile_34]] - closed_hand_34[tile_34] = 0 - - new_waiting, new_shanten = self.hand_builder.calculate_waits(tiles_34, melds_34) - new_waits_cnt = self.hand_builder.count_tiles(new_waiting, closed_hand_34) + previous_waits_count = self.hand_builder.count_tiles(previous_waiting, closed_hand_34) else: - # if we can use or tile in the hand for the forms other than KAN tiles.append(tile) - closed_hand_tiles.append(tile) - closed_hand_34[tile_34] += 1 - previous_results, previous_shanten = self.hand_builder.find_discard_options( + previous_shanten, previous_waits_count = self._calculate_shanten_for_kan( tiles, closed_hand_tiles, self.player.melds ) - previous_results = [x for x in previous_results if x.shanten == previous_shanten] - - # it is possible that we don't have results here - # when we are in agari state (but without yaku) - if not previous_results: - return None + # shanten calculator doesn't like working with kans, so we pretend it's a pon + melds_34 += [[tile_34, tile_34, tile_34]] + new_waiting, new_shanten = self.hand_builder.calculate_waits(tiles_34, melds_34) - previous_waits_cnt = sorted(previous_results, key=lambda x: -x.ukeire)[0].ukeire + closed_hand_34[tile_34] = 4 + new_waits_count = self.hand_builder.count_tiles(new_waiting, closed_hand_34) - # shanten calculator doesn't like working with kans, so we pretend it's a pon - closed_hand_34[tile_34] = 0 - melds_34 += [[tile_34, tile_34, tile_34]] + # it is possible that we don't have results here + # when we are in agari state (but without yaku) + if previous_shanten is None: + return None - new_waiting, new_shanten = self.hand_builder.calculate_waits(tiles_34, melds_34) - new_waits_cnt = self.hand_builder.count_tiles(new_waiting, closed_hand_34) + # it is not possible to reduce number of shanten by calling a kan + assert new_shanten >= previous_shanten - # it is not possible to reduce number of shanten by calling a kan - assert new_shanten >= previous_shanten + # if shanten number is the same, we should only call kan if ukeire didn't become worse + if new_shanten == previous_shanten: + # we cannot improve ukeire by calling kan (not considering the tile we drew from the dead wall) + assert new_waits_count <= previous_waits_count - # if shanten number is the same, we should only call kan if ukeire didn't become worse - if new_shanten == previous_shanten: - # we cannot improve ukeire by calling kan (not considering the tile we drew from the dead wall) - assert new_waits_cnt <= previous_waits_cnt - if new_waits_cnt == previous_waits_cnt: - return Meld.KAN + if new_waits_count == previous_waits_count: + return has_chankan_candidate and Meld.CHANKAN or Meld.KAN return None @@ -324,3 +336,21 @@ def enemy_players(self): Return list of players except our bot """ return self.player.table.players[1:] + + def _calculate_shanten_for_kan(self, tiles, closed_hand_tiles, melds): + previous_results, previous_shanten = self.hand_builder.find_discard_options( + tiles, + closed_hand_tiles, + melds + ) + + previous_results = [x for x in previous_results if x.shanten == previous_shanten] + + # it is possible that we don't have results here + # when we are in agari state (but without yaku) + if not previous_results: + return None, None + + previous_waits_cnt = sorted(previous_results, key=lambda x: -x.ukeire)[0].ukeire + + return previous_shanten, previous_waits_cnt diff --git a/project/game/ai/first_version/tests/tests_ai.py b/project/game/ai/first_version/tests/tests_ai.py index ba7b220e..6e0ac334 100644 --- a/project/game/ai/first_version/tests/tests_ai.py +++ b/project/game/ai/first_version/tests/tests_ai.py @@ -395,7 +395,7 @@ def test_using_tiles_of_different_suit_for_chi(self): meld, _ = player.try_to_call_meld(tile, True) self.assertIsNotNone(meld) - def test_upgrade_opened_pon_to_kan(self): + def test_call_chankan_and_bad_ukeire_after_call(self): table = Table() table.count_of_remaining_tiles = 10 @@ -409,6 +409,31 @@ def test_upgrade_opened_pon_to_kan(self): self.assertEqual(len(table.player.melds), 1) self.assertEqual(len(table.player.tiles), 13) + self.assertEqual(table.player.should_call_kan(tile, False), None) + + def test_call_chankan_and_bad_ukeire_after_call_second(self): + table = Table() + table.count_of_remaining_tiles = 10 + + tiles = self._string_to_136_array(man='3455567', sou='222', honors='666') + table.player.init_hand(tiles) + table.player.add_called_meld(self._make_meld(Meld.PON, man='555')) + table.player.add_called_meld(self._make_meld(Meld.PON, honors='666')) + + tile = self._string_to_136_tile(man='5') + + self.assertEqual(table.player.should_call_kan(tile, False), None) + + def test_upgrade_opened_pon_to_kan(self): + table = Table() + table.count_of_remaining_tiles = 10 + + tiles = self._string_to_136_array(man='3455567', sou='222', honors='666') + table.player.init_hand(tiles) + table.player.add_called_meld(self._make_meld(Meld.PON, honors='666')) + + tile = self._string_to_136_tile(honors='6') + self.assertEqual(table.player.should_call_kan(tile, False), Meld.CHANKAN) def test_call_closed_kan(self): @@ -451,6 +476,8 @@ def test_opened_kan(self): tile = self._string_to_136_tile(sou='1') self.assertEqual(table.player.should_call_kan(tile, True), None) + # test case 2 + table = Table() table.count_of_remaining_tiles = 10 @@ -466,6 +493,24 @@ def test_opened_kan(self): tile = self._string_to_136_tile(sou='1') self.assertEqual(table.player.should_call_kan(tile, True), Meld.KAN) + # test case 3 + # we are in tempai already and there was a crash on 5s meld suggestion + + table = Table() + table.count_of_remaining_tiles = 10 + table.add_dora_indicator(self._string_to_136_tile(honors='5')) + + tiles = self._string_to_136_array(man='456', sou='55567678', honors='66') + table.player.init_hand(tiles) + table.player.add_called_meld(self._make_meld(Meld.CHI, sou='678')) + + # to rebuild all caches + table.player.draw_tile(self._string_to_136_tile(pin='9')) + table.player.discard_tile() + + tile = self._string_to_136_tile(sou='5') + self.assertEqual(table.player.should_call_kan(tile, True), None) + def test_dont_call_kan_in_defence_mode(self): table = Table() From b1da51832fbd42bfe41f83a9606dd9029822b5a7 Mon Sep 17 00:00:00 2001 From: Nihisil Date: Tue, 4 Dec 2018 15:17:32 +0700 Subject: [PATCH 125/126] Add more tests and rename chankan to shouminkan in comments --- project/game/ai/base/main.py | 2 +- project/game/ai/first_version/main.py | 8 ++--- .../game/ai/first_version/tests/tests_ai.py | 36 +++++++++++++++---- project/game/player.py | 4 +-- project/game/table.py | 4 +-- project/tenhou/client.py | 6 ++-- 6 files changed, 43 insertions(+), 17 deletions(-) diff --git a/project/game/ai/base/main.py b/project/game/ai/base/main.py index eec5ee19..2af139ac 100644 --- a/project/game/ai/base/main.py +++ b/project/game/ai/base/main.py @@ -58,7 +58,7 @@ def should_call_riichi(self): def should_call_kan(self, tile, is_open_kan, from_riichi=False): """ - When bot can call kan or chankan this method will be called + When bot can call kan or shouminkan this method will be called :param tile: 136 tile format :param is_open_kan: boolean :param from_riichi: boolean diff --git a/project/game/ai/first_version/main.py b/project/game/ai/first_version/main.py index 8499d4a9..d8323f53 100644 --- a/project/game/ai/first_version/main.py +++ b/project/game/ai/first_version/main.py @@ -248,11 +248,11 @@ def should_call_kan(self, tile, open_kan, from_riichi=False): # let's check can we upgrade opened pon to the kan pon_melds = [x for x in self.player.meld_34_tiles if is_pon(x)] - has_chankan_candidate = False + has_shouminkan_candidate = False for meld in pon_melds: # tile is equal to our already opened pon if tile_34 in meld: - has_chankan_candidate = True + has_shouminkan_candidate = True tiles.append(tile) closed_hand_tiles.append(tile) @@ -272,7 +272,7 @@ def should_call_kan(self, tile, open_kan, from_riichi=False): ) new_waits_count = self.hand_builder.count_tiles(new_waiting, tiles_34) - if not has_chankan_candidate: + if not has_shouminkan_candidate: # we don't have enough tiles in the hand if closed_hand_34[tile_34] != 3: return None @@ -312,7 +312,7 @@ def should_call_kan(self, tile, open_kan, from_riichi=False): assert new_waits_count <= previous_waits_count if new_waits_count == previous_waits_count: - return has_chankan_candidate and Meld.CHANKAN or Meld.KAN + return has_shouminkan_candidate and Meld.CHANKAN or Meld.KAN return None diff --git a/project/game/ai/first_version/tests/tests_ai.py b/project/game/ai/first_version/tests/tests_ai.py index 6e0ac334..3b76676f 100644 --- a/project/game/ai/first_version/tests/tests_ai.py +++ b/project/game/ai/first_version/tests/tests_ai.py @@ -395,7 +395,7 @@ def test_using_tiles_of_different_suit_for_chi(self): meld, _ = player.try_to_call_meld(tile, True) self.assertIsNotNone(meld) - def test_call_chankan_and_bad_ukeire_after_call(self): + def test_call_upgrade_pon_and_bad_ukeire_after_call(self): table = Table() table.count_of_remaining_tiles = 10 @@ -411,9 +411,10 @@ def test_call_chankan_and_bad_ukeire_after_call(self): self.assertEqual(len(table.player.tiles), 13) self.assertEqual(table.player.should_call_kan(tile, False), None) - def test_call_chankan_and_bad_ukeire_after_call_second(self): + def test_call_upgrade_pon_and_bad_ukeire_after_call_second_case(self): table = Table() table.count_of_remaining_tiles = 10 + player = table.player tiles = self._string_to_136_array(man='3455567', sou='222', honors='666') table.player.init_hand(tiles) @@ -424,7 +425,31 @@ def test_call_chankan_and_bad_ukeire_after_call_second(self): self.assertEqual(table.player.should_call_kan(tile, False), None) - def test_upgrade_opened_pon_to_kan(self): + player.draw_tile(tile) + discarded_tile = player.discard_tile() + + self.assertEqual(self._to_string([discarded_tile]), '3m') + + def test_call_upgrade_pon_and_bad_ukeire_after_call_third_case(self): + table = Table() + table.count_of_remaining_tiles = 10 + player = table.player + + tiles = self._string_to_136_array(man='67', pin='6', sou='1344478999') + table.player.init_hand(tiles) + table.player.add_called_meld(self._make_meld(Meld.PON, sou='444')) + + tile = self._string_to_136_tile(sou='4') + + # we don't want to call shouminkan here + self.assertEqual(table.player.should_call_kan(tile, False), None) + + player.draw_tile(tile) + discarded_tile = player.discard_tile() + + self.assertEqual(self._to_string([discarded_tile]), '6p') + + def test_call_shouminkan(self): table = Table() table.count_of_remaining_tiles = 10 @@ -476,8 +501,7 @@ def test_opened_kan(self): tile = self._string_to_136_tile(sou='1') self.assertEqual(table.player.should_call_kan(tile, True), None) - # test case 2 - + def test_opened_kan_second_case(self): table = Table() table.count_of_remaining_tiles = 10 @@ -493,7 +517,7 @@ def test_opened_kan(self): tile = self._string_to_136_tile(sou='1') self.assertEqual(table.player.should_call_kan(tile, True), Meld.KAN) - # test case 3 + def test_opened_kan_third_case(self): # we are in tempai already and there was a crash on 5s meld suggestion table = Table() diff --git a/project/game/player.py b/project/game/player.py index 6e3758bb..41d47e59 100644 --- a/project/game/player.py +++ b/project/game/player.py @@ -65,13 +65,13 @@ def erase_state(self): self.round_step = 0 def add_called_meld(self, meld: Meld): - # we already added chankan as a pon set + # we already added shouminkan as a pon set if meld.type == Meld.CHANKAN: tile_34 = meld.tiles[0] // 4 pon_set = [x for x in self.melds if x.type == Meld.PON and (x.tiles[0] // 4) == tile_34] - # when we are doing reconnect and we have called chankan set + # when we are doing reconnect and we have called shouminkan set # we will not have called pon set in the hand if pon_set: self.melds.remove(pon_set[0]) diff --git a/project/game/table.py b/project/game/table.py index 1f12a5f0..fdbf82cb 100644 --- a/project/game/table.py +++ b/project/game/table.py @@ -108,7 +108,7 @@ def add_called_meld(self, player_seat, meld): else: # can't have a pon or chi from the hand assert meld.type == Meld.KAN or meld.type == meld.CHANKAN - # player draws additional tile from the wall in case of closed kan or chankan + # player draws additional tile from the wall in case of closed kan or shouminkan self.count_of_remaining_tiles -= 1 self.get_player(player_seat).add_called_meld(meld) @@ -119,7 +119,7 @@ def add_called_meld(self, player_seat, meld): if meld.called_tile is not None: tiles.remove(meld.called_tile) - # for chankan we already added 3 tiles + # for shouminkan we already added 3 tiles if meld.type == meld.CHANKAN: tiles = [meld.tiles[0]] diff --git a/project/tenhou/client.py b/project/tenhou/client.py index fb7118f7..4d0a82ac 100644 --- a/project/tenhou/client.py +++ b/project/tenhou/client.py @@ -281,11 +281,13 @@ def start_game(self): if kan_type == Meld.CHANKAN: meld_type = 5 + logger.info('We upgraded pon to kan!') else: meld_type = 4 + logger.info('We called a closed kan set!') self._send_message(''.format(meld_type, drawn_tile)) - logger.info('We called a closed kan\chankan set!') + continue if not main_player.in_riichi: @@ -345,7 +347,7 @@ def start_game(self): ] # we win by other player's discard if any(i in message for i in win_suggestions): - # enemy called chankan and we can win there + # enemy called shouminkan and we can win there if self.decoder.is_opened_set_message(message): meld = self.decoder.parse_meld(message) tile = meld.called_tile From 2765985d7981847a5686e8d26d7a2db9ee85a4dd Mon Sep 17 00:00:00 2001 From: Nihisil Date: Tue, 4 Dec 2018 19:07:01 +0700 Subject: [PATCH 126/126] Improve tests a bit --- project/game/ai/first_version/tests/tests_ai.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/project/game/ai/first_version/tests/tests_ai.py b/project/game/ai/first_version/tests/tests_ai.py index 3b76676f..94ab478a 100644 --- a/project/game/ai/first_version/tests/tests_ai.py +++ b/project/game/ai/first_version/tests/tests_ai.py @@ -413,22 +413,23 @@ def test_call_upgrade_pon_and_bad_ukeire_after_call(self): def test_call_upgrade_pon_and_bad_ukeire_after_call_second_case(self): table = Table() + table.add_dora_indicator(self._string_to_136_tile(honors='5')) table.count_of_remaining_tiles = 10 player = table.player tiles = self._string_to_136_array(man='3455567', sou='222', honors='666') - table.player.init_hand(tiles) - table.player.add_called_meld(self._make_meld(Meld.PON, man='555')) - table.player.add_called_meld(self._make_meld(Meld.PON, honors='666')) + player.init_hand(tiles) + player.add_called_meld(self._make_meld(Meld.PON, man='555')) + player.add_called_meld(self._make_meld(Meld.PON, honors='666')) tile = self._string_to_136_tile(man='5') - self.assertEqual(table.player.should_call_kan(tile, False), None) + self.assertEqual(player.should_call_kan(tile, False), None) player.draw_tile(tile) discarded_tile = player.discard_tile() - self.assertEqual(self._to_string([discarded_tile]), '3m') + self.assertEqual(self._to_string([discarded_tile]), '2s') def test_call_upgrade_pon_and_bad_ukeire_after_call_third_case(self): table = Table() @@ -534,6 +535,7 @@ def test_opened_kan_third_case(self): tile = self._string_to_136_tile(sou='5') self.assertEqual(table.player.should_call_kan(tile, True), None) + self.assertEqual(table.player.try_to_call_meld(tile, True), (None, None)) def test_dont_call_kan_in_defence_mode(self): table = Table()