package dev.kske.chess.board; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; 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. * * @since Chess v0.5-alpha * @author Kai S. K. Engelbart */ 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 if the FEN string contains invalid syntax */ 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; try { Constructor pieceConstructor = Piece .fromFirstChar(c) .getDeclaredConstructor(Color.class, Board.class); pieceConstructor.setAccessible(true); board.getBoardArr()[j][i] = pieceConstructor.newInstance(color, board); } catch ( InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException | NoSuchMethodException | SecurityException e ) { e.printStackTrace(); } ++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.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; } }