From 2821f30dbe61c53f0c5921d44deefb4863a16ecc Mon Sep 17 00:00:00 2001 From: kske Date: Sun, 3 Nov 2019 15:46:08 +0100 Subject: [PATCH] Implemented proper pawn promotion * Moved move execution and reversion to the Move class * Removed Move.Type enumeration * Added Move subclasses Castling, EnPassant and PawnPromotion * Generating all four possible pawn promotions in the Pawn class * Temporarily removed special move support from NaturalPlayer --- src/dev/kske/chess/board/Board.java | 89 ++++----------------- src/dev/kske/chess/board/Castling.java | 35 ++++++++ src/dev/kske/chess/board/EnPassant.java | 35 ++++++++ src/dev/kske/chess/board/King.java | 19 ++--- src/dev/kske/chess/board/Move.java | 32 ++++---- src/dev/kske/chess/board/Pawn.java | 68 +++++++--------- src/dev/kske/chess/board/PawnPromotion.java | 46 +++++++++++ src/dev/kske/chess/game/NaturalPlayer.java | 27 ++----- 8 files changed, 189 insertions(+), 162 deletions(-) create mode 100644 src/dev/kske/chess/board/Castling.java create mode 100644 src/dev/kske/chess/board/EnPassant.java create mode 100644 src/dev/kske/chess/board/PawnPromotion.java diff --git a/src/dev/kske/chess/board/Board.java b/src/dev/kske/chess/board/Board.java index 35f0b82..7304f64 100644 --- a/src/dev/kske/chess/board/Board.java +++ b/src/dev/kske/chess/board/Board.java @@ -61,9 +61,6 @@ public class Board { Piece piece = getPos(move); if (piece == null || !piece.isValidMove(move)) return false; else { - // Set type after validation - if (move.getType() == Move.Type.UNKNOWN) move.type = Move.Type.NORMAL; - // Move piece move(move); @@ -86,37 +83,8 @@ public class Board { Piece piece = getPos(move); Piece capturePiece = getDest(move); - switch (move.getType()) { - case PAWN_PROMOTION: - setPos(move, null); - // TODO: Select promotion - setDest(move, new Queen(piece.getColor(), this)); - break; - case EN_PASSANT: - setDest(move, piece); - setPos(move, null); - boardArr[move.getDest().x][move.getDest().y - move.getySign()] = null; - break; - case CASTLING: - // Move the king - setDest(move, piece); - setPos(move, null); - - // Move the rook - Move rookMove = move.getDest().x == 6 ? new Move(7, move.getPos().y, 5, move.getPos().y) // Kingside - : new Move(0, move.getPos().y, 3, move.getPos().y); // Queenside - setDest(rookMove, getPos(rookMove)); - setPos(rookMove, null); - break; - case UNKNOWN: - System.err.printf("Move of unknown type %s found!%n", move); - case NORMAL: - setDest(move, piece); - setPos(move, null); - break; - default: - System.err.printf("Move %s of unimplemented type found!%n", move); - } + // Execute the move + move.execute(this); // Update the king's position if the moved piece is the king if (piece.getType() == Type.KING) kingPos.put(piece.getColor(), move.getDest()); @@ -136,15 +104,15 @@ public class Board { Pattern.compile( "^(?[NBRQK])(?:(?[a-h])|(?[1-8])|(?[a-h][1-8]))?x?(?[a-h][1-8])(?:\\+{0,2}|\\#)$")); patterns.put("pawnCapture", - Pattern.compile("^(?[a-h])(?[1-8])?x(?[a-h][1-8])(?[NBRQK])?(?:\\+{0,2}|\\#)?$")); - patterns.put("pawnPush", Pattern.compile("^(?[a-h][1-8])(?[NBRQK])?(?:\\+{0,2}|\\#)$")); + Pattern.compile("^(?[a-h])(?[1-8])?x(?[a-h][1-8])(?[NBRQ])?(?:\\+{0,2}|\\#)?$")); + patterns.put("pawnPush", Pattern.compile("^(?[a-h][1-8])(?[NBRQ])?(?:\\+{0,2}|\\#)$")); patterns.put("castling", Pattern.compile("^(?O-O-O)|(?O-O)(?:\\+{0,2}|\\#)?$")); patterns.forEach((patternName, pattern) -> { Matcher m = pattern.matcher(sanMove); if (m.find()) { - Position pos = null, dest = null; - Move.Type moveType = Move.Type.NORMAL; + Position pos = null, dest = null; + Move move = null; switch (patternName) { case "pieceMove": dest = Position.fromLAN(m.group("toSquare")); @@ -163,12 +131,16 @@ public class Board { pos = Position.fromLAN(String.format("%c%d", file, rank)); } else pos = get(type, dest); } + move = new Move(pos, dest); break; case "pawnCapture": dest = Position.fromLAN(m.group("toSquare")); char file = m.group("fromFile").charAt(0); int rank = m.group("fromRank") == null ? get(Type.PAWN, file) : Integer.parseInt(m.group("fromRank")); pos = Position.fromLAN(String.format("%c%d", file, rank)); + if (m.group("promotedTo") != null) { + + } break; case "pawnPush": dest = Position.fromLAN(m.group("toSquare")); @@ -183,10 +155,10 @@ public class Board { case "castling": pos = new Position(4, log.getActiveColor() == Color.WHITE ? 7 : 0); dest = new Position(m.group("kingside") != null ? 6 : 2, pos.y); - moveType = Move.Type.CASTLING; + move = new Castling(pos, dest); break; } - move(new Move(pos, dest, moveType)); + move(move); return; } }); @@ -196,40 +168,11 @@ public class Board { * Reverts the last move and removes it from the log. */ public void revert() { - MoveNode moveNode = log.getLast(); - Move move = moveNode.move; - Piece capturedPiece = moveNode.capturedPiece; + MoveNode moveNode = log.getLast(); + Move move = moveNode.move; - switch (move.getType()) { - case PAWN_PROMOTION: - setPos(move, new Pawn(getDest(move).getColor(), this)); - setDest(move, capturedPiece); - break; - case EN_PASSANT: - setPos(move, getDest(move)); - setDest(move, null); - boardArr[move.getDest().x][move.getDest().y - move.getySign()] = new Pawn(getPos(move).getColor().opposite(), this); - break; - case CASTLING: - // Move the king - setPos(move, getDest(move)); - setDest(move, null); - - // Move the rook - Move rookMove = move.getDest().x == 6 ? new Move(5, move.getPos().y, 7, move.getPos().y) // Kingside - : new Move(3, move.getPos().y, 0, move.getPos().y); // Queenside - setDest(rookMove, getPos(rookMove)); - setPos(rookMove, null); - break; - case UNKNOWN: - System.err.printf("Move of unknown type %s found!%n", move); - case NORMAL: - setPos(move, getDest(move)); - setDest(move, capturedPiece); - break; - default: - System.err.printf("Move %s of unimplemented type found!%n", move); - } + // Revert the move + move.revert(this, moveNode.capturedPiece); // Update the king's position if the moved piece is the king if (getPos(move).getType() == Type.KING) kingPos.put(getPos(move).getColor(), move.getPos()); diff --git a/src/dev/kske/chess/board/Castling.java b/src/dev/kske/chess/board/Castling.java new file mode 100644 index 0000000..e631a24 --- /dev/null +++ b/src/dev/kske/chess/board/Castling.java @@ -0,0 +1,35 @@ +package dev.kske.chess.board; + +/** + * Project: Chess
+ * File: Castling.java
+ * Created: 2 Nov 2019
+ * + * @since Chess v0.5-alpha + * @author Kai S. K. Engelbart + */ +public class Castling extends Move { + + private final Move rookMove; + + public Castling(Position pos, Position dest) { + super(pos, dest); + rookMove = dest.x == 6 ? new Move(7, pos.y, 5, pos.y) : new Move(0, pos.y, 3, pos.y); + } + + public Castling(int xPos, int yPos, int xDest, int yDest) { this(new Position(xPos, yPos), new Position(xDest, yDest)); } + + @Override + public void execute(Board board) { + // Move the king and the rook + super.execute(board); + rookMove.execute(board); + } + + @Override + public void revert(Board board, Piece capturedPiece) { + // Move the king and the rook + super.revert(board, capturedPiece); + rookMove.revert(board, null); + } +} diff --git a/src/dev/kske/chess/board/EnPassant.java b/src/dev/kske/chess/board/EnPassant.java new file mode 100644 index 0000000..69c48d9 --- /dev/null +++ b/src/dev/kske/chess/board/EnPassant.java @@ -0,0 +1,35 @@ +package dev.kske.chess.board; + +/** + * Project: Chess
+ * File: EnPassant.java
+ * Created: 2 Nov 2019
+ * + * @since Chess v0.5-alpha + * @author Kai S. K. Engelbart + */ +public class EnPassant extends Move { + + private final Position capturePos; + + public EnPassant(Position pos, Position dest) { + super(pos, dest); + capturePos = new Position(dest.x, dest.y - ySign); + } + + public EnPassant(int xPos, int yPos, int xDest, int yDest) { this(new Position(xPos, yPos), new Position(xDest, yDest)); } + + @Override + public void execute(Board board) { + super.execute(board); + board.set(capturePos, null); + } + + @Override + public void revert(Board board, Piece capturedPiece) { + super.revert(board, capturedPiece); + board.set(capturePos, new Pawn(board.get(pos).getColor().opposite(), board)); + } + + public Position getCapturePos() { return capturePos; } +} diff --git a/src/dev/kske/chess/board/King.java b/src/dev/kske/chess/board/King.java index f539e14..cde5a98 100644 --- a/src/dev/kske/chess/board/King.java +++ b/src/dev/kske/chess/board/King.java @@ -17,18 +17,9 @@ public class King extends Piece { @Override public boolean isValidMove(Move move) { - // Castling - if (move.getxDist() == 2 && move.getyDist() == 0) { - if (canCastleKingside()) { - move.type = Move.Type.CASTLING; - return true; - } - if (canCastleQueenside()) { - move.type = Move.Type.CASTLING; - return true; - } - } - return move.getxDist() <= 1 && move.getyDist() <= 1 && checkDestination(move); + return (move.getxDist() == 2 && move.getyDist() == 0 + && (move.getDest().x == 6 && canCastleKingside() || move.getDest().x == 2 && canCastleQueenside())) + || move.getxDist() <= 1 && move.getyDist() <= 1 && checkDestination(move); } @Override @@ -42,8 +33,8 @@ public class King extends Piece { } // Castling - if (canCastleKingside()) moves.add(new Move(pos, new Position(6, pos.y), Move.Type.CASTLING)); - if (canCastleQueenside()) moves.add(new Move(pos, new Position(2, pos.y), Move.Type.CASTLING)); + if (canCastleKingside()) moves.add(new Castling(pos, new Position(6, pos.y))); + if (canCastleQueenside()) moves.add(new Castling(pos, new Position(2, pos.y))); return moves; } diff --git a/src/dev/kske/chess/board/Move.java b/src/dev/kske/chess/board/Move.java index e6e9ae0..f71cf37 100644 --- a/src/dev/kske/chess/board/Move.java +++ b/src/dev/kske/chess/board/Move.java @@ -14,24 +14,33 @@ public class Move { protected final Position pos, dest; protected final int xDist, yDist, xSign, ySign; - public Type type; - public Move(Position pos, Position dest, Type type) { + public Move(Position pos, Position dest) { this.pos = pos; this.dest = dest; - this.type = type; xDist = Math.abs(dest.x - pos.x); yDist = Math.abs(dest.y - pos.y); xSign = (int) Math.signum(dest.x - pos.x); ySign = (int) Math.signum(dest.y - pos.y); } - public Move(Position pos, Position dest) { this(pos, dest, Type.NORMAL); } - public Move(int xPos, int yPos, int xDest, int yDest) { this(new Position(xPos, yPos), new Position(xDest, yDest)); } + // TODO: Pawn promotion public static Move fromLAN(String move) { return new Move(Position.fromLAN(move.substring(0, 2)), Position.fromLAN(move.substring(2))); } + public void execute(Board board) { + // Move the piece to the move's destination square and clean the old position + board.set(dest, board.get(pos)); + board.set(pos, null); + } + + public void revert(Board board, Piece capturedPiece) { + // Move the piece to the move's position square and clean the destination + board.set(pos, board.get(dest)); + board.set(dest, capturedPiece); + } + public String toLAN() { return getPos().toLAN() + getDest().toLAN(); } public boolean isHorizontal() { return getyDist() == 0; } @@ -44,7 +53,7 @@ public class Move { public String toString() { return String.format("%s -> %s", getPos(), getDest()); } @Override - public int hashCode() { return Objects.hash(getDest(), getPos(), getType(), getxDist(), getxSign(), getyDist(), getySign()); } + public int hashCode() { return Objects.hash(getDest(), getPos(), getxDist(), getxSign(), getyDist(), getySign()); } @Override public boolean equals(Object obj) { @@ -52,9 +61,8 @@ public class Move { if (obj == null) return false; if (getClass() != obj.getClass()) return false; Move other = (Move) obj; - return Objects.equals(getDest(), other.getDest()) && Objects.equals(getPos(), other.getPos()) && getType() == other.getType() - && getxDist() == other.getxDist() && getxSign() == other.getxSign() && getyDist() == other.getyDist() - && getySign() == other.getySign(); + return Objects.equals(getDest(), other.getDest()) && Objects.equals(getPos(), other.getPos()) && getxDist() == other.getxDist() + && getxSign() == other.getxSign() && getyDist() == other.getyDist() && getySign() == other.getySign(); } public Position getPos() { return pos; } @@ -68,10 +76,4 @@ public class Move { public int getxSign() { return xSign; } public int getySign() { return ySign; } - - public Type getType() { return type; } - - public static enum Type { - NORMAL, PAWN_PROMOTION, CASTLING, EN_PASSANT, UNKNOWN - } } diff --git a/src/dev/kske/chess/board/Pawn.java b/src/dev/kske/chess/board/Pawn.java index 90fb7a2..3f7ddf9 100644 --- a/src/dev/kske/chess/board/Pawn.java +++ b/src/dev/kske/chess/board/Pawn.java @@ -13,31 +13,18 @@ import java.util.List; */ public class Pawn extends Piece { - public Pawn(Color color, Board board) { - super(color, board); - } + public Pawn(Color color, Board board) { super(color, board); } @Override public boolean isValidMove(Move move) { boolean step = move.isVertical() && move.getyDist() == 1; boolean doubleStep = move.isVertical() && move.getyDist() == 2; boolean strafe = move.isDiagonal() && move.getxDist() == 1; - boolean enPassant = false; + boolean enPassant = strafe && move.getDest().equals(board.getLog().getEnPassant()); if (getColor() == Color.WHITE) doubleStep &= move.getPos().y == 6; else doubleStep &= move.getPos().y == 1; - // Mark move as pawn promotion if necessary - if (move.getySign() == 1 && move.getPos().y == 6 || move.getySign() == -1 && move.getPos().y == 1) - move.type = Move.Type.PAWN_PROMOTION; // TODO: Remove - - // Mark the move as en passant if necessary - if (strafe && move.getDest().equals(board.getLog().getEnPassant())) { - enPassant = true; - move.type = Move.Type.EN_PASSANT; - } - - return enPassant || (step ^ doubleStep ^ strafe) && move.getySign() == (getColor() == Color.WHITE ? -1 : 1) - && isFreePath(move); + return enPassant || (step ^ doubleStep ^ strafe) && move.getySign() == (getColor() == Color.WHITE ? -1 : 1) && isFreePath(move); } @Override @@ -53,48 +40,47 @@ public class Pawn extends Piece { @Override protected List getPseudolegalMoves(Position pos) { - List moves = new ArrayList<>(); - - int sign = getColor() == Color.WHITE ? -1 : 1; + List moves = new ArrayList<>(); + int sign = getColor() == Color.WHITE ? -1 : 1; + boolean pawnPromotion = sign == 1 && pos.y == 6 || sign == -1 && pos.y == 1; // Strafe left - if (pos.x > 0) { - Move move = new Move(pos, new Position(pos.x - 1, pos.y + sign)); - if (isFreePath(move)) moves.add(move); - } + if (pos.x > 0) addMoveIfValid(moves, pos, new Position(pos.x - 1, pos.y + sign), pawnPromotion); // Strafe right - if (pos.x < 7) { - Move move = new Move(pos, new Position(pos.x + 1, pos.y + sign)); - if (isFreePath(move)) moves.add(move); - } + if (pos.x < 7) addMoveIfValid(moves, pos, new Position(pos.x + 1, pos.y + sign), pawnPromotion); // Step forward - if (sign == 1 && pos.y < 7 || sign == -1 && pos.y > 0) { - Move move = new Move(pos, new Position(pos.x, pos.y + sign)); - if (isFreePath(move)) moves.add(move); - } + if (sign == 1 && pos.y < 7 || sign == -1 && pos.y > 0) addMoveIfValid(moves, pos, new Position(pos.x, pos.y + sign), pawnPromotion); // Double step forward - if (sign == 1 && pos.y == 1 || sign == -1 && pos.y == 6) { - Move move = new Move(pos, new Position(pos.x, pos.y + 2 * sign)); - if (isFreePath(move)) moves.add(move); - } - - // TODO: Check against instance - // Mark moves as pawn promotion if necessary - if (sign == 1 && pos.y == 6 || sign == -1 && pos.y == 1) - moves.parallelStream().forEach(m -> m.type = Move.Type.PAWN_PROMOTION); + if (sign == 1 && pos.y == 1 || sign == -1 && pos.y == 6) addMoveIfValid(moves, pos, new Position(pos.x, pos.y + 2 * sign), pawnPromotion); // Add en passant move if necessary if (board.getLog().getEnPassant() != null) { - Move move = new Move(pos, board.getLog().getEnPassant(), Move.Type.EN_PASSANT); + Move move = new EnPassant(pos, board.getLog().getEnPassant()); if (move.isDiagonal() && move.getxDist() == 1) moves.add(move); } return moves; } + private void addMoveIfValid(List moves, Position pos, Position dest, boolean pawnPromotion) { + Move move = new Move(pos, dest); + if (isFreePath(move)) { + if (pawnPromotion) { + try { + moves.add(new PawnPromotion(pos, dest, Queen.class)); + moves.add(new PawnPromotion(pos, dest, Rook.class)); + moves.add(new PawnPromotion(pos, dest, Knight.class)); + moves.add(new PawnPromotion(pos, dest, Bishop.class)); + } catch (NoSuchMethodException | SecurityException e) { + e.printStackTrace(); + } + } else moves.add(move); + } + } + @Override public Type getType() { return Type.PAWN; } } diff --git a/src/dev/kske/chess/board/PawnPromotion.java b/src/dev/kske/chess/board/PawnPromotion.java new file mode 100644 index 0000000..f8fa22b --- /dev/null +++ b/src/dev/kske/chess/board/PawnPromotion.java @@ -0,0 +1,46 @@ +package dev.kske.chess.board; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; + +import dev.kske.chess.board.Piece.Color; + +/** + * Project: Chess
+ * File: PawnPromotion.java
+ * Created: 2 Nov 2019
+ * + * @since Chess v0.5-alpha + * @author Kai S. K. Engelbart + */ +public class PawnPromotion extends Move { + + private Constructor promotionPieceConstructor; + + public PawnPromotion(Position pos, Position dest, Class promotionPiece) throws NoSuchMethodException, SecurityException { + super(pos, dest); + promotionPieceConstructor = promotionPiece.getDeclaredConstructor(Color.class, Board.class); + promotionPieceConstructor.setAccessible(true); + } + + public PawnPromotion(int xPos, int yPos, int xDest, int yDest, Class promotionPiece) + throws NoSuchMethodException, SecurityException { + this(new Position(xPos, yPos), new Position(xDest, yDest), promotionPiece); + } + + @Override + public void execute(Board board) { + try { + board.set(pos, promotionPieceConstructor.newInstance(board.get(pos).getColor(), board)); + super.execute(board); + } catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException | SecurityException e) { + e.printStackTrace(); + } + } + + @Override + public void revert(Board board, Piece capturedPiece) { + super.revert(board, capturedPiece); + board.set(pos, new Pawn(board.get(dest).getColor(), board)); + } +} diff --git a/src/dev/kske/chess/game/NaturalPlayer.java b/src/dev/kske/chess/game/NaturalPlayer.java index 9455b41..4e12c04 100644 --- a/src/dev/kske/chess/game/NaturalPlayer.java +++ b/src/dev/kske/chess/game/NaturalPlayer.java @@ -7,7 +7,6 @@ import java.util.stream.Collectors; import dev.kske.chess.board.Board; import dev.kske.chess.board.Move; -import dev.kske.chess.board.Move.Type; import dev.kske.chess.board.Piece.Color; import dev.kske.chess.board.Position; import dev.kske.chess.ui.OverlayComponent; @@ -37,42 +36,32 @@ public class NaturalPlayer extends Player implements MouseListener { } @Override - public void requestMove() { - moveRequested = true; - } + public void requestMove() { moveRequested = true; } @Override - public void cancelMove() { - moveRequested = false; - } + public void cancelMove() { moveRequested = false; } @Override - public void disconnect() { - overlayComponent.removeMouseListener(this); - } + public void disconnect() { overlayComponent.removeMouseListener(this); } @Override public void mousePressed(MouseEvent evt) { if (!moveRequested) return; if (pos == null) { - pos = new Position(evt.getPoint().x / overlayComponent.getTileSize(), - evt.getPoint().y / overlayComponent.getTileSize()); + pos = new Position(evt.getPoint().x / overlayComponent.getTileSize(), evt.getPoint().y / overlayComponent.getTileSize()); Board board = new Board(this.board); if (board.get(pos) != null && board.get(pos).getColor() == color) { - List positions = board.getMoves(pos) - .stream() - .map(move -> move.getDest()) - .collect(Collectors.toList()); + List positions = board.getMoves(pos).stream().map(move -> move.getDest()).collect(Collectors.toList()); overlayComponent.displayDots(positions); } else pos = null; } else { - Position dest = new Position(evt.getPoint().x / overlayComponent.getTileSize(), - evt.getPoint().y / overlayComponent.getTileSize()); + Position dest = new Position(evt.getPoint().x / overlayComponent.getTileSize(), evt.getPoint().y / overlayComponent.getTileSize()); overlayComponent.clearDots(); moveRequested = false; - game.onMove(NaturalPlayer.this, new Move(pos, dest, Type.UNKNOWN)); + // TODO: Special moves + game.onMove(NaturalPlayer.this, new Move(pos, dest)); pos = null; } }