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

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 

7 

8 

9class TanyaoStrategy(BaseStrategy): 

10 min_shanten = 3 

11 not_suitable_tiles = TERMINAL_INDICES + HONOR_INDICES 

12 

13 def get_open_hand_han(self): 

14 return 1 

15 

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

22 

23 result = super(TanyaoStrategy, self).should_activate_strategy(tiles_136) 

24 if not result: 

25 return False 

26 

27 tiles = TilesConverter.to_34_array(self.player.tiles) 

28 

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 ] 

33 

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 

43 

44 if x in self.not_suitable_tiles and tile == 3: 

45 count_of_terminal_pon_sets += 1 

46 

47 if x in self.not_suitable_tiles and tile == 2: 

48 count_of_terminal_pairs += 1 

49 

50 if x in self.player.valued_honors: 

51 count_of_valued_pairs += 1 

52 

53 if x in self.not_suitable_tiles: 

54 count_of_not_suitable_tiles += tile 

55 

56 if x in self.not_suitable_tiles and x not in isolated_tiles: 

57 count_of_not_suitable_not_isolated_tiles += tile 

58 

59 # we have too much terminals and honors 

60 if count_of_not_suitable_tiles >= 5: 

61 return False 

62 

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 

67 

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 

72 

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 

77 

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 

82 

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 

88 

89 # TODO: don't open from good 1-shanten into tanyao 1-shaten with same ukeire or worse 

90 

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

93 

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 

100 

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 

105 

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 

110 

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 

115 

116 return True 

117 

118 def determine_what_to_discard(self, discard_options, hand, open_melds): 

119 is_open_hand = self.player.is_open_hand 

120 

121 # our hand is closed, we don't need to discard terminal tiles here 

122 if not is_open_hand: 

123 return discard_options 

124 

125 first_option = sorted(discard_options, key=lambda x: x.shanten)[0] 

126 shanten = first_option.shanten 

127 

128 if shanten > 1: 

129 return super(TanyaoStrategy, self).determine_what_to_discard(discard_options, hand, open_melds) 

130 

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 

138 

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) 

148 

149 if not_suitable_tiles: 

150 return not_suitable_tiles 

151 

152 # we don't have a choice 

153 # we had to have on bad wait 

154 if not results: 

155 return discard_options 

156 

157 return results 

158 

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 

167 

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 

172 

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 

176 

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

187 

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 } 

196 

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 

202 

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 

211 

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 

216 

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 

224 

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 

231 

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