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 

3from game.ai.defence.enemy_analyzer import EnemyAnalyzer 

4from game.ai.discard import DiscardOption 

5from game.ai.placement import Placement 

6from mahjong.tile import TilesConverter 

7from mahjong.utils import is_chi, is_honor, is_pair, is_terminal, plus_dora, simplify 

8 

9 

10class Riichi: 

11 def __init__(self, player): 

12 self.player = player 

13 

14 def should_call_riichi(self, discard_option: DiscardOption, threats: List[EnemyAnalyzer]): 

15 assert discard_option.shanten == 0 

16 assert not self.player.is_open_hand 

17 

18 hand_builder = self.player.ai.hand_builder 

19 

20 waiting_34 = discard_option.waiting 

21 # empty waiting can be found in some cases 

22 if not waiting_34: 

23 return False 

24 

25 # save original hand state 

26 # we will restore it after we have finished our routines 

27 tiles_original, discards_original = hand_builder.emulate_discard(discard_option) 

28 

29 count_tiles = hand_builder.count_tiles(waiting_34, TilesConverter.to_34_array(self.player.closed_hand)) 

30 if count_tiles == 0: 

31 # don't call karaten riichi 

32 hand_builder.restore_after_emulate_discard(tiles_original, discards_original) 

33 return False 

34 

35 # we decide if we should riichi or not before making a discard, hence we check for round step == 0 

36 first_discard = self.player.round_step == 0 

37 if first_discard and not self.player.table.meld_was_called: 

38 hand_builder.restore_after_emulate_discard(tiles_original, discards_original) 

39 # it is daburi! 

40 return True 

41 

42 # regular path 

43 if len(waiting_34) == 1: 

44 should_riichi = self._should_call_riichi_one_sided(waiting_34, threats) 

45 else: 

46 should_riichi = self._should_call_riichi_many_sided(waiting_34, threats) 

47 

48 hand_builder.restore_after_emulate_discard(tiles_original, discards_original) 

49 return should_riichi 

50 

51 def _should_call_riichi_one_sided(self, waiting_34: List[int], threats: List[EnemyAnalyzer]): 

52 count_tiles = self.player.ai.hand_builder.count_tiles( 

53 waiting_34, TilesConverter.to_34_array(self.player.closed_hand) 

54 ) 

55 waiting_34 = waiting_34[0] 

56 hand_value = self.player.ai.estimate_hand_value_or_get_from_cache(waiting_34, call_riichi=False) 

57 hand_value_with_riichi = self.player.ai.estimate_hand_value_or_get_from_cache(waiting_34, call_riichi=True) 

58 

59 must_riichi = self.player.ai.placement.must_riichi( 

60 has_yaku=(hand_value.yaku is not None and hand_value.cost is not None), 

61 num_waits=count_tiles, 

62 cost_with_riichi=hand_value_with_riichi.cost["main"], 

63 cost_with_damaten=(hand_value.cost and hand_value.cost["main"] or 0), 

64 ) 

65 if must_riichi == Placement.MUST_RIICHI: 

66 return True 

67 elif must_riichi == Placement.MUST_DAMATEN: 

68 return False 

69 

70 tiles = self.player.closed_hand[:] 

71 closed_melds = [x for x in self.player.melds if not x.opened] 

72 for meld in closed_melds: 

73 tiles.extend(meld.tiles[:3]) 

74 

75 results, tiles_34 = self.player.ai.hand_builder.divide_hand(tiles, waiting_34) 

76 result = results[0] 

77 

78 closed_tiles_34 = TilesConverter.to_34_array(self.player.closed_hand) 

79 

80 have_suji, have_kabe = self.player.ai.hand_builder.check_suji_and_kabe(closed_tiles_34, waiting_34) 

81 

82 # what if we have yaku 

83 if hand_value.yaku is not None and hand_value.cost is not None: 

84 min_cost = hand_value.cost["main"] 

85 min_cost_with_riichi = hand_value_with_riichi and hand_value_with_riichi.cost["main"] or 0 

86 

87 # tanki honor is a good wait, let's damaten only if hand is already expensive 

88 if is_honor(waiting_34): 

89 if self.player.is_dealer and min_cost < 12000: 

90 return True 

91 

92 if not self.player.is_dealer and min_cost < 8000: 

93 return True 

94 

95 return False 

96 

97 is_chiitoitsu = len([x for x in result if is_pair(x)]) == 7 

98 simplified_waiting = simplify(waiting_34) 

99 

100 for hand_set in result: 

101 if waiting_34 not in hand_set: 

102 continue 

103 

104 # tanki wait but not chiitoitsu 

105 if is_pair(hand_set) and not is_chiitoitsu: 

106 # let's not riichi tanki 4, 5, 6 

107 if 3 <= simplified_waiting <= 5: 

108 return False 

109 

110 # don't riichi tanki wait on 1, 2, 3, 7, 8, 9 if it's only 1 tile 

111 if count_tiles == 1: 

112 return False 

113 

114 # don't riichi 2378 tanki if hand has good value 

115 if simplified_waiting != 0 and simplified_waiting != 8: 

116 if self.player.is_dealer and min_cost >= 7700: 

117 return False 

118 

119 if not self.player.is_dealer and min_cost >= 5200: 

120 return False 

121 

122 # only riichi if we have suji-trap or there is kabe 

123 if not have_suji and not have_kabe: 

124 return False 

125 

126 # let's not push these bad wait against threats 

127 if threats: 

128 return False 

129 

130 return True 

131 

132 # tanki wait with chiitoitsu 

133 if is_pair(hand_set) and is_chiitoitsu: 

134 # chiitoitsu on last suit tile is not the best 

135 if count_tiles == 1: 

136 return False 

137 

138 # early riichi on 19 tanki is good 

139 if (simplified_waiting == 0 or simplified_waiting == 8) and self.player.round_step < 7: 

140 return True 

141 

142 # riichi on 19 tanki is good later too if we have 3 tiles to wait for 

143 if ( 

144 (simplified_waiting == 0 or simplified_waiting == 8) 

145 and self.player.round_step < 12 

146 and count_tiles == 3 

147 ): 

148 return True 

149 

150 # riichi on 28 tanki is good if we have 3 tiles to wait for 

151 if ( 

152 (simplified_waiting == 1 or simplified_waiting == 7) 

153 and self.player.round_step < 12 

154 and count_tiles == 3 

155 ): 

156 return True 

157 

158 # otherwise only riichi if we have suji-trab or there is kabe 

159 if not have_suji and not have_kabe: 

160 return False 

161 

162 # let's not push these bad wait against threats 

163 if threats: 

164 return False 

165 

166 return True 

167 

168 # 1-sided wait means kanchan or penchan 

169 if is_chi(hand_set): 

170 # if we only have 1 tile to wait for, let's damaten 

171 if count_tiles == 1: 

172 return False 

173 

174 # for dealer it is always riichi 

175 if self.player.is_dealer: 

176 return True 

177 # let's not push cheap hands against threats 

178 elif threats and min_cost_with_riichi < 2600: 

179 return False 

180 

181 if 3 <= simplified_waiting <= 5: 

182 if min_cost_with_riichi >= 2600: 

183 return True 

184 

185 # for not dealer let's not riichi cheap kanchan on 4, 5, 6 

186 return False 

187 

188 # if we have 2 tiles to wait for and hand cost is good without riichi, 

189 # let's damaten 

190 if count_tiles == 2: 

191 if self.player.is_dealer and min_cost >= 7700: 

192 return False 

193 

194 if not self.player.is_dealer and min_cost >= 5200: 

195 return False 

196 

197 # if we have more than two tiles to wait for and we have kabe or suji - insta riichi 

198 if count_tiles > 2 and (have_suji or have_kabe): 

199 return True 

200 

201 # 2 and 8 are good waits but not in every condition 

202 if simplified_waiting == 1 or simplified_waiting == 7: 

203 if self.player.round_step < 7: 

204 if self.player.is_dealer and min_cost < 18000: 

205 return True 

206 

207 if not self.player.is_dealer and min_cost < 8000: 

208 return True 

209 

210 if self.player.round_step < 12: 

211 if self.player.is_dealer and min_cost < 12000: 

212 return True 

213 

214 if not self.player.is_dealer and min_cost < 5200: 

215 return True 

216 

217 if self.player.round_step < 15: 

218 if self.player.is_dealer and 2000 < min_cost < 7700: 

219 return True 

220 

221 # 3 and 7 are ok waits sometimes too 

222 if simplified_waiting == 2 or simplified_waiting == 6: 

223 if self.player.round_step < 7: 

224 if self.player.is_dealer and min_cost < 12000: 

225 return True 

226 

227 if not self.player.is_dealer and min_cost < 5200: 

228 return True 

229 

230 if self.player.round_step < 12: 

231 if self.player.is_dealer and min_cost < 7700: 

232 return True 

233 

234 if not self.player.is_dealer and min_cost < 5200: 

235 return True 

236 

237 if self.player.round_step < 15: 

238 if self.player.is_dealer and 2000 < min_cost < 7700: 

239 return True 

240 

241 # otherwise only riichi if we have suji-trap or there is kabe 

242 if not have_suji and not have_kabe: 

243 return False 

244 

245 return True 

246 

247 # what if we don't have yaku 

248 # our tanki wait is good, let's riichi 

249 if is_honor(waiting_34): 

250 return True 

251 

252 if count_tiles > 1: 

253 # terminal tanki is ok, too, just should be more than one tile left 

254 if is_terminal(waiting_34): 

255 return True 

256 

257 # whatever dora wait is ok, too, just should be more than one tile left 

258 if plus_dora(waiting_34 * 4, self.player.table.dora_indicators, add_aka_dora=False) > 0: 

259 return True 

260 

261 simplified_waiting = simplify(waiting_34) 

262 

263 for hand_set in result: 

264 if waiting_34 not in hand_set: 

265 continue 

266 

267 if is_pair(hand_set): 

268 # let's not riichi tanki wait without suji-trap or kabe 

269 if not have_suji and not have_kabe: 

270 return False 

271 

272 # let's not riichi tanki on last suit tile if it's early 

273 if count_tiles == 1 and self.player.round_step < 6: 

274 return False 

275 

276 # let's not riichi tanki 4, 5, 6 if it's early 

277 if 3 <= simplified_waiting <= 5 and self.player.round_step < 6: 

278 return False 

279 

280 # 1-sided wait means kanchan or penchan 

281 # let's only riichi this bad wait if 

282 # it has all 4 tiles available or it 

283 # it's not too early 

284 # and there are no threats 

285 if not threats and is_chi(hand_set) and 4 <= simplified_waiting <= 6: 

286 return count_tiles == 4 or self.player.round_step >= 6 

287 

288 return True 

289 

290 def _should_call_riichi_many_sided(self, waiting_34: List[int], threats: List[EnemyAnalyzer]): 

291 count_tiles = self.player.ai.hand_builder.count_tiles( 

292 waiting_34, TilesConverter.to_34_array(self.player.closed_hand) 

293 ) 

294 hand_costs = [] 

295 hand_costs_with_riichi = [] 

296 waits_with_yaku = 0 

297 for wait in waiting_34: 

298 hand_value = self.player.ai.estimate_hand_value_or_get_from_cache(wait, call_riichi=False) 

299 if hand_value.error is None: 

300 hand_costs.append(hand_value.cost["main"]) 

301 if hand_value.yaku is not None and hand_value.cost is not None: 

302 waits_with_yaku += 1 

303 

304 hand_value_with_riichi = self.player.ai.estimate_hand_value_or_get_from_cache(wait, call_riichi=True) 

305 if hand_value_with_riichi.error is None: 

306 hand_costs_with_riichi.append(hand_value_with_riichi.cost["main"]) 

307 

308 min_cost = hand_costs and min(hand_costs) or 0 

309 min_cost_with_riichi = hand_costs_with_riichi and min(hand_costs_with_riichi) or 0 

310 

311 must_riichi = self.player.ai.placement.must_riichi( 

312 has_yaku=waits_with_yaku == len(waiting_34), 

313 num_waits=count_tiles, 

314 cost_with_riichi=min_cost_with_riichi, 

315 cost_with_damaten=min_cost, 

316 ) 

317 if must_riichi == Placement.MUST_RIICHI: 

318 return True 

319 elif must_riichi == Placement.MUST_DAMATEN: 

320 return False 

321 

322 is_dealer_threat = any([x.enemy.is_dealer for x in threats]) 

323 

324 # we don't want to push cheap hand against dealer 

325 if is_dealer_threat and min_cost_with_riichi <= 1300: 

326 return False 

327 

328 # if we have yaku on every wait 

329 if waits_with_yaku == len(waiting_34): 

330 # let's not riichi this bad wait 

331 if count_tiles <= 2: 

332 return False 

333 

334 # chasing riichi on late steps of the game is not profitable 

335 if threats and self.player.round_step >= 9: 

336 return False 

337 

338 # if wait is slightly better, we will riichi only a cheap hand 

339 if count_tiles <= 4: 

340 if self.player.is_dealer and min_cost >= 7700: 

341 return False 

342 

343 if not self.player.is_dealer and min_cost >= 5200: 

344 return False 

345 

346 return True 

347 

348 # wait is even better, but still don't call riichi on damaten mangan 

349 if count_tiles <= 6: 

350 # if it's early riichi more readily 

351 if self.player.round_step > 6: 

352 if self.player.is_dealer and min_cost >= 11600: 

353 return False 

354 

355 if not self.player.is_dealer and min_cost >= 7700: 

356 return False 

357 else: 

358 if self.player.is_dealer and min_cost >= 18000: 

359 return False 

360 

361 if not self.player.is_dealer and min_cost >= 12000: 

362 return False 

363 

364 return True 

365 

366 # if wait is good we only damaten haneman 

367 if self.player.is_dealer and min_cost >= 18000: 

368 return False 

369 

370 if not self.player.is_dealer and min_cost >= 12000: 

371 return False 

372 

373 return True 

374 

375 # if we don't have yaku on every wait and it's two-sided or more, we call riichi 

376 return True