mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2024-11-26 09:30:24 +00:00
Chess: Added abilty to import PGN files
This patch allows the user to load games using PGN files. The parsing is not complete and has a bunch of work left to be done, but it's okay for our use case here. It can load all of the games our PGN exporter can save. As the Chess program impoves so can the PGN parser.
This commit is contained in:
parent
4d9837d792
commit
f631e73519
Notes:
sideshowbarker
2024-07-19 17:31:17 +09:00
Author: https://github.com/AnicJov Commit: https://github.com/SerenityOS/serenity/commit/f631e73519a Pull-request: https://github.com/SerenityOS/serenity/pull/4380
5 changed files with 200 additions and 7 deletions
|
@ -374,6 +374,110 @@ String ChessWidget::get_fen() const
|
||||||
return m_playback ? m_board_playback.to_fen() : m_board.to_fen();
|
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
|
bool ChessWidget::export_pgn(const StringView& export_path) const
|
||||||
{
|
{
|
||||||
auto file_or_error = Core::File::open(export_path, Core::File::WriteOnly);
|
auto file_or_error = Core::File::open(export_path, Core::File::WriteOnly);
|
||||||
|
|
|
@ -67,6 +67,7 @@ public:
|
||||||
RefPtr<Gfx::Bitmap> get_piece_graphic(const Chess::Piece& piece) const;
|
RefPtr<Gfx::Bitmap> get_piece_graphic(const Chess::Piece& piece) const;
|
||||||
|
|
||||||
String get_fen() const;
|
String get_fen() const;
|
||||||
|
bool import_pgn(const StringView& import_path);
|
||||||
bool export_pgn(const StringView& export_path) const;
|
bool export_pgn(const StringView& export_path) const;
|
||||||
|
|
||||||
void resign();
|
void resign();
|
||||||
|
|
|
@ -73,7 +73,7 @@ int main(int argc, char** argv)
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (unveil(Core::StandardPaths::home_directory().characters(), "wcb") < 0) {
|
if (unveil(Core::StandardPaths::home_directory().characters(), "wcbr") < 0) {
|
||||||
perror("unveil");
|
perror("unveil");
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
@ -107,7 +107,17 @@ int main(int argc, char** argv)
|
||||||
app_menu.add_separator();
|
app_menu.add_separator();
|
||||||
|
|
||||||
app_menu.add_action(GUI::Action::create("Import PGN...", { Mod_Ctrl, Key_O }, [&](auto&) {
|
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<String> 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&) {
|
app_menu.add_action(GUI::Action::create("Export PGN...", { Mod_Ctrl, Key_S }, [&](auto&) {
|
||||||
Optional<String> export_path = GUI::FilePicker::get_save_filepath(window, "Untitled", "pgn");
|
Optional<String> export_path = GUI::FilePicker::get_save_filepath(window, "Untitled", "pgn");
|
||||||
|
|
|
@ -106,10 +106,10 @@ String Square::to_algebraic() const
|
||||||
return builder.build();
|
return builder.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
Move::Move(const StringView& algebraic)
|
Move::Move(const StringView& long_algebraic)
|
||||||
: from(algebraic.substring_view(0, 2))
|
: from(long_algebraic.substring_view(0, 2))
|
||||||
, to(algebraic.substring_view(2, 2))
|
, to(long_algebraic.substring_view(2, 2))
|
||||||
, promote_to(piece_for_char_promotion((algebraic.length() >= 5) ? algebraic.substring_view(4, 1) : ""))
|
, 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();
|
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
|
String Move::to_algebraic() const
|
||||||
{
|
{
|
||||||
if (piece.type == Type::King && from.file == 4) {
|
if (piece.type == Type::King && from.file == 4) {
|
||||||
|
|
|
@ -101,6 +101,8 @@ struct Square {
|
||||||
String to_algebraic() const;
|
String to_algebraic() const;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
class Board;
|
||||||
|
|
||||||
struct Move {
|
struct Move {
|
||||||
Square from;
|
Square from;
|
||||||
Square to;
|
Square to;
|
||||||
|
@ -111,7 +113,7 @@ struct Move {
|
||||||
bool is_capture = false;
|
bool is_capture = false;
|
||||||
bool is_ambiguous = false;
|
bool is_ambiguous = false;
|
||||||
Square ambiguous { 50, 50 };
|
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)
|
Move(const Square& from, const Square& to, const Type& promote_to = Type::None)
|
||||||
: from(from)
|
: from(from)
|
||||||
, to(to)
|
, 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; }
|
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_long_algebraic() const;
|
||||||
String to_algebraic() const;
|
String to_algebraic() const;
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in a new issue