Design a Chess Board OO

Hard
9 years ago

Design a Chess Board using Object-Oriented principles. Consider the classes, their attributes, and methods. Focus on the core components like the board, pieces, and interactions between them. For example, how would you represent the different chess pieces (pawn, rook, knight, etc.) and their unique movement rules? How would you handle the logic for checking if a move is valid, including considering piece-specific constraints and checking for checkmate or stalemate scenarios? Illustrate with code snippets to showcase how these objects would interact. How will you handle special moves like castling and en passant? How would you keep track of the game state and handle turn management?

Sample Answer

Chess Board Design

This document outlines the design of a chess board using object-oriented principles. It covers the core classes, their attributes, methods, and interactions, with a focus on representing pieces, movement rules, handling move validity, and managing the game state.

Classes

  • Piece (Abstract Class):
    • Attributes:
      • color: Color of the piece (e.g., "white", "black").
      • type: The type of the piece, use PieceType enum (e.g., PAWN, ROOK, KNIGHT, BISHOP, QUEEN, KING).
      • position: Current position on the board (e.g., (0, 0) for A1).
      • is_alive: Boolean indicating if the piece is still in the game.
    • Methods:
      • __init__(self, color, piece_type, position): Constructor to initialize the piece.
      • possible_moves(self, board): Abstract method to return a list of valid moves for the piece on the given board.
      • move(self, new_position, board): Moves the piece to a new position if the move is valid. Returns True if successful, False otherwise.
      • capture(self): Sets is_alive to False when the piece is captured.
      • __repr__(self): String representation of the piece for debugging.
  • Pawn, Rook, Knight, Bishop, Queen, King (Concrete Classes):
    • Inherit from the Piece class.
    • Implement the possible_moves(self, board) method according to their specific movement rules.
  • Board:
    • Attributes:
      • grid: A 2D array (e.g., list of lists) representing the board. Each element contains a Piece object or None if the square is empty.
      • width: width of the board. Standard chess board is 8.
      • height: height of the board. Standard chess board is 8.
    • Methods:
      • __init__(self): Initializes the board with all pieces in their starting positions.
      • get_piece(self, position): Returns the Piece object at the given position, or None if the square is empty.
      • set_piece(self, position, piece): Places a Piece object at the given position.
      • is_valid_move(self, start_position, end_position): Checks if a move from start_position to end_position is valid according to chess rules.
      • move_piece(self, start_position, end_position): Moves a piece from start_position to end_position, updating the board state.
      • is_check(self, color): Checks if the king of the given color is in check.
      • is_checkmate(self, color): Checks if the king of the given color is in checkmate.
      • is_stalemate(self, color): Checks if the game is in stalemate.
      • print_board(self): Prints a text-based representation of the board.
  • Game:
    • Attributes:
      • board: The Board object representing the chessboard.
      • current_player: The color of the player whose turn it is (e.g., "white", "black").
      • move_history: A list of moves made during the game.
    • Methods:
      • __init__(self): Initializes the game with a new board and sets the starting player to white.
      • make_move(self, start_position, end_position): Attempts to make a move from start_position to end_position for the current player. Handles move validation, piece movement, capturing, and updates the game state.
      • is_game_over(self): Checks if the game is over (checkmate or stalemate).
      • switch_player(self): Switches the current player to the other color.

Code Snippets

Piece Class

from enum import Enum

class Color(Enum):
    WHITE = "white"
    BLACK = "black"

class PieceType(Enum):
    PAWN = "pawn"
    ROOK = "rook"
    KNIGHT = "knight"
    BISHOP = "bishop"
    QUEEN = "queen"
    KING = "king"

class Piece:
    def __init__(self, color, piece_type, position):
        self.color = color
        self.type = piece_type
        self.position = position
        self.is_alive = True

    def possible_moves(self, board):
        raise NotImplementedError("Subclasses must implement possible_moves")

    def move(self, new_position, board):
        if new_position in self.possible_moves(board):
            board.move_piece(self.position, new_position)
            self.position = new_position
            return True
        return False

    def capture(self):
        self.is_alive = False

    def __repr__(self):
        return f"{self.color.value[0].upper()}{self.type.value[0].upper()}"

Pawn Class

class Pawn(Piece):
    def __init__(self, color, position):
        super().__init__(color, PieceType.PAWN, position)
        self.has_moved = False  # To handle double move on first move

    def possible_moves(self, board):
        moves = []
        x, y = self.position
        direction = 1 if self.color == Color.WHITE else -1  # White moves up, black moves down

        # Move one square forward
        new_x = x + direction
        if 0 <= new_x < board.height and board.get_piece((new_x, y)) is None:
            moves.append((new_x, y))

            # Move two squares forward on first move
            if not self.has_moved and 0 <= new_x + direction < board.height and board.get_piece((new_x + direction, y)) is None:
                moves.append((new_x + direction, y))

        # Capture diagonally
        for dy in [-1, 1]:
            new_y = y + dy
            if 0 <= new_x < board.height and 0 <= new_y < board.width:
                target_piece = board.get_piece((new_x, new_y))
                if target_piece and target_piece.color != self.color:
                    moves.append((new_x, new_y))

        # TODO: En passant

        return moves

    def move(self, new_position, board):
        if super().move(new_position, board):
            self.has_moved = True
            return True
        return False

Board Class

class Board:
    def __init__(self):
        self.width = 8
        self.height = 8
        self.grid = [[None for _ in range(self.width)] for _ in range(self.height)]
        self.setup_board()

    def setup_board(self):
        # Place pawns
        for y in range(self.width):
            self.grid[1][y] = Pawn(Color.WHITE, (1, y))
            self.grid[self.height - 2][y] = Pawn(Color.BLACK, (self.height - 2, y))

        # Place rooks
        self.grid[0][0] = Rook(Color.WHITE, (0, 0))
        self.grid[0][self.width - 1] = Rook(Color.WHITE, (0, self.width - 1))
        self.grid[self.height - 1][0] = Rook(Color.BLACK, (self.height - 1, 0))
        self.grid[self.height - 1][self.width - 1] = Rook(Color.BLACK, (self.height - 1, self.width - 1))

        # Place knights
        self.grid[0][1] = Knight(Color.WHITE, (0, 1))
        self.grid[0][self.width - 2] = Knight(Color.WHITE, (0, self.width - 2))
        self.grid[self.height - 1][1] = Knight(Color.BLACK, (self.height - 1, 1))
        self.grid[self.height - 1][self.width - 2] = Knight(Color.BLACK, (self.height - 1, self.width - 2))

        # Place bishops
        self.grid[0][2] = Bishop(Color.WHITE, (0, 2))
        self.grid[0][self.width - 3] = Bishop(Color.WHITE, (0, self.width - 3))
        self.grid[self.height - 1][2] = Bishop(Color.BLACK, (self.height - 1, 2))
        self.grid[self.height - 1][self.width - 3] = Bishop(Color.BLACK, (self.height - 1, self.width - 3))

        # Place queens
        self.grid[0][3] = Queen(Color.WHITE, (0, 3))
        self.grid[self.height - 1][3] = Queen(Color.BLACK, (self.height - 1, 3))

        # Place kings
        self.grid[0][4] = King(Color.WHITE, (0, 4))
        self.grid[self.height - 1][4] = King(Color.BLACK, (self.height - 1, 4))

    def get_piece(self, position):
        x, y = position
        return self.grid[x][y]

    def set_piece(self, position, piece):
        x, y = position
        self.grid[x][y] = piece

    def is_valid_move(self, start_position, end_position):
        piece = self.get_piece(start_position)
        if not piece:
            return False

        return end_position in piece.possible_moves(self)

    def move_piece(self, start_position, end_position):
        piece = self.get_piece(start_position)
        if not piece:
            return False

        target_piece = self.get_piece(end_position)
        if target_piece:
            target_piece.capture()

        x1, y1 = start_position
        x2, y2 = end_position
        self.grid[x2][y2] = piece
        self.grid[x1][y1] = None

        piece.position = (x2, y2)
        return True


    def is_check(self, color):
        # Find the king of the given color
        king_position = None
        for i in range(self.height):
            for j in range(self.width):
                piece = self.get_piece((i, j))
                if piece and piece.color == color and piece.type == PieceType.KING:
                    king_position = (i, j)
                    break
            if king_position:
                break

        if not king_position:
            return False  # King not found (shouldn't happen in a valid game)

        # Check if any opponent's piece can attack the king
        for i in range(self.height):
            for j in range(self.width):
                piece = self.get_piece((i, j))
                if piece and piece.color != color:
                    if king_position in piece.possible_moves(self):
                        return True

        return False

    def is_checkmate(self, color):
        if not self.is_check(color):
            return False

        # Check if the king can move out of check
        king_position = None
        for i in range(self.height):
            for j in range(self.width):
                piece = self.get_piece((i, j))
                if piece and piece.color == color and piece.type == PieceType.KING:
                    king_position = (i, j)
                    break
            if king_position:
                break

        if not king_position:
            return False

        king = self.get_piece(king_position)
        possible_king_moves = king.possible_moves(self)

        for move in possible_king_moves:
            # Simulate the move
            original_piece = self.get_piece(move)
            self.move_piece(king_position, move)

            # Check if the king is still in check after the move
            if not self.is_check(color):
                # The king can move out of check, so it's not checkmate
                # Undo the move
                self.move_piece(move, king_position)
                self.set_piece(move, original_piece)
                return False

            # Undo the move
            self.move_piece(move, king_position)
            self.set_piece(move, original_piece)

        # The king cannot move out of check, so it's checkmate
        return True

    def is_stalemate(self, color):
        # Implement stalemate detection logic here
        return False  # Placeholder

    def print_board(self):
        for row in self.grid[::-1]:
            print(" ".join(str(p) if p else "--" for p in row))
        print()

Game Class

class Game:
    def __init__(self):
        self.board = Board()
        self.current_player = Color.WHITE
        self.move_history = []

    def make_move(self, start_position, end_position):
        if not self.board.is_valid_move(start_position, end_position):
            print("Invalid move!")
            return False

        piece = self.board.get_piece(start_position)
        if piece.color != self.current_player:
            print("Not your turn!")
            return False

        if self.board.move_piece(start_position, end_position):
            self.move_history.append((start_position, end_position))
            if self.board.is_check(Color.BLACK if self.current_player == Color.WHITE else Color.WHITE):
                print("Check!")
            self.switch_player()
            return True
        else:
            return False

    def is_game_over(self):
        if self.board.is_checkmate(Color.WHITE) or self.board.is_checkmate(Color.BLACK):
            print("Checkmate!")
            return True
        elif self.board.is_stalemate(Color.WHITE) or self.board.is_stalemate(Color.BLACK):
            print("Stalemate!")
            return True
        return False

    def switch_player(self):
        self.current_player = Color.BLACK if self.current_player == Color.WHITE else Color.WHITE

Example Usage

game = Game()
game.board.print_board()

# Example move
game.make_move((1, 0), (2, 0))  # White pawn moves
game.board.print_board()

game.make_move((6, 0), (5, 0)) # Black pawn moves
game.board.print_board()

game.make_move((0, 1), (2, 0)) # White knight moves
game.board.print_board()


while not game.is_game_over():
    start_pos = tuple(map(int, input("Enter start position (x, y): ").split(',')))
    end_pos = tuple(map(int, input("Enter end position (x, y): ").split(',')))
    game.make_move(start_pos, end_pos)
    game.board.print_board()

Special Moves

  • Castling:
    • The King class needs a can_castle attribute, initialized to True.
    • The Rook class also needs a can_castle attribute.
    • The possible_moves method of the King class should check if castling is possible (King and Rook haven't moved, no pieces between them, not in check, and doesn't pass through check).
    • The move method of the King class should handle the Rook's movement during castling.
  • En Passant:
    • The Pawn class needs a way to track if it has moved two squares in the previous turn.
    • The Board class needs a last_move attribute to store the last move made.
    • The possible_moves method of the Pawn class should check if en passant is possible based on the last_move.
    • The move_piece method of the Board class should handle the removal of the captured pawn during en passant.
  • Pawn Promotion:
    • The Pawn class should have a promote() method that changes the pawn's type to another piece (Queen, Rook, Bishop, or Knight) when it reaches the opposite end of the board.
    • The Game class should handle the promotion process, allowing the player to choose the new piece type.

Game State and Turn Management

  • The Game class keeps track of the current_player and move_history.
  • The make_move method updates the game state after each valid move.
  • The is_check, is_checkmate, and is_stalemate methods check the game's termination conditions.
  • The switch_player method alternates between players after each move.

Further Considerations

  • Move Validation: The is_valid_move method in the Board class needs to be comprehensive, checking for all possible invalid moves.
  • Check and Checkmate Detection: The is_check and is_checkmate methods need to be efficient and accurate.
  • Stalemate Detection: Implementing stalemate detection can be complex and requires careful consideration of all possible scenarios.
  • User Interface: A graphical user interface (GUI) could be added to make the game more user-friendly.
  • AI Opponent: An AI opponent could be implemented to allow players to play against the computer.