Coverage for project/game/ai/strategies_v2/honitsu.py : 92%

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)
15class HonitsuStrategy(BaseStrategy):
16 min_shanten = 4
18 chosen_suit = None
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
26 def get_open_hand_han(self):
27 return 2
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 """
34 result = super(HonitsuStrategy, self).should_activate_strategy(tiles_136)
35 if not result:
36 return False
38 tiles_34 = TilesConverter.to_34_array(tiles_136)
39 suits = count_tiles_by_suits(tiles_34)
41 suits = [x for x in suits if x["name"] != "honor"]
42 suits = sorted(suits, key=lambda x: x["count"], reverse=True)
44 suit = suits[0]
46 count_of_shuntsu_other_suits = 0
47 count_of_koutsu_other_suits = 0
48 count_of_ryanmen_other_suits = 0
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"])
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"])
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"])
59 self._calculate_suitable_and_not_suitable_tiles_cnt(tiles_34, suit["function"])
60 self._initialize_honitsu_dora_count(tiles_136, suit)
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
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
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
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
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
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 )
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
104 # if we have twu ryanmens in other suits
105 if count_of_ryanmen_other_suits >= 2:
106 return False
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
114 # also looks more like pinfu
115 if self.tiles_count_other_suits >= 4:
116 return False
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
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
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
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
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
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
159 # already 1 shanten, no need to throw away complete set
160 if self.player.ai.shanten == 1:
161 return False
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
167 self.chosen_suit = suit["function"]
169 return True
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)
180 def meld_had_to_be_called(self, tile):
181 has_not_suitable_tiles = False
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
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
193 return False
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
199 for x in range(0, 34):
200 tile_count = tiles_34[x]
201 if not tile_count:
202 continue
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
211 def _initialize_honitsu_dora_count(self, tiles_136, suit):
212 tiles_34 = TilesConverter.to_34_array(tiles_136)
214 dora_count_man = 0
215 dora_count_pin = 0
216 dora_count_sou = 0
218 dora_count_man_not_isolated = 0
219 dora_count_pin_not_isolated = 0
220 dora_count_sou_not_isolated = 0
222 for tile_136 in tiles_136:
223 tile_34 = tile_136 // 4
225 dora_count = plus_dora(
226 tile_136, self.player.table.dora_indicators, add_aka_dora=self.player.table.has_aka_dora
227 )
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
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
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
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
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
262 if suit(x):
263 suit_tiles.append(x)
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
273 # bordered tile
274 if x + 1 == len(simple_tiles):
275 continue
277 if tile + 1 == simple_tiles[x + 1]:
278 count_of_ryanmen_waits += 1
280 return count_of_ryanmen_waits
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
293 if suit(x):
294 suit_tiles.append(x)
296 count_of_left_tiles = 0
297 count_of_middle_tiles = 0
298 count_of_right_tiles = 0
300 simple_tiles = [simplify(x) for x in suit_tiles]
301 for x in range(0, len(simple_tiles)):
302 tile = simple_tiles[x]
304 if tile + 1 in simple_tiles and tile + 2 in simple_tiles:
305 count_of_left_tiles += 1
307 if tile - 1 in simple_tiles and tile + 1 in simple_tiles:
308 count_of_middle_tiles += 1
310 if tile - 2 in simple_tiles and tile - 1 in simple_tiles:
311 count_of_right_tiles += 1
313 return (count_of_left_tiles + count_of_middle_tiles + count_of_right_tiles) // 3
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
321 for x in range(0, 34):
322 tile = tiles[x]
323 if not tile:
324 continue
326 if suit(x) and tile >= 3:
327 count_of_koutsu += 1
329 return count_of_koutsu