Serenity Operating System
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}