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 game.ai.strategies_v2.main import BaseStrategy 

2from mahjong.tile import TilesConverter 

3from mahjong.utils import ( 

4 count_tiles_by_suits, 

5 is_honor, 

6 is_man, 

7 is_pin, 

8 is_sou, 

9 is_tile_strictly_isolated, 

10 plus_dora, 

11 simplify, 

12) 

13 

14 

15class HonitsuStrategy(BaseStrategy): 

16 min_shanten = 4 

17 

18 chosen_suit = None 

19 

20 tiles_count_our_suit = 0 

21 dora_count_our_suit = 0 

22 dora_count_other_suits_not_isolated = 0 

23 tiles_count_other_suits = 0 

24 tiles_count_other_suits_not_isolated = 0 

25 

26 def get_open_hand_han(self): 

27 return 2 

28 

29 def should_activate_strategy(self, tiles_136, meld_tile=None): 

30 """ 

31 We can go for honitsu strategy if we have prevalence of one suit and honor tiles 

32 """ 

33 

34 result = super(HonitsuStrategy, self).should_activate_strategy(tiles_136) 

35 if not result: 

36 return False 

37 

38 tiles_34 = TilesConverter.to_34_array(tiles_136) 

39 suits = count_tiles_by_suits(tiles_34) 

40 

41 suits = [x for x in suits if x["name"] != "honor"] 

42 suits = sorted(suits, key=lambda x: x["count"], reverse=True) 

43 

44 suit = suits[0] 

45 

46 count_of_shuntsu_other_suits = 0 

47 count_of_koutsu_other_suits = 0 

48 count_of_ryanmen_other_suits = 0 

49 

50 count_of_shuntsu_other_suits += self._count_of_shuntsu(tiles_34, suits[1]["function"]) 

51 count_of_shuntsu_other_suits += self._count_of_shuntsu(tiles_34, suits[2]["function"]) 

52 

53 count_of_koutsu_other_suits += self._count_of_koutsu(tiles_34, suits[1]["function"]) 

54 count_of_koutsu_other_suits += self._count_of_koutsu(tiles_34, suits[2]["function"]) 

55 

56 count_of_ryanmen_other_suits += self._find_ryanmen_waits(tiles_34, suits[1]["function"]) 

57 count_of_ryanmen_other_suits += self._find_ryanmen_waits(tiles_34, suits[2]["function"]) 

58 

59 self._calculate_suitable_and_not_suitable_tiles_cnt(tiles_34, suit["function"]) 

60 self._initialize_honitsu_dora_count(tiles_136, suit) 

61 

62 # let's not go for honitsu if we have 5 or more tiles in other suits 

63 if self.tiles_count_other_suits >= 5: 

64 return False 

65 

66 # 7th turn and still 4 tiles in other suits - meh 

67 if self.tiles_count_other_suits >= 4 and self.player.round_step > 6: 

68 return False 

69 

70 # 12th turn is too late and we still have too many tiles in other suits 

71 if self.tiles_count_other_suits >= 3 and self.player.round_step > 11: 

72 return False 

73 

74 # let's not go for honitsu if we have 2 or more non-isolated doras 

75 # in other suits 

76 if self.dora_count_other_suits_not_isolated >= 2: 

77 return False 

78 

79 # if we have a pon of valued doras, let's not go for honitsu 

80 # we have a mangan anyway, let's go for fastest hand 

81 valued_pons = [x for x in self.player.valued_honors if tiles_34[x] >= 3] 

82 for pon in valued_pons: 

83 dora_count_valued_pons = plus_dora(pon * 4, self.player.table.dora_indicators) 

84 if dora_count_valued_pons > 0: 

85 return False 

86 

87 valued_pairs = len([x for x in self.player.valued_honors if tiles_34[x] == 2]) 

88 honor_pairs_or_pons = len([x for x in range(0, 34) if is_honor(x) and tiles_34[x] >= 2]) 

89 honor_doras_pairs_or_pons = len( 

90 [ 

91 x 

92 for x in range(0, 34) 

93 if is_honor(x) and tiles_34[x] >= 2 and plus_dora(x * 4, self.player.table.dora_indicators) 

94 ] 

95 ) 

96 unvalued_singles = len( 

97 [x for x in range(0, 34) if is_honor(x) and x not in self.player.valued_honors and tiles_34[x] == 1] 

98 ) 

99 

100 # what's the point of honitsu if there is not a single honor pair 

101 if honor_pairs_or_pons == 0: 

102 return False 

103 

104 # if we have twu ryanmens in other suits 

105 if count_of_ryanmen_other_suits >= 2: 

106 return False 

107 

108 # let's not go for honitsu nomi 

109 if not valued_pairs and not valued_pons: 

110 # this is not honitsu, maybe it will be pinfu one day 

111 if self.tiles_count_our_suit <= 7 and honor_pairs_or_pons < 2: 

112 return False 

113 

114 # also looks more like pinfu 

115 if self.tiles_count_other_suits >= 4: 

116 return False 

117 

118 # so-so, let's just not go for honitsu nomi 

119 if self.tiles_count_our_suit <= 9 and honor_pairs_or_pons == 1: 

120 if not self.dora_count_our_suit and not honor_doras_pairs_or_pons: 

121 return False 

122 

123 # if we have some decent amount of not isolated tiles in other suits 

124 # we may not rush for honitsu considering other conditions 

125 if self.tiles_count_other_suits_not_isolated >= 3: 

126 # if we don't have pair or pon of honored doras 

127 if honor_doras_pairs_or_pons == 0: 

128 # if we have a ryanmen with dora in other suit and no honor doras, so let's not rush honitsu 

129 if count_of_ryanmen_other_suits >= 1 and self.dora_count_other_suits_not_isolated >= 1: 

130 return False 

131 

132 # we need to either have a valued pair or have at least two honor 

133 # pairs to consider honitsu 

134 if valued_pairs == 0 and honor_pairs_or_pons < 2: 

135 return False 

136 

137 # doesn't matter valued or not, if we have just one honor pair 

138 # and have some single unvalued tiles, let's throw them away 

139 # first 

140 if honor_pairs_or_pons == 1 and unvalued_singles >= 2: 

141 return False 

142 

143 # 3 non-isolated unsuitable tiles, 1-shanen and already 8th turn 

144 # let's not consider honitsu here 

145 if self.player.ai.shanten == 1 and self.player.round_step > 8: 

146 return False 

147 else: 

148 # we have a pon of unvalued honor doras, but it looks like 

149 # it's faster to build our hand without honitsu 

150 if self.player.ai.shanten == 1: 

151 return False 

152 

153 # if we have a complete set in other suits, we can only throw it away if it's early in the game 

154 if count_of_shuntsu_other_suits + count_of_koutsu_other_suits >= 1: 

155 # too late to throw away chi after 5 step 

156 if self.player.round_step > 5: 

157 return False 

158 

159 # already 1 shanten, no need to throw away complete set 

160 if self.player.ai.shanten == 1: 

161 return False 

162 

163 # dora is not isolated and we have a complete set, let's not go for honitsu 

164 if self.dora_count_other_suits_not_isolated >= 1: 

165 return False 

166 

167 self.chosen_suit = suit["function"] 

168 

169 return True 

170 

171 def is_tile_suitable(self, tile): 

172 """ 

173 We can use only tiles of chosen suit and honor tiles 

174 :param tile: 136 tiles format 

175 :return: True 

176 """ 

177 tile //= 4 

178 return self.chosen_suit(tile) or is_honor(tile) 

179 

180 def meld_had_to_be_called(self, tile): 

181 has_not_suitable_tiles = False 

182 

183 for hand_tile in self.player.tiles: 

184 if not self.is_tile_suitable(hand_tile): 

185 has_not_suitable_tiles = True 

186 break 

187 

188 # if we still have unsuitable tiles, let's call honor pons 

189 # even if they don't change number of shanten 

190 if has_not_suitable_tiles and is_honor(tile // 4): 

191 return True 

192 

193 return False 

194 

195 def _calculate_suitable_and_not_suitable_tiles_cnt(self, tiles_34, suit): 

196 self.tiles_count_other_suits = 0 

197 self.tiles_count_other_suits_not_isolated = 0 

198 

199 for x in range(0, 34): 

200 tile_count = tiles_34[x] 

201 if not tile_count: 

202 continue 

203 

204 if suit(x): 

205 self.tiles_count_our_suit += tile_count 

206 elif not is_honor(x): 

207 self.tiles_count_other_suits += tile_count 

208 if not is_tile_strictly_isolated(tiles_34, x): 

209 self.tiles_count_other_suits_not_isolated += tile_count 

210 

211 def _initialize_honitsu_dora_count(self, tiles_136, suit): 

212 tiles_34 = TilesConverter.to_34_array(tiles_136) 

213 

214 dora_count_man = 0 

215 dora_count_pin = 0 

216 dora_count_sou = 0 

217 

218 dora_count_man_not_isolated = 0 

219 dora_count_pin_not_isolated = 0 

220 dora_count_sou_not_isolated = 0 

221 

222 for tile_136 in tiles_136: 

223 tile_34 = tile_136 // 4 

224 

225 dora_count = plus_dora( 

226 tile_136, self.player.table.dora_indicators, add_aka_dora=self.player.table.has_aka_dora 

227 ) 

228 

229 if is_man(tile_34): 

230 dora_count_man += dora_count 

231 if not is_tile_strictly_isolated(tiles_34, tile_34): 

232 dora_count_man_not_isolated += dora_count 

233 

234 if is_pin(tile_34): 

235 dora_count_pin += dora_count 

236 if not is_tile_strictly_isolated(tiles_34, tile_34): 

237 dora_count_pin_not_isolated += dora_count 

238 

239 if is_sou(tile_34): 

240 dora_count_sou += dora_count 

241 if not is_tile_strictly_isolated(tiles_34, tile_34): 

242 dora_count_sou_not_isolated += dora_count 

243 

244 if suit["name"] == "pin": 

245 self.dora_count_our_suit = dora_count_pin 

246 self.dora_count_other_suits_not_isolated = dora_count_man_not_isolated + dora_count_sou_not_isolated 

247 elif suit["name"] == "sou": 

248 self.dora_count_our_suit = dora_count_sou 

249 self.dora_count_other_suits_not_isolated = dora_count_man_not_isolated + dora_count_pin_not_isolated 

250 elif suit["name"] == "man": 

251 self.dora_count_our_suit = dora_count_man 

252 self.dora_count_other_suits_not_isolated = dora_count_sou_not_isolated + dora_count_pin_not_isolated 

253 

254 @staticmethod 

255 def _find_ryanmen_waits(tiles, suit): 

256 suit_tiles = [] 

257 for x in range(0, 34): 

258 tile = tiles[x] 

259 if not tile: 

260 continue 

261 

262 if suit(x): 

263 suit_tiles.append(x) 

264 

265 count_of_ryanmen_waits = 0 

266 simple_tiles = [simplify(x) for x in suit_tiles] 

267 for x in range(0, len(simple_tiles)): 

268 tile = simple_tiles[x] 

269 # we cant build ryanmen with 1 and 9 

270 if tile == 0 or tile == 8: 

271 continue 

272 

273 # bordered tile 

274 if x + 1 == len(simple_tiles): 

275 continue 

276 

277 if tile + 1 == simple_tiles[x + 1]: 

278 count_of_ryanmen_waits += 1 

279 

280 return count_of_ryanmen_waits 

281 

282 # we know we have no more that 5 tiles of other suit, 

283 # so this is a simplified version 

284 # be aware, that it will return 2 for 2345 form so use with care 

285 @staticmethod 

286 def _count_of_shuntsu(tiles, suit): 

287 suit_tiles = [] 

288 for x in range(0, 34): 

289 tile = tiles[x] 

290 if not tile: 

291 continue 

292 

293 if suit(x): 

294 suit_tiles.append(x) 

295 

296 count_of_left_tiles = 0 

297 count_of_middle_tiles = 0 

298 count_of_right_tiles = 0 

299 

300 simple_tiles = [simplify(x) for x in suit_tiles] 

301 for x in range(0, len(simple_tiles)): 

302 tile = simple_tiles[x] 

303 

304 if tile + 1 in simple_tiles and tile + 2 in simple_tiles: 

305 count_of_left_tiles += 1 

306 

307 if tile - 1 in simple_tiles and tile + 1 in simple_tiles: 

308 count_of_middle_tiles += 1 

309 

310 if tile - 2 in simple_tiles and tile - 1 in simple_tiles: 

311 count_of_right_tiles += 1 

312 

313 return (count_of_left_tiles + count_of_middle_tiles + count_of_right_tiles) // 3 

314 

315 # we know we have no more that 5 tiles of other suit, 

316 # so this is a simplified version 

317 @staticmethod 

318 def _count_of_koutsu(tiles, suit): 

319 count_of_koutsu = 0 

320 

321 for x in range(0, 34): 

322 tile = tiles[x] 

323 if not tile: 

324 continue 

325 

326 if suit(x) and tile >= 3: 

327 count_of_koutsu += 1 

328 

329 return count_of_koutsu