Coverage for project/game/ai/main.py : 93%

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
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
19class MahjongAI:
20 version = "0.6.0-dev"
22 agari = None
23 shanten_calculator = None
24 defence = None
25 riichi = None
26 hand_divider = None
27 finished_hand = None
29 shanten = 7
30 ukeire = 0
31 ukeire_second = 0
32 waiting = None
34 hand_cache_shanten = {}
35 hand_cache_estimation = {}
37 def __init__(self, player):
38 self.player = player
39 self.table = player.table
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)
52 self.suji = Suji(player)
53 self.kabe = Kabe(player)
55 self.erase_state()
57 def erase_state(self):
58 self.shanten = 7
59 self.ukeire = 0
60 self.ukeire_second = 0
61 self.waiting = None
63 self.open_hand_handler.current_strategy = None
64 self.open_hand_handler.last_discard_option = None
66 self.hand_cache_shanten = {}
67 self.hand_cache_estimation = {}
69 # to erase hand cache
70 self.finished_hand = HandCalculator()
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 )
82 self.shanten, _ = self.hand_builder.calculate_shanten_and_decide_hand_structure(
83 TilesConverter.to_34_array(self.player.tiles)
84 )
86 def draw_tile(self, tile_136):
87 if not self.player.in_riichi:
88 self.open_hand_handler.determine_strategy(self.player.tiles)
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
96 return self.hand_builder.process_discard_option(self.open_hand_handler.last_discard_option)
98 return self.hand_builder.discard_tile()
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)
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
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
112 if not tiles:
113 tiles = self.player.tiles[:]
114 else:
115 tiles = tiles[:]
117 tiles += [win_tile_136]
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 )
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 )
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[:]
154 tiles += [win_tile_136]
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
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 )
182 return self._estimate_hand_value_or_get_from_cache(
183 win_tile_136, tiles, call_riichi, is_tsumo, additional_han, config
184 )
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)
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 )
213 self.hand_cache_estimation[cache_key] = result
214 return result
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)
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 )
227 if not hand_cost.cost:
228 continue
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]
235 cost = number_of_tiles and int(weighted_hand_cost / number_of_tiles) or 0
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
242 if self.player.round_step > 15 and cost < 2500:
243 cost = 2500
245 return cost
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
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
260 # currently we don't support win skipping for tsumo
261 if is_tsumo:
262 return True
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
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
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)
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
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
308 @property
309 def enemy_players(self):
310 """
311 Return list of players except our bot
312 """
313 return self.player.table.players[1:]