mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2025-01-01 07:35:02 +03:00
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:
parent
e636ed43eb
commit
87ace131bc
Notes:
sideshowbarker
2024-07-18 17:24:20 +09:00
Author: https://github.com/gunnarbeutner Commit: https://github.com/SerenityOS/serenity/commit/87ace131bcf Pull-request: https://github.com/SerenityOS/serenity/pull/7443
@ -4,6 +4,7 @@ set(SOURCES
|
||||
Game.cpp
|
||||
main.cpp
|
||||
Player.cpp
|
||||
ScoreCard.cpp
|
||||
SettingsDialog.cpp
|
||||
HeartsGML.h
|
||||
)
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
};
|
||||
|
||||
|
@ -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;
|
||||
|
89
Userland/Games/Hearts/ScoreCard.cpp
Normal file
89
Userland/Games/Hearts/ScoreCard.cpp
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
32
Userland/Games/Hearts/ScoreCard.h
Normal file
32
Userland/Games/Hearts/ScoreCard.h
Normal 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 };
|
||||
};
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user