Serenity Operating System
at master 677 lines 21 kB view raw
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}