223 lines
6.5 KiB
Java
223 lines
6.5 KiB
Java
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: <strong>Chess</strong><br>
|
|
* File: <strong>FENString.java</strong><br>
|
|
* Created: <strong>20 Oct 2019</strong><br>
|
|
* <br>
|
|
* 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(
|
|
"^(?<piecePlacement>(?:[1-8nbrqkpNBRQKP]{1,8}\\/){7}[1-8nbrqkpNBRQKP]{1,8}) (?<activeColor>[wb]) (?<castlingAvailability>-|[KQkq]{1,4}) (?<enPassantTargetSquare>-|[a-h][1-8]) (?<halfmoveClock>\\d+) (?<fullmoveNumber>\\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; }
|
|
}
|