Coverage for project/game/ai/riichi.py : 68%

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
1from typing import List
3from game.ai.defence.enemy_analyzer import EnemyAnalyzer
4from game.ai.discard import DiscardOption
5from game.ai.placement import Placement
6from mahjong.tile import TilesConverter
7from mahjong.utils import is_chi, is_honor, is_pair, is_terminal, plus_dora, simplify
10class Riichi:
11 def __init__(self, player):
12 self.player = player
14 def should_call_riichi(self, discard_option: DiscardOption, threats: List[EnemyAnalyzer]):
15 assert discard_option.shanten == 0
16 assert not self.player.is_open_hand
18 hand_builder = self.player.ai.hand_builder
20 waiting_34 = discard_option.waiting
21 # empty waiting can be found in some cases
22 if not waiting_34:
23 return False
25 # save original hand state
26 # we will restore it after we have finished our routines
27 tiles_original, discards_original = hand_builder.emulate_discard(discard_option)
29 count_tiles = hand_builder.count_tiles(waiting_34, TilesConverter.to_34_array(self.player.closed_hand))
30 if count_tiles == 0:
31 # don't call karaten riichi
32 hand_builder.restore_after_emulate_discard(tiles_original, discards_original)
33 return False
35 # we decide if we should riichi or not before making a discard, hence we check for round step == 0
36 first_discard = self.player.round_step == 0
37 if first_discard and not self.player.table.meld_was_called:
38 hand_builder.restore_after_emulate_discard(tiles_original, discards_original)
39 # it is daburi!
40 return True
42 # regular path
43 if len(waiting_34) == 1:
44 should_riichi = self._should_call_riichi_one_sided(waiting_34, threats)
45 else:
46 should_riichi = self._should_call_riichi_many_sided(waiting_34, threats)
48 hand_builder.restore_after_emulate_discard(tiles_original, discards_original)
49 return should_riichi
51 def _should_call_riichi_one_sided(self, waiting_34: List[int], threats: List[EnemyAnalyzer]):
52 count_tiles = self.player.ai.hand_builder.count_tiles(
53 waiting_34, TilesConverter.to_34_array(self.player.closed_hand)
54 )
55 waiting_34 = waiting_34[0]
56 hand_value = self.player.ai.estimate_hand_value_or_get_from_cache(waiting_34, call_riichi=False)
57 hand_value_with_riichi = self.player.ai.estimate_hand_value_or_get_from_cache(waiting_34, call_riichi=True)
59 must_riichi = self.player.ai.placement.must_riichi(
60 has_yaku=(hand_value.yaku is not None and hand_value.cost is not None),
61 num_waits=count_tiles,
62 cost_with_riichi=hand_value_with_riichi.cost["main"],
63 cost_with_damaten=(hand_value.cost and hand_value.cost["main"] or 0),
64 )
65 if must_riichi == Placement.MUST_RIICHI:
66 return True
67 elif must_riichi == Placement.MUST_DAMATEN:
68 return False
70 tiles = self.player.closed_hand[:]
71 closed_melds = [x for x in self.player.melds if not x.opened]
72 for meld in closed_melds:
73 tiles.extend(meld.tiles[:3])
75 results, tiles_34 = self.player.ai.hand_builder.divide_hand(tiles, waiting_34)
76 result = results[0]
78 closed_tiles_34 = TilesConverter.to_34_array(self.player.closed_hand)
80 have_suji, have_kabe = self.player.ai.hand_builder.check_suji_and_kabe(closed_tiles_34, waiting_34)
82 # what if we have yaku
83 if hand_value.yaku is not None and hand_value.cost is not None:
84 min_cost = hand_value.cost["main"]
85 min_cost_with_riichi = hand_value_with_riichi and hand_value_with_riichi.cost["main"] or 0
87 # tanki honor is a good wait, let's damaten only if hand is already expensive
88 if is_honor(waiting_34):
89 if self.player.is_dealer and min_cost < 12000:
90 return True
92 if not self.player.is_dealer and min_cost < 8000:
93 return True
95 return False
97 is_chiitoitsu = len([x for x in result if is_pair(x)]) == 7
98 simplified_waiting = simplify(waiting_34)
100 for hand_set in result:
101 if waiting_34 not in hand_set:
102 continue
104 # tanki wait but not chiitoitsu
105 if is_pair(hand_set) and not is_chiitoitsu:
106 # let's not riichi tanki 4, 5, 6
107 if 3 <= simplified_waiting <= 5:
108 return False
110 # don't riichi tanki wait on 1, 2, 3, 7, 8, 9 if it's only 1 tile
111 if count_tiles == 1:
112 return False
114 # don't riichi 2378 tanki if hand has good value
115 if simplified_waiting != 0 and simplified_waiting != 8:
116 if self.player.is_dealer and min_cost >= 7700:
117 return False
119 if not self.player.is_dealer and min_cost >= 5200:
120 return False
122 # only riichi if we have suji-trap or there is kabe
123 if not have_suji and not have_kabe:
124 return False
126 # let's not push these bad wait against threats
127 if threats:
128 return False
130 return True
132 # tanki wait with chiitoitsu
133 if is_pair(hand_set) and is_chiitoitsu:
134 # chiitoitsu on last suit tile is not the best
135 if count_tiles == 1:
136 return False
138 # early riichi on 19 tanki is good
139 if (simplified_waiting == 0 or simplified_waiting == 8) and self.player.round_step < 7:
140 return True
142 # riichi on 19 tanki is good later too if we have 3 tiles to wait for
143 if (
144 (simplified_waiting == 0 or simplified_waiting == 8)
145 and self.player.round_step < 12
146 and count_tiles == 3
147 ):
148 return True
150 # riichi on 28 tanki is good if we have 3 tiles to wait for
151 if (
152 (simplified_waiting == 1 or simplified_waiting == 7)
153 and self.player.round_step < 12
154 and count_tiles == 3
155 ):
156 return True
158 # otherwise only riichi if we have suji-trab or there is kabe
159 if not have_suji and not have_kabe:
160 return False
162 # let's not push these bad wait against threats
163 if threats:
164 return False
166 return True
168 # 1-sided wait means kanchan or penchan
169 if is_chi(hand_set):
170 # if we only have 1 tile to wait for, let's damaten
171 if count_tiles == 1:
172 return False
174 # for dealer it is always riichi
175 if self.player.is_dealer:
176 return True
177 # let's not push cheap hands against threats
178 elif threats and min_cost_with_riichi < 2600:
179 return False
181 if 3 <= simplified_waiting <= 5:
182 if min_cost_with_riichi >= 2600:
183 return True
185 # for not dealer let's not riichi cheap kanchan on 4, 5, 6
186 return False
188 # if we have 2 tiles to wait for and hand cost is good without riichi,
189 # let's damaten
190 if count_tiles == 2:
191 if self.player.is_dealer and min_cost >= 7700:
192 return False
194 if not self.player.is_dealer and min_cost >= 5200:
195 return False
197 # if we have more than two tiles to wait for and we have kabe or suji - insta riichi
198 if count_tiles > 2 and (have_suji or have_kabe):
199 return True
201 # 2 and 8 are good waits but not in every condition
202 if simplified_waiting == 1 or simplified_waiting == 7:
203 if self.player.round_step < 7:
204 if self.player.is_dealer and min_cost < 18000:
205 return True
207 if not self.player.is_dealer and min_cost < 8000:
208 return True
210 if self.player.round_step < 12:
211 if self.player.is_dealer and min_cost < 12000:
212 return True
214 if not self.player.is_dealer and min_cost < 5200:
215 return True
217 if self.player.round_step < 15:
218 if self.player.is_dealer and 2000 < min_cost < 7700:
219 return True
221 # 3 and 7 are ok waits sometimes too
222 if simplified_waiting == 2 or simplified_waiting == 6:
223 if self.player.round_step < 7:
224 if self.player.is_dealer and min_cost < 12000:
225 return True
227 if not self.player.is_dealer and min_cost < 5200:
228 return True
230 if self.player.round_step < 12:
231 if self.player.is_dealer and min_cost < 7700:
232 return True
234 if not self.player.is_dealer and min_cost < 5200:
235 return True
237 if self.player.round_step < 15:
238 if self.player.is_dealer and 2000 < min_cost < 7700:
239 return True
241 # otherwise only riichi if we have suji-trap or there is kabe
242 if not have_suji and not have_kabe:
243 return False
245 return True
247 # what if we don't have yaku
248 # our tanki wait is good, let's riichi
249 if is_honor(waiting_34):
250 return True
252 if count_tiles > 1:
253 # terminal tanki is ok, too, just should be more than one tile left
254 if is_terminal(waiting_34):
255 return True
257 # whatever dora wait is ok, too, just should be more than one tile left
258 if plus_dora(waiting_34 * 4, self.player.table.dora_indicators, add_aka_dora=False) > 0:
259 return True
261 simplified_waiting = simplify(waiting_34)
263 for hand_set in result:
264 if waiting_34 not in hand_set:
265 continue
267 if is_pair(hand_set):
268 # let's not riichi tanki wait without suji-trap or kabe
269 if not have_suji and not have_kabe:
270 return False
272 # let's not riichi tanki on last suit tile if it's early
273 if count_tiles == 1 and self.player.round_step < 6:
274 return False
276 # let's not riichi tanki 4, 5, 6 if it's early
277 if 3 <= simplified_waiting <= 5 and self.player.round_step < 6:
278 return False
280 # 1-sided wait means kanchan or penchan
281 # let's only riichi this bad wait if
282 # it has all 4 tiles available or it
283 # it's not too early
284 # and there are no threats
285 if not threats and is_chi(hand_set) and 4 <= simplified_waiting <= 6:
286 return count_tiles == 4 or self.player.round_step >= 6
288 return True
290 def _should_call_riichi_many_sided(self, waiting_34: List[int], threats: List[EnemyAnalyzer]):
291 count_tiles = self.player.ai.hand_builder.count_tiles(
292 waiting_34, TilesConverter.to_34_array(self.player.closed_hand)
293 )
294 hand_costs = []
295 hand_costs_with_riichi = []
296 waits_with_yaku = 0
297 for wait in waiting_34:
298 hand_value = self.player.ai.estimate_hand_value_or_get_from_cache(wait, call_riichi=False)
299 if hand_value.error is None:
300 hand_costs.append(hand_value.cost["main"])
301 if hand_value.yaku is not None and hand_value.cost is not None:
302 waits_with_yaku += 1
304 hand_value_with_riichi = self.player.ai.estimate_hand_value_or_get_from_cache(wait, call_riichi=True)
305 if hand_value_with_riichi.error is None:
306 hand_costs_with_riichi.append(hand_value_with_riichi.cost["main"])
308 min_cost = hand_costs and min(hand_costs) or 0
309 min_cost_with_riichi = hand_costs_with_riichi and min(hand_costs_with_riichi) or 0
311 must_riichi = self.player.ai.placement.must_riichi(
312 has_yaku=waits_with_yaku == len(waiting_34),
313 num_waits=count_tiles,
314 cost_with_riichi=min_cost_with_riichi,
315 cost_with_damaten=min_cost,
316 )
317 if must_riichi == Placement.MUST_RIICHI:
318 return True
319 elif must_riichi == Placement.MUST_DAMATEN:
320 return False
322 is_dealer_threat = any([x.enemy.is_dealer for x in threats])
324 # we don't want to push cheap hand against dealer
325 if is_dealer_threat and min_cost_with_riichi <= 1300:
326 return False
328 # if we have yaku on every wait
329 if waits_with_yaku == len(waiting_34):
330 # let's not riichi this bad wait
331 if count_tiles <= 2:
332 return False
334 # chasing riichi on late steps of the game is not profitable
335 if threats and self.player.round_step >= 9:
336 return False
338 # if wait is slightly better, we will riichi only a cheap hand
339 if count_tiles <= 4:
340 if self.player.is_dealer and min_cost >= 7700:
341 return False
343 if not self.player.is_dealer and min_cost >= 5200:
344 return False
346 return True
348 # wait is even better, but still don't call riichi on damaten mangan
349 if count_tiles <= 6:
350 # if it's early riichi more readily
351 if self.player.round_step > 6:
352 if self.player.is_dealer and min_cost >= 11600:
353 return False
355 if not self.player.is_dealer and min_cost >= 7700:
356 return False
357 else:
358 if self.player.is_dealer and min_cost >= 18000:
359 return False
361 if not self.player.is_dealer and min_cost >= 12000:
362 return False
364 return True
366 # if wait is good we only damaten haneman
367 if self.player.is_dealer and min_cost >= 18000:
368 return False
370 if not self.player.is_dealer and min_cost >= 12000:
371 return False
373 return True
375 # if we don't have yaku on every wait and it's two-sided or more, we call riichi
376 return True