From 3603153254e367e61339d775adce9b609c681fb7 Mon Sep 17 00:00:00 2001 From: kske Date: Thu, 24 Oct 2019 06:09:16 +0200 Subject: [PATCH] Added FENString class --- src/dev/kske/chess/board/FENString.java | 194 ++++++++++++++++++++++++ 1 file changed, 194 insertions(+) create mode 100644 src/dev/kske/chess/board/FENString.java diff --git a/src/dev/kske/chess/board/FENString.java b/src/dev/kske/chess/board/FENString.java new file mode 100644 index 0000000..5e53511 --- /dev/null +++ b/src/dev/kske/chess/board/FENString.java @@ -0,0 +1,194 @@ +package dev.kske.chess.board; + +import java.util.LinkedHashMap; +import java.util.Map; +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 Map fields = new LinkedHashMap<>(); + + public static enum FENField { + PIECE_PLACEMENT, ACTIVE_COLOR, CASTLING_AVAILABILITY, EN_PASSANT_TARGET_SQUARE, HALFMOVE_CLOCK, FULLMOVE_NUMBER + } + + /** + * 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(); + fields.put(FENField.PIECE_PLACEMENT, "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR"); + fields.put(FENField.ACTIVE_COLOR, "w"); + fields.put(FENField.CASTLING_AVAILABILITY, "KQkq"); + fields.put(FENField.EN_PASSANT_TARGET_SQUARE, "-"); + fields.put(FENField.HALFMOVE_CLOCK, "0"); + fields.put(FENField.FULLMOVE_NUMBER, "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()); + for (FENField field : FENField.values()) + fields.put(field, matcher.group(field.toString())); + + // 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 = fields.get(FENField.PIECE_PLACEMENT).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()[i][j] = new King(color, board); + break; + case 'Q': + board.getBoardArr()[i][j] = new Queen(color, board); + break; + case 'R': + board.getBoardArr()[i][j] = new Rook(color, board); + break; + case 'N': + board.getBoardArr()[i][j] = new Knight(color, board); + break; + case 'B': + board.getBoardArr()[i][j] = new Bishop(color, board); + break; + case 'P': + board.getBoardArr()[i][j] = new Pawn(color, board); + break; + } + ++j; + } + } + } + + // Active color + board.getLog().setActiveColor(Color.fromFirstChar(fields.get(FENField.ACTIVE_COLOR).charAt(0))); + + // TODO: Castling availability + + // En passant square + if (!fields.get(FENField.EN_PASSANT_TARGET_SQUARE).equals("-")) + board.getLog().setEnPassant(Position.fromLAN(fields.get(FENField.EN_PASSANT_TARGET_SQUARE))); + + // Halfmove clock + board.getLog().setHalfmoveClock(Integer.parseInt(fields.get(FENField.HALFMOVE_CLOCK))); + + // Fullmove number + board.getLog().setFullmoveNumber(Integer.parseInt(fields.get(FENField.FULLMOVE_NUMBER))); + } + + /** + * 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()[i][j]; + + 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('/'); + } + fields.put(FENField.PIECE_PLACEMENT, sb.toString()); + + // Active color + fields.put(FENField.ACTIVE_COLOR, String.valueOf(board.getLog().getActiveColor().firstChar())); + + // TODO: Castling availability + + // En passant availability + final Position enPassantPosition = board.getLog().getEnPassant(); + fields.put(FENField.EN_PASSANT_TARGET_SQUARE, enPassantPosition == null ? "-" : enPassantPosition.toLAN()); + + // Halfmove clock + fields.put(FENField.HALFMOVE_CLOCK, String.valueOf(board.getLog().getHalfmoveClock())); + + // Fullmove counter + fields.put(FENField.FULLMOVE_NUMBER, String.valueOf(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.join(" ", fields.values()); + } + + /** + * @return a {@link Board} object corresponding to this {@link FENString} + */ + public Board getBoard() { return board; } +}