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 List 

2 

3import utils.decisions_constants as log 

4from game.ai.defence.main import TileDangerHandler 

5from game.ai.hand_builder import HandBuilder 

6from game.ai.helpers.kabe import Kabe 

7from game.ai.helpers.suji import Suji 

8from game.ai.kan import Kan 

9from mahjong.agari import Agari 

10from mahjong.constants import AKA_DORA_LIST, DISPLAY_WINDS 

11from mahjong.hand_calculating.divider import HandDivider 

12from mahjong.hand_calculating.hand import HandCalculator 

13from mahjong.hand_calculating.hand_config import HandConfig, OptionalRules 

14from mahjong.shanten import Shanten 

15from mahjong.tile import TilesConverter 

16from utils.cache import build_estimate_hand_value_cache_key, build_shanten_cache_key 

17 

18 

19class MahjongAI: 

20 version = "0.6.0-dev" 

21 

22 agari = None 

23 shanten_calculator = None 

24 defence = None 

25 riichi = None 

26 hand_divider = None 

27 finished_hand = None 

28 

29 shanten = 7 

30 ukeire = 0 

31 ukeire_second = 0 

32 waiting = None 

33 

34 hand_cache_shanten = {} 

35 hand_cache_estimation = {} 

36 

37 def __init__(self, player): 

38 self.player = player 

39 self.table = player.table 

40 

41 self.kan = Kan(player) 

42 self.agari = Agari() 

43 self.shanten_calculator = Shanten() 

44 self.defence = TileDangerHandler(player) 

45 self.hand_divider = HandDivider() 

46 self.finished_hand = HandCalculator() 

47 self.hand_builder = HandBuilder(player, self) 

48 self.riichi = player.config.RIICHI_HANDLER_CLASS(player) 

49 self.placement = player.config.PLACEMENT_HANDLER_CLASS(player) 

50 self.open_hand_handler = player.config.OPEN_HAND_HANDLER_CLASS(player) 

51 

52 self.suji = Suji(player) 

53 self.kabe = Kabe(player) 

54 

55 self.erase_state() 

56 

57 def erase_state(self): 

58 self.shanten = 7 

59 self.ukeire = 0 

60 self.ukeire_second = 0 

61 self.waiting = None 

62 

63 self.open_hand_handler.current_strategy = None 

64 self.open_hand_handler.last_discard_option = None 

65 

66 self.hand_cache_shanten = {} 

67 self.hand_cache_estimation = {} 

68 

69 # to erase hand cache 

70 self.finished_hand = HandCalculator() 

71 

72 def init_hand(self): 

73 self.player.logger.debug( 

74 log.INIT_HAND, 

75 context=[ 

76 f"Round wind: {DISPLAY_WINDS[self.table.round_wind_tile]}", 

77 f"Player wind: {DISPLAY_WINDS[self.player.player_wind]}", 

78 f"Hand: {self.player.format_hand_for_print()}", 

79 ], 

80 ) 

81 

82 self.shanten, _ = self.hand_builder.calculate_shanten_and_decide_hand_structure( 

83 TilesConverter.to_34_array(self.player.tiles) 

84 ) 

85 

86 def draw_tile(self, tile_136): 

87 if not self.player.in_riichi: 

88 self.open_hand_handler.determine_strategy(self.player.tiles) 

89 

90 def discard_tile(self, discard_tile): 

91 # we called meld and we had discard tile that we wanted to discard 

92 if discard_tile is not None: 

93 if not self.open_hand_handler.last_discard_option: 

94 return discard_tile, False 

95 

96 return self.hand_builder.process_discard_option(self.open_hand_handler.last_discard_option) 

97 

98 return self.hand_builder.discard_tile() 

99 

100 def try_to_call_meld(self, tile_136, is_kamicha_discard): 

101 return self.open_hand_handler.try_to_call_meld(tile_136, is_kamicha_discard) 

102 

103 def estimate_hand_value_or_get_from_cache( 

104 self, win_tile_34, tiles=None, call_riichi=False, is_tsumo=False, is_rinshan=False, is_chankan=False 

105 ): 

106 win_tile_136 = win_tile_34 * 4 

107 

108 # we don't need to think, that our waiting is aka dora 

109 if win_tile_136 in AKA_DORA_LIST: 

110 win_tile_136 += 1 

111 

112 if not tiles: 

113 tiles = self.player.tiles[:] 

114 else: 

115 tiles = tiles[:] 

116 

117 tiles += [win_tile_136] 

118 

119 config = HandConfig( 

120 is_riichi=call_riichi, 

121 player_wind=self.player.player_wind, 

122 round_wind=self.player.table.round_wind_tile, 

123 is_tsumo=is_tsumo, 

124 is_rinshan=is_rinshan, 

125 is_chankan=is_chankan, 

126 options=OptionalRules( 

127 has_aka_dora=self.player.table.has_aka_dora, 

128 has_open_tanyao=self.player.table.has_open_tanyao, 

129 has_double_yakuman=False, 

130 ), 

131 tsumi_number=self.player.table.count_of_honba_sticks, 

132 kyoutaku_number=self.player.table.count_of_riichi_sticks, 

133 ) 

134 

135 return self._estimate_hand_value_or_get_from_cache( 

136 win_tile_136, tiles, call_riichi, is_tsumo, 0, config, is_rinshan, is_chankan 

137 ) 

138 

139 def calculate_exact_hand_value_or_get_from_cache( 

140 self, 

141 win_tile_136, 

142 tiles=None, 

143 call_riichi=False, 

144 is_tsumo=False, 

145 is_chankan=False, 

146 is_haitei=False, 

147 is_ippatsu=False, 

148 ): 

149 if not tiles: 

150 tiles = self.player.tiles[:] 

151 else: 

152 tiles = tiles[:] 

153 

154 tiles += [win_tile_136] 

155 

156 additional_han = 0 

157 if is_chankan: 

158 additional_han += 1 

159 if is_haitei: 

160 additional_han += 1 

161 if is_ippatsu: 

162 additional_han += 1 

163 

164 config = HandConfig( 

165 is_riichi=call_riichi, 

166 player_wind=self.player.player_wind, 

167 round_wind=self.player.table.round_wind_tile, 

168 is_tsumo=is_tsumo, 

169 options=OptionalRules( 

170 has_aka_dora=self.player.table.has_aka_dora, 

171 has_open_tanyao=self.player.table.has_open_tanyao, 

172 has_double_yakuman=False, 

173 ), 

174 is_chankan=is_chankan, 

175 is_ippatsu=is_ippatsu, 

176 is_haitei=is_tsumo and is_haitei or False, 

177 is_houtei=(not is_tsumo) and is_haitei or False, 

178 tsumi_number=self.player.table.count_of_honba_sticks, 

179 kyoutaku_number=self.player.table.count_of_riichi_sticks, 

180 ) 

181 

182 return self._estimate_hand_value_or_get_from_cache( 

183 win_tile_136, tiles, call_riichi, is_tsumo, additional_han, config 

184 ) 

185 

186 def _estimate_hand_value_or_get_from_cache( 

187 self, win_tile_136, tiles, call_riichi, is_tsumo, additional_han, config, is_rinshan=False, is_chankan=False 

188 ): 

189 cache_key = build_estimate_hand_value_cache_key( 

190 tiles, 

191 call_riichi, 

192 is_tsumo, 

193 self.player.melds, 

194 self.player.table.dora_indicators, 

195 self.player.table.count_of_riichi_sticks, 

196 self.player.table.count_of_honba_sticks, 

197 additional_han, 

198 is_rinshan, 

199 is_chankan, 

200 ) 

201 if self.hand_cache_estimation.get(cache_key): 

202 return self.hand_cache_estimation.get(cache_key) 

203 

204 result = self.finished_hand.estimate_hand_value( 

205 tiles, 

206 win_tile_136, 

207 self.player.melds, 

208 self.player.table.dora_indicators, 

209 config, 

210 use_hand_divider_cache=True, 

211 ) 

212 

213 self.hand_cache_estimation[cache_key] = result 

214 return result 

215 

216 def estimate_weighted_mean_hand_value(self, discard_option): 

217 weighted_hand_cost = 0 

218 number_of_tiles = 0 

219 for waiting in discard_option.waiting: 

220 tiles = self.player.tiles[:] 

221 tiles.remove(discard_option.tile_to_discard_136) 

222 

223 hand_cost = self.estimate_hand_value_or_get_from_cache( 

224 waiting, tiles=tiles, call_riichi=discard_option.with_riichi, is_tsumo=True 

225 ) 

226 

227 if not hand_cost.cost: 

228 continue 

229 

230 weighted_hand_cost += ( 

231 hand_cost.cost["main"] + 2 * hand_cost.cost["additional"] 

232 ) * discard_option.wait_to_ukeire[waiting] 

233 number_of_tiles += discard_option.wait_to_ukeire[waiting] 

234 

235 cost = number_of_tiles and int(weighted_hand_cost / number_of_tiles) or 0 

236 

237 # we are karaten, or we don't have yaku 

238 # in that case let's add possible tempai cost 

239 if cost == 0 and self.player.round_step > 12: 

240 cost = 1000 

241 

242 if self.player.round_step > 15 and cost < 2500: 

243 cost = 2500 

244 

245 return cost 

246 

247 def should_call_kyuushu_kyuuhai(self) -> bool: 

248 """ 

249 Kyuushu kyuuhai 「九種九牌」 

250 (9 kinds of honor or terminal tiles) 

251 """ 

252 # TODO aim for kokushi 

253 return True 

254 

255 def should_call_win(self, tile, is_tsumo, enemy_seat=None, is_chankan=False): 

256 # don't skip win in riichi 

257 if self.player.in_riichi: 

258 return True 

259 

260 # currently we don't support win skipping for tsumo 

261 if is_tsumo: 

262 return True 

263 

264 # fast path - check it first to not calculate hand cost 

265 cost_needed = self.placement.get_minimal_cost_needed() 

266 if cost_needed == 0: 

267 return True 

268 

269 # 1 and not 0 because we call check for win this before updating remaining tiles 

270 is_hotei = self.player.table.count_of_remaining_tiles == 1 

271 

272 hand_response = self.calculate_exact_hand_value_or_get_from_cache( 

273 tile, 

274 tiles=self.player.tiles, 

275 call_riichi=self.player.in_riichi, 

276 is_tsumo=is_tsumo, 

277 is_chankan=is_chankan, 

278 is_haitei=is_hotei, 

279 ) 

280 assert hand_response is not None 

281 assert not hand_response.error, hand_response.error 

282 cost = hand_response.cost 

283 return self.placement.should_call_win(cost, is_tsumo, enemy_seat) 

284 

285 def enemy_called_riichi(self, enemy_seat): 

286 """ 

287 After enemy riichi we had to check will we fold or not 

288 it is affect open hand decisions 

289 :return: 

290 """ 

291 pass 

292 

293 def calculate_shanten_or_get_from_cache(self, closed_hand_34: List[int], use_chiitoitsu: bool): 

294 """ 

295 Sometimes we are calculating shanten for the same hand multiple times 

296 to save some resources let's cache previous calculations 

297 """ 

298 key = build_shanten_cache_key(closed_hand_34, use_chiitoitsu) 

299 if key in self.hand_cache_shanten: 

300 return self.hand_cache_shanten[key] 

301 if use_chiitoitsu and not self.player.is_open_hand: 

302 result = self.shanten_calculator.calculate_shanten_for_chiitoitsu_hand(closed_hand_34) 

303 else: 

304 result = self.shanten_calculator.calculate_shanten_for_regular_hand(closed_hand_34) 

305 self.hand_cache_shanten[key] = result 

306 return result 

307 

308 @property 

309 def enemy_players(self): 

310 """ 

311 Return list of players except our bot 

312 """ 

313 return self.player.table.players[1:]