Chess: Add time controls

This commit adds functionality to the new game dialog to allow time
controls to be selected. The game logic is updated to take into account
losing on time, time bonuses for moves, and exporting the time control
to PGN.
This commit is contained in:
dgaston 2024-05-27 20:39:19 -04:00 committed by Nico Weber
parent 215d348600
commit edb10aa17a
7 changed files with 275 additions and 15 deletions

View file

@ -30,6 +30,20 @@
mode: "DisplayOnly"
focus_policy: "NoFocus"
}
@GUI::Label {
text_alignment: "CenterLeft"
font_weight: "Bold"
autosize: true
name: "white_time_label"
}
@GUI::Label {
text_alignment: "CenterLeft"
font_weight: "Bold"
autosize: true
name: "black_time_label"
}
}
}
}

View file

@ -11,6 +11,7 @@
#include "PromotionDialog.h"
#include <AK/Enumerate.h>
#include <AK/GenericLexer.h>
#include <AK/NumberFormat.h>
#include <AK/Random.h>
#include <AK/String.h>
#include <LibCore/Account.h>
@ -293,8 +294,17 @@ void ChessWidget::mouseup_event(GUI::MouseEvent& event)
move.promote_to = promotion_dialog->selected_piece();
}
if (board().moves().size() == 0) {
if (!m_timer->is_active() && !m_unlimited_time_control) {
m_timer->start();
}
}
if (board().apply_move(move)) {
update_move_display_widget(m_board);
if (!m_unlimited_time_control) {
apply_increment(move);
}
m_playback_move_number = board().moves().size();
m_playback = false;
m_board_playback = m_board;
@ -434,9 +444,19 @@ void ChessWidget::reset()
update_move_display_widget(m_board);
m_side = (get_random<u32>() % 2) ? Chess::Color::White : Chess::Color::Black;
m_drag_enabled = true;
m_white_time_elapsed = 0;
m_black_time_elapsed = 0;
m_timer->stop();
if (m_engine)
m_engine->start_new_game();
if (m_unlimited_time_control) {
m_white_time_label->set_text("White time: -"_string);
m_black_time_label->set_text("Black time: -"_string);
} else {
update_time_labels(m_time_control_seconds, m_time_control_seconds);
}
input_engine_move();
update();
}
@ -474,6 +494,12 @@ void ChessWidget::input_engine_move()
if (drag_was_enabled)
set_drag_enabled(false);
if (board().moves().size() == 0) {
if (!m_timer->is_active() && !m_unlimited_time_control) {
m_timer->start();
}
}
set_override_cursor(Gfx::StandardCursor::Wait);
m_engine->get_best_move(board(), 4000, [this, drag_was_enabled](ErrorOr<Chess::Move> move) {
set_override_cursor(Gfx::StandardCursor::None);
@ -483,6 +509,9 @@ void ChessWidget::input_engine_move()
if (!move.is_error()) {
VERIFY(board().apply_move(move.release_value()));
update_move_display_widget(board());
if (!m_unlimited_time_control) {
apply_increment(move.release_value());
}
if (check_game_over(ClaimDrawBehavior::Prompt))
return;
}
@ -783,7 +812,11 @@ ErrorOr<void> ChessWidget::export_pgn(Core::File& file) const
TRY(file.write_until_depleted("[WhiteElo \"?\"]\n"sv.bytes()));
TRY(file.write_until_depleted("[BlackElo \"?\"]\n"sv.bytes()));
TRY(file.write_until_depleted("[Variant \"Standard\"]\n"sv.bytes()));
TRY(file.write_until_depleted("[TimeControl \"-\"]\n"sv.bytes()));
if (m_unlimited_time_control) {
TRY(file.write_until_depleted("[TimeControl \"-\"]\n"sv.bytes()));
} else {
TRY(file.write_until_depleted(TRY(String::formatted("[TimeControl \"{}+{}\"]\n", m_time_control_seconds, m_time_control_increment))));
}
TRY(file.write_until_depleted("[Annotator \"SerenityOS Chess\"]\n"sv.bytes()));
TRY(file.write_until_depleted("\n"sv.bytes()));
@ -837,6 +870,7 @@ int ChessWidget::resign()
board().set_resigned(m_board.turn());
set_drag_enabled(false);
m_timer->stop();
update();
auto const msg = Chess::Board::result_to_string(m_board.game_result(), m_board.turn());
GUI::MessageBox::show(window(), msg, "Game Over"sv, GUI::MessageBox::Type::Information);
@ -844,6 +878,59 @@ int ChessWidget::resign()
return 0;
}
void ChessWidget::initialize_timer()
{
m_timer = Core::Timer::create_repeating(
1000, [this] {
// FIXME: Look into using AK/Timer methods to calculate elapsed time from
// start for greater accuracy.
auto white_time = m_time_control_seconds - m_white_time_elapsed;
auto black_time = m_time_control_seconds - m_black_time_elapsed;
if (m_board.turn() == Chess::Color::White) {
m_white_time_elapsed++;
update_time_labels(m_time_control_seconds - m_white_time_elapsed, black_time);
check_resign_on_time("White time out. Black wins."sv);
} else if (m_board.turn() == Chess::Color::Black) {
m_black_time_elapsed++;
update_time_labels(white_time, m_time_control_seconds - m_black_time_elapsed);
check_resign_on_time("Black time out. White wins."sv);
}
},
this);
}
void ChessWidget::apply_increment(Chess::Move move)
{
if (move.piece.color == Chess::Color::White) {
m_white_time_elapsed -= m_time_control_increment;
} else {
m_black_time_elapsed -= m_time_control_increment;
}
update_time_labels(m_time_control_seconds - m_white_time_elapsed, m_time_control_seconds - m_black_time_elapsed);
}
void ChessWidget::update_time_labels(u32 white_time, u32 black_time)
{
m_white_time_label->set_text(MUST(String::formatted("White time: {}", human_readable_digital_time(white_time))));
m_black_time_label->set_text(MUST(String::formatted("Black time: {}", human_readable_digital_time(black_time))));
}
void ChessWidget::check_resign_on_time(StringView msg)
{
if (m_white_time_elapsed >= m_time_control_seconds) {
m_board.set_resigned(Chess::Color::White);
} else if (m_black_time_elapsed >= m_time_control_seconds) {
m_board.set_resigned(Chess::Color::Black);
} else {
return;
}
m_timer->stop();
set_override_cursor(Gfx::StandardCursor::None);
set_drag_enabled(false);
update();
GUI::MessageBox::show(window(), msg, "Game Over"sv, GUI::MessageBox::Type::Information);
}
bool ChessWidget::check_game_over(ClaimDrawBehavior claim_draw_behavior)
{
if (board().game_result() == Chess::Board::Result::NotFinished)
@ -879,6 +966,7 @@ bool ChessWidget::check_game_over(ClaimDrawBehavior claim_draw_behavior)
set_override_cursor(Gfx::StandardCursor::None);
set_drag_enabled(false);
m_timer->stop();
update();
auto msg = Chess::Board::result_to_string(board().game_result(), board().turn());
GUI::MessageBox::show(window(), msg, "Game Over"sv, GUI::MessageBox::Type::Information);

View file

@ -16,6 +16,7 @@
#include <LibChess/Chess.h>
#include <LibConfig/Listener.h>
#include <LibGUI/Frame.h>
#include <LibGUI/Label.h>
#include <LibGUI/TextEditor.h>
#include <LibGfx/Bitmap.h>
@ -109,6 +110,8 @@ public:
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 set_white_time_label(RefPtr<GUI::Label> time_label) { m_white_time_label = time_label; }
void set_black_time_label(RefPtr<GUI::Label> time_label) { m_black_time_label = time_label; }
void input_engine_move();
bool want_engine_move();
@ -119,6 +122,18 @@ public:
void set_highlight_checks(bool highlight_checks) { m_highlight_checks = highlight_checks; }
bool highlight_checks() const { return m_highlight_checks; }
void set_unlimited_time_control(bool unlimited) { m_unlimited_time_control = unlimited; }
bool unlimited_time_control() const { return m_unlimited_time_control; }
void set_time_control_seconds(i32 seconds) { m_time_control_seconds = seconds; }
i32 time_control_seconds() const { return m_time_control_seconds; }
void set_time_control_increment(i32 seconds) { m_time_control_increment = seconds; }
i32 time_control_increment() const { return m_time_control_increment; }
void initialize_timer();
void update_time_labels(u32 white_time, u32 black_time);
struct BoardMarking {
Chess::Square from { 50, 50 };
Chess::Square to { 50, 50 };
@ -153,6 +168,8 @@ private:
virtual void config_bool_did_change(StringView domain, StringView group, StringView key, bool value) override;
bool check_game_over(ClaimDrawBehavior);
void check_resign_on_time(StringView msg);
void apply_increment(Chess::Move move);
Chess::Board m_board;
Chess::Board m_board_playback;
@ -178,6 +195,14 @@ private:
bool m_coordinates { true };
bool m_highlight_checks { true };
RefPtr<GUI::TextEditor> m_move_display_widget;
RefPtr<GUI::Label> m_white_time_label;
RefPtr<GUI::Label> m_black_time_label;
bool m_unlimited_time_control { true };
i32 m_time_control_seconds { 0 };
i32 m_time_control_increment { 0 };
i32 m_white_time_elapsed { 0 };
i32 m_black_time_elapsed { 0 };
RefPtr<Core::Timer> m_timer;
};
}

View file

@ -5,34 +5,68 @@
*/
#include "NewGameDialog.h"
#include <LibGUI/BoxLayout.h>
#include <LibGUI/Button.h>
#include <LibGUI/Frame.h>
#include <LibGUI/CheckBox.h>
#include <LibGUI/SpinBox.h>
namespace Chess {
ErrorOr<NonnullRefPtr<NewGameDialog>> NewGameDialog::try_create(GUI::Window* parent_window)
ErrorOr<NonnullRefPtr<NewGameDialog>> NewGameDialog::try_create(GUI::Window* parent_window, bool unlimited_time_control, i32 time_control_seconds, i32 time_control_increment)
{
auto new_game_widget = TRY(Chess::NewGameWidget::try_create());
auto new_game_dialog = TRY(adopt_nonnull_ref_or_enomem(new (nothrow) NewGameDialog(move(new_game_widget), move(parent_window))));
auto new_game_dialog = TRY(adopt_nonnull_ref_or_enomem(new (nothrow) NewGameDialog(move(new_game_widget), move(parent_window), unlimited_time_control, time_control_seconds, time_control_increment)));
return new_game_dialog;
}
NewGameDialog::NewGameDialog(NonnullRefPtr<Chess::NewGameWidget> new_game_widget, GUI::Window* parent_window)
NewGameDialog::NewGameDialog(NonnullRefPtr<Chess::NewGameWidget> new_game_widget, GUI::Window* parent_window, bool unlimited_time_control, i32 time_control_seconds, i32 time_control_increment)
: GUI::Dialog(parent_window)
, m_unlimited_time_control(unlimited_time_control)
, m_time_control_seconds(time_control_seconds)
, m_time_control_increment(time_control_increment)
{
set_title("New Game");
set_main_widget(new_game_widget);
m_minutes_spinbox_value = m_time_control_seconds / 60;
m_seconds_spinbox_value = m_time_control_seconds % 60;
m_minutes_spinbox = new_game_widget->find_descendant_of_type_named<GUI::SpinBox>("minutes_spinbox");
m_minutes_spinbox->set_value(m_minutes_spinbox_value);
m_minutes_spinbox->on_change = [&](auto value) {
m_minutes_spinbox_value = value;
m_time_control_seconds = m_minutes_spinbox_value * 60 + m_seconds_spinbox_value;
};
m_seconds_spinbox = new_game_widget->find_descendant_of_type_named<GUI::SpinBox>("seconds_spinbox");
m_seconds_spinbox->set_value(m_seconds_spinbox_value);
m_seconds_spinbox->on_change = [&](auto value) {
m_seconds_spinbox_value = value;
m_time_control_seconds = m_minutes_spinbox_value * 60 + m_seconds_spinbox_value;
};
m_increment_spinbox = new_game_widget->find_descendant_of_type_named<GUI::SpinBox>("increment_spinbox");
m_increment_spinbox->set_value(m_time_control_increment);
m_increment_spinbox->on_change = [&](auto value) {
m_time_control_increment = value;
};
auto unlimited_checkbox = new_game_widget->find_descendant_of_type_named<GUI::CheckBox>("unlimited_time_control");
unlimited_checkbox->set_checked(m_unlimited_time_control);
unlimited_checkbox->on_checked = [&](bool checked) {
m_unlimited_time_control = checked;
m_minutes_spinbox->set_enabled(!checked);
m_seconds_spinbox->set_enabled(!checked);
m_increment_spinbox->set_enabled(!checked);
};
m_minutes_spinbox->set_enabled(!m_unlimited_time_control);
m_seconds_spinbox->set_enabled(!m_unlimited_time_control);
m_increment_spinbox->set_enabled(!m_unlimited_time_control);
auto start_button = new_game_widget->find_descendant_of_type_named<GUI::Button>("start_button");
start_button->on_click = [this](auto) {
done(ExecResult::OK);
};
}
void NewGameDialog::event(Core::Event& event)
{
GUI::Dialog::event(event);
}
}

View file

@ -15,11 +15,23 @@ namespace Chess {
class NewGameDialog final : public GUI::Dialog {
C_OBJECT_ABSTRACT(NewGameDialog)
public:
static ErrorOr<NonnullRefPtr<NewGameDialog>> try_create(GUI::Window* parent_window);
static ErrorOr<NonnullRefPtr<NewGameDialog>> try_create(GUI::Window* parent_window, bool unlimited_time_control, i32 time_control_seconds, i32 time_control_increment);
bool unlimited_time_control() { return m_unlimited_time_control; }
i32 time_control_seconds() { return m_time_control_seconds; }
i32 time_control_increment() { return m_time_control_increment; }
private:
NewGameDialog(NonnullRefPtr<Chess::NewGameWidget> new_game_widget_widget, GUI::Window* parent_window);
virtual void event(Core::Event&) override;
NewGameDialog(NonnullRefPtr<Chess::NewGameWidget> new_game_widget_widget, GUI::Window* parent_window, bool unlimited_time_control, i32 time_control_seconds, i32 time_control_increment);
bool m_unlimited_time_control;
i32 m_time_control_seconds;
i32 m_time_control_increment;
i32 m_minutes_spinbox_value;
i32 m_seconds_spinbox_value;
RefPtr<GUI::SpinBox> m_minutes_spinbox;
RefPtr<GUI::SpinBox> m_seconds_spinbox;
RefPtr<GUI::SpinBox> m_increment_spinbox;
};
}

View file

@ -4,11 +4,82 @@
margins: [8]
}
@GUI::GroupBox {
title: "Time Controls"
preferred_height: "fit"
min_width: 250
layout: @GUI::VerticalBoxLayout {
margins: [8]
}
@GUI::CheckBox {
name: "unlimited_time_control"
text: "Unlimited"
checkbox_position: "Right"
}
@GUI::Widget {
layout: @GUI::HorizontalBoxLayout {
spacing: 16
}
@GUI::Label {
text: "Minutes:"
text_alignment: "CenterLeft"
}
@GUI::SpinBox {
name: "minutes_spinbox"
min: 0
max: 180
fixed_width: 50
}
}
@GUI::Widget {
layout: @GUI::HorizontalBoxLayout {
spacing: 16
}
@GUI::Label {
text: "Seconds:"
text_alignment: "CenterLeft"
}
@GUI::SpinBox {
name: "seconds_spinbox"
min: 0
max: 59
fixed_width: 50
}
}
@GUI::Widget {
layout: @GUI::HorizontalBoxLayout {
spacing: 16
}
@GUI::Label {
text: "Increment in seconds:"
text_alignment: "CenterLeft"
}
@GUI::SpinBox {
name: "increment_spinbox"
min: 0
max: 180
fixed_width: 50
}
}
}
@GUI::Widget {
layout: @GUI::HorizontalBoxLayout {
spacing: 10
}
@GUI::Layout::Spacer {}
@GUI::Button {
name: "start_button"
text: "Start"

View file

@ -71,6 +71,10 @@ ErrorOr<int> serenity_main(Main::Arguments arguments)
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));
auto& white_time_label = *main_widget->find_descendant_of_type_named<GUI::Label>("white_time_label");
chess_widget.set_white_time_label(move(white_time_label));
auto& black_time_label = *main_widget->find_descendant_of_type_named<GUI::Label>("black_time_label");
chess_widget.set_black_time_label(move(black_time_label));
window->set_main_widget(main_widget);
window->set_focused_widget(&chess_widget);
@ -98,6 +102,10 @@ ErrorOr<int> serenity_main(Main::Arguments arguments)
chess_widget.set_coordinates(Config::read_bool("Games"sv, "Chess"sv, "ShowCoordinates"sv, true));
chess_widget.set_show_available_moves(Config::read_bool("Games"sv, "Chess"sv, "ShowAvailableMoves"sv, true));
chess_widget.set_highlight_checks(Config::read_bool("Games"sv, "Chess"sv, "HighlightChecks"sv, true));
chess_widget.set_unlimited_time_control(Config::read_bool("Games"sv, "Chess"sv, "UnlimitedTimeControl"sv, true));
chess_widget.set_time_control_seconds(Config::read_i32("Games"sv, "Chess"sv, "TimeControlSeconds"sv, 300));
chess_widget.set_time_control_increment(Config::read_i32("Games"sv, "Chess"sv, "TimeControlIncrement"sv, 3));
chess_widget.initialize_timer();
auto game_menu = window->add_menu("&Game"_string);
@ -149,7 +157,8 @@ ErrorOr<int> serenity_main(Main::Arguments arguments)
if (chess_widget.resign() < 0)
return;
}
auto new_game_dialog_or_error = Chess::NewGameDialog::try_create(window);
auto new_game_dialog_or_error = Chess::NewGameDialog::try_create(window, chess_widget.unlimited_time_control(), chess_widget.time_control_seconds(), chess_widget.time_control_increment());
if (new_game_dialog_or_error.is_error()) {
GUI::MessageBox::show(window, "Failed to load the new game window"sv, "Unable to Open New Game Dialog"sv, GUI::MessageBox::Type::Error);
return;
@ -159,6 +168,13 @@ ErrorOr<int> serenity_main(Main::Arguments arguments)
if (new_game_dialog->exec() != GUI::Dialog::ExecResult::OK)
return;
chess_widget.set_unlimited_time_control(new_game_dialog->unlimited_time_control());
chess_widget.set_time_control_seconds(new_game_dialog->time_control_seconds());
chess_widget.set_time_control_increment(new_game_dialog->time_control_increment());
Config::write_bool("Games"sv, "Chess"sv, "UnlimitedTimeControl"sv, new_game_dialog->unlimited_time_control());
Config::write_i32("Games"sv, "Chess"sv, "TimeControlSeconds"sv, new_game_dialog->time_control_seconds());
Config::write_i32("Games"sv, "Chess"sv, "TimeControlIncrement"sv, new_game_dialog->time_control_increment());
chess_widget.reset();
}));
game_menu->add_separator();