Coverage for project/game/ai/kan.py : 96%

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 Optional
3import utils.decisions_constants as log
4from mahjong.tile import TilesConverter
5from mahjong.utils import is_pon
6from utils.decisions_logger import MeldPrint
9class Kan:
10 def __init__(self, player):
11 self.player = player
13 # TODO for better readability need to separate it on three methods:
14 # should_call_closed_kan, should_call_open_kan, should_call_shouminkan
15 def should_call_kan(self, tile_136: int, open_kan: bool, from_riichi=False) -> Optional[str]:
16 """
17 Method will decide should we call a kan, or upgrade pon to kan
18 :return: kan type
19 """
21 # we can't call kan on the latest tile
22 if self.player.table.count_of_remaining_tiles <= 1:
23 return None
25 if self.player.config.FEATURE_DEFENCE_ENABLED:
26 threats = self.player.ai.defence.get_threatening_players()
27 else:
28 threats = []
30 if open_kan:
31 # we don't want to start open our hand from called kan
32 if not self.player.is_open_hand:
33 return None
35 # there is no sense to call open kan when we are not in tempai
36 if not self.player.in_tempai:
37 return None
39 # we have a bad wait, rinshan chance is low
40 if len(self.player.ai.waiting) < 2 or self.player.ai.ukeire < 5:
41 return None
43 # there are threats, open kan is probably a bad idea
44 if threats:
45 return None
47 tile_34 = tile_136 // 4
48 tiles_34 = TilesConverter.to_34_array(self.player.tiles)
50 # save original hand state
51 original_tiles = self.player.tiles[:]
53 new_shanten = 0
54 previous_shanten = 0
55 new_waits_count = 0
56 previous_waits_count = 0
58 # let's check can we upgrade opened pon to the kan
59 pon_melds = [x for x in self.player.meld_34_tiles if is_pon(x)]
60 has_shouminkan_candidate = False
61 for meld in pon_melds:
62 # tile is equal to our already opened pon
63 if tile_34 in meld:
64 has_shouminkan_candidate = True
66 closed_hand_34 = TilesConverter.to_34_array(self.player.closed_hand)
67 previous_shanten, previous_waits_count = self._calculate_shanten_for_kan()
68 self.player.tiles = original_tiles[:]
70 closed_hand_34[tile_34] -= 1
71 tiles_34[tile_34] -= 1
72 new_waiting, new_shanten = self.player.ai.hand_builder.calculate_waits(
73 closed_hand_34, tiles_34, use_chiitoitsu=False
74 )
75 new_waits_count = self.player.ai.hand_builder.count_tiles(new_waiting, closed_hand_34)
77 closed_hand_34 = TilesConverter.to_34_array(self.player.closed_hand)
78 if not open_kan and not has_shouminkan_candidate and closed_hand_34[tile_34] != 4:
79 return None
81 if open_kan and closed_hand_34[tile_34] != 3:
82 return None
84 closed_hand_34 = TilesConverter.to_34_array(self.player.closed_hand)
85 tiles_34 = TilesConverter.to_34_array(self.player.tiles)
87 if not has_shouminkan_candidate:
88 if open_kan:
89 # this 4 tiles can only be used in kan, no other options
90 previous_waiting, previous_shanten = self.player.ai.hand_builder.calculate_waits(
91 closed_hand_34, tiles_34, use_chiitoitsu=False
92 )
93 previous_waits_count = self.player.ai.hand_builder.count_tiles(previous_waiting, closed_hand_34)
94 elif from_riichi:
95 # hand did not change since we last recalculated it, and the only thing we can do is to call kan
96 previous_waits_count = self.player.ai.ukeire
97 else:
98 previous_shanten, previous_waits_count = self._calculate_shanten_for_kan()
99 self.player.tiles = original_tiles[:]
101 closed_hand_34[tile_34] = 0
102 new_waiting, new_shanten = self.player.ai.hand_builder.calculate_waits(
103 closed_hand_34, tiles_34, use_chiitoitsu=False
104 )
106 closed_hand_34[tile_34] = 4
107 new_waits_count = self.player.ai.hand_builder.count_tiles(new_waiting, closed_hand_34)
109 # it is possible that we don't have results here
110 # when we are in agari state (but without yaku)
111 if previous_shanten is None:
112 return None
114 # it is not possible to reduce number of shanten by calling a kan
115 assert new_shanten >= previous_shanten
117 # if shanten number is the same, we should only call kan if ukeire didn't become worse
118 if new_shanten == previous_shanten:
119 # we cannot improve ukeire by calling kan (not considering the tile we drew from the dead wall)
120 assert new_waits_count <= previous_waits_count
122 if new_waits_count == previous_waits_count:
123 kan_type = has_shouminkan_candidate and MeldPrint.SHOUMINKAN or MeldPrint.KAN
124 if kan_type == MeldPrint.SHOUMINKAN:
125 if threats:
126 # there are threats and we are not even in tempai - let's not do shouminkan
127 if not self.player.in_tempai:
128 return None
130 # there are threats and our tempai is weak, let's not do shouminkan
131 if len(self.player.ai.waiting) < 2 or self.player.ai.ukeire < 3:
132 return None
133 else:
134 # no threats, but too many shanten, let's not do shouminkan
135 if new_shanten > 2:
136 return None
138 # no threats, and ryanshanten, but ukeire is meh, let's not do shouminkan
139 if new_shanten == 2:
140 if self.player.ai.ukeire < 16:
141 return None
143 self.player.logger.debug(log.KAN_DEBUG, f"Open kan type='{kan_type}'")
144 return kan_type
146 return None
148 def _calculate_shanten_for_kan(self):
149 previous_results, previous_shanten = self.player.ai.hand_builder.find_discard_options()
151 previous_results = [x for x in previous_results if x.shanten == previous_shanten]
153 # it is possible that we don't have results here
154 # when we are in agari state (but without yaku)
155 if not previous_results:
156 return None, None
158 previous_waits_cnt = sorted(previous_results, key=lambda x: -x.ukeire)[0].ukeire
160 return previous_shanten, previous_waits_cnt