validate_fen static method

Map validate_fen(
  1. String fen
)

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]};
}