/* -*- Mode: vala; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- * * Copyright (C) 2010-2014 Robert Ancell * Copyright (C) 2015-2016 Sahil Sareen * * This program is free software: you can redistribute it and/or modify it under * the terms of the GNU General Public License as published by the Free Software * Foundation, either version 3 of the License, or (at your option) any later * version. See http://www.gnu.org/copyleft/gpl.html the full text of the * license. */ public enum CheckState { NONE, CHECK, CHECKMATE } public class ChessState : Object { public int number = 0; public ChessPlayer players[2]; public ChessPlayer current_player; public ChessPlayer opponent { get { return current_player.color == Color.WHITE ? players[Color.BLACK] : players[Color.WHITE]; } } public bool can_castle_kingside[2]; public bool can_castle_queenside[2]; public int en_passant_index = -1; public CheckState check_state; public int halfmove_clock; public ChessPiece board[64]; public ChessMove? last_move = null; /* Bitmap of all the pieces */ private uint64 piece_masks[2]; private ChessState.empty () { } // FIXME Enable or remove these exceptions. public ChessState (string fen) { players[Color.WHITE] = new ChessPlayer (Color.WHITE); players[Color.BLACK] = new ChessPlayer (Color.BLACK); for (int i = 0; i < 64; i++) board[i] = null; string[] fields = fen.split (" "); //if (fields.length != 6) // throw new Error ("Invalid FEN string"); /* Field 1: Piece placement */ string[] ranks = fields[0].split ("/"); //if (ranks.length != 8) // throw new Error ("Invalid piece placement"); for (int rank = 0; rank < 8; rank++) { var rank_string = ranks[7 - rank]; for (int file = 0, offset = 0; file < 8 && offset < rank_string.length; offset++) { var c = rank_string[offset]; if (c >= '1' && c <= '8') { file += c - '0'; continue; } PieceType type; var color = c.isupper () ? Color.WHITE : Color.BLACK; /*if (!*/ decode_piece_type (c.toupper (), out type) //) ;//throw new Error (""); int index = get_index (rank, file); ChessPiece piece = new ChessPiece (players[color], type); board[index] = piece; uint64 mask = BitBoard.set_location_masks[index]; piece_masks[color] |= mask; file++; } } /* Field 2: Active color */ if (fields[1] == "w") current_player = players[Color.WHITE]; else if (fields[1] == "b") current_player = players[Color.BLACK]; //else // throw new Error ("Unknown active color: %s", fields[1]); /* Field 3: Castling availability */ if (fields[2] != "-") { for (int i = 0; i < fields[2].length; i++) { var c = fields[2][i]; if (c == 'K') can_castle_kingside[Color.WHITE] = true; else if (c == 'Q') can_castle_queenside[Color.WHITE] = true; else if (c == 'k') can_castle_kingside[Color.BLACK] = true; else if (c == 'q') can_castle_queenside[Color.BLACK] = true; //else // throw new Error (""); } } /* Field 4: En passant target square */ if (fields[3] != "-") { //if (fields[3].length != 2) // throw new Error (""); en_passant_index = get_index (fields[3][1] - '1', fields[3][0] - 'a'); } /* Field 5: Halfmove clock */ halfmove_clock = int.parse (fields[4]); /* Field 6: Fullmove number */ number = (int.parse (fields[5]) - 1) * 2; if (current_player.color == Color.BLACK) number++; check_state = get_check_state (current_player); } public ChessState copy () { ChessState state = new ChessState.empty (); state.number = number; state.players[Color.WHITE] = players[Color.WHITE]; state.players[Color.BLACK] = players[Color.BLACK]; state.current_player = current_player; state.can_castle_kingside[Color.WHITE] = can_castle_kingside[Color.WHITE]; state.can_castle_queenside[Color.WHITE] = can_castle_queenside[Color.WHITE]; state.can_castle_kingside[Color.BLACK] = can_castle_kingside[Color.BLACK]; state.can_castle_queenside[Color.BLACK] = can_castle_queenside[Color.BLACK]; state.en_passant_index = en_passant_index; state.check_state = check_state; if (last_move != null) state.last_move = last_move.copy (); for (int i = 0; i < 64; i++) state.board[i] = board[i]; state.piece_masks[Color.WHITE] = piece_masks[Color.WHITE]; state.piece_masks[Color.BLACK] = piece_masks[Color.BLACK]; state.halfmove_clock = halfmove_clock; return state; } public bool equals (ChessState state) { /* * Check first if there is the same layout of pieces (unlikely), * then that the same player is on move, then that the move castling * and en-passant state are the same. This follows the rules for * determining threefold repetition: * * https://en.wikipedia.org/wiki/Threefold_repetition */ if (piece_masks[Color.WHITE] != state.piece_masks[Color.WHITE] || piece_masks[Color.BLACK] != state.piece_masks[Color.BLACK] || current_player.color != state.current_player.color || can_castle_kingside[Color.WHITE] != state.can_castle_kingside[Color.WHITE] || can_castle_queenside[Color.WHITE] != state.can_castle_queenside[Color.WHITE] || can_castle_kingside[Color.BLACK] != state.can_castle_kingside[Color.BLACK] || can_castle_queenside[Color.BLACK] != state.can_castle_queenside[Color.BLACK] || en_passant_index != state.en_passant_index) return false; /* Finally check the same piece types are present */ for (int i = 0; i < 64; i++) { if (board[i] != null && board[i].type != state.board[i].type) return false; } return true; } public string get_fen () { var value = new StringBuilder (); for (int rank = 7; rank >= 0; rank--) { int skip_count = 0; for (int file = 0; file < 8; file++) { var p = board[get_index (rank, file)]; if (p == null) skip_count++; else { if (skip_count > 0) { value.append_printf ("%d", skip_count); skip_count = 0; } value.append_printf ("%c", (int) p.symbol); } } if (skip_count > 0) value.append_printf ("%d", skip_count); if (rank != 0) value.append_c ('/'); } value.append_c (' '); if (current_player.color == Color.WHITE) value.append_c ('w'); else value.append_c ('b'); value.append_c (' '); if (can_castle_kingside[Color.WHITE]) value.append_c ('K'); if (can_castle_queenside[Color.WHITE]) value.append_c ('Q'); if (can_castle_kingside[Color.BLACK]) value.append_c ('k'); if (can_castle_queenside[Color.BLACK]) value.append_c ('q'); if (!(can_castle_kingside[Color.WHITE] | can_castle_queenside[Color.WHITE] | can_castle_kingside[Color.BLACK] | can_castle_queenside[Color.BLACK])) value.append_c ('-'); value.append_c (' '); if (en_passant_index >= 0) value.append_printf ("%c%d", 'a' + get_file (en_passant_index), get_rank (en_passant_index) + 1); else value.append_c ('-'); value.append_c (' '); value.append_printf ("%d", halfmove_clock); value.append_c (' '); if (current_player.color == Color.WHITE) value.append_printf ("%d", number / 2); else value.append_printf ("%d", number / 2 + 1); return value.str; } public int get_index (int rank, int file) { return rank * 8 + file; } public int get_rank (int index) { return index / 8; } public int get_file (int index) { return index % 8; } public bool move (string move, bool apply = true) { int r0, f0, r1, f1; PieceType promotion_type; if (!decode_move (current_player, move, out r0, out f0, out r1, out f1, out promotion_type)) return false; if (!move_with_coords (current_player, r0, f0, r1, f1, promotion_type, apply)) return false; return true; } public bool move_with_coords (ChessPlayer player, int r0, int f0, int r1, int f1, PieceType promotion_type = PieceType.QUEEN, bool apply = true, bool test_check = true) { // FIXME: Make this use indexes to be faster var start = get_index (r0, f0); var end = get_index (r1, f1); var color = player.color; var opponent_color = color == Color.WHITE ? Color.BLACK : Color.WHITE; /* Must be moving own piece */ var piece = board[start]; if (piece == null || piece.player != player) return false; /* Check valid move */ uint64 end_mask = BitBoard.set_location_masks[end]; uint64 move_mask = BitBoard.move_masks[color * 64*6 + piece.type * 64 + start]; if ((end_mask & move_mask) == 0) return false; /* Check no pieces in the way */ uint64 over_mask = BitBoard.over_masks[start * 64 + end]; if ((over_mask & (piece_masks[Color.WHITE] | piece_masks[Color.BLACK])) != 0) return false; /* Get victim of move */ var victim = board[end]; var victim_index = end; /* Can't take own pieces */ if (victim != null && victim.player == player) return false; /* Check special moves */ int rook_start = -1, rook_end = -1; bool is_promotion = false; bool en_passant = false; bool ambiguous_rank = false; bool ambiguous_file = false; switch (piece.type) { case PieceType.PAWN: /* Check if taking an marched pawn */ if (victim == null && end == en_passant_index) { en_passant = true; victim_index = get_index (r1 == 2 ? 3 : 4, f1); victim = board[victim_index]; } /* If moving diagonally there must be a victim */ if (f0 != f1) { if (victim == null) return false; } else { /* If moving forward can't take enemy */ if (victim != null) return false; } is_promotion = r1 == 0 || r1 == 7; /* Always show the file of a pawn capturing */ if (victim != null) ambiguous_file = true; break; case PieceType.KING: /* If moving more than one square must be castling */ if ((f0 - f1).abs () > 1) { /* File the rook is on */ rook_start = get_index (r0, f1 > f0 ? 7 : 0); rook_end = get_index (r0, f1 > f0 ? f1 - 1 : f1 + 1); /* Check if can castle */ if (f1 > f0) { if (!can_castle_kingside[color]) return false; } else { if (!can_castle_queenside[color]) return false; } var rook = board[rook_start]; if (rook == null || rook.type != PieceType.ROOK || rook.color != color) return false; /* Check rook can move */ uint64 rook_over_mask = BitBoard.over_masks[rook_start * 64 + rook_end]; if ((rook_over_mask & (piece_masks[Color.WHITE] | piece_masks[Color.BLACK])) != 0) return false; /* Can't castle when in check */ if (check_state == CheckState.CHECK) return false; /* Square moved across can't be under attack */ if (!move_with_coords (player, r0, f0, get_rank (rook_end), get_file (rook_end), PieceType.QUEEN, false, true)) return false; } break; default: break; } if (!apply && !test_check) return true; /* Check if other pieces of the same type can make this move - this is required for SAN notation */ if (apply) { for (int i = 0; i < 64; i++) { /* Ignore our move */ if (i == start) continue; /* Check for a friendly piece of the same type */ var p = board[i]; if (p == null || p.player != player || p.type != piece.type) continue; /* If more than one piece can move then the rank and/or file are ambiguous */ var r = get_rank (i); var f = get_file (i); if (move_with_coords (player, r, f, r1, f1, PieceType.QUEEN, false)) { if (r != r0) ambiguous_rank = true; if (f != f0) ambiguous_file = true; } } } var old_white_mask = piece_masks[Color.WHITE]; var old_black_mask = piece_masks[Color.BLACK]; var old_white_can_castle_kingside = can_castle_kingside[Color.WHITE]; var old_white_can_castle_queenside = can_castle_queenside[Color.WHITE]; var old_black_can_castle_kingside = can_castle_kingside[Color.BLACK]; var old_black_can_castle_queenside = can_castle_queenside[Color.BLACK]; var old_en_passant_index = en_passant_index; var old_halfmove_clock = halfmove_clock; /* Update board */ board[start] = null; piece_masks[Color.WHITE] &= BitBoard.clear_location_masks[start]; piece_masks[Color.BLACK] &= BitBoard.clear_location_masks[start]; if (victim != null) { board[victim_index] = null; piece_masks[Color.WHITE] &= BitBoard.clear_location_masks[victim_index]; piece_masks[Color.BLACK] &= BitBoard.clear_location_masks[victim_index]; } if (is_promotion) board[end] = new ChessPiece (player, promotion_type); else board[end] = piece; piece_masks[color] |= end_mask; piece_masks[opponent_color] &= BitBoard.clear_location_masks[end]; if (rook_start >= 0) { var rook = board[rook_start]; board[rook_start] = null; piece_masks[color] &= BitBoard.clear_location_masks[rook_start]; board[rook_end] = rook; piece_masks[color] |= BitBoard.set_location_masks[rook_end]; } /* Can't castle once king has moved */ if (piece.type == PieceType.KING) { can_castle_kingside[color] = false; can_castle_queenside[color] = false; } /* Can't castle once rooks have moved */ else if (piece.type == PieceType.ROOK) { int base_rank = color == Color.WHITE ? 0 : 7; if (r0 == base_rank) { if (f0 == 0) can_castle_queenside[color] = false; else if (f0 == 7) can_castle_kingside[color] = false; } } /* Can't castle once the rooks have been captured */ else if (victim != null && victim.type == PieceType.ROOK) { int base_rank = opponent_color == Color.WHITE ? 0 : 7; if (r1 == base_rank) { if (f1 == 0) can_castle_queenside[opponent_color] = false; else if (f1 == 7) can_castle_kingside[opponent_color] = false; } } /* Pawn square moved over is vulnerable */ if (piece.type == PieceType.PAWN && over_mask != 0) en_passant_index = get_index ((r0 + r1) / 2, f0); else en_passant_index = -1; /* Reset halfmove count when pawn moved or piece taken */ if (piece.type == PieceType.PAWN || victim != null) halfmove_clock = 0; else halfmove_clock++; /* Test if this move would leave that player in check */ bool result = true; if (test_check && is_in_check (player)) result = false; /* Undo move */ if (!apply || !result) { board[start] = piece; board[end] = null; if (victim != null) board[victim_index] = victim; if (rook_start >= 0) { var rook = board[rook_end]; board[rook_start] = rook; board[rook_end] = null; } piece_masks[Color.WHITE] = old_white_mask; piece_masks[Color.BLACK] = old_black_mask; can_castle_kingside[Color.WHITE] = old_white_can_castle_kingside; can_castle_queenside[Color.WHITE] = old_white_can_castle_queenside; can_castle_kingside[Color.BLACK] = old_black_can_castle_kingside; can_castle_queenside[Color.BLACK] = old_black_can_castle_queenside; en_passant_index = old_en_passant_index; halfmove_clock = old_halfmove_clock; return result; } current_player = color == Color.WHITE ? players[Color.BLACK] : players[Color.WHITE]; check_state = get_check_state (current_player); last_move = new ChessMove (); last_move.number = number; last_move.piece = piece; if (is_promotion) last_move.promotion_piece = board[end]; last_move.victim = victim; if (rook_end >= 0) last_move.castling_rook = board[rook_end]; last_move.r0 = r0; last_move.f0 = f0; last_move.r1 = r1; last_move.f1 = f1; last_move.ambiguous_rank = ambiguous_rank; last_move.ambiguous_file = ambiguous_file; last_move.en_passant = en_passant; last_move.check_state = check_state; return true; } public ChessResult get_result (out ChessRule rule) { rule = ChessRule.CHECKMATE; if (check_state == CheckState.CHECKMATE) { if (current_player.color == Color.WHITE) { rule = ChessRule.CHECKMATE; return ChessResult.BLACK_WON; } else { rule = ChessRule.CHECKMATE; return ChessResult.WHITE_WON; } } if (!can_move (current_player)) { rule = ChessRule.STALEMATE; return ChessResult.DRAW; } if (last_move != null && last_move.victim != null && !have_sufficient_material ()) { rule = ChessRule.INSUFFICIENT_MATERIAL; return ChessResult.DRAW; } return ChessResult.IN_PROGRESS; } private CheckState get_check_state (ChessPlayer player) { if (is_in_check (player)) { if (is_in_checkmate (player)) return CheckState.CHECKMATE; else return CheckState.CHECK; } return CheckState.NONE; } public bool get_positions_threatening_king (ChessPlayer player, out int[] rank, out int[] file) { var opponent = player.color == Color.WHITE ? players[Color.BLACK] : players[Color.WHITE]; bool found = false; /* Is in check if any piece can take the king */ for (int king_index = 0; king_index < 64; king_index++) { var p = board[king_index]; if (p != null && p.player == player && p.type == PieceType.KING) { /* See if any enemy pieces can take the king */ int[] ranks = {}; int[] files = {}; for (int start = 0; start < 64; start++) { if (move_with_coords (opponent, get_rank (start), get_file (start), get_rank (king_index), get_file (king_index), PieceType.QUEEN, false, false)) { ranks += get_rank (start); files += get_file (start); found = true; } } rank = ranks; file = files; } } return found; } public bool is_in_check (ChessPlayer player) { int[] rank, file; return get_positions_threatening_king (player, out rank, out file); } private bool is_in_checkmate (ChessPlayer player) { /* Is in checkmate if no pieces can move */ for (int piece_index = 0; piece_index < 64; piece_index++) { var p = board[piece_index]; if (p != null && p.player == player) { for (int end = 0; end < 64; end++) { if (move_with_coords (player, get_rank (piece_index), get_file (piece_index), get_rank (end), get_file (end), PieceType.QUEEN, false, true)) return false; } } } return true; } public bool can_move (ChessPlayer player) { bool have_pieces = false; for (int start = 0; start < 64; start++) { var p = board[start]; if (p != null && p.player == player) { have_pieces = true; /* See if can move anywhere */ for (int end = 0; end < 64; end++) { if (move_with_coords (player, get_rank (start), get_file (start), get_rank (end), get_file (end), PieceType.QUEEN, false, true)) return true; } } } /* Only mark as stalemate if have at least one piece */ if (have_pieces) return false; else return true; } public bool have_sufficient_material () { var white_knight_count = 0; var white_bishop_count = 0; var white_bishop_on_white_square = false; var white_bishop_on_black_square = false; var black_knight_count = 0; var black_bishop_count = 0; var black_bishop_on_white_square = false; var black_bishop_on_black_square = false; for (int i = 0; i < 64; i++) { var p = board[i]; if (p == null) continue; /* Any pawns, rooks or queens can perform checkmate */ if (p.type == PieceType.PAWN || p.type == PieceType.ROOK || p.type == PieceType.QUEEN) return true; /* Otherwise, count the minor pieces for each colour... */ if (p.type == PieceType.KNIGHT) { if (p.color == Color.WHITE) white_knight_count++; else black_knight_count++; } if (p.type == PieceType.BISHOP) { var color = Color.BLACK; if ((i + i/8) % 2 != 0) color = Color.WHITE; if (p.color == Color.WHITE) { if (color == Color.WHITE) white_bishop_on_white_square = true; else white_bishop_on_black_square = true; white_bishop_count++; } else { if (color == Color.WHITE) black_bishop_on_white_square = true; else black_bishop_on_black_square = true; black_bishop_count++; } } /* * We count the following positions as insufficient: * * 1) king versus king * 2) king and bishop versus king * 3) king and knight versus king * 4) king and bishop versus king and bishop with the bishops on the same color. (Any * number of additional bishops of either color on the same color of square due to * underpromotion do not affect the situation.) * * From: https://en.wikipedia.org/wiki/Draw_(chess)#Draws_in_all_games * * Note also that this follows FIDE rules, not USCF rules. E.g. K+N+N vs. K cannot be * forced, so it's not counted as a draw. * * This is also what CECP engines will be expecting: * * "Note that (in accordance with FIDE rules) only KK, KNK, KBK and KBKB with all * bishops on the same color can be claimed as draws on the basis of insufficient mating * material. The end-games KNNK, KBKN, KNKN and KBKB with unlike bishops do have mate * positions, and cannot be claimed. Complex draws based on locked Pawn chains will not * be recognized as draws by most interfaces, so do not claim in such positions, but * just offer a draw or play on." * * From: http://www.open-aurec.com/wbforum/WinBoard/engine-intf.html * * (In contrast, UCI seems to expect the interface to handle draws itself.) */ /* Two knights versus king can checkmate (though not against an optimal opponent) */ if (white_knight_count > 1 || black_knight_count > 1) return true; /* Bishop and knight versus king can checkmate */ if (white_bishop_count > 0 && white_knight_count > 0) return true; if (black_bishop_count > 0 && black_knight_count > 0) return true; /* King and bishops versus king can checkmate as long as the bishops are on both colours */ if (white_bishop_on_white_square && white_bishop_on_black_square) return true; if (black_bishop_on_white_square && black_bishop_on_black_square) return true; /* King and minor piece vs. King and knight is surprisingly not a draw */ if ((white_bishop_count > 0 || white_knight_count > 0) && black_knight_count > 0) return true; if ((black_bishop_count > 0 || black_knight_count > 0) && white_knight_count > 0) return true; /* King and bishop can checkmate vs. king and bishop if bishops are on opposite colors */ if (white_bishop_count > 0 && black_bishop_count > 0) { if (white_bishop_on_white_square && black_bishop_on_black_square) return true; else if (white_bishop_on_black_square && black_bishop_on_white_square) return true; } } return false; } private bool decode_piece_type (unichar c, out PieceType type) { type = PieceType.PAWN; switch (c) { case 'P': type = PieceType.PAWN; return true; case 'R': type = PieceType.ROOK; return true; case 'N': type = PieceType.KNIGHT; return true; case 'B': type = PieceType.BISHOP; return true; case 'Q': type = PieceType.QUEEN; return true; case 'K': type = PieceType.KING; return true; default: return false; } } private bool decode_move (ChessPlayer player, string move, out int r0, out int f0, out int r1, out int f1, out PieceType promotion_type) { int i = 0; promotion_type = PieceType.QUEEN; if (move.has_prefix ("O-O-O")) { if (player.color == Color.WHITE) r0 = r1 = 0; else r0 = r1 = 7; f0 = 4; f1 = 2; i += (int) "O-O-O".length; } else if (move.has_prefix ("O-O")) { if (player.color == Color.WHITE) r0 = r1 = 0; else r0 = r1 = 7; f0 = 4; f1 = 6; i += (int) "O-O".length; } else { PieceType type = PieceType.PAWN; if (decode_piece_type (move[i], out type)) i++; r0 = f0 = r1 = f1 = -1; if (move[i] >= 'a' && move[i] <= 'h') { f1 = (int) (move[i] - 'a'); i++; } if (move[i] >= '1' && move[i] <= '8') { r1 = (int) (move[i] - '1'); i++; } if (move[i] == 'x' || move[i] == '-') i++; if (move[i] >= 'a' && move[i] <= 'h') { f0 = f1; f1 = (int) (move[i] - 'a'); i++; } if (move[i] >= '1' && move[i] <= '8') { r0 = r1; r1 = (int) (move[i] - '1'); i++; } if (move[i] == '=') { i++; if (decode_piece_type (move[i], out promotion_type)) i++; } else if (move[i] != '\0') { switch (move[i]) { case 'q': case 'Q': promotion_type = PieceType.QUEEN; i++; break; case 'n': case 'N': promotion_type = PieceType.KNIGHT; i++; break; case 'r': case 'R': promotion_type = PieceType.ROOK; i++; break; case 'b': case 'B': promotion_type = PieceType.BISHOP; i++; break; } } /* Don't have a destination to move to */ if (r1 < 0 || f1 < 0) { debug ("Move %s missing destination", move); return false; } /* Find source piece */ if (r0 < 0 || f0 < 0) { int match_rank = -1, match_file = -1; for (int file = 0; file < 8; file++) { if (f0 >= 0 && file != f0) continue; for (int rank = 0; rank < 8; rank++) { if (r0 >= 0 && rank != r0) continue; /* Only check this players pieces of the correct type */ var piece = board[get_index (rank, file)]; if (piece == null || piece.type != type || piece.player != player) continue; /* See if can move here */ if (!this.move_with_coords (player, rank, file, r1, f1, PieceType.QUEEN, false)) continue; /* Duplicate match */ if (match_rank >= 0) { debug ("Move %s is ambiguous", move); return false; } match_rank = rank; match_file = file; } } if (match_rank < 0) { debug ("Move %s has no matches", move); return false; } r0 = match_rank; f0 = match_file; } } if (move[i] == '+') i++; else if (move[i] == '#') i++; if (move[i] != '\0') { debug ("Move %s has unexpected characters", move); return false; } return true; } }