diff --git a/Userland/Games/Hearts/CMakeLists.txt b/Userland/Games/Hearts/CMakeLists.txt index ddc6b2a7eae..312b5904b9c 100644 --- a/Userland/Games/Hearts/CMakeLists.txt +++ b/Userland/Games/Hearts/CMakeLists.txt @@ -4,6 +4,7 @@ set(SOURCES Game.cpp main.cpp Player.cpp + ScoreCard.cpp SettingsDialog.cpp HeartsGML.h ) diff --git a/Userland/Games/Hearts/Game.cpp b/Userland/Games/Hearts/Game.cpp index 10554b90349..328361679ba 100644 --- a/Userland/Games/Hearts/Game.cpp +++ b/Userland/Games/Hearts/Game.cpp @@ -7,9 +7,12 @@ #include "Game.h" #include "Helpers.h" +#include "ScoreCard.h" #include #include +#include #include +#include #include #include #include @@ -107,6 +110,76 @@ void Game::reset() 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(); + score_widget.set_fill_with_background_color(true); + auto& layout = score_widget.set_layout(); + layout.set_margins({ 10, 10, 10, 10 }); + layout.set_spacing(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"); + close_button.on_click = [&score_dialog] { + score_dialog->done(GUI::Dialog::ExecOK); + }; + 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"); + if (game_over) + title_builder.append(" - Game Over"); + score_dialog->set_title(title_builder.to_string()); + + RefPtr close_timer; + if (!m_players[0].is_human) { + close_timer = Core::Timer::create_single_shot(2000, [&] { + score_dialog->close(); + }); + close_timer->start(); + } + + score_dialog->exec(); +} + +void Game::setup(String 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; @@ -125,22 +198,6 @@ void Game::reset() } } else m_state = State::Play; - 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::setup(String player_name) -{ - m_players[0].name = move(player_name); - - reset(); if (m_hand_number % 4 != 3) { m_passing_button->set_visible(true); @@ -338,21 +395,29 @@ void Game::continue_game_after_delay(int interval_ms) void Game::advance_game() { if (m_state == State::Play && game_ended()) { - m_state = State::GameEndedWaiting; - on_status_change("Game ended."); - continue_game_after_delay(2000); - return; - } - - if (m_state == State::GameEndedWaiting) { m_state = State::GameEnded; - if (!m_players[0].is_human) - setup(move(m_players[0].name)); + on_status_change("Game ended."); + advance_game(); return; } - if (m_state == State::GameEnded) + 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) { @@ -602,7 +667,7 @@ void Game::mouseup_event(GUI::MouseEvent& event) } } -bool Game::is_winner(Player& player) +int Game::calculate_score(Player& player) { Optional min_score; Optional max_score; @@ -622,7 +687,12 @@ bool Game::is_winner(Player& player) player_score = score; } constexpr int sum_points_of_all_cards = 26; - return (max_score.value() != sum_points_of_all_cards && player_score == min_score.value()) || player_score == sum_points_of_all_cards; + 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; @@ -747,8 +817,7 @@ void Game::paint_event(GUI::PaintEvent& event) for (auto& player : m_players) { auto& font = painter.font().bold_variant(); - auto font_color = game_ended() && is_winner(player) ? Color::Blue : Color::Black; - painter.draw_text(player.name_position, player.name, font, player.name_alignment, font_color, Gfx::TextElision::None); + 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) diff --git a/Userland/Games/Hearts/Game.h b/Userland/Games/Hearts/Game.h index 0447c656b0e..c21beb37a9f 100644 --- a/Userland/Games/Hearts/Game.h +++ b/Userland/Games/Hearts/Game.h @@ -24,7 +24,7 @@ public: virtual ~Game() override; - void setup(String player_name); + void setup(String player_name, int hand_number = 0); Function on_status_change; @@ -33,6 +33,8 @@ private: void reset(); + void show_score_card(bool game_over); + void dump_state() const; void play_card(Player& player, size_t card_index); @@ -45,7 +47,7 @@ private: size_t player_index(Player& player); Player& current_player(); bool game_ended() const { return m_trick_number == 13; } - bool is_winner(Player& player); + int calculate_score(Player& player); bool other_player_has_lower_value_card(Player& player, Card& card); bool other_player_has_higher_value_card(Player& player, Card& card); @@ -77,7 +79,6 @@ private: PassingSelectConfirmed, PassingAccept, Play, - GameEndedWaiting, GameEnded, }; diff --git a/Userland/Games/Hearts/Player.h b/Userland/Games/Hearts/Player.h index f225c2a18d7..db30a7ef4cd 100644 --- a/Userland/Games/Hearts/Player.h +++ b/Userland/Games/Hearts/Player.h @@ -49,6 +49,7 @@ public: Vector> hand; Vector> cards_taken; + Vector scores; Gfx::IntPoint first_card_position; Gfx::IntPoint card_offset; Gfx::IntRect name_position; diff --git a/Userland/Games/Hearts/ScoreCard.cpp b/Userland/Games/Hearts/ScoreCard.cpp new file mode 100644 index 00000000000..09968f7b8c2 --- /dev/null +++ b/Userland/Games/Hearts/ScoreCard.cpp @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2021, Gunnar Beutner + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include "ScoreCard.h" +#include +#include +#include +#include + +namespace Hearts { + +ScoreCard::ScoreCard(Player (&players)[4], bool game_over) + : m_players(players) + , m_game_over(game_over) +{ + set_min_size(recommended_size()); + resize(recommended_size()); +} + +Gfx::IntSize ScoreCard::recommended_size() +{ + auto& card_font = font().bold_variant(); + + return Gfx::IntSize { + 4 * column_width + 3 * cell_padding, + 16 * card_font.glyph_height() + 15 * cell_padding + }; +} +void ScoreCard::paint_event(GUI::PaintEvent& event) +{ + GUI::Widget::paint_event(event); + + GUI::Painter painter(*this); + painter.add_clip_rect(frame_inner_rect()); + painter.add_clip_rect(event.rect()); + + auto& font = painter.font().bold_variant(); + + auto cell_rect = [this, &font](int x, int y) { + return Gfx::IntRect { + frame_inner_rect().left() + x * column_width + x * cell_padding, + frame_inner_rect().top() + y * font.glyph_height() + y * cell_padding, + column_width, + font.glyph_height(), + }; + }; + + VERIFY(!m_players[0].scores.is_empty()); + + int leading_score = -1; + for (size_t player_index = 0; player_index < 4; player_index++) { + auto& player = m_players[player_index]; + auto cumulative_score = player.scores[player.scores.size() - 1]; + if (leading_score == -1 || cumulative_score < leading_score) + leading_score = cumulative_score; + } + + for (int player_index = 0; player_index < 4; player_index++) { + auto& player = m_players[player_index]; + auto cumulative_score = player.scores[player.scores.size() - 1]; + auto leading_color = m_game_over ? Color::Magenta : Color::Blue; + auto text_color = cumulative_score == leading_score ? leading_color : Color::Black; + dbgln("text_rect: {}", cell_rect(player_index, 0)); + painter.draw_text(cell_rect(player_index, 0), + player.name, + font, Gfx::TextAlignment::Center, + text_color); + for (int score_index = 0; score_index < (int)player.scores.size(); score_index++) { + auto text_rect = cell_rect(player_index, 1 + score_index); + auto score_text = String::formatted("{}", player.scores[score_index]); + auto score_text_width = font.width(score_text); + if (score_index != (int)player.scores.size() - 1) { + painter.draw_line( + { text_rect.left() + text_rect.width() / 2 - score_text_width / 2 - 3, text_rect.top() + font.glyph_height() / 2 }, + { text_rect.right() - text_rect.width() / 2 + score_text_width / 2 + 3, text_rect.top() + font.glyph_height() / 2 }, + text_color); + } + painter.draw_text(text_rect, + score_text, + font, Gfx::TextAlignment::Center, + text_color); + } + } +} + +} diff --git a/Userland/Games/Hearts/ScoreCard.h b/Userland/Games/Hearts/ScoreCard.h new file mode 100644 index 00000000000..7275815c463 --- /dev/null +++ b/Userland/Games/Hearts/ScoreCard.h @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2021, Gunnar Beutner + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include "Player.h" +#include +#include + +namespace Hearts { + +class ScoreCard : public GUI::Frame { + C_OBJECT(ScoreCard); + + Gfx::IntSize recommended_size(); + +private: + ScoreCard(Player (&players)[4], bool game_over); + + virtual void paint_event(GUI::PaintEvent&) override; + + static constexpr int column_width = 70; + static constexpr int cell_padding = 5; + + Player (&m_players)[4]; + bool m_game_over { false }; +}; + +}