Coverage for project/game/ai/defence/yaku_analyzer/honitsu.py : 96%

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.defence.yaku_analyzer.chinitsu import ChinitsuAnalyzer
2from game.ai.defence.yaku_analyzer.honitsu_analyzer_base import HonitsuAnalyzerBase
3from game.ai.helpers.defence import TileDanger
4from mahjong.tile import TilesConverter
5from mahjong.utils import count_tiles_by_suits, is_honor
8class HonitsuAnalyzer(HonitsuAnalyzerBase):
9 id = "honitsu"
11 MIN_DISCARD = 6
12 MAX_MELDS = 3
13 EARLY_DISCARD_DIVISOR = 4
14 LESS_SUIT_PERCENTAGE_BORDER = 20
15 HONORS_PERCENTAGE_BORDER = 30
17 def is_yaku_active(self):
18 # TODO: in some distant future we may want to analyze menhon as well
19 if not self.enemy.melds:
20 return False
22 total_melds = len(self.enemy.melds)
23 total_discards = len(self.enemy.discards)
25 # let's check if there is too little info to analyze
26 if total_discards < HonitsuAnalyzer.MIN_DISCARD and total_melds < HonitsuAnalyzer.MAX_MELDS:
27 return False
29 # first of all - check melds, they must be all from one suit or honors
30 current_suit = None
31 for meld in self.enemy.melds:
32 tile = meld.tiles[0]
33 tile_34 = tile // 4
35 if is_honor(tile_34):
36 continue
38 suit = ChinitsuAnalyzer._get_tile_suit(tile)
39 if not current_suit:
40 current_suit = suit
41 elif suit["name"] != current_suit["name"]:
42 return False
44 # let's check discards
45 discards = [x.value for x in self.enemy.discards]
46 discards_34 = TilesConverter.to_34_array(discards)
47 result = count_tiles_by_suits(discards_34)
49 honors = [x for x in result if x["name"] == "honor"][0]
50 suits = [x for x in result if x["name"] != "honor"]
51 suits = sorted(suits, key=lambda x: x["count"], reverse=False)
53 less_suit = suits[0]
54 less_suit_tiles = less_suit["count"]
55 percentage_of_less_suit = (less_suit_tiles / total_discards) * 100
56 percentage_of_honor_tiles = (honors["count"] / total_discards) * 100
58 # there is not too much one suit + honor tiles in the discard
59 # so we can tell that user trying to collect honitsu
60 if (
61 percentage_of_less_suit <= HonitsuAnalyzer.LESS_SUIT_PERCENTAGE_BORDER
62 and percentage_of_honor_tiles <= HonitsuAnalyzer.HONORS_PERCENTAGE_BORDER
63 ):
64 if not current_suit:
65 current_suit = less_suit
66 elif current_suit != less_suit:
67 return False
69 # still cannot determine the suit - this is probably not honitsu
70 if not current_suit:
71 return False
73 if not self._check_discard_order(current_suit, int(total_discards / HonitsuAnalyzer.EARLY_DISCARD_DIVISOR)):
74 return False
76 # all checks have passed - assume this is honitsu
77 self.chosen_suit = current_suit["function"]
78 return True
80 def melds_han(self):
81 return self.enemy.is_open_hand and 2 or 3
83 def get_safe_tiles_34(self):
84 if not self.chosen_suit:
85 return []
87 safe_tiles = []
88 for x in range(0, 34):
89 if not self.chosen_suit(x) and not is_honor(x):
90 safe_tiles.append(x)
92 return safe_tiles
94 def get_bonus_danger(self, tile_136, number_of_revealed_tiles):
95 tile_34 = tile_136 // 4
97 if is_honor(tile_34):
98 if number_of_revealed_tiles == 4:
99 return []
100 elif number_of_revealed_tiles == 3:
101 return [TileDanger.HONITSU_THIRD_HONOR_BONUS_DANGER]
102 elif number_of_revealed_tiles == 2:
103 return [TileDanger.HONITSU_SECOND_HONOR_BONUS_DANGER]
104 else:
105 return [TileDanger.HONITSU_SHONPAI_HONOR_BONUS_DANGER]
107 return []
109 def is_absorbed(self, possible_yaku, tile_34=None):
110 return self._is_absorbed_by(possible_yaku, ChinitsuAnalyzer.id, tile_34)