/* * Copyright (c) 2020, Till Mayer * Copyright (c) 2021, Gunnar Beutner * Copyright (c) 2022, the SerenityOS developers. * * SPDX-License-Identifier: BSD-2-Clause */ #include "Game.h" #include "Helpers.h" #include "ScoreCard.h" #include #include #include #include #include #include #include #include #include REGISTER_WIDGET(Hearts, Game); namespace Hearts { Game::Game() { m_delay_timer = Core::Timer::create_single_shot(0, [this] { dbgln_if(HEARTS_DEBUG, "Continuing game after delay..."); advance_game(); }).release_value_but_fixme_should_propagate_errors(); constexpr int card_overlap = 20; constexpr int outer_border_size = 15; constexpr int player_deck_width = 12 * card_overlap + Card::width; constexpr int player_deck_height = 12 * card_overlap + Card::height; constexpr int text_height = 15; constexpr int text_offset = 5; m_players[0].first_card_position = { (width - player_deck_width) / 2, height - outer_border_size - Card::height }; m_players[0].card_offset = { card_overlap, 0 }; m_players[0].name_position = { (width - player_deck_width) / 2 - 50, height - outer_border_size - text_height - text_offset, 50 - text_offset, text_height }; m_players[0].name_alignment = Gfx::TextAlignment::BottomRight; m_players[0].name = "Gunnar"; m_players[0].is_human = true; m_players[0].taken_cards_target = { width / 2 - Card::width / 2, height }; m_players[1].first_card_position = { outer_border_size, (height - player_deck_height) / 2 }; m_players[1].card_offset = { 0, card_overlap }; m_players[1].name_position = { outer_border_size, (height - player_deck_height) / 2 - text_height - text_offset, Card::width, text_height }; m_players[1].name_alignment = Gfx::TextAlignment::BottomLeft; m_players[1].name = "Paul"; m_players[1].taken_cards_target = { -Card::width, height / 2 - Card::height / 2 }; m_players[2].first_card_position = { width - (width - player_deck_width) / 2 - Card::width, outer_border_size }; m_players[2].card_offset = { -card_overlap, 0 }; m_players[2].name_position = { width - (width - player_deck_width) / 2 + text_offset, outer_border_size + text_offset, Card::width, text_height }; m_players[2].name_alignment = Gfx::TextAlignment::TopLeft; m_players[2].name = "Simon"; m_players[2].taken_cards_target = { width / 2 - Card::width / 2, -Card::height }; m_players[3].first_card_position = { width - outer_border_size - Card::width, height - (height - player_deck_height) / 2 - Card::height }; m_players[3].card_offset = { 0, -card_overlap }; m_players[3].name_position = { width - outer_border_size - Card::width, height - (height - player_deck_height) / 2 + text_offset, Card::width, text_height }; m_players[3].name_alignment = Gfx::TextAlignment::TopRight; m_players[3].name = "Lisa"; m_players[3].taken_cards_target = { width, height / 2 - Card::height / 2 }; m_passing_button = add("Pass Left"_string.release_value_but_fixme_should_propagate_errors()); constexpr int button_width = 120; constexpr int button_height = 30; m_passing_button->set_relative_rect(width / 2 - button_width / 2, height - 3 * outer_border_size - Card::height - button_height, button_width, button_height); m_passing_button->on_click = [this](unsigned int) { if (m_state == State::PassingSelect) m_state = State::PassingSelectConfirmed; else m_state = State::Play; advance_game(); }; reset(); }; void Game::reset() { dbgln_if(HEARTS_DEBUG, "====="); dbgln_if(HEARTS_DEBUG, "Resetting game"); stop_animation(); m_hand_number = 0; m_passing_button->set_enabled(false); m_passing_button->set_visible(false); m_cards_highlighted.clear(); m_trick.clear_with_capacity(); m_trick_number = 0; for (auto& player : m_players) { player.hand.clear_with_capacity(); player.cards_taken.clear_with_capacity(); } } void Game::show_score_card(bool game_over) { auto score_dialog = GUI::Dialog::construct(window()); score_dialog->set_resizable(false); score_dialog->set_icon(window()->icon()); auto score_widget = score_dialog->set_main_widget().release_value_but_fixme_should_propagate_errors(); score_widget->set_fill_with_background_color(true); score_widget->set_layout(10, 15); auto& card_container = score_widget->add(); auto& score_card = card_container.add(m_players, game_over); auto& button_container = score_widget->add(); button_container.set_shrink_to_fit(true); button_container.set_layout(); auto& close_button = button_container.add("OK"_short_string); close_button.on_click = [&score_dialog](auto) { score_dialog->done(GUI::Dialog::ExecResult::OK); }; close_button.set_min_width(70); close_button.resize(70, 30); // FIXME: Why is this necessary? score_dialog->resize({ 20 + score_card.width() + 15 + close_button.width(), 20 + score_card.height() }); StringBuilder title_builder; title_builder.append("Score Card"sv); if (game_over) title_builder.append(" - Game Over"sv); score_dialog->set_title(title_builder.to_deprecated_string()); RefPtr close_timer; if (!m_players[0].is_human) { close_timer = Core::Timer::create_single_shot(2000, [&] { score_dialog->close(); }).release_value_but_fixme_should_propagate_errors(); close_timer->start(); } score_dialog->exec(); } void Game::setup(DeprecatedString player_name, int hand_number) { m_players[0].name = move(player_name); reset(); m_hand_number = hand_number; if (m_hand_number == 0) { for (auto& player : m_players) player.scores.clear_with_capacity(); } if (m_hand_number % 4 != 3) { m_state = State::PassingSelect; m_human_can_play = true; switch (passing_direction()) { case PassingDirection::Left: m_passing_button->set_text("Pass Left"_string.release_value_but_fixme_should_propagate_errors()); break; case PassingDirection::Across: m_passing_button->set_text("Pass Across"_string.release_value_but_fixme_should_propagate_errors()); break; case PassingDirection::Right: m_passing_button->set_text("Pass Right"_string.release_value_but_fixme_should_propagate_errors()); break; default: VERIFY_NOT_REACHED(); } } else m_state = State::Play; if (m_hand_number % 4 != 3) { m_passing_button->set_visible(true); m_passing_button->set_focus(false); } Vector> deck = Cards::create_standard_deck(Cards::Shuffle::Yes).release_value_but_fixme_should_propagate_errors(); for (auto& player : m_players) { player.hand.ensure_capacity(Card::card_count); for (uint8_t i = 0; i < Card::card_count; ++i) { auto card = deck.take_last(); if constexpr (!HEARTS_DEBUG) { if (&player != &m_players[0]) card->set_upside_down(true); } player.hand.append(card); } player.sort_hand(); Gfx::IntRect update_rect; reposition_hand(player); for (auto& card : player.hand) update_rect = update_rect.united(card->rect()); update(update_rect); } continue_game_after_delay(); } void Game::start_animation(Vector> cards, Gfx::IntPoint end, Function did_finish_callback, int initial_delay_ms, int steps) { stop_animation(); m_animation_end = end; m_animation_current_step = 0; m_animation_steps = steps; m_animation_cards.clear_with_capacity(); for (auto& card : cards) m_animation_cards.empend(card, card->position()); m_animation_did_finish = make>(move(did_finish_callback)); m_animation_delay_timer = Core::Timer::create_single_shot(initial_delay_ms, [&] { m_animation_playing = true; start_timer(10); }).release_value_but_fixme_should_propagate_errors(); m_animation_delay_timer->start(); } void Game::stop_animation() { if (m_animation_playing) { for (auto& animation : m_animation_cards) animation.card->set_position(m_animation_end); m_animation_playing = false; } if (m_animation_delay_timer) m_animation_delay_timer->stop(); stop_timer(); } void Game::timer_event(Core::TimerEvent&) { if (!m_animation_playing) return; for (auto& animation : m_animation_cards) { Gfx::IntRect update_rect = animation.card->rect(); animation.card->set_position(animation.start + (m_animation_end - animation.start) * m_animation_current_step / m_animation_steps); update_rect = update_rect.united(animation.card->rect()); update(update_rect); } if (m_animation_current_step >= m_animation_steps) { stop_timer(); m_animation_playing = false; if (m_animation_did_finish) { // The did_finish handler might end up destroying/replacing the handler // so we have to save it first. OwnPtr> animation_did_finish = move(m_animation_did_finish); (*animation_did_finish)(); } } m_animation_current_step++; } bool Game::other_player_has_lower_value_card(Player& player, Card& card) { for (auto& other_player : m_players) { if (&player != &other_player) { for (auto& other_card : other_player.hand) { if (other_card && card.suit() == other_card->suit() && hearts_card_value(*other_card) < hearts_card_value(card)) return true; } } } return false; } bool Game::other_player_has_higher_value_card(Player& player, Card& card) { for (auto& other_player : m_players) { if (&player != &other_player) { for (auto& other_card : other_player.hand) { if (other_card && card.suit() == other_card->suit() && hearts_card_value(*other_card) > hearts_card_value(card)) return true; } } } return false; } bool Game::other_player_has_queen_of_spades(Player& player) { for (auto& other_player : m_players) { if (&player != &other_player) { for (auto& other_card : other_player.hand) { if (other_card && other_card->suit() == Cards::Suit::Spades && hearts_card_value(*other_card) == CardValue::Queen) return true; } } } return false; } #define RETURN_CARD_IF_VALID(card) \ do { \ auto card_index = (card); \ if (card_index.has_value()) \ return card_index.value(); \ } while (0) size_t Game::pick_card(Player& player) { bool is_leading_player = m_trick.is_empty(); bool is_first_trick = m_trick_number == 0; if (is_leading_player) { if (is_first_trick) { auto clubs_2 = player.pick_specific_card(Cards::Suit::Clubs, CardValue::Number_2); VERIFY(clubs_2.has_value()); return clubs_2.value(); } else { auto valid_card = [this, &player](Card& card) { return is_valid_play(player, card); }; auto prefer_card = [this, &player](Card& card) { return !other_player_has_lower_value_card(player, card) && other_player_has_higher_value_card(player, card); }; return player.pick_lead_card(move(valid_card), move(prefer_card)); } } auto* high_card = m_trick[0].ptr(); for (auto& card : m_trick) if (high_card->suit() == card->suit() && hearts_card_value(card) > hearts_card_value(*high_card)) high_card = card; if (high_card->suit() == Cards::Suit::Spades && hearts_card_value(*high_card) > CardValue::Queen) RETURN_CARD_IF_VALID(player.pick_specific_card(Cards::Suit::Spades, CardValue::Queen)); auto card_has_points = [](Card& card) { return hearts_card_points(card) > 0; }; auto trick_has_points = m_trick.first_matching(card_has_points).has_value(); bool is_trailing_player = m_trick.size() == 3; if (!trick_has_points && is_trailing_player) { RETURN_CARD_IF_VALID(player.pick_low_points_high_value_card(m_trick[0]->suit())); if (is_first_trick) return player.pick_low_points_high_value_card().value(); else { auto ignore_card = [this, &player](Card& card) { return !other_player_has_higher_value_card(player, card); }; return player.pick_max_points_card(move(ignore_card)); } } RETURN_CARD_IF_VALID(player.pick_lower_value_card(*high_card)); bool is_third_player = m_trick.size() == 2; bool play_highest_value_card = false; if (is_trailing_player) play_highest_value_card = true; if (is_third_player && !trick_has_points) { play_highest_value_card = true; if (high_card->suit() == Cards::Suit::Spades && other_player_has_queen_of_spades(player)) { Optional chosen_card_index = player.pick_low_points_high_value_card(high_card->suit()); if (chosen_card_index.has_value()) { auto& card = player.hand[chosen_card_index.value()]; if (hearts_card_value(*card) > CardValue::Queen) play_highest_value_card = false; } } } if (play_highest_value_card) RETURN_CARD_IF_VALID(player.pick_low_points_high_value_card(high_card->suit())); else RETURN_CARD_IF_VALID(player.pick_slightly_higher_value_card(*high_card)); if (is_first_trick) return player.pick_low_points_high_value_card().value(); auto ignore_card = [this, &player](Card& card) { return !other_player_has_higher_value_card(player, card); }; return player.pick_max_points_card(move(ignore_card)); } size_t Game::pick_first_card_ltr(Player& player) { for (size_t i = 0; i < player.hand.size(); i++) { auto& card = player.hand[i]; if (card.is_null()) continue; if (is_valid_play(player, *card)) { return i; } } VERIFY_NOT_REACHED(); } void Game::let_player_play_card() { auto& player = current_player(); if (&player == &m_players[0]) on_status_change("Select a card to play."); else on_status_change(DeprecatedString::formatted("Waiting for {} to play a card...", player)); if (player.is_human) { m_human_can_play = true; if constexpr (HEARTS_DEBUG) { auto card_index = pick_card(player); auto& card = player.hand[card_index]; card->set_inverted(true); update(card->rect()); } return; } play_card(player, pick_card(player)); } size_t Game::player_index(Player& player) { return &player - m_players; } Player& Game::current_player() { VERIFY(m_trick.size() < 4); auto player_index = m_leading_player - m_players; auto current_player_index = (player_index + m_trick.size()) % 4; dbgln_if(HEARTS_DEBUG, "Leading player: {}, current player: {}", *m_leading_player, m_players[current_player_index]); return m_players[current_player_index]; } void Game::continue_game_after_delay(int interval_ms) { m_delay_timer->start(interval_ms); } void Game::advance_game() { if (m_animation_playing) return; if (m_inverted_card) { m_inverted_card->set_inverted(false); update(m_inverted_card->rect()); m_inverted_card.clear(); } if (m_state == State::Play && game_ended()) { m_state = State::GameEnded; on_status_change("Game ended."); advance_game(); return; } if (m_state == State::GameEnded) { int highest_score = 0; for (auto& player : m_players) { int previous_score = player.scores.is_empty() ? 0 : player.scores[player.scores.size() - 1]; auto score = previous_score + calculate_score((player)); player.scores.append(score); if (score > highest_score) highest_score = score; } bool game_over = highest_score >= 100; show_score_card(game_over); auto next_hand_number = m_hand_number + 1; if (game_over) next_hand_number = 0; setup(move(m_players[0].name), next_hand_number); return; } if (m_state == State::PassingSelect) { if (!m_players[0].is_human) { select_cards_for_passing(); m_state = State::PassingSelectConfirmed; continue_game_after_delay(); } return; } if (m_state == State::PassingSelectConfirmed) { pass_cards(); continue_game_after_delay(); return; } if (m_state == State::PassingAccept) { if (!m_players[0].is_human) { m_state = State::Play; continue_game_after_delay(); } return; } clear_highlighted_cards(); m_passing_button->set_visible(false); if (m_trick_number == 0 && m_trick.is_empty()) { // Find whoever has 2 of Clubs, they get to play the first card for (auto& player : m_players) { auto clubs_2_card = player.hand.first_matching([](auto& card) { return card->suit() == Cards::Suit::Clubs && hearts_card_value(*card) == CardValue::Number_2; }); if (clubs_2_card.has_value()) { m_leading_player = &player; let_player_play_card(); return; } } } if (m_trick.size() < 4) { let_player_play_card(); return; } auto leading_card_suit = m_trick[0]->suit(); size_t taker_index = 0; auto taker_value = hearts_card_value(m_trick[0]); for (size_t i = 1; i < 4; i++) { if (m_trick[i]->suit() != leading_card_suit) continue; if (hearts_card_value(m_trick[i]) <= taker_value) continue; taker_index = i; taker_value = hearts_card_value(m_trick[i]); } auto leading_player_index = player_index(*m_leading_player); auto taking_player_index = (leading_player_index + taker_index) % 4; auto& taking_player = m_players[taking_player_index]; dbgln_if(HEARTS_DEBUG, "{} takes the trick", taking_player); for (auto& card : m_trick) { if (hearts_card_points(card) == 0) continue; dbgln_if(HEARTS_DEBUG, "{} takes card {}", taking_player, card); taking_player.cards_taken.append(card); } start_animation( m_trick, taking_player.taken_cards_target, [&] { ++m_trick_number; if (game_ended()) for (auto& player : m_players) quick_sort(player.cards_taken, hearts_card_less); m_trick.clear_with_capacity(); m_leading_player = &taking_player; dbgln_if(HEARTS_DEBUG, "-----"); advance_game(); }, 750); return; } void Game::keydown_event(GUI::KeyEvent& event) { if (event.shift() && event.key() == KeyCode::Key_F10) { m_players[0].is_human = !m_players[0].is_human; advance_game(); } else if (event.key() == KeyCode::Key_F10) { if (m_human_can_play && m_state == State::Play) play_card(m_players[0], pick_card(m_players[0])); else if (m_state == State::PassingSelect) select_cards_for_passing(); } else if (event.key() == KeyCode::Key_Space) { if (m_human_can_play && m_state == State::Play) play_card(m_players[0], pick_first_card_ltr(m_players[0])); } else if (event.shift() && event.key() == KeyCode::Key_F11) { dump_state(); } else { event.ignore(); } } void Game::play_card(Player& player, size_t card_index) { if (player.is_human) m_human_can_play = false; VERIFY(player.hand[card_index]); VERIFY(m_trick.size() < 4); RefPtr card; swap(player.hand[card_index], card); dbgln_if(HEARTS_DEBUG, "{} plays {}", player, *card); VERIFY(is_valid_play(player, *card)); card->set_upside_down(false); m_trick.append(*card); const Gfx::IntPoint trick_card_positions[] = { { width / 2 - Card::width / 2, height / 2 - 30 }, { width / 2 - Card::width + 15, height / 2 - Card::height / 2 - 15 }, { width / 2 - Card::width / 2 + 15, height / 2 - Card::height + 15 }, { width / 2, height / 2 - Card::height / 2 }, }; VERIFY(m_leading_player); size_t leading_player_index = player_index(*m_leading_player); Vector> cards; cards.append(*card); start_animation( cards, trick_card_positions[(leading_player_index + m_trick.size() - 1) % 4], [&] { advance_game(); }, 0); } bool Game::is_valid_play(Player& player, Card& card, DeprecatedString* explanation) const { // First card must be 2 of Clubs. if (m_trick_number == 0 && m_trick.is_empty()) { if (explanation) *explanation = "The first card must be Two of Clubs."; return card.suit() == Cards::Suit::Clubs && hearts_card_value(card) == CardValue::Number_2; } // Can't play hearts or The Queen in the first trick. if (m_trick_number == 0 && hearts_card_points(card) > 0) { bool all_points_cards = true; for (auto& other_card : player.hand) { if (hearts_card_points(*other_card) == 0) { all_points_cards = false; break; } } // ... unless the player only has points cards (e.g. all Hearts or // 12 Hearts + Queen of Spades), in which case they're allowed to play Hearts. if (all_points_cards && card.suit() == Cards::Suit::Hearts) return true; if (explanation) *explanation = "You can't play a card worth points in the first trick."; return false; } // Leading card can't be hearts until hearts are broken // unless the player only has hearts cards. if (m_trick.is_empty()) { if (are_hearts_broken() || card.suit() != Cards::Suit::Hearts) return true; auto non_hearts_card = player.hand.first_matching([](auto const& other_card) { return !other_card.is_null() && other_card->suit() != Cards::Suit::Hearts; }); auto only_has_hearts = !non_hearts_card.has_value(); if (!only_has_hearts && explanation) *explanation = "Hearts haven't been broken."; return only_has_hearts; } // Player must follow suit unless they don't have any matching cards. auto leading_card_suit = m_trick[0]->suit(); if (leading_card_suit == card.suit()) return true; auto has_matching_card = player.has_card_of_suit(leading_card_suit); if (has_matching_card && explanation) *explanation = "You must follow suit."; return !has_matching_card; } bool Game::are_hearts_broken() const { for (auto& player : m_players) for (auto& card : player.cards_taken) if (card->suit() == Cards::Suit::Hearts) return true; return false; } void Game::card_clicked_during_passing(size_t, Card& card) { if (!is_card_highlighted(card)) { if (m_cards_highlighted.size() < 3) highlight_card(card); } else unhighlight_card(card); m_passing_button->set_enabled(m_cards_highlighted.size() == 3); } void Game::card_clicked_during_play(size_t card_index, Card& card) { DeprecatedString explanation; if (!is_valid_play(m_players[0], card, &explanation)) { if (m_inverted_card) m_inverted_card->set_inverted(false); card.set_inverted(true); update(card.rect()); m_inverted_card = card; on_status_change(DeprecatedString::formatted("You can't play this card: {}", explanation)); continue_game_after_delay(); return; } play_card(m_players[0], card_index); } void Game::card_clicked(size_t card_index, Card& card) { if (m_state == State::PassingSelect) card_clicked_during_passing(card_index, card); else card_clicked_during_play(card_index, card); } void Game::mouseup_event(GUI::MouseEvent& event) { GUI::Frame::mouseup_event(event); if (event.button() != GUI::MouseButton::Primary) return; if (!m_human_can_play) return; for (ssize_t i = m_players[0].hand.size() - 1; i >= 0; i--) { auto& card = m_players[0].hand[i]; if (card.is_null()) continue; if (card->rect().contains(event.position())) { card_clicked(i, *card); break; } } } int Game::calculate_score(Player& player) { Optional min_score; Optional max_score; int player_score = 0; for (auto& other_player : m_players) { int score = 0; for (auto& card : other_player.cards_taken) if (card->suit() == Cards::Suit::Spades && card->rank() == Cards::Rank::Queen) score += 13; else if (card->suit() == Cards::Suit::Hearts) score++; if (!min_score.has_value() || score < min_score.value()) min_score = score; if (!max_score.has_value() || score > max_score.value()) max_score = score; if (&other_player == &player) player_score = score; } constexpr int sum_points_of_all_cards = 26; if (player_score == sum_points_of_all_cards) return 0; else if (max_score.value() == sum_points_of_all_cards) return 26; else return player_score; } static constexpr int card_highlight_offset = -20; bool Game::is_card_highlighted(Card& card) { return m_cards_highlighted.contains(card); } void Game::highlight_card(Card& card) { VERIFY(!m_cards_highlighted.contains(card)); m_cards_highlighted.set(card); Gfx::IntRect update_rect = card.rect(); card.set_position(card.position().translated(0, card_highlight_offset)); update_rect = update_rect.united(card.rect()); update(update_rect); } void Game::unhighlight_card(Card& card) { VERIFY(m_cards_highlighted.contains(card)); m_cards_highlighted.remove(card); Gfx::IntRect update_rect = card.rect(); card.set_position(card.position().translated(0, -card_highlight_offset)); update_rect = update_rect.united(card.rect()); update(update_rect); } void Game::clear_highlighted_cards() { for (auto& card : m_cards_highlighted) card->set_position(card->position().translated(0, -card_highlight_offset)); m_cards_highlighted.clear(); } void Game::reposition_hand(Player& player) { auto card_position = player.first_card_position; for (auto& card : player.hand) { card->set_position(is_card_highlighted(*card) ? card_position.translated(0, card_highlight_offset) : card_position); card_position.translate_by(player.card_offset); } } void Game::select_cards_for_passing() { clear_highlighted_cards(); auto selected_cards = m_players[0].pick_cards_to_pass(passing_direction()); highlight_card(selected_cards[0]); highlight_card(selected_cards[1]); highlight_card(selected_cards[2]); m_passing_button->set_enabled(true); } void Game::pass_cards() { Vector> first_player_cards; for (auto& card : m_cards_highlighted) first_player_cards.append(*card); clear_highlighted_cards(); VERIFY(first_player_cards.size() == 3); Vector> passed_cards[4]; passed_cards[0] = first_player_cards; passed_cards[1] = m_players[1].pick_cards_to_pass(passing_direction()); passed_cards[2] = m_players[2].pick_cards_to_pass(passing_direction()); passed_cards[3] = m_players[3].pick_cards_to_pass(passing_direction()); for (size_t i = 0; i < 4; i++) { m_players[i].remove_cards(passed_cards[i]); int destination_player_index = i; switch (passing_direction()) { case PassingDirection::Left: destination_player_index += 1; break; case PassingDirection::Across: destination_player_index += 2; break; case PassingDirection::Right: destination_player_index += 3; break; default: VERIFY_NOT_REACHED(); } destination_player_index %= 4; for (auto& card : passed_cards[i]) { m_players[destination_player_index].hand.append(card); if constexpr (!HEARTS_DEBUG) card->set_upside_down(destination_player_index != 0); if (destination_player_index == 0) highlight_card(card); } } for (auto& player : m_players) { VERIFY(player.hand.size() == 13); player.sort_hand(); reposition_hand(player); Gfx::IntRect update_rect; for (auto& card : player.hand) update_rect = update_rect.united(card->rect()); update(update_rect); } m_state = State::PassingAccept; m_passing_button->set_text("OK"_short_string); m_passing_button->set_enabled(true); } PassingDirection Game::passing_direction() const { VERIFY(m_hand_number % 4 != 3); return static_cast(m_hand_number % 4); } void Game::paint_event(GUI::PaintEvent& event) { GUI::Frame::paint_event(event); GUI::Painter painter(*this); painter.add_clip_rect(frame_inner_rect()); painter.add_clip_rect(event.rect()); Gfx::Color background_color = this->background_color(); painter.clear_rect(frame_inner_rect(), background_color); for (auto& player : m_players) { auto& font = painter.font().bold_variant(); painter.draw_text(player.name_position, player.name, font, player.name_alignment, Color::Black, Gfx::TextElision::None); if (!game_ended()) { for (auto& card : player.hand) if (!card.is_null()) card->paint(painter); } else { // FIXME: reposition cards in advance_game() maybe auto card_position = player.first_card_position; for (auto& card : player.cards_taken) { card->set_upside_down(false); card->set_position(card_position); card->paint(painter); card_position.translate_by(player.card_offset); } } } for (size_t i = 0; i < m_trick.size(); i++) m_trick[i]->paint(painter); } void Game::dump_state() const { if constexpr (HEARTS_DEBUG) { dbgln("------------------------------"); for (uint8_t i = 0; i < 4; ++i) { auto& player = m_players[i]; dbgln("Player {}", player.name); dbgln("Hand:"); for (auto const& card : player.hand) if (card.is_null()) dbgln(" "); else dbgln(" {}", *card); dbgln("Taken:"); for (auto const& card : player.cards_taken) dbgln(" {}", card); } } } }