diff --git a/Games/Chess/ChessWidget.cpp b/Games/Chess/ChessWidget.cpp index ae77721ea26..bca2cb0f670 100644 --- a/Games/Chess/ChessWidget.cpp +++ b/Games/Chess/ChessWidget.cpp @@ -374,6 +374,110 @@ String ChessWidget::get_fen() const return m_playback ? m_board_playback.to_fen() : m_board.to_fen(); } +bool ChessWidget::import_pgn(const StringView& import_path) +{ + auto file_or_error = Core::File::open(import_path, Core::File::OpenMode::ReadOnly); + if (file_or_error.is_error()) { + warnln("Couldn't open '{}': {}", import_path, file_or_error.error()); + return false; + } + auto& file = *file_or_error.value(); + + m_board = Chess::Board(); + + ByteBuffer bytes = file.read_all(); + StringView content = bytes; + auto lines = content.lines(); + StringView line; + size_t i = 0; + + // Tag Pair Section + // FIXME: Parse these tags when they become relevant + do { + line = lines.at(i++); + } while (!line.is_empty() || i >= lines.size()); + + // Movetext Section + bool skip = false; + bool recursive_annotation = false; + bool future_expansion = false; + Chess::Colour turn = Chess::Colour::White; + String movetext; + + for (size_t j = i; j < lines.size(); j++) + movetext = String::formatted("{}{}", movetext, lines.at(i).to_string()); + + for (auto token : movetext.split(' ')) { + token = token.trim_whitespace(); + + // FIXME: Parse all of these tokens when we start caring about them + if (token.ends_with("}")) { + skip = false; + continue; + } + if (skip) + continue; + if (token.starts_with("{")) { + if (token.ends_with("}")) + continue; + skip = true; + continue; + } + if (token.ends_with(")")) { + recursive_annotation = false; + continue; + } + if (recursive_annotation) + continue; + if (token.starts_with("(")) { + if (token.ends_with(")")) + continue; + recursive_annotation = true; + continue; + } + if (token.ends_with(">")) { + future_expansion = false; + continue; + } + if (future_expansion) + continue; + if (token.starts_with("<")) { + if (token.ends_with(">")) + continue; + future_expansion = true; + continue; + } + if (token.starts_with("$")) + continue; + if (token.contains("*")) + break; + // FIXME: When we become able to set more of the game state, fix these end results + if (token.contains("1-0")) { + m_board.set_resigned(Chess::Colour::Black); + break; + } + if (token.contains("0-1")) { + m_board.set_resigned(Chess::Colour::White); + break; + } + if (token.contains("1/2-1/2")) { + break; + } + if (!token.ends_with(".")) { + m_board.apply_move(Chess::Move::from_algebraic(token, turn, m_board)); + turn = Chess::opposing_colour(turn); + } + } + + m_board_playback = m_board; + m_playback_move_number = m_board_playback.moves().size(); + m_playback = true; + update(); + + file.close(); + return true; +} + bool ChessWidget::export_pgn(const StringView& export_path) const { auto file_or_error = Core::File::open(export_path, Core::File::WriteOnly); diff --git a/Games/Chess/ChessWidget.h b/Games/Chess/ChessWidget.h index 9706d1d44b6..7535ce8bdfa 100644 --- a/Games/Chess/ChessWidget.h +++ b/Games/Chess/ChessWidget.h @@ -67,6 +67,7 @@ public: RefPtr get_piece_graphic(const Chess::Piece& piece) const; String get_fen() const; + bool import_pgn(const StringView& import_path); bool export_pgn(const StringView& export_path) const; void resign(); diff --git a/Games/Chess/main.cpp b/Games/Chess/main.cpp index 69844039d32..150b25caaf7 100644 --- a/Games/Chess/main.cpp +++ b/Games/Chess/main.cpp @@ -73,7 +73,7 @@ int main(int argc, char** argv) return 1; } - if (unveil(Core::StandardPaths::home_directory().characters(), "wcb") < 0) { + if (unveil(Core::StandardPaths::home_directory().characters(), "wcbr") < 0) { perror("unveil"); return 1; } @@ -107,7 +107,17 @@ int main(int argc, char** argv) app_menu.add_separator(); app_menu.add_action(GUI::Action::create("Import PGN...", { Mod_Ctrl, Key_O }, [&](auto&) { - GUI::MessageBox::show(window, "Feature not yet available.", "TODO", GUI::MessageBox::Type::Information); + Optional import_path = GUI::FilePicker::get_open_filepath(window); + + if (!import_path.has_value()) + return; + + if (!widget.import_pgn(import_path.value())) { + GUI::MessageBox::show(window, "Unable to import game.\n", "Error", GUI::MessageBox::Type::Error); + return; + } + + dbgln("Imported PGN file from {}", import_path.value()); })); app_menu.add_action(GUI::Action::create("Export PGN...", { Mod_Ctrl, Key_S }, [&](auto&) { Optional export_path = GUI::FilePicker::get_save_filepath(window, "Untitled", "pgn"); diff --git a/Libraries/LibChess/Chess.cpp b/Libraries/LibChess/Chess.cpp index 57dd575ded9..ccd9b02769c 100644 --- a/Libraries/LibChess/Chess.cpp +++ b/Libraries/LibChess/Chess.cpp @@ -106,10 +106,10 @@ String Square::to_algebraic() const return builder.build(); } -Move::Move(const StringView& algebraic) - : from(algebraic.substring_view(0, 2)) - , to(algebraic.substring_view(2, 2)) - , promote_to(piece_for_char_promotion((algebraic.length() >= 5) ? algebraic.substring_view(4, 1) : "")) +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) : "")) { } @@ -122,6 +122,81 @@ String Move::to_long_algebraic() const return builder.build(); } +Move Move::from_algebraic(const StringView& algebraic, const Colour turn, const Board& board) +{ + String move_string = algebraic; + Move move({ 50, 50 }, { 50, 50 }); + + if (move_string.contains("-")) { + move.from = Square(turn == Colour::White ? 0 : 7, 4); + move.to = Square(turn == Colour::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) { diff --git a/Libraries/LibChess/Chess.h b/Libraries/LibChess/Chess.h index dc708f34569..09ae6c28789 100644 --- a/Libraries/LibChess/Chess.h +++ b/Libraries/LibChess/Chess.h @@ -101,6 +101,8 @@ struct Square { String to_algebraic() const; }; +class Board; + struct Move { Square from; Square to; @@ -111,7 +113,7 @@ struct Move { bool is_capture = false; bool is_ambiguous = false; Square ambiguous { 50, 50 }; - Move(const StringView& algebraic); + Move(const StringView& long_algebraic); Move(const Square& from, const Square& to, const Type& promote_to = Type::None) : from(from) , to(to) @@ -120,6 +122,7 @@ struct Move { } 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 Colour turn, const Board& board); String to_long_algebraic() const; String to_algebraic() const; };