From fb62eed05889b1fef9373843ac048e4b05056a4b Mon Sep 17 00:00:00 2001 From: Peter Elliott Date: Wed, 19 Aug 2020 17:53:50 -0600 Subject: [PATCH] Chess: Add support for UCI engines --- Games/Chess/CMakeLists.txt | 1 + Games/Chess/ChessWidget.cpp | 20 ++++++++ Games/Chess/ChessWidget.h | 6 +++ Games/Chess/Engine.cpp | 88 ++++++++++++++++++++++++++++++++++++ Games/Chess/Engine.h | 58 ++++++++++++++++++++++++ Games/Chess/main.cpp | 23 +++++++++- Libraries/LibChess/Chess.cpp | 3 ++ Libraries/LibChess/Chess.h | 5 +- 8 files changed, 202 insertions(+), 2 deletions(-) create mode 100644 Games/Chess/Engine.cpp create mode 100644 Games/Chess/Engine.h diff --git a/Games/Chess/CMakeLists.txt b/Games/Chess/CMakeLists.txt index 1f2fe123e8..29f9ec310c 100644 --- a/Games/Chess/CMakeLists.txt +++ b/Games/Chess/CMakeLists.txt @@ -2,6 +2,7 @@ set(SOURCES main.cpp ChessWidget.cpp PromotionDialog.cpp + Engine.cpp ) serenity_bin(Chess) diff --git a/Games/Chess/ChessWidget.cpp b/Games/Chess/ChessWidget.cpp index 5c9a944e89..bd18c5d895 100644 --- a/Games/Chess/ChessWidget.cpp +++ b/Games/Chess/ChessWidget.cpp @@ -175,6 +175,8 @@ void ChessWidget::mouseup_event(GUI::MouseEvent& event) update(); GUI::MessageBox::show(window(), msg, "Game Over", GUI::MessageBox::Type::Information); } + } else { + maybe_input_engine_move(); } } @@ -240,7 +242,9 @@ RefPtr ChessWidget::get_piece_graphic(const Chess::Piece& piece) co void ChessWidget::reset() { m_board = Chess::Board(); + m_side = (arc4random() % 2) ? Chess::Colour::White : Chess::Colour::Black; m_drag_enabled = true; + maybe_input_engine_move(); update(); } @@ -258,3 +262,19 @@ void ChessWidget::set_board_theme(const StringView& name) set_board_theme("Beige"); } } + +void ChessWidget::maybe_input_engine_move() +{ + if (!m_engine || board().turn() == side()) + return; + + bool drag_was_enabled = drag_enabled(); + if (drag_was_enabled) + set_drag_enabled(false); + + m_engine->get_best_move(board(), 500, [this, drag_was_enabled](Chess::Move move) { + set_drag_enabled(drag_was_enabled); + ASSERT(board().apply_move(move)); + update(); + }); +} diff --git a/Games/Chess/ChessWidget.h b/Games/Chess/ChessWidget.h index 893187f360..da881277b6 100644 --- a/Games/Chess/ChessWidget.h +++ b/Games/Chess/ChessWidget.h @@ -26,6 +26,7 @@ #pragma once +#include "Engine.h" #include #include #include @@ -73,6 +74,10 @@ public: void set_board_theme(const BoardTheme& theme) { m_board_theme = theme; } void set_board_theme(const StringView& name); + void set_engine(RefPtr engine) { m_engine = engine; } + + void maybe_input_engine_move(); + private: Chess::Board m_board; BoardTheme m_board_theme { "Beige", Color::from_rgb(0xb58863), Color::from_rgb(0xf0d9b5) }; @@ -84,4 +89,5 @@ private: Gfx::IntPoint m_drag_point; bool m_dragging_piece { false }; bool m_drag_enabled { true }; + RefPtr m_engine; }; diff --git a/Games/Chess/Engine.cpp b/Games/Chess/Engine.cpp new file mode 100644 index 0000000000..522c94aa91 --- /dev/null +++ b/Games/Chess/Engine.cpp @@ -0,0 +1,88 @@ +/* + * 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 "Engine.h" +#include +#include +#include +#include +#include + +Engine::~Engine() +{ + if (m_pid != -1) + kill(m_pid, SIGINT); +} + +Engine::Engine(const StringView& command) +{ + int wpipefds[2]; + int rpipefds[2]; + if (pipe2(wpipefds, O_CLOEXEC) < 0) { + perror("pipe2"); + ASSERT_NOT_REACHED(); + } + + if (pipe2(rpipefds, O_CLOEXEC) < 0) { + perror("pipe2"); + ASSERT_NOT_REACHED(); + } + + posix_spawn_file_actions_t file_actions; + posix_spawn_file_actions_init(&file_actions); + posix_spawn_file_actions_adddup2(&file_actions, wpipefds[0], STDIN_FILENO); + posix_spawn_file_actions_adddup2(&file_actions, rpipefds[1], STDOUT_FILENO); + + String cstr(command); + const char* argv[] = { cstr.characters(), nullptr }; + if (posix_spawnp(&m_pid, cstr.characters(), &file_actions, nullptr, const_cast(argv), environ) < 0) { + perror("posix_spawnp"); + ASSERT_NOT_REACHED(); + } + + posix_spawn_file_actions_destroy(&file_actions); + + close(wpipefds[0]); + close(rpipefds[1]); + + auto infile = Core::File::construct(); + infile->open(rpipefds[0], Core::IODevice::ReadOnly, Core::File::ShouldCloseFileDescription::Yes); + set_in(infile); + + auto outfile = Core::File::construct(); + outfile->open(wpipefds[1], Core::IODevice::WriteOnly, Core::File::ShouldCloseFileDescription::Yes); + set_out(outfile); + + send_command(Chess::UCI::UCICommand()); +} + +void Engine::handle_bestmove(const Chess::UCI::BestMoveCommand& command) +{ + if (m_bestmove_callback) + m_bestmove_callback(command.move()); + + m_bestmove_callback = nullptr; +} diff --git a/Games/Chess/Engine.h b/Games/Chess/Engine.h new file mode 100644 index 0000000000..d07ef9cfcf --- /dev/null +++ b/Games/Chess/Engine.h @@ -0,0 +1,58 @@ +/* + * 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 + +class Engine : public Chess::UCI::Endpoint { + C_OBJECT(Engine) +public: + virtual ~Engine() override; + + Engine(const StringView& command); + + Engine(const Engine&) = delete; + Engine& operator=(const Engine&) = delete; + + virtual void handle_bestmove(const Chess::UCI::BestMoveCommand&); + + template + void get_best_move(const Chess::Board& board, int time_limit, Callback&& callback) + { + send_command(Chess::UCI::PositionCommand({}, board.moves())); + Chess::UCI::GoCommand go_command; + go_command.movetime = time_limit; + send_command(go_command); + m_bestmove_callback = move(callback); + } + +private: + Function m_bestmove_callback; + pid_t m_pid { -1 }; +}; diff --git a/Games/Chess/main.cpp b/Games/Chess/main.cpp index 45e52bc0a1..7ee2536719 100644 --- a/Games/Chess/main.cpp +++ b/Games/Chess/main.cpp @@ -40,7 +40,6 @@ int main(int argc, char** argv) auto window = GUI::Window::construct(); auto& widget = window->set_main_widget(); - widget.set_side(Chess::Colour::White); RefPtr config = Core::ConfigFile::get_for_app("Chess"); @@ -106,6 +105,27 @@ int main(int argc, char** argv) board_theme_menu.add_action(*action); } + auto& engine_menu = menubar->add_menu("Engine"); + + GUI::ActionGroup engines_action_group; + engines_action_group.set_exclusive(true); + auto& engine_submenu = engine_menu.add_submenu("Engine"); + for (auto& engine : Vector({ "Human", "ChessEngine" })) { + auto action = GUI::Action::create_checkable(engine, [&](auto& action) { + if (action.text() == "Human") { + widget.set_engine(nullptr); + } else { + widget.set_engine(Engine::construct(action.text())); + widget.maybe_input_engine_move(); + } + }); + engines_action_group.add_action(*action); + if (engine == String("Human")) + action->set_checked(true); + + engine_submenu.add_action(*action); + } + auto& help_menu = menubar->add_menu("Help"); help_menu.add_action(GUI::Action::create("About", [&](auto&) { GUI::AboutDialog::show("Chess", Gfx::Bitmap::load_from_file("/res/icons/32x32/app-chess.png"), window); @@ -114,6 +134,7 @@ int main(int argc, char** argv) app->set_menubar(move(menubar)); window->show(); + widget.reset(); return app->exec(); } diff --git a/Libraries/LibChess/Chess.cpp b/Libraries/LibChess/Chess.cpp index 28249c6956..7495acf0fb 100644 --- a/Libraries/LibChess/Chess.cpp +++ b/Libraries/LibChess/Chess.cpp @@ -385,10 +385,13 @@ bool Board::apply_illegal_move(const Move& move, Colour colour) { 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_colour(colour); diff --git a/Libraries/LibChess/Chess.h b/Libraries/LibChess/Chess.h index efb43d3315..2e9c078f78 100644 --- a/Libraries/LibChess/Chess.h +++ b/Libraries/LibChess/Chess.h @@ -31,6 +31,7 @@ #include #include #include +#include namespace Chess { @@ -136,7 +137,8 @@ public: void generate_moves(Callback callback, Colour colour = Colour::None) const; Result game_result() const; - Colour turn() const { return m_turn; }; + Colour turn() const { return m_turn; } + const Vector& moves() const { return m_moves; } bool operator==(const Board& other) const; @@ -155,6 +157,7 @@ private: bool m_black_can_castle_queenside { true }; HashMap m_previous_states; + Vector m_moves; friend struct AK::Traits; };