package dev.kske.chess.board; import java.util.HashMap; import java.util.Map; import java.util.Objects; import java.util.regex.Matcher; import java.util.regex.Pattern; import dev.kske.chess.board.Piece.Color; /** * Project: Chess
* File: Move.java
* Created: 02.07.2019
* * @since Chess v0.1-alpha * @author Kai S. K. Engelbart */ public class Move { protected final Position pos, dest; protected final int xDist, yDist, xSign, ySign; /** * Creates an instance of {@link Move}. * * @param pos the position of this move * @param dest the destination of this move */ public Move(Position pos, Position dest) { this.pos = pos; this.dest = dest; 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); } /** * Creates an instance of {@link Move}. * * @param xPos the horizontal position of this move * @param yPos the vertical position of this move * @param xDest the horizontal destination of this move * @param yDest the vertical destination of this move */ public Move(int xPos, int yPos, int xDest, int yDest) { this(new Position(xPos, yPos), new Position(xDest, yDest)); } /** * Executed this move on a board. * * @param board the board to execute this move on. */ 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); } /** * Reverts this move on a board. * * @param board the board to revert this move on * @param capturedPiece the piece to place at the destination of this move * (used * for reinstating captured pieces) */ 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); } /** * @return a new move containing this move's destination as its position and * this move's position as its destination */ public Move invert() { return new Move(dest, pos); } /** * Constructs a move from a string representation in Long Algebraic Notation * (LAN). * * @param move the LAN string to construct the move from * @return the constructed move */ public static Move fromLAN(String move) { Position pos = Position.fromLAN(move.substring(0, 2)); Position dest = Position.fromLAN(move.substring(2)); if (move.length() == 5) try { return new PawnPromotion( pos, dest, Piece.fromFirstChar(move.charAt(4)) ); } catch (Exception e) { e.printStackTrace(); return null; } return new Move(pos, dest); } /** * Generates a string representation of this move in Long Algebraic Notation * (LAN). * * @return the LAN string */ public String toLAN() { return getPos().toLAN() + getDest().toLAN(); } /** * Converts a move string from standard algebraic notation to a {@link Move} * object. * * @param sanMove the move string to convert from * @param board the board on which the move has to be executed * @return the converted {@link Move} object */ public static Move fromSAN(String sanMove, Board board) { Map patterns = new HashMap<>(); patterns.put( "pieceMove", 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])(?[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}|\\#)?$" ) ); for (Map.Entry entry : patterns.entrySet()) { Matcher m = entry.getValue().matcher(sanMove); if (m.find()) { Position pos = null, dest = null; Move move = null; switch (entry.getKey()) { case "pieceMove": dest = Position.fromLAN(m.group("toSquare")); if (m.group("fromSquare") != null) pos = Position.fromLAN(m.group("fromSquare")); else { Class pieceClass = Piece .fromFirstChar(m.group("pieceType").charAt(0)); char file; int rank; if (m.group("fromFile") != null) { file = m.group("fromFile").charAt(0); rank = board.get(pieceClass, file); pos = Position .fromLAN(String.format("%c%d", file, rank)); } else if (m.group("fromRank") != null) { rank = Integer.parseInt( m.group("fromRank").substring(0, 1) ); file = board.get(pieceClass, rank); pos = Position.fromLAN( String.format("%c%d", file, rank) ); } else pos = board.get(pieceClass, dest); } move = new Move(pos, dest); break; case "pawnCapture": char file = m.group("fromFile").charAt(0); int rank = m.group("fromRank") == null ? board.get(Pawn.class, file) : Integer.parseInt(m.group("fromRank")); dest = Position.fromLAN(m.group("toSquare")); pos = Position .fromLAN(String.format("%c%d", file, rank)); if (m.group("promotedTo") != null) try { move = new PawnPromotion( pos, dest, Piece.fromFirstChar(m.group("promotedTo").charAt(0)) ); } catch (Exception e) { e.printStackTrace(); } else move = new Move(pos, dest); break; case "pawnPush": dest = Position.fromLAN(m.group("toSquare")); int step = board.getLog().getActiveColor() == Color.WHITE ? 1 : -1; // One step forward if (board.getBoardArr()[dest.x][dest.y + step] != null) pos = new Position(dest.x, dest.y + step); // Double step forward else pos = new Position(dest.x, dest.y + 2 * step); if (m.group("promotedTo") != null) try { move = new PawnPromotion( pos, dest, Piece.fromFirstChar(m.group("promotedTo").charAt(0)) ); } catch (Exception e) { e.printStackTrace(); } else move = new Move(pos, dest); break; case "castling": pos = new Position( 4, board.getLog().getActiveColor() == Color.WHITE ? 7 : 0 ); dest = new Position( m.group("kingside") != null ? 6 : 2, pos.y ); move = new Castling(pos, dest); break; } return move; } } return null; } /** * Generates a string representation of this move in Standard Algebraic * Notation * (SAN). * * @param board the {@link Board} providing the context of this move * @return the SAN string */ public String toSAN(Board board) { final Piece piece = board.get(pos); StringBuilder sb = new StringBuilder(8); // Piece symbol if (!(piece instanceof Pawn)) sb.append(Character.toUpperCase(piece.firstChar())); // Position // TODO: Deconstruct position into optional file or rank // Omit position if the move is a pawn push if (!(piece instanceof Pawn && xDist == 0)) sb.append(pos.toLAN()); // Capture indicator if (board.get(dest) != null) sb.append('x'); // Destination sb.append(dest.toLAN()); return sb.toString(); } /** * @return {@code true} if the move is purely horizontal */ public boolean isHorizontal() { return getyDist() == 0; } /** * @return {@code true} if the move is purely vertical */ public boolean isVertical() { return getxDist() == 0; } /** * @return {@code true} if the move is diagonal */ public boolean isDiagonal() { return getxDist() == getyDist(); } @Override public String toString() { return toLAN(); } @Override public int hashCode() { return Objects.hash( getDest(), getPos(), getxDist(), getxSign(), getyDist(), getySign() ); } @Override public boolean equals(Object obj) { if (this == obj) return true; 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()) && getxDist() == other.getxDist() && getxSign() == other.getxSign() && getyDist() == other.getyDist() && getySign() == other.getySign(); } /** * @return the position */ public Position getPos() { return pos; } /** * @return the destination */ public Position getDest() { return dest; } /** * @return the x distance */ public int getxDist() { return xDist; } /** * @return the y distance */ public int getyDist() { return yDist; } /** * @return the sign of the x distance */ public int getxSign() { return xSign; } /** * @return the sign of the y distance */ public int getySign() { return ySign; } }