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 typing import Optional 

2 

3import utils.decisions_constants as log 

4from game.ai.configs.default import BotDefaultConfig 

5from game.ai.main import MahjongAI 

6from mahjong.constants import CHUN, EAST, HAKU, HATSU, NORTH, SOUTH, WEST 

7from mahjong.tile import Tile, TilesConverter 

8from utils.decisions_logger import DecisionsLogger, MeldPrint 

9 

10 

11class PlayerInterface: 

12 table = None 

13 discards = None 

14 tiles = None 

15 melds = None 

16 round_step = None 

17 

18 # current player seat 

19 seat = 0 

20 # where is sitting dealer, based on this information we can calculate player wind 

21 dealer_seat = 0 

22 # it is important to know initial seat for correct players sorting 

23 first_seat = 0 

24 

25 # position based on scores 

26 position = 0 

27 scores = None 

28 uma = 0 

29 

30 name = "" 

31 rank = "" 

32 

33 logger = None 

34 

35 in_riichi: bool = False 

36 is_ippatsu: bool = False 

37 is_oikake_riichi: bool = False 

38 is_oikake_riichi_against_dealer_riichi_threat: bool = False 

39 is_riichi_against_open_hand_threat: bool = False 

40 

41 def __init__(self, table, seat, dealer_seat): 

42 self.table = table 

43 self.seat = seat 

44 self.dealer_seat = dealer_seat 

45 self.logger = DecisionsLogger() 

46 

47 self.erase_state() 

48 

49 def __str__(self): 

50 result = "{0}".format(self.name) 

51 if self.scores is not None: 

52 result += " ({:,d})".format(int(self.scores)) 

53 if self.uma: 

54 result += " {0}".format(self.uma) 

55 else: 

56 result += " ({0})".format(self.rank) 

57 return result 

58 

59 def __repr__(self): 

60 return self.__str__() 

61 

62 def init_logger(self, logger): 

63 self.logger.logger = logger 

64 

65 def erase_state(self): 

66 self.tiles = [] 

67 self.discards = [] 

68 self.melds = [] 

69 self.in_riichi = False 

70 self.is_ippatsu = False 

71 self.is_oikake_riichi = False 

72 self.is_oikake_riichi_against_dealer_riichi_threat = False 

73 self.is_riichi_against_open_hand_threat = False 

74 self.position = 0 

75 self.scores = None 

76 self.uma = 0 

77 self.round_step = 0 

78 

79 def add_called_meld(self, meld: MeldPrint): 

80 # we already added shouminkan as a pon set 

81 if meld.type == MeldPrint.SHOUMINKAN: 

82 tile_34 = meld.tiles[0] // 4 

83 

84 pon_set = [x for x in self.melds if x.type == MeldPrint.PON and (x.tiles[0] // 4) == tile_34] 

85 

86 # when we are doing reconnect and we have called shouminkan set 

87 # we will not have called pon set in the hand 

88 if pon_set: 

89 self.melds.remove(pon_set[0]) 

90 

91 # we need to add tile that we used for open can to the hand 

92 if meld.type == MeldPrint.KAN and meld.opened: 

93 self.tiles.append(meld.called_tile) 

94 

95 self.melds.append(meld) 

96 

97 def add_discarded_tile(self, tile: Tile): 

98 self.discards.append(tile) 

99 

100 # all tiles that were discarded after player riichi will be safe against him 

101 # because of furiten 

102 tile = tile.value // 4 

103 for player in self.table.players[1:]: 

104 if player.in_riichi and tile not in player.safe_tiles: 

105 player.safe_tiles.append(tile) 

106 

107 # one discard == one round step 

108 self.round_step += 1 

109 

110 @property 

111 def player_wind(self): 

112 shift = self.dealer_seat - self.seat 

113 position = [0, 1, 2, 3][shift] 

114 if position == 0: 

115 return EAST 

116 elif position == 1: 

117 return NORTH 

118 elif position == 2: 

119 return WEST 

120 else: 

121 return SOUTH 

122 

123 @property 

124 def is_dealer(self): 

125 return self.seat == self.dealer_seat 

126 

127 @property 

128 def is_open_hand(self): 

129 opened_melds = [x for x in self.melds if x.opened] 

130 return len(opened_melds) > 0 

131 

132 @property 

133 def meld_tiles(self): 

134 """ 

135 Array of 136 tiles format 

136 :return: 

137 """ 

138 result = [] 

139 for meld in self.melds: 

140 result.extend(meld.tiles) 

141 return result 

142 

143 @property 

144 def meld_34_tiles(self): 

145 """ 

146 Array of array with 34 tiles indices 

147 :return: array 

148 """ 

149 melds = [x.tiles[:] for x in self.melds] 

150 results = [] 

151 for meld in melds: 

152 meld_34 = [meld[0] // 4, meld[1] // 4, meld[2] // 4] 

153 # kan 

154 if len(meld) > 3: 

155 meld_34.append(meld[3] // 4) 

156 results.append(meld_34) 

157 return results 

158 

159 @property 

160 def valued_honors(self): 

161 return [CHUN, HAKU, HATSU, self.table.round_wind_tile, self.player_wind] 

162 

163 

164class Player(PlayerInterface): 

165 ai: Optional[MahjongAI] = None 

166 config: Optional[BotDefaultConfig] = None 

167 last_draw = None 

168 in_tempai = False 

169 

170 def __init__(self, table, seat, dealer_seat, bot_config: Optional[BotDefaultConfig]): 

171 super().__init__(table, seat, dealer_seat) 

172 self.config = bot_config or BotDefaultConfig() 

173 self.ai = MahjongAI(self) 

174 

175 def erase_state(self): 

176 super().erase_state() 

177 

178 self.last_draw = None 

179 self.in_tempai = False 

180 

181 if self.ai: 

182 self.ai.erase_state() 

183 

184 def init_hand(self, tiles): 

185 self.tiles = tiles 

186 

187 self.ai.init_hand() 

188 

189 def draw_tile(self, tile_136): 

190 context = [ 

191 f"Step: {self.round_step}", 

192 f"Remaining tiles: {self.table.count_of_remaining_tiles}", 

193 f"Hand: {self.format_hand_for_print(tile_136)}", 

194 ] 

195 if self.ai.open_hand_handler.current_strategy: 

196 context.append(f"Current strategy: {self.ai.open_hand_handler.current_strategy}") 

197 

198 self.logger.debug(log.DRAW, context=context) 

199 

200 # it is important to recalculate all threats here 

201 self.ai.defence.erase_threats_cache() 

202 

203 self.last_draw = tile_136 

204 self.tiles.append(tile_136) 

205 

206 # we need sort it to have a better string presentation 

207 self.tiles = sorted(self.tiles) 

208 

209 self.ai.draw_tile(tile_136) 

210 

211 def discard_tile(self, discard_tile=None, force_tsumogiri=False): 

212 if force_tsumogiri: 

213 tile_to_discard = discard_tile 

214 with_riichi = False 

215 else: 

216 tile_to_discard, with_riichi = self.ai.discard_tile(discard_tile) 

217 

218 is_tsumogiri = tile_to_discard == self.last_draw 

219 # it is important to use table method, 

220 # to recalculate revealed tiles and etc. 

221 self.table.add_discarded_tile(0, tile_to_discard, is_tsumogiri) 

222 self.tiles.remove(tile_to_discard) 

223 

224 return tile_to_discard, with_riichi 

225 

226 def should_call_kan(self, tile, open_kan, from_riichi=False): 

227 self.ai.defence.erase_threats_cache() 

228 return self.ai.kan.should_call_kan(tile, open_kan, from_riichi) 

229 

230 def should_call_win(self, tile, is_tsumo, enemy_seat=None, is_chankan=False): 

231 self.ai.defence.erase_threats_cache() 

232 return self.ai.should_call_win(tile, is_tsumo, enemy_seat, is_chankan) 

233 

234 def should_call_kyuushu_kyuuhai(self): 

235 self.ai.defence.erase_threats_cache() 

236 return self.ai.should_call_kyuushu_kyuuhai() 

237 

238 def try_to_call_meld(self, tile, is_kamicha_discard): 

239 self.ai.defence.erase_threats_cache() 

240 return self.ai.try_to_call_meld(tile, is_kamicha_discard) 

241 

242 def enemy_called_riichi(self, player_seat): 

243 self.ai.enemy_called_riichi(player_seat) 

244 

245 def number_of_revealed_tiles(self, tile_34, closed_hand_34): 

246 """ 

247 Return sum of all tiles (discarded + from melds + our hand) 

248 :param tile_34: 34 tile format 

249 :param closed_hand_34: cached list of tiles (to not build it for each iteration) 

250 :return: int 

251 """ 

252 revealed_tiles = closed_hand_34[tile_34] + self.table.revealed_tiles[tile_34] 

253 assert revealed_tiles <= 4, "we have only 4 tiles in the game" 

254 return revealed_tiles 

255 

256 def format_hand_for_print(self, tile_136=None): 

257 hand_string = "{}".format( 

258 TilesConverter.to_one_line_string(self.closed_hand, print_aka_dora=self.table.has_aka_dora) 

259 ) 

260 

261 if tile_136 is not None: 

262 hand_string += " + {}".format( 

263 TilesConverter.to_one_line_string([tile_136], print_aka_dora=self.table.has_aka_dora) 

264 ) 

265 

266 melds = [] 

267 for item in self.melds: 

268 melds.append( 

269 "{}".format(TilesConverter.to_one_line_string(item.tiles, print_aka_dora=self.table.has_aka_dora)) 

270 ) 

271 

272 if melds: 

273 hand_string += " [{}]".format(", ".join(melds)) 

274 

275 return hand_string 

276 

277 # FIXME: remove this method and cleanup tests 

278 def formal_riichi_conditions(self): 

279 return all( 

280 [ 

281 self.in_tempai, 

282 not self.in_riichi, 

283 not self.is_open_hand, 

284 self.scores >= 1000, 

285 self.table.count_of_remaining_tiles > 4, 

286 ] 

287 ) 

288 

289 @property 

290 def closed_hand(self): 

291 tiles = self.tiles[:] 

292 return [item for item in tiles if item not in self.meld_tiles] 

293 

294 

295class EnemyPlayer(PlayerInterface): 

296 # array of tiles in 34 tile format 

297 safe_tiles = None 

298 # tiles that were discarded in the current "step" 

299 # so, for example kamicha discard will be a safe tile for all players 

300 temporary_safe_tiles = None 

301 

302 riichi_tile_136 = None 

303 

304 def erase_state(self): 

305 super().erase_state() 

306 

307 self.safe_tiles = [] 

308 self.temporary_safe_tiles = [] 

309 self.riichi_tile_136 = None 

310 

311 def add_discarded_tile(self, tile: Tile): 

312 super().add_discarded_tile(tile) 

313 

314 tile = tile.value // 4 

315 if tile not in self.safe_tiles: 

316 self.safe_tiles.append(tile) 

317 

318 # erase temporary furiten after tile draw 

319 self.temporary_safe_tiles = [] 

320 affected_players = [1, 2, 3] 

321 affected_players.remove(self.seat) 

322 

323 # temporary furiten, for one "step" 

324 for x in affected_players: 

325 if tile not in self.table.get_player(x).temporary_safe_tiles: 

326 self.table.get_player(x).temporary_safe_tiles.append(tile) 

327 

328 @property 

329 def all_safe_tiles(self): 

330 return list(set(self.temporary_safe_tiles + self.safe_tiles))