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 EAST, SOUTH 

4from mahjong.tile import TilesConverter 

5from utils.decisions_logger import MeldPrint 

6 

7 

8class YakuhaiStrategy(BaseStrategy): 

9 valued_pairs = None 

10 has_valued_anko = None 

11 

12 def __init__(self, strategy_type, player): 

13 super().__init__(strategy_type, player) 

14 

15 self.valued_pairs = [] 

16 self.valued_anko = [] 

17 self.has_valued_anko = False 

18 self.last_chance_calls = [] 

19 

20 def get_open_hand_han(self): 

21 # kinda rough estimation 

22 return len(self.valued_anko) + len(self.valued_pairs) 

23 

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 

32 

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] 

37 

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 

40 

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 

44 

45 opportunity_to_meld_yakuhai = False 

46 

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 

50 

51 has_valued_pair = False 

52 

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 

62 

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 

66 

67 # let's always open double east 

68 if is_double_east_wind: 

69 return True 

70 

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 

75 

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 

81 

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 

85 

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 

94 

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 

98 

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 ): 

106 

107 if pair not in self.last_chance_calls: 

108 self.last_chance_calls.append(pair) 

109 

110 return True 

111 

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 } 

118 

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 

131 

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 

141 

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 

151 

152 return False 

153 

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

155 is_open_hand = self.player.is_open_hand 

156 

157 tiles_34 = TilesConverter.to_34_array(hand) 

158 

159 valued_pairs = [x for x in self.player.valued_honors if tiles_34[x] == 2] 

160 

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 ] 

167 

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 

179 

180 acceptable_options.append(item) 

181 

182 # we don't have a choice 

183 if not acceptable_options: 

184 return discard_options 

185 

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 

192 

193 preferred_options.append(item) 

194 

195 if not preferred_options: 

196 return acceptable_options 

197 

198 return preferred_options 

199 

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 

207 

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] 

212 

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 

221 

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 

225 

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 

229 

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 

235 

236 return False 

237 

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) 

241 

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) 

247 

248 return None, None 

249 

250 return super(YakuhaiStrategy, self).try_to_call_meld(tile, is_kamicha_discard, tiles_136) 

251 

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 

256 

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 

263 

264 # it is fine to destroy pairs if we have plenty of them 

265 if pairs_before_meld > 2: 

266 return True 

267 

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

271 

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 

283 

284 return True 

285 

286 def _is_yakuhai_pon(self, meld): 

287 return meld.type == MeldPrint.PON and meld.tiles[0] // 4 in self.player.valued_honors