Coverage for project/game/ai/strategies_v2/tanyao.py : 92%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1import utils.decisions_constants as log
2from game.ai.strategies_v2.main import BaseStrategy
3from mahjong.constants import HONOR_INDICES, TERMINAL_INDICES
4from mahjong.tile import TilesConverter
5from mahjong.utils import is_honor, is_tile_strictly_isolated
6from utils.test_helpers import tiles_to_string
9class TanyaoStrategy(BaseStrategy):
10 min_shanten = 3
11 not_suitable_tiles = TERMINAL_INDICES + HONOR_INDICES
13 def get_open_hand_han(self):
14 return 1
16 def should_activate_strategy(self, tiles_136, meld_tile=None):
17 """
18 Tanyao hand is a hand without terminal and honor tiles, to achieve this
19 we will use different approaches
20 :return: boolean
21 """
23 result = super(TanyaoStrategy, self).should_activate_strategy(tiles_136)
24 if not result:
25 return False
27 tiles = TilesConverter.to_34_array(self.player.tiles)
29 closed_hand_34 = TilesConverter.to_34_array(self.player.closed_hand)
30 isolated_tiles = [
31 x // 4 for x in self.player.tiles if is_tile_strictly_isolated(closed_hand_34, x // 4) or is_honor(x // 4)
32 ]
34 count_of_terminal_pon_sets = 0
35 count_of_terminal_pairs = 0
36 count_of_valued_pairs = 0
37 count_of_not_suitable_tiles = 0
38 count_of_not_suitable_not_isolated_tiles = 0
39 for x in range(0, 34):
40 tile = tiles[x]
41 if not tile:
42 continue
44 if x in self.not_suitable_tiles and tile == 3:
45 count_of_terminal_pon_sets += 1
47 if x in self.not_suitable_tiles and tile == 2:
48 count_of_terminal_pairs += 1
50 if x in self.player.valued_honors:
51 count_of_valued_pairs += 1
53 if x in self.not_suitable_tiles:
54 count_of_not_suitable_tiles += tile
56 if x in self.not_suitable_tiles and x not in isolated_tiles:
57 count_of_not_suitable_not_isolated_tiles += tile
59 # we have too much terminals and honors
60 if count_of_not_suitable_tiles >= 5:
61 return False
63 # if we already have pon of honor\terminal tiles
64 # we don't need to open hand for tanyao
65 if count_of_terminal_pon_sets > 0:
66 return False
68 # with valued pair (yakuhai wind or dragon)
69 # we don't need to go for tanyao
70 if count_of_valued_pairs > 0:
71 return False
73 # one pair is ok in tanyao pair
74 # but 2+ pairs can't be suitable
75 if count_of_terminal_pairs > 1:
76 return False
78 # 3 or more not suitable tiles that
79 # are not isolated is too much
80 if count_of_not_suitable_not_isolated_tiles >= 3:
81 return False
83 # if we are 1 shanten, even 2 tiles
84 # that are not suitable and not isolated
85 # is too much
86 if count_of_not_suitable_not_isolated_tiles >= 2 and self.player.ai.shanten == 1:
87 return False
89 # TODO: don't open from good 1-shanten into tanyao 1-shaten with same ukeire or worse
91 # 123 and 789 indices
92 indices = [[0, 1, 2], [6, 7, 8], [9, 10, 11], [15, 16, 17], [18, 19, 20], [24, 25, 26]]
94 for index_set in indices:
95 first = tiles[index_set[0]]
96 second = tiles[index_set[1]]
97 third = tiles[index_set[2]]
98 if first >= 1 and second >= 1 and third >= 1:
99 return False
101 # if we have 2 or more non-central doras
102 # we don't want to go for tanyao
103 if self.dora_count_not_central >= 2:
104 return False
106 # if we have less than two central doras
107 # let's not consider open tanyao
108 if self.dora_count_central < 2:
109 return False
111 # if we have only two central doras let's
112 # wait for 5th turn before opening our hand
113 if self.dora_count_central == 2 and self.player.round_step < 5:
114 return False
116 return True
118 def determine_what_to_discard(self, discard_options, hand, open_melds):
119 is_open_hand = self.player.is_open_hand
121 # our hand is closed, we don't need to discard terminal tiles here
122 if not is_open_hand:
123 return discard_options
125 first_option = sorted(discard_options, key=lambda x: x.shanten)[0]
126 shanten = first_option.shanten
128 if shanten > 1:
129 return super(TanyaoStrategy, self).determine_what_to_discard(discard_options, hand, open_melds)
131 results = []
132 not_suitable_tiles = []
133 for item in discard_options:
134 if not self.is_tile_suitable(item.tile_to_discard_136):
135 item.had_to_be_discarded = True
136 not_suitable_tiles.append(item)
137 continue
139 # there is no sense to wait 1-4 if we have open hand
140 # but let's only avoid atodzuke tiles in tempai, the rest will be dealt with in
141 # generic logic
142 if item.shanten == 0:
143 all_waiting_are_fine = all(
144 [(self.is_tile_suitable(x * 4) or item.wait_to_ukeire[x] == 0) for x in item.waiting]
145 )
146 if all_waiting_are_fine:
147 results.append(item)
149 if not_suitable_tiles:
150 return not_suitable_tiles
152 # we don't have a choice
153 # we had to have on bad wait
154 if not results:
155 return discard_options
157 return results
159 def is_tile_suitable(self, tile):
160 """
161 We can use only simples tiles (2-8) in any suit
162 :param tile: 136 tiles format
163 :return: True
164 """
165 tile //= 4
166 return tile not in self.not_suitable_tiles
168 def validate_meld(self, chosen_meld_dict):
169 # if we have already opened our hand, let's go by default riles
170 if self.player.is_open_hand:
171 return True
173 # choose if base method requires us to keep hand closed
174 if not super(TanyaoStrategy, self).validate_meld(chosen_meld_dict):
175 return False
177 # otherwise let's not open hand if that does not improve our ukeire
178 closed_tiles_34 = TilesConverter.to_34_array(self.player.closed_hand)
179 waiting, shanten = self.player.ai.hand_builder.calculate_waits(
180 closed_tiles_34, closed_tiles_34, use_chiitoitsu=False
181 )
182 wait_to_ukeire = dict(
183 zip(waiting, [self.player.ai.hand_builder.count_tiles([x], closed_tiles_34) for x in waiting])
184 )
185 old_ukeire = sum(wait_to_ukeire.values())
186 selected_tile = chosen_meld_dict["discard_tile"]
188 logger_context = {
189 "hand": tiles_to_string(self.player.closed_hand),
190 "meld": chosen_meld_dict,
191 "old_shanten": shanten,
192 "old_ukeire": old_ukeire,
193 "new_shanten": selected_tile.shanten,
194 "new_ukeire": selected_tile.ukeire,
195 }
197 if selected_tile.shanten > shanten:
198 self.player.logger.debug(
199 log.MELD_DEBUG, "Opening into tanyao increases number of shanten, let's not do that", logger_context
200 )
201 return False
203 if selected_tile.shanten == shanten:
204 if old_ukeire >= selected_tile.ukeire:
205 self.player.logger.debug(
206 log.MELD_DEBUG,
207 "Opening into tanyao keeps same number of shanten and does not improve ukeire, let's not do that",
208 logger_context,
209 )
210 return False
212 if old_ukeire != 0:
213 improvement_percent = ((selected_tile.ukeire - old_ukeire) / old_ukeire) * 100
214 else:
215 improvement_percent = selected_tile.ukeire * 100
217 if improvement_percent < 30:
218 self.player.logger.debug(
219 log.MELD_DEBUG,
220 "Opening into tanyao keeps same number of shanten and ukeire improvement is low, don't open",
221 logger_context,
222 )
223 return False
225 self.player.logger.debug(
226 log.MELD_DEBUG,
227 "Opening into tanyao keeps same number of shanten and ukeire improvement is good, let's call meld",
228 logger_context,
229 )
230 return True
232 self.player.logger.debug(
233 log.MELD_DEBUG, "Opening into tanyao improves number of shanten, let's call meld", logger_context
234 )
235 return True