diff --git a/src/dev/kske/chess/board/FENString.java b/src/dev/kske/chess/board/FENString.java
new file mode 100644
index 0000000..5e53511
--- /dev/null
+++ b/src/dev/kske/chess/board/FENString.java
@@ -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: 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.
+ *
+ * @author Kai S. K. Engelbart
+ * @since Chess v0.4-alpha
+ */
+public class FENString {
+
+ private Board board;
+ private Map 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(
+ "^(?(?:[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());
+ 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; }
+}