Added FENString class

This commit is contained in:
Kai S. K. Engelbart 2019-10-24 06:09:16 +02:00
parent 73f5e17405
commit 47db284b9b

View File

@ -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: <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 Map<FENField, String> 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(
"^(?<PIECE_PLACEMENT>(?:[1-8nbrqkpNBRQKP]{1,8}\\/){7}[1-8nbrqkpNBRQKP]{1,8}) (?<ACTIVE_COLOR>[wb]) (?<CASTLING_AVAILABILITY>-|[KQkq]{1,4}) (?<EN_PASSANT_TARGET_SQUARE>-|[a-h][1-8]) (?<HALFMOVE_CLOCK>\\d) (?<FULLMOVE_NUMBER>\\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; }
}