239 lines
6.6 KiB
Java
239 lines
6.6 KiB
Java
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: <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.
|
|
*
|
|
* @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(
|
|
"^(?<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;
|
|
try {
|
|
Constructor<? extends Piece> 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; }
|
|
}
|