Browse Source

Games: Add Hearts

Gunnar Beutner 4 years ago
parent
commit
c3efae85f2

+ 4 - 0
AK/Debug.h.in

@@ -174,6 +174,10 @@
 #cmakedefine01 HEAP_DEBUG
 #endif
 
+#ifndef HEARTS_DEBUG
+#cmakedefine01 HEARTS_DEBUG
+#endif
+
 #ifndef HEX_DEBUG
 #cmakedefine01 HEX_DEBUG
 #endif

+ 4 - 0
Base/res/apps/Hearts.af

@@ -0,0 +1,4 @@
+[App]
+Name=Hearts
+Executable=/bin/Hearts
+Category=Games

BIN
Base/res/icons/16x16/app-hearts.png


BIN
Base/res/icons/32x32/app-hearts.png


+ 1 - 0
Meta/CMake/all_the_debug_macros.cmake

@@ -132,6 +132,7 @@ set(GEMINIJOB_DEBUG ON)
 set(GENERATE_DEBUG_CODE ON)
 set(GLOBAL_DTORS_DEBUG ON)
 set(HEAP_DEBUG ON)
+set(HEARTS_DEBUG ON)
 set(HEX_DEBUG ON)
 set(HTML_SCRIPT_DEBUG ON)
 set(HTTPSJOB_DEBUG ON)

+ 1 - 0
Userland/Games/CMakeLists.txt

@@ -2,6 +2,7 @@ add_subdirectory(2048)
 add_subdirectory(Breakout)
 add_subdirectory(Chess)
 add_subdirectory(GameOfLife)
+add_subdirectory(Hearts)
 add_subdirectory(Minesweeper)
 add_subdirectory(Pong)
 add_subdirectory(Snake)

+ 11 - 0
Userland/Games/Hearts/CMakeLists.txt

@@ -0,0 +1,11 @@
+compile_gml(Hearts.gml HeartsGML.h hearts_gml)
+
+set(SOURCES
+    Game.cpp
+    main.cpp
+    Player.cpp
+    HeartsGML.h
+)
+
+serenity_app(Hearts ICON app-hearts)
+target_link_libraries(Hearts LibCards LibGUI LibGfx LibCore)

+ 521 - 0
Userland/Games/Hearts/Game.cpp

@@ -0,0 +1,521 @@
+/*
+ * Copyright (c) 2020, Till Mayer <till.mayer@web.de>
+ * Copyright (c) 2021, Gunnar Beutner <gbeutner@serenityos.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include "Game.h"
+#include "Helpers.h"
+#include <AK/Debug.h>
+#include <AK/QuickSort.h>
+#include <LibGUI/Painter.h>
+#include <LibGfx/Font.h>
+#include <LibGfx/Palette.h>
+#include <time.h>
+
+REGISTER_WIDGET(Hearts, Game);
+
+namespace Hearts {
+
+Game::Game()
+{
+    srand(time(nullptr));
+
+    m_delay_timer = Core::Timer::create_single_shot(0, [this] {
+        advance_game();
+    });
+
+    constexpr int card_overlap = 20;
+    constexpr int outer_border_size = 15;
+    constexpr int player_deck_width = 12 * card_overlap + Card::width;
+    constexpr int player_deck_height = 12 * card_overlap + Card::height;
+    constexpr int text_height = 15;
+    constexpr int text_offset = 5;
+
+    m_players[0].first_card_position = { (width - player_deck_width) / 2, height - outer_border_size - Card::height };
+    m_players[0].card_offset = { card_overlap, 0 };
+    m_players[0].name_position = {
+        (width - player_deck_width) / 2 - 50, height - outer_border_size - text_height - text_offset,
+        50 - text_offset, text_height
+    };
+    m_players[0].name_alignment = Gfx::TextAlignment::BottomRight;
+    m_players[0].name = "Gunnar";
+    m_players[0].taken_cards_target = { width / 2 - Card::width / 2, height };
+
+    m_players[1].first_card_position = { outer_border_size, (height - player_deck_height) / 2 };
+    m_players[1].card_offset = { 0, card_overlap };
+    m_players[1].name_position = {
+        outer_border_size, (height - player_deck_height) / 2 - text_height - text_offset,
+        Card::width, text_height
+    };
+    m_players[1].name_alignment = Gfx::TextAlignment::BottomLeft;
+    m_players[1].name = "Paul";
+    m_players[1].taken_cards_target = { -Card::width, height / 2 - Card::height / 2 };
+
+    m_players[2].first_card_position = { width - (width - player_deck_width) / 2 - Card::width, outer_border_size };
+    m_players[2].card_offset = { -card_overlap, 0 };
+    m_players[2].name_position = {
+        width - (width - player_deck_width) / 2 + text_offset, outer_border_size + text_offset,
+        Card::width, text_height
+    };
+    m_players[2].name_alignment = Gfx::TextAlignment::TopLeft;
+    m_players[2].name = "Simon";
+    m_players[2].taken_cards_target = { width / 2 - Card::width / 2, -Card::height };
+
+    m_players[3].first_card_position = { width - outer_border_size - Card::width, height - (height - player_deck_height) / 2 - Card::height };
+    m_players[3].card_offset = { 0, -card_overlap };
+    m_players[3].name_position = {
+        width - outer_border_size - Card::width, height - (height - player_deck_height) / 2 + text_offset,
+        Card::width, text_height
+    };
+    m_players[3].name_alignment = Gfx::TextAlignment::TopRight;
+    m_players[3].name = "Lisa";
+    m_players[3].taken_cards_target = { width, height / 2 - Card::height / 2 };
+};
+
+Game::~Game()
+{
+}
+
+void Game::setup()
+{
+    NonnullRefPtrVector<Card> deck;
+
+    dbgln_if(HEARTS_DEBUG, "=====");
+    dbgln_if(HEARTS_DEBUG, "Resetting game");
+
+    stop_animation();
+
+    m_trick.clear_with_capacity();
+    m_trick_number = 0;
+
+    for (int i = 0; i < Card::card_count; ++i) {
+        deck.append(Card::construct(Card::Type::Clubs, i));
+        deck.append(Card::construct(Card::Type::Spades, i));
+        deck.append(Card::construct(Card::Type::Hearts, i));
+        deck.append(Card::construct(Card::Type::Diamonds, i));
+    }
+
+    for (auto& player : m_players) {
+        player.hand.clear_with_capacity();
+        player.cards_taken.clear_with_capacity();
+        for (uint8_t i = 0; i < Card::card_count; ++i) {
+            auto card = deck.take(rand() % deck.size());
+            if constexpr (!HEARTS_DEBUG) {
+                if (&player != &m_players[0])
+                    card->set_upside_down(true);
+            }
+            player.hand.append(card);
+        }
+        quick_sort(player.hand, hearts_card_less);
+        auto card_position = player.first_card_position;
+        for (auto& card : player.hand) {
+            card->set_position(card_position);
+            card_position.translate_by(player.card_offset);
+        }
+    }
+
+    advance_game();
+}
+
+void Game::start_animation(NonnullRefPtrVector<Card> cards, Gfx::IntPoint const& end, Function<void()> did_finish_callback, int initial_delay_ms, int steps)
+{
+    m_animation_end = end;
+    m_animation_current_step = 0;
+    m_animation_steps = steps;
+    m_animation_cards.clear_with_capacity();
+    for (auto& card : cards)
+        m_animation_cards.empend(card, card.position());
+    m_animation_did_finish = make<Function<void()>>(move(did_finish_callback));
+    m_animation_delay_timer = Core::Timer::create_single_shot(initial_delay_ms, [&] {
+        m_animation_playing = true;
+        start_timer(10);
+    });
+    m_animation_delay_timer->start();
+}
+
+void Game::stop_animation()
+{
+    m_animation_playing = false;
+    if (m_animation_delay_timer)
+        m_animation_delay_timer->stop();
+    stop_timer();
+}
+
+void Game::timer_event(Core::TimerEvent&)
+{
+    if (m_animation_playing) {
+        for (auto& animation : m_animation_cards) {
+            animation.card->set_position(animation.start + (m_animation_end - animation.start) * m_animation_current_step / m_animation_steps);
+        }
+        if (m_animation_current_step >= m_animation_steps) {
+            stop_timer();
+            if (m_animation_did_finish)
+                (*m_animation_did_finish)();
+        }
+        m_animation_current_step++;
+    }
+    update();
+}
+
+#define RETURN_CARD_IF_VALID(card)     \
+    do {                               \
+        auto card_index = (card);      \
+        if (card_index.has_value())    \
+            return card_index.value(); \
+    } while (0)
+
+size_t Game::pick_card(Player& player)
+{
+    bool is_leading_player = m_trick.is_empty();
+    bool is_first_trick = m_trick_number == 0;
+    if (is_leading_player) {
+        if (is_first_trick) {
+            auto clubs_2 = player.pick_specific_card(Card::Type::Clubs, CardValue::Number_2);
+            VERIFY(clubs_2.has_value());
+            return clubs_2.value();
+        } else
+            return player.pick_low_points_low_value_card();
+    }
+    auto card_has_points = [](Card& card) { return hearts_card_points(card) > 0; };
+    auto trick_has_points = m_trick.first_matching(card_has_points).has_value();
+    bool is_trailing_player = m_trick.size() == 3;
+    if (!trick_has_points && is_trailing_player) {
+        RETURN_CARD_IF_VALID(player.pick_low_points_high_value_card(m_trick[0].type()));
+        if (is_first_trick)
+            return player.pick_low_points_high_value_card().value();
+        else
+            return player.pick_max_points_card();
+    }
+    auto* high_card = &m_trick[0];
+    for (auto& card : m_trick)
+        if (high_card->type() == card.type() && hearts_card_value(card) > hearts_card_value(*high_card))
+            high_card = &card;
+    if (!is_first_trick && high_card->type() == Card::Type::Spades && hearts_card_value(*high_card) > CardValue::Queen)
+        RETURN_CARD_IF_VALID(player.pick_specific_card(Card::Type::Spades, CardValue::Queen));
+    RETURN_CARD_IF_VALID(player.pick_lower_value_card(*high_card));
+    if (!is_trailing_player)
+        RETURN_CARD_IF_VALID(player.pick_slightly_higher_value_card(*high_card));
+    else
+        RETURN_CARD_IF_VALID(player.pick_low_points_high_value_card(high_card->type()));
+    if (is_first_trick)
+        return player.pick_low_points_high_value_card().value();
+    else
+        return player.pick_max_points_card();
+}
+
+void Game::let_player_play_card()
+{
+    auto& player = current_player();
+
+    if (&player == &m_players[0])
+        on_status_change("Select a card to play.");
+    else
+        on_status_change(String::formatted("Waiting for {} to play a card...", player));
+
+    if (is_human(player)) {
+        m_human_can_play = true;
+        update();
+        return;
+    }
+
+    play_card(player, pick_card(player));
+}
+
+size_t Game::player_index(Player& player)
+{
+    return &player - m_players;
+}
+
+Player& Game::current_player()
+{
+    VERIFY(m_trick.size() < 4);
+    auto player_index = m_leading_player - m_players;
+    auto current_player_index = (player_index + m_trick.size()) % 4;
+    dbgln_if(HEARTS_DEBUG, "Leading player: {}, current player: {}", *m_leading_player, m_players[current_player_index]);
+    return m_players[current_player_index];
+}
+
+void Game::continue_game_after_delay(int interval_ms)
+{
+    m_delay_timer->start(interval_ms);
+}
+
+void Game::advance_game()
+{
+    if (game_ended()) {
+        on_status_change("Game ended.");
+        return;
+    }
+
+    if (m_trick_number == 0 && m_trick.is_empty()) {
+        // Find whoever has 2 of Clubs, they get to play the first card
+        for (auto& player : m_players) {
+            auto clubs_2_card = player.hand.first_matching([](auto& card) {
+                return card->type() == Card::Type::Clubs && hearts_card_value(*card) == CardValue::Number_2;
+            });
+            if (clubs_2_card.has_value()) {
+                m_leading_player = &player;
+                let_player_play_card();
+                return;
+            }
+        }
+    }
+
+    if (m_trick.size() < 4) {
+        let_player_play_card();
+        return;
+    }
+
+    auto leading_card_type = m_trick[0].type();
+    size_t taker_index = 0;
+    auto taker_value = hearts_card_value(m_trick[0]);
+    for (size_t i = 1; i < 4; i++) {
+        if (m_trick[i].type() != leading_card_type)
+            continue;
+        if (hearts_card_value(m_trick[i]) <= taker_value)
+            continue;
+        taker_index = i;
+        taker_value = hearts_card_value(m_trick[i]);
+    }
+    auto leading_player_index = player_index(*m_leading_player);
+    auto taking_player_index = (leading_player_index + taker_index) % 4;
+    auto& taking_player = m_players[taking_player_index];
+    dbgln_if(HEARTS_DEBUG, "{} takes the trick", taking_player);
+    for (auto& card : m_trick) {
+        if (hearts_card_points(card) == 0)
+            continue;
+        dbgln_if(HEARTS_DEBUG, "{} takes card {}", taking_player, card);
+        taking_player.cards_taken.append(card);
+    }
+
+    start_animation(
+        m_trick,
+        taking_player.taken_cards_target,
+        [&] {
+            ++m_trick_number;
+
+            if (game_ended())
+                for (auto& player : m_players)
+                    quick_sort(player.cards_taken, hearts_card_less);
+
+            m_trick.clear_with_capacity();
+            m_leading_player = &taking_player;
+            update();
+            dbgln_if(HEARTS_DEBUG, "-----");
+            advance_game();
+        },
+        750);
+
+    return;
+}
+
+void Game::keydown_event(GUI::KeyEvent& event)
+{
+    if (event.shift() && event.key() == KeyCode::Key_F11)
+        dump_state();
+}
+
+void Game::play_card(Player& player, size_t card_index)
+{
+    if (is_human(player))
+        m_human_can_play = false;
+    VERIFY(player.hand[card_index]);
+    VERIFY(m_trick.size() < 4);
+    RefPtr<Card> card;
+    swap(player.hand[card_index], card);
+    dbgln_if(HEARTS_DEBUG, "{} plays {}", player, *card);
+    VERIFY(is_valid_play(player, *card));
+    card->set_upside_down(false);
+    m_trick.append(*card);
+
+    const Gfx::IntPoint trick_card_positions[] = {
+        { width / 2 - Card::width / 2, height / 2 - 30 },
+        { width / 2 - Card::width + 15, height / 2 - Card::height / 2 - 15 },
+        { width / 2 - Card::width / 2 + 15, height / 2 - Card::height + 15 },
+        { width / 2, height / 2 - Card::height / 2 },
+    };
+
+    VERIFY(m_leading_player);
+    size_t leading_player_index = player_index(*m_leading_player);
+
+    NonnullRefPtrVector<Card> cards;
+    cards.append(*card);
+    start_animation(
+        cards,
+        trick_card_positions[(leading_player_index + m_trick.size() - 1) % 4],
+        [&] {
+            advance_game();
+        },
+        0);
+}
+
+bool Game::is_valid_play(Player& player, Card& card, String* explanation) const
+{
+    // First card must be 2 of Clubs.
+    if (m_trick_number == 0 && m_trick.is_empty()) {
+        if (explanation)
+            *explanation = "The first card must be Two of Clubs.";
+        return card.type() == Card::Type::Clubs && hearts_card_value(card) == CardValue::Number_2;
+    }
+
+    // Can't play hearts or The Queen in the first trick.
+    if (m_trick_number == 0 && hearts_card_points(card) > 0) {
+        bool all_points_cards = true;
+        for (auto& other_card : player.hand) {
+            if (hearts_card_points(*other_card) == 0) {
+                all_points_cards = false;
+                break;
+            }
+        }
+        // ... unless the player only has points cards (e.g. all Hearts or
+        // 12 Hearts + Queen of Spades), in which case they're allowed to play Hearts.
+        if (all_points_cards && card.type() == Card::Type::Hearts)
+            return true;
+        if (explanation)
+            *explanation = "You can't play a card worth points in the first trick.";
+        return false;
+    }
+
+    // Leading card can't be hearts until hearts are broken
+    // unless the player only has hearts cards.
+    if (m_trick.is_empty()) {
+        if (are_hearts_broken() || card.type() != Card::Type::Hearts)
+            return true;
+        auto non_hearts_card = player.hand.first_matching([](auto const& other_card) {
+            return !other_card.is_null() && other_card->type() != Card::Type::Hearts;
+        });
+        auto only_has_hearts = !non_hearts_card.has_value();
+        if (!only_has_hearts && explanation)
+            *explanation = "Hearts haven't been broken.";
+        return only_has_hearts;
+    }
+
+    // Player must follow suit unless they don't have any matching cards.
+    auto leading_card_type = m_trick[0].type();
+    if (leading_card_type == card.type())
+        return true;
+    auto has_matching_card = player.has_card_of_type(leading_card_type);
+    if (has_matching_card && explanation)
+        *explanation = "You must follow suit.";
+    return !has_matching_card;
+}
+
+bool Game::are_hearts_broken() const
+{
+    for (auto& player : m_players)
+        for (auto& card : player.cards_taken)
+            if (card->type() == Card::Type::Hearts)
+                return true;
+    return false;
+}
+
+void Game::mouseup_event(GUI::MouseEvent& event)
+{
+    GUI::Frame::mouseup_event(event);
+
+    if (event.button() != GUI::MouseButton::Left)
+        return;
+
+    if (!m_human_can_play)
+        return;
+
+    for (ssize_t i = m_players[0].hand.size() - 1; i >= 0; i--) {
+        auto& card = m_players[0].hand[i];
+        if (card.is_null())
+            continue;
+        if (card->rect().contains(event.position())) {
+            String explanation;
+            if (!is_valid_play(m_players[0], *card, &explanation)) {
+                on_status_change(String::formatted("You can't play this card: {}", explanation));
+                continue_game_after_delay();
+                return;
+            }
+            play_card(m_players[0], i);
+            update();
+            break;
+        }
+    }
+}
+
+bool Game::is_winner(Player& player)
+{
+    Optional<int> min_score;
+    Optional<int> max_score;
+    int player_score = 0;
+    for (auto& other_player : m_players) {
+        int score = 0;
+        for (auto& card : other_player.cards_taken)
+            if (card->type() == Card::Type::Spades && card->value() == 11)
+                score += 13;
+            else if (card->type() == Card::Type::Hearts)
+                score++;
+        if (!min_score.has_value() || score < min_score.value())
+            min_score = score;
+        if (!max_score.has_value() || score > max_score.value())
+            max_score = score;
+        if (&other_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;
+}
+
+void Game::paint_event(GUI::PaintEvent& event)
+{
+    GUI::Frame::paint_event(event);
+
+    GUI::Painter painter(*this);
+    painter.add_clip_rect(frame_inner_rect());
+    painter.add_clip_rect(event.rect());
+
+    static Gfx::Color s_background_color = palette().color(background_role());
+    painter.clear_rect(frame_inner_rect(), s_background_color);
+
+    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);
+
+        if (!game_ended()) {
+            for (auto& card : player.hand)
+                if (!card.is_null())
+                    card->draw(painter);
+        } else {
+            // FIXME: reposition cards in advance_game() maybe
+            auto card_position = player.first_card_position;
+            for (auto& card : player.cards_taken) {
+                card->set_upside_down(false);
+                card->set_position(card_position);
+                card->draw(painter);
+                card_position.translate_by(player.card_offset);
+            }
+        }
+    }
+
+    for (size_t i = 0; i < m_trick.size(); i++)
+        m_trick[i].draw(painter);
+}
+
+void Game::dump_state() const
+{
+    if constexpr (HEARTS_DEBUG) {
+        dbgln("------------------------------");
+        for (uint8_t i = 0; i < 4; ++i) {
+            auto& player = m_players[i];
+            dbgln("Player {}", player.name);
+            dbgln("Hand:");
+            for (const auto& card : player.hand)
+                if (card.is_null())
+                    dbgln("  <empty>");
+                else
+                    dbgln("  {}", *card);
+            dbgln("Taken:");
+            for (const auto& card : player.cards_taken)
+                dbgln("  {}", card);
+        }
+    }
+}
+
+}

+ 79 - 0
Userland/Games/Hearts/Game.h

@@ -0,0 +1,79 @@
+/*
+ * Copyright (c) 2020, Till Mayer <till.mayer@web.de>
+ * Copyright (c) 2021, Gunnar Beutner <gbeutner@serenityos.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#pragma once
+
+#include "Player.h"
+#include <LibCards/Card.h>
+#include <LibCore/Timer.h>
+#include <LibGUI/Frame.h>
+#include <LibGUI/Painter.h>
+
+using Cards::Card;
+
+namespace Hearts {
+
+class Game final : public GUI::Frame {
+    C_OBJECT(Game)
+public:
+    static constexpr int width = 640;
+    static constexpr int height = 480;
+
+    virtual ~Game() override;
+
+    void setup();
+
+    Function<void(String const&)> on_status_change;
+
+private:
+    Game();
+
+    void dump_state() const;
+
+    void play_card(Player& player, size_t card_index);
+    bool are_hearts_broken() const;
+    bool is_valid_play(Player& player, Card& card, String* explanation = nullptr) const;
+    void let_player_play_card();
+    void continue_game_after_delay(int interval_ms = 750);
+    void advance_game();
+    size_t pick_card(Player& player);
+    bool is_human(Player& player) const { return &player == &m_players[0]; }
+    size_t player_index(Player& player);
+    Player& current_player();
+    bool game_ended() const { return m_trick_number == 13; }
+    bool is_winner(Player& player);
+
+    void start_animation(NonnullRefPtrVector<Card> cards, Gfx::IntPoint const& end, Function<void()> did_finish_callback, int initial_delay_ms, int steps = 30);
+    void stop_animation();
+
+    virtual void paint_event(GUI::PaintEvent&) override;
+    virtual void mouseup_event(GUI::MouseEvent&) override;
+    virtual void keydown_event(GUI::KeyEvent&) override;
+    virtual void timer_event(Core::TimerEvent&) override;
+
+    Player m_players[4];
+    NonnullRefPtrVector<Card> m_trick;
+    Player* m_leading_player { nullptr };
+    u8 m_trick_number { 0 };
+    RefPtr<Core::Timer> m_delay_timer;
+    bool m_human_can_play { false };
+
+    struct AnimatedCard {
+        NonnullRefPtr<Card> card;
+        Gfx::IntPoint start;
+    };
+
+    RefPtr<Core::Timer> m_animation_delay_timer;
+    bool m_animation_playing { false };
+    Vector<AnimatedCard> m_animation_cards;
+    Gfx::IntPoint m_animation_end;
+    int m_animation_current_step { 0 };
+    int m_animation_steps { 0 };
+    OwnPtr<Function<void()>> m_animation_did_finish;
+};
+
+}

+ 16 - 0
Userland/Games/Hearts/Hearts.gml

@@ -0,0 +1,16 @@
+@GUI::Widget {
+    fill_with_background_color: true
+
+    layout: @GUI::VerticalBoxLayout {
+    }
+
+    @Hearts::Game {
+        name: "game"
+        fill_with_background_color: true
+        background_color: "green"
+    }
+
+    @GUI::Statusbar {
+        name: "statusbar"
+    }
+}

+ 58 - 0
Userland/Games/Hearts/Helpers.h

@@ -0,0 +1,58 @@
+/*
+ * Copyright (c) 2021, Gunnar Beutner <gbeutner@serenityos.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#pragma once
+
+#include <LibCards/Card.h>
+
+using Cards::Card;
+
+namespace Hearts {
+
+enum class CardValue : uint8_t {
+    Number_2,
+    Number_3,
+    Number_4,
+    Number_5,
+    Number_6,
+    Number_7,
+    Number_8,
+    Number_9,
+    Number_10,
+    Jack,
+    Queen,
+    King,
+    Ace
+};
+
+inline CardValue hearts_card_value(Card const& card)
+{
+    // Ace has a higher value than all other cards in Hearts
+    if (card.value() == 0)
+        return CardValue::Ace;
+    else
+        return static_cast<CardValue>(card.value() - 1);
+}
+
+inline uint8_t hearts_card_points(Card const& card)
+{
+    if (card.type() == Card::Type::Hearts)
+        return 1;
+    else if (card.type() == Card::Type::Spades && hearts_card_value(card) == CardValue::Queen)
+        return 13;
+    else
+        return 0;
+}
+
+inline bool hearts_card_less(RefPtr<Card>& card1, RefPtr<Card>& card2)
+{
+    if (card1->type() != card2->type())
+        return card1->type() < card2->type();
+    else
+        return hearts_card_value(*card1) < hearts_card_value(*card2);
+}
+
+}

+ 118 - 0
Userland/Games/Hearts/Player.cpp

@@ -0,0 +1,118 @@
+/*
+ * Copyright (c) 2021, Gunnar Beutner <gbeutner@serenityos.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include "Player.h"
+#include "Helpers.h"
+
+namespace Hearts {
+
+size_t Player::pick_low_points_low_value_card()
+{
+    int min_points = -1;
+    int min_value = -1;
+    int card_index = -1;
+    for (size_t i = 0; i < hand.size(); i++) {
+        auto& card = hand[i];
+        if (card.is_null())
+            continue;
+        auto points = hearts_card_points(*card);
+        auto value = hearts_card_value(*card);
+        if (min_points != -1 && (points > min_points || static_cast<int>(value) > min_value))
+            continue;
+        min_points = points;
+        min_value = static_cast<int>(value);
+        card_index = i;
+    }
+    VERIFY(card_index != -1);
+    return card_index;
+}
+
+Optional<size_t> Player::pick_low_points_high_value_card(Optional<Card::Type> type)
+{
+    int min_points = -1;
+    Optional<size_t> card_index;
+    for (ssize_t i = hand.size() - 1; i >= 0; i--) {
+        auto& card = hand[i];
+        if (card.is_null())
+            continue;
+        if (type.has_value() && card->type() != type.value())
+            continue;
+        auto points = hearts_card_points(*card);
+        if (min_points == -1 || points < min_points) {
+            min_points = points;
+            card_index = i;
+        }
+    }
+    VERIFY(card_index.has_value() || type.has_value());
+    return card_index;
+}
+
+Optional<size_t> Player::pick_lower_value_card(Card& other_card)
+{
+    for (ssize_t i = hand.size() - 1; i >= 0; i--) {
+        auto& card = hand[i];
+        if (card.is_null())
+            continue;
+        if (card->type() == other_card.type() && hearts_card_value(*card) < hearts_card_value(other_card))
+            return i;
+    }
+    return {};
+}
+
+Optional<size_t> Player::pick_slightly_higher_value_card(Card& other_card)
+{
+    for (size_t i = 0; i < hand.size(); i++) {
+        auto& card = hand[i];
+        if (card.is_null())
+            continue;
+        if (card->type() == other_card.type() && hearts_card_value(*card) > hearts_card_value(other_card))
+            return i;
+    }
+    return {};
+}
+
+size_t Player::pick_max_points_card()
+{
+    auto queen_of_spades_maybe = pick_specific_card(Card::Type::Spades, CardValue::Queen);
+    if (queen_of_spades_maybe.has_value())
+        return queen_of_spades_maybe.value();
+    if (has_card_of_type(Card::Type::Hearts))
+        return pick_last_card();
+    return pick_low_points_high_value_card().value();
+}
+
+Optional<size_t> Player::pick_specific_card(Card::Type type, CardValue value)
+{
+    for (size_t i = 0; i < hand.size(); i++) {
+        auto& card = hand[i];
+        if (card.is_null())
+            continue;
+        if (card->type() == type && hearts_card_value(*card) == value)
+            return i;
+    }
+    return {};
+}
+
+size_t Player::pick_last_card()
+{
+    for (ssize_t i = hand.size() - 1; i >= 0; i--) {
+        auto& card = hand[i];
+        if (card.is_null())
+            continue;
+        return i;
+    }
+    VERIFY_NOT_REACHED();
+}
+
+bool Player::has_card_of_type(Card::Type type)
+{
+    auto matching_card = hand.first_matching([&](auto const& other_card) {
+        return !other_card.is_null() && other_card->type() == type;
+    });
+    return matching_card.has_value();
+}
+
+}

+ 51 - 0
Userland/Games/Hearts/Player.h

@@ -0,0 +1,51 @@
+/*
+ * Copyright (c) 2021, Gunnar Beutner <gbeutner@serenityos.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#pragma once
+
+#include "Helpers.h"
+#include <LibCards/Card.h>
+
+using Cards::Card;
+
+namespace Hearts {
+
+struct Player {
+    AK_MAKE_NONMOVABLE(Player);
+
+public:
+    Player()
+    {
+    }
+
+    size_t pick_low_points_low_value_card();
+    Optional<size_t> pick_low_points_high_value_card(Optional<Card::Type> type = {});
+    Optional<size_t> pick_lower_value_card(Card& other_card);
+    Optional<size_t> pick_slightly_higher_value_card(Card& other_card);
+    size_t pick_max_points_card();
+    Optional<size_t> pick_specific_card(Card::Type type, CardValue value);
+    size_t pick_last_card();
+    bool has_card_of_type(Card::Type type);
+
+    Vector<RefPtr<Card>> hand;
+    Vector<RefPtr<Card>> cards_taken;
+    Gfx::IntPoint first_card_position;
+    Gfx::IntPoint card_offset;
+    Gfx::IntRect name_position;
+    Gfx::TextAlignment name_alignment;
+    Gfx::IntPoint taken_cards_target;
+    String name;
+};
+
+}
+
+template<>
+struct AK::Formatter<Hearts::Player> : Formatter<FormatString> {
+    void format(FormatBuilder& builder, Hearts::Player const& player)
+    {
+        builder.put_string(player.name);
+    }
+};

+ 100 - 0
Userland/Games/Hearts/main.cpp

@@ -0,0 +1,100 @@
+/*
+ * Copyright (c) 2020, Till Mayer <till.mayer@web.de>
+ * Copyright (c) 2021, Gunnar Beutner <gbeutner@serenityos.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include "Game.h"
+#include <Games/Hearts/HeartsGML.h>
+#include <LibCore/ConfigFile.h>
+#include <LibCore/Timer.h>
+#include <LibGUI/Action.h>
+#include <LibGUI/ActionGroup.h>
+#include <LibGUI/Application.h>
+#include <LibGUI/Icon.h>
+#include <LibGUI/Menu.h>
+#include <LibGUI/Menubar.h>
+#include <LibGUI/MessageBox.h>
+#include <LibGUI/Statusbar.h>
+#include <LibGUI/Window.h>
+#include <stdio.h>
+#include <unistd.h>
+
+int main(int argc, char** argv)
+{
+    auto app = GUI::Application::construct(argc, argv);
+    auto app_icon = GUI::Icon::default_icon("app-hearts");
+    auto config = Core::ConfigFile::get_for_app("Hearts");
+
+    if (pledge("stdio recvfd sendfd rpath wpath cpath", nullptr) < 0) {
+        perror("pledge");
+        return 1;
+    }
+
+    if (unveil("/res", "r") < 0) {
+        perror("unveil");
+        return 1;
+    }
+
+    if (unveil(config->filename().characters(), "crw") < 0) {
+        perror("unveil");
+        return 1;
+    }
+
+    if (unveil(nullptr, nullptr) < 0) {
+        perror("unveil");
+        return 1;
+    }
+
+    auto window = GUI::Window::construct();
+    window->set_title("Hearts");
+
+    auto& widget = window->set_main_widget<GUI::Widget>();
+    widget.load_from_gml(hearts_gml);
+
+    auto& game = *widget.find_descendant_of_type_named<Hearts::Game>("game");
+    game.set_focus(true);
+
+    auto& statusbar = *widget.find_descendant_of_type_named<GUI::Statusbar>("statusbar");
+    statusbar.set_text(0, "Score: 0");
+
+    game.on_status_change = [&](const AK::StringView& status) {
+        statusbar.set_override_text(status);
+    };
+
+    app->on_action_enter = [&](GUI::Action& action) {
+        auto text = action.status_tip();
+        if (text.is_empty())
+            text = Gfx::parse_ampersand_string(action.text());
+        statusbar.set_override_text(move(text));
+    };
+
+    app->on_action_leave = [&](GUI::Action&) {
+        statusbar.set_override_text({});
+    };
+
+    GUI::ActionGroup draw_settng_actions;
+    draw_settng_actions.set_exclusive(true);
+
+    auto menubar = GUI::Menubar::construct();
+    auto& game_menu = menubar->add_menu("&Game");
+
+    game_menu.add_action(GUI::Action::create("&New Game", { Mod_None, Key_F2 }, [&](auto&) {
+        game.setup();
+    }));
+    game_menu.add_separator();
+    game_menu.add_action(GUI::CommonActions::make_quit_action([&](auto&) { app->quit(); }));
+
+    auto& help_menu = menubar->add_menu("&Help");
+    help_menu.add_action(GUI::CommonActions::make_about_action("Hearts", app_icon, window));
+
+    window->set_resizable(false);
+    window->resize(Hearts::Game::width, Hearts::Game::height + statusbar.max_height());
+    window->set_menubar(move(menubar));
+    window->set_icon(app_icon.bitmap_for_size(16));
+    window->show();
+    game.setup();
+
+    return app->exec();
+}

+ 1 - 1
Userland/Libraries/LibCards/Card.h

@@ -30,8 +30,8 @@ public:
     enum Type {
         Clubs,
         Diamonds,
-        Hearts,
         Spades,
+        Hearts,
         __Count
     };