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.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 

7 

8 

9class Table: 

10 # our bot 

11 player = None 

12 # main bot + all other players 

13 players = None 

14 

15 dora_indicators = None 

16 

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 

22 

23 count_of_remaining_tiles = 0 

24 count_of_players = 4 

25 

26 meld_was_called = False 

27 

28 # array of tiles in 34 format 

29 revealed_tiles = None 

30 revealed_tiles_136 = None 

31 

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 

35 

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 = [] 

41 

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 ) 

46 

47 return "Wind: {}, Honba: {}, Dora Indicators: {}".format( 

48 self.round_wind_number, self.count_of_honba_sticks, dora_string 

49 ) 

50 

51 def init_round( 

52 self, round_wind_number, count_of_honba_sticks, count_of_riichi_sticks, dora_indicator, dealer_seat, scores 

53 ): 

54 

55 # we need it to properly display log for each round 

56 self.round_number += 1 

57 

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 

63 

64 self.revealed_tiles = [0] * 34 

65 self.revealed_tiles_136 = [] 

66 

67 self.dora_indicators = [] 

68 self.add_dora_indicator(dora_indicator) 

69 

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) 

75 

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 

80 

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 

87 

88 def erase_state(self): 

89 self.dora_indicators = [] 

90 self.revealed_tiles = [0] * 34 

91 self.revealed_tiles_136 = [] 

92 

93 def add_called_meld(self, player_seat, meld): 

94 self.meld_was_called = True 

95 

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 

108 

109 self.get_player(player_seat).add_called_meld(meld) 

110 

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) 

116 

117 # for shouminkan we already added 3 tiles 

118 if meld.type == meld.SHOUMINKAN: 

119 tiles = [meld.tiles[0]] 

120 

121 for tile in tiles: 

122 self._add_revealed_tile(tile) 

123 

124 for player in self.players: 

125 player.is_ippatsu = False 

126 

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 

133 

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) 

137 

138 def add_called_riichi_step_two(self, player_seat): 

139 player = self.get_player(player_seat) 

140 

141 if player.scores is not None: 

142 player.scores -= 1000 

143 

144 self.count_of_riichi_sticks += 1 

145 

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 

151 

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]) 

156 

157 open_hand_threat = False 

158 for other_player in self.players: 

159 if other_player == player: 

160 continue 

161 

162 for meld in other_player.melds: 

163 dora_number = 0 

164 if meld.type == MeldPrint.CHI: 

165 continue 

166 

167 for tile in meld.tiles: 

168 dora_number += plus_dora(tile, self.dora_indicators, add_aka_dora=self.has_aka_dora) 

169 

170 if dora_number >= 3: 

171 open_hand_threat = True 

172 player.is_riichi_against_open_hand_threat = open_hand_threat 

173 

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 

182 

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) 

187 

188 self._add_revealed_tile(tile_136) 

189 

190 player.is_ippatsu = False 

191 

192 def add_dora_indicator(self, tile): 

193 self.dora_indicators.append(tile) 

194 self._add_revealed_tile(tile) 

195 

196 def is_dora(self, tile): 

197 return plus_dora(tile, self.dora_indicators, add_aka_dora=self.has_aka_dora) 

198 

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 

202 

203 if uma: 

204 self.get_player(i).uma = uma[i] 

205 

206 self.recalculate_players_position() 

207 

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 

213 

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"] 

218 

219 def get_player(self, player_seat): 

220 return self.players[player_seat] 

221 

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) 

224 

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 

235 

236 def is_common_yakuhai(self, tile_34): 

237 return is_sangenpai(tile_34) or tile_34 == self.round_wind_tile 

238 

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 

243 

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])}" 

247 

248 def _init_players(self, bot_config): 

249 self.player = Player(self, 0, self.dealer_seat, bot_config) 

250 

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)