def test_is_kokushi_musou_agari(self): agari = Agari() tiles = self._string_to_136_array(sou='19', pin='19', man='199', honors='1234567') self.assertTrue(agari.is_agari(self._to_34_array(tiles))) tiles = self._string_to_136_array(sou='19', pin='19', man='19', honors='11234567') self.assertTrue(agari.is_agari(self._to_34_array(tiles))) tiles = self._string_to_136_array(sou='19', pin='19', man='19', honors='12345677') self.assertTrue(agari.is_agari(self._to_34_array(tiles))) tiles = self._string_to_136_array(sou='129', pin='19', man='19', honors='1234567') self.assertFalse(agari.is_agari(self._to_34_array(tiles))) tiles = self._string_to_136_array(sou='19', pin='19', man='19', honors='11134567') self.assertFalse(agari.is_agari(self._to_34_array(tiles)))
def test_is_not_agari(self): agari = Agari() tiles = self._string_to_136_array(sou='123456789', pin='12345') self.assertFalse(agari.is_agari(self._to_34_array(tiles))) tiles = self._string_to_136_array(sou='111222444', pin='11145') self.assertFalse(agari.is_agari(self._to_34_array(tiles))) tiles = self._string_to_136_array(sou='11122233356888') self.assertFalse(agari.is_agari(self._to_34_array(tiles)))
def test_is_chitoitsu_agari(self): agari = Agari() tiles = self._string_to_136_array(sou='1133557799', pin='1199') self.assertTrue(agari.is_agari(self._to_34_array(tiles))) tiles = self._string_to_136_array(sou='2244', pin='1199', man='11', honors='2277') self.assertTrue(agari.is_agari(self._to_34_array(tiles))) tiles = self._string_to_136_array(man='11223344556677') self.assertTrue(agari.is_agari(self._to_34_array(tiles)))
def test_is_agari_and_open_hand(self): agari = Agari() tiles = self._string_to_136_array(sou='23455567', pin='222', man='345') open_sets = [ self._string_to_open_34_set(man='345'), self._string_to_open_34_set(sou='555') ] self.assertFalse(agari.is_agari(self._to_34_array(tiles), open_sets))
def test_is_agari(self): agari = Agari() tiles = self._string_to_136_array(sou='123456789', pin='123', man='33') self.assertTrue(agari.is_agari(self._to_34_array(tiles))) tiles = self._string_to_136_array(sou='123456789', pin='11123') self.assertTrue(agari.is_agari(self._to_34_array(tiles))) tiles = self._string_to_136_array(sou='123456789', honors='11777') self.assertTrue(agari.is_agari(self._to_34_array(tiles))) tiles = self._string_to_136_array(sou='12345556778899') self.assertTrue(agari.is_agari(self._to_34_array(tiles))) tiles = self._string_to_136_array(sou='11123456788999') self.assertTrue(agari.is_agari(self._to_34_array(tiles))) tiles = self._string_to_136_array(sou='233334', pin='789', man='345', honors='55') self.assertTrue(agari.is_agari(self._to_34_array(tiles)))
def is_waiting(hand:list, open_sets:list, wall_tiles:list) ->dict: """ :param hand: list of hand tiles in 136-tile format :param open_sets: "list of list" of open sets tiles in 136-tile format :param wall_tiles: list of wall tiles in 136-tile format :return {} if not waiting; {tile1: yaku_result1, tile2: yaku_result2, ...} if waiting ------------------------------ Tile in 136 format (34 format) 1 man: 0, 1, 2, 3 (0) 2 man: 4, 5, 6, 7 (1) 3 man: 8, 9, 10, 11 (2) 4 man: 12, 13, 14, 15 (3) 5 man: 16, 17, 18, 19 (4) 6 man: 20, 21, 22, 23 (5) 7 man: 24, 25, 26, 27 (6) 8 man: 28, 29, 30, 31 (7) 9 man: 32, 33, 34, 35 (8) 1 suo: 36, 37, 38, 39 (9) 2 suo: 40, 41, 42, 43 (10) 3 suo: 44, 45, 46, 47 (11) 4 suo: 48, 49, 50, 51 (12) 5 suo: 52, 53, 54, 55 (13) 6 suo: 56, 57, 58, 59 (14) 7 suo: 60, 61, 62, 63 (15) 8 suo: 64, 65, 66, 67 (16) 9 suo: 68, 69, 70, 71 (17) 1 pin: 72, 73, 74, 75 (18) 2 pin, 76, 77, 78, 79 (19) 3 pin: 80, 81, 82, 83 (20) 4 pin: 84, 85, 86, 87 (21) 5 pin: 88, 89, 90, 91 (22) 6 pin: 92, 93, 94, 95 (23) 7 pin: 96, 97, 98, 99 (24) 8 pin: 100, 101, 102, 103 (25) 9 pin: 104, 105, 106, 107 (26) East: 108, 109, 110, 111 (27) South: 112, 113, 114, 115 (28) West: 116, 117, 118, 119 (29) North: 120, 121, 122, 123 (30) White: 124, 125, 126, 127 (31) Green: 128, 129, 130, 131 (32) Red: 132, 133, 134, 135 (33) """ agari = Agari() check_waiting = CheckWaiting() waiting_result = {} for tile in wall_tiles: open_sets_flattened = [v for open_set in open_sets for v in open_set] completed_hand = hand + open_sets_flattened + [tile] hand_34_array = TilesConverter.to_34_array(completed_hand) melds_34_tiles = [TilesConverter.to_34_tiles(meld) for meld in open_sets] # check whether this tile is a winning tile is_waiting = agari.is_agari(hand_34_array, melds_34_tiles) # if it is a winning tile, append the completed hand result # TODO: we need to consider other params as well, such as riichi and tenhou, etc if is_waiting: result = check_waiting.check(hand, tile, open_sets=melds_34_tiles) assert result['error']==None, "tile {} is not the win tile!".format(tile) waiting_result[tile] = result return waiting_result
class ExtractFeatures(object): """ This class extracts features to predict waiting status and waiting tiles. """ def __init__(self): # ADD: to determine whether it is waiting or not self.agari = Agari() # initially meld sets is empty self.meld_sets = [[],[],[],[]] # four players self.meld_discarded_tiles = [[],[],[],[]] # four players def get_is_waiting_features(self, table): """ Get features from current table state. : revealed tiles : table.revealed_tiles, list of tile occurrence (0-4), of fixed length 34. :tiles of player*: table.players[0].tiles, list of tile number (0-135) : meld sets : table.players[0].melds, list of `Meld` Meld has three attributes: opened: True/False type: chi/pon/kan tiles: list of tile number (0-135) :discarded tiles : table.players[0].discards, list of `Tile` Tile has attribute: value: tile number (0-135) :dora indicators : table.dora_indicators, list of tile number : is dora : table.is_dora(tile number), tile number (0-135) : closed hand*: table.players[0].closed_hand, list of tile number (0-135) *Note: these info (tiles of player, closed hand) is only visible to self player, we can not see hands of other players anyway. """ # If we need one more tile to complete our hand, and this specific tile # we want is known to be within the wall tiles, then the hand is waiting. current_hand = TilesConverter.to_34_array(table.players[0].tiles) winning_tiles = [] for n in range(34): if table.revealed_tiles[n]<4: completed_hand = current_hand[:] completed_hand[n] += 1 can_be_waiting = self.agari.is_agari(completed_hand) if can_be_waiting: winning_tiles.append(n) # n is the winning tile we want # If there is at least one winning tile available in the wall, the hand # is waiting. is_waiting = 0 if len(winning_tiles)>0: is_waiting = 1 # Discarded tiles can be seen by everybody discarded_tiles = [d.value for d in table.players[0].discards] discarded_tiles_136_code = TilesConverter.tiles_to_136_code(discarded_tiles) discarded_tiles_136_code_str = ",".join([str(d) for d in discarded_tiles_136_code]) # Meld sets (both open and concealed) are also visible to everybody meld_sets = [mt.tiles for mt in table.players[0].melds] #open_melds_sets = [mt.tiles for mt in table.players[0].melds if mt.opened] if len(meld_sets)>0: meld_tiles = reduce(lambda x,y:x+y, meld_sets) else: meld_tiles = [] meld_tiles_136_code = TilesConverter.tiles_to_136_code(meld_tiles) meld_tiles_136_code_str = ",".join([str(m) for m in meld_tiles_136_code]) number_of_revealed_melds = len(meld_sets) number_of_discarded_tiles = len(discarded_tiles) # discarded_tiles_34 = TilesConverter.to_34_array(discarded_tiles) # discarded_tiles_34_str = ",".join([str(d) for d in discarded_tiles_34]) # discarded_tiles_37 = TilesConverter.to_37_array(discarded_tiles) # discarded_tiles_37_str = ",".join([str(d) for d in discarded_tiles_37]) # We don't try to estimate the probability of waiting until we # have sufficient information. Therefore we need at least two # discarded tiles in order to do the estimation. if len(discarded_tiles)>1: dora_discarded = [d for d in discarded_tiles if table.is_dora(d) ] dora_discarded_34 = TilesConverter.to_34_array(dora_discarded) dora_discarded_34_str = ",".join([str(d) for d in dora_discarded_34]) last1_discarded_tile = discarded_tiles[-1] # the last discarded tile last1_discarded_tile_37 = TilesConverter.to_37_tiles([last1_discarded_tile])[0] last2_discarded_tile = discarded_tiles[-2] # the second last discarded tile last2_discarded_tile_37 = TilesConverter.to_37_tiles([last2_discarded_tile])[0] string_to_save = "{},{},{},{},{},{},{},{},{},{}".format( is_waiting, number_of_revealed_melds, # after one_hot_encode, will become 5 number_of_discarded_tiles, # after one_hot_encode, will become 19 number_of_revealed_melds, # after one_hot_encode, will become 5 (repeated) last1_discarded_tile_37, # after one_hot_encode, will become 37 last2_discarded_tile_37, # after one_hot_encode, will become 37 last1_discarded_tile_37, # after one_hot_encode, will become 37 (repeated) discarded_tiles_136_code_str, # discarded tiles meld_tiles_136_code_str, # melded tiles dora_discarded_34_str # discarded doras ) return string_to_save return None def tile_136_to_37(self, tile): """ convert a tile in 136 format to 37 format """ tile //= 4 if tile==16: return 34 elif tile==52: return 35 elif tile==88: return 36 else: return tile def tile_136_to_34(self, tile): """ convert a tile in 136 format to 34 format """ tile //= 4 return tile # TODO: unfinished function def get_waiting_tiles_features(self, table): """ Get features from current table state. : revealed tiles : table.revealed_tiles, list of tile occurrence (0-4), of fixed length 34. :tiles of player*: table.players[0].tiles, list of tile number (0-135) : meld sets : table.players[0].melds, list of `Meld` Meld has three attributes: opened: True/False type: chi/pon/kan tiles: list of tile number (0-135) :discarded tiles : table.players[0].discards, list of `Tile` Tile has attribute: value: tile number (0-135) :dora indicators : table.dora_indicators, list of tile number : is dora : table.is_dora(tile number), tile number (0-135) : closed hand*: table.players[0].closed_hand, list of tile number (0-135) *Note: these info (tiles of player, closed hand) is only visible to self player, we can not see hands of other players anyway. """ dora_tiles = [d for d in range(136) if table.is_dora(d) ] table_turns = min([len(table.players[m].discards)+1 for m in range(4)]) table_info = "{},{},{},{},{},{},{},{},{},{}".format( table.count_of_honba_sticks, table.count_of_remaining_tiles, table.count_of_riichi_sticks, table.round_number, table.round_wind, table_turns, table.dealer_seat, table.dora_indicators, dora_tiles, table.revealed_tiles ) player_info = "" for m in range(4): # There are four players player = table.players[m] ''' player.discards player.closed_hand player.dealer_seat player.in_riichi #player.in_defence_mode #player.in_tempai player.is_dealer player.is_open_hand player.last_draw player.melds player.name player.position player.rank player.scores player.seat player.tiles player.uma ''' # Discarded tiles can be seen by everybody discarded_tiles = [(d.value,1) if d.is_tsumogiri else (d.value,0) for d in player.discards] discarded_kinds = [d[0]//4 for d in discarded_tiles] # If we need one more tile to complete our hand, and this specific tile # we want is known to be within the wall tiles, then the hand is waiting. current_hand = TilesConverter.to_34_array(player.tiles) # if m==1: # print(TilesConverter.to_one_line_string(player.tiles),"~~") winning_tiles = [] for n in range(34): # if there is no tile available in the wall, or the tile we need # to complete a hand has been discarded previously, we do not wait # for this tile. But we can still wait for tsumo?? if (table.revealed_tiles[n]<4) and (n not in set(discarded_kinds)): completed_hand = current_hand[:] completed_hand[n] += 1 can_be_waiting = self.agari.is_agari(completed_hand) # if completed_hand==[0,2,0,0,1,1,1,0,0, 0,0,0,0,0,1,1,1,0, 0,0,0,3,0,1,1,1,0, 0,0,0,0,0,0,0]: # sys.exit("bingo!") # print(can_be_waiting, completed_hand, "~~!!") if can_be_waiting: winning_tiles.append(n) # n is the winning tile we want # Meld sets (both open and concealed) are also visible to everybody meld_sets = [mt.tiles for mt in player.melds] # meld_sets_str = [TilesConverter.to_one_line_string(ms) for ms in meld_sets] # meld_types = [mt.type for mt in player.melds] meld_open = [mt.opened for mt in player.melds] if meld_sets != self.meld_sets[m]: self.meld_discarded_tiles[m].append(discarded_tiles[-1]) self.meld_sets[m] = meld_sets # We don't try to estimate the probability of waiting until we # have sufficient information. Therefore we need at least two # discarded tiles in order to do the estimation. # dora_tiles = [d for d in range(136) if table.is_dora(d) ] # dora_kinds = list(set([dt//4 for dt in dora_tiles])) # dora_kind_discarded = [discarded_kinds.count(d) for d in dora_kinds] melds = [(meld_sets[k], 1 if meld_open[k] else 0, self.meld_discarded_tiles[m][k]) for k in range(len(meld_sets))] string_to_save = "{},{},{},{},{},{},{},{},{},{},{},{},{}".format( winning_tiles, discarded_tiles, #player.discards #player.closed_hand, # this info is invisible player.dealer_seat, 1 if player.in_riichi else 0, #player.in_defence_mode #player.in_tempai 1 if player.is_dealer else 0, 1 if player.is_open_hand else 0, #player.last_draw, # this info is invisible # meld_sets, # meld_open, # self.meld_discarded_tiles[m], melds, player.name if player.name else -1, player.position if player.position else -1, player.rank if player.rank else -1, player.scores if player.scores else -1, player.seat if player.seat else -1, #player.tiles, # this info is invisible player.uma if player.uma else -1 ) player_info += string_to_save + ";" if player_info=="": return None else: return table_info + ";" + player_info def get_scores_features(self, table): """ Get features from current table state. : revealed tiles : table.revealed_tiles, list of tile occurrence (0-4), of fixed length 34. :tiles of player*: table.players[0].tiles, list of tile number (0-135) : meld sets : table.players[0].melds, list of `Meld` Meld has three attributes: opened: True/False type: chi/pon/kan tiles: list of tile number (0-135) :discarded tiles : table.players[0].discards, list of `Tile` Tile has attribute: value: tile number (0-135) :dora indicators : table.dora_indicators, list of tile number : is dora : table.is_dora(tile number), tile number (0-135) : closed hand*: table.players[0].closed_hand, list of tile number (0-135) *Note: these info (tiles of player, closed hand) is only visible to self player, we can not see hands of other players anyway. """ dora_tiles = [d for d in range(136) if table.is_dora(d) ] table_turns = min([len(table.players[m].discards)+1 for m in range(4)]) table_info = "{},{},{},{},{},{},{},{},{},{}".format( table.count_of_honba_sticks, table.count_of_remaining_tiles, table.count_of_riichi_sticks, table.round_number, table.round_wind, table_turns, table.dealer_seat, table.dora_indicators, dora_tiles, # 0-136 table.revealed_tiles ) player_info = "" for m in range(4): # There are four players player = table.players[m] ''' player.discards player.closed_hand player.dealer_seat player.in_riichi #player.in_defence_mode #player.in_tempai player.is_dealer player.is_open_hand player.last_draw player.melds player.name player.position player.rank player.scores player.seat player.tiles player.uma ''' # Discarded tiles can be seen by everybody discarded_tiles = [(d.value,1) if d.is_tsumogiri else (d.value,0) for d in player.discards] discarded_kinds = [d[0]//4 for d in discarded_tiles] # If we need one more tile to complete our hand, and this specific tile # we want is known to be within the wall tiles, then the hand is waiting. current_hand = TilesConverter.to_34_array(player.tiles) winning_tiles = [] for n in range(34): # if there is no tile available in the wall, or the tile we need # to complete a hand has been discarded previously, we do not wait # for this tile. But we can still wait for tsumo?? if (table.revealed_tiles[n]<4) and (n not in set(discarded_kinds)): completed_hand = current_hand[:] completed_hand[n] += 1 can_be_waiting = self.agari.is_agari(completed_hand) if can_be_waiting: winning_tiles.append(n) # n is the winning tile we want # hand = [8, 11, 43, 44, 48, 51, 58, 79, 82, 87, 88, 92, 98] #+ [55] # hand34 = TilesConverter.to_34_array(hand) # if current_hand==hand34: # print("player {} tiles: {}".format(m, player.tiles)) # print("player {} winning tiles: {}".format(m, winning_tiles)) # Meld sets (both open and concealed) are also visible to everybody meld_sets = [mt.tiles for mt in player.melds] meld_open = [mt.opened for mt in player.melds] if meld_sets != self.meld_sets[m]: self.meld_discarded_tiles[m].append(discarded_tiles[-1]) self.meld_sets[m] = meld_sets # Example: melds=[([12, 18, 20], 1, (108, 0)), ([40, 45, 49], 1, (7, 0))] melds = [(meld_sets[k], 1 if meld_open[k] else 0, self.meld_discarded_tiles[m][k]) for k in range(len(meld_sets))] string_to_save = "{},{},{},{},{},{},{},{},{},{},{},{},{}".format( winning_tiles, discarded_tiles, #player.discards #player.closed_hand, # this info is invisible player.dealer_seat, 1 if player.in_riichi else 0, #player.in_defence_mode #player.in_tempai 1 if player.is_dealer else 0, 1 if player.is_open_hand else 0, #player.last_draw, # this info is invisible melds, player.name if player.name else -1, player.position if player.position else -1, player.rank if player.rank else -1, player.scores if player.scores else -1, player.seat if player.seat else -1, #player.tiles, # this info is invisible player.uma if player.uma else -1 ) player_info += string_to_save + ";" if player_info=="": return None else: return table_info + ";" + player_info def get_one_player_discards_features(self, table): """ Get features from current table state. : revealed tiles : table.revealed_tiles, list of tile occurrence (0-4), of fixed length 34. :tiles of player*: table.players[0].tiles, list of tile number (0-135) : meld sets : table.players[0].melds, list of `Meld` Meld has three attributes: opened: True/False type: chi/pon/kan tiles: list of tile number (0-135) :discarded tiles : table.players[0].discards, list of `Tile` Tile has attribute: value: tile number (0-135) :dora indicators : table.dora_indicators, list of tile number : is dora : table.is_dora(tile number), tile number (0-135) : closed hand*: table.players[0].closed_hand, list of tile number (0-135) *Note: these info (tiles of player, closed hand) is only visible to self player, we can not see hands of other players anyway. """ dora_tiles = [d for d in range(136) if table.is_dora(d) ] table_turns = min([len(table.players[m].discards)+1 for m in range(4)]) table_info = "{},{},{},{},{},{},{},{},{},{}".format( table.count_of_honba_sticks, table.count_of_remaining_tiles, table.count_of_riichi_sticks, table.round_number, table.round_wind, table_turns, table.dealer_seat, table.dora_indicators, dora_tiles, # 0-136 table.revealed_tiles ) player_info = "" for m in range(4): # There are four players player = table.players[m] ''' player.discards player.closed_hand player.dealer_seat player.in_riichi #player.in_defence_mode #player.in_tempai player.is_dealer player.is_open_hand player.last_draw player.melds player.name player.position player.rank player.scores player.seat player.tiles player.uma ''' # Discarded tiles can be seen by everybody discarded_tiles = [(d.value,1) if d.is_tsumogiri else (d.value,0) for d in player.discards] discarded_kinds = [d[0]//4 for d in discarded_tiles] # If we need one more tile to complete our hand, and this specific tile # we want is known to be within the wall tiles, then the hand is waiting. current_hand = TilesConverter.to_34_array(player.tiles) winning_tiles = [] for n in range(34): # if there is no tile available in the wall, or the tile we need # to complete a hand has been discarded previously, we do not wait # for this tile. But we can still wait for tsumo?? if (table.revealed_tiles[n]<4) and (n not in set(discarded_kinds)): completed_hand = current_hand[:] completed_hand[n] += 1 can_be_waiting = self.agari.is_agari(completed_hand) if can_be_waiting: winning_tiles.append(n) # n is the winning tile we want # hand = [8, 11, 43, 44, 48, 51, 58, 79, 82, 87, 88, 92, 98] #+ [55] # hand34 = TilesConverter.to_34_array(hand) # if current_hand==hand34: # print("player {} tiles: {}".format(m, player.tiles)) # print("player {} winning tiles: {}".format(m, winning_tiles)) # Meld sets (both open and concealed) are also visible to everybody meld_sets = [mt.tiles for mt in player.melds] meld_open = [mt.opened for mt in player.melds] if meld_sets != self.meld_sets[m]: self.meld_discarded_tiles[m].append(discarded_tiles[-1]) self.meld_sets[m] = meld_sets # Example: melds=[([12, 18, 20], 1, (108, 0)), ([40, 45, 49], 1, (7, 0))] melds = [(meld_sets[k], 1 if meld_open[k] else 0, self.meld_discarded_tiles[m][k]) for k in range(len(meld_sets))] string_to_save = "{},{},{},{},{},{},{},{},{},{},{},{},{},{},{}".format( winning_tiles, discarded_tiles, #player.discards # ADD: we need closed hand info to tain one player mahjong here player.closed_hand, # this info is invisible player.dealer_seat, 1 if player.in_riichi else 0, #player.in_defence_mode #player.in_tempai 1 if player.is_dealer else 0, 1 if player.is_open_hand else 0, # ADD: we might not need this info, but better keep it in case we need it later. player.last_draw, # this info is invisible melds, player.name if player.name else -1, player.position if player.position else -1, player.rank if player.rank else -1, player.scores if player.scores else -1, player.seat if player.seat else -1, #player.tiles, # this info is invisible player.uma if player.uma else -1 ) player_info += string_to_save + ";" if player_info=="": return None else: return table_info + ";" + player_info def get_stealing_features(self, table): """ Get features from current table state. : revealed tiles : table.revealed_tiles, list of tile occurrence (0-4), of fixed length 34. :tiles of player*: table.players[0].tiles, list of tile number (0-135) : meld sets : table.players[0].melds, list of `Meld` Meld has three attributes: opened: True/False type: chi/pon/kan tiles: list of tile number (0-135) :discarded tiles : table.players[0].discards, list of `Tile` Tile has attribute: value: tile number (0-135) :dora indicators : table.dora_indicators, list of tile number : is dora : table.is_dora(tile number), tile number (0-135) : closed hand*: table.players[0].closed_hand, list of tile number (0-135) *Note: these info (tiles of player, closed hand) is only visible to self player, we can not see hands of other players anyway. """ dora_tiles = [d for d in range(136) if table.is_dora(d) ] table_turns = min([len(table.players[m].discards)+1 for m in range(4)]) table_info = "{},{},{},{},{},{},{},{},{},{}".format( table.count_of_honba_sticks, table.count_of_remaining_tiles, table.count_of_riichi_sticks, table.round_number, table.round_wind, table_turns, table.dealer_seat, table.dora_indicators, dora_tiles, # 0-136 table.revealed_tiles ) player_info = "" for m in range(4): # There are four players player = table.players[m] ''' player.discards player.closed_hand player.dealer_seat player.in_riichi #player.in_defence_mode #player.in_tempai player.is_dealer player.is_open_hand player.last_draw player.melds player.name player.position player.rank player.scores player.seat player.tiles player.uma ''' # Discarded tiles can be seen by everybody discarded_tiles = [(d.value,1) if d.is_tsumogiri else (d.value,0) for d in player.discards] discarded_kinds = [d[0]//4 for d in discarded_tiles] # If we need one more tile to complete our hand, and this specific tile # we want is known to be within the wall tiles, then the hand is waiting. current_hand = TilesConverter.to_34_array(player.tiles) winning_tiles = [] for n in range(34): # if there is no tile available in the wall, or the tile we need # to complete a hand has been discarded previously, we do not wait # for this tile. But we can still wait for tsumo?? if (table.revealed_tiles[n]<4) and (n not in set(discarded_kinds)): completed_hand = current_hand[:] completed_hand[n] += 1 can_be_waiting = self.agari.is_agari(completed_hand) if can_be_waiting: winning_tiles.append(n) # n is the winning tile we want # hand = [8, 11, 43, 44, 48, 51, 58, 79, 82, 87, 88, 92, 98] #+ [55] # hand34 = TilesConverter.to_34_array(hand) # if current_hand==hand34: # print("player {} tiles: {}".format(m, player.tiles)) # print("player {} winning tiles: {}".format(m, winning_tiles)) # Meld sets (both open and concealed) are also visible to everybody meld_sets = [mt.tiles for mt in player.melds] meld_open = [mt.opened for mt in player.melds] if meld_sets != self.meld_sets[m]: self.meld_discarded_tiles[m].append(discarded_tiles[-1]) self.meld_sets[m] = meld_sets # Example: melds=[([12, 18, 20], 1, (108, 0)), ([40, 45, 49], 1, (7, 0))] melds = [(meld_sets[k], 1 if meld_open[k] else 0, self.meld_discarded_tiles[m][k]) for k in range(len(meld_sets))] string_to_save = "{},{},{},{},{},{},{},{},{},{},{},{},{},{},{}".format( winning_tiles, discarded_tiles, #player.discards player.closed_hand, # this info is invisible player.dealer_seat, 1 if player.in_riichi else 0, #player.in_defence_mode #player.in_tempai 1 if player.is_dealer else 0, 1 if player.is_open_hand else 0, # ADD: we might not need this info, but better keep it in case we need it later. player.last_draw, # this info is invisible melds, player.name if player.name else -1, player.position if player.position else -1, player.rank if player.rank else -1, player.scores if player.scores else -1, player.seat if player.seat else -1, #player.tiles, # this info is invisible. We don't need this, b/c player_closed_hand plus melds already give us the info player.uma if player.uma else -1 ) player_info += string_to_save + ";" if player_info=="": return None else: return table_info + ";" + player_info
def estimate_hand_value(self, tiles, win_tile, is_tsumo=False, is_riichi=False, is_dealer=False, is_ippatsu=False, is_rinshan=False, is_chankan=False, is_haitei=False, is_houtei=False, is_daburu_riichi=False, is_nagashi_mangan=False, is_tenhou=False, is_renhou=False, is_chiihou=False, open_sets=None, dora_indicators=None, called_kan_indices=None, player_wind=None, round_wind=None): """ :param tiles: array with 14 tiles in 136-tile format :param win_tile: tile that caused win (ron or tsumo) :param is_tsumo: :param is_riichi: :param is_dealer: :param is_ippatsu: :param is_rinshan: :param is_chankan: :param is_haitei: :param is_houtei: :param is_tenhou: :param is_renhou: :param is_chiihou: :param is_daburu_riichi: :param is_nagashi_mangan: :param open_sets: array of array with open sets in 136-tile format :param dora_indicators: array of tiles in 136-tile format :param called_kan_indices: array of tiles in 136-tile format :param player_wind: index of player wind :param round_wind: index of round wind :return: The dictionary with hand cost or error response {"cost": {'main': 1000, 'additional': 0}, "han": 1, "fu": 30, "error": None, "hand_yaku": []} {"cost": None, "han": 0, "fu": 0, "error": "Hand is not valid", "hand_yaku": []} """ if not open_sets: open_sets = [] else: # cast 136 format to 34 format for item in open_sets: item[0] //= 4 item[1] //= 4 item[2] //= 4 is_open_hand = len(open_sets) > 0 if not dora_indicators: dora_indicators = [] kan_indices_136 = [] if not called_kan_indices: called_kan_indices = [] else: kan_indices_136 = called_kan_indices called_kan_indices = [x // 4 for x in called_kan_indices] agari = Agari() cost = None error = None hand_yaku = [] han = 0 fu = 0 def return_response(): return { 'cost': cost, 'error': error, 'han': han, 'fu': fu, 'hand_yaku': hand_yaku } # special situation if is_nagashi_mangan: hand_yaku.append(yaku.nagashi_mangan) fu = 30 han = yaku.nagashi_mangan.han['closed'] cost = self.calculate_scores(han, fu, is_tsumo, is_dealer) return return_response() if win_tile not in tiles: error = "Win tile not in the hand" return return_response() if is_riichi and is_open_hand: error = "Riichi can't be declared with open hand" return return_response() if is_ippatsu and is_open_hand: error = "Ippatsu can't be declared with open hand" return return_response() if is_ippatsu and not is_riichi and not is_daburu_riichi: error = "Ippatsu can't be declared without riichi" return return_response() tiles_34 = TilesConverter.to_34_array(tiles) divider = HandDivider() if not agari.is_agari(tiles_34): error = 'Hand is not winning' return return_response() hand_options = divider.divide_hand(tiles_34, open_sets, called_kan_indices) calculated_hands = [] for hand in hand_options: cost = None error = None hand_yaku = [] han = 0 fu = 0 if is_tsumo or is_open_hand: fu += 20 else: fu += 30 pon_sets = [x for x in hand if is_pon(x)] chi_sets = [x for x in hand if is_chi(x)] additional_fu = self.calculate_additional_fu( win_tile, hand, is_tsumo, player_wind, round_wind, open_sets, called_kan_indices) if additional_fu == 0 and len(chi_sets) == 4: """ - A hand without pon and kan sets, so it should contains all sequences and a pair - The pair should be not valued - The waiting must be an open wait (on 2 different tiles) - Hand should be closed """ if is_open_hand: fu += 2 is_pinfu = False else: is_pinfu = True else: fu += additional_fu is_pinfu = False if is_tsumo: if not is_open_hand: hand_yaku.append(yaku.tsumo) # pinfu + tsumo always is 20 fu if not is_pinfu: fu += 2 if is_pinfu: hand_yaku.append(yaku.pinfu) is_chitoitsu = self.is_chitoitsu(hand) # let's skip hand that looks like chitoitsu, but it contains open sets if is_chitoitsu and is_open_hand: continue if is_chitoitsu: hand_yaku.append(yaku.chiitoitsu) is_tanyao = self.is_tanyao(hand) if is_open_hand and not settings.OPEN_TANYAO: is_tanyao = False if is_tanyao: hand_yaku.append(yaku.tanyao) if is_riichi and not is_daburu_riichi: hand_yaku.append(yaku.riichi) if is_daburu_riichi: hand_yaku.append(yaku.daburu_riichi) if is_ippatsu: hand_yaku.append(yaku.ippatsu) if is_rinshan: hand_yaku.append(yaku.rinshan) if is_chankan: hand_yaku.append(yaku.chankan) if is_haitei: hand_yaku.append(yaku.haitei) if is_houtei: hand_yaku.append(yaku.houtei) if is_renhou: hand_yaku.append(yaku.renhou) if is_tenhou: hand_yaku.append(yaku.tenhou) if is_chiihou: hand_yaku.append(yaku.chiihou) if self.is_honitsu(hand): hand_yaku.append(yaku.honitsu) if self.is_chinitsu(hand): hand_yaku.append(yaku.chinitsu) if self.is_tsuisou(hand): hand_yaku.append(yaku.tsuisou) if self.is_honroto(hand): hand_yaku.append(yaku.honroto) if self.is_chinroto(hand): hand_yaku.append(yaku.chinroto) # small optimization, try to detect yaku with chi required sets only if we have chi sets in hand if len(chi_sets): if self.is_chanta(hand): hand_yaku.append(yaku.chanta) if self.is_junchan(hand): hand_yaku.append(yaku.junchan) if self.is_ittsu(hand): hand_yaku.append(yaku.ittsu) if not is_open_hand: if self.is_ryanpeiko(hand): hand_yaku.append(yaku.ryanpeiko) elif self.is_iipeiko(hand): hand_yaku.append(yaku.iipeiko) if self.is_sanshoku(hand): hand_yaku.append(yaku.sanshoku) # small optimization, try to detect yaku with pon required sets only if we have pon sets in hand if len(pon_sets): if self.is_toitoi(hand): hand_yaku.append(yaku.toitoi) if self.is_sanankou(win_tile, hand, open_sets, is_tsumo): hand_yaku.append(yaku.sanankou) if self.is_sanshoku_douko(hand): hand_yaku.append(yaku.sanshoku_douko) if self.is_shosangen(hand): hand_yaku.append(yaku.shosangen) if self.is_haku(hand): hand_yaku.append(yaku.haku) if self.is_hatsu(hand): hand_yaku.append(yaku.hatsu) if self.is_chun(hand): hand_yaku.append(yaku.hatsu) if self.is_east(hand, player_wind, round_wind): if player_wind == EAST: hand_yaku.append(yaku.yakuhai_place) if round_wind == EAST: hand_yaku.append(yaku.yakuhai_round) if self.is_south(hand, player_wind, round_wind): if player_wind == SOUTH: hand_yaku.append(yaku.yakuhai_place) if round_wind == SOUTH: hand_yaku.append(yaku.yakuhai_round) if self.is_west(hand, player_wind, round_wind): if player_wind == WEST: hand_yaku.append(yaku.yakuhai_place) if round_wind == WEST: hand_yaku.append(yaku.yakuhai_round) if self.is_north(hand, player_wind, round_wind): if player_wind == NORTH: hand_yaku.append(yaku.yakuhai_place) if round_wind == NORTH: hand_yaku.append(yaku.yakuhai_round) if self.is_daisangen(hand): hand_yaku.append(yaku.daisangen) if self.is_shosuushi(hand): hand_yaku.append(yaku.shosuushi) if self.is_daisuushi(hand): hand_yaku.append(yaku.daisuushi) if self.is_ryuisou(hand): hand_yaku.append(yaku.ryuisou) if not is_open_hand and self.is_chuuren_poutou(hand): if tiles_34[win_tile // 4] == 2: hand_yaku.append(yaku.daburu_chuuren_poutou) else: hand_yaku.append(yaku.chuuren_poutou) if not is_open_hand and self.is_suuankou( win_tile, hand, is_tsumo): if tiles_34[win_tile // 4] == 2: hand_yaku.append(yaku.suuankou_tanki) else: hand_yaku.append(yaku.suuankou) if self.is_sankantsu(hand, called_kan_indices): hand_yaku.append(yaku.sankantsu) if self.is_suukantsu(hand, called_kan_indices): hand_yaku.append(yaku.suukantsu) # chitoitsu is always 25 fu if is_chitoitsu: fu = 25 tiles_for_dora = tiles + kan_indices_136 count_of_dora = 0 count_of_aka_dora = 0 for tile in tiles_for_dora: count_of_dora += plus_dora(tile, dora_indicators) for tile in tiles_for_dora: if is_aka_dora(tile): count_of_aka_dora += 1 if count_of_dora: yaku_item = yaku.dora yaku_item.han['open'] = count_of_dora yaku_item.han['closed'] = count_of_dora hand_yaku.append(yaku_item) if count_of_aka_dora: yaku_item = yaku.aka_dora yaku_item.han['open'] = count_of_aka_dora yaku_item.han['closed'] = count_of_aka_dora hand_yaku.append(yaku_item) # yakuman is not connected with other yaku yakuman_list = [x for x in hand_yaku if x.is_yakuman] if yakuman_list: hand_yaku = yakuman_list # calculate han for item in hand_yaku: if is_open_hand and item.han['open']: han += item.han['open'] else: han += item.han['closed'] # round up # 22 -> 30 and etc. if fu != 25: fu = int(math.ceil(fu / 10.0)) * 10 if han == 0 or (han == 1 and fu < 30): error = 'Not valid han ({0}) and fu ({1})'.format(han, fu) cost = None else: cost = self.calculate_scores(han, fu, is_tsumo, is_dealer) calculated_hand = { 'cost': cost, 'error': error, 'hand_yaku': hand_yaku, 'han': han, 'fu': fu } calculated_hands.append(calculated_hand) # exception hand if not is_open_hand and self.is_kokushi(tiles_34): if tiles_34[win_tile // 4] == 2: han = yaku.daburu_kokushi.han['closed'] else: han = yaku.kokushi.han['closed'] fu = 0 cost = self.calculate_scores(han, fu, is_tsumo, is_dealer) calculated_hands.append({ 'cost': cost, 'error': None, 'hand_yaku': [yaku.kokushi], 'han': han, 'fu': fu }) # let's use cost for most expensive hand calculated_hands = sorted(calculated_hands, key=lambda x: (x['han'], x['fu']), reverse=True) calculated_hand = calculated_hands[0] cost = calculated_hand['cost'] error = calculated_hand['error'] hand_yaku = calculated_hand['hand_yaku'] han = calculated_hand['han'] fu = calculated_hand['fu'] return return_response()
class GameManager(object): """ Allow to play bots between each other To have a metrics how new version plays agains old versions """ tiles = [] dead_wall = [] clients = [] dora_indicators = [] dealer = None current_client = None round_number = 0 honba_sticks = 0 riichi_sticks = 0 _unique_dealers = 0 def __init__(self, clients): self.tiles = [] self.dead_wall = [] self.dora_indicators = [] self.clients = clients self._set_client_names() self.agari = Agari() self.finished_hand = FinishedHand() def init_game(self): """ Initial of the game. Clients random placement and dealer selection """ shuffle(self.clients, shuffle_seed) for i in range(0, len(self.clients)): self.clients[i].position = i dealer = randint(0, 3) self.set_dealer(dealer) for client in self.clients: client.player.scores = 25000 self._unique_dealers = 1 def init_round(self): # each round should have personal seed global seed_value seed_value = random() self.tiles = [i for i in range(0, 136)] # need to change random function in future shuffle(self.tiles, shuffle_seed) self.dead_wall = self._cut_tiles(14) self.dora_indicators.append(self.dead_wall[8]) for x in range(0, len(self.clients)): client = self.clients[x] # each client think that he is a player with position = 0 # so, we need to move dealer position for each client # and shift scores array client_dealer = self.dealer - x player_scores = deque([i.player.scores / 100 for i in self.clients]) player_scores.rotate(x * -1) player_scores = list(player_scores) client.table.init_round( self.round_number, self.honba_sticks, self.riichi_sticks, self.dora_indicators[0], client_dealer, player_scores ) # each player by rotation draw 4 tiles until they have 12 # after this each player draw one more tile # and this is will be their initial hand # we do it to make the tiles allocation in hands # more random for x in range(0, 3): for client in self.clients: client.player.tiles += self._cut_tiles(4) for client in self.clients: client.player.tiles += self._cut_tiles(1) client.init_hand(client.player.tiles) logger.info('Seed: {0}'.format(shuffle_seed())) logger.info('Dealer: {0}'.format(self.dealer)) logger.info('Wind: {0}. Riichi sticks: {1}. Honba sticks: {2}'.format( self._unique_dealers, self.riichi_sticks, self.honba_sticks )) logger.info('Players: {0}'.format(self.players_sorted_by_scores())) def play_round(self): continue_to_play = True while continue_to_play: client = self._get_current_client() in_tempai = client.player.in_tempai tile = self._cut_tiles(1)[0] # we don't need to add tile to the hand when we are in riichi if client.player.in_riichi: tiles = client.player.tiles + [tile] else: client.draw_tile(tile) tiles = client.player.tiles is_win = self.agari.is_agari(TilesConverter.to_34_array(tiles)) # win by tsumo after tile draw if is_win: result = self.process_the_end_of_the_round(tiles=client.player.tiles, win_tile=tile, winner=client, loser=None, is_tsumo=True) return result # if not in riichi, let's decide what tile to discard if not client.player.in_riichi: tile = client.discard_tile() in_tempai = client.player.in_tempai # after tile discard let's check all other players can they win or not # at this tile for other_client in self.clients: # there is no need to check the current client if other_client == client: continue # let's store other players discards other_client.enemy_discard(other_client.position - client.position, tile) # TODO support multiple ron if self.can_call_ron(other_client, tile): # the end of the round result = self.process_the_end_of_the_round(tiles=other_client.player.tiles, win_tile=tile, winner=other_client, loser=client, is_tsumo=False) return result # if there is no challenger to ron, let's check can we call riichi with tile discard or not if in_tempai and client.player.can_call_riichi(): self.call_riichi(client) self.current_client = self._move_position(self.current_client) # retake if not len(self.tiles): continue_to_play = False result = self.process_the_end_of_the_round([], 0, None, None, False) return result def play_game(self, total_results): """ :param total_results: a dictionary with keys as client ids :return: game results """ logger.info('The start of the game') logger.info('') is_game_end = False self.init_game() played_rounds = 0 while not is_game_end: self.init_round() result = self.play_round() is_game_end = result['is_game_end'] loser = result['loser'] winner = result['winner'] if loser: total_results[loser.id]['lose_rounds'] += 1 if winner: total_results[winner.id]['win_rounds'] += 1 for client in self.clients: if client.player.in_riichi: total_results[client.id]['riichi_rounds'] += 1 played_rounds += 1 self.recalculate_players_position() logger.info('Final Scores: {0}'.format(self.players_sorted_by_scores())) logger.info('The end of the game') return {'played_rounds': played_rounds} def recalculate_players_position(self): """ For players with same count of scores we need to set position based on their initial seat on the table """ temp_clients = sorted(self.clients, key=lambda x: x.player.scores, reverse=True) for i in range(0, len(temp_clients)): temp_client = temp_clients[i] for client in self.clients: if client.id == temp_client.id: client.player.position = i + 1 def can_call_ron(self, client, win_tile): if not client.player.in_tempai or not client.player.in_riichi: return False tiles = client.player.tiles is_ron = self.agari.is_agari(TilesConverter.to_34_array(tiles + [win_tile])) return is_ron def call_riichi(self, client): client.player.in_riichi = True client.player.scores -= 1000 self.riichi_sticks += 1 who_called_riichi = client.position for client in self.clients: client.enemy_riichi(who_called_riichi - client.position) logger.info('Riichi: {0} - 1,000'.format(client.player.name)) def set_dealer(self, dealer): self.dealer = dealer self._unique_dealers += 1 for x in range(0, len(self.clients)): client = self.clients[x] # each client think that he is a player with position = 0 # so, we need to move dealer position for each client # and shift scores array client.player.dealer_seat = dealer - x # first move should be dealer's move self.current_client = dealer def process_the_end_of_the_round(self, tiles, win_tile, winner, loser, is_tsumo): """ Increment a round number and do a scores calculations """ if winner: logger.info('{0}: {1} + {2}'.format( is_tsumo and 'Tsumo' or 'Ron', TilesConverter.to_one_line_string(tiles), TilesConverter.to_one_line_string([win_tile])), ) else: logger.info('Retake') is_game_end = False self.round_number += 1 if winner: hand_value = self.finished_hand.estimate_hand_value(tiles + [win_tile], win_tile, is_tsumo, winner.player.in_riichi, winner.player.is_dealer, False) if hand_value['cost']: hand_value = hand_value['cost']['main'] else: logger.error('Can\'t estimate a hand: {0}. Error: {1}'.format( TilesConverter.to_one_line_string(tiles + [win_tile]), hand_value['error'] )) hand_value = 1000 scores_to_pay = hand_value + self.honba_sticks * 300 riichi_bonus = self.riichi_sticks * 1000 self.riichi_sticks = 0 # if dealer won we need to increment honba sticks if winner.player.is_dealer: self.honba_sticks += 1 else: self.honba_sticks = 0 new_dealer = self._move_position(self.dealer) self.set_dealer(new_dealer) # win by ron if loser: win_amount = scores_to_pay + riichi_bonus winner.player.scores += win_amount loser.player.scores -= scores_to_pay logger.info('Win: {0} + {1:,d}'.format(winner.player.name, win_amount)) logger.info('Lose: {0} - {1:,d}'.format(loser.player.name, scores_to_pay)) # win by tsumo else: scores_to_pay /= 3 # will be changed when real hand calculation will be implemented # round to nearest 100. 333 -> 300 scores_to_pay = 100 * round(float(scores_to_pay) / 100) win_amount = scores_to_pay * 3 + riichi_bonus winner.player.scores += win_amount for client in self.clients: if client != winner: client.player.scores -= scores_to_pay logger.info('Win: {0} + {1:,d}'.format(winner.player.name, win_amount)) # retake else: tempai_users = 0 for client in self.clients: if client.player.in_tempai: tempai_users += 1 if tempai_users == 0 or tempai_users == 4: self.honba_sticks += 1 # no one in tempai, so deal should move if tempai_users == 0: new_dealer = self._move_position(self.dealer) self.set_dealer(new_dealer) else: # 1 tempai user will get 3000 # 2 tempai users will get 1500 each # 3 tempai users will get 1000 each scores_to_pay = 3000 / tempai_users for client in self.clients: if client.player.in_tempai: client.player.scores += scores_to_pay logger.info('{0} + {1:,d}'.format(client.player.name, int(scores_to_pay))) # dealer was tempai, we need to add honba stick if client.player.is_dealer: self.honba_sticks += 1 else: client.player.scores -= 3000 / (4 - tempai_users) # if someone has negative scores, # we need to end the game for client in self.clients: if client.player.scores < 0: is_game_end = True # we have played all 8 winds, let's finish the game if self._unique_dealers > 8: is_game_end = True logger.info('') return { 'winner': winner, 'loser': loser, 'is_tsumo': is_tsumo, 'is_game_end': is_game_end } def players_sorted_by_scores(self): return sorted([i.player for i in self.clients], key=lambda x: x.scores, reverse=True) def _set_client_names(self): """ For better tests output """ names = ['Sato', 'Suzuki', 'Takahashi', 'Tanaka', 'Watanabe', 'Ito', 'Yamamoto', 'Nakamura', 'Kobayashi', 'Kato', 'Yoshida', 'Yamada'] for client in self.clients: name = names[randint(0, len(names) - 1)] names.remove(name) from mahjong.myAI.SLCNNPlayer import SLCNNPlayer print(isinstance(client.player.ai, SLCNNPlayer)) #if isinstance(client.player.ai, SLCNNPlayer): # client.player.name = "Suu" #else: # client.player.name = name def _get_current_client(self) -> Client: return self.clients[self.current_client] def _cut_tiles(self, count_of_tiles) -> []: """ Cut the tiles array :param count_of_tiles: how much tiles to cut :return: the array with specified count of tiles """ result = self.tiles[0:count_of_tiles] self.tiles = self.tiles[count_of_tiles:len(self.tiles)] return result def _move_position(self, current_position): """ loop 0 -> 1 -> 2 -> 3 -> 0 """ current_position += 1 if current_position > 3: current_position = 0 return current_position
class MainAI(BaseAI): version = '0.2.7' agari = None shanten = None defence = None hand_divider = None finished_hand = None previous_shanten = 7 in_defence = False waiting = None current_strategy = None def __init__(self, player): super(MainAI, self).__init__(player) self.agari = Agari() self.shanten = Shanten() self.defence = DefenceHandler(player) self.hand_divider = HandDivider() self.finished_hand = FinishedHand() self.previous_shanten = 7 self.current_strategy = None self.waiting = [] self.in_defence = False def erase_state(self): self.current_strategy = None self.in_defence = False def discard_tile(self): results, shanten = self.calculate_outs(self.player.tiles, self.player.closed_hand, self.player.open_hand_34_tiles) selected_tile = self.process_discard_options_and_select_tile_to_discard( results, shanten) # bot think that there is a threat on the table # and better to fold # if we can't find safe tiles, let's continue to build our hand if self.defence.should_go_to_defence_mode(selected_tile): if not self.in_defence: logger.info('We decided to fold against other players') self.in_defence = True defence_tile = self.defence.try_to_find_safe_tile_to_discard( results) if defence_tile: return self.process_discard_option(defence_tile, self.player.closed_hand) else: self.in_defence = False return self.process_discard_option(selected_tile, self.player.closed_hand) def process_discard_options_and_select_tile_to_discard( self, results, shanten): tiles_34 = TilesConverter.to_34_array(self.player.tiles) # we had to update tiles value there # because it is related with shanten number for result in results: result.tiles_count = self.count_tiles(result.waiting, tiles_34) result.calculate_value(shanten) # current strategy can affect on our discard options # so, don't use strategy specific choices for calling riichi if self.current_strategy: results = self.current_strategy.determine_what_to_discard( self.player.closed_hand, results, shanten, False, None) return self.chose_tile_to_discard(results) def calculate_outs(self, tiles, closed_hand, open_sets_34=None): """ :param tiles: array of tiles in 136 format :param closed_hand: array of tiles in 136 format :param open_sets_34: array of array with tiles in 34 format :return: """ tiles_34 = TilesConverter.to_34_array(tiles) closed_tiles_34 = TilesConverter.to_34_array(closed_hand) is_agari = self.agari.is_agari(tiles_34, self.player.open_hand_34_tiles) results = [] for hand_tile in range(0, 34): if not closed_tiles_34[hand_tile]: continue tiles_34[hand_tile] -= 1 shanten = self.shanten.calculate_shanten(tiles_34, open_sets_34) waiting = [] for j in range(0, 34): if hand_tile == j or tiles_34[j] == 4: continue tiles_34[j] += 1 if self.shanten.calculate_shanten(tiles_34, open_sets_34) == shanten - 1: waiting.append(j) tiles_34[j] -= 1 tiles_34[hand_tile] += 1 if waiting: results.append( DiscardOption(player=self.player, shanten=shanten, tile_to_discard=hand_tile, waiting=waiting, tiles_count=self.count_tiles( waiting, tiles_34))) if is_agari: shanten = Shanten.AGARI_STATE else: shanten = self.shanten.calculate_shanten(tiles_34, open_sets_34) return results, shanten def count_tiles(self, waiting, tiles_34): n = 0 for item in waiting: n += 4 - self.player.total_tiles(item, tiles_34) return n def try_to_call_meld(self, tile, is_kamicha_discard): if not self.current_strategy: return None, None return self.current_strategy.try_to_call_meld(tile, is_kamicha_discard) def determine_strategy(self): # for already opened hand we don't need to give up on selected strategy if self.player.is_open_hand and self.current_strategy: return False old_strategy = self.current_strategy self.current_strategy = None # order is important strategies = [ YakuhaiStrategy(BaseStrategy.YAKUHAI, self.player), HonitsuStrategy(BaseStrategy.HONITSU, self.player), ] if settings.OPEN_TANYAO: strategies.append(TanyaoStrategy(BaseStrategy.TANYAO, self.player)) for strategy in strategies: if strategy.should_activate_strategy(): self.current_strategy = strategy if self.current_strategy: if not old_strategy or self.current_strategy.type != old_strategy.type: message = '{} switched to {} strategy'.format( self.player.name, self.current_strategy) if old_strategy: message += ' from {}'.format(old_strategy) logger.debug(message) logger.debug('With hand: {}'.format( TilesConverter.to_one_line_string(self.player.tiles))) if not self.current_strategy and old_strategy: logger.debug('{} gave up on {}'.format(self.player.name, old_strategy)) return self.current_strategy and True or False def chose_tile_to_discard(self, results: [DiscardOption]) -> DiscardOption: """ Try to find best tile to discard, based on different valuations """ def sorting(x): # - is important for x.tiles_count # in that case we will discard tile that will give for us more tiles # to complete a hand return x.shanten, -x.tiles_count, x.valuation had_to_be_discarded_tiles = [ x for x in results if x.had_to_be_discarded ] if had_to_be_discarded_tiles: had_to_be_discarded_tiles = sorted(had_to_be_discarded_tiles, key=sorting) selected_tile = had_to_be_discarded_tiles[0] else: results = sorted(results, key=sorting) # remove needed tiles from discard options results = [x for x in results if not x.had_to_be_saved] # let's chose most valuable tile first temp_tile = results[0] # and let's find all tiles with same shanten results_with_same_shanten = [ x for x in results if x.shanten == temp_tile.shanten ] possible_options = [temp_tile] for discard_option in results_with_same_shanten: # there is no sense to check already chosen tile if discard_option.tile_to_discard == temp_tile.tile_to_discard: continue # we don't need to select tiles almost dead waits if discard_option.tiles_count <= 2: continue # let's check all other tiles with same shanten # maybe we can find tiles that have almost same tiles count number if temp_tile.tiles_count - 2 < discard_option.tiles_count < temp_tile.tiles_count + 2: possible_options.append(discard_option) # let's sort got tiles by value and let's chose less valuable tile to discard possible_options = sorted(possible_options, key=lambda x: x.valuation) selected_tile = possible_options[0] return selected_tile def process_discard_option(self, selected_tile, closed_hand): self.waiting = selected_tile.waiting self.player.ai.previous_shanten = selected_tile.shanten self.player.in_tempai = self.player.ai.previous_shanten == 0 return selected_tile.find_tile_in_hand(closed_hand) def estimate_hand_value(self, win_tile, tiles=None, call_riichi=False): """ :param win_tile: 34 tile format :param tiles: :param call_riichi: :return: """ win_tile *= 4 # we don't need to think, that our waiting is aka dora if win_tile in AKA_DORA_LIST: win_tile += 1 if not tiles: tiles = self.player.tiles tiles += [win_tile] result = self.finished_hand.estimate_hand_value( tiles=tiles, win_tile=win_tile, is_tsumo=False, is_riichi=call_riichi, is_dealer=self.player.is_dealer, open_sets=self.player.open_hand_34_tiles, player_wind=self.player.player_wind, round_wind=self.player.table.round_wind, dora_indicators=self.player.table.dora_indicators) return result def should_call_riichi(self): # empty waiting can be found in some cases if not self.waiting: return False # we have a good wait, let's riichi if len(self.waiting) > 1: return True waiting = self.waiting[0] tiles = self.player.closed_hand + [waiting * 4] closed_melds = [x for x in self.player.melds if not x.opened] for meld in closed_melds: tiles.extend(meld.tiles[:3]) tiles_34 = TilesConverter.to_34_array(tiles) results = self.hand_divider.divide_hand(tiles_34, [], []) result = results[0] count_of_pairs = len([x for x in result if is_pair(x)]) # with chitoitsu we can call a riichi with pair wait if count_of_pairs == 7: return True for hand_set in result: # better to not call a riichi for a pair wait # it can be easily improved if is_pair(hand_set) and waiting in hand_set: return False return True def can_call_kan(self, tile, open_kan): """ Method will decide should we call a kan, or upgrade pon to kan :param tile: 136 tile format :param open_kan: boolean :return: kan type """ # we don't need to add dora for other players if self.player.ai.in_defence: return None if open_kan: # we don't want to start open our hand from called kan if not self.player.is_open_hand: return None # there is no sense to call open kan when we are not in tempai if not self.player.in_tempai: return None # we have a bad wait, rinshan chance is low if len(self.waiting) < 2: return None tile_34 = tile // 4 tiles_34 = TilesConverter.to_34_array(self.player.tiles) closed_hand_34 = TilesConverter.to_34_array(self.player.closed_hand) pon_melds = [x for x in self.player.open_hand_34_tiles if is_pon(x)] # let's check can we upgrade opened pon to the kan if pon_melds: for meld in pon_melds: # tile is equal to our already opened pon, # so let's call chankan! if tile_34 in meld: return Meld.CHANKAN count_of_needed_tiles = 4 # for open kan 3 tiles is enough to call a kan if open_kan: count_of_needed_tiles = 3 # we have 3 tiles in our hand, # so we can try to call closed meld if closed_hand_34[tile_34] == count_of_needed_tiles: if not open_kan: # to correctly count shanten in the hand # we had do subtract drown tile tiles_34[tile_34] -= 1 melds = self.player.open_hand_34_tiles previous_shanten = self.shanten.calculate_shanten(tiles_34, melds) melds += [[tile_34, tile_34, tile_34]] new_shanten = self.shanten.calculate_shanten(tiles_34, melds) # called kan will not ruin our hand if new_shanten <= previous_shanten: return Meld.KAN return None @property def valued_honors(self): return [ CHUN, HAKU, HATSU, self.player.table.round_wind, self.player.player_wind ]