Serenity Operating System
at master 367 lines 10 kB view raw
1/* 2 * Copyright (c) 2020, Till Mayer <till.mayer@web.de> 3 * 4 * SPDX-License-Identifier: BSD-2-Clause 5 */ 6 7#include "CardStack.h" 8 9namespace Cards { 10 11CardStack::CardStack() 12 : m_position({ 0, 0 }) 13 , m_type(Type::Invalid) 14 , m_base(m_position, { Card::width, Card::height }) 15{ 16} 17 18CardStack::CardStack(Gfx::IntPoint position, Type type, RefPtr<CardStack> covered_stack) 19 : m_covered_stack(move(covered_stack)) 20 , m_position(position) 21 , m_type(type) 22 , m_rules(rules_for_type(type)) 23 , m_base(m_position, { Card::width, Card::height }) 24{ 25 VERIFY(type != Type::Invalid); 26 calculate_bounding_box(); 27} 28 29void CardStack::clear() 30{ 31 m_stack.clear(); 32 m_stack_positions.clear(); 33} 34 35void CardStack::paint(GUI::Painter& painter, Gfx::Color background_color) 36{ 37 auto draw_background_if_empty = [&]() { 38 size_t number_of_moving_cards = 0; 39 for (auto const& card : m_stack) 40 number_of_moving_cards += card->is_moving() ? 1 : 0; 41 42 if (m_covered_stack && !m_covered_stack->is_empty()) 43 return false; 44 if (!is_empty() && (m_stack.size() != number_of_moving_cards)) 45 return false; 46 47 auto paint_rect = m_base; 48 painter.fill_rect_with_rounded_corners(paint_rect, background_color.darkened(0.5), Card::card_radius); 49 paint_rect.shrink(2, 2); 50 51 if (m_highlighted) { 52 auto background_complement = background_color.xored(Color::White); 53 painter.fill_rect_with_rounded_corners(paint_rect, background_complement, Card::card_radius - 1); 54 paint_rect.shrink(4, 4); 55 } 56 57 painter.fill_rect_with_rounded_corners(paint_rect, background_color, Card::card_radius - 1); 58 return true; 59 }; 60 61 switch (m_type) { 62 case Type::Stock: 63 if (draw_background_if_empty()) { 64 painter.fill_rect(m_base.shrunken(Card::width / 4, Card::height / 4), background_color.lightened(1.5)); 65 painter.fill_rect(m_base.shrunken(Card::width / 2, Card::height / 2), background_color); 66 } 67 break; 68 case Type::Foundation: 69 if (draw_background_if_empty()) { 70 for (int y = 0; y < (m_base.height() - 4) / 8; ++y) { 71 for (int x = 0; x < (m_base.width() - 4) / 5; ++x) { 72 painter.draw_rect({ 4 + m_base.x() + x * 5, 4 + m_base.y() + y * 8, 1, 1 }, background_color.darkened(0.5)); 73 } 74 } 75 } 76 break; 77 case Type::Play: 78 case Type::Normal: 79 draw_background_if_empty(); 80 break; 81 case Type::Waste: 82 break; 83 default: 84 VERIFY_NOT_REACHED(); 85 } 86 87 if (is_empty()) 88 return; 89 90 if (m_rules.shift_x == 0 && m_rules.shift_y == 0) { 91 auto& card = peek(); 92 card.paint(painter); 93 return; 94 } 95 96 RefPtr<Card> previewed_card; 97 98 for (size_t i = 0; i < m_stack.size(); ++i) { 99 if (auto& card = m_stack[i]; !card->is_moving()) { 100 if (card->is_previewed()) { 101 VERIFY(!previewed_card); 102 previewed_card = card; 103 continue; 104 } 105 106 auto highlighted = m_highlighted && (i == m_stack.size() - 1); 107 card->clear_and_paint(painter, Gfx::Color::Transparent, highlighted); 108 } 109 } 110 111 if (previewed_card) 112 previewed_card->clear_and_paint(painter, Gfx::Color::Transparent, false); 113} 114 115void CardStack::rebound_cards() 116{ 117 VERIFY(m_stack_positions.size() == m_stack.size()); 118 119 size_t card_index = 0; 120 for (auto& card : m_stack) 121 card->set_position(m_stack_positions.at(card_index++)); 122} 123 124ErrorOr<void> CardStack::add_all_grabbed_cards(Gfx::IntPoint click_location, Vector<NonnullRefPtr<Card>>& grabbed, MovementRule movement_rule) 125{ 126 VERIFY(grabbed.is_empty()); 127 128 if (m_type != Type::Normal) { 129 auto& top_card = peek(); 130 if (top_card.rect().contains(click_location)) { 131 top_card.set_moving(true); 132 TRY(grabbed.try_append(top_card)); 133 } 134 return {}; 135 } 136 137 RefPtr<Card> last_intersect; 138 139 for (auto& card : m_stack) { 140 if (card->rect().contains(click_location)) { 141 if (card->is_upside_down()) 142 continue; 143 144 last_intersect = card; 145 } else if (!last_intersect.is_null()) { 146 if (grabbed.is_empty()) { 147 TRY(grabbed.try_append(*last_intersect)); 148 last_intersect->set_moving(true); 149 } 150 151 if (card->is_upside_down()) { 152 grabbed.clear(); 153 return {}; 154 } 155 156 card->set_moving(true); 157 TRY(grabbed.try_append(card)); 158 } 159 } 160 161 if (grabbed.is_empty() && !last_intersect.is_null()) { 162 TRY(grabbed.try_append(*last_intersect)); 163 last_intersect->set_moving(true); 164 } 165 166 // verify valid stack 167 bool valid_stack = true; 168 uint8_t last_value; 169 Color last_color; 170 for (size_t i = 0; i < grabbed.size(); i++) { 171 auto& card = grabbed.at(i); 172 if (i != 0) { 173 bool color_match; 174 switch (movement_rule) { 175 case MovementRule::Alternating: 176 color_match = card->color() != last_color; 177 break; 178 case MovementRule::Same: 179 color_match = card->color() == last_color; 180 break; 181 case MovementRule::Any: 182 color_match = true; 183 break; 184 } 185 186 if (!color_match || to_underlying(card->rank()) != last_value - 1) { 187 valid_stack = false; 188 break; 189 } 190 } 191 last_value = to_underlying(card->rank()); 192 last_color = card->color(); 193 } 194 195 if (!valid_stack) { 196 for (auto& card : grabbed) { 197 card->set_moving(false); 198 } 199 grabbed.clear(); 200 } 201 202 return {}; 203} 204 205bool CardStack::is_allowed_to_push(Card const& card, size_t stack_size, MovementRule movement_rule) const 206{ 207 if (m_type == Type::Stock || m_type == Type::Waste || m_type == Type::Play) 208 return false; 209 210 if (m_type == Type::Normal && is_empty()) { 211 // FIXME: proper solution for this 212 if (movement_rule == MovementRule::Alternating) { 213 return card.rank() == Rank::King; 214 } 215 return true; 216 } 217 218 if (m_type == Type::Foundation && is_empty()) 219 return card.rank() == Rank::Ace; 220 221 if (!is_empty()) { 222 auto const& top_card = peek(); 223 if (top_card.is_upside_down()) 224 return false; 225 226 if (m_type == Type::Foundation) { 227 // Prevent player from dragging an entire stack of cards to the foundation stack 228 if (stack_size > 1) 229 return false; 230 return top_card.suit() == card.suit() && m_stack.size() == to_underlying(card.rank()); 231 } 232 if (m_type == Type::Normal) { 233 bool color_match; 234 switch (movement_rule) { 235 case MovementRule::Alternating: 236 color_match = card.color() != top_card.color(); 237 break; 238 case MovementRule::Same: 239 color_match = card.color() == top_card.color(); 240 break; 241 case MovementRule::Any: 242 color_match = true; 243 break; 244 } 245 246 return color_match && to_underlying(top_card.rank()) == to_underlying(card.rank()) + 1; 247 } 248 249 VERIFY_NOT_REACHED(); 250 } 251 252 return true; 253} 254 255bool CardStack::preview_card(Gfx::IntPoint click_location) 256{ 257 RefPtr<Card> last_intersect; 258 259 for (auto& card : m_stack) { 260 if (!card->rect().contains(click_location)) 261 continue; 262 if (card->is_upside_down()) 263 continue; 264 265 last_intersect = card; 266 } 267 268 if (!last_intersect) 269 return false; 270 271 last_intersect->set_previewed(true); 272 return true; 273} 274 275void CardStack::clear_card_preview() 276{ 277 for (auto& card : m_stack) 278 card->set_previewed(false); 279} 280 281bool CardStack::make_top_card_visible() 282{ 283 if (is_empty()) 284 return false; 285 286 auto& top_card = peek(); 287 if (top_card.is_upside_down()) { 288 top_card.set_upside_down(false); 289 return true; 290 } 291 292 return false; 293} 294 295ErrorOr<void> CardStack::push(NonnullRefPtr<Card> card) 296{ 297 auto top_most_position = m_stack_positions.is_empty() ? m_position : m_stack_positions.last(); 298 299 if (!m_stack.is_empty() && m_stack.size() % m_rules.step == 0) { 300 if (peek().is_upside_down()) 301 top_most_position.translate_by(m_rules.shift_x, m_rules.shift_y_upside_down); 302 else 303 top_most_position.translate_by(m_rules.shift_x, m_rules.shift_y); 304 } 305 306 if (m_type == Type::Stock) 307 card->set_upside_down(true); 308 309 card->set_position(top_most_position); 310 311 TRY(m_stack.try_append(card)); 312 TRY(m_stack_positions.try_append(top_most_position)); 313 calculate_bounding_box(); 314 return {}; 315} 316 317NonnullRefPtr<Card> CardStack::pop() 318{ 319 auto card = m_stack.take_last(); 320 321 calculate_bounding_box(); 322 if (m_type == Type::Stock) 323 card->set_upside_down(false); 324 325 m_stack_positions.take_last(); 326 return card; 327} 328 329ErrorOr<void> CardStack::take_all(CardStack& stack) 330{ 331 while (!m_stack.is_empty()) { 332 auto card = m_stack.take_first(); 333 m_stack_positions.take_first(); 334 TRY(stack.push(move(card))); 335 } 336 337 calculate_bounding_box(); 338 return {}; 339} 340 341void CardStack::calculate_bounding_box() 342{ 343 m_bounding_box = Gfx::IntRect(m_position, { Card::width, Card::height }); 344 345 if (m_stack.is_empty()) 346 return; 347 348 uint16_t width = 0; 349 uint16_t height = 0; 350 size_t card_position = 0; 351 for (auto& card : m_stack) { 352 if (card_position % m_rules.step == 0 && card_position != 0) { 353 if (card->is_upside_down()) { 354 width += m_rules.shift_x; 355 height += m_rules.shift_y_upside_down; 356 } else { 357 width += m_rules.shift_x; 358 height += m_rules.shift_y; 359 } 360 } 361 ++card_position; 362 } 363 364 m_bounding_box.set_size(Card::width + width, Card::height + height); 365} 366 367}