validateFen static method
Check the formatting of a FEN String is correct Returns a Map with keys valid, error_number, and error
Implementation
static Map validateFen(fen) {
Map errors = {
0: 'No errors.',
1: 'FEN string must contain six space-delimited fields.',
2: '6th field (move number) must be a positive integer.',
3: '5th field (half move counter) must be a non-negative integer.',
4: '4th field (en-passant square) is invalid.',
5: '3rd field (castling availability) is invalid.',
6: '2nd field (side to move) is invalid.',
7: '1st field (piece positions) does not contain 8 \'/\'-delimited rows.',
8: '1st field (piece positions) is invalid [consecutive numbers].',
9: '1st field (piece positions) is invalid [invalid piece].',
10: '1st field (piece positions) is invalid [row too large].',
};
/// Fields of the FEN are separated by spaces
/// retrieve fields form the FEN -> must be 6 of them
/// Wikipedia:
/// A FEN record contains six fields.
/// The separator between fields is a space
List tokens = fen.split(RegExp(r"\s+"));
if (tokens.length != 6) {
return {'valid': false, 'error_number': 1, 'error': errors[1]};
}
/// the last Field of FEN should be the number of moves
/// this should be a positive integer since starts at 1
/// Wikipedia: "Fullmove number: The number of the full move.
/// It starts at 1, and is incremented after Black's move."
int? temp = int.tryParse(tokens[5]);
if (temp != null) {
if (temp <= 0) {
return {'valid': false, 'error_number': 2, 'error': errors[2]};
}
} else {
return {'valid': false, 'error_number': 2, 'error': errors[2]};
}
/// The second to last field is the counter
/// of moves for the 50 moves draw rule
temp = int.tryParse(tokens[4]);
if (temp != null) {
if (temp < 0) {
return {'valid': false, 'error_number': 3, 'error': errors[3]};
}
} else {
return {'valid': false, 'error_number': 3, 'error': errors[3]};
}
/// Wikipedia: En passant target square in algebraic notation.
/// If there's no en passant target square, this is "-".
/// If a pawn has just made a two-square move,
/// this is the position "behind" the pawn.
/// This is recorded regardless of whether there is a pawn
/// in position to make an en passant capture.
RegExp checkEnPassant = new RegExp(r"^(-|[abcdefgh][36])$");
if (checkEnPassant.firstMatch(tokens[3]) == null) {
return {'valid': false, 'error_number': 4, 'error': errors[4]};
}
/// Wikipedia:
/// Castling availability. If neither side can castle, this is "-".
/// Otherwise, this has one or more letters: "K" (White can castle kingside),
/// "Q" (White can castle queenside), "k" (Black can castle kingside),
/// and/or "q" (Black can castle queenside).
/// A move that temporarily prevents castling does not negate this notation.
RegExp checkCastling = new RegExp(r"^(KQ?k?q?|Qk?q?|kq?|q|-)$");
if (checkCastling.firstMatch(tokens[2]) == null) {
return {'valid': false, 'error_number': 5, 'error': errors[5]};
}
/// Wikipedia:
/// Active color. "w" means White moves next,
/// "b" means Black moves next.
RegExp moveTurn = new RegExp(r"^(w|b)$");
if (moveTurn.firstMatch(tokens[1]) == null) {
return {'valid': false, 'error_number': 6, 'error': errors[6]};
}
/// Wikipedia:
/// Piece placement (from White's perspective).
/// Each rank is described, starting with rank 8 and ending with rank 1
/// within each rank, the contents of each square are described from file
/// "a" through file "h". Using the Standard Algebraic Notation (SAN)
/// There must be 8 rows
List rows = tokens[0].split('/');
if (rows.length != 8) {
return {'valid': false, 'error_number': 7, 'error': errors[7]};
}
/// Check validity of rows
for (int i = 0; i < rows.length; i++) {
/// there should be 8 rows, increment
/// sumFields after check each valid row
int sumFields = 0;
bool previousWasNumber = false;
/// Check content of rows, there shouldn't be two successive numbers
/// since each number is the number of empty squares between two pieces
for (int k = 0; k < rows[i].length; k++) {
int? temp2 = int.tryParse(rows[i][k]);
/// if not null then is a number in the row and
/// the previous token should not be a number
if (temp2 != null) {
if (previousWasNumber) {
return {'valid': false, 'error_number': 8, 'error': errors[8]};
}
sumFields += temp2;
previousWasNumber = true;
}
/// if it's not a number then it's a piece token check
/// with a regular expression
else {
RegExp checkOM = RegExp(r"^[prnbqkPRNBQK]$");
if (checkOM.firstMatch(rows[i][k]) == null) {
return {'valid': false, 'error_number': 9, 'error': errors[9]};
}
sumFields += 1;
previousWasNumber = false;
}
}
if (sumFields != 8) {
return {'valid': false, 'error_number': 10, 'error': errors[10]};
}
}
/// Everything is ok
return {'valid': true, 'error_number': 0, 'error': errors[0]};
}