From 6a4e3d90023b529c70e297201156530187fda4a1 Mon Sep 17 00:00:00 2001 From: david072 Date: Sat, 11 Nov 2023 13:41:49 +0100 Subject: [PATCH] Solitaire: Ability to automatically solve the end of the game In Solitaire, when the stock stack is empty and all cards have been revealed, finishing the game is trivial, since you only need to sort the already visible cards onto the foundation stacks. To simplify this, the game will now give you the option to finish the game automatically if these conditions are met. It then sorts the cards with a delay of 100ms between each card. --- Userland/Games/Solitaire/Game.cpp | 63 +++++++++++++++++++++++++- Userland/Games/Solitaire/Game.h | 7 +++ Userland/Games/Solitaire/Solitaire.gml | 19 ++++++++ Userland/Games/Solitaire/main.cpp | 21 ++++++++- 4 files changed, 108 insertions(+), 2 deletions(-) diff --git a/Userland/Games/Solitaire/Game.cpp b/Userland/Games/Solitaire/Game.cpp index 66ce4fc12c..66ac1d827e 100644 --- a/Userland/Games/Solitaire/Game.cpp +++ b/Userland/Games/Solitaire/Game.cpp @@ -2,6 +2,7 @@ * Copyright (c) 2020, Till Mayer * Copyright (c) 2021-2023, Sam Atkins * Copyright (c) 2022, the SerenityOS developers. + * Copyright (c) 2023, David Ganz * * SPDX-License-Identifier: BSD-2-Clause */ @@ -16,6 +17,7 @@ namespace Solitaire { static constexpr uint8_t new_game_animation_delay = 2; static constexpr int s_timer_interval_ms = 1000 / 60; +static constexpr int s_timer_solving_interval_ms = 100; ErrorOr> Game::try_create() { @@ -100,6 +102,10 @@ void Game::timer_event(Core::TimerEvent&) } break; } + case State::Solving: { + step_solve(); + break; + } default: break; } @@ -270,10 +276,16 @@ void Game::mousedown_event(GUI::MouseEvent& event) start_timer_if_necessary(); update(top_card.rect()); remember_flip_for_undo(top_card); + + if (on_move) + on_move(); } } else if (!is_moving_cards()) { - if (is_auto_collecting() && attempt_to_move_card_to_foundations(to_check)) + if (is_auto_collecting() && attempt_to_move_card_to_foundations(to_check)) { + if (on_move) + on_move(); break; + } if (event.button() == GUI::MouseButton::Secondary) { preview_card(to_check, click_location); @@ -316,6 +328,9 @@ void Game::mouseup_event(GUI::MouseEvent& event) score_move(*moving_cards_source_stack(), stack); rebound = false; + + if (on_move) + on_move(); } if (rebound) { @@ -400,6 +415,8 @@ void Game::check_for_game_over() return; } + if (has_timer()) + stop_timer(); start_game_over_animation(); } @@ -662,6 +679,50 @@ void Game::perform_undo() invalidate_layout(); } +bool Game::can_solve() +{ + if (m_state != State::GameInProgress) + return false; + + for (auto const& stack : stacks()) { + switch (stack->type()) { + case Cards::CardStack::Type::Waste: + case Cards::CardStack::Type::Stock: + if (!stack->is_empty()) + return false; + break; + case Cards::CardStack::Type::Normal: + if (!stack->is_empty() && stack->stack().first()->is_upside_down()) + return false; + break; + default: + break; + } + } + + return true; +} + +void Game::start_solving() +{ + if (!can_solve()) + return; + + m_state = State::Solving; + start_timer(s_timer_solving_interval_ms); +} + +void Game::step_solve() +{ + for (auto& stack : stacks()) { + if (stack->type() != Cards::CardStack::Type::Normal) + continue; + + if (attempt_to_move_card_to_foundations(stack)) + break; + } +} + void Game::clear_hovered_stack() { if (!m_hovered_stack) diff --git a/Userland/Games/Solitaire/Game.h b/Userland/Games/Solitaire/Game.h index a0a885289f..140efa760e 100644 --- a/Userland/Games/Solitaire/Game.h +++ b/Userland/Games/Solitaire/Game.h @@ -2,6 +2,7 @@ * Copyright (c) 2020, Till Mayer * Copyright (c) 2021-2023, Sam Atkins * Copyright (c) 2022, the SerenityOS developers. + * Copyright (c) 2023, David Ganz * * SPDX-License-Identifier: BSD-2-Clause */ @@ -45,10 +46,14 @@ public: bool is_auto_collecting() const { return m_auto_collect; } void set_auto_collect(bool collect) { m_auto_collect = collect; } + bool can_solve(); + void start_solving(); + Function on_score_update; Function on_game_start; Function on_game_end; Function on_undo_availability_change; + Function on_move; private: Game(); @@ -188,6 +193,7 @@ private: void check_for_game_over(); void clear_hovered_stack(); void deal_next_card(); + void step_solve(); virtual void paint_event(GUI::PaintEvent&) override; virtual void mousedown_event(GUI::MouseEvent&) override; @@ -211,6 +217,7 @@ private: GameInProgress, StartGameOverAnimationNextFrame, GameOverAnimation, + Solving, }; State m_state { State::WaitingForNewGame }; diff --git a/Userland/Games/Solitaire/Solitaire.gml b/Userland/Games/Solitaire/Solitaire.gml index 72e39f0faf..0e13de342f 100644 --- a/Userland/Games/Solitaire/Solitaire.gml +++ b/Userland/Games/Solitaire/Solitaire.gml @@ -7,6 +7,25 @@ fill_with_background_color: true } + @GUI::Frame { + name: "game_action_bar" + fill_with_background_color: true + fixed_height: 32 + layout: @GUI::HorizontalBoxLayout { + margins: [3] + } + + @GUI::Layout::Spacer {} + + @GUI::Button { + name: "solve_button" + text: "Solve" + fixed_width: 80 + } + + @GUI::Layout::Spacer {} + } + @GUI::Statusbar { name: "statusbar" segment_count: 3 diff --git a/Userland/Games/Solitaire/main.cpp b/Userland/Games/Solitaire/main.cpp index 963a2b0dba..86c0569d93 100644 --- a/Userland/Games/Solitaire/main.cpp +++ b/Userland/Games/Solitaire/main.cpp @@ -2,6 +2,7 @@ * Copyright (c) 2020, Till Mayer * Copyright (c) 2021, the SerenityOS developers. * Copyright (c) 2022-2023, Sam Atkins + * Copyright (c) 2023, David Ganz * * SPDX-License-Identifier: BSD-2-Clause */ @@ -90,6 +91,16 @@ ErrorOr serenity_main(Main::Arguments arguments) auto& game = *widget->find_descendant_of_type_named("game"); game.set_focus(true); + auto& action_bar = *widget->find_descendant_of_type_named("game_action_bar"); + action_bar.set_background_color(game.background_color()); + action_bar.set_visible(false); + + auto& solve_button = *action_bar.find_descendant_of_type_named("solve_button"); + solve_button.on_click = [&](auto) { + game.start_solving(); + solve_button.set_enabled(false); + }; + auto& statusbar = *widget->find_descendant_of_type_named("statusbar"); statusbar.set_text(0, "Score: 0"_string); statusbar.set_text(1, TRY(String::formatted("High Score: {}", high_score()))); @@ -115,14 +126,22 @@ ErrorOr serenity_main(Main::Arguments arguments) })); game.on_game_start = [&]() { + solve_button.set_enabled(false); + action_bar.set_visible(false); seconds_elapsed = 0; timer->start(); statusbar.set_text(2, "Time: 00:00"_string); }; + game.on_move = [&]() { + solve_button.set_enabled(true); + action_bar.set_visible(game.can_solve()); + }; game.on_game_end = [&](Solitaire::GameOverReason reason, uint32_t score) { if (timer->is_active()) timer->stop(); + solve_button.set_enabled(false); + if (reason == Solitaire::GameOverReason::Victory) { if (seconds_elapsed >= 30) { uint32_t bonus = (20'000 / seconds_elapsed) * 35; @@ -228,7 +247,7 @@ ErrorOr serenity_main(Main::Arguments arguments) help_menu->add_action(GUI::CommonActions::make_about_action("Solitaire"_string, app_icon, window)); window->set_resizable(false); - window->resize(Solitaire::Game::width, Solitaire::Game::height + statusbar.max_height().as_int()); + window->resize(Solitaire::Game::width, Solitaire::Game::height + statusbar.max_height().as_int() + action_bar.height()); window->set_icon(app_icon.bitmap_for_size(16)); window->show();