Chess: Add display widget for moves

Adds a TextEditor widget to the chess application to display move
history. It is updated whenever Board::apply_move is called to reflect
the current board state.
This commit is contained in:
dgaston 2024-05-18 20:54:45 -04:00 committed by Andrew Kaster
parent 6a4938a524
commit 2d5cb1e02d
Notes: sideshowbarker 2024-07-16 23:51:07 +09:00
4 changed files with 63 additions and 12 deletions

View file

@ -2,7 +2,34 @@
fill_with_background_color: true fill_with_background_color: true
layout: @GUI::VerticalBoxLayout {} layout: @GUI::VerticalBoxLayout {}
@Chess::ChessWidget { @GUI::HorizontalSplitter {
name: "chess_widget" @GUI::Frame {
name: "chess_widget_frame"
min_width: 508
min_height: 508
layout: @GUI::HorizontalBoxLayout {}
@Chess::ChessWidget {
name: "chess_widget"
}
}
@GUI::Frame {
layout: @GUI::VerticalBoxLayout {}
@GUI::Label {
text: "Moves"
text_alignment: "Center"
font_weight: "Bold"
fixed_height: 24
name: "moves_display_widget_label"
}
@GUI::TextEditor {
name: "move_display_widget"
mode: "DisplayOnly"
focus_policy: "NoFocus"
}
}
} }
} }

View file

@ -9,6 +9,7 @@
#include "ChessWidget.h" #include "ChessWidget.h"
#include "PromotionDialog.h" #include "PromotionDialog.h"
#include <AK/Enumerate.h>
#include <AK/GenericLexer.h> #include <AK/GenericLexer.h>
#include <AK/Random.h> #include <AK/Random.h>
#include <AK/String.h> #include <AK/String.h>
@ -37,7 +38,6 @@ ErrorOr<NonnullRefPtr<ChessWidget>> ChessWidget::try_create()
void ChessWidget::paint_event(GUI::PaintEvent& event) void ChessWidget::paint_event(GUI::PaintEvent& event)
{ {
int const min_size = min(width(), height()); int const min_size = min(width(), height());
int const widget_offset_x = (window()->width() - min_size) / 2;
int const widget_offset_y = (window()->height() - min_size) / 2; int const widget_offset_y = (window()->height() - min_size) / 2;
GUI::Frame::paint_event(event); GUI::Frame::paint_event(event);
@ -47,7 +47,7 @@ void ChessWidget::paint_event(GUI::PaintEvent& event)
painter.fill_rect(frame_inner_rect(), Gfx::Color::Black); painter.fill_rect(frame_inner_rect(), Gfx::Color::Black);
painter.translate(frame_thickness() + widget_offset_x, frame_thickness() + widget_offset_y); painter.translate(frame_thickness(), frame_thickness() + widget_offset_y);
auto square_width = min_size / 8; auto square_width = min_size / 8;
auto square_height = min_size / 8; auto square_height = min_size / 8;
@ -211,7 +211,6 @@ void ChessWidget::paint_event(GUI::PaintEvent& event)
void ChessWidget::mousedown_event(GUI::MouseEvent& event) void ChessWidget::mousedown_event(GUI::MouseEvent& event)
{ {
int const min_size = min(width(), height()); int const min_size = min(width(), height());
int const widget_offset_x = (window()->width() - min_size) / 2;
int const widget_offset_y = (window()->height() - min_size) / 2; int const widget_offset_y = (window()->height() - min_size) / 2;
if (!frame_inner_rect().contains(event.position())) if (!frame_inner_rect().contains(event.position()))
@ -237,7 +236,7 @@ void ChessWidget::mousedown_event(GUI::MouseEvent& event)
if (drag_enabled() && piece.color == board().turn() && !m_playback) { if (drag_enabled() && piece.color == board().turn() && !m_playback) {
m_dragging_piece = true; m_dragging_piece = true;
set_override_cursor(Gfx::StandardCursor::Drag); set_override_cursor(Gfx::StandardCursor::Drag);
m_drag_point = { event.position().x() - widget_offset_x, event.position().y() - widget_offset_y }; m_drag_point = { event.position().x(), event.position().y() - widget_offset_y };
m_moving_square = square.value(); m_moving_square = square.value();
m_board.generate_moves([&](Chess::Move move) { m_board.generate_moves([&](Chess::Move move) {
@ -295,6 +294,7 @@ void ChessWidget::mouseup_event(GUI::MouseEvent& event)
} }
if (board().apply_move(move)) { if (board().apply_move(move)) {
update_move_display_widget(m_board);
m_playback_move_number = board().moves().size(); m_playback_move_number = board().moves().size();
m_playback = false; m_playback = false;
m_board_playback = m_board; m_board_playback = m_board;
@ -310,7 +310,6 @@ void ChessWidget::mouseup_event(GUI::MouseEvent& event)
void ChessWidget::mousemove_event(GUI::MouseEvent& event) void ChessWidget::mousemove_event(GUI::MouseEvent& event)
{ {
int const min_size = min(width(), height()); int const min_size = min(width(), height());
int const widget_offset_x = (window()->width() - min_size) / 2;
int const widget_offset_y = (window()->height() - min_size) / 2; int const widget_offset_y = (window()->height() - min_size) / 2;
if (!frame_inner_rect().contains(event.position())) if (!frame_inner_rect().contains(event.position()))
@ -334,7 +333,7 @@ void ChessWidget::mousemove_event(GUI::MouseEvent& event)
return; return;
} }
m_drag_point = { event.position().x() - widget_offset_x, event.position().y() - widget_offset_y }; m_drag_point = { event.position().x(), event.position().y() - widget_offset_y };
update(); update();
} }
@ -399,10 +398,9 @@ void ChessWidget::set_piece_set(StringView set)
Optional<Chess::Square> ChessWidget::mouse_to_square(GUI::MouseEvent& event) const Optional<Chess::Square> ChessWidget::mouse_to_square(GUI::MouseEvent& event) const
{ {
int const min_size = min(width(), height()); int const min_size = min(width(), height());
int const widget_offset_x = (window()->width() - min_size) / 2;
int const widget_offset_y = (window()->height() - min_size) / 2; int const widget_offset_y = (window()->height() - min_size) / 2;
auto x = event.x() - widget_offset_x; auto x = event.x();
auto y = event.y() - widget_offset_y; auto y = event.y() - widget_offset_y;
if (x < 0 || y < 0 || x > min_size || y > min_size) if (x < 0 || y < 0 || x > min_size || y > min_size)
return {}; return {};
@ -433,6 +431,7 @@ void ChessWidget::reset()
m_playback_move_number = 0; m_playback_move_number = 0;
m_board_playback = Chess::Board(); m_board_playback = Chess::Board();
m_board = Chess::Board(); m_board = Chess::Board();
update_move_display_widget(m_board);
m_side = (get_random<u32>() % 2) ? Chess::Color::White : Chess::Color::Black; m_side = (get_random<u32>() % 2) ? Chess::Color::White : Chess::Color::Black;
m_drag_enabled = true; m_drag_enabled = true;
if (m_engine) if (m_engine)
@ -483,10 +482,10 @@ void ChessWidget::input_engine_move()
set_drag_enabled(drag_was_enabled); set_drag_enabled(drag_was_enabled);
if (!move.is_error()) { if (!move.is_error()) {
VERIFY(board().apply_move(move.release_value())); VERIFY(board().apply_move(move.release_value()));
update_move_display_widget(board());
if (check_game_over(ClaimDrawBehavior::Prompt)) if (check_game_over(ClaimDrawBehavior::Prompt))
return; return;
} }
m_playback_move_number = m_board.moves().size(); m_playback_move_number = m_board.moves().size();
m_playback = false; m_playback = false;
m_board_markings.clear(); m_board_markings.clear();
@ -509,6 +508,7 @@ void ChessWidget::playback_move(PlaybackDirection direction)
m_board_playback = Chess::Board(); m_board_playback = Chess::Board();
for (size_t i = 0; i < m_playback_move_number - 1; i++) for (size_t i = 0; i < m_playback_move_number - 1; i++)
m_board_playback.apply_move(m_board.moves().at(i)); m_board_playback.apply_move(m_board.moves().at(i));
update_move_display_widget(m_board_playback);
m_playback_move_number--; m_playback_move_number--;
break; break;
case PlaybackDirection::Forward: case PlaybackDirection::Forward:
@ -517,6 +517,7 @@ void ChessWidget::playback_move(PlaybackDirection direction)
return; return;
} }
m_board_playback.apply_move(m_board.moves().at(m_playback_move_number++)); m_board_playback.apply_move(m_board.moves().at(m_playback_move_number++));
update_move_display_widget(m_board_playback);
if (m_playback_move_number == m_board.moves().size()) if (m_playback_move_number == m_board.moves().size())
m_playback = false; m_playback = false;
break; break;
@ -535,6 +536,21 @@ void ChessWidget::playback_move(PlaybackDirection direction)
update(); update();
} }
void ChessWidget::update_move_display_widget(Chess::Board& board)
{
size_t turn = 1;
StringBuilder sb;
for (auto [i, move] : enumerate(board.moves())) {
if (i % 2 == 0) {
sb.append(MUST(String::formatted("{}. {}", turn, MUST(move.to_algebraic()))));
} else {
sb.append(MUST(String::formatted(" {}\n", MUST(move.to_algebraic()))));
turn++;
}
}
m_move_display_widget->set_text(sb.string_view());
}
ErrorOr<String> ChessWidget::get_fen() const ErrorOr<String> ChessWidget::get_fen() const
{ {
return TRY(m_playback ? m_board_playback.to_fen() : m_board.to_fen()); return TRY(m_playback ? m_board_playback.to_fen() : m_board.to_fen());
@ -740,6 +756,7 @@ ErrorOr<void, PGNParseError> ChessWidget::import_pgn(Core::File& file)
m_board_playback = m_board; m_board_playback = m_board;
m_playback_move_number = m_board_playback.moves().size(); m_playback_move_number = m_board_playback.moves().size();
m_playback = true; m_playback = true;
update_move_display_widget(m_board_playback);
update(); update();
return {}; return {};

View file

@ -16,6 +16,7 @@
#include <LibChess/Chess.h> #include <LibChess/Chess.h>
#include <LibConfig/Listener.h> #include <LibConfig/Listener.h>
#include <LibGUI/Frame.h> #include <LibGUI/Frame.h>
#include <LibGUI/TextEditor.h>
#include <LibGfx/Bitmap.h> #include <LibGfx/Bitmap.h>
namespace Chess { namespace Chess {
@ -81,6 +82,7 @@ public:
ErrorOr<String> get_fen() const; ErrorOr<String> get_fen() const;
ErrorOr<void, PGNParseError> import_pgn(Core::File&); ErrorOr<void, PGNParseError> import_pgn(Core::File&);
ErrorOr<void> export_pgn(Core::File&) const; ErrorOr<void> export_pgn(Core::File&) const;
void update_move_display_widget(Chess::Board&);
int resign(); int resign();
void flip_board(); void flip_board();
@ -106,6 +108,7 @@ public:
void playback_move(PlaybackDirection); void playback_move(PlaybackDirection);
void set_engine(RefPtr<Engine> engine) { m_engine = engine; } void set_engine(RefPtr<Engine> engine) { m_engine = engine; }
void set_move_display_widget(RefPtr<GUI::TextEditor> move_display_widget) { m_move_display_widget = move_display_widget; }
void input_engine_move(); void input_engine_move();
bool want_engine_move(); bool want_engine_move();
@ -174,6 +177,7 @@ private:
RefPtr<Engine> m_engine; RefPtr<Engine> m_engine;
bool m_coordinates { true }; bool m_coordinates { true };
bool m_highlight_checks { true }; bool m_highlight_checks { true };
RefPtr<GUI::TextEditor> m_move_display_widget;
}; };
} }

View file

@ -66,7 +66,10 @@ ErrorOr<int> serenity_main(Main::Arguments arguments)
auto window = GUI::Window::construct(); auto window = GUI::Window::construct();
auto main_widget = TRY(Chess::MainWidget::try_create()); auto main_widget = TRY(Chess::MainWidget::try_create());
auto& chess_widget = *main_widget->find_descendant_of_type_named<Chess::ChessWidget>("chess_widget"); auto& chess_widget = *main_widget->find_descendant_of_type_named<Chess::ChessWidget>("chess_widget");
auto& move_display_widget = *main_widget->find_descendant_of_type_named<GUI::TextEditor>("move_display_widget");
chess_widget.set_move_display_widget(move(move_display_widget));
window->set_main_widget(main_widget); window->set_main_widget(main_widget);
window->set_focused_widget(&chess_widget); window->set_focused_widget(&chess_widget);
@ -85,7 +88,7 @@ ErrorOr<int> serenity_main(Main::Arguments arguments)
window->set_title("Chess"); window->set_title("Chess");
window->set_base_size({ 4, 4 }); window->set_base_size({ 4, 4 });
window->set_size_increment({ 8, 8 }); window->set_size_increment({ 8, 8 });
window->resize(508, 508); window->resize(668, 508);
window->set_icon(app_icon.bitmap_for_size(16)); window->set_icon(app_icon.bitmap_for_size(16));