diff --git a/Applications/CMakeLists.txt b/Applications/CMakeLists.txt index 7de20e579d1..e8fa7ff0237 100644 --- a/Applications/CMakeLists.txt +++ b/Applications/CMakeLists.txt @@ -2,6 +2,7 @@ add_subdirectory(About) add_subdirectory(Browser) add_subdirectory(Calculator) add_subdirectory(Calendar) +add_subdirectory(ChessEngine) add_subdirectory(Debugger) add_subdirectory(DisplaySettings) add_subdirectory(FileManager) diff --git a/Applications/ChessEngine/CMakeLists.txt b/Applications/ChessEngine/CMakeLists.txt new file mode 100644 index 00000000000..288529a100a --- /dev/null +++ b/Applications/ChessEngine/CMakeLists.txt @@ -0,0 +1,8 @@ +set(SOURCES + ChessEngine.cpp + main.cpp + MCTSTree.cpp +) + +serenity_bin(ChessEngine) +target_link_libraries(ChessEngine LibChess LibCore) diff --git a/Applications/ChessEngine/ChessEngine.cpp b/Applications/ChessEngine/ChessEngine.cpp new file mode 100644 index 00000000000..16394d443c9 --- /dev/null +++ b/Applications/ChessEngine/ChessEngine.cpp @@ -0,0 +1,76 @@ +/* + * 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 "ChessEngine.h" +#include "MCTSTree.h" +#include + +using namespace Chess::UCI; + +void ChessEngine::handle_uci() +{ + send_command(IdCommand(IdCommand::Type::Name, "ChessEngine")); + send_command(IdCommand(IdCommand::Type::Author, "the SerenityOS developers")); + send_command(UCIOkCommand()); +} + +void ChessEngine::handle_position(const PositionCommand& command) +{ + // FIXME: Implement fen board position. + ASSERT(!command.fen().has_value()); + m_board = Chess::Board(); + for (auto& move : command.moves()) { + ASSERT(m_board.apply_move(move)); + } +} + +void ChessEngine::handle_go(const GoCommand& command) +{ + // FIXME: A better algorithm than naive mcts. + // FIXME: Add different ways to terminate search. + ASSERT(command.movetime.has_value()); + + srand(arc4random()); + + Core::ElapsedTimer elapsed_time; + elapsed_time.start(); + + MCTSTree mcts(m_board); + + // FIXME: optimize simulations enough for use. + mcts.set_eval_method(MCTSTree::EvalMethod::Heuristic); + + int rounds = 0; + while (elapsed_time.elapsed() <= command.movetime.value()) { + mcts.do_round(); + ++rounds; + } + dbg() << "MCTS finished " << rounds << " rounds."; + dbg() << "MCTS evaluation " << mcts.expected_value(); + auto best_move = mcts.best_move(); + dbg() << "MCTS best move " << best_move.to_long_algebraic(); + send_command(BestMoveCommand(best_move)); +} diff --git a/Applications/ChessEngine/ChessEngine.h b/Applications/ChessEngine/ChessEngine.h new file mode 100644 index 00000000000..54c0cb7c8af --- /dev/null +++ b/Applications/ChessEngine/ChessEngine.h @@ -0,0 +1,49 @@ +/* + * 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 +#include + +class ChessEngine : public Chess::UCI::Endpoint { + C_OBJECT(ChessEngine) +public: + virtual ~ChessEngine() override { } + + ChessEngine() { } + ChessEngine(NonnullRefPtr in, NonnullRefPtr out) + : Endpoint(in, out) + { + } + + virtual void handle_uci(); + virtual void handle_position(const Chess::UCI::PositionCommand&); + virtual void handle_go(const Chess::UCI::GoCommand&); + +private: + Chess::Board m_board; +}; diff --git a/Applications/ChessEngine/MCTSTree.cpp b/Applications/ChessEngine/MCTSTree.cpp new file mode 100644 index 00000000000..1724f7acd84 --- /dev/null +++ b/Applications/ChessEngine/MCTSTree.cpp @@ -0,0 +1,180 @@ +/* + * 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 "MCTSTree.h" +#include +#include + +MCTSTree::MCTSTree(const Chess::Board& board, double exploration_parameter, MCTSTree* parent) + : m_parent(parent) + , m_exploration_parameter(exploration_parameter) + , m_board(board) +{ + if (m_parent) + m_eval_method = m_parent->eval_method(); +} + +MCTSTree& MCTSTree::select_leaf() +{ + if (!expanded() || m_children.size() == 0) + return *this; + + MCTSTree* node = nullptr; + double max_uct = -double(INFINITY); + for (auto& child : m_children) { + double uct = child.uct(m_board.turn()); + if (uct >= max_uct) { + max_uct = uct; + node = &child; + } + } + ASSERT(node); + return node->select_leaf(); +} + +MCTSTree& MCTSTree::expand() +{ + ASSERT(!expanded() || m_children.size() == 0); + + if (!m_moves_generated) { + m_board.generate_moves([&](Chess::Move move) { + Chess::Board clone = m_board; + clone.apply_move(move); + m_children.append(make(clone, m_exploration_parameter, this)); + return IterationDecision::Continue; + }); + m_moves_generated = true; + } + + if (m_children.size() == 0) { + return *this; + } + + for (auto& child : m_children) { + if (child.m_simulations == 0) { + return child; + } + } + ASSERT_NOT_REACHED(); +} + +int MCTSTree::simulate_game() const +{ + ASSERT_NOT_REACHED(); + Chess::Board clone = m_board; + while (!clone.game_finished()) { + clone.apply_move(clone.random_move()); + } + return clone.game_score(); +} + +int MCTSTree::heuristic() const +{ + if (m_board.game_finished()) + return m_board.game_score(); + + double winchance = max(min(double(m_board.material_imbalance()) / 6, 1.0), -1.0); + + double random = double(rand()) / RAND_MAX; + if (winchance >= random) + return 1; + if (winchance <= -random) + return -1; + + return 0; +} + +void MCTSTree::apply_result(int game_score) +{ + m_simulations++; + m_white_points += game_score; + + if (m_parent) + m_parent->apply_result(game_score); +} + +void MCTSTree::do_round() +{ + auto& node = select_leaf().expand(); + + int result; + if (m_eval_method == EvalMethod::Simulation) { + result = node.simulate_game(); + } else { + result = node.heuristic(); + } + node.apply_result(result); +} + +Chess::Move MCTSTree::best_move() const +{ + int score_multiplier = (m_board.turn() == Chess::Colour::White) ? 1 : -1; + + Chess::Move best_move = { { 0, 0 }, { 0, 0 } }; + double best_score = -double(INFINITY); + ASSERT(m_children.size()); + for (auto& node : m_children) { + double node_score = node.expected_value() * score_multiplier; + if (node_score >= best_score) { + // The best move is the last move made in the child. + best_move = node.m_board.moves()[node.m_board.moves().size() - 1]; + best_score = node_score; + } + } + + return best_move; +} + +double MCTSTree::expected_value() const +{ + if (m_simulations == 0) + return 0; + + return double(m_white_points) / m_simulations; +} + +double MCTSTree::uct(Chess::Colour colour) const +{ + // UCT: Upper Confidence Bound Applied to Trees. + // Kocsis, Levente; Szepesvári, Csaba (2006). "Bandit based Monte-Carlo Planning" + + // Fun fact: Szepesvári was my data structures professor. + double expected = expected_value() * ((colour == Chess::Colour::White) ? 1 : -1); + return expected + m_exploration_parameter * sqrt(log(m_parent->m_simulations) / m_simulations); +} + +bool MCTSTree::expanded() const +{ + if (!m_moves_generated) + return false; + + for (auto& child : m_children) { + if (child.m_simulations == 0) + return false; + } + + return true; +} diff --git a/Applications/ChessEngine/MCTSTree.h b/Applications/ChessEngine/MCTSTree.h new file mode 100644 index 00000000000..857a4c41107 --- /dev/null +++ b/Applications/ChessEngine/MCTSTree.h @@ -0,0 +1,67 @@ +/* + * 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 +#include +#include +#include + +class MCTSTree { +public: + enum EvalMethod { + Simulation, + Heuristic, + }; + + MCTSTree(const Chess::Board& board, double exploration_parameter = sqrt(2), MCTSTree* parent = nullptr); + + MCTSTree& select_leaf(); + MCTSTree& expand(); + int simulate_game() const; + int heuristic() const; + void apply_result(int game_score); + void do_round(); + + Chess::Move best_move() const; + double expected_value() const; + double uct(Chess::Colour colour) const; + bool expanded() const; + + EvalMethod eval_method() const { return m_eval_method; } + void set_eval_method(EvalMethod method) { m_eval_method = method; } + +private: + NonnullOwnPtrVector m_children; + MCTSTree* m_parent { nullptr }; + int m_white_points { 0 }; + int m_simulations { 0 }; + bool m_moves_generated { false }; + double m_exploration_parameter; + EvalMethod m_eval_method { EvalMethod::Simulation }; + Chess::Board m_board; +}; diff --git a/Applications/ChessEngine/main.cpp b/Applications/ChessEngine/main.cpp new file mode 100644 index 00000000000..185b6f065ce --- /dev/null +++ b/Applications/ChessEngine/main.cpp @@ -0,0 +1,36 @@ +/* + * 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 "ChessEngine.h" +#include +#include + +int main() +{ + Core::EventLoop loop; + auto engine = ChessEngine::construct(Core::File::stdin(), Core::File::stdout()); + return loop.exec(); +} diff --git a/Games/Chess/ChessWidget.cpp b/Games/Chess/ChessWidget.cpp index bd18c5d895f..cc936cb21e8 100644 --- a/Games/Chess/ChessWidget.cpp +++ b/Games/Chess/ChessWidget.cpp @@ -272,7 +272,7 @@ void ChessWidget::maybe_input_engine_move() if (drag_was_enabled) set_drag_enabled(false); - m_engine->get_best_move(board(), 500, [this, drag_was_enabled](Chess::Move move) { + m_engine->get_best_move(board(), 4000, [this, drag_was_enabled](Chess::Move move) { set_drag_enabled(drag_was_enabled); ASSERT(board().apply_move(move)); update(); diff --git a/Libraries/LibChess/Chess.cpp b/Libraries/LibChess/Chess.cpp index 7495acf0fbe..6e332e3cc49 100644 --- a/Libraries/LibChess/Chess.cpp +++ b/Libraries/LibChess/Chess.cpp @@ -425,8 +425,8 @@ bool Board::apply_illegal_move(const Move& move, Colour colour) if (move.to == Square("a8") || move.to == Square("c8")) { set_piece(Square("e8"), EmptyPiece); set_piece(Square("a8"), EmptyPiece); - set_piece(Square("c8"), { Colour::White, Type::King }); - set_piece(Square("d8"), { Colour::White, Type::Rook }); + set_piece(Square("c8"), { Colour::Black, Type::King }); + set_piece(Square("d8"), { Colour::Black, Type::Rook }); return true; } else if (move.to == Square("h8") || move.to == Square("g8")) { set_piece(Square("e8"), EmptyPiece); @@ -463,6 +463,23 @@ bool Board::apply_illegal_move(const Move& move, Colour colour) return true; } +Move Board::random_move(Colour colour) const +{ + if (colour == Colour::None) + colour = 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 { bool sufficient_material = false; @@ -533,6 +550,65 @@ Board::Result Board::game_result() const return Result::StaleMate; } +Colour Board::game_winner() const +{ + if (game_result() == Result::CheckMate) + return opposing_colour(turn()); + + return Colour::None; +} + +int Board::game_score() const +{ + switch (game_winner()) { + case Colour::White: + return +1; + case Colour::Black: + return -1; + case Colour::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).colour == Colour::White) { + imbalance += value; + } else { + imbalance -= value; + } + return IterationDecision::Continue; + }); + return imbalance; +} + bool Board::is_promotion_move(const Move& move, Colour colour) const { if (colour == Colour::None) diff --git a/Libraries/LibChess/Chess.h b/Libraries/LibChess/Chess.h index 2e9c078f788..f0e0b91aab2 100644 --- a/Libraries/LibChess/Chess.h +++ b/Libraries/LibChess/Chess.h @@ -135,7 +135,12 @@ public: template void generate_moves(Callback callback, Colour colour = Colour::None) const; + Move random_move(Colour colour = Colour::None) const; Result game_result() const; + Colour game_winner() const; + int game_score() const; + bool game_finished() const; + int material_imbalance() const; Colour turn() const { return m_turn; } const Vector& moves() const { return m_moves; }