diff options
author | Andreas Kling <kling@serenityos.org> | 2021-01-12 12:17:30 +0100 |
---|---|---|
committer | Andreas Kling <kling@serenityos.org> | 2021-01-12 12:17:46 +0100 |
commit | 13d7c09125f8eec703d0a43a9a87fc8aa08f7319 (patch) | |
tree | 70fd643c429cea5c1f9362c2674511d17a53f3b5 /Userland/Libraries/LibChess | |
parent | dc28c07fa526841e05e16161c74a6c23984f1dd5 (diff) | |
download | serenity-13d7c09125f8eec703d0a43a9a87fc8aa08f7319.zip |
Libraries: Move to Userland/Libraries/
Diffstat (limited to 'Userland/Libraries/LibChess')
-rw-r--r-- | Userland/Libraries/LibChess/CMakeLists.txt | 8 | ||||
-rw-r--r-- | Userland/Libraries/LibChess/Chess.cpp | 970 | ||||
-rw-r--r-- | Userland/Libraries/LibChess/Chess.h | 324 | ||||
-rw-r--r-- | Userland/Libraries/LibChess/UCICommand.cpp | 332 | ||||
-rw-r--r-- | Userland/Libraries/LibChess/UCICommand.h | 291 | ||||
-rw-r--r-- | Userland/Libraries/LibChess/UCIEndpoint.cpp | 132 | ||||
-rw-r--r-- | Userland/Libraries/LibChess/UCIEndpoint.h | 80 |
7 files changed, 2137 insertions, 0 deletions
diff --git a/Userland/Libraries/LibChess/CMakeLists.txt b/Userland/Libraries/LibChess/CMakeLists.txt new file mode 100644 index 0000000000..07c6a5aa47 --- /dev/null +++ b/Userland/Libraries/LibChess/CMakeLists.txt @@ -0,0 +1,8 @@ +set(SOURCES + Chess.cpp + UCICommand.cpp + UCIEndpoint.cpp +) + +serenity_lib(LibChess chess) +target_link_libraries(LibChess LibC LibCore) diff --git a/Userland/Libraries/LibChess/Chess.cpp b/Userland/Libraries/LibChess/Chess.cpp new file mode 100644 index 0000000000..fae0c1e513 --- /dev/null +++ b/Userland/Libraries/LibChess/Chess.cpp @@ -0,0 +1,970 @@ +/* + * Copyright (c) 2020, the SerenityOS developers. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include <AK/Assertions.h> +#include <AK/LogStream.h> +#include <AK/String.h> +#include <AK/StringBuilder.h> +#include <AK/Vector.h> +#include <LibChess/Chess.h> +#include <stdlib.h> + +namespace Chess { + +String char_for_piece(Chess::Type type) +{ + switch (type) { + case Type::Knight: + return "N"; + case Type::Bishop: + return "B"; + case Type::Rook: + return "R"; + case Type::Queen: + return "Q"; + case Type::King: + return "K"; + case Type::Pawn: + default: + return ""; + } +} + +Chess::Type piece_for_char_promotion(const StringView& str) +{ + String string = String(str).to_lowercase(); + if (string == "") + return Type::None; + if (string == "n") + return Type::Knight; + if (string == "b") + return Type::Bishop; + if (string == "r") + return Type::Rook; + if (string == "q") + return Type::Queen; + if (string == "k") + return Type::King; + + return Type::None; +} + +Color opposing_color(Color color) +{ + return (color == Color::White) ? Color::Black : Color::White; +} + +Square::Square(const StringView& name) +{ + ASSERT(name.length() == 2); + char filec = name[0]; + char rankc = name[1]; + + if (filec >= 'a' && filec <= 'h') { + file = filec - 'a'; + } else if (filec >= 'A' && filec <= 'H') { + file = filec - 'A'; + } else { + ASSERT_NOT_REACHED(); + } + + if (rankc >= '1' && rankc <= '8') { + rank = rankc - '1'; + } else { + ASSERT_NOT_REACHED(); + } +} + +String Square::to_algebraic() const +{ + StringBuilder builder; + builder.append(file + 'a'); + builder.append(rank + '1'); + return builder.build(); +} + +Move::Move(const StringView& long_algebraic) + : from(long_algebraic.substring_view(0, 2)) + , to(long_algebraic.substring_view(2, 2)) + , promote_to(piece_for_char_promotion((long_algebraic.length() >= 5) ? long_algebraic.substring_view(4, 1) : "")) +{ +} + +String Move::to_long_algebraic() const +{ + StringBuilder builder; + builder.append(from.to_algebraic()); + builder.append(to.to_algebraic()); + builder.append(char_for_piece(promote_to).to_lowercase()); + return builder.build(); +} + +Move Move::from_algebraic(const StringView& algebraic, const Color turn, const Board& board) +{ + String move_string = algebraic; + Move move({ 50, 50 }, { 50, 50 }); + + if (move_string.contains("-")) { + move.from = Square(turn == Color::White ? 0 : 7, 4); + move.to = Square(turn == Color::White ? 0 : 7, move_string == "O-O" ? 6 : 2); + move.promote_to = Type::None; + move.piece = { turn, Type::King }; + + return move; + } + + if (algebraic.contains("#")) { + move.is_mate = true; + move_string = move_string.substring(0, move_string.length() - 1); + } else if (algebraic.contains("+")) { + move.is_check = true; + move_string = move_string.substring(0, move_string.length() - 1); + } + + if (algebraic.contains("=")) { + move.promote_to = piece_for_char_promotion(move_string.split('=').at(1).substring(0, 1)); + move_string = move_string.split('=').at(0); + } + + move.to = Square(move_string.substring(move_string.length() - 2, 2)); + move_string = move_string.substring(0, move_string.length() - 2); + + if (move_string.contains("x")) { + move.is_capture = true; + move_string = move_string.substring(0, move_string.length() - 1); + } + + if (move_string.is_empty() || move_string.characters()[0] >= 'a') { + move.piece = Piece(turn, Type::Pawn); + } else { + move.piece = Piece(turn, piece_for_char_promotion(move_string.substring(0, 1))); + move_string = move_string.substring(1, move_string.length() - 1); + } + + Square::for_each([&](const Square& square) { + if (!move_string.is_empty()) { + if (board.get_piece(square).type == move.piece.type && board.is_legal(Move(square, move.to), turn)) { + if (move_string.length() >= 2) { + if (square == Square(move_string.substring(0, 2))) { + move.from = square; + return IterationDecision::Break; + } + } else if (move_string.characters()[0] <= 57) { + if (square.rank == (unsigned)(move_string.characters()[0] - '0')) { + move.from = square; + return IterationDecision::Break; + } + } else { + if (square.file == (unsigned)(move_string.characters()[0] - 'a')) { + move.from = square; + return IterationDecision::Break; + } + } + } + return IterationDecision::Continue; + } else { + if (board.get_piece(square).type == move.piece.type && board.is_legal(Move(square, move.to), turn)) { + move.from = square; + return IterationDecision::Break; + } + return IterationDecision::Continue; + } + }); + + return move; +} + +String Move::to_algebraic() const +{ + if (piece.type == Type::King && from.file == 4) { + if (to.file == 2) + return "O-O-O"; + if (to.file == 6) + return "O-O"; + } + + StringBuilder builder; + + builder.append(char_for_piece(piece.type)); + + if (is_ambiguous) { + if (from.file != ambiguous.file) + builder.append(from.to_algebraic().substring(0, 1)); + else if (from.rank != ambiguous.rank) + builder.append(from.to_algebraic().substring(1, 1)); + else + builder.append(from.to_algebraic()); + } + + if (is_capture) { + if (piece.type == Type::Pawn) + builder.append(from.to_algebraic().substring(0, 1)); + builder.append("x"); + } + + builder.append(to.to_algebraic()); + + if (promote_to != Type::None) { + builder.append("="); + builder.append(char_for_piece(promote_to)); + } + + if (is_mate) + builder.append("#"); + else if (is_check) + builder.append("+"); + + return builder.build(); +} + +Board::Board() +{ + // Fill empty spaces. + for (unsigned rank = 2; rank < 6; ++rank) { + for (unsigned file = 0; file < 8; ++file) { + set_piece({ rank, file }, EmptyPiece); + } + } + + // Fill white pawns. + for (unsigned file = 0; file < 8; ++file) { + set_piece({ 1, file }, { Color::White, Type::Pawn }); + } + + // Fill black pawns. + for (unsigned file = 0; file < 8; ++file) { + set_piece({ 6, file }, { Color::Black, Type::Pawn }); + } + + // Fill while pieces. + set_piece(Square("a1"), { Color::White, Type::Rook }); + set_piece(Square("b1"), { Color::White, Type::Knight }); + set_piece(Square("c1"), { Color::White, Type::Bishop }); + set_piece(Square("d1"), { Color::White, Type::Queen }); + set_piece(Square("e1"), { Color::White, Type::King }); + set_piece(Square("f1"), { Color::White, Type::Bishop }); + set_piece(Square("g1"), { Color::White, Type::Knight }); + set_piece(Square("h1"), { Color::White, Type::Rook }); + + // Fill black pieces. + set_piece(Square("a8"), { Color::Black, Type::Rook }); + set_piece(Square("b8"), { Color::Black, Type::Knight }); + set_piece(Square("c8"), { Color::Black, Type::Bishop }); + set_piece(Square("d8"), { Color::Black, Type::Queen }); + set_piece(Square("e8"), { Color::Black, Type::King }); + set_piece(Square("f8"), { Color::Black, Type::Bishop }); + set_piece(Square("g8"), { Color::Black, Type::Knight }); + set_piece(Square("h8"), { Color::Black, Type::Rook }); +} + +String Board::to_fen() const +{ + StringBuilder builder; + + // 1. Piece placement + int empty = 0; + for (unsigned rank = 0; rank < 8; rank++) { + for (unsigned file = 0; file < 8; file++) { + const Piece p(get_piece({ 7 - rank, file })); + if (p.type == Type::None) { + empty++; + continue; + } + if (empty > 0) { + builder.append(String::number(empty)); + empty = 0; + } + String piece = char_for_piece(p.type); + if (piece == "") + piece = "P"; + + builder.append(p.color == Color::Black ? piece.to_lowercase() : piece); + } + if (empty > 0) { + builder.append(String::number(empty)); + empty = 0; + } + if (rank < 7) + builder.append("/"); + } + + // 2. Active color + ASSERT(m_turn != Color::None); + builder.append(m_turn == Color::White ? " w " : " b "); + + // 3. Castling availability + builder.append(m_white_can_castle_kingside ? "K" : ""); + builder.append(m_white_can_castle_queenside ? "Q" : ""); + builder.append(m_black_can_castle_kingside ? "k" : ""); + builder.append(m_black_can_castle_queenside ? "q" : ""); + builder.append(" "); + + // 4. En passant target square + if (!m_last_move.has_value()) + builder.append("-"); + else if (m_last_move.value().piece.type == Type::Pawn) { + if (m_last_move.value().from.rank == 1 && m_last_move.value().to.rank == 3) + builder.append(Square(m_last_move.value().to.rank - 1, m_last_move.value().to.file).to_algebraic()); + else if (m_last_move.value().from.rank == 6 && m_last_move.value().to.rank == 4) + builder.append(Square(m_last_move.value().to.rank + 1, m_last_move.value().to.file).to_algebraic()); + else + builder.append("-"); + } else { + builder.append("-"); + } + builder.append(" "); + + // 5. Halfmove clock + builder.append(String::number(min(m_moves_since_capture, m_moves_since_pawn_advance))); + builder.append(" "); + + // 6. Fullmove number + builder.append(String::number(1 + m_moves.size() / 2)); + + return builder.to_string(); +} + +Piece Board::get_piece(const Square& square) const +{ + ASSERT(square.rank < 8); + ASSERT(square.file < 8); + return m_board[square.rank][square.file]; +} + +Piece Board::set_piece(const Square& square, const Piece& piece) +{ + ASSERT(square.rank < 8); + ASSERT(square.file < 8); + return m_board[square.rank][square.file] = piece; +} + +bool Board::is_legal_promotion(const Move& move, Color color) const +{ + auto piece = get_piece(move.from); + + if (move.promote_to == Type::Pawn || move.promote_to == Type::King) { + // attempted promotion to invalid piece + return false; + } + + if (piece.type != Type::Pawn && move.promote_to != Type::None) { + // attempted promotion from invalid piece + return false; + } + + unsigned promotion_rank = (color == Color::White) ? 7 : 0; + + if (move.to.rank != promotion_rank && move.promote_to != Type::None) { + // attempted promotion from invalid rank + return false; + } + + if (piece.type == Type::Pawn && move.to.rank == promotion_rank && move.promote_to == Type::None) { + // attempted move to promotion rank without promoting + return false; + } + + return true; +} + +bool Board::is_legal(const Move& move, Color color) const +{ + if (color == Color::None) + color = turn(); + + if (!is_legal_no_check(move, color)) + return false; + + if (!is_legal_promotion(move, color)) + return false; + + Board clone = *this; + clone.apply_illegal_move(move, color); + if (clone.in_check(color)) + return false; + + // Don't allow castling through check or out of check. + Vector<Square> check_squares; + if (color == Color::White && move.from == Square("e1") && get_piece(Square("e1")) == Piece(Color::White, Type::King)) { + if (move.to == Square("a1") || move.to == Square("c1")) { + check_squares = { Square("e1"), Square("d1"), Square("c1") }; + } else if (move.to == Square("h1") || move.to == Square("g1")) { + check_squares = { Square("e1"), Square("f1"), Square("g1") }; + } + } else if (color == Color::Black && move.from == Square("e8") && get_piece(Square("e8")) == Piece(Color::Black, Type::King)) { + if (move.to == Square("a8") || move.to == Square("c8")) { + check_squares = { Square("e8"), Square("d8"), Square("c8") }; + } else if (move.to == Square("h8") || move.to == Square("g8")) { + check_squares = { Square("e8"), Square("f8"), Square("g8") }; + } + } + for (auto& square : check_squares) { + Board clone = *this; + clone.set_piece(move.from, EmptyPiece); + clone.set_piece(square, { color, Type::King }); + if (clone.in_check(color)) + return false; + } + + return true; +} + +bool Board::is_legal_no_check(const Move& move, Color color) const +{ + auto piece = get_piece(move.from); + + if (piece.color != color) + // attempted move of opponent's piece + return false; + + if (move.to.rank > 7 || move.to.file > 7) + // attempted move outside of board + return false; + + if (piece.type == Type::Pawn) { + int dir = (color == Color::White) ? +1 : -1; + unsigned start_rank = (color == Color::White) ? 1 : 6; + + if (move.from.rank == start_rank && move.to.rank == move.from.rank + (2 * dir) && move.to.file == move.from.file + && get_piece(move.to).type == Type::None && get_piece({ move.from.rank + dir, move.from.file }).type == Type::None) { + // 2 square pawn move from initial position. + return true; + } + + if (move.to.rank != move.from.rank + dir) + // attempted backwards or sideways move + return false; + + if (move.to.file == move.from.file && get_piece(move.to).type == Type::None) { + // Regular pawn move. + return true; + } + + if (move.to.file == move.from.file + 1 || move.to.file == move.from.file - 1) { + unsigned other_start_rank = (color == Color::White) ? 6 : 1; + unsigned en_passant_rank = (color == Color::White) ? 4 : 3; + Move en_passant_last_move = { { other_start_rank, move.to.file }, { en_passant_rank, move.to.file } }; + if (get_piece(move.to).color == opposing_color(color)) { + // Pawn capture. + return true; + } + if (m_last_move.has_value() && move.from.rank == en_passant_rank && m_last_move.value() == en_passant_last_move + && get_piece(en_passant_last_move.to) == Piece(opposing_color(color), Type::Pawn)) { + // En passant. + return true; + } + } + + return false; + } else if (piece.type == Type::Knight) { + int rank_delta = abs(move.to.rank - move.from.rank); + int file_delta = abs(move.to.file - move.from.file); + if (get_piece(move.to).color != color && max(rank_delta, file_delta) == 2 && min(rank_delta, file_delta) == 1) { + return true; + } + } else if (piece.type == Type::Bishop) { + int rank_delta = move.to.rank - move.from.rank; + int file_delta = move.to.file - move.from.file; + if (abs(rank_delta) == abs(file_delta)) { + int dr = rank_delta / abs(rank_delta); + int df = file_delta / abs(file_delta); + for (Square sq = move.from; sq != move.to; sq.rank += dr, sq.file += df) { + if (get_piece(sq).type != Type::None && sq != move.from) { + return false; + } + } + + if (get_piece(move.to).color != color) { + return true; + } + } + } else if (piece.type == Type::Rook) { + int rank_delta = move.to.rank - move.from.rank; + int file_delta = move.to.file - move.from.file; + if (rank_delta == 0 || file_delta == 0) { + int dr = (rank_delta) ? rank_delta / abs(rank_delta) : 0; + int df = (file_delta) ? file_delta / abs(file_delta) : 0; + for (Square sq = move.from; sq != move.to; sq.rank += dr, sq.file += df) { + if (get_piece(sq).type != Type::None && sq != move.from) { + return false; + } + } + + if (get_piece(move.to).color != color) { + return true; + } + } + } else if (piece.type == Type::Queen) { + int rank_delta = move.to.rank - move.from.rank; + int file_delta = move.to.file - move.from.file; + if (abs(rank_delta) == abs(file_delta) || rank_delta == 0 || file_delta == 0) { + int dr = (rank_delta) ? rank_delta / abs(rank_delta) : 0; + int df = (file_delta) ? file_delta / abs(file_delta) : 0; + for (Square sq = move.from; sq != move.to; sq.rank += dr, sq.file += df) { + if (get_piece(sq).type != Type::None && sq != move.from) { + return false; + } + } + + if (get_piece(move.to).color != color) { + return true; + } + } + } else if (piece.type == Type::King) { + int rank_delta = move.to.rank - move.from.rank; + int file_delta = move.to.file - move.from.file; + if (abs(rank_delta) <= 1 && abs(file_delta) <= 1) { + if (get_piece(move.to).color != color) { + return true; + } + } + + if (color == Color::White) { + if ((move.to == Square("a1") || move.to == Square("c1")) && m_white_can_castle_queenside && get_piece(Square("b1")).type == Type::None && get_piece(Square("c1")).type == Type::None && get_piece(Square("d1")).type == Type::None) { + + return true; + } else if ((move.to == Square("h1") || move.to == Square("g1")) && m_white_can_castle_kingside && get_piece(Square("f1")).type == Type::None && get_piece(Square("g1")).type == Type::None) { + + return true; + } + } else { + if ((move.to == Square("a8") || move.to == Square("c8")) && m_black_can_castle_queenside && get_piece(Square("b8")).type == Type::None && get_piece(Square("c8")).type == Type::None && get_piece(Square("d8")).type == Type::None) { + + return true; + } else if ((move.to == Square("h8") || move.to == Square("g8")) && m_black_can_castle_kingside && get_piece(Square("f8")).type == Type::None && get_piece(Square("g8")).type == Type::None) { + + return true; + } + } + } + + return false; +} + +bool Board::in_check(Color color) const +{ + Square king_square = { 50, 50 }; + Square::for_each([&](const Square& square) { + if (get_piece(square) == Piece(color, Type::King)) { + king_square = square; + return IterationDecision::Break; + } + return IterationDecision::Continue; + }); + + bool check = false; + Square::for_each([&](const Square& square) { + if (is_legal_no_check({ square, king_square }, opposing_color(color))) { + check = true; + return IterationDecision::Break; + } + return IterationDecision::Continue; + }); + + return check; +} + +bool Board::apply_move(const Move& move, Color color) +{ + if (color == Color::None) + color = turn(); + + if (!is_legal(move, color)) + return false; + + const_cast<Move&>(move).piece = get_piece(move.from); + + return apply_illegal_move(move, color); +} + +bool Board::apply_illegal_move(const Move& move, Color color) +{ + Board clone = *this; + clone.m_previous_states = {}; + clone.m_moves = {}; + auto state_count = 0; + if (m_previous_states.contains(clone)) + state_count = m_previous_states.get(clone).value(); + + m_previous_states.set(clone, state_count + 1); + m_moves.append(move); + + m_turn = opposing_color(color); + + m_last_move = move; + m_moves_since_capture++; + m_moves_since_pawn_advance++; + + if (move.from == Square("a1") || move.to == Square("a1") || move.from == Square("e1")) + m_white_can_castle_queenside = false; + if (move.from == Square("h1") || move.to == Square("h1") || move.from == Square("e1")) + m_white_can_castle_kingside = false; + if (move.from == Square("a8") || move.to == Square("a8") || move.from == Square("e8")) + m_black_can_castle_queenside = false; + if (move.from == Square("h8") || move.to == Square("h8") || move.from == Square("e8")) + m_black_can_castle_kingside = false; + + if (color == Color::White && move.from == Square("e1") && get_piece(Square("e1")) == Piece(Color::White, Type::King)) { + if (move.to == Square("a1") || move.to == Square("c1")) { + set_piece(Square("e1"), EmptyPiece); + set_piece(Square("a1"), EmptyPiece); + set_piece(Square("c1"), { Color::White, Type::King }); + set_piece(Square("d1"), { Color::White, Type::Rook }); + return true; + } else if (move.to == Square("h1") || move.to == Square("g1")) { + set_piece(Square("e1"), EmptyPiece); + set_piece(Square("h1"), EmptyPiece); + set_piece(Square("g1"), { Color::White, Type::King }); + set_piece(Square("f1"), { Color::White, Type::Rook }); + return true; + } + } else if (color == Color::Black && move.from == Square("e8") && get_piece(Square("e8")) == Piece(Color::Black, Type::King)) { + if (move.to == Square("a8") || move.to == Square("c8")) { + set_piece(Square("e8"), EmptyPiece); + set_piece(Square("a8"), EmptyPiece); + set_piece(Square("c8"), { Color::Black, Type::King }); + set_piece(Square("d8"), { Color::Black, Type::Rook }); + return true; + } else if (move.to == Square("h8") || move.to == Square("g8")) { + set_piece(Square("e8"), EmptyPiece); + set_piece(Square("h8"), EmptyPiece); + set_piece(Square("g8"), { Color::Black, Type::King }); + set_piece(Square("f8"), { Color::Black, Type::Rook }); + return true; + } + } + + if (move.piece.type == Type::Pawn) + m_moves_since_pawn_advance = 0; + + if (get_piece(move.to).color != Color::None) { + const_cast<Move&>(move).is_capture = true; + m_moves_since_capture = 0; + } + + if (get_piece(move.from).type == Type::Pawn && ((color == Color::Black && move.to.rank == 0) || (color == Color::White && move.to.rank == 7))) { + // Pawn Promotion + set_piece(move.to, { color, move.promote_to }); + set_piece(move.from, EmptyPiece); + + if (in_check(m_turn)) + const_cast<Move&>(move).is_check = true; + + return true; + } + + if (get_piece(move.from).type == Type::Pawn && move.from.file != move.to.file && get_piece(move.to).type == Type::None) { + // En passant. + if (color == Color::White) { + set_piece({ move.to.rank - 1, move.to.file }, EmptyPiece); + } else { + set_piece({ move.to.rank + 1, move.to.file }, EmptyPiece); + } + const_cast<Move&>(move).is_capture = true; + m_moves_since_capture = 0; + } + + Square::for_each([&](Square sq) { + // Ambiguous Move + if (sq != move.from && get_piece(sq).type == move.piece.type && get_piece(sq).color == move.piece.color) { + if (is_legal(Move(sq, move.to), get_piece(sq).color)) { + m_moves.last().is_ambiguous = true; + m_moves.last().ambiguous = sq; + + return IterationDecision::Break; + } + } + return IterationDecision::Continue; + }); + + set_piece(move.to, get_piece(move.from)); + set_piece(move.from, EmptyPiece); + + if (in_check(m_turn)) + const_cast<Move&>(move).is_check = true; + + return true; +} + +Move Board::random_move(Color color) const +{ + if (color == Color::None) + color = turn(); + + Move move = { { 50, 50 }, { 50, 50 } }; + int probability = 1; + generate_moves([&](Move m) { + if (rand() % probability == 0) + move = m; + ++probability; + return IterationDecision::Continue; + }); + + return move; +} + +Board::Result Board::game_result() const +{ + if (m_resigned != Color::None) + return (m_resigned == Color::White) ? Result::WhiteResign : Result::BlackResign; + + bool sufficient_material = false; + bool no_more_pieces_allowed = false; + Optional<Square> bishop; + Square::for_each([&](Square sq) { + if (get_piece(sq).type == Type::Queen || get_piece(sq).type == Type::Rook || get_piece(sq).type == Type::Pawn) { + sufficient_material = true; + return IterationDecision::Break; + } + + if (get_piece(sq).type != Type::None && get_piece(sq).type != Type::King && no_more_pieces_allowed) { + sufficient_material = true; + return IterationDecision::Break; + } + + if (get_piece(sq).type == Type::Knight) + no_more_pieces_allowed = true; + + if (get_piece(sq).type == Type::Bishop) { + if (bishop.has_value()) { + if (get_piece(sq).color == get_piece(bishop.value()).color) { + sufficient_material = true; + return IterationDecision::Break; + } else if (sq.is_light() != bishop.value().is_light()) { + sufficient_material = true; + return IterationDecision::Break; + } + no_more_pieces_allowed = true; + } else { + bishop = sq; + } + } + + return IterationDecision::Continue; + }); + + if (!sufficient_material) + return Result::InsufficientMaterial; + + bool are_legal_moves = false; + generate_moves([&]([[maybe_unused]] Move m) { + are_legal_moves = true; + return IterationDecision::Break; + }); + + if (are_legal_moves) { + if (m_moves_since_capture >= 75 * 2) + return Result::SeventyFiveMoveRule; + if (m_moves_since_capture == 50 * 2) + return Result::FiftyMoveRule; + + auto repeats = m_previous_states.get(*this); + if (repeats.has_value()) { + if (repeats.value() == 3) + return Result::ThreeFoldRepetition; + if (repeats.value() >= 5) + return Result::FiveFoldRepetition; + } + + return Result::NotFinished; + } + + if (in_check(turn())) { + const_cast<Vector<Move>&>(m_moves).last().is_mate = true; + return Result::CheckMate; + } + + return Result::StaleMate; +} + +Color Board::game_winner() const +{ + if (game_result() == Result::CheckMate) + return opposing_color(turn()); + + return Color::None; +} + +int Board::game_score() const +{ + switch (game_winner()) { + case Color::White: + return +1; + case Color::Black: + return -1; + case Color::None: + return 0; + } + return 0; +} + +bool Board::game_finished() const +{ + return game_result() != Result::NotFinished; +} + +int Board::material_imbalance() const +{ + int imbalance = 0; + Square::for_each([&](Square square) { + int value = 0; + switch (get_piece(square).type) { + case Type::Pawn: + value = 1; + break; + case Type::Knight: + case Type::Bishop: + value = 3; + break; + case Type::Rook: + value = 5; + break; + case Type::Queen: + value = 9; + break; + default: + break; + } + + if (get_piece(square).color == Color::White) { + imbalance += value; + } else { + imbalance -= value; + } + return IterationDecision::Continue; + }); + return imbalance; +} + +bool Board::is_promotion_move(const Move& move, Color color) const +{ + if (color == Color::None) + color = turn(); + + unsigned promotion_rank = (color == Color::White) ? 7 : 0; + if (move.to.rank != promotion_rank) + return false; + + if (get_piece(move.from).type != Type::Pawn) + return false; + + Move queen_move = move; + queen_move.promote_to = Type::Queen; + if (!is_legal(queen_move, color)) + return false; + + return true; +} + +bool Board::operator==(const Board& other) const +{ + bool equal_squares = true; + Square::for_each([&](Square sq) { + if (get_piece(sq) != other.get_piece(sq)) { + equal_squares = false; + return IterationDecision::Break; + } + return IterationDecision::Continue; + }); + if (!equal_squares) + return false; + + if (m_white_can_castle_queenside != other.m_white_can_castle_queenside) + return false; + if (m_white_can_castle_kingside != other.m_white_can_castle_kingside) + return false; + if (m_black_can_castle_queenside != other.m_black_can_castle_queenside) + return false; + if (m_black_can_castle_kingside != other.m_black_can_castle_kingside) + return false; + + return turn() == other.turn(); +} + +void Board::set_resigned(Chess::Color c) +{ + m_resigned = c; +} + +String Board::result_to_string(Result result, Color turn) +{ + switch (result) { + case Result::CheckMate: + ASSERT(turn != Chess::Color::None); + return turn == Chess::Color::White ? "Black wins by Checkmate" : "White wins by Checkmate"; + case Result::WhiteResign: + return "Black wins by Resignation"; + case Result::BlackResign: + return "White wins by Resignation"; + case Result::StaleMate: + return "Draw by Stalemate"; + case Chess::Board::Result::FiftyMoveRule: + return "Draw by 50 move rule"; + case Chess::Board::Result::SeventyFiveMoveRule: + return "Draw by 75 move rule"; + case Chess::Board::Result::ThreeFoldRepetition: + return "Draw by threefold repetition"; + case Chess::Board::Result::FiveFoldRepetition: + return "Draw by fivefold repetition"; + case Chess::Board::Result::InsufficientMaterial: + return "Draw by insufficient material"; + case Chess::Board::Result::NotFinished: + return "Game not finished"; + default: + ASSERT_NOT_REACHED(); + } +} + +String Board::result_to_points(Result result, Color turn) +{ + switch (result) { + case Result::CheckMate: + ASSERT(turn != Chess::Color::None); + return turn == Chess::Color::White ? "0-1" : "1-0"; + case Result::WhiteResign: + return "0-1"; + case Result::BlackResign: + return "1-0"; + case Result::StaleMate: + return "1/2-1/2"; + case Chess::Board::Result::FiftyMoveRule: + return "1/2-1/2"; + case Chess::Board::Result::SeventyFiveMoveRule: + return "1/2-1/2"; + case Chess::Board::Result::ThreeFoldRepetition: + return "1/2-1/2"; + case Chess::Board::Result::FiveFoldRepetition: + return "1/2-1/2"; + case Chess::Board::Result::InsufficientMaterial: + return "1/2-1/2"; + case Chess::Board::Result::NotFinished: + return "*"; + default: + ASSERT_NOT_REACHED(); + } +} + +} diff --git a/Userland/Libraries/LibChess/Chess.h b/Userland/Libraries/LibChess/Chess.h new file mode 100644 index 0000000000..798a6cc716 --- /dev/null +++ b/Userland/Libraries/LibChess/Chess.h @@ -0,0 +1,324 @@ +/* + * Copyright (c) 2020, the SerenityOS developers. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <AK/HashMap.h> +#include <AK/IterationDecision.h> +#include <AK/Optional.h> +#include <AK/StringView.h> +#include <AK/Traits.h> +#include <AK/Vector.h> + +namespace Chess { + +enum class Type { + Pawn, + Knight, + Bishop, + Rook, + Queen, + King, + None, +}; + +String char_for_piece(Type type); +Chess::Type piece_for_char_promotion(const StringView& str); + +enum class Color { + White, + Black, + None, +}; + +Color opposing_color(Color color); + +struct Piece { + constexpr Piece() + : color(Color::None) + , type(Type::None) + { + } + constexpr Piece(Color c, Type t) + : color(c) + , type(t) + { + } + Color color : 4; + Type type : 4; + bool operator==(const Piece& other) const { return color == other.color && type == other.type; } +}; + +constexpr Piece EmptyPiece = { Color::None, Type::None }; + +struct Square { + unsigned rank; // zero indexed; + unsigned file; + Square(const StringView& name); + Square(const unsigned& rank, const unsigned& file) + : rank(rank) + , file(file) + { + } + bool operator==(const Square& other) const { return rank == other.rank && file == other.file; } + + template<typename Callback> + static void for_each(Callback callback) + { + for (int rank = 0; rank < 8; ++rank) { + for (int file = 0; file < 8; ++file) { + if (callback(Square(rank, file)) == IterationDecision::Break) + return; + } + } + } + + bool in_bounds() const { return rank < 8 && file < 8; } + bool is_light() const { return (rank % 2) != (file % 2); } + String to_algebraic() const; +}; + +class Board; + +struct Move { + Square from; + Square to; + Type promote_to; + Piece piece; + bool is_check = false; + bool is_mate = false; + bool is_capture = false; + bool is_ambiguous = false; + Square ambiguous { 50, 50 }; + Move(const StringView& long_algebraic); + Move(const Square& from, const Square& to, const Type& promote_to = Type::None) + : from(from) + , to(to) + , promote_to(promote_to) + { + } + bool operator==(const Move& other) const { return from == other.from && to == other.to && promote_to == other.promote_to; } + + static Move from_algebraic(const StringView& algebraic, const Color turn, const Board& board); + String to_long_algebraic() const; + String to_algebraic() const; +}; + +class Board { +public: + Board(); + + Piece get_piece(const Square&) const; + Piece set_piece(const Square&, const Piece&); + + bool is_legal(const Move&, Color color = Color::None) const; + bool in_check(Color color) const; + + bool is_promotion_move(const Move&, Color color = Color::None) const; + + bool apply_move(const Move&, Color color = Color::None); + const Optional<Move>& last_move() const { return m_last_move; } + + String to_fen() const; + + enum class Result { + CheckMate, + StaleMate, + WhiteResign, + BlackResign, + FiftyMoveRule, + SeventyFiveMoveRule, + ThreeFoldRepetition, + FiveFoldRepetition, + InsufficientMaterial, + NotFinished, + }; + + static String result_to_string(Result, Color turn); + static String result_to_points(Result, Color turn); + + template<typename Callback> + void generate_moves(Callback callback, Color color = Color::None) const; + Move random_move(Color color = Color::None) const; + Result game_result() const; + Color game_winner() const; + int game_score() const; + bool game_finished() const; + void set_resigned(Color); + int material_imbalance() const; + + Color turn() const { return m_turn; } + const Vector<Move>& moves() const { return m_moves; } + + bool operator==(const Board& other) const; + +private: + bool is_legal_no_check(const Move&, Color color) const; + bool is_legal_promotion(const Move&, Color color) const; + bool apply_illegal_move(const Move&, Color color); + + Piece m_board[8][8]; + Color m_turn { Color::White }; + Color m_resigned { Color::None }; + Optional<Move> m_last_move; + int m_moves_since_capture { 0 }; + int m_moves_since_pawn_advance { 0 }; + + bool m_white_can_castle_kingside { true }; + bool m_white_can_castle_queenside { true }; + bool m_black_can_castle_kingside { true }; + bool m_black_can_castle_queenside { true }; + + HashMap<Board, int> m_previous_states; + Vector<Move> m_moves; + friend struct AK::Traits<Board>; +}; + +template<typename Callback> +void Board::generate_moves(Callback callback, Color color) const +{ + if (color == Color::None) + color = turn(); + + auto try_move = [&](Move m) { + if (is_legal(m, color)) { + if (callback(m) == IterationDecision::Break) + return false; + } + return true; + }; + + Square::for_each([&](Square sq) { + auto piece = get_piece(sq); + if (piece.color != color) + return IterationDecision::Continue; + + bool keep_going = true; + if (piece.type == Type::Pawn) { + for (auto& piece : Vector({ Type::None, Type::Knight, Type::Bishop, Type::Rook, Type::Queen })) { + keep_going = try_move({ sq, { sq.rank + 1, sq.file }, piece }) + && try_move({ sq, { sq.rank + 2, sq.file }, piece }) + && try_move({ sq, { sq.rank - 1, sq.file }, piece }) + && try_move({ sq, { sq.rank - 2, sq.file }, piece }) + && try_move({ sq, { sq.rank + 1, sq.file + 1 }, piece }) + && try_move({ sq, { sq.rank + 1, sq.file - 1 }, piece }) + && try_move({ sq, { sq.rank - 1, sq.file + 1 }, piece }) + && try_move({ sq, { sq.rank - 1, sq.file - 1 }, piece }); + } + } else if (piece.type == Type::Knight) { + keep_going = try_move({ sq, { sq.rank + 2, sq.file + 1 } }) + && try_move({ sq, { sq.rank + 2, sq.file - 1 } }) + && try_move({ sq, { sq.rank + 1, sq.file + 2 } }) + && try_move({ sq, { sq.rank + 1, sq.file - 2 } }) + && try_move({ sq, { sq.rank - 2, sq.file + 1 } }) + && try_move({ sq, { sq.rank - 2, sq.file - 1 } }) + && try_move({ sq, { sq.rank - 1, sq.file + 2 } }) + && try_move({ sq, { sq.rank - 1, sq.file - 2 } }); + } else if (piece.type == Type::Bishop) { + for (int dr = -1; dr <= 1; dr += 2) { + for (int df = -1; df <= 1; df += 2) { + for (Square to = sq; to.in_bounds(); to = { to.rank + dr, to.file + df }) { + if (!try_move({ sq, to })) + return IterationDecision::Break; + } + } + } + } else if (piece.type == Type::Rook) { + for (int dr = -1; dr <= 1; dr++) { + for (int df = -1; df <= 1; df++) { + if ((dr == 0) != (df == 0)) { + for (Square to = sq; to.in_bounds(); to = { to.rank + dr, to.file + df }) { + if (!try_move({ sq, to })) + return IterationDecision::Break; + } + } + } + } + } else if (piece.type == Type::Queen) { + for (int dr = -1; dr <= 1; dr++) { + for (int df = -1; df <= 1; df++) { + if (dr != 0 || df != 0) { + for (Square to = sq; to.in_bounds(); to = { to.rank + dr, to.file + df }) { + if (!try_move({ sq, to })) + return IterationDecision::Break; + } + } + } + } + } else if (piece.type == Type::King) { + for (int dr = -1; dr <= 1; dr++) { + for (int df = -1; df <= 1; df++) { + if (!try_move({ sq, { sq.rank + dr, sq.file + df } })) + return IterationDecision::Break; + } + } + + // Castling moves. + if (sq == Square("e1")) { + keep_going = try_move({ sq, Square("c1") }) && try_move({ sq, Square("g1") }); + } else if (sq == Square("e8")) { + keep_going = try_move({ sq, Square("c8") }) && try_move({ sq, Square("g8") }); + } + } + + if (keep_going) { + return IterationDecision::Continue; + } else { + return IterationDecision::Break; + } + }); +} + +} + +template<> +struct AK::Traits<Chess::Piece> : public GenericTraits<Chess::Piece> { + static unsigned hash(Chess::Piece piece) + { + return pair_int_hash(static_cast<u32>(piece.color), static_cast<u32>(piece.type)); + } +}; + +template<> +struct AK::Traits<Chess::Board> : public GenericTraits<Chess::Board> { + static unsigned hash(Chess::Board chess) + { + unsigned hash = 0; + hash = pair_int_hash(hash, static_cast<u32>(chess.m_white_can_castle_queenside)); + hash = pair_int_hash(hash, static_cast<u32>(chess.m_white_can_castle_kingside)); + hash = pair_int_hash(hash, static_cast<u32>(chess.m_black_can_castle_queenside)); + hash = pair_int_hash(hash, static_cast<u32>(chess.m_black_can_castle_kingside)); + + hash = pair_int_hash(hash, static_cast<u32>(chess.m_black_can_castle_kingside)); + + Chess::Square::for_each([&](Chess::Square sq) { + hash = pair_int_hash(hash, Traits<Chess::Piece>::hash(chess.get_piece(sq))); + return IterationDecision::Continue; + }); + + return hash; + } +}; diff --git a/Userland/Libraries/LibChess/UCICommand.cpp b/Userland/Libraries/LibChess/UCICommand.cpp new file mode 100644 index 0000000000..3676c52efe --- /dev/null +++ b/Userland/Libraries/LibChess/UCICommand.cpp @@ -0,0 +1,332 @@ +/* + * Copyright (c) 2020, the SerenityOS developers. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "UCICommand.h" +#include <AK/StringBuilder.h> + +namespace Chess::UCI { + +UCICommand UCICommand::from_string(const StringView& command) +{ + auto tokens = command.split_view(' '); + ASSERT(tokens[0] == "uci"); + ASSERT(tokens.size() == 1); + return UCICommand(); +} + +String UCICommand::to_string() const +{ + return "uci\n"; +} + +DebugCommand DebugCommand::from_string(const StringView& command) +{ + auto tokens = command.split_view(' '); + ASSERT(tokens[0] == "debug"); + ASSERT(tokens.size() == 2); + if (tokens[1] == "on") + return DebugCommand(Flag::On); + if (tokens[1] == "off") + return DebugCommand(Flag::On); + + ASSERT_NOT_REACHED(); +} + +String DebugCommand::to_string() const +{ + if (flag() == Flag::On) { + return "debug on\n"; + } else { + return "debug off\n"; + } +} + +IsReadyCommand IsReadyCommand::from_string(const StringView& command) +{ + auto tokens = command.split_view(' '); + ASSERT(tokens[0] == "isready"); + ASSERT(tokens.size() == 1); + return IsReadyCommand(); +} + +String IsReadyCommand::to_string() const +{ + return "isready\n"; +} + +SetOptionCommand SetOptionCommand::from_string(const StringView& command) +{ + auto tokens = command.split_view(' '); + ASSERT(tokens[0] == "setoption"); + ASSERT(tokens[1] == "name"); + if (tokens.size() == 3) { + return SetOptionCommand(tokens[1]); + } else if (tokens.size() == 4) { + ASSERT(tokens[2] == "value"); + return SetOptionCommand(tokens[1], tokens[3]); + } + ASSERT_NOT_REACHED(); +} + +String SetOptionCommand::to_string() const +{ + StringBuilder builder; + builder.append("setoption name "); + builder.append(name()); + if (value().has_value()) { + builder.append(" value "); + builder.append(value().value()); + } + builder.append('\n'); + return builder.build(); +} + +PositionCommand PositionCommand::from_string(const StringView& command) +{ + auto tokens = command.split_view(' '); + ASSERT(tokens.size() >= 3); + ASSERT(tokens[0] == "position"); + ASSERT(tokens[2] == "moves"); + + Optional<String> fen; + if (tokens[1] != "startpos") + fen = tokens[1]; + + Vector<Move> moves; + for (size_t i = 3; i < tokens.size(); ++i) { + moves.append(Move(tokens[i])); + } + return PositionCommand(fen, moves); +} + +String PositionCommand::to_string() const +{ + StringBuilder builder; + builder.append("position "); + if (fen().has_value()) { + builder.append(fen().value()); + } else { + builder.append("startpos "); + } + builder.append("moves"); + for (auto& move : moves()) { + builder.append(' '); + builder.append(move.to_long_algebraic()); + } + builder.append('\n'); + return builder.build(); +} + +GoCommand GoCommand::from_string(const StringView& command) +{ + auto tokens = command.split_view(' '); + ASSERT(tokens[0] == "go"); + + GoCommand go_command; + for (size_t i = 1; i < tokens.size(); ++i) { + if (tokens[i] == "searchmoves") { + ASSERT_NOT_REACHED(); + } else if (tokens[i] == "ponder") { + go_command.ponder = true; + } else if (tokens[i] == "wtime") { + ASSERT(i++ < tokens.size()); + go_command.wtime = tokens[i].to_int().value(); + } else if (tokens[i] == "btime") { + ASSERT(i++ < tokens.size()); + go_command.btime = tokens[i].to_int().value(); + } else if (tokens[i] == "winc") { + ASSERT(i++ < tokens.size()); + go_command.winc = tokens[i].to_int().value(); + } else if (tokens[i] == "binc") { + ASSERT(i++ < tokens.size()); + go_command.binc = tokens[i].to_int().value(); + } else if (tokens[i] == "movestogo") { + ASSERT(i++ < tokens.size()); + go_command.movestogo = tokens[i].to_int().value(); + } else if (tokens[i] == "depth") { + ASSERT(i++ < tokens.size()); + go_command.depth = tokens[i].to_int().value(); + } else if (tokens[i] == "nodes") { + ASSERT(i++ < tokens.size()); + go_command.nodes = tokens[i].to_int().value(); + } else if (tokens[i] == "mate") { + ASSERT(i++ < tokens.size()); + go_command.mate = tokens[i].to_int().value(); + } else if (tokens[i] == "movetime") { + ASSERT(i++ < tokens.size()); + go_command.movetime = tokens[i].to_int().value(); + } else if (tokens[i] == "infinite") { + go_command.infinite = true; + } + } + + return go_command; +} + +String GoCommand::to_string() const +{ + StringBuilder builder; + builder.append("go"); + + if (searchmoves.has_value()) { + builder.append(" searchmoves"); + for (auto& move : searchmoves.value()) { + builder.append(' '); + builder.append(move.to_long_algebraic()); + } + } + + if (ponder) + builder.append(" ponder"); + if (wtime.has_value()) + builder.appendff(" wtime {}", wtime.value()); + if (btime.has_value()) + builder.appendff(" btime {}", btime.value()); + if (winc.has_value()) + builder.appendff(" winc {}", winc.value()); + if (binc.has_value()) + builder.appendff(" binc {}", binc.value()); + if (movestogo.has_value()) + builder.appendff(" movestogo {}", movestogo.value()); + if (depth.has_value()) + builder.appendff(" depth {}", depth.value()); + if (nodes.has_value()) + builder.appendff(" nodes {}", nodes.value()); + if (mate.has_value()) + builder.appendff(" mate {}", mate.value()); + if (movetime.has_value()) + builder.appendff(" movetime {}", movetime.value()); + if (infinite) + builder.append(" infinite"); + + builder.append('\n'); + return builder.build(); +} + +StopCommand StopCommand::from_string(const StringView& command) +{ + auto tokens = command.split_view(' '); + ASSERT(tokens[0] == "stop"); + ASSERT(tokens.size() == 1); + return StopCommand(); +} + +String StopCommand::to_string() const +{ + return "stop\n"; +} + +IdCommand IdCommand::from_string(const StringView& command) +{ + auto tokens = command.split_view(' '); + ASSERT(tokens[0] == "id"); + StringBuilder value; + for (size_t i = 2; i < tokens.size(); ++i) { + if (i != 2) + value.append(' '); + + value.append(tokens[i]); + } + + if (tokens[1] == "name") { + return IdCommand(Type::Name, value.build()); + } else if (tokens[1] == "author") { + return IdCommand(Type::Author, value.build()); + } + ASSERT_NOT_REACHED(); +} + +String IdCommand::to_string() const +{ + StringBuilder builder; + builder.append("id "); + if (field_type() == Type::Name) { + builder.append("name "); + } else { + builder.append("author "); + } + builder.append(value()); + builder.append('\n'); + return builder.build(); +} + +UCIOkCommand UCIOkCommand::from_string(const StringView& command) +{ + auto tokens = command.split_view(' '); + ASSERT(tokens[0] == "uciok"); + ASSERT(tokens.size() == 1); + return UCIOkCommand(); +} + +String UCIOkCommand::to_string() const +{ + return "uciok\n"; +} + +ReadyOkCommand ReadyOkCommand::from_string(const StringView& command) +{ + auto tokens = command.split_view(' '); + ASSERT(tokens[0] == "readyok"); + ASSERT(tokens.size() == 1); + return ReadyOkCommand(); +} + +String ReadyOkCommand::to_string() const +{ + return "readyok\n"; +} + +BestMoveCommand BestMoveCommand::from_string(const StringView& command) +{ + auto tokens = command.split_view(' '); + ASSERT(tokens[0] == "bestmove"); + ASSERT(tokens.size() == 2); + return BestMoveCommand(Move(tokens[1])); +} + +String BestMoveCommand::to_string() const +{ + StringBuilder builder; + builder.append("bestmove "); + builder.append(move().to_long_algebraic()); + builder.append('\n'); + return builder.build(); +} + +InfoCommand InfoCommand::from_string([[maybe_unused]] const StringView& command) +{ + // FIXME: Implement this. + ASSERT_NOT_REACHED(); +} + +String InfoCommand::to_string() const +{ + // FIXME: Implement this. + ASSERT_NOT_REACHED(); + return "info"; +} + +} diff --git a/Userland/Libraries/LibChess/UCICommand.h b/Userland/Libraries/LibChess/UCICommand.h new file mode 100644 index 0000000000..cd79751d98 --- /dev/null +++ b/Userland/Libraries/LibChess/UCICommand.h @@ -0,0 +1,291 @@ +/* + * Copyright (c) 2020, the SerenityOS developers. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <AK/Optional.h> +#include <AK/String.h> +#include <LibChess/Chess.h> +#include <LibCore/Event.h> + +namespace Chess::UCI { + +class Command : public Core::Event { +public: + enum Type { + // GUI to engine commands. + UCI = 12000, + Debug, + IsReady, + SetOption, + Register, + UCINewGame, + Position, + Go, + Stop, + PonderHit, + Quit, + // Engine to GUI commands. + Id, + UCIOk, + ReadyOk, + BestMove, + CopyProtection, + Registration, + Info, + Option, + }; + + explicit Command(Type type) + : Core::Event(type) + { + } + + virtual String to_string() const = 0; + + virtual ~Command() { } +}; + +class UCICommand : public Command { +public: + explicit UCICommand() + : Command(Command::Type::UCI) + { + } + + static UCICommand from_string(const StringView& command); + + virtual String to_string() const; +}; + +class DebugCommand : public Command { +public: + enum class Flag { + On, + Off + }; + + explicit DebugCommand(Flag flag) + : Command(Command::Type::Debug) + , m_flag(flag) + { + } + + static DebugCommand from_string(const StringView& command); + + virtual String to_string() const; + + Flag flag() const { return m_flag; } + +private: + Flag m_flag; +}; + +class IsReadyCommand : public Command { +public: + explicit IsReadyCommand() + : Command(Command::Type::IsReady) + { + } + + static IsReadyCommand from_string(const StringView& command); + + virtual String to_string() const; +}; + +class SetOptionCommand : public Command { +public: + explicit SetOptionCommand(const StringView& name, Optional<String> value = {}) + : Command(Command::Type::SetOption) + , m_name(name) + , m_value(value) + { + } + + static SetOptionCommand from_string(const StringView& command); + + virtual String to_string() const; + + const String& name() const { return m_name; } + const Optional<String>& value() const { return m_value; } + +private: + String m_name; + Optional<String> m_value; +}; + +class PositionCommand : public Command { +public: + explicit PositionCommand(const Optional<String>& fen, const Vector<Chess::Move>& moves) + : Command(Command::Type::Position) + , m_fen(fen) + , m_moves(moves) + { + } + + static PositionCommand from_string(const StringView& command); + + virtual String to_string() const; + + const Optional<String>& fen() const { return m_fen; } + const Vector<Chess::Move>& moves() const { return m_moves; } + +private: + Optional<String> m_fen; + Vector<Chess::Move> m_moves; +}; + +class GoCommand : public Command { +public: + explicit GoCommand() + : Command(Command::Type::Go) + { + } + + static GoCommand from_string(const StringView& command); + + virtual String to_string() const; + + Optional<Vector<Chess::Move>> searchmoves; + bool ponder { false }; + Optional<int> wtime; + Optional<int> btime; + Optional<int> winc; + Optional<int> binc; + Optional<int> movestogo; + Optional<int> depth; + Optional<int> nodes; + Optional<int> mate; + Optional<int> movetime; + bool infinite { false }; +}; + +class StopCommand : public Command { +public: + explicit StopCommand() + : Command(Command::Type::Stop) + { + } + + static StopCommand from_string(const StringView& command); + + virtual String to_string() const; +}; + +class IdCommand : public Command { +public: + enum class Type { + Name, + Author, + }; + + explicit IdCommand(Type field_type, const StringView& value) + : Command(Command::Type::Id) + , m_field_type(field_type) + , m_value(value) + { + } + + static IdCommand from_string(const StringView& command); + + virtual String to_string() const; + + Type field_type() const { return m_field_type; } + const String& value() const { return m_value; } + +private: + Type m_field_type; + String m_value; +}; + +class UCIOkCommand : public Command { +public: + explicit UCIOkCommand() + : Command(Command::Type::UCIOk) + { + } + + static UCIOkCommand from_string(const StringView& command); + + virtual String to_string() const; +}; + +class ReadyOkCommand : public Command { +public: + explicit ReadyOkCommand() + : Command(Command::Type::ReadyOk) + { + } + + static ReadyOkCommand from_string(const StringView& command); + + virtual String to_string() const; +}; + +class BestMoveCommand : public Command { +public: + explicit BestMoveCommand(const Chess::Move& move) + : Command(Command::Type::BestMove) + , m_move(move) + { + } + + static BestMoveCommand from_string(const StringView& command); + + virtual String to_string() const; + + Chess::Move move() const { return m_move; } + +private: + Chess::Move m_move; +}; + +class InfoCommand : public Command { +public: + explicit InfoCommand() + : Command(Command::Type::BestMove) + { + } + + static InfoCommand from_string(const StringView& command); + + virtual String to_string() const; + + Optional<int> depth; + Optional<int> seldepth; + Optional<int> time; + Optional<int> nodes; + Optional<Vector<Chess::Move>> pv; + // FIXME: Add multipv. + Optional<int> score_cp; + Optional<int> score_mate; + // FIXME: Add score bounds. + Optional<Chess::Move> currmove; + Optional<int> currmove_number; + // FIXME: Add additional fields. +}; + +} diff --git a/Userland/Libraries/LibChess/UCIEndpoint.cpp b/Userland/Libraries/LibChess/UCIEndpoint.cpp new file mode 100644 index 0000000000..1b7fc07f54 --- /dev/null +++ b/Userland/Libraries/LibChess/UCIEndpoint.cpp @@ -0,0 +1,132 @@ +/* + * Copyright (c) 2020, the SerenityOS developers. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "UCIEndpoint.h" +#include <AK/ByteBuffer.h> +#include <AK/String.h> +#include <LibCore/EventLoop.h> +#include <LibCore/File.h> + +// #define UCI_DEBUG + +namespace Chess::UCI { + +Endpoint::Endpoint(NonnullRefPtr<Core::IODevice> in, NonnullRefPtr<Core::IODevice> out) + : m_in(in) + , m_out(out) + , m_in_notifier(Core::Notifier::construct(in->fd(), Core::Notifier::Read)) +{ + set_in_notifier(); +} + +void Endpoint::send_command(const Command& command) +{ +#ifdef UCI_DEBUG + dbgln("{} Sent UCI Command: {}", class_name(), String(command.to_string().characters(), Chomp)); +#endif + m_out->write(command.to_string()); +} + +void Endpoint::event(Core::Event& event) +{ + switch (event.type()) { + case Command::Type::UCI: + return handle_uci(); + case Command::Type::Debug: + return handle_debug(static_cast<const DebugCommand&>(event)); + case Command::Type::IsReady: + return handle_uci(); + case Command::Type::SetOption: + return handle_setoption(static_cast<const SetOptionCommand&>(event)); + case Command::Type::Position: + return handle_position(static_cast<const PositionCommand&>(event)); + case Command::Type::Go: + return handle_go(static_cast<const GoCommand&>(event)); + case Command::Type::Stop: + return handle_stop(); + case Command::Type::Id: + return handle_id(static_cast<const IdCommand&>(event)); + case Command::Type::UCIOk: + return handle_uciok(); + case Command::Type::ReadyOk: + return handle_readyok(); + case Command::Type::BestMove: + return handle_bestmove(static_cast<const BestMoveCommand&>(event)); + case Command::Type::Info: + return handle_info(static_cast<const InfoCommand&>(event)); + default: + break; + } +} + +void Endpoint::set_in_notifier() +{ + m_in_notifier = Core::Notifier::construct(m_in->fd(), Core::Notifier::Read); + m_in_notifier->on_ready_to_read = [this] { + while (m_in->can_read_line()) + Core::EventLoop::current().post_event(*this, read_command()); + }; +} + +NonnullOwnPtr<Command> Endpoint::read_command() +{ + String line(ReadonlyBytes(m_in->read_line(4096).bytes()), Chomp); + +#ifdef UCI_DEBUG + dbgln("{} Received UCI Command: {}", class_name(), line); +#endif + + if (line == "uci") { + return make<UCICommand>(UCICommand::from_string(line)); + } else if (line.starts_with("debug")) { + return make<DebugCommand>(DebugCommand::from_string(line)); + } else if (line.starts_with("isready")) { + return make<IsReadyCommand>(IsReadyCommand::from_string(line)); + } else if (line.starts_with("setoption")) { + return make<SetOptionCommand>(SetOptionCommand::from_string(line)); + } else if (line.starts_with("position")) { + return make<PositionCommand>(PositionCommand::from_string(line)); + } else if (line.starts_with("go")) { + return make<GoCommand>(GoCommand::from_string(line)); + } else if (line.starts_with("stop")) { + return make<StopCommand>(StopCommand::from_string(line)); + } else if (line.starts_with("id")) { + return make<IdCommand>(IdCommand::from_string(line)); + } else if (line.starts_with("uciok")) { + return make<UCIOkCommand>(UCIOkCommand::from_string(line)); + } else if (line.starts_with("readyok")) { + return make<ReadyOkCommand>(ReadyOkCommand::from_string(line)); + } else if (line.starts_with("bestmove")) { + return make<BestMoveCommand>(BestMoveCommand::from_string(line)); + } else if (line.starts_with("info")) { + return make<InfoCommand>(InfoCommand::from_string(line)); + } + + dbgln("command line: {}", line); + ASSERT_NOT_REACHED(); +} + +}; diff --git a/Userland/Libraries/LibChess/UCIEndpoint.h b/Userland/Libraries/LibChess/UCIEndpoint.h new file mode 100644 index 0000000000..0913c12b35 --- /dev/null +++ b/Userland/Libraries/LibChess/UCIEndpoint.h @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2020, the SerenityOS developers. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <LibChess/UCICommand.h> +#include <LibCore/IODevice.h> +#include <LibCore/Notifier.h> +#include <LibCore/Object.h> + +namespace Chess::UCI { + +class Endpoint : public Core::Object { + C_OBJECT(Endpoint) +public: + virtual ~Endpoint() override { } + + Endpoint() { } + Endpoint(NonnullRefPtr<Core::IODevice> in, NonnullRefPtr<Core::IODevice> out); + + virtual void handle_uci() { } + virtual void handle_debug(const DebugCommand&) { } + virtual void handle_isready() { } + virtual void handle_setoption(const SetOptionCommand&) { } + virtual void handle_position(const PositionCommand&) { } + virtual void handle_go(const GoCommand&) { } + virtual void handle_stop() { } + virtual void handle_id(const IdCommand&) { } + virtual void handle_uciok() { } + virtual void handle_readyok() { } + virtual void handle_bestmove(const BestMoveCommand&) { } + virtual void handle_info(const InfoCommand&) { } + + void send_command(const Command&); + + virtual void event(Core::Event&); + + Core::IODevice& in() { return *m_in; } + Core::IODevice& out() { return *m_out; } + + void set_in(RefPtr<Core::IODevice> in) + { + m_in = in; + set_in_notifier(); + } + void set_out(RefPtr<Core::IODevice> out) { m_out = out; } + +private: + void set_in_notifier(); + NonnullOwnPtr<Command> read_command(); + + RefPtr<Core::IODevice> m_in; + RefPtr<Core::IODevice> m_out; + RefPtr<Core::Notifier> m_in_notifier; +}; + +} |