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();