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.
This commit is contained in:
Gunnar Beutner 2021-05-24 22:54:47 +02:00 committed by Andreas Kling
parent e636ed43eb
commit 87ace131bc
Notes: sideshowbarker 2024-07-18 17:24:20 +09:00
6 changed files with 226 additions and 33 deletions

View File

@ -4,6 +4,7 @@ set(SOURCES
Game.cpp
main.cpp
Player.cpp
ScoreCard.cpp
SettingsDialog.cpp
HeartsGML.h
)

View File

@ -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,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<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)

View File

@ -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,
};

View File

@ -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;

View File

@ -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);
}
}
}
}

View File

@ -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 };
};
}