Coverage for project/game/table.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.player import EnemyPlayer, Player
2from mahjong.constants import EAST, NORTH, SOUTH, WEST
3from mahjong.tile import Tile, TilesConverter
4from mahjong.utils import plus_dora
5from utils.decisions_logger import MeldPrint
6from utils.general import is_sangenpai
9class Table:
10 # our bot
11 player = None
12 # main bot + all other players
13 players = None
15 dora_indicators = None
17 dealer_seat = 0
18 round_number = -1
19 round_wind_number = 0
20 count_of_riichi_sticks = 0
21 count_of_honba_sticks = 0
23 count_of_remaining_tiles = 0
24 count_of_players = 4
26 meld_was_called = False
28 # array of tiles in 34 format
29 revealed_tiles = None
30 revealed_tiles_136 = None
32 # bot is playing mainly with ari-ari rules, so we can have them as default
33 has_open_tanyao = True
34 has_aka_dora = True
36 def __init__(self, bot_config=None):
37 self._init_players(bot_config)
38 self.dora_indicators = []
39 self.revealed_tiles = [0] * 34
40 self.revealed_tiles_136 = []
42 def __str__(self):
43 dora_string = TilesConverter.to_one_line_string(
44 self.dora_indicators, print_aka_dora=self.player.table.has_aka_dora
45 )
47 return "Wind: {}, Honba: {}, Dora Indicators: {}".format(
48 self.round_wind_number, self.count_of_honba_sticks, dora_string
49 )
51 def init_round(
52 self, round_wind_number, count_of_honba_sticks, count_of_riichi_sticks, dora_indicator, dealer_seat, scores
53 ):
55 # we need it to properly display log for each round
56 self.round_number += 1
58 self.meld_was_called = False
59 self.dealer_seat = dealer_seat
60 self.round_wind_number = round_wind_number
61 self.count_of_honba_sticks = count_of_honba_sticks
62 self.count_of_riichi_sticks = count_of_riichi_sticks
64 self.revealed_tiles = [0] * 34
65 self.revealed_tiles_136 = []
67 self.dora_indicators = []
68 self.add_dora_indicator(dora_indicator)
70 # erase players state
71 for player in self.players:
72 player.erase_state()
73 player.dealer_seat = dealer_seat
74 self.set_players_scores(scores)
76 # 136 - total count of tiles
77 # 14 - tiles in dead wall
78 # 13 - tiles in each player hand
79 self.count_of_remaining_tiles = 136 - 14 - self.count_of_players * 13
81 if round_wind_number == 0 and count_of_honba_sticks == 0:
82 i = 0
83 seats = [0, 1, 2, 3]
84 for player in self.players:
85 player.first_seat = seats[i - dealer_seat]
86 i += 1
88 def erase_state(self):
89 self.dora_indicators = []
90 self.revealed_tiles = [0] * 34
91 self.revealed_tiles_136 = []
93 def add_called_meld(self, player_seat, meld):
94 self.meld_was_called = True
96 # if meld was called from the other player, then we skip one draw from the wall
97 if meld.opened:
98 # but if it's an opened kan, player will get a tile from
99 # a dead wall, so total number of tiles in the wall is the same
100 # as if he just draws a tile
101 if meld.type != MeldPrint.KAN and meld.type != meld.SHOUMINKAN:
102 self.count_of_remaining_tiles += 1
103 else:
104 # can't have a pon or chi from the hand
105 assert meld.type == MeldPrint.KAN or meld.type == meld.SHOUMINKAN
106 # player draws additional tile from the wall in case of closed kan or shouminkan
107 self.count_of_remaining_tiles -= 1
109 self.get_player(player_seat).add_called_meld(meld)
111 tiles = meld.tiles[:]
112 # called tile was already added to revealed array
113 # because it was called on the discard
114 if meld.called_tile is not None:
115 tiles.remove(meld.called_tile)
117 # for shouminkan we already added 3 tiles
118 if meld.type == meld.SHOUMINKAN:
119 tiles = [meld.tiles[0]]
121 for tile in tiles:
122 self._add_revealed_tile(tile)
124 for player in self.players:
125 player.is_ippatsu = False
127 def add_called_riichi_step_one(self, player_seat):
128 """
129 We need to mark player in riichi to properly defence against his riichi tile discard
130 """
131 player = self.get_player(player_seat)
132 player.in_riichi = True
134 # we had to check will we go for defence or not
135 if player_seat != 0:
136 self.player.enemy_called_riichi(player_seat)
138 def add_called_riichi_step_two(self, player_seat):
139 player = self.get_player(player_seat)
141 if player.scores is not None:
142 player.scores -= 1000
144 self.count_of_riichi_sticks += 1
146 player.is_ippatsu = True
147 assert len(player.discards) >= 1, "Player had to have at least one discarded tile after riichi"
148 latest_discard = player.discards[-1]
149 latest_discard.riichi_discard = True
150 player.riichi_tile_136 = latest_discard.value
152 player.is_oikake_riichi = len([x for x in self.players if x.in_riichi]) > 1
153 if not player.is_oikake_riichi:
154 other_riichi_players = [x for x in self.players if x.in_riichi and x != player]
155 player.is_oikake_riichi_against_dealer_riichi_threat = any([x.is_dealer for x in other_riichi_players])
157 open_hand_threat = False
158 for other_player in self.players:
159 if other_player == player:
160 continue
162 for meld in other_player.melds:
163 dora_number = 0
164 if meld.type == MeldPrint.CHI:
165 continue
167 for tile in meld.tiles:
168 dora_number += plus_dora(tile, self.dora_indicators, add_aka_dora=self.has_aka_dora)
170 if dora_number >= 3:
171 open_hand_threat = True
172 player.is_riichi_against_open_hand_threat = open_hand_threat
174 def add_discarded_tile(self, player_seat, tile_136, is_tsumogiri):
175 """
176 :param player_seat:
177 :param tile_136: 136 format tile
178 :param is_tsumogiri: was tile discarded from hand or not
179 """
180 if player_seat != 0:
181 self.count_of_remaining_tiles -= 1
183 tile = Tile(tile_136, is_tsumogiri)
184 tile.riichi_discard = False
185 player = self.get_player(player_seat)
186 player.add_discarded_tile(tile)
188 self._add_revealed_tile(tile_136)
190 player.is_ippatsu = False
192 def add_dora_indicator(self, tile):
193 self.dora_indicators.append(tile)
194 self._add_revealed_tile(tile)
196 def is_dora(self, tile):
197 return plus_dora(tile, self.dora_indicators, add_aka_dora=self.has_aka_dora)
199 def set_players_scores(self, scores, uma=None):
200 for i in range(0, len(scores)):
201 self.get_player(i).scores = scores[i] * 100
203 if uma:
204 self.get_player(i).uma = uma[i]
206 self.recalculate_players_position()
208 def recalculate_players_position(self):
209 temp_players = self.get_players_sorted_by_scores()
210 for i in range(0, len(temp_players)):
211 temp_player = temp_players[i]
212 self.get_player(temp_player.seat).position = i + 1
214 def set_players_names_and_ranks(self, values):
215 for x in range(0, len(values)):
216 self.get_player(x).name = values[x]["name"]
217 self.get_player(x).rank = values[x]["rank"]
219 def get_player(self, player_seat):
220 return self.players[player_seat]
222 def get_players_sorted_by_scores(self):
223 return sorted(self.players, key=lambda x: (x.scores or 0, -x.first_seat), reverse=True)
225 @property
226 def round_wind_tile(self):
227 if self.round_wind_number < 4:
228 return EAST
229 elif 4 <= self.round_wind_number < 8:
230 return SOUTH
231 elif 8 <= self.round_wind_number < 12:
232 return WEST
233 else:
234 return NORTH
236 def is_common_yakuhai(self, tile_34):
237 return is_sangenpai(tile_34) or tile_34 == self.round_wind_tile
239 def _add_revealed_tile(self, tile):
240 self.revealed_tiles_136.append(tile)
241 tile_34 = tile // 4
242 self.revealed_tiles[tile_34] += 1
244 assert (
245 self.revealed_tiles[tile_34] <= 4
246 ), f"we have only 4 tiles in the game: {TilesConverter.to_one_line_string([tile])}"
248 def _init_players(self, bot_config):
249 self.player = Player(self, 0, self.dealer_seat, bot_config)
251 self.players = [self.player]
252 for seat in range(1, self.count_of_players):
253 player = EnemyPlayer(self, seat, self.dealer_seat)
254 self.players.append(player)