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.
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.
Gracias, Asier, lo pruebo.......Abrazos...
ResponderEliminarBuenas tardes, lo terminaste?
ResponderEliminarSaludos
Siempre he dicho que si no vas a terminar lo que empiezas, será mejor no empezar.
ResponderEliminar