mirror of
https://github.com/RGBCube/serenity
synced 2025-07-27 05:17:35 +00:00
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.
This commit is contained in:
parent
999a44969d
commit
6a4e3d9002
4 changed files with 108 additions and 2 deletions
|
@ -2,6 +2,7 @@
|
||||||
* Copyright (c) 2020, Till Mayer <till.mayer@web.de>
|
* Copyright (c) 2020, Till Mayer <till.mayer@web.de>
|
||||||
* Copyright (c) 2021-2023, Sam Atkins <atkinssj@serenityos.org>
|
* Copyright (c) 2021-2023, Sam Atkins <atkinssj@serenityos.org>
|
||||||
* Copyright (c) 2022, the SerenityOS developers.
|
* Copyright (c) 2022, the SerenityOS developers.
|
||||||
|
* Copyright (c) 2023, David Ganz <david.g.ganz@gmail.com>
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: BSD-2-Clause
|
* SPDX-License-Identifier: BSD-2-Clause
|
||||||
*/
|
*/
|
||||||
|
@ -16,6 +17,7 @@ namespace Solitaire {
|
||||||
|
|
||||||
static constexpr uint8_t new_game_animation_delay = 2;
|
static constexpr uint8_t new_game_animation_delay = 2;
|
||||||
static constexpr int s_timer_interval_ms = 1000 / 60;
|
static constexpr int s_timer_interval_ms = 1000 / 60;
|
||||||
|
static constexpr int s_timer_solving_interval_ms = 100;
|
||||||
|
|
||||||
ErrorOr<NonnullRefPtr<Game>> Game::try_create()
|
ErrorOr<NonnullRefPtr<Game>> Game::try_create()
|
||||||
{
|
{
|
||||||
|
@ -100,6 +102,10 @@ void Game::timer_event(Core::TimerEvent&)
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case State::Solving: {
|
||||||
|
step_solve();
|
||||||
|
break;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -270,10 +276,16 @@ void Game::mousedown_event(GUI::MouseEvent& event)
|
||||||
start_timer_if_necessary();
|
start_timer_if_necessary();
|
||||||
update(top_card.rect());
|
update(top_card.rect());
|
||||||
remember_flip_for_undo(top_card);
|
remember_flip_for_undo(top_card);
|
||||||
|
|
||||||
|
if (on_move)
|
||||||
|
on_move();
|
||||||
}
|
}
|
||||||
} else if (!is_moving_cards()) {
|
} 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;
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
if (event.button() == GUI::MouseButton::Secondary) {
|
if (event.button() == GUI::MouseButton::Secondary) {
|
||||||
preview_card(to_check, click_location);
|
preview_card(to_check, click_location);
|
||||||
|
@ -316,6 +328,9 @@ void Game::mouseup_event(GUI::MouseEvent& event)
|
||||||
|
|
||||||
score_move(*moving_cards_source_stack(), stack);
|
score_move(*moving_cards_source_stack(), stack);
|
||||||
rebound = false;
|
rebound = false;
|
||||||
|
|
||||||
|
if (on_move)
|
||||||
|
on_move();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rebound) {
|
if (rebound) {
|
||||||
|
@ -400,6 +415,8 @@ void Game::check_for_game_over()
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (has_timer())
|
||||||
|
stop_timer();
|
||||||
start_game_over_animation();
|
start_game_over_animation();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -662,6 +679,50 @@ void Game::perform_undo()
|
||||||
invalidate_layout();
|
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()
|
void Game::clear_hovered_stack()
|
||||||
{
|
{
|
||||||
if (!m_hovered_stack)
|
if (!m_hovered_stack)
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
* Copyright (c) 2020, Till Mayer <till.mayer@web.de>
|
* Copyright (c) 2020, Till Mayer <till.mayer@web.de>
|
||||||
* Copyright (c) 2021-2023, Sam Atkins <atkinssj@serenityos.org>
|
* Copyright (c) 2021-2023, Sam Atkins <atkinssj@serenityos.org>
|
||||||
* Copyright (c) 2022, the SerenityOS developers.
|
* Copyright (c) 2022, the SerenityOS developers.
|
||||||
|
* Copyright (c) 2023, David Ganz <david.g.ganz@gmail.com>
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: BSD-2-Clause
|
* SPDX-License-Identifier: BSD-2-Clause
|
||||||
*/
|
*/
|
||||||
|
@ -45,10 +46,14 @@ public:
|
||||||
bool is_auto_collecting() const { return m_auto_collect; }
|
bool is_auto_collecting() const { return m_auto_collect; }
|
||||||
void set_auto_collect(bool collect) { m_auto_collect = collect; }
|
void set_auto_collect(bool collect) { m_auto_collect = collect; }
|
||||||
|
|
||||||
|
bool can_solve();
|
||||||
|
void start_solving();
|
||||||
|
|
||||||
Function<void(uint32_t)> on_score_update;
|
Function<void(uint32_t)> on_score_update;
|
||||||
Function<void()> on_game_start;
|
Function<void()> on_game_start;
|
||||||
Function<void(GameOverReason, uint32_t)> on_game_end;
|
Function<void(GameOverReason, uint32_t)> on_game_end;
|
||||||
Function<void(bool)> on_undo_availability_change;
|
Function<void(bool)> on_undo_availability_change;
|
||||||
|
Function<void()> on_move;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Game();
|
Game();
|
||||||
|
@ -188,6 +193,7 @@ private:
|
||||||
void check_for_game_over();
|
void check_for_game_over();
|
||||||
void clear_hovered_stack();
|
void clear_hovered_stack();
|
||||||
void deal_next_card();
|
void deal_next_card();
|
||||||
|
void step_solve();
|
||||||
|
|
||||||
virtual void paint_event(GUI::PaintEvent&) override;
|
virtual void paint_event(GUI::PaintEvent&) override;
|
||||||
virtual void mousedown_event(GUI::MouseEvent&) override;
|
virtual void mousedown_event(GUI::MouseEvent&) override;
|
||||||
|
@ -211,6 +217,7 @@ private:
|
||||||
GameInProgress,
|
GameInProgress,
|
||||||
StartGameOverAnimationNextFrame,
|
StartGameOverAnimationNextFrame,
|
||||||
GameOverAnimation,
|
GameOverAnimation,
|
||||||
|
Solving,
|
||||||
};
|
};
|
||||||
State m_state { State::WaitingForNewGame };
|
State m_state { State::WaitingForNewGame };
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,25 @@
|
||||||
fill_with_background_color: true
|
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 {
|
@GUI::Statusbar {
|
||||||
name: "statusbar"
|
name: "statusbar"
|
||||||
segment_count: 3
|
segment_count: 3
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
* Copyright (c) 2020, Till Mayer <till.mayer@web.de>
|
* Copyright (c) 2020, Till Mayer <till.mayer@web.de>
|
||||||
* Copyright (c) 2021, the SerenityOS developers.
|
* Copyright (c) 2021, the SerenityOS developers.
|
||||||
* Copyright (c) 2022-2023, Sam Atkins <atkinssj@serenityos.org>
|
* Copyright (c) 2022-2023, Sam Atkins <atkinssj@serenityos.org>
|
||||||
|
* Copyright (c) 2023, David Ganz <david.g.ganz@gmail.com>
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: BSD-2-Clause
|
* SPDX-License-Identifier: BSD-2-Clause
|
||||||
*/
|
*/
|
||||||
|
@ -90,6 +91,16 @@ ErrorOr<int> serenity_main(Main::Arguments arguments)
|
||||||
auto& game = *widget->find_descendant_of_type_named<Solitaire::Game>("game");
|
auto& game = *widget->find_descendant_of_type_named<Solitaire::Game>("game");
|
||||||
game.set_focus(true);
|
game.set_focus(true);
|
||||||
|
|
||||||
|
auto& action_bar = *widget->find_descendant_of_type_named<GUI::Widget>("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<GUI::Button>("solve_button");
|
||||||
|
solve_button.on_click = [&](auto) {
|
||||||
|
game.start_solving();
|
||||||
|
solve_button.set_enabled(false);
|
||||||
|
};
|
||||||
|
|
||||||
auto& statusbar = *widget->find_descendant_of_type_named<GUI::Statusbar>("statusbar");
|
auto& statusbar = *widget->find_descendant_of_type_named<GUI::Statusbar>("statusbar");
|
||||||
statusbar.set_text(0, "Score: 0"_string);
|
statusbar.set_text(0, "Score: 0"_string);
|
||||||
statusbar.set_text(1, TRY(String::formatted("High Score: {}", high_score())));
|
statusbar.set_text(1, TRY(String::formatted("High Score: {}", high_score())));
|
||||||
|
@ -115,14 +126,22 @@ ErrorOr<int> serenity_main(Main::Arguments arguments)
|
||||||
}));
|
}));
|
||||||
|
|
||||||
game.on_game_start = [&]() {
|
game.on_game_start = [&]() {
|
||||||
|
solve_button.set_enabled(false);
|
||||||
|
action_bar.set_visible(false);
|
||||||
seconds_elapsed = 0;
|
seconds_elapsed = 0;
|
||||||
timer->start();
|
timer->start();
|
||||||
statusbar.set_text(2, "Time: 00:00"_string);
|
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) {
|
game.on_game_end = [&](Solitaire::GameOverReason reason, uint32_t score) {
|
||||||
if (timer->is_active())
|
if (timer->is_active())
|
||||||
timer->stop();
|
timer->stop();
|
||||||
|
|
||||||
|
solve_button.set_enabled(false);
|
||||||
|
|
||||||
if (reason == Solitaire::GameOverReason::Victory) {
|
if (reason == Solitaire::GameOverReason::Victory) {
|
||||||
if (seconds_elapsed >= 30) {
|
if (seconds_elapsed >= 30) {
|
||||||
uint32_t bonus = (20'000 / seconds_elapsed) * 35;
|
uint32_t bonus = (20'000 / seconds_elapsed) * 35;
|
||||||
|
@ -228,7 +247,7 @@ ErrorOr<int> serenity_main(Main::Arguments arguments)
|
||||||
help_menu->add_action(GUI::CommonActions::make_about_action("Solitaire"_string, app_icon, window));
|
help_menu->add_action(GUI::CommonActions::make_about_action("Solitaire"_string, app_icon, window));
|
||||||
|
|
||||||
window->set_resizable(false);
|
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->set_icon(app_icon.bitmap_for_size(16));
|
||||||
window->show();
|
window->show();
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue