Hide keyboard shortcuts

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 

2 

3import utils.decisions_constants as log 

4from mahjong.tile import TilesConverter 

5from mahjong.utils import is_pon 

6from utils.decisions_logger import MeldPrint 

7 

8 

9class Kan: 

10 def __init__(self, player): 

11 self.player = player 

12 

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 """ 

20 

21 # we can't call kan on the latest tile 

22 if self.player.table.count_of_remaining_tiles <= 1: 

23 return None 

24 

25 if self.player.config.FEATURE_DEFENCE_ENABLED: 

26 threats = self.player.ai.defence.get_threatening_players() 

27 else: 

28 threats = [] 

29 

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 

34 

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 

38 

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 

42 

43 # there are threats, open kan is probably a bad idea 

44 if threats: 

45 return None 

46 

47 tile_34 = tile_136 // 4 

48 tiles_34 = TilesConverter.to_34_array(self.player.tiles) 

49 

50 # save original hand state 

51 original_tiles = self.player.tiles[:] 

52 

53 new_shanten = 0 

54 previous_shanten = 0 

55 new_waits_count = 0 

56 previous_waits_count = 0 

57 

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 

65 

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[:] 

69 

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) 

76 

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 

80 

81 if open_kan and closed_hand_34[tile_34] != 3: 

82 return None 

83 

84 closed_hand_34 = TilesConverter.to_34_array(self.player.closed_hand) 

85 tiles_34 = TilesConverter.to_34_array(self.player.tiles) 

86 

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[:] 

100 

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 ) 

105 

106 closed_hand_34[tile_34] = 4 

107 new_waits_count = self.player.ai.hand_builder.count_tiles(new_waiting, closed_hand_34) 

108 

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 

113 

114 # it is not possible to reduce number of shanten by calling a kan 

115 assert new_shanten >= previous_shanten 

116 

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 

121 

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 

129 

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 

137 

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 

142 

143 self.player.logger.debug(log.KAN_DEBUG, f"Open kan type='{kan_type}'") 

144 return kan_type 

145 

146 return None 

147 

148 def _calculate_shanten_for_kan(self): 

149 previous_results, previous_shanten = self.player.ai.hand_builder.find_discard_options() 

150 

151 previous_results = [x for x in previous_results if x.shanten == previous_shanten] 

152 

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 

157 

158 previous_waits_cnt = sorted(previous_results, key=lambda x: -x.ukeire)[0].ukeire 

159 

160 return previous_shanten, previous_waits_cnt