lunes, 11 de enero de 2016

Juego de Ajedrez (III). Definiendo las piezas

Artículos anteriores:
Juego de Ajedrez (I). Introducción a la Programación Orientada a Objetos
Juego de Ajedrez (II). Creando la estructura del proyecto

El código completo tanto en C# como en Visual Basic .NET está disponible en:

Códigos de muestra - Ejemplos MSDN. Juego de Ajedrez

Una vez creada la estructura de clases del proyecto voy a definir la funcionalidad de cada una de las piezas del juego. Para ello crearé una clase para cada tipo de pieza que herede de la clase abstracta ChessPiece.

En primer lugar crearé una serie de elementos que nos serán de utilidad a la hora de definir el comportamiento de la pieza.

En el archivo Chess.cs/Chess.vb voy a crear una estructura Movement para definir un movimiento de una pieza. La estructura tendrá dos campos enteros: Forward que indica el número de celdas que la pieza se moverá hacia adelante (en caso de moverse hacia atrás se indicarán números negativos) y ToRight que indica el numeró de celdas que la pieza se moverá hacia la derecha (en caso de moverse hacia la izquierda se indicarán números negativos).

    struct Movement
    {
        public int Forward;
        public int ToRight;
    }
Structure Movement
    Public Forward As Integer
    Public ToRight As Integer
End Structure

En la clase Chessboard voy a crear una nueva sobrecarga para el método GetSquare que recibirá como parámetros una celda origen y un movimiento y que devolverá la celda a la que se movería la pieza ubicada en la celda origen tras realizar el movimiento.

        public Square GetSquare(Square fromSquare, Movement move)
        {
            ChessPiece piece = fromSquare.Piece;
            if (piece == null) return null;

            return GetSquare(fromSquare.Row
                    + (piece.Color == PlayerOnTop ? move.Forward : -move.Forward)
                , fromSquare.Column + move.ToRight);
        }
    Public Function GetSquare(fromSquare As Square, move As Movement) As Square
        Dim piece As ChessPiece = fromSquare.Piece
        If piece Is Nothing Then Return Nothing

        Return GetSquare(fromSquare.Row _
                         + IIf(piece.Color = PlayerOnTop, move.Forward, -move.Forward) _
                         , fromSquare.Column + move.ToRight)
    End Function

Si en la celda origen no hay ninguna pieza el método no devuelve nada. Si la hay calcula la celda destino teniendo en cuenta el color de la pieza, ya que un movimiento hacia adelante de una pieza blanca no va en la misma dirección que un movimiento hacia adelante de una pieza negra.

En la clase abstracta ChessPiece definiré un método CanBeDestination que determinará si una celda puede ser destino del movimiento de la pieza. Básicamente una pieza se puede mover a una celda si la celda está vacía o si está ocupada por una pieza de otro color (en este caso se "comería" a la otra pieza).

        protected virtual bool CanBeDestination(Square destinationSquare)
        {
            if (destinationSquare == null) return false;

            return (destinationSquare.Piece == null || destinationSquare.Piece.Color != Color);
        }
    Public Function CanBeDestination(destinationSquare As Square) As Boolean
        If destinationSquare Is Nothing Then Return False

        Return (destinationSquare.Piece Is Nothing OrElse destinationSquare.Piece.Color <> Color)
    End Function

También crearé un método DestinationSquaresForMoves que recibirá como parámetro una serie de movimientos y devolverá las celdas a las que podría moverse la pieza realizando esos movimientos. Para ello obtengo para cada movimiento cual sería la celda destino utilizando el método GetSquare y compruebo que la celda sea un destino válido utilizando el método CanBeDestination.

        protected IEnumerable<Square> DestinationSquaresForMoves(IEnumerable<Movement> moves)
        {
            Chessboard board = BoardSquare.Board;
            List<Square> squares = new List<Square>();
            foreach (Movement move in moves)
            {
                Square destination = board.GetSquare(BoardSquare, move);
                if (CanBeDestination(destination))
                    squares.Add(destination);
            }
            return squares;
        }
    Protected Function DestinationSquaresForMoves(
        moves As IEnumerable(Of Movement)) As IEnumerable(Of Square)

        Dim board As Chessboard = BoardSquare.Board
        Dim squares As New List(Of Square)
        For Each move As Movement In moves
            Dim destination As Square = board.GetSquare(BoardSquare, move)
            If CanBeDestination(destination) Then
                squares.Add(destination)
            End If
        Next
        Return squares
    End Function

Por último crearé un método, similar al anterior, que devolverá los posibles celdas destino al moverse la pieza en una dirección. Este método nos será útil para obtener los posibles destinos de los movimientos de torres, alfiles y reinas. La dirección en la que se moverá se determinará por dos valores enteros: un incremento hacia adelante (si es negativo indicará una dirección hacia atrás) y un incremento hacia la derecha (si es negativo indicará una dirección hacia la izquierda).

        protected IEnumerable<Square> DestinationSquaresOnOneDirection(
            int forwardIncrement, int rightIncrement)
        {
            Chessboard board = BoardSquare.Board;

            List<Square> squares = new List<Square>();
            int forward = forwardIncrement;
            int right = rightIncrement;
            bool pieceOrEndFounded = false;
            while (!pieceOrEndFounded)
            {
                Square destination = board.GetSquare(BoardSquare, new Movement { Forward = forward, ToRight = right });
                if (CanBeDestination(destination))
                    squares.Add(destination);
                pieceOrEndFounded = (destination == null || destination.Piece != null);
                forward += forwardIncrement;
                right += rightIncrement;
            }
            return squares;
        }
    Protected Function DestinationSquaresOnOneDirection(
        forwardIncrement As Integer, rightIncrement As Integer) As IEnumerable(Of Square)

        Dim board As Chessboard = BoardSquare.Board

        Dim squares As New List(Of Square)
        Dim forward As Integer = forwardIncrement
        Dim right As Integer = rightIncrement
        Dim pieceOrEndFounded As Boolean = False

        Do While Not pieceOrEndFounded
            Dim destination As Square = board.GetSquare(BoardSquare,
                    New Movement() With {.Forward = forward, .ToRight = right})
            If CanBeDestination(destination) Then
                squares.Add(destination)
            End If
            pieceOrEndFounded = (destination Is Nothing OrElse destination.Piece IsNot Nothing)
            forward += forwardIncrement
            right += rightIncrement
        Loop
        Return squares
    End Function

Básicamente el método va recorriendo las celdas en la dirección especificada y añadiéndolas a la lista a devolver hasta que llega al final del tablero o a una celda ocupada por otra pieza.

Bien, con todas estas "armas" ya estamos en condiciones de empezar a implementar las clases para los diferentes tipos de piezas:




El rey


Para implementar la funcionalidad del rey voy a crear una clase King que heredará de la clase ChessPiece y en la que deberemos sobrescribir el método GetDestinationSquares que definimos como abstracto en la clase base.

Este método debe devolver las celdas a las que puede moverse la pieza para lo que utilizaré el método DestinationSquareForMoves pasándole los diferentes movimientos que puede realizar el rey.

Posibles movimientos del rey

De esta forma la clase King quedaría:

    class King : ChessPiece
    {

        public King(PlayerColor color) : base(color) { }

        private Movement[] moves =
        {
            new Movement() {Forward=1, ToRight=-1 },
            new Movement() {Forward=1, ToRight=0 },
            new Movement() {Forward=1, ToRight=1 },
            new Movement() {Forward=0, ToRight=-1 },
            new Movement() {Forward=0, ToRight=1 },
            new Movement() {Forward=-1, ToRight=-1 },
            new Movement() {Forward=-1, ToRight=0 },
            new Movement() {Forward=-1, ToRight=1 }
        };

        public override IEnumerable<Square> GetDestinationSquares()
        {
            if (BoardSquare == null) return null;

            return DestinationSquaresForMoves(moves);
        }

    }
Class King
    Inherits ChessPiece

    Public Sub New(pieceColor As PlayerColor)
        MyBase.New(pieceColor)
    End Sub

    Private moves As Movement() = {
            New Movement() With {.Forward = 1, .ToRight = -1},
            New Movement() With {.Forward = 1, .ToRight = 0},
            New Movement() With {.Forward = 1, .ToRight = 1},
            New Movement() With {.Forward = 0, .ToRight = -1},
            New Movement() With {.Forward = 0, .ToRight = 1},
            New Movement() With {.Forward = -1, .ToRight = -1},
            New Movement() With {.Forward = -1, .ToRight = 0},
            New Movement() With {.Forward = -1, .ToRight = 1}
        }

    Public Overrides Function GetDestinationSquares() As IEnumerable(Of Square)
        If BoardSquare Is Nothing Then Return Nothing

        Return DestinationSquaresForMoves(moves)
    End Function

End Class

La Torre


De forma similar definiré la clase Rook que definirá el comportamiento del la torre. La única diferencia es que para obtener las posibles celdas destino utilizaré el método DestinationSquaresOnOneDirection con las cuatro direcciones en las que se puede mover la torre: hacia la izquierda (Forward: 0, ToRight: -1), hacia la derecha (Forward: 0, ToRight: 1),  hacia adelante (Forward: 1, ToRight: 0) y hacia atrás (Forward: -1, ToRight: 0).

    class Rook : ChessPiece
    {

        public Rook(PlayerColor color) : base(color) { }

        public override IEnumerable<Square> GetDestinationSquares()
        {
            if (BoardSquare == null) return null;

            List<Square> posibleSquares = new List<Square>();
            posibleSquares.AddRange(DestinationSquaresOnOneDirection(0, -1));
            posibleSquares.AddRange(DestinationSquaresOnOneDirection(0, 1));
            posibleSquares.AddRange(DestinationSquaresOnOneDirection(1, 0));
            posibleSquares.AddRange(DestinationSquaresOnOneDirection(-1, 0));

            return posibleSquares;
        }

    }
Class Rook
    Inherits ChessPiece

    Public Sub New(pieceColor As PlayerColor)
        MyBase.New(pieceColor)
    End Sub

    Public Overrides Function GetDestinationSquares() As IEnumerable(Of Square)
        If BoardSquare Is Nothing Then Return Nothing

        Dim posibleSquares As New List(Of Square)
        posibleSquares.AddRange(DestinationSquaresOnOneDirection(0, -1))
        posibleSquares.AddRange(DestinationSquaresOnOneDirection(0, 1))
        posibleSquares.AddRange(DestinationSquaresOnOneDirection(1, 0))
        posibleSquares.AddRange(DestinationSquaresOnOneDirection(-1, 0))

        Return posibleSquares
    End Function
End Class

El Alfil, la Reina y el Caballo

Siguiendo el ejemplo del rey y la torre podemos definir las clases Bishop para el alfil, Queen para la reina y Knight para el caballo.

    class Bishop : ChessPiece
    {

        public Bishop(PlayerColor color) : base(color) { }

        public override IEnumerable<Square> GetDestinationSquares()
        {
            if (BoardSquare == null) return null;

            List<Square> posibleSquares = new List<Square>();
            posibleSquares.AddRange(DestinationSquaresOnOneDirection(1, -1));
            posibleSquares.AddRange(DestinationSquaresOnOneDirection(1, 1));
            posibleSquares.AddRange(DestinationSquaresOnOneDirection(-1, -1));
            posibleSquares.AddRange(DestinationSquaresOnOneDirection(-1, 1));

            return posibleSquares;
        }
    }

    class Queen : ChessPiece
    {

        public Queen(PlayerColor color) : base(color) { }

        public override IEnumerable<Square> GetDestinationSquares()
        {
            if (BoardSquare == null) return null;

            List<Square> posibleSquares = new List<Square>();
            posibleSquares.AddRange(DestinationSquaresOnOneDirection(1, -1));
            posibleSquares.AddRange(DestinationSquaresOnOneDirection(1, 0));
            posibleSquares.AddRange(DestinationSquaresOnOneDirection(1, 1));
            posibleSquares.AddRange(DestinationSquaresOnOneDirection(0, -1));
            posibleSquares.AddRange(DestinationSquaresOnOneDirection(0, 1));
            posibleSquares.AddRange(DestinationSquaresOnOneDirection(-1, -1));
            posibleSquares.AddRange(DestinationSquaresOnOneDirection(-1, 0));
            posibleSquares.AddRange(DestinationSquaresOnOneDirection(-1, 1));

            return posibleSquares;
        }
    }

    class Knight : ChessPiece
    {

        public Knight(PlayerColor color) : base(color) { }

        private Movement[] moves =
        {
            new Movement {Forward=1, ToRight=-2 },
            new Movement {Forward=2, ToRight=-1 },
            new Movement {Forward=2, ToRight=1 },
            new Movement {Forward=1, ToRight=2 },
            new Movement {Forward=-1, ToRight=-2 },
            new Movement {Forward=-2, ToRight=-1 },
            new Movement {Forward=-2, ToRight=1 },
            new Movement {Forward=-1, ToRight=2 }
        };

        public override IEnumerable<Square> GetDestinationSquares()
        {
            if (BoardSquare == null) return null;

            return DestinationSquaresForMoves(moves);
        }
    }
Class Bishop
    Inherits ChessPiece

    Public Sub New(pieceColor As PlayerColor)
        MyBase.New(pieceColor)
    End Sub

    Public Overrides Function GetDestinationSquares() As IEnumerable(Of Square)
        If BoardSquare Is Nothing Then Return Nothing

        Dim posibleSquares As New List(Of Square)
        posibleSquares.AddRange(DestinationSquaresOnOneDirection(1, -1))
        posibleSquares.AddRange(DestinationSquaresOnOneDirection(1, 1))
        posibleSquares.AddRange(DestinationSquaresOnOneDirection(-1, -1))
        posibleSquares.AddRange(DestinationSquaresOnOneDirection(-1, 1))

        Return posibleSquares

    End Function

End Class

Class Queen
    Inherits ChessPiece

    Public Sub New(pieceColor As PlayerColor)
        MyBase.New(pieceColor)
    End Sub

    Public Overrides Function GetDestinationSquares() As IEnumerable(Of Square)
        If BoardSquare Is Nothing Then Return Nothing

        Dim posibleSquares As New List(Of Square)
        posibleSquares.AddRange(DestinationSquaresOnOneDirection(1, -1))
        posibleSquares.AddRange(DestinationSquaresOnOneDirection(1, 0))
        posibleSquares.AddRange(DestinationSquaresOnOneDirection(1, 1))
        posibleSquares.AddRange(DestinationSquaresOnOneDirection(0, -1))
        posibleSquares.AddRange(DestinationSquaresOnOneDirection(0, 1))
        posibleSquares.AddRange(DestinationSquaresOnOneDirection(-1, -1))
        posibleSquares.AddRange(DestinationSquaresOnOneDirection(-1, 0))
        posibleSquares.AddRange(DestinationSquaresOnOneDirection(-1, 1))

        Return posibleSquares

    End Function
End Class

Class Knight
    Inherits ChessPiece

    Public Sub New(pieceColor As PlayerColor)
        MyBase.New(pieceColor)
    End Sub

    Private moves As Movement() = {
            New Movement() With {.Forward = 1, .ToRight = -2},
            New Movement() With {.Forward = 2, .ToRight = -1},
            New Movement() With {.Forward = 2, .ToRight = 1},
            New Movement() With {.Forward = 1, .ToRight = 2},
            New Movement() With {.Forward = -1, .ToRight = -2},
            New Movement() With {.Forward = -2, .ToRight = -1},
            New Movement() With {.Forward = -2, .ToRight = 1},
            New Movement() With {.Forward = -1, .ToRight = 2}
        }

    Public Overrides Function GetDestinationSquares() As IEnumerable(Of Square)
        If BoardSquare Is Nothing Then Return Nothing

        Return DestinationSquaresForMoves(moves)
    End Function
End Class

El peón

El caso del peón es un caso especial, sus movimientos dependen de diferentes variables que las del resto de piezas:
  • Puede realizar un movimiento de dos celdas hacia adelante siempre que se encuentre en la posición inicial y las dos celdas de delante estén vacías
  • Puede realizar un movimiento de una celda hacia adelante si ésta está vacía
  • Puede realizar un movimiento de una posición hacia adelante y otra hacia izquierda o derecha siempre que la celda destino esté ocupada por una pieza de otro color
Así que en el caso de la clase Pawn que definirá el comportamiento del peón deberemos añadir algo más de lógica al método GetDestinationSquares.

    class Pawn : ChessPiece
    {

        public Pawn(PlayerColor color) : base(color) { }

        public override IEnumerable<Square> GetDestinationSquares()
        {
            if (BoardSquare == null) return null;

            Chessboard board = BoardSquare.Board;
            bool isInStartPosition = (board.GetSquare(BoardSquare, new Movement { Forward = -2, ToRight = 0 }) == null);

            List<Square> posibleSquares = new List<Square>();
            Square destinationSquare = board.GetSquare(BoardSquare, new Movement { Forward = 1, ToRight = 0 });
            if (destinationSquare != null && destinationSquare.Piece == null)
            {
                posibleSquares.Add(destinationSquare);
                if (isInStartPosition)
                {
                    destinationSquare = board.GetSquare(BoardSquare, new Movement { Forward = 2, ToRight = 0 });
                    if (destinationSquare != null && destinationSquare.Piece == null)
                        posibleSquares.Add(destinationSquare);
                }
            }
            destinationSquare = board.GetSquare(BoardSquare, new Movement { Forward = 1, ToRight = -1 });
            if (destinationSquare != null && destinationSquare.Piece != null && destinationSquare.Piece.Color != Color)
                posibleSquares.Add(destinationSquare);
            destinationSquare = board.GetSquare(BoardSquare, new Movement { Forward = 1, ToRight = 1 });
            if (destinationSquare != null && destinationSquare.Piece != null && destinationSquare.Piece.Color != Color)
                posibleSquares.Add(destinationSquare);

            return posibleSquares;
        }
    }
Class Pawn
    Inherits ChessPiece

    Public Sub New(pieceColor As PlayerColor)
        MyBase.New(pieceColor)
    End Sub

    Public Overrides Function GetDestinationSquares() As IEnumerable(Of Square)
        If BoardSquare Is Nothing Then Return Nothing

        Dim board As Chessboard = BoardSquare.Board
        Dim isInStartPosition =
            (board.GetSquare(BoardSquare, New Movement With {.Forward = -2, .ToRight = 0}) Is Nothing)

        Dim posibleSquares As New List(Of Square)
        Dim destinationSquare =
            board.GetSquare(BoardSquare, New Movement With {.Forward = 1, .ToRight = 0})
        If (destinationSquare IsNot Nothing AndAlso destinationSquare.Piece Is Nothing) Then
            posibleSquares.Add(destinationSquare)
            If isInStartPosition Then
                destinationSquare =
                    board.GetSquare(BoardSquare, New Movement With {.Forward = 2, .ToRight = 0})
                If (destinationSquare IsNot Nothing AndAlso destinationSquare.Piece Is Nothing) Then
                    posibleSquares.Add(destinationSquare)
                End If
            End If
        End If
        destinationSquare =
            board.GetSquare(BoardSquare, New Movement With {.Forward = 1, .ToRight = -1})
        If (destinationSquare IsNot Nothing AndAlso destinationSquare.Piece IsNot Nothing _
            AndAlso destinationSquare.Piece.Color <> Color) Then
            posibleSquares.Add(destinationSquare)
        End If
        destinationSquare =
            board.GetSquare(BoardSquare, New Movement With {.Forward = 1, .ToRight = 1})
        If destinationSquare IsNot Nothing AndAlso destinationSquare.Piece IsNot Nothing _
            AndAlso destinationSquare.Piece.Color <> Color Then
            posibleSquares.Add(destinationSquare)
        End If

        Return posibleSquares

    End Function

End Class


De esta forma tenemos definidas todas las piezas del juego. En la próxima entrada continuaré añadiendo funcionalidad a las celdas y el tablero de juego.


3 comentarios: