Ejemplo n.º 1
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()
Ejemplo n.º 2
0
    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))
Ejemplo n.º 3
0
    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)))
Ejemplo n.º 4
0
    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
Ejemplo n.º 5
0
    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)))
Ejemplo n.º 6
0
    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)))
Ejemplo n.º 7
0
    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)))
Ejemplo n.º 8
0
    def __init__(self, table, player):
        super(MainAI, self).__init__(table, player)

        self.agari = Agari()
        self.shanten = Shanten()
        self.defence = Defence(table)
Ejemplo n.º 9
0
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
Ejemplo n.º 10
0
 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
Ejemplo n.º 11
0
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
Ejemplo n.º 12
0
    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()
Ejemplo n.º 13
0
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
Ejemplo n.º 14
0
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
        ]