Coverage for project/game/ai/strategies_v2/yakuhai.py : 84%

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 EAST, SOUTH
4from mahjong.tile import TilesConverter
5from utils.decisions_logger import MeldPrint
8class YakuhaiStrategy(BaseStrategy):
9 valued_pairs = None
10 has_valued_anko = None
12 def __init__(self, strategy_type, player):
13 super().__init__(strategy_type, player)
15 self.valued_pairs = []
16 self.valued_anko = []
17 self.has_valued_anko = False
18 self.last_chance_calls = []
20 def get_open_hand_han(self):
21 # kinda rough estimation
22 return len(self.valued_anko) + len(self.valued_pairs)
24 def should_activate_strategy(self, tiles_136, meld_tile=None):
25 """
26 We can go for yakuhai strategy if we have at least one yakuhai pair in the hand
27 :return: boolean
28 """
29 result = super(YakuhaiStrategy, self).should_activate_strategy(tiles_136)
30 if not result:
31 return False
33 tiles_34 = TilesConverter.to_34_array(tiles_136)
34 player_hand_tiles_34 = TilesConverter.to_34_array(self.player.tiles)
35 player_closed_hand_tiles_34 = TilesConverter.to_34_array(self.player.closed_hand)
36 self.valued_pairs = [x for x in self.player.valued_honors if player_hand_tiles_34[x] == 2]
38 is_double_east_wind = len([x for x in self.valued_pairs if x == EAST]) == 2
39 is_double_south_wind = len([x for x in self.valued_pairs if x == SOUTH]) == 2
41 self.valued_pairs = list(set(self.valued_pairs))
42 self.valued_anko = [x for x in self.player.valued_honors if player_hand_tiles_34[x] >= 3]
43 self.has_valued_anko = len(self.valued_anko) >= 1
45 opportunity_to_meld_yakuhai = False
47 for x in range(0, 34):
48 if x in self.valued_pairs and tiles_34[x] - player_hand_tiles_34[x] == 1:
49 opportunity_to_meld_yakuhai = True
51 has_valued_pair = False
53 for pair in self.valued_pairs:
54 # we have valued pair in the hand and there are enough tiles
55 # in the wall
56 if (
57 opportunity_to_meld_yakuhai
58 or self.player.number_of_revealed_tiles(pair, player_closed_hand_tiles_34) < 4
59 ):
60 has_valued_pair = True
61 break
63 # we don't have valuable pair or pon to open our hand
64 if not has_valued_pair and not self.has_valued_anko:
65 return False
67 # let's always open double east
68 if is_double_east_wind:
69 return True
71 # let's open double south if we have a dora in the hand
72 # or we have other valuable pairs
73 if is_double_south_wind and (self.dora_count_total >= 1 or len(self.valued_pairs) >= 2):
74 return True
76 # there are 2+ valuable pairs let's open hand
77 if len(self.valued_pairs) >= 2:
78 # if we are dealer let's open hand
79 if self.player.is_dealer:
80 return True
82 # if we have 1+ dora in the hand it is fine to open yakuhai
83 if self.dora_count_total >= 1:
84 return True
86 # If we have 2+ dora in the hand let's open hand
87 if self.dora_count_total >= 2:
88 for x in range(0, 34):
89 # we have other pair in the hand
90 # so we can open hand for atodzuke
91 if player_hand_tiles_34[x] >= 2 and x not in self.valued_pairs:
92 self.go_for_atodzuke = True
93 return True
95 # If we have 1+ dora in the hand and there is 5+ round step let's open hand
96 if self.dora_count_total >= 1 and self.player.round_step > 5:
97 return True
99 for pair in self.valued_pairs:
100 # last chance to get that yakuhai, let's go for it
101 if (
102 opportunity_to_meld_yakuhai
103 and self.player.number_of_revealed_tiles(pair, player_closed_hand_tiles_34) == 3
104 and self.player.ai.shanten >= 1
105 ):
107 if pair not in self.last_chance_calls:
108 self.last_chance_calls.append(pair)
110 return True
112 # finally check if we need a cheap hand in oorasu - so don't skip first yakujai
113 if self.player.ai.placement.is_oorasu and opportunity_to_meld_yakuhai:
114 placement = self.player.ai.placement.get_current_placement()
115 logger_context = {
116 "placement": placement,
117 }
119 if placement and placement["place"] == 4:
120 enough_cost = self.player.ai.placement.get_minimal_cost_needed_considering_west()
121 simple_han_scale = [0, 1000, 2000, 3900, 7700, 8000, 12000, 12000]
122 num_han = self.get_open_hand_han() + self.dora_count_total
123 if num_han >= len(simple_han_scale):
124 # why are we even here?
125 self.player.logger.debug(
126 log.PLACEMENT_MELD_DECISION,
127 "We are 4th in oorasu and have expensive hand, call meld",
128 logger_context,
129 )
130 return True
132 # be pessimistic and don't count on direct ron
133 hand_cost = simple_han_scale[num_han]
134 if hand_cost >= enough_cost:
135 self.player.logger.debug(
136 log.PLACEMENT_MELD_DECISION,
137 "We are 4th in oorasu and our hand can give us 3rd with meld, take it",
138 logger_context,
139 )
140 return True
142 if (
143 placement
144 and placement["place"] == 3
145 and placement["diff_with_4th"] < self.player.ai.placement.comfortable_diff
146 ):
147 self.player.logger.debug(
148 log.PLACEMENT_MELD_DECISION, "We are 3rd in oorasu and want to secure it, take meld", logger_context
149 )
150 return True
152 return False
154 def determine_what_to_discard(self, discard_options, hand, open_melds):
155 is_open_hand = self.player.is_open_hand
157 tiles_34 = TilesConverter.to_34_array(hand)
159 valued_pairs = [x for x in self.player.valued_honors if tiles_34[x] == 2]
161 # closed pon sets
162 valued_pons = [x for x in self.player.valued_honors if tiles_34[x] == 3]
163 # open pon sets
164 valued_pons += [
165 x for x in open_melds if x.type == MeldPrint.PON and x.tiles[0] // 4 in self.player.valued_honors
166 ]
168 acceptable_options = []
169 for item in discard_options:
170 if is_open_hand:
171 if len(valued_pons) == 0:
172 # don't destroy our only yakuhai pair
173 if len(valued_pairs) == 1 and item.tile_to_discard_34 in valued_pairs:
174 continue
175 elif len(valued_pons) == 1:
176 # don't destroy our only yakuhai pon
177 if item.tile_to_discard_34 in valued_pons:
178 continue
180 acceptable_options.append(item)
182 # we don't have a choice
183 if not acceptable_options:
184 return discard_options
186 preferred_options = []
187 for item in acceptable_options:
188 # ignore wait without yakuhai yaku if possible
189 if is_open_hand and len(valued_pons) == 0 and len(valued_pairs) == 1:
190 if item.shanten == 0 and valued_pairs[0] not in item.waiting:
191 continue
193 preferred_options.append(item)
195 if not preferred_options:
196 return acceptable_options
198 return preferred_options
200 def is_tile_suitable(self, tile):
201 """
202 For yakuhai we don't have any limits
203 :param tile: 136 tiles format
204 :return: True
205 """
206 return True
208 def meld_had_to_be_called(self, tile):
209 tile //= 4
210 tiles_34 = TilesConverter.to_34_array(self.player.tiles)
211 valued_pairs = [x for x in self.player.valued_honors if tiles_34[x] == 2]
213 # for big shanten number we don't need to check already opened pon set,
214 # because it will improve our hand anyway
215 if self.player.ai.shanten < 2:
216 for meld in self.player.melds:
217 # we have already opened yakuhai pon
218 # so we don't need to open hand without shanten improvement
219 if self._is_yakuhai_pon(meld):
220 return False
222 # if we don't have any yakuhai pon and this is our last chance, we must call this tile
223 if tile in self.last_chance_calls:
224 return True
226 # in all other cases for closed hand we don't need to open hand with special conditions
227 if not self.player.is_open_hand:
228 return False
230 # we have opened the hand already and don't yet have yakuhai pon
231 # so we now must get it
232 for valued_pair in valued_pairs:
233 if valued_pair == tile:
234 return True
236 return False
238 def try_to_call_meld(self, tile, is_kamicha_discard, tiles_136):
239 if self.has_valued_anko:
240 return super(YakuhaiStrategy, self).try_to_call_meld(tile, is_kamicha_discard, tiles_136)
242 tile_34 = tile // 4
243 # we will open hand for atodzuke only in the special cases
244 if not self.player.is_open_hand and tile_34 not in self.valued_pairs:
245 if self.go_for_atodzuke:
246 return super(YakuhaiStrategy, self).try_to_call_meld(tile, is_kamicha_discard, tiles_136)
248 return None, None
250 return super(YakuhaiStrategy, self).try_to_call_meld(tile, is_kamicha_discard, tiles_136)
252 def validate_meld(self, chosen_meld_dict):
253 # choose if base method requires us to keep hand closed
254 if not super(YakuhaiStrategy, self).validate_meld(chosen_meld_dict):
255 return False
257 closed_tiles_34 = TilesConverter.to_34_array(self.player.closed_hand)
258 pairs_before_meld = len([x for x in closed_tiles_34 if x == 2])
259 valued_pairs_before_meld = len([x for x in self.player.valued_honors if closed_tiles_34[x] == 2])
260 # we don't have valued pairs to keep
261 if not valued_pairs_before_meld:
262 return True
264 # it is fine to destroy pairs if we have plenty of them
265 if pairs_before_meld > 2:
266 return True
268 closed_tiles_34 = TilesConverter.to_34_array(chosen_meld_dict["closed_hand_tiles_after_meld"])
269 pairs_after_meld = len([x for x in closed_tiles_34 if x == 2])
270 valued_pairs_after_meld = len([x for x in self.player.valued_honors if closed_tiles_34[x] == 2])
272 # condition to prevent calling from form 344m 77z on 4m
273 if pairs_after_meld < pairs_before_meld and valued_pairs_before_meld == valued_pairs_after_meld:
274 self.player.logger.debug(
275 log.MELD_DEBUG,
276 "Yakuhai: let's skip meld that destroying our pair",
277 {
278 "pairs_after_meld": pairs_after_meld,
279 "pairs_before_meld": pairs_before_meld,
280 },
281 )
282 return False
284 return True
286 def _is_yakuhai_pon(self, meld):
287 return meld.type == MeldPrint.PON and meld.tiles[0] // 4 in self.player.valued_honors