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. 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/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/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 f010c014..a2e984f2 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: @@ -202,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 @@ -304,15 +296,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 +329,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 +393,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 +498,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 +522,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, -x.ukeire_second, x.valuation))[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, @@ -635,22 +601,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 [] + call_riichi = not self.player.is_open_hand - tiles = copy.copy(self.player.tiles) - tiles.remove(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() + + 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 + 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 - tiles.append(wait_136) + self.player.tiles.append(wait_136) results, shanten = self.find_discard_options( - tiles, + self.player.tiles, self.player.closed_hand, self.player.melds ) @@ -658,15 +635,55 @@ def calculate_second_level_ukeire(self, discard_option): # 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) + result_has_atodzuke = False + if self.player.is_open_hand: + best_one = results[0] + best_ukeire = 0 + for result in results: + has_atodzuke = False + ukeire = 0 + for wait_34 in result.waiting: + if wait_34 in not_suitable_tiles: + has_atodzuke = True + else: + ukeire += result.wait_to_ukeire[wait_34] + + # let's consider atodzuke waits to be worse than non-atodzuke ones + if has_atodzuke: + ukeire /= 2 + + 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: + 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, call_riichi=call_riichi) + # we reduce tile valuation for atodzuke + if result_has_atodzuke: + cost_x_ukeire /= 2 + sum_cost += 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) @@ -675,7 +692,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: @@ -688,3 +706,39 @@ 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] + + 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] + + # 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/main.py b/project/game/ai/first_version/main.py index 17b2af71..38d07d1f 100644 --- a/project/game/ai/first_version/main.py +++ b/project/game/ai/first_version/main.py @@ -81,13 +81,9 @@ 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) + 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/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/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/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/strategies/yakuhai.py b/project/game/ai/first_version/strategies/yakuhai.py index 428c746d..a6a10a92 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 @@ -97,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_chiitoitsu.py b/project/game/ai/first_version/tests/strategies/tests_chiitoitsu.py index 75605f31..c72e5bd2 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.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_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_formal_tempai.py b/project/game/ai/first_version/tests/strategies/tests_formal_tempai.py index c93144ad..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') @@ -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) @@ -60,18 +60,17 @@ 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) 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/strategies/tests_tanyao.py b/project/game/ai/first_version/tests/strategies/tests_tanyao.py index 137560b4..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,15 +220,55 @@ 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) - 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..543dc2a9 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() @@ -512,9 +518,28 @@ 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')) 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') diff --git a/project/game/ai/first_version/tests/tests_ai.py b/project/game/ai/first_version/tests/tests_ai.py index 2de7825f..ba7b220e 100644 --- a/project/game/ai/first_version/tests/tests_ai.py +++ b/project/game/ai/first_version/tests/tests_ai.py @@ -267,26 +267,40 @@ 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) + player.draw_tile(self._string_to_136_tile(honors='5')) + player.discard_tile() - # 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) + 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_discards.py b/project/game/ai/first_version/tests/tests_discards.py index d55c0175..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) @@ -718,3 +718,74 @@ def test_choose_furiten_over_karaten(self): self._string_to_136_tile(honors='2'), '6z' ) + + def test_discard_tile_based_on_second_level_ukeire_and_cost(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() + discard_correct = self._to_string([discarded_tile]) == '2p' or self._to_string([discarded_tile]) == '3p' + self.assertEqual(discard_correct, True) + + 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] + 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) 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)