9
\$\begingroup\$

I started to try my hand at making a Chess Game. It works as a game, but I know this certainly not the most efficient way of doing it. I'm just looking for advice and critique on how to get better. Please be tough!

MOVEMAP = {'a':0 , 'b': 1, 'c': 2, 'd': 3,'e': 4,'f': 5, 'g': 6, 'h': 7,
                   '1': 7, '2': 6, '3' : 5, '4': 4, '5': 3, '6':2, '7':1, '8':0}

PIECEMAP = {'P' :[(1,0),(-1,0)],
            'R': [(1,0),(2,0),(3,0),(4,0),(5,0),(6,0),(7,0), 
                  (-1,0),(-2,0),(-3,0),(-4,0),(-5,0),(-6,0),(-7,0),
                  (0,1),(0,2),(0,3),(0,4),(0,5),(0,6),(0,7),
                  (0,-1),(0,-2),(0,-3),(0,-4),(0,-5),(0,-6),(0,-7)],
            'N' : [(2,1),(2,-1),(-2,1),(-2,-1),(1,2),(-1,2),(1,-2),(-1,-2)],
            'B' : [(1,1),(2,2),(3,3),(4,4),(5,5),(6,6),(7,7),
                  (1,-1),(2,-2),(3,-3),(4,-4),(5,-5),(6,-6),(7,-7),
                  (-1,1),(-2,2),(-3,3),(-4,4),(-5,5),(-6,6),(-7,7),
                  (-1,-1),(-2,-2),(-3,-3),(-4,-4),(-5,-5),(-6,-6),(-7,-7)],
            'Q' : [(1,1),(2,2),(3,3),(4,4),(5,5),(6,6),(7,7),
                  (1,-1),(2,-2),(3,-3),(4,-4),(5,-5),(6,-6),(7,-7),
                  (-1,1),(-2,2),(-3,3),(-4,4),(-5,5),(-6,6),(-7,7),
                  (-1,-1),(-2,-2),(-3,-3),(-4,-4),(-5,-5),(-6,-6),(-7,-7),
                   (1,0),(2,0),(3,0),(4,0),(5,0),(6,0),(7,0), 
                  (-1,0),(-2,0),(-3,0),(-4,0),(-5,0),(-6,0),(-7,0),
                  (0,1),(0,2),(0,3),(0,4),(0,5),(0,6),(0,7),
                  (0,-1),(0,-2),(0,-3),(0,-4),(0,-5),(0,-6),(0,-7)],
            'K' : [(1,0),(-1,0),(0,1),(0,-1),(-1,1),(1,-1),(-1,-1),(1,1)]}


CHECK = False 
CHECKMATE = False
hostiles = [] 



class Board: 

    def __init__(self,board = None): 

        if board == None:        
            self.board = [['WR','WN','WB','WQ','WK','WB','WN','WR'],
                      ['WP','WP','WP','WP','WP','WP','WP','WP'],
                      ['0 ','0 ','0 ','0 ','0 ','0 ','0 ','0 '],
                      ['0 ','0 ','0 ','0 ','0 ','0 ','0 ','0 '],
                      ['0 ','0 ','0 ','0 ','0 ','0 ','0 ','0 '],
                      ['0 ','0 ','0 ','0 ','0 ','0 ','0 ','0 '],
                      ['BP','BP','BP','BP','BP','BP','BP','BP'],
                      ['BR','BN','BB','BQ','BK','BB','BN','BR']]

            self.HAS_MOVED_LIST = [] 
        else: 
                self.board = [[board[row][col] for col in range(8)] 
                           for row in range(8)]
                self.HAS_MOVED_LIST = [] 

    def __str__(self):    
        '''
        returns human readable board representation of given game state
        '''
        y = ['8','7','6','5','4','3','2','1']
        x = '   a,    b,    c,    d,    e,    f,    g,    h'
        ans = ""
        for row in range(8):
                ans += y[row] + str(self.board[row])
                ans += "\n"
        ans += x 
        return ans

    def has_moved(self,x,y):
        ''' 
        Determines whether or not a piece has moved
        '''        
        has_moved = False  
        piece_coord = (x,y) 
        piece_pos_list = [(0,0),(0,1),(0,2),(0,3),(0,4),(0,5),(0,6),(0,7),
                          (1,0),(1,1),(1,2),(1,3),(1,4),(1,5),(1,6),(1,7),
                          (6,0),(6,1),(6,2),(6,3),(6,4),(6,5),(6,6),(6,7),
                          (7,0),(7,1),(7,2),(7,3),(7,4),(7,5),(7,6),(7,7)]
        if piece_coord not in piece_pos_list: 
            has_moved = True 
            return has_moved         
        for piece in piece_pos_list: 
             if self.is_empty(piece[0],piece[1]) == True:
                    self.HAS_MOVED_LIST.append(piece) 
        if piece_coord in self.HAS_MOVED_LIST: 
            has_moved = True 
        else: 
            has_moved = False 
        return has_moved 

    def is_empty(self,x,y):
        '''
        determines if the given cell is empty 
        ''' 
        if self.board[x][y] == '0 ': 
            return True
        else:
            return False

    def clone(self): 
      '''
      Makes a copy of the current board
      '''
      return Board(board = self.board)

    def is_inpath(self,x,y,v,w):
        '''
        determines list of coordinates that correspond to the path from the given points
        '''
        path = []
        piece = self.board[x][y]
        prev_pos = (x,y) 
        new_pos = (v,w) 
        diff = (x - v, y - w)         
        #Avoid dividing by zero to determine offset 
        if diff[0] == 0: 
            offset = diff[0],diff[1]/abs(diff[1])
        elif diff[1] == 0: 
            offset = diff[0]/abs(diff[0]),diff[1]
        else: 
            offset = diff[0]/abs(diff[0]),diff[1]/abs(diff[1])  
        #figures out the coordinates from prev_pos and new_pos
        while new_pos != prev_pos: 
            new_pos = new_pos[0] + offset[0],new_pos[1] + offset[1]  
            path.append(new_pos)
        return path 

    def switch_player(self,player): 
      '''
      Returns the opposite player 
      '''         
      if player == 'B': 
            return 'W'
      elif player == 'W': 
            return 'B' 

    def find_pieces(self,piece):
      '''
      Returns the coordinates of all the pieces in question
      ''' 
      coord = []
      for i in range(8): 
          for j in range(8): 
              if self.board[i][j] == piece: 
                  coord.append((i,j)) 
      return coord

    def in_danger(self,x,y): 
        '''
        determines whether piece is one move away from being killed
        '''
        global hostiles       
        piece = self.board[x][y]
        player = piece[0] 
        coord = (x,y) 
        enemies = []       
        hostiles = []        
        #Find all the oppossing player's location on the map 
        for i in range(8): 
            for j in range(8):
                look_in = self.board[i][j]
                if look_in[0] != player and look_in[0] != '0':
                    enemies.append((i,j))      
        #Determine if each enemy's next move is possible to attack (x,y) 
        hostiles = []   
        for enemy in enemies: 
            if self.is_possible(enemy[0],enemy[1],x,y,self.switch_player(player)) == True: 
                hostiles.append(enemy)  
        if len(hostiles) > 0: 
            return True
        else: 
            return False 

    def string_coord(self,x,y): 
      '''
      Returns the string input of the coordinates
      '''        
      keys = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', '1', '2', '3', '4', '5', '6', '7', '8']        
      x_axis = keys[y]
      y_axis = keys[15 - x]        
      pos = str(x_axis)  + str(y_axis)        
      return pos 

    def check_or_checkmate(self,player):
        ''' 
        Check if opposing player is in check or checkmate
        '''                 
        global CHECK
        global CHECKMATE
        global hostiles 
        possible_mov = []
        only_moves = []
        checkless_moves = []
        saved = True 
        mov_range = PIECEMAP['K']
        #Determine where king is 
        if player == 'B': 
            coord = self.find_pieces('WK')[0] 
        elif player == 'W': 
            coord = self.find_pieces('BK')[0]           
        #Convert coord into string input         
        str_coord = self.string_coord(coord[0],coord[1]) 
        #check if opposing player is in check
        if self.in_danger(coord[0],coord[1]) == True:
                CHECK = True
        else:
            return CHECKMATE            
        #Check whether piece is in checkmate 
        #Create list of possible movements 
        for mov in mov_range:
            c = (coord[0] + mov[0],coord[1] + mov[1])
            if 8 > c[0] > -1 and 8 > c[1] > -1: 
                possible_mov.append(c)               
        #Create list of possible moves 
        for mov in possible_mov: 
            if self.is_possible(coord[0],coord[1],mov[0],mov[1],self.switch_player(player)) == True:
                only_moves.append((mov[0],mov[1])) 
        #simulate each possible each possible move to determine checkmate  
        for mov in only_moves:
            simu_game = self.clone()
            simu_coord = self.string_coord(mov[0],mov[1])
            simu_game.move(str_coord,simu_coord,self.switch_player(player))
            if simu_game.in_danger(mov[0],mov[1]) == False:
                        checkless_moves.append(mov)       
        my_team = []
        for i in range(8): 
                for j in range(8):
                    look_in = self.board[i][j]
                    if look_in[0] != player and look_in[0] != '0' and look_in[1] != 'K':
                        my_team.append((i,j))
        self.in_danger(coord[0],coord[1])
        saviors = []
        for h in hostiles:
             save_path = self.is_inpath(coord[0],coord[1],h[0],h[1]) 
             for mate in my_team:
                 for loc in save_path: 
                     if self.is_possible(mate[0],mate[1],loc[0],loc[1],self.switch_player(player)) == True:
                         saviors.append(mate)        
        if len(saviors) != 0:
            saviors = False 
        else:
            True 
        #Determine what CHECKMATE and CHECK booleans are  
        if len(checkless_moves) == 0 and saved == False:   
                CHECKMATE = True            
        if CHECK == True and CHECKMATE == True:   
                    return CHECKMATE 
        elif CHECK == True and CHECKMATE == False: 
                return CHECK  
        else: 
                return CHECK 

    def is_possible(self,x,y,v,w,player):
        '''
        determines if the given move allowed to be placed in the game
        '''       
        n_mov = (v,w) 
        move = (x,y)
        piece = self.board[x][y]
        n_piece = self.board[v][w]
        possible_mov = []
        passable = []
        impassable = []
        # Cases where input is incorrect 
        if piece == '0 ':
            return False
        if piece[0] != player:
            print 'not player'
            return False 
        #Determine range of movements 
        mov_range = PIECEMAP[piece[1]]  
        # directionality of pawn
        if piece[1] == 'P':
            if piece[0] == 'B':
                mov_range = [(-1,0)]
            elif piece[0] == 'W':
                mov_range = [(1,0)]                       
        # if pawn is attacking 
        if piece[1] == 'P' and n_piece[0] != player and n_piece[0] != '0': 
            if piece[0] == 'B': 
                mov_range = [(-1,-1),(-1,1)]
            elif piece[0] == 'W':
                mov_range = [(1,1),(1,-1)]       
        #if pawn can move two steps 
        if piece[1] == 'P' and self.has_moved(x,y) == False:
            if piece[0] == 'B':
                mov_range = [(-2,0),(-1,0)]
            elif piece[0] == 'W':
                mov_range = [(2,0),(1,0)]
        #Castling conditions 
        if piece[1] == 'K' and n_piece[1] == 'R' and self.has_moved(x,y) == False and self.has_moved(v,w) == False: 
            castling_obs = [] 
            path = self.is_inpath(x,y,v,w)
            for piece in path[:-1]:
                if self.is_empty(piece[0],piece[1]) == False:
                    castling_obs.append(piece)
            if len(castling_obs) > 0:
                return False 
            else:
                return True               
        # All possible moves on board
        for mov in mov_range:
           c = (move[0] + mov[0],move[1] + mov[1])
           if 8 > c[0] > -1 and 8 > c[1] > -1: 
                possible_mov.append(c)       
        # All passable moves that are not obstructed by player's pieces   
        for mov in possible_mov:
            piece_mov = self.board[mov[0]][mov[1]]
            if player == 'B': 
                if piece_mov[0] != 'B':
                    passable.append(mov) 
                else: 
                    impassable.append(mov)
            elif player == 'W':
                if piece_mov[0] != 'W':
                    passable.append(mov) 
                else: 
                    impassable.append(mov)       
        # Knight exception for passable moves 
        if piece[1] == 'N':
            if n_mov in passable: 
                return True
            else: 
                return False       
        #Determine path and if movement is obstucted
        else:
            if n_mov not in passable:
                return False 
            else:
                path = self.is_inpath(x,y,v,w)
                for mov in path[:-1]: 
                      moveable_space = self.board[mov[0]][mov[1]]
                      if moveable_space != '0 ':
                            return False  
                if len(set(impassable).intersection(path)) > 0:
                     return False
                else: 
                     return True

    def move(self,prev_move,new_move,player):
        '''
        places new move into board, given that the move is possible 
        '''  
        # Coordinated of move 
        new_pos =  MOVEMAP[new_move[1]],MOVEMAP[new_move[0]]
        prev_pos = MOVEMAP[prev_move[1]],MOVEMAP[prev_move[0]]       
        # if move is empty or the attack or path is not obstructed 
        if self.is_possible(prev_pos[0],prev_pos[1],new_pos[0],new_pos[1],player) == True:
            self.has_moved(prev_pos[0],prev_pos[1]) 
            old = self.board[prev_pos[0]][prev_pos[1]]
            new = self.board[new_pos[0]][new_pos[1]]
            # Castling Condition
            if old[1] == 'K' and new[1] == 'R':
                if new_pos == (7,7): 
                    self.board[7][6] = old
                    self.board[7][5] = new
                    self.board[new_pos[0]][new_pos[1]] = '0 '
                    self.board[prev_pos[0]][prev_pos[1]] = '0 '
                elif new_pos == (7,0):
                    self.board[7][2] = old
                    self.board[7][3] = new
                    self.board[new_pos[0]][new_pos[1]] = '0 '
                    self.board[prev_pos[0]][prev_pos[1]] = '0 '
                elif new_pos == (0,0):
                    self.board[0][2] = old
                    self.board[0][3] = new
                    self.board[new_pos[0]][new_pos[1]] = '0 '
                    self.board[prev_pos[0]][prev_pos[1]] = '0 '
                elif new_pos == (0,7):
                    self.board[0][6] = old
                    self.board[0][5] = new
                    self.board[new_pos[0]][new_pos[1]] = '0 '
                    self.board[prev_pos[0]][prev_pos[1]] = '0 '                      
            #replace killed piece with empty 
            elif new[0] != player: 
                self.board[new_pos[0]][new_pos[1]] = old
                self.board[prev_pos[0]][prev_pos[1]] = '0 '
            else: 
                self.board[new_pos[0]][new_pos[1]] = old
                self.board[prev_pos[0]][prev_pos[1]] = new        
        else: 
            return False

#####################################################################################################################################
 #User Interface
###################################################################################################################################          


def play():
    print 'Welcome to Chess'
    print ' '
    print 'Please type the coordinate of the pieace you want to move and the coordinates where you like to go to... Have fun!'
    print ' ' 
    global CHECKMATE
    global CHECK
    game = Board()
    print game
    player = 'B'

    while CHECKMATE == False:               
         if player == 'B':
             Player = 'Black'
         elif player == 'W':
             Player = 'White'

         o_player = game.switch_player(player) 
         move = raw_input( Player + "'s Move: ")
         print ' '
         #Determine whether input is correct 
         current_move = move[0:2]
         new_move =  move[2:4]
         move_list= [current_move[0],current_move[1],new_move[0],new_move[1]]         
         not_allowed = []
         for m in move_list:
            if m not in MOVEMAP.keys():
                not_allowed.append(move)               
         if len(not_allowed) != 0:
             print "That doesnt even make any sense, try again"

         elif len(move) != 4:
             print 'Try again using this format: e2e3'
         else:
                 o_piece = game.board[MOVEMAP[current_move[1]]][MOVEMAP[current_move[0]]]
                 n_piece = game.board[MOVEMAP[new_move[1]]][MOVEMAP[new_move[0]]]        
                 if game.move(current_move,new_move, player) == False:
                     print "you can't move that, try again"     
                 elif game.check_or_checkmate(player) == True:
                         player =  game.switch_player(player)
                         print str(o_player) + ' ' + (' in Check') 
                         print game 
                 elif game.check_or_checkmate(o_player) == True:
                         game.board[MOVEMAP[current_move[1]]][MOVEMAP[current_move[0]]] = o_piece
                         game.board[MOVEMAP[new_move[1]]][MOVEMAP[new_move[0]]] = n_piece
                         CHECKMATE = False 
                         print str(Player) + ' ' + (' will be in check with this move')           
                 else:
                         print game 
                         player =  game.switch_player(player)
    print 'Game Over' 
\$\endgroup\$
  • \$\begingroup\$ You indicate in your description that your chess program works by saying: "It works as a game". I tried to run with Python 2 and nothing happened, no errors reported either. So, I added a few "print" statements to print a few line numbers along the way including one at the very end. They all printed indicating that Python read all the code and there are no errors. Is there more code somewhere that I did not find? What do you mean by "it works"? It is hard to critique something that does nothing. \$\endgroup\$ – Richard Kircher Oct 9 '18 at 20:26
5
\$\begingroup\$

Note: You've posted a large chunk of code, so I might miss something during my review. Please excuse me for any mistake. Better documentation is appreciated.

I'm a C++ chess engine programmer, so I'll share my experience with you.

  • The Python coding is fairly standard so this is ok
  • Your board representation is too slow for a chess engine, but acceptable for a chess GUI. If you would like to write your own chess engine, everything would have be rewritten.
  • You forgot to check whether the castling squares are attacked by the enemy pieces.
  • You forgot to implement promotions and under-promotions
  • You forgot to check for pinned pieces. For example, your program would move a knight away from it's king when it can't.
  • You only need to know whether the rooks and king have moved or not. You don't care your queen, knight and bishop. You only need rook and king because for castling. Therefore, the function has_moved is wrong.
  • How you check whether or not a piece has moved is too complex and unnecessary. Professional softwares do it with Zobrist hashing. Consider to hash your position. If you don't want to do it, use a simple dictionary. Stop looping and then looping.
  • How you find a piece is slow because it is a linear search. Fortunately, it's not very important unless you want to code a chess engine (there're only 64 squares in chess). Professional softwares use bitboard.
  • in_danger is wrong. You check whether your piece is being attacked by the enemy piece. But what if your piece is protected? A piece is really in danger if it's being attacked and unprotected.
  • Furthermore, a protected piece is in danger if it's being attacked by a less valuable piece. For example, your protected queen is being attacked by your opponent's pawn. You might want to check static exchange evaluation.

Overall, I don't think it works correctly for the chess rules. You should check your board generator against Perft.

Too much two-dimensional looping. It's okay for chess because we have only 64 squares, but try to be smarter in your next project. This is an opportunity for you to learn hashing and dictionary.

On a 1-10 scale, if a commercial quality GUI (such as Chessbase's Fritz) is 10/10. I'd rate this attempt as 1/10.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.