From 87ace131bcf1191027ce8c871dd9d197c38953c4 Mon Sep 17 00:00:00 2001 From: Gunnar Beutner Date: Mon, 24 May 2021 22:54:47 +0200 Subject: [PATCH] Hearts: Add support for playing more than one hand This changes the game so that more than one hand can be played. Once one player has 100 or more points the game ends. A score card is shown between each hand. Fixes #7374. --- Userland/Games/Hearts/CMakeLists.txt | 1 + Userland/Games/Hearts/Game.cpp | 129 ++++++++++++++++++++------- Userland/Games/Hearts/Game.h | 7 +- Userland/Games/Hearts/Player.h | 1 + Userland/Games/Hearts/ScoreCard.cpp | 89 ++++++++++++++++++ Userland/Games/Hearts/ScoreCard.h | 32 +++++++ 6 files changed, 226 insertions(+), 33 deletions(-) create mode 100644 Userland/Games/Hearts/ScoreCard.cpp create mode 100644 Userland/Games/Hearts/ScoreCard.h 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 }; +}; + +}