mirror of
https://github.com/RGBCube/serenity
synced 2025-05-30 23:48:11 +00:00
Chess: Add ability to export game as PGN file
This patch adds an option to the menubar for exporting the current game as a PGN file. This file can then be read by other chess programs (and ours eventually) to replay the game or analyze it. The implementation is mostly PGN spec compliant, however the code could use some more work. Particularly the `const_cast`s... But it's a start. :^) Fixup: Chess: Fixed hard-coded home path in unveil() call Fixup: Chess: Removed castling flags from Move struct The castling detection logic is done inside Move::to_algebraic() now, removing the need for is_castle_short and is_castle_long flags inside of the Move struct.
This commit is contained in:
parent
01b62cc7f4
commit
fe1628746c
5 changed files with 205 additions and 11 deletions
|
@ -27,9 +27,12 @@
|
||||||
#include "ChessWidget.h"
|
#include "ChessWidget.h"
|
||||||
#include "PromotionDialog.h"
|
#include "PromotionDialog.h"
|
||||||
#include <AK/String.h>
|
#include <AK/String.h>
|
||||||
|
#include <LibCore/DateTime.h>
|
||||||
|
#include <LibCore/File.h>
|
||||||
#include <LibGUI/MessageBox.h>
|
#include <LibGUI/MessageBox.h>
|
||||||
#include <LibGUI/Painter.h>
|
#include <LibGUI/Painter.h>
|
||||||
#include <LibGfx/Font.h>
|
#include <LibGfx/Font.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
ChessWidget::ChessWidget(const StringView& set)
|
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()
|
void ChessWidget::flip_board()
|
||||||
{
|
{
|
||||||
m_side = Chess::opposing_colour(m_side);
|
m_side = Chess::opposing_colour(m_side);
|
||||||
|
@ -306,6 +360,6 @@ void ChessWidget::resign()
|
||||||
|
|
||||||
set_drag_enabled(false);
|
set_drag_enabled(false);
|
||||||
update();
|
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);
|
GUI::MessageBox::show(window(), msg, "Game Over", GUI::MessageBox::Type::Information);
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,6 +48,7 @@ public:
|
||||||
virtual void mousemove_event(GUI::MouseEvent&) override;
|
virtual void mousemove_event(GUI::MouseEvent&) override;
|
||||||
|
|
||||||
Chess::Board& board() { return m_board; };
|
Chess::Board& board() { return m_board; };
|
||||||
|
const Chess::Board& board() const { return m_board; };
|
||||||
|
|
||||||
Chess::Colour side() const { return m_side; };
|
Chess::Colour side() const { return m_side; };
|
||||||
void set_side(Chess::Colour side) { m_side = 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; }
|
void set_drag_enabled(bool e) { m_drag_enabled = e; }
|
||||||
RefPtr<Gfx::Bitmap> get_piece_graphic(const Chess::Piece& piece) const;
|
RefPtr<Gfx::Bitmap> get_piece_graphic(const Chess::Piece& piece) const;
|
||||||
|
|
||||||
|
bool export_pgn(const StringView& export_path) const;
|
||||||
|
|
||||||
void resign();
|
void resign();
|
||||||
void flip_board();
|
void flip_board();
|
||||||
void reset();
|
void reset();
|
||||||
|
|
|
@ -30,9 +30,11 @@
|
||||||
#include <LibGUI/AboutDialog.h>
|
#include <LibGUI/AboutDialog.h>
|
||||||
#include <LibGUI/ActionGroup.h>
|
#include <LibGUI/ActionGroup.h>
|
||||||
#include <LibGUI/Application.h>
|
#include <LibGUI/Application.h>
|
||||||
|
#include <LibGUI/FilePicker.h>
|
||||||
#include <LibGUI/Icon.h>
|
#include <LibGUI/Icon.h>
|
||||||
#include <LibGUI/Menu.h>
|
#include <LibGUI/Menu.h>
|
||||||
#include <LibGUI/MenuBar.h>
|
#include <LibGUI/MenuBar.h>
|
||||||
|
#include <LibGUI/MessageBox.h>
|
||||||
#include <LibGUI/Window.h>
|
#include <LibGUI/Window.h>
|
||||||
|
|
||||||
int main(int argc, char** argv)
|
int main(int argc, char** argv)
|
||||||
|
@ -65,6 +67,16 @@ int main(int argc, char** argv)
|
||||||
return 1;
|
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) {
|
if (unveil(nullptr, nullptr) < 0) {
|
||||||
perror("unveil");
|
perror("unveil");
|
||||||
return 1;
|
return 1;
|
||||||
|
@ -93,6 +105,24 @@ int main(int argc, char** argv)
|
||||||
}));
|
}));
|
||||||
app_menu.add_separator();
|
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<String> 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&) {
|
app_menu.add_action(GUI::Action::create("New game", { Mod_None, Key_F2 }, [&](auto&) {
|
||||||
widget.reset();
|
widget.reset();
|
||||||
}));
|
}));
|
||||||
|
|
|
@ -122,6 +122,49 @@ String Move::to_long_algebraic() const
|
||||||
return builder.build();
|
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()
|
Board::Board()
|
||||||
{
|
{
|
||||||
// Fill empty spaces.
|
// Fill empty spaces.
|
||||||
|
@ -362,6 +405,7 @@ bool Board::is_legal_no_check(const Move& move, Colour colour) const
|
||||||
|
|
||||||
return true;
|
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) {
|
} 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;
|
return true;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -369,6 +413,7 @@ bool Board::is_legal_no_check(const Move& move, Colour colour) const
|
||||||
|
|
||||||
return true;
|
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) {
|
} 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;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -408,6 +453,8 @@ bool Board::apply_move(const Move& move, Colour colour)
|
||||||
if (!is_legal(move, colour))
|
if (!is_legal(move, colour))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
const_cast<Move&>(move).piece = get_piece(move.from);
|
||||||
|
|
||||||
return apply_illegal_move(move, colour);
|
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&>(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))) {
|
if (get_piece(move.from).type == Type::Pawn && ((colour == Colour::Black && move.to.rank == 0) || (colour == Colour::White && move.to.rank == 7))) {
|
||||||
// Pawn Promotion
|
// Pawn Promotion
|
||||||
set_piece(move.to, { colour, move.promote_to });
|
set_piece(move.to, { colour, move.promote_to });
|
||||||
set_piece(move.from, EmptyPiece);
|
set_piece(move.from, EmptyPiece);
|
||||||
|
|
||||||
|
if (in_check(m_turn))
|
||||||
|
const_cast<Move&>(move).is_check = true;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -481,15 +537,29 @@ bool Board::apply_illegal_move(const Move& move, Colour colour)
|
||||||
} else {
|
} else {
|
||||||
set_piece({ move.to.rank + 1, move.to.file }, EmptyPiece);
|
set_piece({ move.to.rank + 1, move.to.file }, EmptyPiece);
|
||||||
}
|
}
|
||||||
|
const_cast<Move&>(move).is_capture = true;
|
||||||
m_moves_since_capture = 0;
|
m_moves_since_capture = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (get_piece(move.to).colour != Colour::None)
|
Square::for_each([&](Square sq) {
|
||||||
m_moves_since_capture = 0;
|
// 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.to, get_piece(move.from));
|
||||||
set_piece(move.from, EmptyPiece);
|
set_piece(move.from, EmptyPiece);
|
||||||
|
|
||||||
|
if (in_check(m_turn))
|
||||||
|
const_cast<Move&>(move).is_check = true;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -577,8 +647,10 @@ Board::Result Board::game_result() const
|
||||||
return Result::NotFinished;
|
return Result::NotFinished;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (in_check(turn()))
|
if (in_check(turn())) {
|
||||||
|
const_cast<Vector<Move>&>(m_moves).last().is_mate = true;
|
||||||
return Result::CheckMate;
|
return Result::CheckMate;
|
||||||
|
}
|
||||||
|
|
||||||
return Result::StaleMate;
|
return Result::StaleMate;
|
||||||
}
|
}
|
||||||
|
@ -692,14 +764,12 @@ void Board::set_resigned(Chess::Colour c)
|
||||||
m_resigned = 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:
|
case Result::CheckMate:
|
||||||
if (m_turn == Chess::Colour::White)
|
ASSERT(turn != Chess::Colour::None);
|
||||||
return "Black wins by Checkmate";
|
return turn == Chess::Colour::White ? "Black wins by Checkmate" : "White wins by Checkmate";
|
||||||
else
|
|
||||||
return "White wins by Checkmate";
|
|
||||||
case Result::WhiteResign:
|
case Result::WhiteResign:
|
||||||
return "Black wins by Resignation";
|
return "Black wins by Resignation";
|
||||||
case Result::BlackResign:
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -105,6 +105,12 @@ struct Move {
|
||||||
Square from;
|
Square from;
|
||||||
Square to;
|
Square to;
|
||||||
Type promote_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 StringView& algebraic);
|
||||||
Move(const Square& from, const Square& to, const Type& promote_to = Type::None)
|
Move(const Square& from, const Square& to, const Type& promote_to = Type::None)
|
||||||
: from(from)
|
: 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; }
|
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_long_algebraic() const;
|
||||||
|
String to_algebraic() const;
|
||||||
};
|
};
|
||||||
|
|
||||||
class Board {
|
class Board {
|
||||||
|
@ -145,7 +152,8 @@ public:
|
||||||
NotFinished,
|
NotFinished,
|
||||||
};
|
};
|
||||||
|
|
||||||
String result_to_string(Result) const;
|
static String result_to_string(Result, Colour turn);
|
||||||
|
static String result_to_points(Result, Colour turn);
|
||||||
|
|
||||||
template<typename Callback>
|
template<typename Callback>
|
||||||
void generate_moves(Callback callback, Colour colour = Colour::None) const;
|
void generate_moves(Callback callback, Colour colour = Colour::None) const;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue