Serenity Operating System
1/*
2 * Copyright (c) 2020, Till Mayer <till.mayer@web.de>
3 * Copyright (c) 2021-2023, Sam Atkins <atkinssj@serenityos.org>
4 * Copyright (c) 2022, the SerenityOS developers.
5 *
6 * SPDX-License-Identifier: BSD-2-Clause
7 */
8
9#include "Game.h"
10#include <AK/Debug.h>
11#include <AK/Random.h>
12#include <LibGUI/Painter.h>
13#include <LibGfx/Palette.h>
14
15REGISTER_WIDGET(Solitaire, Game);
16
17namespace Solitaire {
18
19static constexpr uint8_t new_game_animation_delay = 2;
20static constexpr int s_timer_interval_ms = 1000 / 60;
21
22ErrorOr<NonnullRefPtr<Game>> Game::try_create()
23{
24 auto game = TRY(adopt_nonnull_ref_or_enomem(new (nothrow) Game()));
25
26 TRY(game->add_stack(Gfx::IntPoint { 10, 10 }, CardStack::Type::Stock));
27 TRY(game->add_stack(Gfx::IntPoint { 10 + Card::width + 10, 10 }, CardStack::Type::Waste));
28 TRY(game->add_stack(Gfx::IntPoint { 10 + Card::width + 10, 10 }, CardStack::Type::Play, game->stack_at_location(Waste)));
29 TRY(game->add_stack(Gfx::IntPoint { Game::width - 4 * Card::width - 40, 10 }, CardStack::Type::Foundation));
30 TRY(game->add_stack(Gfx::IntPoint { Game::width - 3 * Card::width - 30, 10 }, CardStack::Type::Foundation));
31 TRY(game->add_stack(Gfx::IntPoint { Game::width - 2 * Card::width - 20, 10 }, CardStack::Type::Foundation));
32 TRY(game->add_stack(Gfx::IntPoint { Game::width - Card::width - 10, 10 }, CardStack::Type::Foundation));
33 TRY(game->add_stack(Gfx::IntPoint { 10, 10 + Card::height + 10 }, CardStack::Type::Normal));
34 TRY(game->add_stack(Gfx::IntPoint { 10 + Card::width + 10, 10 + Card::height + 10 }, CardStack::Type::Normal));
35 TRY(game->add_stack(Gfx::IntPoint { 10 + 2 * Card::width + 20, 10 + Card::height + 10 }, CardStack::Type::Normal));
36 TRY(game->add_stack(Gfx::IntPoint { 10 + 3 * Card::width + 30, 10 + Card::height + 10 }, CardStack::Type::Normal));
37 TRY(game->add_stack(Gfx::IntPoint { 10 + 4 * Card::width + 40, 10 + Card::height + 10 }, CardStack::Type::Normal));
38 TRY(game->add_stack(Gfx::IntPoint { 10 + 5 * Card::width + 50, 10 + Card::height + 10 }, CardStack::Type::Normal));
39 TRY(game->add_stack(Gfx::IntPoint { 10 + 6 * Card::width + 60, 10 + Card::height + 10 }, CardStack::Type::Normal));
40
41 return game;
42}
43
44Game::Game() = default;
45
46static float rand_float()
47{
48 return get_random_uniform(RAND_MAX) / static_cast<float>(RAND_MAX);
49}
50
51void Game::deal_next_card()
52{
53 VERIFY(m_state == State::NewGameAnimation);
54
55 auto& current_pile = stack_at_location(piles.at(m_new_game_animation_pile));
56
57 if (current_pile.count() < m_new_game_animation_pile) {
58 auto card = m_new_deck.take_last();
59 card->set_upside_down(true);
60 current_pile.push(card).release_value_but_fixme_should_propagate_errors();
61 } else {
62 current_pile.push(m_new_deck.take_last()).release_value_but_fixme_should_propagate_errors();
63 ++m_new_game_animation_pile;
64 }
65
66 update(current_pile.bounding_box());
67
68 if (m_new_game_animation_pile == piles.size()) {
69 auto& stock_pile = stack_at_location(Stock);
70 while (!m_new_deck.is_empty())
71 stock_pile.push(m_new_deck.take_last()).release_value_but_fixme_should_propagate_errors();
72
73 update(stock_pile.bounding_box());
74
75 m_state = State::WaitingForNewGame;
76 stop_timer();
77 }
78}
79
80void Game::timer_event(Core::TimerEvent&)
81{
82 switch (m_state) {
83 case State::StartGameOverAnimationNextFrame: {
84 m_state = State::GameOverAnimation;
85 set_background_fill_enabled(false);
86 break;
87 }
88 case State::GameOverAnimation: {
89 if (m_animation.position().x() >= Game::width || m_animation.card_rect().right() <= 0)
90 create_new_animation_card();
91
92 if (m_animation.tick())
93 update(m_animation.card_rect());
94 break;
95 }
96 case State::NewGameAnimation: {
97 if (m_new_game_animation_delay < new_game_animation_delay) {
98 ++m_new_game_animation_delay;
99 } else {
100 m_new_game_animation_delay = 0;
101 deal_next_card();
102 }
103 break;
104 }
105 default:
106 break;
107 }
108}
109
110void Game::create_new_animation_card()
111{
112 auto suit = static_cast<Cards::Suit>(get_random_uniform(to_underlying(Cards::Suit::__Count)));
113 auto rank = static_cast<Cards::Rank>(get_random_uniform(to_underlying(Cards::Rank::__Count)));
114 Gfx::IntPoint position { get_random_uniform(Game::width - Card::width), get_random_uniform(Game::height / 8) };
115
116 int x_direction = position.x() > (Game::width / 2) ? -1 : 1;
117 m_animation = Animation(suit, rank, position, rand_float() + .4f, x_direction * (get_random_uniform(3) + 2), .6f + rand_float() * .4f);
118}
119
120void Game::set_background_fill_enabled(bool enabled)
121{
122 Widget* widget = this;
123 while (widget) {
124 widget->set_fill_with_background_color(enabled);
125 widget = widget->parent_widget();
126 }
127}
128
129void Game::start_game_over_animation()
130{
131 if (m_state == State::GameOverAnimation || m_state == State::StartGameOverAnimationNextFrame)
132 return;
133
134 m_last_move = {};
135 if (on_undo_availability_change)
136 on_undo_availability_change(false);
137
138 create_new_animation_card();
139
140 // We wait one frame, to make sure that the foundation stacks are repainted before we start.
141 // Otherwise, if the game ended from an attempt_to_move_card_to_foundations() move, the
142 // foundations could appear empty or otherwise incorrect.
143 m_state = State::StartGameOverAnimationNextFrame;
144
145 start_timer(s_timer_interval_ms);
146
147 if (on_game_end)
148 on_game_end(GameOverReason::Victory, m_score);
149}
150
151void Game::stop_game_over_animation()
152{
153 if (m_state != State::GameOverAnimation)
154 return;
155
156 set_background_fill_enabled(true);
157 m_state = State::NewGameAnimation;
158 update();
159
160 stop_timer();
161}
162
163void Game::setup(Mode mode)
164{
165 if (m_state == State::NewGameAnimation)
166 stop_timer();
167
168 stop_game_over_animation();
169 m_mode = mode;
170
171 if (on_game_end)
172 on_game_end(GameOverReason::NewGame, m_score);
173
174 for (auto& stack : stacks())
175 stack->clear();
176
177 m_new_deck.clear();
178 m_new_game_animation_pile = 0;
179 m_passes_left_before_punishment = recycle_rules().passes_allowed_before_punishment;
180 m_score = 0;
181 update_score(0);
182 if (on_undo_availability_change)
183 on_undo_availability_change(false);
184
185 m_new_deck = Cards::create_standard_deck(Cards::Shuffle::Yes).release_value_but_fixme_should_propagate_errors();
186
187 clear_moving_cards();
188
189 m_state = State::NewGameAnimation;
190 start_timer(s_timer_interval_ms);
191 update();
192}
193
194void Game::start_timer_if_necessary()
195{
196 if (on_game_start && m_state == State::WaitingForNewGame) {
197 on_game_start();
198 m_state = State::GameInProgress;
199 }
200}
201
202void Game::score_move(CardStack& from, CardStack& to, bool inverse)
203{
204 if (from.type() == CardStack::Type::Play && to.type() == CardStack::Type::Normal) {
205 update_score(5 * (inverse ? -1 : 1));
206 } else if (from.type() == CardStack::Type::Play && to.type() == CardStack::Type::Foundation) {
207 update_score(10 * (inverse ? -1 : 1));
208 } else if (from.type() == CardStack::Type::Normal && to.type() == CardStack::Type::Foundation) {
209 update_score(10 * (inverse ? -1 : 1));
210 } else if (from.type() == CardStack::Type::Foundation && to.type() == CardStack::Type::Normal) {
211 update_score(-15 * (inverse ? -1 : 1));
212 }
213}
214
215void Game::score_flip(bool inverse)
216{
217 update_score(5 * (inverse ? -1 : 1));
218}
219
220void Game::update_score(int to_add)
221{
222 m_score = max(static_cast<int>(m_score) + to_add, 0);
223
224 if (on_score_update)
225 on_score_update(m_score);
226}
227
228void Game::keydown_event(GUI::KeyEvent& event)
229{
230 if (is_moving_cards() || m_state == State::NewGameAnimation || m_state == State::GameOverAnimation) {
231 event.ignore();
232 return;
233 }
234
235 if (event.shift() && event.key() == KeyCode::Key_F12) {
236 start_game_over_animation();
237 } else if (event.key() == KeyCode::Key_Tab) {
238 auto_move_eligible_cards_to_foundations();
239 } else if (event.key() == KeyCode::Key_Space) {
240 draw_cards();
241 } else if (event.shift() && event.key() == KeyCode::Key_F11) {
242 if constexpr (SOLITAIRE_DEBUG) {
243 dump_layout();
244 }
245 } else {
246 event.ignore();
247 }
248}
249
250void Game::mousedown_event(GUI::MouseEvent& event)
251{
252 GUI::Frame::mousedown_event(event);
253
254 if (m_state == State::NewGameAnimation || m_state == State::GameOverAnimation)
255 return;
256
257 auto click_location = event.position();
258 for (auto& to_check : stacks()) {
259 if (to_check->type() == CardStack::Type::Waste)
260 continue;
261
262 if (to_check->bounding_box().contains(click_location)) {
263 if (to_check->type() == CardStack::Type::Stock) {
264 draw_cards();
265 } else if (!to_check->is_empty()) {
266 auto& top_card = to_check->peek();
267
268 if (top_card.is_upside_down()) {
269 if (top_card.rect().contains(click_location)) {
270 top_card.set_upside_down(false);
271 score_flip();
272 start_timer_if_necessary();
273 update(top_card.rect());
274 remember_flip_for_undo(top_card);
275 }
276 } else if (!is_moving_cards()) {
277 if (is_auto_collecting() && attempt_to_move_card_to_foundations(to_check))
278 break;
279
280 if (event.button() == GUI::MouseButton::Secondary) {
281 preview_card(to_check, click_location);
282 } else {
283 pick_up_cards_from_stack(to_check, click_location, Cards::CardStack::MovementRule::Alternating).release_value_but_fixme_should_propagate_errors();
284 m_mouse_down_location = click_location;
285 m_mouse_down = true;
286 }
287
288 start_timer_if_necessary();
289 }
290 }
291 break;
292 }
293 }
294}
295
296void Game::mouseup_event(GUI::MouseEvent& event)
297{
298 GUI::Frame::mouseup_event(event);
299 clear_hovered_stack();
300
301 if (is_previewing_card()) {
302 clear_card_preview();
303 return;
304 }
305
306 if (!is_moving_cards() || m_state == State::NewGameAnimation || m_state == State::GameOverAnimation)
307 return;
308
309 bool rebound = true;
310 if (auto target_stack = find_stack_to_drop_on(Cards::CardStack::MovementRule::Alternating); !target_stack.is_null()) {
311 auto& stack = *target_stack;
312 remember_move_for_undo(*moving_cards_source_stack(), stack, moving_cards());
313
314 drop_cards_on_stack(stack, Cards::CardStack::MovementRule::Alternating).release_value_but_fixme_should_propagate_errors();
315
316 if (moving_cards_source_stack()->type() == CardStack::Type::Play)
317 pop_waste_to_play_stack();
318
319 score_move(*moving_cards_source_stack(), stack);
320 rebound = false;
321 }
322
323 if (rebound) {
324 for (auto& to_intersect : moving_cards())
325 mark_intersecting_stacks_dirty(to_intersect);
326
327 moving_cards_source_stack()->rebound_cards();
328 update(moving_cards_source_stack()->bounding_box());
329 }
330
331 m_mouse_down = false;
332}
333
334void Game::mousemove_event(GUI::MouseEvent& event)
335{
336 GUI::Frame::mousemove_event(event);
337
338 if (!m_mouse_down || m_state == State::NewGameAnimation || m_state == State::GameOverAnimation)
339 return;
340
341 auto click_location = event.position();
342 int dx = click_location.dx_relative_to(m_mouse_down_location);
343 int dy = click_location.dy_relative_to(m_mouse_down_location);
344
345 if (auto target_stack = find_stack_to_drop_on(Cards::CardStack::MovementRule::Alternating)) {
346 if (target_stack != m_hovered_stack) {
347 clear_hovered_stack();
348
349 m_hovered_stack = move(target_stack);
350 m_hovered_stack->set_highlighted(true);
351 update(m_hovered_stack->bounding_box());
352 }
353 } else {
354 clear_hovered_stack();
355 }
356
357 for (auto& to_intersect : moving_cards()) {
358 mark_intersecting_stacks_dirty(to_intersect);
359 to_intersect->rect().translate_by(dx, dy);
360 update(to_intersect->rect());
361 }
362
363 m_mouse_down_location = click_location;
364}
365
366void Game::doubleclick_event(GUI::MouseEvent& event)
367{
368 GUI::Frame::doubleclick_event(event);
369
370 if (m_state == State::GameOverAnimation) {
371 setup(mode());
372 return;
373 }
374
375 if (m_state == State::NewGameAnimation) {
376 while (m_state == State::NewGameAnimation)
377 deal_next_card();
378 return;
379 }
380
381 auto click_location = event.position();
382 for (auto& to_check : stacks()) {
383 if (to_check->type() != CardStack::Type::Normal && to_check->type() != CardStack::Type::Play)
384 continue;
385
386 if (to_check->bounding_box().contains(click_location) && !to_check->is_empty()) {
387 auto& top_card = to_check->peek();
388 if (!top_card.is_upside_down() && top_card.rect().contains(click_location))
389 attempt_to_move_card_to_foundations(to_check);
390
391 break;
392 }
393 }
394}
395
396void Game::check_for_game_over()
397{
398 for (auto foundationID : foundations) {
399 auto& foundation = stack_at_location(foundationID);
400
401 if (foundation.count() != Card::card_count)
402 return;
403 }
404
405 start_game_over_animation();
406}
407
408void Game::draw_cards()
409{
410 auto& waste = stack_at_location(Waste);
411 auto& stock = stack_at_location(Stock);
412 auto& play = stack_at_location(Play);
413
414 if (stock.is_empty()) {
415 if (waste.is_empty() && play.is_empty())
416 return;
417
418 update(waste.bounding_box());
419 update(play.bounding_box());
420
421 Vector<NonnullRefPtr<Card>> moved_cards;
422 while (!play.is_empty()) {
423 auto card = play.pop();
424 stock.push(card).release_value_but_fixme_should_propagate_errors();
425 moved_cards.prepend(card);
426 }
427
428 while (!waste.is_empty()) {
429 auto card = waste.pop();
430 stock.push(card).release_value_but_fixme_should_propagate_errors();
431 moved_cards.prepend(card);
432 }
433
434 remember_move_for_undo(waste, stock, moved_cards);
435
436 if (m_passes_left_before_punishment == 0)
437 update_score(recycle_rules().punishment);
438 else
439 --m_passes_left_before_punishment;
440
441 update(stock.bounding_box());
442 } else {
443 auto play_bounding_box = play.bounding_box();
444 play.take_all(waste).release_value_but_fixme_should_propagate_errors();
445
446 size_t cards_to_draw = 0;
447 switch (m_mode) {
448 case Mode::SingleCardDraw:
449 cards_to_draw = 1;
450 break;
451 case Mode::ThreeCardDraw:
452 cards_to_draw = 3;
453 break;
454 default:
455 VERIFY_NOT_REACHED();
456 break;
457 }
458
459 update(stock.bounding_box());
460
461 Vector<NonnullRefPtr<Card>> cards_drawn;
462 for (size_t i = 0; (i < cards_to_draw) && !stock.is_empty(); ++i) {
463 auto card = stock.pop();
464 cards_drawn.prepend(card);
465 play.push(move(card)).release_value_but_fixme_should_propagate_errors();
466 }
467
468 remember_move_for_undo(stock, play, cards_drawn);
469
470 if (play.bounding_box().size().width() > play_bounding_box.size().width())
471 update(play.bounding_box());
472 else
473 update(play_bounding_box);
474 }
475
476 start_timer_if_necessary();
477}
478
479void Game::pop_waste_to_play_stack()
480{
481 auto& waste = stack_at_location(Waste);
482 auto& play = stack_at_location(Play);
483 if (play.is_empty() && !waste.is_empty()) {
484 auto card = waste.pop();
485 moving_cards().append(card);
486 play.push(move(card)).release_value_but_fixme_should_propagate_errors();
487 }
488}
489
490bool Game::attempt_to_move_card_to_foundations(CardStack& from)
491{
492 if (from.is_empty())
493 return false;
494
495 auto& top_card = from.peek();
496 if (top_card.is_upside_down())
497 return false;
498
499 bool card_was_moved = false;
500
501 for (auto foundationID : foundations) {
502 auto& foundation = stack_at_location(foundationID);
503
504 if (foundation.is_allowed_to_push(top_card)) {
505 update(from.bounding_box());
506
507 auto card = from.pop();
508
509 mark_intersecting_stacks_dirty(card);
510 foundation.push(card).release_value_but_fixme_should_propagate_errors();
511
512 Vector<NonnullRefPtr<Card>> moved_card;
513 moved_card.append(card);
514 remember_move_for_undo(from, foundation, moved_card);
515
516 score_move(from, foundation);
517
518 update(foundation.bounding_box());
519
520 card_was_moved = true;
521 break;
522 }
523 }
524
525 if (card_was_moved) {
526 if (from.type() == CardStack::Type::Play)
527 pop_waste_to_play_stack();
528
529 start_timer_if_necessary();
530 check_for_game_over();
531 }
532
533 return card_was_moved;
534}
535
536void Game::auto_move_eligible_cards_to_foundations()
537{
538 while (true) {
539 bool card_was_moved = false;
540 for (auto& to_check : stacks()) {
541 if (to_check->type() != CardStack::Type::Normal && to_check->type() != CardStack::Type::Play)
542 continue;
543
544 if (attempt_to_move_card_to_foundations(to_check))
545 card_was_moved = true;
546 }
547
548 if (!card_was_moved)
549 break;
550 }
551}
552
553void Game::paint_event(GUI::PaintEvent& event)
554{
555 Gfx::Color background_color = this->background_color();
556
557 GUI::Frame::paint_event(event);
558
559 GUI::Painter painter(*this);
560 painter.add_clip_rect(frame_inner_rect());
561 painter.add_clip_rect(event.rect());
562
563 if (m_state == State::GameOverAnimation) {
564 m_animation.draw(painter);
565 return;
566 }
567
568 if (is_moving_cards()) {
569 for (auto& card : moving_cards())
570 card->clear(painter, background_color);
571 }
572
573 for (auto& stack : stacks()) {
574 stack->paint(painter, background_color);
575 }
576
577 if (is_moving_cards()) {
578 for (auto& card : moving_cards()) {
579 card->paint(painter);
580 card->save_old_position();
581 }
582 }
583
584 if (!m_mouse_down) {
585 if (is_moving_cards()) {
586 check_for_game_over();
587 for (auto& card : moving_cards())
588 card->set_moving(false);
589 }
590
591 clear_moving_cards();
592 }
593}
594
595void Game::remember_move_for_undo(CardStack& from, CardStack& to, Vector<NonnullRefPtr<Card>> moved_cards)
596{
597 m_last_move.type = LastMove::Type::MoveCards;
598 m_last_move.from = &from;
599 m_last_move.cards = moved_cards;
600 m_last_move.to = &to;
601 if (on_undo_availability_change)
602 on_undo_availability_change(true);
603}
604
605void Game::remember_flip_for_undo(Card& card)
606{
607 Vector<NonnullRefPtr<Card>> cards;
608 cards.append(card);
609 m_last_move.type = LastMove::Type::FlipCard;
610 m_last_move.cards = cards;
611 if (on_undo_availability_change)
612 on_undo_availability_change(true);
613}
614
615void Game::perform_undo()
616{
617 if (m_last_move.type == LastMove::Type::Invalid)
618 return;
619
620 if (m_last_move.type == LastMove::Type::FlipCard) {
621 m_last_move.cards[0]->set_upside_down(true);
622 if (on_undo_availability_change)
623 on_undo_availability_change(false);
624 invalidate_layout();
625 score_flip(true);
626 return;
627 }
628
629 if (m_last_move.from->type() == CardStack::Type::Play && m_mode == Mode::SingleCardDraw) {
630 auto& waste = stack_at_location(Waste);
631 if (!m_last_move.from->is_empty())
632 waste.push(m_last_move.from->pop()).release_value_but_fixme_should_propagate_errors();
633 }
634
635 for (auto& to_intersect : m_last_move.cards) {
636 mark_intersecting_stacks_dirty(to_intersect);
637 m_last_move.from->push(to_intersect).release_value_but_fixme_should_propagate_errors();
638 (void)m_last_move.to->pop();
639 }
640
641 if (m_last_move.from->type() == CardStack::Type::Stock) {
642 auto& waste = stack_at_location(Waste);
643 auto& play = stack_at_location(Play);
644 Vector<NonnullRefPtr<Card>> cards_popped;
645 for (size_t i = 0; i < m_last_move.cards.size(); i++) {
646 if (!waste.is_empty()) {
647 auto card = waste.pop();
648 cards_popped.prepend(card);
649 }
650 }
651 for (auto& card : cards_popped) {
652 play.push(card).release_value_but_fixme_should_propagate_errors();
653 }
654 }
655
656 if (m_last_move.from->type() == CardStack::Type::Waste && m_last_move.to->type() == CardStack::Type::Stock)
657 pop_waste_to_play_stack();
658
659 score_move(*m_last_move.from, *m_last_move.to, true);
660
661 m_last_move = {};
662 if (on_undo_availability_change)
663 on_undo_availability_change(false);
664 invalidate_layout();
665}
666
667void Game::clear_hovered_stack()
668{
669 if (!m_hovered_stack)
670 return;
671
672 m_hovered_stack->set_highlighted(false);
673 update(m_hovered_stack->bounding_box());
674 m_hovered_stack = nullptr;
675}
676
677}