diff --git a/AK/Debug.h.in b/AK/Debug.h.in index ee1a9311c8..4b6c4d767e 100644 --- a/AK/Debug.h.in +++ b/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 diff --git a/Base/res/apps/Hearts.af b/Base/res/apps/Hearts.af new file mode 100644 index 0000000000..49cd5d09d1 --- /dev/null +++ b/Base/res/apps/Hearts.af @@ -0,0 +1,4 @@ +[App] +Name=Hearts +Executable=/bin/Hearts +Category=Games diff --git a/Base/res/icons/16x16/app-hearts.png b/Base/res/icons/16x16/app-hearts.png new file mode 100644 index 0000000000..f2d24a43e2 Binary files /dev/null and b/Base/res/icons/16x16/app-hearts.png differ diff --git a/Base/res/icons/32x32/app-hearts.png b/Base/res/icons/32x32/app-hearts.png new file mode 100644 index 0000000000..8e37fd149d Binary files /dev/null and b/Base/res/icons/32x32/app-hearts.png differ diff --git a/Meta/CMake/all_the_debug_macros.cmake b/Meta/CMake/all_the_debug_macros.cmake index d9480516f1..dc377d61f5 100644 --- a/Meta/CMake/all_the_debug_macros.cmake +++ b/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) diff --git a/Userland/Games/CMakeLists.txt b/Userland/Games/CMakeLists.txt index fabe700499..c1cd514220 100644 --- a/Userland/Games/CMakeLists.txt +++ b/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) diff --git a/Userland/Games/Hearts/CMakeLists.txt b/Userland/Games/Hearts/CMakeLists.txt new file mode 100644 index 0000000000..8d240e507b --- /dev/null +++ b/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) diff --git a/Userland/Games/Hearts/Game.cpp b/Userland/Games/Hearts/Game.cpp new file mode 100644 index 0000000000..5298bbb3cf --- /dev/null +++ b/Userland/Games/Hearts/Game.cpp @@ -0,0 +1,521 @@ +/* + * Copyright (c) 2020, Till Mayer + * Copyright (c) 2021, Gunnar Beutner + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include "Game.h" +#include "Helpers.h" +#include +#include +#include +#include +#include +#include + +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 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 cards, Gfx::IntPoint const& end, Function 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>(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; + 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 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 min_score; + Optional 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(" "); + else + dbgln(" {}", *card); + dbgln("Taken:"); + for (const auto& card : player.cards_taken) + dbgln(" {}", card); + } + } +} + +} diff --git a/Userland/Games/Hearts/Game.h b/Userland/Games/Hearts/Game.h new file mode 100644 index 0000000000..03f2d6c970 --- /dev/null +++ b/Userland/Games/Hearts/Game.h @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2020, Till Mayer + * Copyright (c) 2021, Gunnar Beutner + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include "Player.h" +#include +#include +#include +#include + +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 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 cards, Gfx::IntPoint const& end, Function 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 m_trick; + Player* m_leading_player { nullptr }; + u8 m_trick_number { 0 }; + RefPtr m_delay_timer; + bool m_human_can_play { false }; + + struct AnimatedCard { + NonnullRefPtr card; + Gfx::IntPoint start; + }; + + RefPtr m_animation_delay_timer; + bool m_animation_playing { false }; + Vector m_animation_cards; + Gfx::IntPoint m_animation_end; + int m_animation_current_step { 0 }; + int m_animation_steps { 0 }; + OwnPtr> m_animation_did_finish; +}; + +} diff --git a/Userland/Games/Hearts/Hearts.gml b/Userland/Games/Hearts/Hearts.gml new file mode 100644 index 0000000000..ce02c66671 --- /dev/null +++ b/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" + } +} diff --git a/Userland/Games/Hearts/Helpers.h b/Userland/Games/Hearts/Helpers.h new file mode 100644 index 0000000000..b2e5766e7d --- /dev/null +++ b/Userland/Games/Hearts/Helpers.h @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2021, Gunnar Beutner + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include + +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(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& card1, RefPtr& card2) +{ + if (card1->type() != card2->type()) + return card1->type() < card2->type(); + else + return hearts_card_value(*card1) < hearts_card_value(*card2); +} + +} diff --git a/Userland/Games/Hearts/Player.cpp b/Userland/Games/Hearts/Player.cpp new file mode 100644 index 0000000000..ab129f4739 --- /dev/null +++ b/Userland/Games/Hearts/Player.cpp @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2021, Gunnar Beutner + * + * 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(value) > min_value)) + continue; + min_points = points; + min_value = static_cast(value); + card_index = i; + } + VERIFY(card_index != -1); + return card_index; +} + +Optional Player::pick_low_points_high_value_card(Optional type) +{ + int min_points = -1; + Optional 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 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 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 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(); +} + +} diff --git a/Userland/Games/Hearts/Player.h b/Userland/Games/Hearts/Player.h new file mode 100644 index 0000000000..be1febd6a9 --- /dev/null +++ b/Userland/Games/Hearts/Player.h @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2021, Gunnar Beutner + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include "Helpers.h" +#include + +using Cards::Card; + +namespace Hearts { + +struct Player { + AK_MAKE_NONMOVABLE(Player); + +public: + Player() + { + } + + size_t pick_low_points_low_value_card(); + Optional pick_low_points_high_value_card(Optional type = {}); + Optional pick_lower_value_card(Card& other_card); + Optional pick_slightly_higher_value_card(Card& other_card); + size_t pick_max_points_card(); + Optional pick_specific_card(Card::Type type, CardValue value); + size_t pick_last_card(); + bool has_card_of_type(Card::Type type); + + Vector> hand; + Vector> 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 : Formatter { + void format(FormatBuilder& builder, Hearts::Player const& player) + { + builder.put_string(player.name); + } +}; diff --git a/Userland/Games/Hearts/main.cpp b/Userland/Games/Hearts/main.cpp new file mode 100644 index 0000000000..43a3292f4c --- /dev/null +++ b/Userland/Games/Hearts/main.cpp @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2020, Till Mayer + * Copyright (c) 2021, Gunnar Beutner + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include "Game.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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(); + widget.load_from_gml(hearts_gml); + + auto& game = *widget.find_descendant_of_type_named("game"); + game.set_focus(true); + + auto& statusbar = *widget.find_descendant_of_type_named("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(); +} diff --git a/Userland/Libraries/LibCards/Card.h b/Userland/Libraries/LibCards/Card.h index 7c2fe41d0a..7710e7f21c 100644 --- a/Userland/Libraries/LibCards/Card.h +++ b/Userland/Libraries/LibCards/Card.h @@ -30,8 +30,8 @@ public: enum Type { Clubs, Diamonds, - Hearts, Spades, + Hearts, __Count };