validate_fen static method
Check the formatting of a FEN String is correct Returns a Map with keys valid, error_number, and error
Implementation
static Map validate_fen(String fen) {
const 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].',
11: '1st field (piece positions) is invalid [wrong kings counts]',
12: '1st field (piece positions) is invalid [kings on neighbours cells]',
13: '1st field (piece positions) is invalid [pawn(s) on first/last rank]',
14: 'King of opponent player is in check.',
};
/* 1st criterion: 6 space-seperated fields? */
List tokens = fen.split(RegExp(r'\s+'));
if (tokens.length != 6) {
return {'valid': false, 'error_number': 1, 'error': errors[1]};
}
/* 2nd criterion: move number field is a integer value > 0? */
var 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]};
}
/* 3rd criterion: half move counter is an integer >= 0? */
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]};
}
/* 4th criterion: 4th field is a valid e.p.-string? */
final check4 = RegExp(r'^(-|[abcdefgh][36])$');
if (check4.firstMatch(tokens[3]) == null) {
return {'valid': false, 'error_number': 4, 'error': errors[4]};
}
/* 5th criterion: 3th field is a valid castle-string? */
final check5 = RegExp(r'^(KQ?k?q?|Qk?q?|kq?|q|-)$');
if (check5.firstMatch(tokens[2]) == null) {
return {'valid': false, 'error_number': 5, 'error': errors[5]};
}
/* 6th criterion: 2nd field is "w" (white) or "b" (black)? */
var check6 = RegExp(r'^([wb])$');
if (check6.firstMatch(tokens[1]) == null) {
return {'valid': false, 'error_number': 6, 'error': errors[6]};
}
/* 7th criterion: 1st field contains 8 rows? */
List rows = tokens[0].split('/');
if (rows.length != 8) {
return {'valid': false, 'error_number': 7, 'error': errors[7]};
}
/* 8th criterion: every row is valid? */
for (var i = 0; i < rows.length; i++) {
/* check for right sum of fields AND not two numbers in succession */
var sum_fields = 0;
var previous_was_number = false;
for (var k = 0; k < rows[i].length; k++) {
final temp2 = int.tryParse(rows[i][k]);
if (temp2 != null) {
if (previous_was_number) {
return {'valid': false, 'error_number': 8, 'error': errors[8]};
}
sum_fields += temp2;
previous_was_number = true;
} else {
final checkOM = RegExp(r'^[prnbqkPRNBQK]$');
if (checkOM.firstMatch(rows[i][k]) == null) {
return {'valid': false, 'error_number': 9, 'error': errors[9]};
}
sum_fields += 1;
previous_was_number = false;
}
}
if (sum_fields != 8) {
return {'valid': false, 'error_number': 10, 'error': errors[10]};
}
}
final boardPart = fen.split(' ')[0];
/* Is white and black kings' count legal (except for empty board) ? */
final isEmptyBoard = boardPart == '8/8/8/8/8/8/8/8';
final whiteKingCount =
boardPart.split('').where((elem) => elem == 'K').length;
final blackKingCount =
boardPart.split('').where((elem) => elem == 'k').length;
if (!isEmptyBoard && (whiteKingCount != 1 || blackKingCount != 1)) {
return {'valid': false, 'error_number': 11, 'error': errors[11]};
}
/* Are both kings on neighbours cells ? */
// Computes a kind of 'expanded' FEN : cells are translated as underscores,
// and removing all slashes.
final expandedFen = boardPart.split('').fold<String>('', (accum, curr) {
final digitValue = int.tryParse(curr);
if (curr == '/') {
return accum;
} else if (digitValue != null) {
var result = '';
for (var i = 0; i < digitValue; i++) {
result += '_';
}
return accum + result;
} else {
return accum + curr;
}
});
final whiteKingIndex = expandedFen.indexOf('K');
final blackKingIndex = expandedFen.indexOf('k');
final whiteKingCoords = [whiteKingIndex % 8, whiteKingIndex ~/ 8];
final blackKingCoords = [blackKingIndex % 8, blackKingIndex ~/ 8];
final deltaX = (whiteKingCoords[0] - blackKingCoords[0]).abs();
final deltaY = (whiteKingCoords[1] - blackKingCoords[1]).abs();
final kingsTooClose = (deltaX <= 1) && (deltaY <= 1);
if (!isEmptyBoard && kingsTooClose) {
return {'valid': false, 'error_number': 12, 'error': errors[12]};
}
/* Any pawn on first or last rank ? */
final firstRank = boardPart.split('/')[0];
final lastRank = boardPart.split('/')[7];
final whitePawnOnFirstRank = firstRank.contains('P');
final blackPawnOnFirstRank = firstRank.contains('p');
final whitePawnOnLastRank = lastRank.contains('P');
final blackPawnOnLastRank = lastRank.contains('p');
if (whitePawnOnFirstRank ||
whitePawnOnLastRank ||
blackPawnOnFirstRank ||
blackPawnOnLastRank) {
return {'valid': false, 'error_number': 13, 'error': errors[13]};
}
/* Is king of player in turn in check (without being in checkmate) ? */
final board = Chess.fromFEN(fen, check_validity: false);
final turn = board.turn;
final opponentTurn = turn == Color.WHITE ? Color.BLACK : Color.WHITE;
final kingInChess =
!board.in_checkmate && board.king_attacked(opponentTurn);
if (kingInChess) {
return {'valid': false, 'error_number': 14, 'error': errors[14]};
}
/* everything's okay! */
return {'valid': true, 'error_number': 0, 'error': errors[0]};
}