ColorLines.cpp 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399
  1. /*
  2. * Copyright (c) 2022, Oleg Kosenkov <oleg@kosenkov.ca>
  3. * Copyright (c) 2022, the SerenityOS developers.
  4. *
  5. * SPDX-License-Identifier: BSD-2-Clause
  6. */
  7. #include "ColorLines.h"
  8. #include "HueFilter.h"
  9. #include "Marble.h"
  10. #include "MarbleBoard.h"
  11. #include <AK/String.h>
  12. #include <LibConfig/Client.h>
  13. #include <LibGUI/MessageBox.h>
  14. #include <LibGUI/Painter.h>
  15. #include <LibGfx/Font/Emoji.h>
  16. ColorLines::BitmapArray ColorLines::build_marble_color_bitmaps()
  17. {
  18. auto marble_bitmap = MUST(Gfx::Bitmap::load_from_file("/res/icons/colorlines/colorlines.png"sv));
  19. float constexpr hue_degrees[Marble::number_of_colors] = {
  20. 0, // Red
  21. 45, // Brown/Yellow
  22. 90, // Green
  23. 180, // Cyan
  24. 225, // Blue
  25. 300 // Purple
  26. };
  27. BitmapArray colored_bitmaps;
  28. colored_bitmaps.ensure_capacity(Marble::number_of_colors);
  29. for (int i = 0; i < Marble::number_of_colors; ++i) {
  30. auto bitmap = MUST(marble_bitmap->clone());
  31. HueFilter filter { hue_degrees[i] };
  32. filter.apply(*bitmap, bitmap->rect(), *marble_bitmap, marble_bitmap->rect());
  33. colored_bitmaps.append(bitmap);
  34. }
  35. return colored_bitmaps;
  36. }
  37. ColorLines::BitmapArray ColorLines::build_marble_trace_bitmaps()
  38. {
  39. // Use "Paw Prints" Unicode Character (U+1F43E)
  40. auto trace_bitmap = NonnullRefPtr<Gfx::Bitmap const>(*Gfx::Emoji::emoji_for_code_point(0x1F43E));
  41. BitmapArray result;
  42. result.ensure_capacity(number_of_marble_trace_bitmaps);
  43. result.append(trace_bitmap);
  44. result.append(MUST(result.last()->rotated(Gfx::RotationDirection::Clockwise)));
  45. result.append(MUST(result.last()->rotated(Gfx::RotationDirection::Clockwise)));
  46. result.append(MUST(result.last()->rotated(Gfx::RotationDirection::Clockwise)));
  47. return result;
  48. }
  49. ColorLines::ColorLines(StringView app_name)
  50. : m_app_name { app_name }
  51. , m_game_state { GameState::Idle }
  52. , m_board { make<MarbleBoard>() }
  53. , m_marble_bitmaps { build_marble_color_bitmaps() }
  54. , m_trace_bitmaps { build_marble_trace_bitmaps() }
  55. , m_score_font { Gfx::BitmapFont::load_from_file("/res/fonts/MarietaBold24.font") }
  56. {
  57. VERIFY(m_marble_bitmaps.size() == Marble::number_of_colors);
  58. set_font(Gfx::FontDatabase::default_fixed_width_font().bold_variant());
  59. m_high_score = Config::read_i32(m_app_name, m_app_name, "HighScore"sv, 0);
  60. reset();
  61. }
  62. void ColorLines::reset()
  63. {
  64. set_game_state(GameState::StartingGame);
  65. }
  66. void ColorLines::mousedown_event(GUI::MouseEvent& event)
  67. {
  68. if (m_game_state != GameState::Idle && m_game_state != GameState::MarbleSelected)
  69. return;
  70. auto const event_position = event.position().translated(
  71. -frame_inner_rect().x(),
  72. -frame_inner_rect().y() - board_vertical_margin);
  73. if (event_position.x() < 0 || event_position.y() < 0)
  74. return;
  75. auto const clicked_cell = Point { event_position.x() / board_cell_dimension.width(),
  76. event_position.y() / board_cell_dimension.height() };
  77. if (!MarbleBoard::in_bounds(clicked_cell))
  78. return;
  79. if (m_board->has_selected_marble()) {
  80. auto const selected_cell = m_board->selected_marble().position();
  81. if (selected_cell == clicked_cell) {
  82. m_board->reset_selection();
  83. set_game_state(GameState::Idle);
  84. return;
  85. }
  86. if (m_board->is_empty_cell_at(clicked_cell)) {
  87. if (m_board->build_marble_path(selected_cell, clicked_cell, m_marble_path))
  88. set_game_state(GameState::MarbleMoving);
  89. return;
  90. }
  91. if (m_board->select_marble(clicked_cell))
  92. set_game_state(GameState::MarbleSelected);
  93. return;
  94. }
  95. if (m_board->select_marble(clicked_cell))
  96. set_game_state(GameState::MarbleSelected);
  97. }
  98. void ColorLines::timer_event(Core::TimerEvent&)
  99. {
  100. switch (m_game_state) {
  101. case GameState::GeneratingMarbles:
  102. update();
  103. if (--m_marble_animation_frame < AnimationFrames::marble_generating_end) {
  104. m_marble_animation_frame = AnimationFrames::marble_default;
  105. set_game_state(GameState::CheckingMarbles);
  106. }
  107. break;
  108. case GameState::MarbleSelected:
  109. m_marble_animation_frame = (m_marble_animation_frame + 1) % AnimationFrames::number_of_marble_bounce_frames;
  110. update();
  111. break;
  112. case GameState::MarbleMoving:
  113. m_marble_animation_frame = (m_marble_animation_frame + 1) % AnimationFrames::number_of_marble_bounce_frames;
  114. update();
  115. if (m_marble_path.remaining_steps() != 1 && m_marble_animation_frame != AnimationFrames::marble_at_top)
  116. break;
  117. if (auto const point = m_marble_path.next_point(); m_marble_path.is_empty()) {
  118. auto const color = m_board->selected_marble().color();
  119. m_board->reset_selection();
  120. m_board->set_color_at(point, color);
  121. if (m_board->check_and_remove_marbles())
  122. set_game_state(GameState::MarblesRemoving);
  123. else
  124. set_game_state(GameState::GeneratingMarbles);
  125. }
  126. break;
  127. case GameState::MarblesRemoving:
  128. update();
  129. if (++m_marble_animation_frame > AnimationFrames::marble_removing_end) {
  130. m_marble_animation_frame = AnimationFrames::marble_default;
  131. m_score += 2 * m_board->removed_marbles().size();
  132. set_game_state(GameState::Idle);
  133. }
  134. break;
  135. case GameState::StartingGame:
  136. case GameState::Idle:
  137. case GameState::CheckingMarbles:
  138. break;
  139. case GameState::GameOver: {
  140. stop_timer();
  141. update();
  142. StringBuilder text;
  143. text.appendff("Your score is {}", m_score);
  144. if (m_score > m_high_score) {
  145. text.append("\nThis is a new high score!"sv);
  146. Config::write_i32(m_app_name, m_app_name, "HighScore"sv, int(m_high_score = m_score));
  147. }
  148. GUI::MessageBox::show(window(),
  149. text.string_view(),
  150. "Game Over"sv,
  151. GUI::MessageBox::Type::Information);
  152. reset();
  153. break;
  154. }
  155. default:
  156. VERIFY_NOT_REACHED();
  157. }
  158. }
  159. void ColorLines::paint_event(GUI::PaintEvent& event)
  160. {
  161. GUI::Frame::paint_event(event);
  162. GUI::Painter painter(*this);
  163. painter.add_clip_rect(frame_inner_rect());
  164. painter.add_clip_rect(event.rect());
  165. auto paint_cell = [&](GUI::Painter& painter, Gfx::IntRect rect, int color, int animation_frame) {
  166. painter.draw_rect(rect, Color::Black);
  167. rect.shrink(0, 1, 1, 0);
  168. painter.draw_line(rect.bottom_left(), rect.top_left(), Color::White);
  169. painter.draw_line(rect.top_left(), rect.top_right(), Color::White);
  170. painter.draw_line(rect.top_right(), rect.bottom_right(), Color::DarkGray);
  171. painter.draw_line(rect.bottom_right(), rect.bottom_left(), Color::DarkGray);
  172. rect.shrink(1, 1, 1, 1);
  173. painter.draw_line(rect.bottom_left(), rect.top_left(), Color::LightGray);
  174. painter.draw_line(rect.top_left(), rect.top_right(), Color::LightGray);
  175. painter.draw_line(rect.top_right(), rect.bottom_right(), Color::MidGray);
  176. painter.draw_line(rect.bottom_right(), rect.bottom_left(), Color::MidGray);
  177. rect.shrink(1, 1, 1, 1);
  178. painter.fill_rect(rect, tile_color);
  179. rect.shrink(1, 1, 1, 1);
  180. if (color >= 0 && color < Marble::number_of_colors) {
  181. auto const source_rect = Gfx::IntRect { animation_frame * marble_pixel_size, 0, marble_pixel_size, marble_pixel_size };
  182. painter.draw_scaled_bitmap(rect, *m_marble_bitmaps[color], source_rect,
  183. 1.0f, Gfx::Painter::ScalingMode::BilinearBlend);
  184. }
  185. };
  186. painter.set_font(*m_score_font);
  187. // Draw board header with score, high score
  188. auto board_header_size = frame_inner_rect().size();
  189. board_header_size.set_height(board_vertical_margin);
  190. auto const board_header_rect = Gfx::IntRect { frame_inner_rect().top_left(), board_header_size };
  191. painter.fill_rect(board_header_rect, Color::Black);
  192. auto const text_margin = 8;
  193. // Draw score
  194. auto const score_text = MUST(String::formatted("{:05}"sv, m_score));
  195. auto text_width = static_cast<int>(ceilf(m_score_font->width(score_text)));
  196. auto const score_text_rect = Gfx::IntRect {
  197. frame_inner_rect().top_left().translated(text_margin),
  198. Gfx::IntSize { text_width, font().pixel_size_rounded_up() }
  199. };
  200. painter.draw_text(score_text_rect, score_text, Gfx::TextAlignment::CenterLeft, text_color);
  201. // Draw high score
  202. auto const high_score_text = MUST(String::formatted("{:05}"sv, m_high_score));
  203. text_width = m_score_font->width(high_score_text);
  204. auto const high_score_text_rect = Gfx::IntRect {
  205. frame_inner_rect().top_right().translated(-(text_margin + text_width), text_margin),
  206. Gfx::IntSize { text_width, font().pixel_size_rounded_up() }
  207. };
  208. painter.draw_text(high_score_text_rect, high_score_text, Gfx::TextAlignment::CenterLeft, text_color);
  209. auto const cell_rect
  210. = Gfx::IntRect(frame_inner_rect().top_left(), board_cell_dimension)
  211. .translated(0, board_vertical_margin);
  212. // Draw all cells and the selected marble if it exists
  213. for (int y = 0; y < MarbleBoard::board_size.height(); ++y)
  214. for (int x = 0; x < MarbleBoard::board_size.width(); ++x) {
  215. auto const& destination_rect = cell_rect.translated(
  216. x * board_cell_dimension.width(),
  217. y * board_cell_dimension.height());
  218. auto const point = Point { x, y };
  219. auto const animation_frame = m_game_state == GameState::MarbleSelected && m_board->has_selected_marble()
  220. && m_board->selected_marble().position() == point
  221. ? m_marble_animation_frame
  222. : AnimationFrames::marble_default;
  223. paint_cell(painter, destination_rect, m_board->color_at(point), animation_frame);
  224. }
  225. // Draw preview marbles in the board
  226. for (auto const& marble : m_board->preview_marbles()) {
  227. auto const& point = marble.position();
  228. if (m_marble_path.contains(point) || !m_board->is_empty_cell_at(point))
  229. continue;
  230. auto const& destination_rect = cell_rect.translated(
  231. point.x() * board_cell_dimension.width(),
  232. point.y() * board_cell_dimension.height());
  233. auto get_animation_frame = [this]() -> int {
  234. switch (m_game_state) {
  235. case GameState::GameOver:
  236. return AnimationFrames::marble_default;
  237. case GameState::GeneratingMarbles:
  238. case GameState::CheckingMarbles:
  239. return m_marble_animation_frame;
  240. default:
  241. return AnimationFrames::marble_generating_start;
  242. }
  243. };
  244. paint_cell(painter, destination_rect, marble.color(), get_animation_frame());
  245. }
  246. // Draw preview marbles in the board header
  247. for (size_t i = 0; i < MarbleBoard::number_of_preview_marbles; ++i) {
  248. auto const& marble = m_board->preview_marbles()[i];
  249. auto const& destination_rect = cell_rect.translated(
  250. int(i + 3) * board_cell_dimension.width(),
  251. -board_vertical_margin)
  252. .shrunken(10, 10);
  253. paint_cell(painter, destination_rect, marble.color(), AnimationFrames::marble_preview);
  254. }
  255. // Draw moving marble
  256. if (!m_marble_path.is_empty()) {
  257. auto const point = m_marble_path.current_point();
  258. auto const& destination_rect = cell_rect.translated(
  259. point.x() * board_cell_dimension.width(),
  260. point.y() * board_cell_dimension.height());
  261. paint_cell(painter, destination_rect, m_board->selected_marble().color(), m_marble_animation_frame);
  262. }
  263. // Draw removing marble
  264. if (m_game_state == GameState::MarblesRemoving)
  265. for (auto const& marble : m_board->removed_marbles()) {
  266. auto const& point = marble.position();
  267. auto const& destination_rect = cell_rect.translated(
  268. point.x() * board_cell_dimension.width(),
  269. point.y() * board_cell_dimension.height());
  270. paint_cell(painter, destination_rect, marble.color(), m_marble_animation_frame);
  271. }
  272. // Draw marble move trace
  273. if (m_game_state == GameState::MarbleMoving && m_marble_path.remaining_steps() > 1) {
  274. auto const trace_size = Gfx::IntSize { m_trace_bitmaps.first()->width(), m_trace_bitmaps.first()->height() };
  275. auto const target_trace_size = Gfx::IntSize { 14, 14 };
  276. auto const source_rect = Gfx::FloatRect(Gfx::IntPoint {}, trace_size);
  277. for (size_t i = 0; i < m_marble_path.remaining_steps() - 1; ++i) {
  278. auto const& current_step = m_marble_path[i];
  279. auto const destination_rect = Gfx::IntRect(frame_inner_rect().top_left(), target_trace_size)
  280. .translated(
  281. current_step.x() * board_cell_dimension.width(),
  282. board_vertical_margin + current_step.y() * board_cell_dimension.height())
  283. .translated(
  284. (board_cell_dimension.width() - target_trace_size.width()) / 2,
  285. (board_cell_dimension.height() - target_trace_size.height()) / 2);
  286. auto get_direction_bitmap_index = [&]() -> size_t {
  287. auto const& previous_step = m_marble_path[i + 1];
  288. if (previous_step.x() > current_step.x())
  289. return 3;
  290. if (previous_step.x() < current_step.x())
  291. return 1;
  292. if (previous_step.y() > current_step.y())
  293. return 0;
  294. return 2;
  295. };
  296. painter.draw_scaled_bitmap(destination_rect, *m_trace_bitmaps[get_direction_bitmap_index()], source_rect,
  297. 1.0f, Gfx::Painter::ScalingMode::BilinearBlend);
  298. }
  299. }
  300. }
  301. void ColorLines::restart_timer(int milliseconds)
  302. {
  303. stop_timer();
  304. start_timer(milliseconds);
  305. }
  306. void ColorLines::set_game_state(GameState state)
  307. {
  308. m_game_state = state;
  309. switch (state) {
  310. case GameState::StartingGame:
  311. m_marble_path.reset();
  312. m_board->reset();
  313. m_score = 0;
  314. m_marble_animation_frame = AnimationFrames::marble_default;
  315. update();
  316. if (m_board->update_preview_marbles(false))
  317. set_game_state(GameState::GeneratingMarbles);
  318. else
  319. set_game_state(GameState::GameOver);
  320. break;
  321. case GameState::GeneratingMarbles:
  322. m_board->reset_selection();
  323. m_marble_animation_frame = AnimationFrames::marble_generating_start;
  324. update();
  325. if (m_board->ensure_all_preview_marbles_are_on_empty_cells())
  326. restart_timer(TimerIntervals::generating_marbles);
  327. else
  328. set_game_state(GameState::GameOver);
  329. break;
  330. case GameState::MarblesRemoving:
  331. m_marble_animation_frame = AnimationFrames::marble_removing_start;
  332. update();
  333. restart_timer(TimerIntervals::removing_marbles);
  334. break;
  335. case GameState::Idle:
  336. m_marble_animation_frame = AnimationFrames::marble_default;
  337. update();
  338. if (m_board->ensure_all_preview_marbles_are_on_empty_cells() && m_board->has_empty_cells())
  339. stop_timer();
  340. else
  341. set_game_state(GameState::GameOver);
  342. break;
  343. case GameState::MarbleSelected:
  344. restart_timer(TimerIntervals::selected_marble);
  345. m_marble_animation_frame = AnimationFrames::marble_default;
  346. update();
  347. break;
  348. case GameState::CheckingMarbles:
  349. m_marble_animation_frame = AnimationFrames::marble_default;
  350. update();
  351. if (!m_board->place_preview_marbles_on_board())
  352. set_game_state(GameState::GameOver);
  353. else if (m_board->check_and_remove_marbles())
  354. set_game_state(GameState::MarblesRemoving);
  355. else
  356. set_game_state(GameState::Idle);
  357. break;
  358. case GameState::MarbleMoving:
  359. restart_timer(TimerIntervals::moving_marble);
  360. m_board->clear_color_at(m_board->selected_marble().position());
  361. update();
  362. break;
  363. case GameState::GameOver:
  364. m_marble_animation_frame = AnimationFrames::marble_default;
  365. update();
  366. break;
  367. default:
  368. VERIFY_NOT_REACHED();
  369. }
  370. }