diff --git a/Games/Chess/ChessWidget.cpp b/Games/Chess/ChessWidget.cpp index 053d0c7e55..827df77f6a 100644 --- a/Games/Chess/ChessWidget.cpp +++ b/Games/Chess/ChessWidget.cpp @@ -27,9 +27,12 @@ #include "ChessWidget.h" #include "PromotionDialog.h" #include +#include +#include #include #include #include +#include ChessWidget::ChessWidget(const StringView& set) { @@ -289,6 +292,57 @@ void ChessWidget::maybe_input_engine_move() }); } +bool ChessWidget::export_pgn(const StringView& export_path) const +{ + auto file_or_error = Core::File::open(export_path, Core::File::WriteOnly); + if (file_or_error.is_error()) { + warnln("Couldn't open '{}': {}", export_path, file_or_error.error()); + return false; + } + auto& file = *file_or_error.value(); + + // Tag Pair Section + file.write("[Event \"Casual Game\"]\n"); + file.write("[Site \"SerenityOS Chess\"]\n"); + file.write(String::formatted("[Date \"{}\"]\n", Core::DateTime::now().to_string("%Y.%m.%d"))); + file.write("[Round \"1\"]\n"); + + String username(getlogin()); + const String player1 = (!username.is_empty() ? username : "?"); + const String player2 = (!m_engine.is_null() ? "SerenityOS ChessEngine" : "?"); + file.write(String::formatted("[White \"{}\"]\n", m_side == Chess::Colour::White ? player1 : player2)); + file.write(String::formatted("[Black \"{}\"]\n", m_side == Chess::Colour::Black ? player1 : player2)); + + file.write(String::formatted("[Result \"{}\"]\n", Chess::Board::result_to_points(m_board.game_result(), m_board.turn()))); + file.write("[WhiteElo \"?\"]\n"); + file.write("[BlackElo \"?\"]\n"); + file.write("[Variant \"Standard\"]\n"); + file.write("[TimeControl \"-\"]\n"); + file.write("[Annotator \"SerenityOS Chess\"]\n"); + file.write("\n"); + + // Movetext Section + for (size_t i = 0, move_no = 1; i < m_board.moves().size(); i += 2, move_no++) { + const String white = m_board.moves().at(i).to_algebraic(); + + if (i + 1 < m_board.moves().size()) { + const String black = m_board.moves().at(i + 1).to_algebraic(); + file.write(String::formatted("{}. {} {} ", move_no, white, black)); + } else { + file.write(String::formatted("{}. {} ", move_no, white)); + } + } + + file.write("{ "); + file.write(Chess::Board::result_to_string(m_board.game_result(), m_board.turn())); + file.write(" } "); + file.write(Chess::Board::result_to_points(m_board.game_result(), m_board.turn())); + file.write("\n"); + + file.close(); + return true; +} + void ChessWidget::flip_board() { m_side = Chess::opposing_colour(m_side); @@ -306,6 +360,6 @@ void ChessWidget::resign() set_drag_enabled(false); update(); - const String msg = m_board.result_to_string(m_board.game_result()); + const String msg = Chess::Board::result_to_string(m_board.game_result(), m_board.turn()); GUI::MessageBox::show(window(), msg, "Game Over", GUI::MessageBox::Type::Information); } diff --git a/Games/Chess/ChessWidget.h b/Games/Chess/ChessWidget.h index 657caeefbd..d21b1ac488 100644 --- a/Games/Chess/ChessWidget.h +++ b/Games/Chess/ChessWidget.h @@ -48,6 +48,7 @@ public: virtual void mousemove_event(GUI::MouseEvent&) override; Chess::Board& board() { return m_board; }; + const Chess::Board& board() const { return m_board; }; Chess::Colour side() const { return m_side; }; void set_side(Chess::Colour side) { m_side = side; }; @@ -61,6 +62,8 @@ public: void set_drag_enabled(bool e) { m_drag_enabled = e; } RefPtr get_piece_graphic(const Chess::Piece& piece) const; + bool export_pgn(const StringView& export_path) const; + void resign(); void flip_board(); void reset(); diff --git a/Games/Chess/main.cpp b/Games/Chess/main.cpp index 97a5de81f7..ba0a21deed 100644 --- a/Games/Chess/main.cpp +++ b/Games/Chess/main.cpp @@ -30,9 +30,11 @@ #include #include #include +#include #include #include #include +#include #include int main(int argc, char** argv) @@ -65,6 +67,16 @@ int main(int argc, char** argv) return 1; } + if (unveil("/etc/passwd", "r") < 0) { + perror("unveil"); + return 1; + } + + if (unveil(Core::StandardPaths::home_directory().characters(), "wcb") < 0) { + perror("unveil"); + return 1; + } + if (unveil(nullptr, nullptr) < 0) { perror("unveil"); return 1; @@ -93,6 +105,24 @@ int main(int argc, char** argv) })); app_menu.add_separator(); + app_menu.add_action(GUI::Action::create("Import PGN...", { Mod_None, Key_F6 }, [&](auto&) { + GUI::MessageBox::show(window, "Feature not yet available.", "TODO", GUI::MessageBox::Type::Information); + })); + app_menu.add_action(GUI::Action::create("Export PGN...", { Mod_None, Key_F7 }, [&](auto&) { + Optional export_path = GUI::FilePicker::get_save_filepath(window, "Untitled", "pgn"); + + if (!export_path.has_value()) + return; + + if (!widget.export_pgn(export_path.value())) { + GUI::MessageBox::show(window, "Unable to export game.\n", "Error", GUI::MessageBox::Type::Error); + return; + } + + dbgln("Exported PGN file to {}", export_path.value()); + })); + app_menu.add_separator(); + app_menu.add_action(GUI::Action::create("New game", { Mod_None, Key_F2 }, [&](auto&) { widget.reset(); })); diff --git a/Libraries/LibChess/Chess.cpp b/Libraries/LibChess/Chess.cpp index 7a10049a39..f26ed57b78 100644 --- a/Libraries/LibChess/Chess.cpp +++ b/Libraries/LibChess/Chess.cpp @@ -122,6 +122,49 @@ String Move::to_long_algebraic() const return builder.build(); } +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. @@ -362,6 +405,7 @@ bool Board::is_legal_no_check(const Move& move, Colour colour) const 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 { @@ -369,6 +413,7 @@ bool Board::is_legal_no_check(const Move& move, Colour colour) const 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; } } @@ -408,6 +453,8 @@ bool Board::apply_move(const Move& move, Colour colour) if (!is_legal(move, colour)) return false; + const_cast(move).piece = get_piece(move.from); + return apply_illegal_move(move, colour); } @@ -467,10 +514,19 @@ bool Board::apply_illegal_move(const Move& move, Colour colour) } } + if (get_piece(move.to).colour != Colour::None) { + const_cast(move).is_capture = true; + m_moves_since_capture = 0; + } + if (get_piece(move.from).type == Type::Pawn && ((colour == Colour::Black && move.to.rank == 0) || (colour == Colour::White && move.to.rank == 7))) { // Pawn Promotion set_piece(move.to, { colour, move.promote_to }); set_piece(move.from, EmptyPiece); + + if (in_check(m_turn)) + const_cast(move).is_check = true; + return true; } @@ -481,15 +537,29 @@ bool Board::apply_illegal_move(const Move& move, Colour colour) } else { set_piece({ move.to.rank + 1, move.to.file }, EmptyPiece); } + const_cast(move).is_capture = true; m_moves_since_capture = 0; } - if (get_piece(move.to).colour != Colour::None) - 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).colour == move.piece.colour) { + if (is_legal(Move(sq, move.to), get_piece(sq).colour)) { + 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).is_check = true; + return true; } @@ -577,8 +647,10 @@ Board::Result Board::game_result() const return Result::NotFinished; } - if (in_check(turn())) + if (in_check(turn())) { + const_cast&>(m_moves).last().is_mate = true; return Result::CheckMate; + } return Result::StaleMate; } @@ -692,14 +764,12 @@ void Board::set_resigned(Chess::Colour c) m_resigned = c; } -String Board::result_to_string(Result r) const +String Board::result_to_string(Result result, Colour turn) { - switch (r) { + switch (result) { case Result::CheckMate: - if (m_turn == Chess::Colour::White) - return "Black wins by Checkmate"; - else - return "White wins by Checkmate"; + ASSERT(turn != Chess::Colour::None); + return turn == Chess::Colour::White ? "Black wins by Checkmate" : "White wins by Checkmate"; case Result::WhiteResign: return "Black wins by Resignation"; case Result::BlackResign: @@ -723,4 +793,33 @@ String Board::result_to_string(Result r) const } } +String Board::result_to_points(Result result, Colour turn) +{ + switch (result) { + case Result::CheckMate: + ASSERT(turn != Chess::Colour::None); + return turn == Chess::Colour::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/Libraries/LibChess/Chess.h b/Libraries/LibChess/Chess.h index 787a6ed4b2..85b135197d 100644 --- a/Libraries/LibChess/Chess.h +++ b/Libraries/LibChess/Chess.h @@ -105,6 +105,12 @@ 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& algebraic); Move(const Square& from, const Square& to, const Type& promote_to = Type::None) : from(from) @@ -115,6 +121,7 @@ struct Move { bool operator==(const Move& other) const { return from == other.from && to == other.to && promote_to == other.promote_to; } String to_long_algebraic() const; + String to_algebraic() const; }; class Board { @@ -145,7 +152,8 @@ public: NotFinished, }; - String result_to_string(Result) const; + static String result_to_string(Result, Colour turn); + static String result_to_points(Result, Colour turn); template void generate_moves(Callback callback, Colour colour = Colour::None) const;