diff --git a/.classpath b/.classpath index e4beaea..8469573 100644 --- a/.classpath +++ b/.classpath @@ -7,6 +7,11 @@ + + + + + diff --git a/src/dev/kske/chess/board/Board.java b/src/dev/kske/chess/board/Board.java index 435b371..fbab10d 100644 --- a/src/dev/kske/chess/board/Board.java +++ b/src/dev/kske/chess/board/Board.java @@ -1,9 +1,11 @@ package dev.kske.chess.board; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -18,63 +20,48 @@ import dev.kske.chess.board.Piece.Type; */ public class Board { - private Piece[][] boardArr = new Piece[8][8]; - private Map kingPos = new HashMap<>(); - private Map> castlingRights = new HashMap<>(); - private Log log = new Log(); + private Piece[][] boardArr = new Piece[8][8]; + private Map kingPos = new HashMap<>(); + private Log log = new Log(); private static final Map positionScores; static { positionScores = new HashMap<>(); positionScores.put(Type.KING, - new int[][] { new int[] { -3, -4, -4, -5, -5, -4, -4, -3 }, + new int[][] { new int[] { -3, -4, -4, -5, -5, -4, -4, -3 }, new int[] { -3, -4, -4, -5, -4, -4, -4, -3 }, new int[] { -3, -4, -4, -5, -4, -4, -4, -3 }, new int[] { -3, -4, -4, -5, -4, -4, -4, -3 }, - new int[] { -3, -4, -4, -5, -4, -4, -4, -3 }, new int[] { -2, -3, -3, -2, -2, -2, -2, -1 }, - new int[] { -1, -2, -2, -2, -2, -2, -2, -1 }, new int[] { 2, 2, 0, 0, 0, 0, 2, 2 }, - new int[] { 2, 3, 1, 0, 0, 1, 3, 2 } }); + new int[] { -2, -3, -3, -2, -2, -2, -2, -1 }, new int[] { -1, -2, -2, -2, -2, -2, -2, -1 }, + new int[] { 2, 2, 0, 0, 0, 0, 2, 2 }, new int[] { 2, 3, 1, 0, 0, 1, 3, 2 } }); positionScores.put(Type.QUEEN, new int[][] { new int[] { -2, -1, -1, -1, -1, -1, -1, -2 }, new int[] { -1, 0, 0, 0, 0, 0, 0, -1 }, - new int[] { -1, 0, 1, 1, 1, 1, 0, -1 }, new int[] { -1, 0, 1, 1, 1, 1, 0, -1 }, - new int[] { 0, 0, 1, 1, 1, 1, 0, -1 }, new int[] { -1, 1, 1, 1, 1, 1, 0, -1 }, - new int[] { -1, 0, 1, 0, 0, 0, 0, -1 }, new int[] { -2, -1, -1, -1, -1, -1, -1, -2 } }); + new int[] { -1, 0, 1, 1, 1, 1, 0, -1 }, new int[] { -1, 0, 1, 1, 1, 1, 0, -1 }, new int[] { 0, 0, 1, 1, 1, 1, 0, -1 }, + new int[] { -1, 1, 1, 1, 1, 1, 0, -1 }, new int[] { -1, 0, 1, 0, 0, 0, 0, -1 }, + new int[] { -2, -1, -1, -1, -1, -1, -1, -2 } }); positionScores.put(Type.ROOK, - new int[][] { new int[] { 0, 0, 0, 0, 0, 0, 0, 0 }, new int[] { 1, 1, 1, 1, 1, 1, 1, 1 }, - new int[] { -1, 0, 0, 0, 0, 0, 0, -1 }, new int[] { -1, 0, 0, 0, 0, 0, 0, -1 }, - new int[] { -1, 0, 0, 0, 0, 0, 0, -1 }, new int[] { -1, 0, 0, 0, 0, 0, 0, -1 }, + new int[][] { new int[] { 0, 0, 0, 0, 0, 0, 0, 0 }, new int[] { 1, 1, 1, 1, 1, 1, 1, 1 }, new int[] { -1, 0, 0, 0, 0, 0, 0, -1 }, + new int[] { -1, 0, 0, 0, 0, 0, 0, -1 }, new int[] { -1, 0, 0, 0, 0, 0, 0, -1 }, new int[] { -1, 0, 0, 0, 0, 0, 0, -1 }, new int[] { -1, 0, 0, 0, 0, 0, 0, -1 }, new int[] { 0, 0, 0, 1, 1, 0, 0, 0 } }); positionScores.put(Type.KNIGHT, new int[][] { new int[] { -5, -4, -3, -3, -3, -3, -4, -5 }, new int[] { -4, -2, 0, 0, 0, 0, -2, -4 }, - new int[] { -3, 0, 1, 2, 2, 1, 0, -3 }, new int[] { -3, 1, 2, 2, 2, 2, 1, -3 }, - new int[] { -3, 0, 2, 2, 2, 2, 0, -1 }, new int[] { -3, 1, 1, 2, 2, 1, 1, -3 }, - new int[] { -4, -2, 0, 1, 1, 0, -2, -4 }, new int[] { -5, -4, -3, -3, -3, -3, -4, -5 } }); + new int[] { -3, 0, 1, 2, 2, 1, 0, -3 }, new int[] { -3, 1, 2, 2, 2, 2, 1, -3 }, new int[] { -3, 0, 2, 2, 2, 2, 0, -1 }, + new int[] { -3, 1, 1, 2, 2, 1, 1, -3 }, new int[] { -4, -2, 0, 1, 1, 0, -2, -4 }, + new int[] { -5, -4, -3, -3, -3, -3, -4, -5 } }); positionScores.put(Type.BISHOP, new int[][] { new int[] { -2, -1, -1, -1, -1, -1, -1, 2 }, new int[] { -1, 0, 0, 0, 0, 0, 0, -1 }, - new int[] { -1, 0, 1, 1, 1, 1, 0, -1 }, new int[] { -1, 1, 1, 1, 1, 1, 1, -1 }, - new int[] { -1, 0, 1, 1, 1, 1, 0, -1 }, new int[] { -1, 1, 1, 1, 1, 1, 1, -1 }, - new int[] { -1, 1, 0, 0, 0, 0, 1, -1 }, new int[] { -2, -1, -1, -1, -1, -1, -1, -2 } }); + new int[] { -1, 0, 1, 1, 1, 1, 0, -1 }, new int[] { -1, 1, 1, 1, 1, 1, 1, -1 }, new int[] { -1, 0, 1, 1, 1, 1, 0, -1 }, + new int[] { -1, 1, 1, 1, 1, 1, 1, -1 }, new int[] { -1, 1, 0, 0, 0, 0, 1, -1 }, + new int[] { -2, -1, -1, -1, -1, -1, -1, -2 } }); positionScores.put(Type.PAWN, - new int[][] { new int[] { 0, 0, 0, 0, 0, 0, 0, 0 }, new int[] { 5, 5, 5, 5, 5, 5, 5, 5 }, - new int[] { 1, 1, 2, 3, 3, 2, 1, 1 }, new int[] { 0, 0, 1, 3, 3, 1, 0, 0 }, - new int[] { 0, 0, 0, 2, 2, 0, 0, 0 }, new int[] { 0, 0, -1, 0, 0, -1, 0, 0 }, + new int[][] { new int[] { 0, 0, 0, 0, 0, 0, 0, 0 }, new int[] { 5, 5, 5, 5, 5, 5, 5, 5 }, new int[] { 1, 1, 2, 3, 3, 2, 1, 1 }, + new int[] { 0, 0, 1, 3, 3, 1, 0, 0 }, new int[] { 0, 0, 0, 2, 2, 0, 0, 0 }, new int[] { 0, 0, -1, 0, 0, -1, 0, 0 }, new int[] { 0, 1, 1, -2, -2, 1, 1, 0 }, new int[] { 0, 0, 0, 0, 0, 0, 0, 0 } }); } /** * Initializes the board with the default chess starting position. */ - public Board() { - initDefaultPositions(); - } - - /** - * Initializes the board with data from a FEN-string. - * - * @param fen The FEN-string to initialize the board from - */ - public Board(String fen) { - initFromFEN(fen); - } + public Board() { initDefaultPositions(); } /** * Creates a copy of another {@link Board} instance.
@@ -93,10 +80,6 @@ public class Board { } kingPos.putAll(other.kingPos); - Map whiteCastling = new HashMap<>(other.castlingRights.get(Color.WHITE)), - blackCastling = new HashMap<>(other.castlingRights.get(Color.BLACK)); - castlingRights.put(Color.WHITE, whiteCastling); - castlingRights.put(Color.BLACK, blackCastling); log = new Log(other.log, false); } @@ -156,8 +139,6 @@ public class Board { : new Move(0, move.pos.y, 3, move.pos.y); // Queenside setDest(rookMove, getPos(rookMove)); setPos(rookMove, null); - - getDest(rookMove).incMoveCounter(); break; case UNKNOWN: System.err.printf("Move of unknown type %s found!%n", move); @@ -169,17 +150,11 @@ public class Board { System.err.printf("Move %s of unimplemented type found!%n", move); } - // Increment move counter - getDest(move).incMoveCounter(); - - // Update the king's position if the moved piece is the king and castling - // availability + // Update the king's position if the moved piece is the king if (piece.getType() == Type.KING) kingPos.put(piece.getColor(), move.dest); // Update log - log.add(move, capturePiece, piece.getType() == Type.PAWN); - - updateCastlingRights(); + log.add(move, piece, capturePiece); } /** @@ -193,9 +168,7 @@ 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}|\\#)?$")); + 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}|\\#)$")); patterns.put("castling", Pattern.compile("^(?O-O-O)|(?O-O)(?:\\+{0,2}|\\#)?$")); @@ -226,8 +199,7 @@ public class Board { 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")); + int rank = m.group("fromRank") == null ? get(Type.PAWN, file) : Integer.parseInt(m.group("fromRank")); pos = Position.fromLAN(String.format("%c%d", file, rank)); break; case "pawnPush": @@ -280,8 +252,6 @@ public class Board { : new Move(3, move.pos.y, 0, move.pos.y); // Queenside setDest(rookMove, getPos(rookMove)); setPos(rookMove, null); - - getDest(rookMove).decMoveCounter(); break; case UNKNOWN: System.err.printf("Move of unknown type %s found!%n", move); @@ -293,38 +263,11 @@ public class Board { System.err.printf("Move %s of unimplemented type found!%n", move); } - // Decrement move counter - getPos(move).decMoveCounter(); - // Update the king's position if the moved piece is the king if (getPos(move).getType() == Type.KING) kingPos.put(getPos(move).getColor(), move.pos); // Update log log.removeLast(); - - updateCastlingRights(); - } - - private void updateCastlingRights() { - // White - if (new Position(4, 7).equals(kingPos.get(Color.WHITE))) { - final King king = (King) get(kingPos.get(Color.WHITE)); - castlingRights.get(Color.WHITE).put(Type.KING, king.canCastleKingside()); - castlingRights.get(Color.WHITE).put(Type.QUEEN, king.canCastleQueenside()); - } else { - castlingRights.get(Color.WHITE).put(Type.KING, false); - castlingRights.get(Color.WHITE).put(Type.QUEEN, false); - } - - // Black - if (new Position(4, 0).equals(kingPos.get(Color.BLACK))) { - final King king = (King) get(kingPos.get(Color.BLACK)); - castlingRights.get(Color.BLACK).put(Type.KING, king.canCastleKingside()); - castlingRights.get(Color.BLACK).put(Type.QUEEN, king.canCastleQueenside()); - } else { - castlingRights.get(Color.BLACK).put(Type.KING, false); - castlingRights.get(Color.BLACK).put(Type.QUEEN, false); - } } /** @@ -337,14 +280,11 @@ public class Board { List moves = new ArrayList<>(); for (int i = 0; i < 8; i++) for (int j = 0; j < 8; j++) - if (boardArr[i][j] != null && boardArr[i][j].getColor() == color) - moves.addAll(boardArr[i][j].getMoves(new Position(i, j))); + if (boardArr[i][j] != null && boardArr[i][j].getColor() == color) moves.addAll(boardArr[i][j].getMoves(new Position(i, j))); return moves; } - public List getMoves(Position pos) { - return get(pos).getMoves(pos); - } + public List getMoves(Position pos) { return get(pos).getMoves(pos); } /** * Checks, if the king is in check. @@ -356,9 +296,7 @@ public class Board { for (int i = 0; i < 8; i++) for (int j = 0; j < 8; j++) { Position pos = new Position(i, j); - if (get(pos) != null && get(pos).getColor() != color - && get(pos).isValidMove(new Move(pos, kingPos.get(color)))) - return true; + if (get(pos) != null && get(pos).getColor() != color && get(pos).isValidMove(new Move(pos, kingPos.get(color)))) return true; } return false; } @@ -386,8 +324,7 @@ public class Board { public GameState getGameEventType(Color color) { return checkCheck(color) ? checkCheckmate(color) ? GameState.CHECKMATE : GameState.CHECK - : getMoves(color).isEmpty() || log.getLast().halfmoveClock >= 50 ? GameState.STALEMATE - : GameState.NORMAL; + : getMoves(color).isEmpty() || log.getLast().halfmoveClock >= 50 ? GameState.STALEMATE : GameState.NORMAL; } /** @@ -469,160 +406,32 @@ public class Board { for (int j = 2; j < 6; j++) boardArr[i][j] = null; - // Initialize castling rights - Map whiteCastling = new HashMap<>(), blackCastling = new HashMap<>(); - whiteCastling.put(Type.KING, true); - whiteCastling.put(Type.QUEEN, true); - blackCastling.put(Type.KING, true); - blackCastling.put(Type.QUEEN, true); - castlingRights.put(Color.WHITE, whiteCastling); - castlingRights.put(Color.BLACK, blackCastling); - log.reset(); } - /** - * Initialized the board with a position specified in a FEN-encoded string. - * - * @param fen The FEN-encoded string representing target state of the board - */ - public void initFromFEN(String fen) { - String[] parts = fen.split(" "); - log.reset(); - - // Piece placement (from white's perspective) - String[] rows = parts[0].split("/"); - for (int i = 0; i < 8; i++) { - char[] places = rows[i].toCharArray(); - for (int j = 0, k = 0; k < places.length; j++, k++) { - if (Character.isDigit(places[k])) { - for (int l = j; l < Character.digit(places[k], 10); l++, j++) - boardArr[j][i] = null; - --j; - } else { - Color color = Character.isUpperCase(places[k]) ? Color.WHITE : Color.BLACK; - switch (Character.toLowerCase(places[k])) { - case 'k': - boardArr[j][i] = new King(color, this); - kingPos.put(color, new Position(j, i)); - break; - case 'q': - boardArr[j][i] = new Queen(color, this); - break; - case 'r': - boardArr[j][i] = new Rook(color, this); - break; - case 'n': - boardArr[j][i] = new Knight(color, this); - break; - case 'b': - boardArr[j][i] = new Bishop(color, this); - break; - case 'p': - boardArr[j][i] = new Pawn(color, this); - break; - default: - System.err.printf("Unknown character '%c' in board declaration of FEN string '%s'%n", - places[k], - fen); - } - } - } - } - - // Active color - log.setActiveColor(Color.fromFirstChar(parts[1].charAt(0))); - - // Castling rights - Map whiteCastling = new HashMap<>(), blackCastling = new HashMap<>(); - for (char c : parts[2].toCharArray()) - switch (c) { - case 'K': - whiteCastling.put(Type.KING, true); - case 'Q': - whiteCastling.put(Type.QUEEN, true); - case 'k': - blackCastling.put(Type.KING, true); - case 'q': - blackCastling.put(Type.QUEEN, true); - case '-': - break; - default: - System.err - .printf("Unknown character '%c' in castling rights declaration of FEN string '%s'", c, fen); - } - castlingRights.put(Color.WHITE, whiteCastling); - castlingRights.put(Color.BLACK, blackCastling); - - // En passant availability - if (!parts[3].equals("-")) log.setEnPassant(Position.fromLAN(parts[3])); - - // Halfmove clock - log.setHalfmoveClock(Integer.parseInt(parts[4])); - - // Fullmove counter - log.setFullmoveCounter(Integer.parseInt(parts[5])); + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + Arrays.deepHashCode(boardArr); + result = prime * result + Objects.hash(kingPos, log); + return result; } - /** - * @return a FEN-encoded string representing the board - */ - public String toFEN() { - StringBuilder sb = new StringBuilder(); - - // Piece placement (from white's perspective) - for (int i = 0; i < 8; i++) { - int emptyCount = 0; - for (int j = 0; j < 8; j++) { - final Piece piece = boardArr[j][i]; - if (piece == null) ++emptyCount; - else { // TODO: rnbqkbnr/pp1ppppp/8/2p5/4P3/5N2/PPPP1PPP/RNBQKB1R b - - 1 2 error - if (emptyCount != 0) { - sb.append(emptyCount); - emptyCount = 0; - } - char p = boardArr[j][i].getType().firstChar(); - sb.append(piece.getColor() == Color.WHITE ? Character.toUpperCase(p) : p); - } - } - if (emptyCount != 0) sb.append(emptyCount); - if (i < 7) sb.append('/'); - } - - // Active color - sb.append(" " + log.getActiveColor().firstChar()); - - // Castling Rights - sb.append(' '); - StringBuilder castlingSb = new StringBuilder(); - if (castlingRights.get(Color.WHITE).get(Type.KING)) castlingSb.append('K'); - if (castlingRights.get(Color.WHITE).get(Type.QUEEN)) castlingSb.append('Q'); - if (castlingRights.get(Color.BLACK).get(Type.KING)) castlingSb.append('k'); - if (castlingRights.get(Color.BLACK).get(Type.QUEEN)) castlingSb.append('q'); - if (castlingSb.length() == 0) sb.append("-"); - sb.append(castlingSb); - - final MoveNode lastMove = log.getLast(); - - // En passant availability - sb.append(" " + (lastMove == null || lastMove.enPassant == null ? "-" : lastMove.enPassant.toLAN())); - - // Halfmove clock - sb.append(" " + String.valueOf(lastMove == null ? 0 : lastMove.halfmoveClock)); - - // Fullmove counter - sb.append(" " + String.valueOf(lastMove == null ? 1 : lastMove.fullmoveCounter)); - - return sb.toString(); + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (getClass() != obj.getClass()) return false; + Board other = (Board) obj; + return Arrays.deepEquals(boardArr, other.boardArr) && Objects.equals(kingPos, other.kingPos) && Objects.equals(log, other.log); } /** * @param pos The position from which to return a piece * @return The piece at the position */ - public Piece get(Position pos) { - return boardArr[pos.x][pos.y]; - } + public Piece get(Position pos) { return boardArr[pos.x][pos.y]; } /** * Searches for a {@link Piece} inside a file (A - H). @@ -635,9 +444,7 @@ public class Board { public int get(Type type, char file) { int x = file - 97; for (int i = 0; i < 8; i++) - if (boardArr[x][i] != null && boardArr[x][i].getType() == type - && boardArr[x][i].getColor() == log.getActiveColor()) - return 8 - i; + if (boardArr[x][i] != null && boardArr[x][i].getType() == type && boardArr[x][i].getColor() == log.getActiveColor()) return 8 - i; return -1; } @@ -652,8 +459,7 @@ public class Board { public char get(Type type, int rank) { int y = rank - 1; for (int i = 0; i < 8; i++) - if (boardArr[i][y] != null && boardArr[i][y].getType() == type - && boardArr[i][y].getColor() == log.getActiveColor()) + if (boardArr[i][y] != null && boardArr[i][y].getType() == type && boardArr[i][y].getColor() == log.getActiveColor()) return (char) (i + 97); return '-'; } @@ -668,8 +474,7 @@ public class Board { public Position get(Type type, Position dest) { for (int i = 0; i < 8; i++) for (int j = 0; j < 8; j++) - if (boardArr[i][j] != null && boardArr[i][j].getType() == type - && boardArr[i][j].getColor() == log.getActiveColor()) { + if (boardArr[i][j] != null && boardArr[i][j].getType() == type && boardArr[i][j].getColor() == log.getActiveColor()) { Position pos = new Position(i, j); if (boardArr[i][j].isValidMove(new Move(pos, dest))) return pos; } @@ -682,25 +487,19 @@ public class Board { * @param pos The position to place the piece at * @param piece The piece to place */ - public void set(Position pos, Piece piece) { - boardArr[pos.x][pos.y] = piece; - } + public void set(Position pos, Piece piece) { boardArr[pos.x][pos.y] = piece; } /** * @param move The move from which position to return a piece * @return The piece at the position of the move */ - public Piece getPos(Move move) { - return get(move.pos); - } + public Piece getPos(Move move) { return get(move.pos); } /** * @param move The move from which destination to return a piece * @return The piece at the destination of the move */ - public Piece getDest(Move move) { - return get(move.dest); - } + public Piece getDest(Move move) { return get(move.dest); } /** * Places a piece at the position of a move. @@ -708,9 +507,7 @@ public class Board { * @param move The move at which position to place the piece * @param piece The piece to place */ - public void setPos(Move move, Piece piece) { - set(move.pos, piece); - } + public void setPos(Move move, Piece piece) { set(move.pos, piece); } /** * Places a piece at the destination of a move. @@ -718,9 +515,7 @@ public class Board { * @param move The move at which destination to place the piece * @param piece The piece to place */ - public void setDest(Move move, Piece piece) { - set(move.dest, piece); - } + public void setDest(Move move, Piece piece) { set(move.dest, piece); } /** * @return The board array diff --git a/src/dev/kske/chess/board/FENString.java b/src/dev/kske/chess/board/FENString.java new file mode 100644 index 0000000..536cecf --- /dev/null +++ b/src/dev/kske/chess/board/FENString.java @@ -0,0 +1,222 @@ +package dev.kske.chess.board; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import dev.kske.chess.board.Piece.Color; +import dev.kske.chess.exception.ChessException; + +/** + * Project: Chess
+ * File: FENString.java
+ * Created: 20 Oct 2019
+ *
+ * Represents a FEN string and enables parsing an existing FEN string or + * serializing a {@link Board} to one. + * + * @author Kai S. K. Engelbart + * @since Chess v0.4-alpha + */ +public class FENString { + + private Board board; + private String piecePlacement, castlingAvailability; + private int halfmoveClock, fullmoveNumber; + private Color activeColor; + private Position enPassantTargetSquare; + + /** + * Constructs a {@link FENString} representing the starting position + * {@code rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1}. + */ + public FENString() { + board = new Board(); + piecePlacement = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR"; + activeColor = Color.WHITE; + castlingAvailability = "KQkq"; + halfmoveClock = 0; + fullmoveNumber = 1; + } + + /** + * Constructs a {@link FENString} by parsing an existing string. + * + * @param fen the FEN string to parse + * @throws ChessException + */ + public FENString(String fen) throws ChessException { + // Check fen string against regex + Pattern fenPattern = Pattern.compile( + "^(?(?:[1-8nbrqkpNBRQKP]{1,8}\\/){7}[1-8nbrqkpNBRQKP]{1,8}) (?[wb]) (?-|[KQkq]{1,4}) (?-|[a-h][1-8]) (?\\d+) (?\\d+)$"); + Matcher matcher = fenPattern.matcher(fen); + if (!matcher.find()) throw new ChessException("FEN string does not match pattern " + fenPattern.pattern()); + + // Initialize data fields + piecePlacement = matcher.group("piecePlacement"); + activeColor = Color.fromFirstChar(matcher.group("activeColor").charAt(0)); + castlingAvailability = matcher.group("castlingAvailability"); + if (!matcher.group("enPassantTargetSquare").equals("-")) enPassantTargetSquare = Position.fromLAN(matcher.group("enPassantTargetSquare")); + halfmoveClock = Integer.parseInt(matcher.group("halfmoveClock")); + fullmoveNumber = Integer.parseInt(matcher.group("fullmoveNumber")); + + // Initialize and clean board + board = new Board(); + for (int i = 0; i < 8; i++) + for (int j = 0; j < 8; j++) + board.getBoardArr()[i][j] = null; + + // Parse individual fields + + // Piece placement + final String[] rows = piecePlacement.split("/"); + if (rows.length != 8) throw new ChessException("FEN string contains invalid piece placement"); + for (int i = 0; i < 8; i++) { + final char[] cols = rows[i].toCharArray(); + int j = 0; + for (char c : cols) { + + // Empty space + if (Character.isDigit(c)) { + j += Character.getNumericValue(c); + } else { + Color color = Character.isUpperCase(c) ? Color.WHITE : Color.BLACK; + switch (Character.toUpperCase(c)) { + case 'K': + board.getBoardArr()[j][i] = new King(color, board); + break; + case 'Q': + board.getBoardArr()[j][i] = new Queen(color, board); + break; + case 'R': + board.getBoardArr()[j][i] = new Rook(color, board); + break; + case 'N': + board.getBoardArr()[j][i] = new Knight(color, board); + break; + case 'B': + board.getBoardArr()[j][i] = new Bishop(color, board); + break; + case 'P': + board.getBoardArr()[j][i] = new Pawn(color, board); + break; + } + ++j; + } + } + } + + // Active color + board.getLog().setActiveColor(activeColor); + + // Castling availability + boolean castlingRights[] = new boolean[4]; + for (char c : castlingAvailability.toCharArray()) + switch (c) { + case 'K': + castlingRights[MoveNode.WHITE_KINGSIDE] = true; + break; + case 'Q': + castlingRights[MoveNode.WHITE_QUEENSIDE] = true; + break; + case 'k': + castlingRights[MoveNode.BLACK_KINGSIDE] = true; + break; + case 'q': + castlingRights[MoveNode.BLACK_QUEENSIDE] = true; + break; + } + board.getLog().setCastlingRights(castlingRights); + + // En passant square + board.getLog().setEnPassant(enPassantTargetSquare); + + // Halfmove clock + board.getLog().setHalfmoveClock(halfmoveClock); + + // Fullmove number + board.getLog().setFullmoveNumber(fullmoveNumber); + } + + /** + * Constructs a {@link FENString} form a {@link Board} object. + * + * @param board the {@link Board} object to encode in this {@link FENString} + */ + public FENString(Board board) { + this.board = board; + + // Serialize individual fields + + // Piece placement + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < 8; i++) { + int empty = 0; + for (int j = 0; j < 8; j++) { + final Piece piece = board.getBoardArr()[j][i]; + + if (piece == null) ++empty; + else { + + // Write empty field count + if (empty > 0) { + sb.append(empty); + empty = 0; + } + + // Write piece character + char p = piece.getType().firstChar(); + sb.append(piece.getColor() == Color.WHITE ? Character.toUpperCase(p) : p); + } + } + + // Write empty field count + if (empty > 0) { + sb.append(empty); + empty = 0; + } + + if (i < 7) sb.append('/'); + } + piecePlacement = sb.toString(); + + // Active color + activeColor = board.getLog().getActiveColor(); + + // Castling availability + castlingAvailability = ""; + final char castlingRightsChars[] = new char[] { 'K', 'Q', 'k', 'q' }; + for (int i = 0; i < 4; i++) + if (board.getLog().getCastlingRights()[i]) castlingAvailability += castlingRightsChars[i]; + if (castlingAvailability.isEmpty()) castlingAvailability = "-"; + + // En passant availability + enPassantTargetSquare = board.getLog().getEnPassant(); + + // Halfmove clock + halfmoveClock = board.getLog().getHalfmoveClock(); + + // Fullmove counter + fullmoveNumber = board.getLog().getFullmoveNumber(); + } + + /** + * Exports this {@link FENString} object to a FEN string. + * + * @return a FEN string representing the board + */ + @Override + public String toString() { + return String.format("%s %c %s %s %d %d", + piecePlacement, + activeColor.firstChar(), + castlingAvailability, + enPassantTargetSquare == null ? "-" : enPassantTargetSquare.toLAN(), + halfmoveClock, + fullmoveNumber); + } + + /** + * @return a {@link Board} object corresponding to this {@link FENString} + */ + public Board getBoard() { return board; } +} diff --git a/src/dev/kske/chess/board/King.java b/src/dev/kske/chess/board/King.java index 78f672d..de90fbf 100644 --- a/src/dev/kske/chess/board/King.java +++ b/src/dev/kske/chess/board/King.java @@ -11,9 +11,7 @@ import java.util.List; */ public class King extends Piece { - public King(Color color, Board board) { - super(color, board); - } + public King(Color color, Board board) { super(color, board); } @Override public boolean isValidMove(Move move) { @@ -33,22 +31,20 @@ public class King extends Piece { } public boolean canCastleKingside() { - if (getMoveCounter() == 0) { + if (board.getLog().getCastlingRights()[getColor() == Color.WHITE ? MoveNode.WHITE_KINGSIDE : MoveNode.BLACK_KINGSIDE]) { int y = getColor() == Color.WHITE ? 7 : 0; Position rookPos = new Position(7, y); Piece rook = board.get(rookPos); - return rook != null && rook.getType() == Type.ROOK && rook.getMoveCounter() == 0 - && isFreePath(new Move(new Position(4, y), new Position(6, y))); + return rook != null && rook.getType() == Type.ROOK && isFreePath(new Move(new Position(4, y), new Position(6, y))); } else return false; } public boolean canCastleQueenside() { - if (getMoveCounter() == 0) { + if (board.getLog().getCastlingRights()[getColor() == Color.WHITE ? MoveNode.WHITE_QUEENSIDE : MoveNode.BLACK_QUEENSIDE]) { int y = getColor() == Color.WHITE ? 7 : 0; Position rookPos = new Position(0, y); Piece rook = board.get(rookPos); - return rook != null && rook.getType() == Type.ROOK && rook.getMoveCounter() == 0 - && isFreePath(new Move(new Position(4, y), new Position(1, y))); + return rook != null && rook.getType() == Type.ROOK && isFreePath(new Move(new Position(4, y), new Position(1, y))); } else return false; } diff --git a/src/dev/kske/chess/board/Log.java b/src/dev/kske/chess/board/Log.java index afa5f00..3ae372e 100644 --- a/src/dev/kske/chess/board/Log.java +++ b/src/dev/kske/chess/board/Log.java @@ -1,8 +1,11 @@ package dev.kske.chess.board; +import java.util.Arrays; import java.util.Iterator; +import java.util.Objects; import dev.kske.chess.board.Piece.Color; +import dev.kske.chess.board.Piece.Type; /** * Project: Chess
@@ -14,13 +17,12 @@ public class Log implements Iterable { private MoveNode root, current; - private Position enPassant; private Color activeColor; - private int fullmoveCounter, halfmoveClock; + private boolean[] castlingRights; + private Position enPassant; + private int fullmoveNumber, halfmoveClock; - public Log() { - reset(); - } + public Log() { reset(); } /** * Creates a (partially deep) copy of another {@link Log} instance which begins @@ -33,15 +35,16 @@ public class Log implements Iterable { */ public Log(Log other, boolean copyVariations) { enPassant = other.enPassant; + castlingRights = other.castlingRights.clone(); activeColor = other.activeColor; - fullmoveCounter = other.fullmoveCounter; + fullmoveNumber = other.fullmoveNumber; halfmoveClock = other.halfmoveClock; // The new root is the current node of the copied instance if (!other.isEmpty()) { - root = new MoveNode(other.current, copyVariations); + root = new MoveNode(other.current, copyVariations); root.setParent(null); - current = root; + current = root; } } @@ -53,9 +56,7 @@ public class Log implements Iterable { private boolean hasNext = true; @Override - public boolean hasNext() { - return hasNext; - } + public boolean hasNext() { return hasNext; } @Override public MoveNode next() { @@ -71,16 +72,20 @@ public class Log implements Iterable { * Adds a move to the move history and adjusts the log to the new position. * * @param move The move to log + * @param piece The piece that performed the move * @param capturedPiece The piece captured with the move - * @param pawnMove {@code true} if the move was made by a pawn */ - public void add(Move move, Piece capturedPiece, boolean pawnMove) { - enPassant = pawnMove && move.yDist == 2 ? new Position(move.pos.x, move.pos.y + move.ySign) : null; - if (activeColor == Color.BLACK) ++fullmoveCounter; - if (pawnMove || capturedPiece != null) halfmoveClock = 0; + public void add(Move move, Piece piece, Piece capturedPiece) { + enPassant = piece.getType() == Type.PAWN && move.yDist == 2 ? new Position(move.pos.x, move.pos.y + move.ySign) : null; + if (activeColor == Color.BLACK) ++fullmoveNumber; + if (piece.getType() == Type.PAWN || capturedPiece != null) halfmoveClock = 0; else++halfmoveClock; activeColor = activeColor.opposite(); - final MoveNode leaf = new MoveNode(move, capturedPiece, enPassant, activeColor, fullmoveCounter, halfmoveClock); + + // Disable castling rights if a king or a rook has been moved + if (piece.getType() == Type.KING || piece.getType() == Type.ROOK) disableCastlingRights(piece, move.pos); + + final MoveNode leaf = new MoveNode(move, capturedPiece, castlingRights.clone(), enPassant, activeColor, fullmoveNumber, halfmoveClock); if (isEmpty()) { root = leaf; @@ -105,9 +110,7 @@ public class Log implements Iterable { public boolean isEmpty() { return root == null; } - public boolean hasParent() { - return !isEmpty() && current.hasParent(); - } + public boolean hasParent() { return !isEmpty() && current.hasParent(); } /** * Reverts the log to its initial state corresponding to the default board @@ -116,15 +119,17 @@ public class Log implements Iterable { public void reset() { root = null; current = null; + castlingRights = new boolean[] { true, true, true, true }; enPassant = null; activeColor = Color.WHITE; - fullmoveCounter = 1; + fullmoveNumber = 1; halfmoveClock = 0; } /** + * Changes the current node to one of its children (variations). * - * @param index + * @param index the index of the variation to select */ public void selectNextNode(int index) { if (!isEmpty() && current.hasVariations() && index < current.getVariations().size()) { @@ -155,11 +160,41 @@ public class Log implements Iterable { private void update() { activeColor = current.activeColor; + castlingRights = current.castlingRights.clone(); enPassant = current.enPassant; - fullmoveCounter = current.fullmoveCounter; + fullmoveNumber = current.fullmoveCounter; halfmoveClock = current.halfmoveClock; } + private void disableCastlingRights(Piece piece, Position initialPosition) { + // Kingside + if (piece.getType() == Type.KING || piece.getType() == Type.ROOK && initialPosition.x == 7) + castlingRights[piece.getColor() == Color.WHITE ? MoveNode.WHITE_KINGSIDE : MoveNode.BLACK_KINGSIDE] = false; + + // Queenside + if (piece.getType() == Type.KING || piece.getType() == Type.ROOK && initialPosition.x == 0) + castlingRights[piece.getColor() == Color.WHITE ? MoveNode.WHITE_QUEENSIDE : MoveNode.BLACK_QUEENSIDE] = false; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + Arrays.hashCode(castlingRights); + result = prime * result + Objects.hash(activeColor, current, enPassant, fullmoveNumber, halfmoveClock); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (getClass() != obj.getClass()) return false; + Log other = (Log) obj; + return activeColor == other.activeColor && Arrays.equals(castlingRights, other.castlingRights) && Objects.equals(current, other.current) + && Objects.equals(enPassant, other.enPassant) && fullmoveNumber == other.fullmoveNumber && halfmoveClock == other.halfmoveClock; + } + /** * @return The first logged move, or {@code null} if there is none */ @@ -170,6 +205,10 @@ public class Log implements Iterable { */ public MoveNode getLast() { return current; } + public boolean[] getCastlingRights() { return castlingRights; } + + public void setCastlingRights(boolean[] castlingRights) { this.castlingRights = castlingRights; } + public Position getEnPassant() { return enPassant; } public void setEnPassant(Position enPassant) { this.enPassant = enPassant; } @@ -178,9 +217,9 @@ public class Log implements Iterable { public void setActiveColor(Color activeColor) { this.activeColor = activeColor; } - public int getFullmoveCounter() { return fullmoveCounter; } + public int getFullmoveNumber() { return fullmoveNumber; } - public void setFullmoveCounter(int fullmoveCounter) { this.fullmoveCounter = fullmoveCounter; } + public void setFullmoveNumber(int fullmoveCounter) { this.fullmoveNumber = fullmoveCounter; } public int getHalfmoveClock() { return halfmoveClock; } diff --git a/src/dev/kske/chess/board/Move.java b/src/dev/kske/chess/board/Move.java index 74f8cb7..34cacf9 100644 --- a/src/dev/kske/chess/board/Move.java +++ b/src/dev/kske/chess/board/Move.java @@ -1,5 +1,7 @@ package dev.kske.chess.board; +import java.util.Objects; + /** * Project: Chess
* File: Move.java
@@ -50,6 +52,21 @@ public class Move { return String.format("%s -> %s", pos, dest); } + @Override + public int hashCode() { + return Objects.hash(dest, pos, type, xDist, xSign, yDist, ySign); + } + + @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(dest, other.dest) && Objects.equals(pos, other.pos) && type == other.type + && xDist == other.xDist && xSign == other.xSign && yDist == other.yDist && ySign == other.ySign; + } + public static enum Type { NORMAL, PAWN_PROMOTION, CASTLING, EN_PASSANT, UNKNOWN } diff --git a/src/dev/kske/chess/board/MoveNode.java b/src/dev/kske/chess/board/MoveNode.java index 6d4afad..600a0c9 100644 --- a/src/dev/kske/chess/board/MoveNode.java +++ b/src/dev/kske/chess/board/MoveNode.java @@ -1,7 +1,9 @@ package dev.kske.chess.board; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; +import java.util.Objects; import dev.kske.chess.board.Piece.Color; @@ -13,8 +15,11 @@ import dev.kske.chess.board.Piece.Color; */ public class MoveNode { + public static final int WHITE_KINGSIDE = 0, WHITE_QUEENSIDE = 1, BLACK_KINGSIDE = 2, BLACK_QUEENSIDE = 3; + public final Move move; public final Piece capturedPiece; + public final boolean[] castlingRights; public final Position enPassant; public final Color activeColor; public final int fullmoveCounter, halfmoveClock; @@ -33,10 +38,11 @@ public class MoveNode { * @param fullmoveCounter * @param halfmoveClock */ - public MoveNode(Move move, Piece capturedPiece, Position enPassant, Color activeColor, int fullmoveCounter, - int halfmoveClock) { + public MoveNode(Move move, Piece capturedPiece, boolean castlingRights[], Position enPassant, Color activeColor, + int fullmoveCounter, int halfmoveClock) { this.move = move; this.capturedPiece = capturedPiece; + this.castlingRights = castlingRights; this.enPassant = enPassant; this.activeColor = activeColor; this.fullmoveCounter = fullmoveCounter; @@ -52,8 +58,8 @@ public class MoveNode { * considers subsequent variations */ public MoveNode(MoveNode other, boolean copyVariations) { - this(other.move, other.capturedPiece, other.enPassant, other.activeColor, other.fullmoveCounter, - other.halfmoveClock); + this(other.move, other.capturedPiece, other.castlingRights.clone(), other.enPassant, other.activeColor, + other.fullmoveCounter, other.halfmoveClock); if (copyVariations && other.variations != null) { if (variations == null) variations = new ArrayList<>(); other.variations.forEach(variation -> { @@ -93,4 +99,26 @@ public class MoveNode { public boolean hasParent() { return parent != null; } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + Arrays.hashCode(castlingRights); + result = prime * result + + Objects.hash(activeColor, capturedPiece, enPassant, fullmoveCounter, halfmoveClock, move); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (getClass() != obj.getClass()) return false; + MoveNode other = (MoveNode) obj; + return activeColor == other.activeColor && Objects.equals(capturedPiece, other.capturedPiece) + && Arrays.equals(castlingRights, other.castlingRights) && Objects.equals(enPassant, other.enPassant) + && fullmoveCounter == other.fullmoveCounter && halfmoveClock == other.halfmoveClock + && Objects.equals(move, other.move); + } } \ No newline at end of file diff --git a/src/dev/kske/chess/board/Piece.java b/src/dev/kske/chess/board/Piece.java index e9ca766..ef4ea90 100644 --- a/src/dev/kske/chess/board/Piece.java +++ b/src/dev/kske/chess/board/Piece.java @@ -2,6 +2,7 @@ package dev.kske.chess.board; import java.util.Iterator; import java.util.List; +import java.util.Objects; /** * Project: Chess
@@ -13,7 +14,6 @@ public abstract class Piece implements Cloneable { private final Color color; protected Board board; - private int moveCounter; public Piece(Color color, Board board) { this.color = color; @@ -74,14 +74,18 @@ public abstract class Piece implements Cloneable { public Color getColor() { return color; } - public int getMoveCounter() { return moveCounter; } - - public void incMoveCounter() { - ++moveCounter; + @Override + public int hashCode() { + return Objects.hash(color); } - public void decMoveCounter() { - --moveCounter; + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (getClass() != obj.getClass()) return false; + Piece other = (Piece) obj; + return color == other.color; } public static enum Type { diff --git a/src/dev/kske/chess/game/Game.java b/src/dev/kske/chess/game/Game.java index 13cf92b..feb8781 100644 --- a/src/dev/kske/chess/game/Game.java +++ b/src/dev/kske/chess/game/Game.java @@ -12,10 +12,10 @@ import dev.kske.chess.board.Piece.Color; import dev.kske.chess.event.EventBus; import dev.kske.chess.event.MoveEvent; import dev.kske.chess.game.ai.AIPlayer; +import dev.kske.chess.io.EngineUtil; +import dev.kske.chess.io.EngineUtil.EngineInfo; import dev.kske.chess.ui.BoardComponent; import dev.kske.chess.ui.BoardPane; -import dev.kske.chess.ui.EngineUtil; -import dev.kske.chess.ui.EngineUtil.EngineInfo; import dev.kske.chess.ui.OverlayComponent; /** diff --git a/src/dev/kske/chess/game/UCIPlayer.java b/src/dev/kske/chess/game/UCIPlayer.java index 12b73d3..369c16d 100644 --- a/src/dev/kske/chess/game/UCIPlayer.java +++ b/src/dev/kske/chess/game/UCIPlayer.java @@ -2,6 +2,7 @@ package dev.kske.chess.game; import java.io.IOException; +import dev.kske.chess.board.FENString; import dev.kske.chess.board.Move; import dev.kske.chess.board.Piece.Color; import dev.kske.chess.uci.UCIHandle; @@ -30,7 +31,7 @@ public class UCIPlayer extends Player implements UCIListener { @Override public void requestMove() { - handle.positionFEN(board.toFEN()); + handle.positionFEN(new FENString(board).toString()); handle.go(); } diff --git a/src/dev/kske/chess/ui/EngineUtil.java b/src/dev/kske/chess/io/EngineUtil.java similarity index 95% rename from src/dev/kske/chess/ui/EngineUtil.java rename to src/dev/kske/chess/io/EngineUtil.java index b56c416..608006e 100644 --- a/src/dev/kske/chess/ui/EngineUtil.java +++ b/src/dev/kske/chess/io/EngineUtil.java @@ -1,4 +1,4 @@ -package dev.kske.chess.ui; +package dev.kske.chess.io; import java.io.FileInputStream; import java.io.FileOutputStream; diff --git a/src/dev/kske/chess/ui/TextureUtil.java b/src/dev/kske/chess/io/TextureUtil.java similarity index 95% rename from src/dev/kske/chess/ui/TextureUtil.java rename to src/dev/kske/chess/io/TextureUtil.java index d7e6910..58fc621 100644 --- a/src/dev/kske/chess/ui/TextureUtil.java +++ b/src/dev/kske/chess/io/TextureUtil.java @@ -1,4 +1,4 @@ -package dev.kske.chess.ui; +package dev.kske.chess.io; import java.awt.Image; import java.awt.image.BufferedImage; diff --git a/src/dev/kske/chess/pgn/PGNGame.java b/src/dev/kske/chess/pgn/PGNGame.java index 9b56b8f..6293976 100644 --- a/src/dev/kske/chess/pgn/PGNGame.java +++ b/src/dev/kske/chess/pgn/PGNGame.java @@ -7,6 +7,7 @@ import java.util.regex.MatchResult; import java.util.regex.Pattern; import dev.kske.chess.board.Board; +import dev.kske.chess.board.FENString; import dev.kske.chess.exception.ChessException; /** @@ -26,8 +27,7 @@ public class PGNGame { MatchResult matchResult; Pattern tagPairPattern = Pattern.compile("\\[(\\w+) \"(.*)\"]"), movePattern = Pattern.compile("\\d+\\.\\s+(?:(?:(\\S+)\\s+(\\S+))|(?:O-O-O)|(?:O-O))(?:\\+{0,2}|\\#)"), - nagPattern = Pattern.compile("(\\$\\d{1,3})*"), - terminationMarkerPattern = Pattern.compile("1-0|0-1|1\\/2-1\\/2|\\*"); + nagPattern = Pattern.compile("(\\$\\d{1,3})*"), terminationMarkerPattern = Pattern.compile("1-0|0-1|1\\/2-1\\/2|\\*"); // Parse tag pairs while (sc.findInLine(tagPairPattern) != null) { @@ -48,30 +48,23 @@ public class PGNGame { matchResult = sc.match(); if (matchResult.groupCount() > 0) for (int i = 1; i < matchResult.groupCount() + 1; i++) { game.board.move(matchResult.group(i)); - System.out.println(game.getBoard().getLog().getLast().move.toLAN() + ": " + game.board.toFEN()); + System.out.println(game.getBoard().getLog().getLast().move.toLAN() + ": " + new FENString(game.board).toString()); } else break; } else break; } // Parse game termination marker - if (sc.findWithinHorizon(terminationMarkerPattern, 20) == null) - System.err.println("Termination marker expected"); + if (sc.findWithinHorizon(terminationMarkerPattern, 20) == null) System.err.println("Termination marker expected"); return game; } - public String getTag(String tagName) { - return tagPairs.get(tagName); - } + public String getTag(String tagName) { return tagPairs.get(tagName); } - public boolean hasTag(String tagName) { - return tagPairs.containsKey(tagName); - } + public boolean hasTag(String tagName) { return tagPairs.containsKey(tagName); } - public void setTag(String tagName, String tagValue) { - tagPairs.put(tagName, tagValue); - } + public void setTag(String tagName, String tagValue) { tagPairs.put(tagName, tagValue); } public Board getBoard() { return board; } } diff --git a/src/dev/kske/chess/ui/BoardComponent.java b/src/dev/kske/chess/ui/BoardComponent.java index 09b97d7..802af23 100644 --- a/src/dev/kske/chess/ui/BoardComponent.java +++ b/src/dev/kske/chess/ui/BoardComponent.java @@ -6,6 +6,7 @@ import java.awt.Graphics; import javax.swing.JComponent; import dev.kske.chess.board.Board; +import dev.kske.chess.io.TextureUtil; /** * Project: Chess
diff --git a/src/dev/kske/chess/ui/DialogUtil.java b/src/dev/kske/chess/ui/DialogUtil.java index a25711d..f591881 100644 --- a/src/dev/kske/chess/ui/DialogUtil.java +++ b/src/dev/kske/chess/ui/DialogUtil.java @@ -15,6 +15,9 @@ import javax.swing.JFileChooser; import javax.swing.JLabel; import javax.swing.JOptionPane; import javax.swing.JPanel; +import javax.swing.filechooser.FileFilter; + +import dev.kske.chess.io.EngineUtil; /** * Project: Chess
@@ -26,11 +29,25 @@ public class DialogUtil { private DialogUtil() {} - public static void showFileSelectionDialog(Component parent, Consumer action) { + public static void showFileSelectionDialog(Component parent, Consumer> action) { JFileChooser fileChooser = new JFileChooser(); fileChooser.setCurrentDirectory(new File(System.getProperty("user.home"))); - if (fileChooser.showOpenDialog(parent) == JFileChooser.APPROVE_OPTION) - action.accept(fileChooser.getSelectedFile()); + fileChooser.setAcceptAllFileFilterUsed(false); + fileChooser.addChoosableFileFilter(new FileFilter() { + + @Override + public boolean accept(File f) { + int dotIndex = f.getName().lastIndexOf('.'); + if (dotIndex >= 0) { + String extension = f.getName().substring(dotIndex).toLowerCase(); + return extension.equals(".fen") || extension.equals(".pgn"); + } else return f.isDirectory(); + } + + @Override + public String getDescription() { return "FEN and PGN files"; } + }); + if (fileChooser.showOpenDialog(parent) == JFileChooser.APPROVE_OPTION) action.accept(Arrays.asList(fileChooser.getSelectedFile())); } public static void showGameConfigurationDialog(Component parent, BiConsumer action) { diff --git a/src/dev/kske/chess/ui/FENDropTarget.java b/src/dev/kske/chess/ui/FENDropTarget.java deleted file mode 100644 index b8ffc6d..0000000 --- a/src/dev/kske/chess/ui/FENDropTarget.java +++ /dev/null @@ -1,56 +0,0 @@ -package dev.kske.chess.ui; - -import java.awt.datatransfer.DataFlavor; -import java.awt.datatransfer.UnsupportedFlavorException; -import java.awt.dnd.DnDConstants; -import java.awt.dnd.DropTargetAdapter; -import java.awt.dnd.DropTargetDropEvent; -import java.io.BufferedReader; -import java.io.File; -import java.io.FileReader; -import java.io.IOException; -import java.util.List; - -import dev.kske.chess.board.Board; -import dev.kske.chess.game.Game; - -/** - * Project: Chess
- * File: FENDropTarget.java
- * Created: 13 Aug 2019
- * Author: Kai S. K. Engelbart - */ -public class FENDropTarget extends DropTargetAdapter { - - private MainWindow mainWindow; - - public FENDropTarget(MainWindow mainWindow) { - this.mainWindow = mainWindow; - } - - @SuppressWarnings("unchecked") - @Override - public void drop(DropTargetDropEvent evt) { - try { - evt.acceptDrop(DnDConstants.ACTION_COPY_OR_MOVE); - ((List) evt.getTransferable().getTransferData(DataFlavor.javaFileListFlavor)).forEach(file -> { - try (BufferedReader br = new BufferedReader(new FileReader(file))) { - final GamePane gamePane = mainWindow.addGamePane(); - final String fen = br.readLine(); - DialogUtil.showGameConfigurationDialog(null, (whiteName, blackName) -> { - final Game game = new Game(gamePane.getBoardPane(), whiteName, blackName, new Board(fen)); - gamePane.setGame(game); - game.start(); - }); - evt.dropComplete(true); - } catch (IOException e) { - e.printStackTrace(); - evt.rejectDrop(); - } - }); - } catch (UnsupportedFlavorException | IOException ex) { - ex.printStackTrace(); - evt.rejectDrop(); - } - } -} diff --git a/src/dev/kske/chess/ui/GameDropTarget.java b/src/dev/kske/chess/ui/GameDropTarget.java new file mode 100644 index 0000000..c6da82b --- /dev/null +++ b/src/dev/kske/chess/ui/GameDropTarget.java @@ -0,0 +1,35 @@ +package dev.kske.chess.ui; + +import java.awt.datatransfer.DataFlavor; +import java.awt.datatransfer.UnsupportedFlavorException; +import java.awt.dnd.DnDConstants; +import java.awt.dnd.DropTargetAdapter; +import java.awt.dnd.DropTargetDropEvent; +import java.io.File; +import java.io.IOException; +import java.util.List; + +/** + * Project: Chess
+ * File: GameDropTarget.java
+ * Created: 13 Aug 2019
+ * Author: Kai S. K. Engelbart + */ +public class GameDropTarget extends DropTargetAdapter { + + private MainWindow mainWindow; + + public GameDropTarget(MainWindow mainWindow) { this.mainWindow = mainWindow; } + + @SuppressWarnings("unchecked") + @Override + public void drop(DropTargetDropEvent evt) { + try { + evt.acceptDrop(DnDConstants.ACTION_COPY_OR_MOVE); + mainWindow.loadFiles((List) evt.getTransferable().getTransferData(DataFlavor.javaFileListFlavor)); + } catch (UnsupportedFlavorException | IOException ex) { + ex.printStackTrace(); + evt.rejectDrop(); + } + } +} diff --git a/src/dev/kske/chess/ui/MainWindow.java b/src/dev/kske/chess/ui/MainWindow.java index edaa113..cb6271f 100644 --- a/src/dev/kske/chess/ui/MainWindow.java +++ b/src/dev/kske/chess/ui/MainWindow.java @@ -3,10 +3,23 @@ package dev.kske.chess.ui; import java.awt.EventQueue; import java.awt.Toolkit; import java.awt.dnd.DropTarget; +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.List; +import javax.swing.JComboBox; import javax.swing.JFrame; +import javax.swing.JOptionPane; import javax.swing.JTabbedPane; +import dev.kske.chess.board.Board; +import dev.kske.chess.board.FENString; +import dev.kske.chess.exception.ChessException; +import dev.kske.chess.game.Game; +import dev.kske.chess.pgn.PGNDatabase; +import dev.kske.chess.pgn.PGNGame; + /** * Project: Chess
* File: MainWindow.java
@@ -55,7 +68,7 @@ public class MainWindow extends JFrame { getContentPane().add(tabbedPane); setJMenuBar(new MenuBar(this)); - new DropTarget(this, new FENDropTarget(this)); + new DropTarget(this, new GameDropTarget(this)); // Update position and dimensions pack(); @@ -74,9 +87,7 @@ public class MainWindow extends JFrame { * * @return The new {@link GamePane} */ - public GamePane addGamePane() { - return addGamePane("Game " + (tabbedPane.getComponentCount() + 1)); - } + public GamePane addGamePane() { return addGamePane("Game " + (tabbedPane.getComponentCount() + 1)); } /** * Creates a new {@link GamePane}, adds it to the tabbed pane and opens it. @@ -91,12 +102,64 @@ public class MainWindow extends JFrame { return gamePane; } + public GamePane addGamePane(String title, Board board) { + GamePane gamePane = addGamePane(title); + DialogUtil.showGameConfigurationDialog(this, + (whiteName, blackName) -> { + Game game = new Game(gamePane.getBoardPane(), whiteName, blackName, board); + gamePane.setGame(game); + game.start(); + }); + return gamePane; + } + /** * Removes a {@link GamePane} form the tabbed pane. * * @param index The index of the {@link GamePane} to remove */ - public void removeGamePane(int index) { - tabbedPane.remove(index); + public void removeGamePane(int index) { tabbedPane.remove(index); } + + /** + * Loads a game file (FEN or PGN) and adds it to a new {@link GamePane}. + * + * @param files the files to load the game from + */ + public void loadFiles(List files) { + files.forEach(file -> { + final String name = file.getName().substring(0, file.getName().lastIndexOf('.')); + final String extension = file.getName().substring(file.getName().lastIndexOf('.')).toLowerCase(); + try { + Board board; + switch (extension) { + case ".fen": + board = new FENString(new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8)).getBoard(); + break; + case ".pgn": + PGNDatabase pgnDB = new PGNDatabase(); + pgnDB.load(file); + if (pgnDB.getGames().size() > 0) { + String[] gameNames = new String[pgnDB.getGames().size()]; + for (int i = 0; i < gameNames.length; i++) { + final PGNGame game = pgnDB.getGames().get(i); + gameNames[i] = String.format("%s vs %s: %s", game.getTag("White"), game.getTag("Black"), game.getTag("Result")); + } + JComboBox comboBox = new JComboBox<>(gameNames); + JOptionPane.showInputDialog(this, comboBox, "Select a game", JOptionPane.QUESTION_MESSAGE); + board = pgnDB.getGames().get(comboBox.getSelectedIndex()).getBoard(); + } else throw new ChessException("The PGN database '" + name + "' is empty!"); + break; + default: + throw new ChessException("The file extension '" + extension + "' is not supported!"); + } + addGamePane(name, board); + } catch (Exception e) { + e.printStackTrace(); + JOptionPane.showMessageDialog(this, + "Failed to load the file " + file.getName() + ": " + e.toString(), + "File loading error", + JOptionPane.ERROR_MESSAGE); + } + }); } } diff --git a/src/dev/kske/chess/ui/MenuBar.java b/src/dev/kske/chess/ui/MenuBar.java index c7c74d0..a224268 100644 --- a/src/dev/kske/chess/ui/MenuBar.java +++ b/src/dev/kske/chess/ui/MenuBar.java @@ -2,20 +2,16 @@ package dev.kske.chess.ui; import java.awt.Toolkit; import java.awt.datatransfer.StringSelection; -import java.io.File; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import javax.swing.JComboBox; import javax.swing.JMenu; import javax.swing.JMenuBar; import javax.swing.JMenuItem; import javax.swing.JOptionPane; -import dev.kske.chess.board.Board; +import dev.kske.chess.board.FENString; +import dev.kske.chess.exception.ChessException; import dev.kske.chess.game.Game; -import dev.kske.chess.pgn.PGNDatabase; -import dev.kske.chess.pgn.PGNGame; +import dev.kske.chess.io.EngineUtil; /** * Project: Chess
@@ -41,8 +37,7 @@ public class MenuBar extends JMenuBar { JMenu gameMenu = new JMenu("Game"); JMenuItem newGameMenuItem = new JMenuItem("New Game"); - newGameMenuItem - .addActionListener((evt) -> DialogUtil.showGameConfigurationDialog(mainWindow, (whiteName, blackName) -> { + newGameMenuItem.addActionListener((evt) -> DialogUtil.showGameConfigurationDialog(mainWindow, (whiteName, blackName) -> { GamePane gamePane = mainWindow.addGamePane(); Game game = new Game(gamePane.getBoardPane(), whiteName, blackName); gamePane.setGame(game); @@ -51,7 +46,7 @@ public class MenuBar extends JMenuBar { gameMenu.add(newGameMenuItem); JMenuItem loadFileMenu = new JMenuItem("Load game file"); - loadFileMenu.addActionListener((evt) -> DialogUtil.showFileSelectionDialog(mainWindow, this::loadFile)); + loadFileMenu.addActionListener((evt) -> DialogUtil.showFileSelectionDialog(mainWindow, mainWindow::loadFiles)); gameMenu.add(loadFileMenu); add(gameMenu); @@ -63,10 +58,8 @@ public class MenuBar extends JMenuBar { JMenuItem addEngineMenuItem = new JMenuItem("Add engine"); addEngineMenuItem.addActionListener((evt) -> { - String enginePath = JOptionPane.showInputDialog(getParent(), - "Enter the path to a UCI-compatible chess engine:", - "Engine selection", - JOptionPane.QUESTION_MESSAGE); + String enginePath = JOptionPane + .showInputDialog(getParent(), "Enter the path to a UCI-compatible chess engine:", "Engine selection", JOptionPane.QUESTION_MESSAGE); if (enginePath != null) EngineUtil.addEngine(enginePath); }); @@ -82,7 +75,7 @@ public class MenuBar extends JMenuBar { JMenuItem exportFENMenuItem = new JMenuItem("Export board to FEN"); exportFENMenuItem.addActionListener((evt) -> { - final String fen = mainWindow.getSelectedGamePane().getGame().getBoard().toFEN(); + final String fen = new FENString(mainWindow.getSelectedGamePane().getGame().getBoard()).toString(); Toolkit.getDefaultToolkit().getSystemClipboard().setContents(new StringSelection(fen), null); JOptionPane.showMessageDialog(mainWindow, String.format("FEN-string copied to clipboard!%n%s", fen)); }); @@ -93,77 +86,20 @@ public class MenuBar extends JMenuBar { final GamePane gamePane = mainWindow.addGamePane(); final String fen = JOptionPane.showInputDialog("Enter a FEN string: "); DialogUtil.showGameConfigurationDialog(mainWindow, (whiteName, blackName) -> { - final Game game = new Game(gamePane.getBoardPane(), whiteName, blackName, new Board(fen)); - gamePane.setGame(game); - game.start(); + Game game; + try { + game = new Game(gamePane.getBoardPane(), whiteName, blackName, new FENString(fen).getBoard()); + gamePane.setGame(game); + game.start(); + } catch (ChessException e) { + e.printStackTrace(); + JOptionPane + .showMessageDialog(mainWindow, "Failed to load FEN string: " + e.toString(), "FEN loading error", JOptionPane.ERROR_MESSAGE); + } }); }); toolsMenu.add(loadFromFENMenuItem); add(toolsMenu); } - - private void loadFile(File file) { - final String name = file.getName().substring(0, file.getName().lastIndexOf('.')); - final String extension = file.getName().substring(file.getName().lastIndexOf('.')).toLowerCase(); - switch (extension) { - case ".fen": - try { - final GamePane gamePane = mainWindow.addGamePane(name); - final String fen = new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8); - DialogUtil.showGameConfigurationDialog(mainWindow, (whiteName, blackName) -> { - final Game game = new Game(gamePane.getBoardPane(), whiteName, blackName, new Board(fen)); - gamePane.setGame(game); - game.start(); - }); - } catch (Exception e) { - e.printStackTrace(); - JOptionPane.showMessageDialog(mainWindow, - "Failed to load the file " + file.getName() + ": " + e.toString(), - "File loading error", - JOptionPane.ERROR_MESSAGE); - } - break; - case ".pgn": - try { - final GamePane gamePane = mainWindow.addGamePane(name); - PGNDatabase pgnDB = new PGNDatabase(); - pgnDB.load(file); - if (pgnDB.getGames().size() > 0) { - String[] gameNames = new String[pgnDB.getGames().size()]; - for (int i = 0; i < gameNames.length; i++) { - final PGNGame game = pgnDB.getGames().get(i); - gameNames[i] = String.format("%s vs %s: %s", - game.getTag("White"), - game.getTag("Black"), - game.getTag("Result")); - } - JComboBox comboBox = new JComboBox<>(gameNames); - JOptionPane - .showInputDialog(mainWindow, comboBox, "Select a game", JOptionPane.QUESTION_MESSAGE); - DialogUtil.showGameConfigurationDialog(mainWindow, (whiteName, blackName) -> { - final Game game = new Game(gamePane.getBoardPane(), whiteName, blackName, - pgnDB.getGames().get(comboBox.getSelectedIndex()).getBoard()); - game.start(); - }); - } - } catch (Exception e) { - e.printStackTrace(); - JOptionPane.showMessageDialog(mainWindow, - "Failed to load the file " + file.getName() + ": " + e.toString(), - "File loading error", - JOptionPane.ERROR_MESSAGE); - } - break; - default: - JOptionPane.showMessageDialog(mainWindow, - "The file extension '" + extension + "' is not supported!", - "File loading error", - JOptionPane.ERROR_MESSAGE); - } - } - - public void loadFENFile(File file) { - - } } diff --git a/test/dev/kske/chess/board/FENStringTest.java b/test/dev/kske/chess/board/FENStringTest.java new file mode 100644 index 0000000..b2c1e43 --- /dev/null +++ b/test/dev/kske/chess/board/FENStringTest.java @@ -0,0 +1,72 @@ +package dev.kske.chess.board; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import dev.kske.chess.board.Piece.Color; +import dev.kske.chess.exception.ChessException; + +/** + * Project: Chess
+ * File: FENStringTest.java
+ * Created: 24 Oct 2019
+ * + * @author Kai S. K. Engelbart + */ +class FENStringTest { + + List fenStrings = new ArrayList<>(); + List boards = new ArrayList<>(); + + void cleanBoard(Board board) { + for (int i = 0; i < 8; i++) + for (int j = 0; j < 8; j++) + board.getBoardArr()[i][j] = null; + } + + /** + * @throws java.lang.Exception + */ + @BeforeEach + void setUp() throws Exception { + fenStrings.addAll(Arrays.asList("rnbqkbnr/pp1ppppp/8/2p5/4P3/5N2/PPPP1PPP/RNBQKB1R b KQkq - 1 2")); + Board board = new Board(); + board.set(Position.fromLAN("c7"), null); + board.set(Position.fromLAN("c5"), new Pawn(Color.BLACK, board)); + board.set(Position.fromLAN("e4"), new Pawn(Color.WHITE, board)); + board.set(Position.fromLAN("f3"), new Knight(Color.WHITE, board)); + board.set(Position.fromLAN("e2"), null); + board.set(Position.fromLAN("g1"), null); + + board.getLog().setActiveColor(Color.BLACK); + board.getLog().setHalfmoveClock(1); + board.getLog().setFullmoveNumber(2); + boards.add(board); + } + + /** + * Test method for {@link dev.kske.chess.board.FENString#toString()}. + */ + @Test + void testToString() { + for (int i = 0; i < fenStrings.size(); i++) + assertEquals(fenStrings.get(i), new FENString(boards.get(i)).toString()); + } + + /** + * Test method for {@link dev.kske.chess.board.FENString#getBoard()}. + * + * @throws ChessException + */ + @Test + void testGetBoard() throws ChessException { + for (int i = 0; i < boards.size(); i++) + assertEquals(boards.get(i), new FENString(fenStrings.get(i)).getBoard()); + } +} diff --git a/test/dev/kske/chess/board/LogTest.java b/test/dev/kske/chess/board/LogTest.java index 4e682d4..fafc6dc 100644 --- a/test/dev/kske/chess/board/LogTest.java +++ b/test/dev/kske/chess/board/LogTest.java @@ -30,7 +30,7 @@ class LogTest { assertNull(log.getRoot()); assertEquals(log.getActiveColor(), Color.WHITE); assertNull(log.getEnPassant()); - assertEquals(log.getFullmoveCounter(), 1); + assertEquals(log.getFullmoveNumber(), 1); assertEquals(log.getHalfmoveClock(), 0); } @@ -43,10 +43,10 @@ class LogTest { log.setActiveColor(Color.WHITE); other.setActiveColor(Color.BLACK); assertNotEquals(log.getActiveColor(), other.getActiveColor()); - log.add(Move.fromLAN("a2a4"), null, true); - log.add(Move.fromLAN("a4a5"), null, true); - other.add(Move.fromLAN("a2a4"), null, true); - other.add(Move.fromLAN("a4a5"), null, true); + log.add(Move.fromLAN("a2a4"), new Pawn(Color.WHITE, null), null); + log.add(Move.fromLAN("a4a5"), new Pawn(Color.WHITE, null), null); + other.add(Move.fromLAN("a2a4"), new Pawn(Color.WHITE, null), null); + other.add(Move.fromLAN("a4a5"), new Pawn(Color.WHITE, null), null); assertNotEquals(log.getRoot(), other.getRoot()); assertNotEquals(log.getRoot().getVariations(), other.getRoot().getVariations()); } @@ -132,7 +132,7 @@ class LogTest { } /** - * Test method for {@link dev.kske.chess.board.Log#getFullmoveCounter()}. + * Test method for {@link dev.kske.chess.board.Log#getFullmoveNumber()}. */ @Test void testGetFullmoveCounter() { @@ -140,7 +140,7 @@ class LogTest { } /** - * Test method for {@link dev.kske.chess.board.Log#setFullmoveCounter(int)}. + * Test method for {@link dev.kske.chess.board.Log#setFullmoveNumber(int)}. */ @Test void testSetFullmoveCounter() { diff --git a/test/dev/kske/chess/pgn/PGNDatabaseTest.java b/test/dev/kske/chess/pgn/PGNDatabaseTest.java deleted file mode 100644 index 35e31e1..0000000 --- a/test/dev/kske/chess/pgn/PGNDatabaseTest.java +++ /dev/null @@ -1,36 +0,0 @@ -package dev.kske.chess.pgn; - -import static org.junit.jupiter.api.Assertions.fail; - -import java.io.File; -import java.io.FileNotFoundException; - -import org.junit.jupiter.api.Test; - -import dev.kske.chess.exception.ChessException; - -/** - * Project: Chess
- * File: PGNDatabaseTest.java
- * Created: 4 Oct 2019
- * Author: Kai S. K. Engelbart - */ -class PGNDatabaseTest { - - /** - * Test method for {@link dev.kske.chess.pgn.PGNDatabase#load(java.io.File)}. - * - * @throws ChessException - * @throws FileNotFoundException - */ - @Test - void testLoad() { - PGNDatabase db = new PGNDatabase(); - try { - db.load(new File(getClass().getClassLoader().getResource("test.pgn").getFile())); - } catch (FileNotFoundException | ChessException e) { - e.printStackTrace(); - fail(e); - } - } -} diff --git a/test_res/test.pgn b/test_res/test.pgn deleted file mode 100644 index cb2e4ab..0000000 --- a/test_res/test.pgn +++ /dev/null @@ -1,18 +0,0 @@ -[Event "Test Game"] -[Site "Test Environment"] -[Date "2019.10.04"] -[Round "1"] -[Result "1-0"] -[White "Kai Engelbart"] -[Black "Kai Engelbart 2"] - -1. e4 c5 2. Nf3 e6 3. d4 cxd4 4. Nxd4 a6 5. Bd3 Nf6 6. O-O d6 -7. c4 Bd7 8. Nc3 Nc6 9. Be3 Be7 10. h3 Ne5 11. Be2 Rc8 12. Qb3 -Qc7 13. Rac1 O-O 14. f4 Nc6 15. Nf3 Qb8 16. Qd1 Be8 17. Qd2 -Na5 18. b3 b6 19. Bd3 Nc6 20. Qf2 b5 21. Rfd1 Nb4 22. Bf1 bxc4 -23. bxc4 a5 24. Nd4 Qa8 25. Qf3 Na6 26. Ndb5 Nc5 27. e5 dxe5 -28. Qxa8 Rxa8 29. fxe5 Nfe4 30. Nd6 Bc6 31. Ncxe4 Nxe4 32. c5 -Ng3 33. Bc4 h5 34. Bf2 h4 35. Bxg3 hxg3 36. Bb5 Bxb5 37. Nxb5 -f6 38. Rd7 Bd8 39. Rc3 fxe5 40. Rxg3 Rf7 41. Rxf7 Kxf7 42. c6 -Bb6+ 43. Kf1 Kf8 44. c7 Rc8 45. a4 e4 46. Ke2 e5 47. Rg6 Bd4 -48. h4 Bb2 1-0 \ No newline at end of file