diff options
author | Gunnar Beutner <gbeutner@serenityos.org> | 2021-05-24 22:54:47 +0200 |
---|---|---|
committer | Andreas Kling <kling@serenityos.org> | 2021-05-25 21:05:35 +0200 |
commit | 87ace131bcf1191027ce8c871dd9d197c38953c4 (patch) | |
tree | fb871ccba7df51db342fb77c2f082a7b87c0c556 /Userland/Games | |
parent | e636ed43ebad685fc67f510b6c492418c0a572dd (diff) | |
download | serenity-87ace131bcf1191027ce8c871dd9d197c38953c4.zip |
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.
Diffstat (limited to 'Userland/Games')
-rw-r--r-- | Userland/Games/Hearts/CMakeLists.txt | 1 | ||||
-rw-r--r-- | Userland/Games/Hearts/Game.cpp | 127 | ||||
-rw-r--r-- | Userland/Games/Hearts/Game.h | 7 | ||||
-rw-r--r-- | Userland/Games/Hearts/Player.h | 1 | ||||
-rw-r--r-- | Userland/Games/Hearts/ScoreCard.cpp | 89 | ||||
-rw-r--r-- | Userland/Games/Hearts/ScoreCard.h | 32 |
6 files changed, 225 insertions, 32 deletions
diff --git a/Userland/Games/Hearts/CMakeLists.txt b/Userland/Games/Hearts/CMakeLists.txt index ddc6b2a7ea..312b5904b9 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 10554b9034..328361679b 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 <AK/Debug.h> #include <AK/QuickSort.h> +#include <LibGUI/BoxLayout.h> #include <LibGUI/Button.h> +#include <LibGUI/Dialog.h> #include <LibGUI/Painter.h> #include <LibGfx/Font.h> #include <LibGfx/Palette.h> @@ -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<GUI::Widget>(); + score_widget.set_fill_with_background_color(true); + auto& layout = score_widget.set_layout<GUI::HorizontalBoxLayout>(); + layout.set_margins({ 10, 10, 10, 10 }); + layout.set_spacing(15); + + auto& card_container = score_widget.add<GUI::Widget>(); + auto& score_card = card_container.add<ScoreCard>(m_players, game_over); + + auto& button_container = score_widget.add<GUI::Widget>(); + button_container.set_shrink_to_fit(true); + button_container.set_layout<GUI::VerticalBoxLayout>(); + + auto& close_button = button_container.add<GUI::Button>("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<Core::Timer> 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,22 +395,30 @@ void Game::continue_game_after_delay(int interval_ms) void Game::advance_game() { if (m_state == State::Play && game_ended()) { - m_state = State::GameEndedWaiting; + m_state = State::GameEnded; on_status_change("Game ended."); - continue_game_after_delay(2000); + advance_game(); return; } - if (m_state == State::GameEndedWaiting) { - m_state = State::GameEnded; - if (!m_players[0].is_human) - setup(move(m_players[0].name)); + 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::GameEnded) - return; - if (m_state == State::PassingSelect) { if (!m_players[0].is_human) { select_cards_for_passing(); @@ -602,7 +667,7 @@ void Game::mouseup_event(GUI::MouseEvent& event) } } -bool Game::is_winner(Player& player) +int Game::calculate_score(Player& player) { Optional<int> min_score; Optional<int> 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 0447c656b0..c21beb37a9 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<void(String const&)> 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 f225c2a18d..db30a7ef4c 100644 --- a/Userland/Games/Hearts/Player.h +++ b/Userland/Games/Hearts/Player.h @@ -49,6 +49,7 @@ public: Vector<RefPtr<Card>> hand; Vector<RefPtr<Card>> cards_taken; + Vector<int> 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 0000000000..09968f7b8c --- /dev/null +++ b/Userland/Games/Hearts/ScoreCard.cpp @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2021, Gunnar Beutner <gbeutner@serenityos.org> + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include "ScoreCard.h" +#include <LibGUI/Button.h> +#include <LibGUI/Painter.h> +#include <LibGUI/Window.h> +#include <LibGfx/Font.h> + +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 0000000000..7275815c46 --- /dev/null +++ b/Userland/Games/Hearts/ScoreCard.h @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2021, Gunnar Beutner <gbeutner@serenityos.org> + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include "Player.h" +#include <AK/Function.h> +#include <LibGUI/Frame.h> + +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 }; +}; + +} |