Refined IO functionality, fixed FEN string serialization and deserialization #7
194
src/dev/kske/chess/board/FENString.java
Normal file
194
src/dev/kske/chess/board/FENString.java
Normal 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; }
|
||||||
|
}
|
Reference in New Issue
Block a user