diff --git a/Games/Chess/ChessWidget.cpp b/Games/Chess/ChessWidget.cpp index 827df77f6a..44596ce713 100644 --- a/Games/Chess/ChessWidget.cpp +++ b/Games/Chess/ChessWidget.cpp @@ -292,6 +292,11 @@ void ChessWidget::maybe_input_engine_move() }); } +String ChessWidget::get_fen() const +{ + return m_board.to_fen(); +} + 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 d21b1ac488..4ecd38f4f9 100644 --- a/Games/Chess/ChessWidget.h +++ b/Games/Chess/ChessWidget.h @@ -62,6 +62,7 @@ public: void set_drag_enabled(bool e) { m_drag_enabled = e; } RefPtr get_piece_graphic(const Chess::Piece& piece) const; + String get_fen() const; bool export_pgn(const StringView& export_path) const; void resign(); diff --git a/Games/Chess/main.cpp b/Games/Chess/main.cpp index e68e8c5aee..69844039d3 100644 --- a/Games/Chess/main.cpp +++ b/Games/Chess/main.cpp @@ -30,6 +30,7 @@ #include #include #include +#include #include #include #include @@ -121,6 +122,10 @@ int main(int argc, char** argv) dbgln("Exported PGN file to {}", export_path.value()); })); + app_menu.add_action(GUI::Action::create("Copy FEN", { Mod_Ctrl, Key_C }, [&](auto&) { + GUI::Clipboard::the().set_data(widget.get_fen().bytes()); + GUI::MessageBox::show(window, "Board state copied to clipboard as FEN.", "Copy FEN", GUI::MessageBox::Type::Information); + })); app_menu.add_separator(); app_menu.add_action(GUI::Action::create("New game", { Mod_None, Key_F2 }, [&](auto&) { diff --git a/Libraries/LibChess/Chess.cpp b/Libraries/LibChess/Chess.cpp index f26ed57b78..57dd575ded 100644 --- a/Libraries/LibChess/Chess.cpp +++ b/Libraries/LibChess/Chess.cpp @@ -205,6 +205,73 @@ Board::Board() set_piece(Square("h8"), { Colour::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.colour == Colour::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 != Colour::None); + builder.append(m_turn == Colour::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); @@ -474,6 +541,7 @@ bool Board::apply_illegal_move(const Move& move, Colour colour) 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; @@ -514,6 +582,9 @@ bool Board::apply_illegal_move(const Move& move, Colour colour) } } + if (move.piece.type == Type::Pawn) + m_moves_since_pawn_advance = 0; + if (get_piece(move.to).colour != Colour::None) { const_cast(move).is_capture = true; m_moves_since_capture = 0; diff --git a/Libraries/LibChess/Chess.h b/Libraries/LibChess/Chess.h index 85b135197d..dc708f3456 100644 --- a/Libraries/LibChess/Chess.h +++ b/Libraries/LibChess/Chess.h @@ -139,6 +139,8 @@ public: bool apply_move(const Move&, Colour colour = Colour::None); const Optional& last_move() const { return m_last_move; } + String to_fen() const; + enum class Result { CheckMate, StaleMate, @@ -180,6 +182,7 @@ private: Colour m_resigned { Colour::None }; Optional 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 };