mirror of
				https://github.com/RGBCube/serenity
				synced 2025-10-31 22:02:44 +00:00 
			
		
		
		
	
							parent
							
								
									fab073f33c
								
							
						
					
					
						commit
						ac1eba2f7c
					
				
					 4 changed files with 248 additions and 5 deletions
				
			
		|  | @ -9,6 +9,7 @@ | ||||||
| #include "Helpers.h" | #include "Helpers.h" | ||||||
| #include <AK/Debug.h> | #include <AK/Debug.h> | ||||||
| #include <AK/QuickSort.h> | #include <AK/QuickSort.h> | ||||||
|  | #include <LibGUI/Button.h> | ||||||
| #include <LibGUI/Painter.h> | #include <LibGUI/Painter.h> | ||||||
| #include <LibGfx/Font.h> | #include <LibGfx/Font.h> | ||||||
| #include <LibGfx/Palette.h> | #include <LibGfx/Palette.h> | ||||||
|  | @ -23,6 +24,7 @@ Game::Game() | ||||||
|     srand(time(nullptr)); |     srand(time(nullptr)); | ||||||
| 
 | 
 | ||||||
|     m_delay_timer = Core::Timer::create_single_shot(0, [this] { |     m_delay_timer = Core::Timer::create_single_shot(0, [this] { | ||||||
|  |         dbgln_if(HEARTS_DEBUG, "Continuing game after delay..."); | ||||||
|         advance_game(); |         advance_game(); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|  | @ -74,6 +76,18 @@ Game::Game() | ||||||
|     m_players[3].name = "Lisa"; |     m_players[3].name = "Lisa"; | ||||||
|     m_players[3].taken_cards_target = { width, height / 2 - Card::height / 2 }; |     m_players[3].taken_cards_target = { width, height / 2 - Card::height / 2 }; | ||||||
| 
 | 
 | ||||||
|  |     m_passing_button = add<GUI::Button>("Pass Left"); | ||||||
|  |     constexpr int button_width = 120; | ||||||
|  |     constexpr int button_height = 30; | ||||||
|  |     m_passing_button->set_relative_rect(width / 2 - button_width / 2, height - 3 * outer_border_size - Card::height - button_height, button_width, button_height); | ||||||
|  |     m_passing_button->on_click = [this](unsigned int) { | ||||||
|  |         if (m_state == State::PassingSelect) | ||||||
|  |             m_state = State::PassingSelectConfirmed; | ||||||
|  |         else | ||||||
|  |             m_state = State::Play; | ||||||
|  |         advance_game(); | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|     reset(); |     reset(); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | @ -88,6 +102,31 @@ void Game::reset() | ||||||
| 
 | 
 | ||||||
|     stop_animation(); |     stop_animation(); | ||||||
| 
 | 
 | ||||||
|  |     m_hand_number = 0; | ||||||
|  | 
 | ||||||
|  |     m_passing_button->set_enabled(false); | ||||||
|  |     m_passing_button->set_visible(false); | ||||||
|  | 
 | ||||||
|  |     if (m_hand_number % 4 != 3) { | ||||||
|  |         m_state = State::PassingSelect; | ||||||
|  |         m_human_can_play = true; | ||||||
|  |         switch (passing_direction()) { | ||||||
|  |         case PassingDirection::Left: | ||||||
|  |             m_passing_button->set_text("Pass Left"); | ||||||
|  |             break; | ||||||
|  |         case PassingDirection::Across: | ||||||
|  |             m_passing_button->set_text("Pass Across"); | ||||||
|  |             break; | ||||||
|  |         case PassingDirection::Right: | ||||||
|  |             m_passing_button->set_text("Pass Right"); | ||||||
|  |             break; | ||||||
|  |         default: | ||||||
|  |             VERIFY_NOT_REACHED(); | ||||||
|  |         } | ||||||
|  |     } else | ||||||
|  |         m_state = State::Play; | ||||||
|  |     m_cards_highlighted.clear(); | ||||||
|  | 
 | ||||||
|     m_trick.clear_with_capacity(); |     m_trick.clear_with_capacity(); | ||||||
|     m_trick_number = 0; |     m_trick_number = 0; | ||||||
| 
 | 
 | ||||||
|  | @ -103,6 +142,11 @@ void Game::setup(String player_name) | ||||||
| 
 | 
 | ||||||
|     reset(); |     reset(); | ||||||
| 
 | 
 | ||||||
|  |     if (m_hand_number % 4 != 3) { | ||||||
|  |         m_passing_button->set_visible(true); | ||||||
|  |         m_passing_button->set_focus(false); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     NonnullRefPtrVector<Card> deck; |     NonnullRefPtrVector<Card> deck; | ||||||
| 
 | 
 | ||||||
|     for (int i = 0; i < Card::card_count; ++i) { |     for (int i = 0; i < Card::card_count; ++i) { | ||||||
|  | @ -125,7 +169,9 @@ void Game::setup(String player_name) | ||||||
|         reposition_hand(player); |         reposition_hand(player); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     advance_game(); |     update(); | ||||||
|  | 
 | ||||||
|  |     continue_game_after_delay(); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| void Game::start_animation(NonnullRefPtrVector<Card> cards, Gfx::IntPoint const& end, Function<void()> did_finish_callback, int initial_delay_ms, int steps) | void Game::start_animation(NonnullRefPtrVector<Card> cards, Gfx::IntPoint const& end, Function<void()> did_finish_callback, int initial_delay_ms, int steps) | ||||||
|  | @ -296,6 +342,32 @@ void Game::advance_game() | ||||||
|         return; |         return; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     if (m_state == State::PassingSelect) { | ||||||
|  |         if (!m_players[0].is_human) { | ||||||
|  |             select_cards_for_passing(); | ||||||
|  |             m_state = State::PassingSelectConfirmed; | ||||||
|  |             continue_game_after_delay(); | ||||||
|  |         } | ||||||
|  |         return; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (m_state == State::PassingSelectConfirmed) { | ||||||
|  |         pass_cards(); | ||||||
|  |         continue_game_after_delay(); | ||||||
|  |         return; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (m_state == State::PassingAccept) { | ||||||
|  |         if (!m_players[0].is_human) { | ||||||
|  |             m_state = State::Play; | ||||||
|  |             continue_game_after_delay(); | ||||||
|  |         } | ||||||
|  |         return; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     clear_highlighted_cards(); | ||||||
|  |     m_passing_button->set_visible(false); | ||||||
|  | 
 | ||||||
|     if (m_trick_number == 0 && m_trick.is_empty()) { |     if (m_trick_number == 0 && m_trick.is_empty()) { | ||||||
|         // Find whoever has 2 of Clubs, they get to play the first card
 |         // Find whoever has 2 of Clubs, they get to play the first card
 | ||||||
|         for (auto& player : m_players) { |         for (auto& player : m_players) { | ||||||
|  | @ -364,8 +436,10 @@ void Game::keydown_event(GUI::KeyEvent& event) | ||||||
|         m_players[0].is_human = !m_players[0].is_human; |         m_players[0].is_human = !m_players[0].is_human; | ||||||
|         advance_game(); |         advance_game(); | ||||||
|     } else if (event.key() == KeyCode::Key_F10) { |     } else if (event.key() == KeyCode::Key_F10) { | ||||||
|         if (m_human_can_play) |         if (m_human_can_play && m_state == State::Play) | ||||||
|             play_card(m_players[0], pick_card(m_players[0])); |             play_card(m_players[0], pick_card(m_players[0])); | ||||||
|  |         else if (m_state == State::PassingSelect) | ||||||
|  |             select_cards_for_passing(); | ||||||
|     } else if (event.shift() && event.key() == KeyCode::Key_F11) |     } else if (event.shift() && event.key() == KeyCode::Key_F11) | ||||||
|         dump_state(); |         dump_state(); | ||||||
| } | } | ||||||
|  | @ -464,7 +538,18 @@ bool Game::are_hearts_broken() const | ||||||
|     return false; |     return false; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| void Game::card_clicked(size_t card_index, Card& card) | void Game::card_clicked_during_passing(size_t, Card& card) | ||||||
|  | { | ||||||
|  |     if (!is_card_highlighted(card)) { | ||||||
|  |         if (m_cards_highlighted.size() < 3) | ||||||
|  |             highlight_card(card); | ||||||
|  |     } else | ||||||
|  |         unhighlight_card(card); | ||||||
|  | 
 | ||||||
|  |     m_passing_button->set_enabled(m_cards_highlighted.size() == 3); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | void Game::card_clicked_during_play(size_t card_index, Card& card) | ||||||
| { | { | ||||||
|     String explanation; |     String explanation; | ||||||
|     if (!is_valid_play(m_players[0], card, &explanation)) { |     if (!is_valid_play(m_players[0], card, &explanation)) { | ||||||
|  | @ -476,6 +561,14 @@ void Game::card_clicked(size_t card_index, Card& card) | ||||||
|     update(); |     update(); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | void Game::card_clicked(size_t card_index, Card& card) | ||||||
|  | { | ||||||
|  |     if (m_state == State::PassingSelect) | ||||||
|  |         card_clicked_during_passing(card_index, card); | ||||||
|  |     else | ||||||
|  |         card_clicked_during_play(card_index, card); | ||||||
|  | } | ||||||
|  | 
 | ||||||
| void Game::mouseup_event(GUI::MouseEvent& event) | void Game::mouseup_event(GUI::MouseEvent& event) | ||||||
| { | { | ||||||
|     GUI::Frame::mouseup_event(event); |     GUI::Frame::mouseup_event(event); | ||||||
|  | @ -520,15 +613,115 @@ bool Game::is_winner(Player& player) | ||||||
|     return (max_score.value() != sum_points_of_all_cards && player_score == min_score.value()) || player_score == sum_points_of_all_cards; |     return (max_score.value() != sum_points_of_all_cards && player_score == min_score.value()) || player_score == sum_points_of_all_cards; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | static constexpr int card_highlight_offset = -20; | ||||||
|  | 
 | ||||||
|  | bool Game::is_card_highlighted(Card& card) | ||||||
|  | { | ||||||
|  |     return m_cards_highlighted.contains(card); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | void Game::highlight_card(Card& card) | ||||||
|  | { | ||||||
|  |     VERIFY(!m_cards_highlighted.contains(card)); | ||||||
|  |     m_cards_highlighted.set(card); | ||||||
|  |     card.set_position(card.position().translated(0, card_highlight_offset)); | ||||||
|  |     update(); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | void Game::unhighlight_card(Card& card) | ||||||
|  | { | ||||||
|  |     VERIFY(m_cards_highlighted.contains(card)); | ||||||
|  |     m_cards_highlighted.remove(card); | ||||||
|  |     card.set_position(card.position().translated(0, -card_highlight_offset)); | ||||||
|  |     update(); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | void Game::clear_highlighted_cards() | ||||||
|  | { | ||||||
|  |     for (auto& card : m_cards_highlighted) | ||||||
|  |         card->set_position(card->position().translated(0, -card_highlight_offset)); | ||||||
|  |     m_cards_highlighted.clear(); | ||||||
|  | } | ||||||
|  | 
 | ||||||
| void Game::reposition_hand(Player& player) | void Game::reposition_hand(Player& player) | ||||||
| { | { | ||||||
|     auto card_position = player.first_card_position; |     auto card_position = player.first_card_position; | ||||||
|     for (auto& card : player.hand) { |     for (auto& card : player.hand) { | ||||||
|         card->set_position(card_position); |         card->set_position(is_card_highlighted(*card) ? card_position.translated(0, card_highlight_offset) : card_position); | ||||||
|         card_position.translate_by(player.card_offset); |         card_position.translate_by(player.card_offset); | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | void Game::select_cards_for_passing() | ||||||
|  | { | ||||||
|  |     clear_highlighted_cards(); | ||||||
|  |     auto selected_cards = m_players[0].pick_cards_to_pass(passing_direction()); | ||||||
|  |     highlight_card(selected_cards[0]); | ||||||
|  |     highlight_card(selected_cards[1]); | ||||||
|  |     highlight_card(selected_cards[2]); | ||||||
|  |     m_passing_button->set_enabled(true); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | void Game::pass_cards() | ||||||
|  | { | ||||||
|  |     NonnullRefPtrVector<Card> first_player_cards; | ||||||
|  |     for (auto& card : m_cards_highlighted) | ||||||
|  |         first_player_cards.append(*card); | ||||||
|  |     clear_highlighted_cards(); | ||||||
|  |     VERIFY(first_player_cards.size() == 3); | ||||||
|  | 
 | ||||||
|  |     NonnullRefPtrVector<Card> passed_cards[4]; | ||||||
|  |     passed_cards[0] = first_player_cards; | ||||||
|  |     passed_cards[1] = m_players[1].pick_cards_to_pass(passing_direction()); | ||||||
|  |     passed_cards[2] = m_players[2].pick_cards_to_pass(passing_direction()); | ||||||
|  |     passed_cards[3] = m_players[3].pick_cards_to_pass(passing_direction()); | ||||||
|  | 
 | ||||||
|  |     for (size_t i = 0; i < 4; i++) { | ||||||
|  |         m_players[i].remove_cards(passed_cards[i]); | ||||||
|  | 
 | ||||||
|  |         int destination_player_index = i; | ||||||
|  |         switch (passing_direction()) { | ||||||
|  |         case PassingDirection::Left: | ||||||
|  |             destination_player_index += 1; | ||||||
|  |             break; | ||||||
|  |         case PassingDirection::Across: | ||||||
|  |             destination_player_index += 2; | ||||||
|  |             break; | ||||||
|  |         case PassingDirection::Right: | ||||||
|  |             destination_player_index += 3; | ||||||
|  |             break; | ||||||
|  |         default: | ||||||
|  |             VERIFY_NOT_REACHED(); | ||||||
|  |         } | ||||||
|  |         destination_player_index %= 4; | ||||||
|  | 
 | ||||||
|  |         for (auto& card : passed_cards[i]) { | ||||||
|  |             m_players[destination_player_index].hand.append(card); | ||||||
|  |             if constexpr (!HEARTS_DEBUG) | ||||||
|  |                 card.set_upside_down(destination_player_index != 0); | ||||||
|  |             if (destination_player_index == 0) | ||||||
|  |                 highlight_card(card); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     for (auto& player : m_players) { | ||||||
|  |         VERIFY(player.hand.size() == 13); | ||||||
|  |         player.sort_hand(); | ||||||
|  |         reposition_hand(player); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     m_state = State::PassingAccept; | ||||||
|  |     m_passing_button->set_text("OK"); | ||||||
|  |     m_passing_button->set_enabled(true); | ||||||
|  |     update(); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | PassingDirection Game::passing_direction() const | ||||||
|  | { | ||||||
|  |     VERIFY(m_hand_number % 4 != 3); | ||||||
|  |     return static_cast<PassingDirection>(m_hand_number % 4); | ||||||
|  | } | ||||||
|  | 
 | ||||||
| void Game::paint_event(GUI::PaintEvent& event) | void Game::paint_event(GUI::PaintEvent& event) | ||||||
| { | { | ||||||
|     GUI::Frame::paint_event(event); |     GUI::Frame::paint_event(event); | ||||||
|  |  | ||||||
|  | @ -49,7 +49,14 @@ private: | ||||||
|     bool other_player_has_lower_value_card(Player& player, Card& card); |     bool other_player_has_lower_value_card(Player& player, Card& card); | ||||||
|     bool other_player_has_higher_value_card(Player& player, Card& card); |     bool other_player_has_higher_value_card(Player& player, Card& card); | ||||||
| 
 | 
 | ||||||
|     void reposition_hand(Player& player); |     void reposition_hand(Player&); | ||||||
|  |     bool is_card_highlighted(Card& card); | ||||||
|  |     void clear_highlighted_cards(); | ||||||
|  |     void highlight_card(Card& card); | ||||||
|  |     void unhighlight_card(Card& card); | ||||||
|  |     void select_cards_for_passing(); | ||||||
|  |     void pass_cards(); | ||||||
|  |     PassingDirection passing_direction() const; | ||||||
| 
 | 
 | ||||||
|     void start_animation(NonnullRefPtrVector<Card> cards, Gfx::IntPoint const& end, Function<void()> did_finish_callback, int initial_delay_ms, int steps = 30); |     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(); |     void stop_animation(); | ||||||
|  | @ -60,6 +67,22 @@ private: | ||||||
|     virtual void timer_event(Core::TimerEvent&) override; |     virtual void timer_event(Core::TimerEvent&) override; | ||||||
| 
 | 
 | ||||||
|     void card_clicked(size_t card_index, Card& card); |     void card_clicked(size_t card_index, Card& card); | ||||||
|  |     void card_clicked_during_passing(size_t card_index, Card& card); | ||||||
|  |     void card_clicked_during_play(size_t card_index, Card& card); | ||||||
|  | 
 | ||||||
|  |     RefPtr<GUI::Button> m_passing_button; | ||||||
|  | 
 | ||||||
|  |     enum class State { | ||||||
|  |         PassingSelect, | ||||||
|  |         PassingSelectConfirmed, | ||||||
|  |         PassingAccept, | ||||||
|  |         Play, | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     State m_state { State::PassingSelect }; | ||||||
|  |     int m_hand_number { 0 }; | ||||||
|  | 
 | ||||||
|  |     HashTable<NonnullRefPtr<Card>> m_cards_highlighted; | ||||||
| 
 | 
 | ||||||
|     Player m_players[4]; |     Player m_players[4]; | ||||||
|     NonnullRefPtrVector<Card> m_trick; |     NonnullRefPtrVector<Card> m_trick; | ||||||
|  |  | ||||||
|  | @ -11,6 +11,16 @@ | ||||||
| 
 | 
 | ||||||
| namespace Hearts { | namespace Hearts { | ||||||
| 
 | 
 | ||||||
|  | NonnullRefPtrVector<Card> Player::pick_cards_to_pass(PassingDirection) | ||||||
|  | { | ||||||
|  |     auto sorted_hand = hand_sorted_by_points_and_value(); | ||||||
|  |     NonnullRefPtrVector<Card> cards; | ||||||
|  |     cards.append(*sorted_hand[0].card); | ||||||
|  |     cards.append(*sorted_hand[1].card); | ||||||
|  |     cards.append(*sorted_hand[2].card); | ||||||
|  |     return cards; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| Vector<CardWithIndex> Player::hand_sorted_by_points_and_value() const | Vector<CardWithIndex> Player::hand_sorted_by_points_and_value() const | ||||||
| { | { | ||||||
|     Vector<CardWithIndex> sorted_hand; |     Vector<CardWithIndex> sorted_hand; | ||||||
|  | @ -149,4 +159,13 @@ bool Player::has_card_of_type(Card::Type type) | ||||||
|     return matching_card.has_value(); |     return matching_card.has_value(); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | void Player::remove_cards(const NonnullRefPtrVector<Card>& cards) | ||||||
|  | { | ||||||
|  |     for (auto& card : cards) { | ||||||
|  |         hand.remove_first_matching([&card](auto& other_card) { | ||||||
|  |             return other_card == card; | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -14,6 +14,12 @@ using Cards::Card; | ||||||
| 
 | 
 | ||||||
| namespace Hearts { | namespace Hearts { | ||||||
| 
 | 
 | ||||||
|  | enum class PassingDirection { | ||||||
|  |     Left, | ||||||
|  |     Right, | ||||||
|  |     Across | ||||||
|  | }; | ||||||
|  | 
 | ||||||
| struct CardWithIndex { | struct CardWithIndex { | ||||||
|     NonnullRefPtr<Card> card; |     NonnullRefPtr<Card> card; | ||||||
|     size_t index; |     size_t index; | ||||||
|  | @ -27,6 +33,7 @@ public: | ||||||
|     { |     { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     NonnullRefPtrVector<Card> pick_cards_to_pass(PassingDirection); | ||||||
|     size_t pick_lead_card(Function<bool(Card&)>, Function<bool(Card&)>, Function<bool(Card&)>); |     size_t pick_lead_card(Function<bool(Card&)>, Function<bool(Card&)>, Function<bool(Card&)>); | ||||||
|     Optional<size_t> pick_low_points_high_value_card(Optional<Card::Type> type = {}); |     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_lower_value_card(Card& other_card); | ||||||
|  | @ -38,6 +45,7 @@ public: | ||||||
|     Vector<CardWithIndex> hand_sorted_by_points_and_value() const; |     Vector<CardWithIndex> hand_sorted_by_points_and_value() const; | ||||||
| 
 | 
 | ||||||
|     void sort_hand() { quick_sort(hand, hearts_card_less); } |     void sort_hand() { quick_sort(hand, hearts_card_less); } | ||||||
|  |     void remove_cards(NonnullRefPtrVector<Card> const& cards); | ||||||
| 
 | 
 | ||||||
|     Vector<RefPtr<Card>> hand; |     Vector<RefPtr<Card>> hand; | ||||||
|     Vector<RefPtr<Card>> cards_taken; |     Vector<RefPtr<Card>> cards_taken; | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Gunnar Beutner
						Gunnar Beutner